loading...
shiro反序列化漏洞复现
Published in:2023-07-05 | category: Java 反序列化 Shiro

0x00前言

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

0x01原理

I.shiro550

漏洞原理

shiro会在cookie中生成一个rememberMe字段,里面的值是输入的内容通过系列操作写入的。一般的流程为:从前端传入值,序列化后传到cookies,进行AES加密,之后通过java模块的base64加密。

而反序列化漏洞的时候,进行base64解密。之后AES解密,这个是shiro默认加密方式,其中AES的key存在密钥泄露,之后反序列化,这里是漏洞的成因。

入口点在Usercontroller.class的doLoginPage()

第一行的subject是一个安全主体,不是重点,往下走根据doLoginPage跳到Subject.class的login(),在DelegatingSubject类

这里先是调用当前类的clearRunAsIdentitiesInternal函数,进入函数看看

这里调用当前类的clearRunAsIdentities函数

发现是检查当前是否有session的,有的话就移除,步入getSession其实没有什么值得看的地方,继续往下走

跳securityManager.login,会跳转到DefaultSecurityManager类

从this.authenticate函数进入,会跳转到AbstractAuthenticator类的authenticate函数,发现有一个info = this.doAuthenticate(token),继续进入查看

一直跟进函数,最后发现跳转到了ModularRealmAuthenticatior类里面,调用了doAuthenticate函数,猜测assertRealmsConfigured函数作用是判断是否有一些配置,然后获取realms赋值给realms

三目运算符,判断1就进入doSingleRealmAuthentication否则进入doMultiRealmAuthentication

1
realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);

单步进入doSingleRealmAuthentication,因为token是不存在的目前,if会抛出异常,直接看realm.getAuthenticationInfo(token)部分,这里doMultiRealmAuthentication和doSingleRealmAuthentication的执行流程类似,不再详细分析

该函数在AuthenticatingRealm类里面,执行流程可以看到是先从缓存获取token,如果token是空则走doGetAuthenticationInfo(token),进入函数查看

根据分析,只有info不存在的时候就会抛出异常,否则始终会返回一个info,并且查看全部的跳转函数都没有发现反序列化的逻辑,要退回到AbstractAuthenticator类的authenticate函数,往下走会也是会看到只要参数是空的就会触发异常,并且始终还是返回info,直接往下看到this.notifySuccess函数,查看里面的onSuccess也没有发现反序列化的操作

返回到DefaultSecurityManager类的login函数里面,继续往下走,进入this.createSubject(token, info, subject)函数,这里写了3个set函数,并在此之前还写了个createSubjectContext函数,先查看下createSubjectContext,会返回一个DefaultSubjectContext

看上去暂时还用不到,继续往下走

进入this.createSubject(context)函数,可以看到这里是将刚才的3个set复制到这里,类似做了一个map

分别点开看看这几个函数看看

在第三个函数this.resolvePrincipals(context)发现到相关的remember函数,看到又套了一层context.resolvePrincipals(),直接进入this.getRememberedIdentity(context)看看

先创建了一个rmm对象,是一个remember管理器,如果rmm不会为空就返回rmm.getRememberedPrincipals(subjectContext)

一直跟进发现会跳到AbstractRememberMeManager类的getRememberedPrincipals函数

发现疑似相关序列化函数this.getRememberedSerializedIdentity(subjectContext),跟进查看函数的实现,函数在CookieRememberMeManager类中,可以看到发起了一个http请求,对获取到的数据进行base64解码,返回解码内容

在this.convertBytesToPrincipals(bytes, subjectContext)发现了最终会返回一个反序列化对象,跟进this.getSerializer().deserialize(serializedIdentity),最终实现反序列化

进入this.decrypt(bytes)函数,对解码后的base64内容进行解密,并使用this.getDecryptionCipherKey()获取原始密钥

跟进这个函数能找到默认的密钥kPH+bIxk5D2deZiIxcaaaA==

利用步骤

使用ysoserial工具生成调用链,一般使用CC6,因为通用性相较于其他较强,不太受环境限制。或通过TemplatesImpl加载字节码和JRMPClient与远程RMI协议交互,前者绕过InvokerTransformer限制或数组类加载的问题,一般用于内存马注入,后者可用于不出网环境探测

II.shiro721

其入口点还是跟shiro550一样,都是从rememberMe字段进去,shiro在1.4.2版本之前使用的是AES-128-CBC加密方式,容易受到Padding Oracle Attack 影响,如果填充不正确,系统的响应可能不一样,攻击者会根据系统响应的数据来逐字块解密,与shiro550不同的是这个漏洞触发需要一个已知的账号且处于登录状态

