拖了很久的 shiro 分析
漏洞概述
Apache Shiro <= 1.2.4 版本中, 加密的用户信息序列化后存储在 Cookie 的 rememberMe 字段中, 攻击者可以使用 Shiro 的 AES 加密算法的默认密钥来构造恶意的 Cookie rememberMe 值, 发送到 Shiro 服务端之后会先后进行 Base64 解码, AES 解密, readObject() 反序列化, 从而触发 Java 原生反序列化漏洞, 进而实现 RCE.
该漏洞的根源在于硬编码 Key.
漏洞复现
使用 shiroattack 工具
Dnslog 接收到请求
执行命令
漏洞分析
远程调试
用 idea 连 vulhub 的 docker 环境来进行调试
首先进入容器
docker exec -it 34db756dfcfc /bin/bash
可以看到是用 jar 包起的环境, 把文件拷贝出来 (也可以
docker-compose up -d
之后使用
docker ps --no-trunc
来查看容器默认的启动命令)
docker cp 34db756dfcfc:/shirodemo-1.0-SNAPSHOT.jar ~/
然后把 jar 包解压了之后用 idea 打开
libraries 里导入
在 module 中添加 BOOT_INF 这个目录
另外还需要改一下 dockerfile
idea 远程调试 docker
需要增加一组端口供调试用, 这里我们用 idea 默认的 5005
vulhub 的 shiro 环境是 java -jar xxx.jar 的形式运行的, 那么添加对 jar 程序启动的调试命令即可, 在启动 docker 时用自定义的 COMMAND 替换默认的 COMMAND
- version: '2'
- services:
- web:
- image: vulhub/shiro:1.2.4
- ports:
- - "8080:8080"
- - "5005:5005"
- command: java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar /shirodemo-1.0-SNAPSHOT.jar
然后配置好 remote
下好断点, 输入登录账户密码测试, 看到以下界面说明成功了
原理分析
从官方的 https://issues.apache.org/jira/browse/SHIRO-550 上来看, 存在几个重要的点:
rememberMe cookie
CookieRememberMeManager.java
Base64
AES
加密密钥硬编码
Java serialization
首先正常登录
返回的 cookie 值中的 rememberme 值如下:
o5NA+QAjgJpe6uKkIJ1li/WLqOAR2KfLIY3BzwfAUFbbkBSEfs3/259i4Qc2jq6lxUpLabYK2c0oR4faB3l8m0GDhzMvVYjFOR2TPKRtQmqlUAdaiJT06biAC56EMu6UbBJAujAgP1msHuJYkV1fDuhdLN5wGK+IlEpr1Xf+aa6oNkPqBkRG7+B/3bXQNTqmLFlarZUWxB6TwZyshplhx0ckyIfc0qJuf/f5Tt60gK/D28JUI93Gp3vGi/P7UUiwv2Qyzpz4hXZSUocGlC73qE+62ZvQ1ryzVRjpfTkG4Hat6sst04wbpFwdUSJxh6t4FLJ2i9bs5eIm/1UJpVHP9Eia5WaAShQa45qr5yIMA+q1rYxtz0WufvOi67fpqY3qi8LQ/ZnGwXUY+o6dLu2qmqHwTXbRTRKP4G5d3e5SA9FNvXUhYWRhcwo2zJ2lS2JK/D6S0u3HAak04+3wpbZm0UCJxafXlaFPUDhmiXBtIULcEELqBOaqLr1n7LV+F1Cl7kYNU6GozLVPvNlqW5UtLA==
跟一下登录生成 cookie 的过程
生成 cookie
shiro 会提供 rememberme 功能, 可以通过 cookie 记录登录用户, 从而记录登录用户的身份认证信息, 即下次无需登录即可访问. 而其中对 rememberme 的 cookie 做了加密处理, 漏洞主要原因是加密的 AES 密钥是硬编码在文件中的, 那么对于 AES 加密算法我们已知密钥, 并且 IV 为 cookie 进行 base64 解码后的前 16 个字节, 因此我们可以构造任意的可控序列化 payload.
处理 rememberme 的 cookie 的类为
org.apache.shiro.Web.mgt.CookieRememberMeManager
它继承自
org.apache.shiro.mgt.AbstractRememberMeManager
, 其中在
AbstractRememberMeManager
中定义了加密 cookie 所需要使用的密钥, 当我们成功登录时, 如果勾选了 rememberme 选项, 那么此时将进入 onSuccessfulLogin 方法
之后进入 serialize, 对登录认证信息进行序列化
然后进行加密
org.apache.shiro.mgt.AbstractRememberMeManager 中的 encrypt 方法如下
- protected byte[] encrypt(byte[] serialized) {
- byte[] value = serialized;
- CipherService cipherService = getCipherService();
- if (cipherService != null) {
- ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
- value = byteSource.getBytes();
- }
- return value;
- }
- ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
调用的即为 AES 算法
可以看到使用 CBC 模式的 AES 加密算法, 其中 Padding 规则是 PKCS5.
具体实现在 org/apache/shiro/crypto/JcaCipherService.java 中
- private ByteSource encrypt(byte[] plaintext, byte[] key, byte[] iv, boolean prependIv) throws CryptoException {
- final int MODE = javax.crypto.Cipher.ENCRYPT_MODE;
- byte[] output;
- if (prependIv && iv != null && iv.length> 0) {
- byte[] encrypted = crypt(plaintext, key, iv, MODE);
- output = new byte[iv.length + encrypted.length];
- //now copy the iv bytes + encrypted bytes into one output array:
- // iv bytes:
- System.arraycopy(iv, 0, output, 0, iv.length);
- // + encrypted bytes:
- System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
- } else {
- output = crypt(plaintext, key, iv, MODE);
- }
- if (log.isTraceEnabled()) {
- log.trace("Incoming plaintext of size" + (plaintext != null ? plaintext.length : 0) + ". Ciphertext" +
- "byte array is size" + (output != null ? output.length : 0));
- }
- return ByteSource.Util.bytes(output);
- }
IV(初始化向量) 是随机生成的, 将 IV 放在 crtpt() 加密的数据之前然后返回
加密结束后, 在
org/apache/shiro/Web/mgt/CookieRememberMeManager.java 的 rememberSerializedIdentity
方法中进行 base64 编码, 并通过 response 返回
这里的 byte 是前 16 位随机的 IV+AES 密文, 然后经过 base64 编码
解析 cookie
org/apache/shiro/Web/mgt/CookieRememberMeManager.java 中会将传递的 base64 字符串进行解码后放到字节数组中, 因为 java 的序列化字符串即为字节数组
byte[] decoded = Base64.decode(base64);
然后进入解密流程
先解密后进行反序列化
AES 是对称加密, 加解密密钥都是相同的, 并且 shiro 都是将密钥硬编码
- public void setCipherKey(byte[] cipherKey) {
- //Since this method should only be used in symmetric ciphers
- //(where the enc and dec keys are the same), set it on both:
- setEncryptionCipherKey(cipherKey);
- setDecryptionCipherKey(cipherKey);
- }
- public AbstractRememberMeManager() {
- this.serializer = new DefaultSerializer<PrincipalCollection>();
- this.cipherService = new AesCipherService();
- setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
- }
在
org/apache/shiro/crypto/JcaCipherService.java
的 decrypt() 方法中进行解密从 cookie 中取出 iv 与加密的序列化数据
调用 crypt 方法利用密文, key,iv 进行解密
解密完成后进入反序列化, 看上面的 public AbstractRememberMeManager() 这里用的是默认反序列化类
- public T deserialize(byte[] serialized) throws SerializationException {
- if (serialized == null) {
- String msg = "argument cannot be null.";
- throw new IllegalArgumentException(msg);
- }
- ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
- BufferedInputStream bis = new BufferedInputStream(bais);
- try {
- ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
- @SuppressWarnings({"unchecked"})
- T deserialized = (T) ois.readObject();
- ois.close();
- return deserialized;
- } catch (Exception e) {
- String msg = "Unable to deserialze argument byte array.";
- throw new SerializationException(msg, e);
- }
- }
readobject() 触发反序列化
至此, Shiro 对 Cookie 的 rememberMe 的处理流程已整体调试分析结束.
漏洞修复
Apache Shiro 1.2.5 版本的源码, 修复方法就是将使用默认 Key 加密改为生成随机的 Key 加密:
参考
https://ares-x.com/2020/04/20/IDEA 远程调试 Docker 中程序的方法 /
- https://paper.seebug.org/shiro-rememberme-1-2-4/
- https://www.mi1k7ea.com/2020/10/03 / 浅析 Shiro-rememberMe 反序列化漏洞 (Shiro550)/
来源: http://www.bubuko.com/infodetail-3716285.html