# 前言

文章其实早就写好了,但前段时间 run 到了甲方,巨忙,一直没空整理发出来。= =

PS:个人学习见解,有错的地方,大佬们尽管喷。

这里针对 Tomcat 服务器中的内存马进行了查杀。主要是因为 Springboot 项目通常是一个 jar 包启动的,这种情况下我们没办法类似 Tomcat 使用 JSP 来执行任意代码卸载内存马。

对于 jar 包的想要杀掉内存马其实可以通过打反序列化漏洞链子把修复代码打进去,但其实就是相当于漏洞的反向利用了,但这要求应急人员对内存马和 java 反序列化漏洞有较深的理解。

而这就不得不提一个致命的问题了:

①服务能重启吗?

-> 能 -> 直接重启解决。

- 不能 -> 服务很重要 -> 你敢打反序列链子进去么?打崩了怎么办?-> 不敢打,没法重启 ->GG

因此就目前看来 jar 包 Web 想要杀掉内存马最简单的办法就是重启了,如果不允许重启的话,就只能在 Waf 方面做手脚,把内存马的映射地址给拦截掉之类的。

因此出于上述问题,其实针对内存马的查杀更多的应用场景还是 Tomcat 服务器,jar 包的通常重启完事了,不能重启的想必也不敢打链子进去。

因此这里我们是针对 Tomcat 下的内存马做查杀。

# 工具介绍

# arthas(仅排查)

arthas 主要就是通过人工分析内存,查看内存中的类,分析其代码逻辑,来确定是不是真正的内存马。

https://arthas.aliyun.com/doc

# 规则类工具自动化提取排查 Copagent (仅排查,只适用于 jdk1.8)

基于 Alibaba Arthas 编写的 copagent 项目,分析 JVM 中所有的 Class,根据危险注解和类名等信息 dump 可疑的组件,结合人工反编译后进行分析。

copagent 使用 javaagent 技术获得了全部的类,判断其包名、实现类名、接口名、注解来提取关键类,并根据类是否在磁盘上有资源链接对象、类中是否包含恶意行为关键字来判断其是否为内存马。

https://github.com/LandGrey/copagent

# java-memshell-scanner(可查可杀,推荐)

(jsp 文件,不能用在 jar 包服务)

通过名称、对应的 Class 是否存在来判断是否是内存马
优点在于一个简单的 JSP 文件即可结合人工审查(类名和 ClassLoader 等信息)对内存马进行查杀,也可以对有风险的 Class 进行 dump 后反编译分析。

c0ny 大佬的是三年前的祖传代码,只能查杀 servlet-api 类型的,因此我这边在 c0ny 大佬的基础上更新了这版:

新增 tomcat-vlaue、websocket、timer、ugrade、executorshell 内存马的查杀逻辑,并给上一些 tips,帮助不太懂内存马的同学更好的进行分析。

GitHub - ruyueattention/java-memshell-scanner: 通过 jsp 脚本扫描并查杀 Tomcat 内存马,当前支持 Servlet-api、Tomcat-Value、Timer、Websocket 、Upgrade 、ExecutorShell 内存马的查杀逻辑。

# 工具 attach 不上目标 java 进程的问题

如果我们的工具 attach 不上目标 java 进程的话,可能原因有:

①权限不对:切换到和目标 java 进程相同权限运行工具。

②java 版本不对:使用和目标 java 进程相同的 java 版本运行工具。

③/tmp/.java_pid {pid} 文件被删了。(这种情况下,只能重启 java 服务)

PS:在冰蝎中有一个防检测功能后,勾选上后并且为 linux 系统的话,会删除掉 java socket 文件,导致后续其他的 jar 包都无法注入。也就是说我们的 arthas 也无法注入,自然也就无法调试了。

image-20230309153331933

image-20230309153341662

这种时候我们只能重启 java 服务,或者使用 gdb 导出 进程快照 (coredump) 再转成 Java heap dump 来简单分析。

Java 无法 attach 到目标进程,使用 core dump 转换为 Java heap dump - 见识 动脑 品质 - 生活的美好 (tianxiaohui.com)

# 查杀思路

首先,我们需要分析常见的内存马存在的一些可疑的特征。通常情况下更多的是使用以下特征去判断:

