前言 前置知识(?)。
RememberMe 先把shiro依赖版本改成1.2.4。
漏洞发生在shiro的rememberMe功能,因为这项功能只能允许访问user权限的路径,所以先改改配置文件中的权限配置:
1 2 map.put("/logout" , "user" ); map.put("/user" , "user" );
再改改控制器代码,开启这项功能:
1 2 3 UsernamePasswordToken token = new UsernamePasswordToken ("Twings" , "123456" ); token.setRememberMe(true ); subject.login(token);
之后shiro会将身份信息加密,base64编码保存在cookie中,用于下一次身份认证:
RememberMe解密 开始找相关的代码,直接搜索名字相关的类可以找到CookieRememberMeManager类及其父类AbstractRememberMeManager,但是换了几个点,断了几次都没有断下来,后来发现浏览器重启后第一次访问才会触发断点,原因不明,可能跟session有关,后面再来研究。最后的断点打在了getRememberedPrincipals函数,此时的调用栈如下:
getRememberedPrincipals函数如下:
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; }
这段代码以前似乎还出现过代号为SHIRO-138的问题,这里通过getRememberedSerializedIdentity函数获取了base64解码后的cookie中保存的加密数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 protected byte [] getRememberedSerializedIdentity(SubjectContext subjectContext) { ... String base64 = getCookie().readValue(request, response); if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null ; if (base64 != null ) { base64 = ensurePadding(base64); ... byte [] decoded = Base64.decode(base64); ... return decoded; } ... }
然后调用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 10 11 12 13 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; }public byte [] getDecryptionCipherKey() { return decryptionCipherKey; }
解密的key来自该对象中的decryptionCipherKey属性,通过调试可以发现该属性通过setter赋值,而在不特别设置的情况下,该属性在实例化时赋值:
1 2 3 4 5 6 7 private static final byte [] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==" );public AbstractRememberMeManager () { this .serializer = new DefaultSerializer <PrincipalCollection>(); this .cipherService = new AesCipherService (); setCipherKey(DEFAULT_CIPHER_KEY_BYTES); }
是一个固定值,而cipherService是一个JcaCipherService对象,其在解密时会从密文中读取iv:
1 System.arraycopy(ciphertext, 0 , iv, 0 , ivByteSize);
第二部分的deserialize函数负责反序列化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public T deserialize (byte [] serialized) throws SerializationException { ... 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); } }
一个没有做安全检验的反序列化点,如果依赖中存在可利用的反序列化链就会导致一个反序列化漏洞。
后续 尝试复现漏洞时发现出现这么一个错误:
1 org.apache .shiro .io .SerializationException : Unable to deserialze argument byte array.
看起来是不能处理数组类型的对象?不过读读代码就会发现反序列化中只要捕捉到Exception就会抛出这种错误,晚点再看看具体原因。
commons-beanutils shiro自带的commons-beanutils依赖是1.8.3版本的:
1.9.3版本下打得通的利用链在这个版本生成时会出现一点问题:
问题发生在类的初始化函数中:
1 2 3 public BeanComparator ( String property ) { this ( property, ComparableComparator.getInstance() ); }
简单来说就是BeanComparator对象的实例化依赖于commons-collections中的一个类org.apache.commons.collections.comparators.ComparableComparator,而由于依赖中不存在commons-collections就导致了这个问题。按照参考文章的说法就是:
所以如果想在这个版本下实现RCE,就要通过反射的方式实例化BeanComparator类再给它的属性赋值,Comparator找不找倒是无所谓:
1 2 BeanComparator beanComparator = (BeanComparator)Utils.createWithoutConstructor("org.apache.commons.beanutils.BeanComparator" ); Utils.setField(beanComparator, "property" , "outputProperties" );
要注意的一点是,最好用byte[]类型来存放Java序列化数据,后面通过base64编码输出,不然转换为String类型时可能会有编码问题,序列化数据头会被弄成两个问号,一开始没注意踩了一会的坑,生成序列化数据后用python写个脚本发送就行了。
commons-collections CommonsCollections的利用链可以直接打通,看参考文章似乎在tomcat+shiro环境下,会因为数组形式存在加载问题而无法利用。
JRMP 本地Java版本为8u281,这个版本已经有了对JRMP利用的限制,反序列化时会触发安全检查:
1 2021 -10 -20 21 :49 :54.771 INFO 42580 --- [127.0 .0 .1 :1099 ]] java.io.serialization : ObjectInputFilter REJECTED: class javax .management.BadAttributeValueExpException, array length: -1 , nRefs: 2 , depth: 1 , bytes: 110 , ex: n/a
ObjectInputStream类在反序列化JRMPClient传输的数据时,会调用filterCheck函数:
其检验代码如下:
1 2 3 4 try { status = serialFilter.checkInput(new FilterValues (clazz, arrayLength, totalObjectRefs, depth, bytesRead)); }
serialFilter是一个DGCImpl_Stub对象,其检验代码如下:
1 2 3 4 5 if (var1.isPrimitive()) { return Status.ALLOWED; } else { return var1 != UID.class && var1 != VMID.class && var1 != Lease.class && (var1.getPackage() == null || !Throwable.class.isAssignableFrom(var1) || !"java.lang" .equals(var1.getPackage().getName()) && !"java.rmi" .equals(var1.getPackage().getName())) && var1 != StackTraceElement.class && var1 != ArrayList.class && var1 != Object.class && !var1.getName().equals("java.util.Collections$UnmodifiableList" ) && !var1.getName().equals("java.util.Collections$UnmodifiableCollection" ) && !var1.getName().equals("java.util.Collections$UnmodifiableRandomAccessList" ) && !var1.getName().equals("java.util.Collections$EmptyList" ) ? Status.REJECTED : Status.ALLOWED; }
对要反序列化的类做了白名单检测,按照文章 所说,应该是8u231版本做的安全检验,然后因为出现了绕过所以8u241做了进一步的修复。
tomcat+shiro 留坑。
参考 https://www.freebuf.com/vuls/178014.html
https://cloud.tencent.com/developer/article/1816604
https://blog.zsxsoft.com/post/35
https://www.cnblogs.com/W4nder/p/14508817.html