Padding Oracle Attack

填充提示攻击,Padding的含义是“填充”,在解密时,如果算法发现解密后得到的结果,它的填充方式不符合规则,那么表示输入数据有问题,对于解密的类库来说,往往便会抛出一个异常,提示Padding不正确

密码学里的iv,并没有保密性的要求,所以对于使用CBC Mode的加密算法来说,iv经常会随着密文一起发送。常见的做法是将iv作为一个前缀,附着在密文的前面。对于CBC Mode来说,iv的长度必须与分组的长度相等

密钥生成

可以看到550的是生成静态key而721之后是使用AES-CBC模式生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// shiro < 1.2.4
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

// 1.2.4 ≤ shiro < 1.4.2
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
AesCipherService cipherService = new AesCipherService();
this.cipherService = cipherService;
setCipherKey(cipherService.generateNewKey().getEncoded());
}

漏洞原理

进入generateNewKey函数看看

继续查看init函数,传入的是要生成的密钥的位长度和一个随机数生成器

然后调用kg.generateKey函数,跟进

跟进到engineGenerateKey函数,可以发现这里会生成16字节随机序列,并返回var1,var1是SecretKeySpec对象,最后就是在上面AbstractRememberMeManager代码里面调用getEncoded(),结束密钥生成流程

所以为什么会失败,这里跟进下shiro判断cookie解密失败的流程

在AbstractRememberMeManager.decrypt函数

继续跟进函数cipherService.decrypt(encrypted, getDecryptionCipherKey())

这里有2个套娃crypt函数,一直跟进

跟进这个doFinal,发现这里会抛出2个异常类IllegalBlockSizeException,BadPaddingException

抛出的异常会一直往上传递,一直到AbstractRememberMeManager类的getRememberedPrincipals函数捕捉,并会调用onRememberedPrincipalFailure函数,跟进查看

进入到forgetIdentity函数,又是一个套娃

进入removeFrom函数

可以看到有个addCookieHeader函数,可以看到addHeader里面就是设置了请求头Set-Cookie

这里已经是结束了,因为上面代码有一个deletecookie的值,总的来说就是当shiro校验cookie解密如果不符合填充就会发送一个请求头Set-Cookie: deleteMe

这个漏洞是AES的CBC加密模式缺陷和shiro的错误返回与正确返回内容不同形成了一个类似SQL注入的布尔盲注,并且需要不断发包去碰撞,个人感觉从防守角度来看攻击太过于明显,一般不太常用这个漏洞去做渗透

0x02漏洞复现

I.shiro550

1、从vulhub下载漏洞环境

2、判断是否是shiro,在登录框部分会有一个rememberMe勾选,勾选成功通过bp抓包,里面有一个set-cookies:rememberMe=deleteMe,可以判断使用了shiro框架。

3、确认是否存在漏洞,使用URLDNS链。从dnslog上面复制网址到payload上,可以使用ysoserial工具构造链

4、将payload进行aes加密编码

1
java -jar ysoserial URLDNS 'http://xxx.dnslog.cn'>1.txt

其中要删除JSESSIONID,当该字段存在就不会进行反序列化操作

5、查看dnslog,看到有请求,证明漏洞利用成功

6、使用CC链进行远程代码执行

II.shiro721

构建一个shiro-721环境

1
2
3
4
git clone https://github.com/inspiringz/Shiro-721.git
cd Shiro-721/Docker
docker build -t shiro-721 .
docker run -p 8080:8080 -d shiro-721

访问系统

需要登录获得cookie的rememberMe值

用yeoserial生成payload

1
java -jar ysoserial-all.jar CommonsBeanutils1 "touch /tmp/test" > poc.ser

使用脚本去爆破尝试带有恶意的cookie,若成功直接使用cookie进行漏洞利用

shiro检测工具

https://github.com/safe6Sec/ShiroExp

padding爆破脚本

https://github.com/inspiringz/Shiro-721/blob/master/shiro_exp.py

0X03总结

shiro550反序列化的路径

加密:payload——>序列化——>AES——>base64

解密:base64——>AES——>反序列化——>payload

1、对于shiro550,其版本低于1.2.4

2、对于shiro721,其版本低于1.4.2并高于1.2.4

0x04修复

1、升级最新版本的shiro框架(shiro550 >= 1.2.4,shiro721 >= 1.4.2)

2、限制rememberMe长度

3、使用WAF

4、避免默认密钥编码

Prev:
服务及环境变量配置(Docker版)
Next:
应急响应流程
catalog
catalog