1. 继承可能实现 Webshell 接口,例如 Servlet,Filter,Listener,Interceptor

• javax.servlet.http.HttpServlet

•org.springframework.web.servlet.handler.AbstractHandlerMapping

• javax.servlet.Filter

• javax.servlet.Servlet

• javax.servlet.ServletRequestListener

•…

2. 名字:内存马名可能包含 shell 等关键字,并且常见的包名如下

• net.rebeyond.

• com.metasploit.

3. 高危 classloader 加载:查看 classloader 是不是 Templates 或 bcel 等

一般来说,正常的 Servlet-api 等组件都是由中间件的 WebappClassLoader 加载的。

而反序列化漏洞喜欢利用 TemplatesImpl 和 bcel 执行任意代码。所以这些 class 往往就是以下这两个:

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader

com.sun.org.apache.bcel.internal.util.ClassLoader

而由 JSP 加载的内存马对应的 ClassLoader 则为 ClassLoader 为 org.apache.jasper.servlet.JasperLoader

4. 对应的 ClassLoader 路径下没有 class 文件。

# 查杀例子

在 tomcat 中的内存马主要有以下类型:

Servlet-api、java agent、 tomcate value、timer、webscket、Executor、Upgrade

基本上我们使用 java-memshell-scanner 就能完成查杀内存马的操作了。但如果有特殊需求,可以搭配 arthars 去使用。

PS:冰蝎的内存马策略是 javaagent,而哥斯拉则是 Servlet-API 类型

# Servlet-api 类型(代表:哥斯拉)

PS:也可以用 cop.jar,直接跑就行,最终会输出结果和对应的 class 文件,然后根据危险等级,结合人工分析去确定是否为内存马。

这种类型的内存马最多,实现起来也是最简单的。其中的典型就是哥斯拉和我们平时一些利用工具,他们都是这种类型的内存马。

这里我们使用 java-memshell-scanner 去扫描,可以发现,扫到了很多没有 class 文件的内存马。(图中展示的是哥斯拉注入的内存马)

主要特征:可以看到,这种由工具生成的内存马它们的特征还是很明显的。classloader 都是 org.apache.coyote.xxx ,并且磁盘上没有对应的 class 文件,就能够很好的和正常的 Servlet 进行区分。

image-20230309153744507

显然使用工具注入的内存马有着不可避免的一些特征,但如果攻击者通过其他手段来注入内存马呢?

比如下面:攻击者通过上传 JSP 文件来注入内存马。可以看到,同是 Servlet-api 类型的内存马,它和哥斯拉有个区别就是磁盘中存在 class 文件。

image-20230309154626722

这种情况下,就不太好区分了,我们主要还是去看它的 classload,可以看到,这里我们通过 JSP 来注入内存马,它的 classload 是 org.apache.jasper.servlet.JasperLoader,而正常情况下的 Servlet-api 是由中间件的 WebappClassLoader 来加载的。

因此 classload 不是 WebappClassLoader 的 Servlet-api 就很有可能是内存马,但想要盖棺定论最好还是把内存 dump 下来,手动分析。

具体分析代码可以尝试 dump 他的内存下来分析。

如果 dump 不下来的话,可以使用 arthars 来搞。

查看 JVM 已加载的类信息,筛选 Servlet 类,其中我们内存马如下:

sc *.Servlet

image-20230309153842352

使用 jad 命令反编译这个可疑类

jad org.apache.coyote.ObjectReader

我们把它 copy 出来分析,其实就是哥斯拉的马子,到这里我们就能够确认这个是内存马了。

PS: 代码太长的话也可以把 class dump 出来本地用 IDEA 反编译分析

dump -d /home/momo/Desktop/MemSHell/copagent org.apache.coyote.ObjectReader

image-20230309153909622

image-20230309153916976

到这里已经可以确定是内存马了,接着我们还可以看看是谁把它加载到了 JVM。

sc -d org.apache.coyote.ObjectReader

其中 classLoad 就是加载它的类:org.apache.jsp.godzilla_jsp

image-20230309154215759

继续追踪,找到代码路径,到 web 目录把 godzilla.jsp 给删了。

sc -d org.apache.jsp.godzilla_jsp

