Apache Shiro 1.2.4 反序列化漏洞

Apache Shiro 1.2.4 反序列化漏洞

漏洞描述

Apache Shiro 1.2.4 反序列化漏洞即shiro-550反序列化漏洞。Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

工作原理

Apache Shiro框架提供了记住我的功能(RememberMe),用户登陆成功后会生成经过加密并编码的cookie。cookie的key为RememberMe,cookie的值是经过对相关信息进行序列化,然后使用aes加密,最后在使用base64编码处理形成的。

Shiro记住用户会话功能的逻辑为:

获取RememberMe的值 —> Base64解密 —> ASE解密 –> 反序列化

在服务端接收cookie值时,按照如下步骤来解析处理:

1、检索RememberMe cookie 的值
2、Base 64解码
3、使用AES解密(加密密钥硬编码)
4、进行反序列化操作(未作过滤处理)
在调用反序列化时未进行任何过滤,导致可以触发远程代码执行漏洞。

漏洞原理

因为在反序列化时,不会对其进行过滤,所以如果传入恶意代码将会造成安全问题

在 1.2.4 版本前,是默认ASE秘钥,Key: kPH+bIxk5D2deZiIxcaaaA==,可以直接反序列化执行恶意代码。而在1.2.4之后,ASE秘钥就不为默认了,需要获取到Key才可以进行渗透

漏洞特征: shiro反序列化的特征:在返回包的 Set-Cookie 中存在 rememberMe=deleteMe 字段

影响版本

  • Apache Shiro <=1.2.4

shiro特征

  • 未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段

  • 登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段

  • 不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段

  • 勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段

判断网站是否使用shiro

  1. 找到网站登录的地方,随便输入账号密码抓包(一定要输入点击登录),看返回包是否有remembeMe字段

  2. 如果以上没有返回remembeMe字段还可以尝试在请求包中的cookie中加入 rememberMe=1 ,来查看返回包是否有rememberMe=deleteMe字段。如果cookie字段有值则先清空

如我们直接访问登录的页面不进行登录,此时返回的数据包是没有remember字段的

这时,我们手动加上一个cookie:rememberMe=1,注意cookie要放在Upgrade的上面,则返回了remember字段。说明使用了shiro框架

shiro漏洞环境搭建

1
2
3
docker run -d -p 10005:8080 vulhub/shiro:1.2.4
或者
docker start a7e1941938c8

进入http://101.x.x.x:10005

这里使用工具,因为自己构造利用很是复杂

发现了AES密钥

点击检测当前利用链

然后点击命令执行

直接获取到root权限

拒绝脚本小子–来点代审!

JAVA反序列化与序列化

把对象转换为字节序列(字符串)的过程称为对象的序列化;把字节序列恢复为对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:

  1. 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中(持久化对象);
  2. 在网络上传送对象的字节序列(网络传输对象)。

当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接受方则需要把序列再恢复为java对象。

JAVA序列化实例

例子:文件名为People.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.Serializable;

public class People implements Serializable {
public String name;
public int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}


上述代码定义了People类,并且实现了Serializable接口,我们便可以对其进行序列化和反序列化操作:
下面的代码是Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.*;

public class Test {
public static void main(String[] args) throws Exception {
// 初始化对象
People people = new People();
people.setName("xiaoming");
people.setAge(18);
// 序列化步骤
// 1. 创建一个ObjectOutputStream输出流
// 2. 调用ObjectOutputStream对象的writeObject输出可序列化对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("./People.txt")));
oos.writeObject(people);
System.out.println("people对象序列化成功!");
// 反序列化步骤
// 1. 创建一个ObjectInputStream输入流
// 2. 调用ObjectInputStream对象的readObject()得到序列化的对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("./People.txt")));
People people1 = (People) ois.readObject();
System.out.println("people对象反序列化成功!");
System.out.println(people1.getName());
System.out.println(people1.getAge());
}
}

运行Test.java

代码运行结果:

