这篇文章主要是对yemoli
和R1ckyZ
在KCON2023中的议题《Magic In Java API》的一次学习记录
对应的slide可以在https://github.com/knownsec/KCon/tree/master/2023中获取
该议题的结构如下
议题中API指代的就是sun.print.UnixPrintServiceLookup
这个类
这是一个用于打印服务注册的功能的接口
一个用于查找可用的打印机服务
public static void main(String[] args) throws IOException { // 获取 UnixPrintServiceLookup 实例 PrintServiceLookup lookup = (PrintServiceLookup) PrintServiceLookup.lookupDefaultPrintService(); // 使用 UnixPrintServiceLookup 查找所有可用的打印服务 PrintService[] printServices = lookup.getPrintServices(); if (printServices.length == 0) { System.out.println("No printers available."); } else { System.out.println("Available printers:"); for (PrintService printer : printServices) { System.out.println(printer.getName()); } } }
在windows中对应的功能是在Win32PrintServiceLookup
中实现的
public static void main(String[] args) { // 获取 PrintServiceLookup 实例 // 获取所有可用的打印服务 PrintService[] printServices = PrintServiceLookup.lookupPrintServices(null, null); if (printServices.length == 0) { System.out.println("No printers available."); } else { System.out.println("Available printers:"); for (PrintService printer : printServices) { System.out.println(printer.getName()); } } }
在UnixPrintServiceLookup
初始化的时候
创建了一个PrinterChangeListener
监听器,是一个线程类,不断的更新服务
过程中存在有可能利用的点getAllPrinterNamesBSD
方法中
存在有命令执行的位置,传入的参数是在lpcAllCom
数组中的其中一个元素
如果我们能够控制传入的参数就能够达到RCE的目的
之后就是从修改类属性的”不同方法“的角度来阐明该API可能导入RCE的情况
想要更改类的属性值,我们常用的结合反序列化可以通过java中的反射机制进行类属性值的更改,这个想法在很多其他的常见反序列化利用中很常见,但是可惜的是,在UnixPrintServiceLookup
类并没有实现Serializable
接口
但是跟进过dubbo rpc框架的CVE也能够知道,在类似hession这类远程调用协议,在序列化和反序列化的过程中可以反序列化没有实现Serializable
接口的类
在前面更改类属性值得其中第一个方法也就是通过反序列化过程中的反射机制来进行类属性值得更改
前面只提到了在dubbo使用hession的时候可以在没有实现Serializable
接口得情况下进行属性值的更改
在原slide中作者总结了常见的序列化协议是否支持没有实现Serializable
接口
也主要就是hession类和json处理类的几个协议
在图中,可以利用的hession版本是在3.x才可以
我们简单跟进一下具体的能够进行利用的原理(即为什么可以支持没有实现Serializable
接口的类的反序列化和是怎样通过反射来修改类属性
这里我们默认是对Java对象进行反序列化,因为在hessian的反序列化的过程中将会针对不同的类型选择不同的序列化器
针对Java对象,默认是使用JavaDeserializer
这个反序列化器
首先是在Hessian2Input#readObject
方法中
根据tag位的不同选择不同的处理方法
而在Hessian2Input#readObjectDefinition
中主要是将反序列化的类中所有存在的类名给保存在了_classDefs
属性中
之后又重新回到了Hessian2Input#readObject
中进行类对象的实例化
这里并没有显示的指明反序列化的类,进而通过序列化工厂类的反序列化逻辑进行反序列化
通过type确定使用默认的JavaDeserializer
反序列化器
来到了JavaDeserializer#readObject
中进行对象的实例化
在JavaDeserializer#instantiate
中存在有调用构造方法的操作
在得到了反序列化类的实例之后,将其传入JavaDeserializer#readObject
的一个同名方法中进行类属性的反序列化
对于不同的属性的类型,将会选择不同的反序列化器进行处理
这里我们象征性的看一下String类型的反序列过程
这里是通过反射进行恢复属性值
总结一下,上面也就是在hessian反序列化的过程中可能调用构造方法和反射恢复属性值的原理
至于hessian中的可以序列化和反序列化不实现Serializable
接口的类,我们可以在SerializerFactory#getDefaultSerializer
中找到出处
在抛出IllegalStateException
异常需要满足条件if (!Serializable.class.isAssignableFrom(cl) && !this._isAllowNonSerializable)
如果没有实现Serializable
接口,也可以选择将属性_isAllowNonSerializable
设置为true
hessianOutput.getSerializerFactory().setAllowNonSerializable(true);
在4.x的版本中在前面的默认的反序列化器直接简单的设置为JavaDeserializer
的基础上进行了一个限制
具体在SerializerFactory#getDefaultSerializer
这个方法中,这个方法是用来返回一个没有被直接匹配的序列化器的一个默认的序列化器
这里的逻辑为:
_defaultSerializer
不为null,就返回这个序列化器进行类对象的序列化操作,如果为null则执行 2 操作Serializable
接口的情况下同时没有开启_isAllowNonSerializable
将会抛出异常,否则执行 3 操作writeReplace
的无参方法,就构造一个UnsafeSerializer
返回而在API的触发点 -- 构造方法的入口的位置instantiate
方法中
在3.x版本中将会在这里调用构造方法,结合之后的反射修改属性值控制参数能够达到RCE
而在这里4.x版本,使用的是UnsafeDeserializer#instantiate
方法
使用了allocateInstance这个native方法实例化一个被序列化的对象,但是这里不会执行构造函数,也就不能够利用该API
在属性值的恢复也使用的是Unsafe类中的API
构造一个简单的JavaBean,观察构造函数是否调用
import java.io.Serializable; public class ObjectTest implements Serializable { private String cmd; public ObjectTest() { System.out.println("construct..."); } public String getCmd() { return cmd; } public void setCmd(String cmd) { this.cmd = cmd; } }
分别使用3.x 4.x版本的hessian进行序列化和反序列化操作
这个协议是在alibaba用在Dubbo这个RPC框架中的一个内置版本
维护了一些常见的黑名单
这也是导致CVE-2022-39198的sink点,我之前也分析过这个链子
这个也和hessian-lite类似,都是在原始hessian的基础上的修改版本,也内置了一个黑名单
和原始的hessian类似,在4.x版本中同样使用了Unsafe的API,将不会进行构造方法的触发
这里主要是针对hessian和其衍生的协议的角度详细学习了hessian中能够调用构造方法的原理,同时也对hessian协议有了更深的理解