image-20230309154240825

# agent 类型内存马查杀(代表:冰蝎)

原理:利用 instrument 机制,在不增加新类和新方法的情况下,对现有类的执行逻辑进行修改实现的内存马。

这类内存马实现方式比较多,检测比较困难,copagent 和 memershell 都无法监测冰蝎 4 注入的内存马。

因此这里我们用 artars 去分析,主要关注的其实是 agent 内存马常改的一些类

javax.servlet.http.HttpServlet

org.apache.tomcat.websocket.server.WsFilter

org.apache.catalina.core.ApplicationFilterChain

其中冰蝎内存马通过修改 javax.servlet.http.HttpServlet#service 方法,添加自己的处理逻辑,也就是内存马。

jad javax.servlet.http.HttpServlet

可以看到这个类被添加上了一些冰蝎服务端的一些逻辑,妥妥的内存马。

image-20230309154912176

当然如果 arthars attach 不上 java 进程,我们也可也导出 coredump 内存来看。(有这闲心,不如重启)

image-20230309155002599

PS:zhouyu 内存马的策略

image-20230309155102021

杀掉 agent 马的话,我们其实就是修改还原这个被篡改的类的字节码。需要编写一个 javaagent,然后注入到目标中间件即可。

image-20230309155247945

image-20230309155120996

此时再次连接内存马失败,并且后续的内存马都注入不上去。

image-20230309155155735

# Tomcat-Value 内存马

原理:Tomcat-Value 通常是一个 Java 对象,其中包含用户的会话信息,例如用户名、密码、购物车、浏览历史记录等。

而 Tomcat-Value 内存马就是针对 Tomcat-Value 的内存马。攻击者可以通过反序列化或者代码执行在 Tomcat 容器中注入恶意的会话数据,使得 Java Web 应用程序在读取和处理会话数据时,执行恶意代码,从而实现攻击目的。

这里根据 Tomcat-Value 的实现,简单的写了个 jsp 来获取当前的 Value。

image-20230309155311895

image-20230309155315752

删除其实就是调用 Pipeline 对象的 removeValve 方法即可,和注入是一个原理。

image-20230309155328068

# Timer 内存马

原理:对于 Java 环境下的 Timer 内存马,一般是通过恶意代码注入到 Timer 定时任务的回调函数中,从而在回调函数执行时触发恶意代码的执行,进而实现对服务器的攻击。一般是新建一个 Timer 定时任务,其中回调函数逻辑为获取当前 Tomcat 线程,从线程中找到 http 请求对象,获取其中的命令并执行。因此通常需要 BP 大量发包来触发。

这类内存马查找的话主要是找内存中的 java.util.Timer 类。

然后看到可疑的加载类就 jad 反编译,查看源码分析是否恶意。

image-20230309155350392

随便跟进一个 Timer,可以看到,我们跟进的这个 Timer 执行了 list.get (1) 这个参数。而 list 又是由 TimerShell_jsp 这个类的 getRequest 方法得来的。

image-20230309155358601

接着跟进这个 TimerShell_jsp 类

image-20230309155405735

看下源码就可以知道这其实就是通过 Tomcat 线程,读取 HTTP 头的数据,传给前面的 Timer 执行 getRuntime 了。

image-20230309155414733

查杀的话,这种内存马是创建了 Timer 定时任务来做的。

所以我的想法就是找到存储了 Timer 定时任务的一个对象,想办法把内存马从这个对象中卸载出去。

这里跟了下流程找到相关逻辑,其实就是把这个 TimerTask 给取消掉就行了

大致思路就是遍历当前线程,找到 TimerThread 的类,然后通过反射去拿到目标对象,最后调用 TimerTask 类的 cancle 方法来取消这个 TimerTask。

image-20230309155456506

同样的,在我写的 java-memshell-scanner 中已经集成。

image-20230309155514194

# Websocket 内存马查杀

原理:创建一个 Websocket 服务端,服务段逻辑为 webshell 逻辑。其实和 Servlet 差不多的。

其实和打入是一样的,找到 wsServerContainer 获取里面的 websocket 服务端,调用 remove 方法即可。

image-20230309155546055

# Tomcat 中实现一个 Websocket 服务端