1
2
3
4
people对象序列化成功!
people对象反序列化成功!
xiaoming
18

序列化过程将字节流保存在当前目录下的People.txt中,我们可以在People.txt看到序列化后的二进制对象(其中开头的aced 0005是Java序列化文件的文件头):

反序列化漏洞

回想一下CTF比赛经常遇到的PHP的反序列化漏洞,反序列化对象时会调用类的魔法函数__construct()(创建对象时触发),我们可以构造pop链来控制(改造)__construct()函数,从而反序列化时执行我们需要的操作,Java也是类似。

在上面的代码中,我们通过调用readObject()方法来从一个源输入流中读取字节序列,再把他们反序列化为一个对象,那么我们如果控制了此类的readObject()方法会怎么样?为了验证想法,我们修改一下People类,重写其readObject()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.io.Serializable;
import java.io.IOException;

public class People implements Serializable {
public String name;
public int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("calc.exe");
}


}


切换到Test.java,运行程序,当执行People people1 = (People) ois.readObject();语句时会调用People类的readObject方法,弹出计算器:

由此可知控制了类的readObject方法便可以再反序列化该类时执行任意操作,事实上大多数Java反序列化漏洞可追溯到readObject方法,通过构造pop链最终改造readObject()方法。

源码审计分析

Apache Shiro是一个Java安全框架,执行身份验证、授权、密码和会话管理。2016年网络中曝光Apache Shiro1.2.4以前的版本存在反序列化漏洞,尽管该漏洞已经曝光几年,但是在实战中仍然比较实用。

影响版本

Apache Shiro <= 1.2.4

漏洞原理

Apache Shiro框架提供了记住我的功能(RememberMe),用户登录成功后会生成经过加密并编码的Cookie。Cookie的key为RememberMe,Cookie的值是经过对相关信息进行序列化,然后使用AES加密,最后在使用Base64编码处理形成的。

在服务端接收Cookie值时,按照如下步骤来解析处理:

  1. 检索RememberMe Cookie的值;
  2. base64解码;
  3. 使用AES解密(加密密钥硬编码);
  4. 进行反序列化操作(未作过滤处理)。

在调用反序列化时未进行任何过滤,导致可以触发远程代码执行漏洞。

IDEA部署环境

下面再本地IDEA部署Apache shrio 1.2.4 漏洞环境,以便于进行漏洞动态调试分析。

  1. 首先在Github下载项目源码:

    1
    2
    https://github.com/apache/shiro
    https://github.com/apache/shiro/tags?after=shiro-root-1.2.6-release-vote1

  2. 编辑shiro/samples/web路径下的pom.xml文件,给jstl指定版本:

  3. 使用IDEA导入此MVN项目,导入上面修改的pom.xml文件即可

  1. 等待IDEA自动下载并导入完项目依赖的包,build完成后项目结构如下:

注意,pom.xml里面的配置会让程序自动下载shiro-core依赖包(后面程序加断电调试会用到该部分文件)

  1. 接着设置run/debug configurations,添加本地Tomacat环境(需要提前在本地安装Tomcat环境):

  1. 添加项目War包samples-web.war进Tomcat中:

附:此处该War包之所以存在,是因为它也是前面pom.xml配置文件设置的自动下载到本地的:

  1. 配置完以上的准备工作,就可以直接run运行程序了:

运行成功后浏览器自动打开目标程序站点,本地环境部署到此结束:

  1. 访问登录页面进行已提示账户的登录,抓包可见remerberme字段:

  1. 为了开始调试该程序,先停止程序运行,然后在外部库中找到shiro-core-1.2.4的jar包,打开RememberMeManager.class文件并在onSuccessfulLogin函数前加断点,然后点击Debug按钮开始在调试模式下运行程序:

  1. 接着在Web端登录账户root/secret,勾选上Remember Me的按钮,程序会停在断点处,然后便可以开始正式的Debug漏洞调试了;

  1. 以上项目工程相当于是只导入 samples-web 文件夹,实际上也可以直接在 IDEA 导入在 Github 下载的完整的 shiro-shiro-root-1.2.4 源码工程文件夹(以maven项目打开),待 Build 自动下载完所需的依赖包后,同样的步骤配置 Tomcat,然后直接找到 core 文件夹下的RememberMeManager.java 文件并在 onSuccessfulLogin 函数前加断点:

  1. 接着在Debug模式下运行程序,也可以在断点处拦截程序,进行调试:

