反序列化漏洞与Shiro-550
本文先从零介绍了反序列化漏洞,并紧接着介绍Shiro 550反序列化漏洞的形成原理、利用方式和防护方法。
(不涉及对利用链的具体分析)
第一部分:前置知识
第二部分:Apache Shiro 550反序列化漏洞的形成分析
第三部分:Apache Shiro反序列化漏洞的利用&防护
1.Shiro反序列化漏洞的前置知识
1.1.序列化与反序列化
序列化是什么,解决了什么问题?
-
序列化:将程序代码中的对象转换为字节流序列的过程。
-
反序列化:即序列化的逆过程,即将字节流序列转化为对象的过程。
-
应用情景:由于程序中的对象是复杂且极具平台特征的,所以在网络传输、持久化存储及跨平台兼容等方面有天然的不足,所以程序员通过序列化,将复杂各异、立体的程序对象转换为通用、扁平的字节流序列进行传输或存储,在特定时间对其进行反序列化,以尽可能地还原出原始的对象。
举一下以上三个情景的例子:
-
网络传输(B/S架构的在线商城)
-
客户端
-
用户在客户端中填写了订单信息;
-
客户端(JS)使用序列化技术,将订单对象转为字节流;
-
客户端将字节流放在HTTP请求体中进行相应的请求。
-
-
服务端
- 服务端将HTTP请求体中的相关字节流取出,并将其反序列化为订单对象;
- 接着,服务端就订单对象进行一系列相关操作(如:验证订单号、计算金额、拉起支付等);
- 处理完毕后,服务端将订单信息存入数据库中作为记录。
-
-
持久化存储(如Cookie)
- 服务端将用户的相关会话对象进行序列化后得到一个JSON字符串,并将其放入响应报文的
Set-Cookie
字段中(Ps:JSON是一种数据格式,它并非只能代表序列化之后的字符串,也可以是未经序列化的对象); - 客户端将该JSON字符串反序列化得到一个Cookie对象,在进行请求时带上相关的Cookie值。
- 服务端将用户的相关会话对象进行序列化后得到一个JSON字符串,并将其放入响应报文的
-
跨平台兼容
比如Python向php传递对象:
-
# Python将person对象序列化为JSON字符串,并将其编码后传给php处理
import json
import base64
# 定义一个person对象
person = {
"name": "John",
"age": 25
}
# 将该对象转换为JSON字符串(序列化)
json_str = json.dumps(person)
# 值为{"name": "John", "age": 25}
# 将JSON字符串进行Base64编码
base64_str = base64.b64encode(json_str.encode()).decode()
# 值为eyJuYW1lIjogIkpvaG4iLCAiYWdlIjogMjV9
# php将收到的数据解码后得到JSON字符串,将JSON字符串反序列化后得到person对象
<?php
// 获取Base64编码的JSON字符串
$base64_str = "eyJuYW1lIjogIkpvaG4iLCAiYWdlIjogMjV9";
// 将Base64字符串解码为JSON字符串
$json_str = base64_decode($base64_str);
// 值为{"name": "John", "age": 25}
// 将JSON字符串解码为PHP对象(反序列化)
$person = json_decode($json_str);
// 值为:object(stdClass)#1 (2) {
// ["name"]=>
// string(4) "John"
// ["age"]=>
// int(25)
//}
// 在PHP中使用解码后的对象或数组
echo "Name: " . $person->name . "\n";
echo "Age: " . $person->age . "\n";
// 执行结果:
// Name: John
// Age: 25
?>
1.2.反序列化漏洞
反序列化漏洞的形成:如果服务端未对客户端的输入进行严格、全面的过滤,便使得攻击者有机会传入精心构造的Payload,当服务端尝试对包含这些POP链的数据进行反序列化时,就可能会触发恶意代码的执行。
-
简单的例子:
<?php // 弱点1 class VunerableClass { public $command; public function __construct($command) { $this->command = $command; } public function execute() { // 执行恶意操作 exec($this->command); } } // 弱点2 class Server { public function handleRequest($serializedData) { // 反序列化数据 $user = unserialize($serializedData); // 如果反序列化的对象是 VunerableClass 类的实例,则立即执行恶意命令 if ($user instanceof VuneableClass) { $user->execute(); } else { // 处理正常逻辑 echo "处理正常逻辑"; } } } // 假设$serializedData存储了攻击者传来的恶意字符串(Payload) $serializedData = 'O:14:"VunerableClass":1:{s:7:"command";s:8:"rm -rf /";}'; $server = new Server(); $server->handleRequest($serializedData); ?>
在上述的简单示例中,攻击者传入了恶意的字符串序列,而服务端在未对其进行充分验证的情况下就进行了反序列化,从而使得攻击者的代码被执行。
1.3.HTTP的会话管理
HTTP是无状态的,所以Web应用在设计时会引入Cookie、Session等会话管理技术。
就Cookie而言,客户端在登录或设置偏好时会向服务端请求Cookie,服务端会在处理后将Cookie写入响应报文的
Set-Cookie
字段中,客户端存储Cookie,并在进行相应请求时附上所需的Cookie字段。
既然Cookie的使用情景涉及到网络传输和存储,那么自然和序列化技术联系上了:
在Shiro框架中,服务端发给客户端的Cookie中便是经序列化之后的字节流,服务端在收到客户端发来的Cookie后需要对其进行反序列化。
1.4.AES
AES是一种可靠的对称密码算法(加解密使用相同的密钥)。
注意,AES的“可靠”不是指使用AES加密算法就没有安全风险:
- 作为对称密码算法,AES必然面临着这类算法无法回避的问题——密钥管理与分发,而早期版本的Shiro(<=1.2.4)中是使用硬编码的方式将AES密钥存储在了源代码中,攻击者便能够在收集到多个版本的Shiro中AES密钥后进行枚举,得知目标Shiro框架所使用的AES密钥。【这点很重要,是实战中能利用Shiro反序列化漏洞的前置条件】
- AES是对称密码算法中的分组密码算法:即将明密文拆分为多个长度固定的分组,AES本身只是对各个分组进行加解密(AES的可靠到此为止,它只能保证分组的密文不被直接破解),所以引入了ECB、CBC及CTR等加密模式作为“引擎”控制AES对各个分组的操作,而ECB这种加密模式本身有较大的安全风险(ECB模式中各个分组之间相互独立,这会导致猜测明文攻击和重放攻击等,具体细节欢迎阅读鄙人这篇:《密码学——现代密码体制总结》)【在Shiro中的AES默认使用CBC加密模式(后期使用GCM),相对ECB来说安全得多】
2.Apache Shiro框架的反序列化漏洞
2.1.关于Apache Shiro:
Shiro(发音为“shee-roh”,日语中“castle(堡垒)”的意思)是一个强大易用的Java安全框架,可执行身份验证、授权、加密和会话管理,可用于保护任何应用程序的安全,从命令行应用程序、移动应用程序到最大的web和企业应用程序。
Shiro有三大概念:Subject、SecurityManager和Realms
Subject(主体):Subject代表当前正在与应用程序交互的用户。它可以是一个人、一个设备或者其他实体。Subject可以执行身份验证和授权操作,并且可以维护与用户相关的会话状态。Subject是Shiro的核心概念,它封装了与当前用户相关的信息和操作。
SecurityManager(安全管理器):SecurityManager是Shiro的核心组件,负责管理所有的Subject、Realm和权限操作。它是应用程序与Shiro之间的桥梁,处理身份验证、授权和会话管理等安全相关的操作。SecurityManager协调Subject与Realm之间的交互,确保安全策略的执行。
Realm(域):Realm是Shiro用于获取安全数据(如用户、角色和权限)的组件。它负责从数据源(如数据库、LDAP等)中获取用户的身份验证和授权信息,并将其提供给SecurityManager进行处理。Realm可以自定义实现,以适应不同的身份验证和授权需求。应用程序可以配置一个或多个Realm,以支持多种身份验证和授权方式。
简单来说,安全管理器SecurityManager会对主体Subject的相关操作进行控制,并向域Realm进行获取安全数据等。
这和MVC有些许相似之处,域Realm对应MVC中的模型Model,安全管理器SecurityManager对应MVC中的控制器Controller,具体的web应用(如Shiro的登录界面)对应MVC的视图View。
2.2.Shiro序列化与反序列化的过程
Shiro在会话管理中提供了一个“记住我”功能,用户可以在登录成功前时选择启用“记住我”功能,该功能实现如下:
- 初次登录
- 对用户的主体数据(Subject)进行序列化
- 对序列化值进行AES加密
- 对密文进行Base64编码
- 将编码值(shiro令牌)写入HTTP响应包的
Set-Cookie
中,返还给用户存储
- 再次访问
- 取出用户Cookie中的Remember字段值(shiro令牌)
- 使用Base64进行解码
- 对编码值进行AES解密
- 对得到的序列化值进行反序列化
2.3.Shiro反序列化漏洞的形成
由于使用了AES加密,Shiro完全相信了请求Cookie中
RememberMe
字段的内容(如果能正常解密的话);但只要攻击者知道了AES的加密细节(密钥、加密模式),便能控制服务器所反序列化的内容,如果知道了反序列化的利用链,那么便能进行RCE;
在Shiro版本<=1.2.4时,AES密钥被硬编码在源代码中,攻击者便可通过收集各版本源码来枚举出目标Shiro系统所使用的AES密钥,从而控制反序列化内容。
2.3.1.代码分析1:宏观设计
前文提到Shiro主要是依靠各安全控制器SecurityManager来实现安全控制,本文所涉及的安全控制器则是DefaultSecurityManager.java
(Shiro框架的安全控制器默认实现)
在安全管理登录会话时,DefaultSecurityManager
会指向RememberMeManager.java
;
RememberMeManager
接口对RememberMe
的相关操作进行了宏观设计,它指定了一系列的方法待后面的类进行具体实现。
进一步看,RememberMeManager
中的各方法由AbstractRememberMeManager
基本实现;
而为了实现Cookie中的“记住我”功能,Shiro写了个CookieRememberMeManager
来继承了AbstractRememberMeManager
,并添加了一些用于操作Cookie的成员。
2.3.2.代码分析2:调试跟进
2.3.2.1.初次登录——生成持久化令牌
对用户传来的数据进行序列化、AES加密及Base64编码后写入Cookie中:
①
AbstractRememberMeManager.java
中的convertPrincipalsToBytes
=>序列化、加密②
CookieRememberMeManager.java
中的RememberSerializedIdentity
=>编码并写Cookie更完整的调用链大致如下:
onSuccessfulLogin
DefaultSecurityManager.java =>
rememberMeSuccessfulLogin
DefaultSecurityManager.java =>
onSuccessfulLogin
RememberMeManager.java ==实际由
onSuccessfulLogin
AbstractRememberMeManager.java实现 =>
rememberIdentity
AbstractRememberMeManager.java =>
convertPrincipalsToBytes
AbstractRememberMeManager.java =>序列化:
serialize
AbstractRememberMeManager.java =>加密:
encrypt
AbstractRememberMeManager.java =>编码并写Cookie:
rememberSerializedIdentity
AbstractRememberMeManager.java ==实际由
rememberSerializedIdentity
CookieRememberMeManager.java实现
2.3.2.1.1.序列化、AES加密
步入encrypt
查看:
向该函数传入序列化后的字节数组serialized
,接着对密码算法进行一定的初始化配置,并在加密后返回密文值
初始化(指定密码算法的相关参数):这里使用了CBC加密模式的128bit密钥长度的AES加密算法(对明文采用PKCS5进行填充)
进行加密(传入明文和密钥):这里以字节数组形式传入密钥
2.3.2.1.2.Base64编码并写Cookie
来到CookieRememberMeManager.java
的RememberSerializedIdentity
方法
2.3.2.2.再次访问——鉴权
从请求Cookie中读出
RememberMe
值进行Base64解码,将密文进行AES解密后进行反序列化:①
CookieRememberMeManager.java
中的getRememberedSerializedIdentity
=>读Cookie并解码②
AbstractRememberMeManager.java
中的convertBytesToPrincipals
=>解密、反序列化更完整的调用链大致如下:
resolvePrincipals
DefaultSecurityManager.java =>
getRememberedIdentity
DefaultSecurityManager.java =>
getRememberedPrincipals
RememberMeManager.java ==实际由
getRememeberedPrincipals
AbstractRememberMeManager.java实现 =>读Cookie并解码:
getRememberedSerializedIdentity
AbstractRememberMeManager.java ==实际由
getRememberedSerializedIdentity
CookieRememberMeManager.java实现 =>
convertBytesToPrincipals
AbstractRememberMeManager.java =>解密:
decrypt
AbstractRememberMeManager.java =>反序列化:
deserialize
AbstractRememberMeManager.java
2.3.2.2.1.读Cookie、Base64解码
进入getRememberedSerializedIdentity
从请求包中拿cookie进行处理
对获取到的cookie进行解码
2.3.2.2.2.AES解密并反序列化
使用AES解密并反序列化
3.Shiro反序列化漏洞的利用&防护
3.1.Shiro-550的利用
利用Shiro-550需要两个数据:①AES密钥 ②反序列化时的利用链
以下使用GUI工具进行漏洞利用演示:https://github.com/j1anFen/shiro_attack/
- 找AES密钥
2. 找利用链
- RCE或注入内存马
3.2.Shiro-550的防护
- 不使用硬编码的AES密钥
- 将Shiro框架升级到最新版本
- 或不使用Shiro默认的AES密钥,而是自己生成一个密钥并妥善保管