tomcat 中存在两种方式:一、ServerEndpoint 注解方式。二、继承抽象类 Endpoint 方式。
这两种方式就好比 Servlet 的注解创建方式和继承创建方式一样。
显然的通过注解,就不需要更多配置就能实现一个简单的 Websocket。

# 注解形式(建议)

Tomcat 在启动时会默认通过 WsSci 内的 ServletContainerInitializer 初始化 Listener 和 servlet。
然后再扫描 classpath 下带有 @ServerEndpoint 注解的类进行 addEndpoint 加入 websocket 服务。

image-20230424125217874

image-20230424125231746

# 继承抽象类 Endpoint 方式

继承抽象类 Endpoint 方式比加注解 @ServerEndpoint 方式更麻烦,主要是需要自己实现 MessageHandler 和 ServerApplicationConfig。@ServerEndpoint 的话都是使用默认的,原理上差不多,只是注解更自动化,更简洁。

image-20230424125319346

# 内存马注入

实现思路类比其他类型内存马,这里以 JSP 注入为例。

  • 获取 ServletContext
  • 获取 WebSocketContainer
  • 创建恶意的 ServerEndpointConfig
  • 调用 addEndpoint ()

首先定义一个 Websocket 服务段。这里我用注解形式来创建。

image-20230424125421163

然后获取 ServletContext 并把 websocket 服务端绑定到指定 url 上。

image-20230424125521762

接着访问 JSP,执行注入。此时,成功注入 Websocket 内存马。

image-20230424125541169

image-20230424125605601

# 查杀 Websocket 内存马

其实和打入是一样的,找到 wsServerContainer 获取里面的 websocket 服务段,调用 remove 方法即可。

image-20230424125709700

# Upgrade 内存马

这类内存马也是完全可以参考其实现,简单的实现查杀。

简单的通过反射去获取 httpUpgradeProtocols,然后从中 remove 掉内存马即可。

image-20230309155602179

image-20230309155606986

还可以把类 dump 下来分析源码确定是否为恶意的。

image-20230309155619640

# Executor 内存马

Executor 内存马的大致原理是:Tomcat 中存在一个对象存储了线程池,这个线程池在请求响应周期中起到作用。因此我们如果自己定义一个恶意的线程池,其中 execute 执行 webshell 逻辑,并把它注册到这个对象,那就是一个内存马了。

# 实现

绕过检测之 Executor 内存马浅析 (内存马系列篇五) - FreeBuf 网络安全行业门户

但上面的代码有点问题:主要就是获取 requests 数据和获取 NioEndpoint 的问题。我改成如下代码:

image-20230309160834750

另外的问题就是 request 数据的获取了:nioChannels 里的数据时而有值时而没值,也就是不太稳定,需要我们多次发包。

image-20230309160051137

如图,当前 http 是存在 cmd:whoami 的,但通过 nioChannels 却获取不到。

image-20230309160058093

文章 Java Tomcat Executor/Processor 内存马 - Twings (aluvion.github.io) 发现 execute 方法中的 command 这个传参中 socketWrapper->socket->appReadBufHandler 对象为 Http11InputBuffer 类,而这个类就恰好是 request 数据的对象,因此通过这个类,我们就能稳定的获取到传参。

因此我们完全可以用这个对象来获取传参,最终完成效果如下:

image-20230309160142619

正当我以为大功告成之际,很快,我就发现另一个致命的问题了 —— 当前获取到的 requests 对象是上一个请求(或者前几次,跟线程数有关)的缓存数据,这问题就大了,这就意味着我们的响应数据可能会返回到别人的 http 会话中。所以还得重新去找存储了 request 数据的对象。

image-20230309160252619

通过文章 Executor 内存马的实现(二) - 先知社区 (aliyun.com) 我找到了解决办法。但跟文章里说的点去找,发现是找不到 attachment 对象的,最终我是在 selector->channelArray 中找到了。

image-20230309160321202

随后就是跟着作者的思路复原即可,最终实现如下:

image-20230309160332009

# 查杀

查杀起来也简单,只要看 Executor 是不是 tomcat 的类,然后把这个对象中存储的线程池还原成原来的就行。

image-20230309155856315

老样子。

image-20230309155930731