后面的审计分析将基于上述导入所有项目源码的工程项目,因为相比于第一种仅导入samples-web文件夹并通过引入shiro-core-1.2.4的jar包来调试class文件的方式,直接审计全部java源码将更为直观。

序列化过程分析

下面开始正式调试分析Apache Shiro框架在登录过程中生成序列化Cookie对象的过程。

  1. 首先看下登录请求发送后断点停留的onSuccessfulLogin函数:

程序首先调用forgetIdentity构造方法处理request和response请求,包括在response中加入cookie信息,然后调用rememberIdentity函数,来处理cookie中的rememberme字段。

  1. 我们按F8来Step Over跨过forgetIdentity构造方法,然后F7来Step Into跟进下rememberIdentity函数:


可以看到,rememberIdentity函数首先调用getIdentityToRemember函数来获取用户身份,这里也就是”root”。

  1. 接着我们F7跟进rememberIdentity构造方法:

  1. 上面调用了convertPrincipalsToBytes方法将accountPrincipals也就是”root”转换为字节形式,跟进该方法查看内部如何转换:

  1. 转换过程是先序列化用户身份”id”,在对其进行encrypt加密,进一步跟进encrypt函数查看加密方式:

  1. encrypt函数就是调用AES加密对序列化的“root”进行加密,加密的密钥由getEncryptionCipherKey()得到,手动点击跟进getEncryptionCipherKey()函数会发现其值为常量(即密钥硬编码):

  1. Shift+F8进行Step Out步出,返回到rememberIdentity函数:

  1. 跟进rememberSerializedIdentity函数查看后续转换流程,发现该函数对上述root的AES加密后的序列化值进行base64编码后,设置到cookie中:

到这里我们可以梳理下上述整个Cookie的生成过程,当我们勾选上Remember Me选项后,以root身份登录,后端会进行如下操作:

1、 序列化用户身份”root”,得到值A;
2、 对root的序列化值A进行AES加密(密钥为硬编码的常量),得到值B;
3、 base64编码上述计算的结果B,得到值C;
4、 将值C设置到response响应包中cookie的rememberme字段。


恢复程序,清除断点
抓包查看

对应上了

反序列化过程分析

以上已经调试分析完shiro生成Cookie字段的序列化、加密过程,下面来进一步调试分析下Cookie字段的反序列、解密过程。

  1. 将断点打在org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity函数处,然后发送一个带有rememberMe Cookie的请求:

  1. 跟进查看getRememberedPrincipals

  1. 跟进getRememberedSerializedIdentity函数,发现函数提取出cookie并将其进行base64解码:

  1. Step Out 返回到getRememberedPrincipals函数,继续跟进到converBytesToPrincipals函数,发现其对cookie进行AES解密和反序列化:

  1. 同理再依次跟进查看 AES 解密函数 decrypt、反序列化函数 deserialize,如下:

至此 Cookie 的反序列化、解密流程分析完毕,整个流程大致为:

  1. 读取 cookie 中 rememberMe 值;
  2. base64 解码;
  3. AES解密;
  4. 反序列化
    其中 AES 加解密的密钥为常量且反序列化过程没有进行过滤,于是我们可以手动构造rememberMe 值,改造其readObject()方法,让其在反序列化时执行任意操作。

Apache Shiro 1.2.4 反序列化漏洞
http://example.com/2025/07/28/Apache_Shiro_1.2.4_反序列化漏洞/
作者
奇怪的奇怪
发布于
2025年7月28日
许可协议