浅析Shiro反序列化Payload长度绕过
起因
之前在研究学习Shiro反序列化漏洞的利用时,发现当注入内存马/回显执行命令时,生成的部分Payload会因为超过Tomcat Header长度限制,而无法成功利用。同时对网上常用的长度绕过方法进行尝试,可能由于一些未知原因,没有绕过成功。于是本文打算通过对网上已有方法进行分析和利用尝试,从而达到成功绕过的目的。
1. Tomcat Header长度限制
在Tomcat中怎样实现对Header长度的限制?
在Tomcat中对Header长度的限制,是通过配置maxHttpHeaderSize来实现的,默认配置是8192字节,即8KB。下面是不同Tomcat版本的maxHttpHeaderSize:
1 | //Tomcat 8.0.47等 |
maxHttpHeaderSize分析
org.apache.coyote.http11.AbstractHttp11Protocol
类的maxHttpHeaderSize属性,可以对Tomcat中的Header长度进行限制。
在这个类中有该属性的get和set方法:
接下来寻找哪个地方调用了其get方法,发现在org.apache.coyote.http11.Http11Processor
类的构造方法中进行了调用:
在上图看到,maxHeaderSize属性的值会影响inputBuffer和outputBuffer两个属性的值,而inputBuffer和outputBuffer两个属性分别是Http11InputBuffer和Http11OutputBuffer的类对象。
于是接下来看Http11InputBuffer类和Http11OutputBuffer类,发现在Http11OutputBuffer类中maxHeaderSize属性的值在构造方法中会作为headerBufferSize被传入到ByteBuffer.allocate
方法,并将方法返回结果赋值到headerBuffer属性(ByteBuffer的类对象):
跟进ByteBuffer.allocate
方法,会return new HeapByteBuffer(capacity, capacity);
,即创建一个新的Header Buffer:
所以,在进行HTTP请求时,将会根据maxHttpHeaderSize属性的值,创建一个新的Header Buffer空间,大小为maxHttpHeaderSize属性的值,即8KB。Header的输出和输入都不可以超出这个大小限制,如果超出这个限制,就会抛出异常。
绕过Header长度限制的思路
根据Header长度限制的原理,很容易想到绕过Header长度限制的根本思路:
增加Tomcat允许HTTP Header最大值
配置参数maxHttpHeaderSize可以设置Tomcat允许的HTTP Header最大值。
减少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 |
|
运行,从Request对象实例中,可以看到Request对象的类全名为org.apache.catalina.connector.RequestFacade
:
于是接下来在RequestFacade类的构造方法中打上断点,进行调试分析:
1.首先跟进到RequestFacade对象创建的地方,即getRequest()方法
该方法有两个 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
14public 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 | public final void invoke(Request request, Response response) throws IOException, ServletException { |
3.接着顺着调用栈寻找Request对象最开始是从哪个方法开始作为入参传递的
找到org.apache.coyote.http11.Http11Processor#service
是Request 对象最开始作为入参传递的地方。
4.那么这个Request 对象又是如何产生的呢?
在Http11Processor的父类AbstractProcessor类中的request属性前打上断点,重新运行调试:
发现会停在AbstractProcessor 类的构造方法,这里就是request最开始产生的地方
5.那么这个coyoteRequest又是怎么来的呢?
顺着调用栈寻找,发现是new出来的
继续顺着调用栈,发现会createProcessor:
在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里面去,等下一次请求使用:
7.那么当进行HTTP请求时,会先看recycledProcessors这个栈结构里面有没有可用的processor:
若没有,则processor对象为null,调用createProcessor方法创建一个新的processor对象,在请求结束之后,将其放入到栈结构里面。
在调用createProcessor方法时,会new一个新的Request对象coyoteRequest,最终这个Request对象会封装为RequestFacade对象。
8.在org.apache.catalina.connector.Request的构造方法中打上断点,调试,顺着调用栈往前找,发现:
在org.apache.catalina.connector.CoyoteAdapter#service
方法中,有一行代码:
1 | request.setCoyoteRequest(req); |
同时,根据**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,说明超过了长度限制
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中多线程同时发送
2.使用Repeater模块重放原先超过长度限制的注入内存马/回显执行命令
等请求的Payload生成的rememberMe(这里以注入Filter内存马为例,由于比较常见不再贴出代码,生成rememberMe的过程和上面生成修改maxHttpHeaderSize的rememberMe
步骤相同):
响应不为400,说明绕过成功了。
3.测试注入的内存马:
发现能成功执行命令。
至此,完成了通过修改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方法一般有两种方式:
- 自定义ClassLoader
- 反射调用defineClass
生成rememberMe
所以,我们可以将调用ClassLoader#defineClass
方法加载HTTP请求体中类字节码的类,生成类字节码,使用TemplatesImpl加载并装配到Gadget chain,最终生成rememberMe的值:
需要注意的是:因为同一个ClassLoader不能重复加载同一个类,所以每次请求都要生成一个新的ClassLoader。
1 | public class MyLoader extends AbstractTranslet { |
生成Payload
这里同样以超过长度限制的注入Filter内存马为例,由于比较常见不再贴出代码。
将构造的恶意类的类字节码进行Base64编码,生成Payload。
触发Payload
当rememberMe值和Payload一起发包后,rememberMe值Base64解码->AES解密->反序列化->Gadget chain
,Gadget chain会加载MyLoader类,在MyLoader类里会将HTTP请求体中发送的Payload进行Base64解码得到恶意类的类字节码,通过ClassLoader#defineClass
方法加载触发。
经过测试,成功触发,注入内存马成功:
这里有个疑问:为什么通过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 | //设置Thead name为Qwzf |
生成并缩短Payload
生成在Thread name上设置的原先超过长度限制的注入内存马/回显执行命令
等请求的Payload(这里也同样以超过长度限制的注入Filter内存马为例,由于比较常见不再贴出代码)。
缩短Payload可参考https://xz.aliyun.com/t/6227进行缩短,不再贴出代码。
分散发包
将Payload分成多段加入到Thread name。即,每次将Payload的一部分追加到Thread name,生成rememberMe,在Cookie中发送。
1 | public class ThreadName extends AbstractTranslet { |
触发Payload
将在Thread name上设置的注入内存马的Payload进行Base64解码得到恶意类的类字节码,通过defineClass方法进行加载触发。
1 | public class Loader extends AbstractTranslet{ |
将触发代码,生成rememberMe,在Cookie中发送,最终注入成功,效果如下:
至此,完成了通过缩短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 | public class DefineClass extends AbstractTranslet { |
实例化并注册恶意filter
写一个正常的Filter内存马,将正常获取注册恶意的filter,改为从classes字段中获取注册恶意的filter(不再贴完整代码)。
即,将正常Filter内存马中的下面代码:
1 | FilterShell filterShell = new FilterShell(); |
改为:
1 | //恶意Filter类的类全名 |
传参执行命令,发现内存马注入成功,效果如下:
产生一个思考:除了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