IDEA配置Java远程调试,以CVE-2024-4956为例

2024-06-04 05:28

本文主要是介绍IDEA配置Java远程调试,以CVE-2024-4956为例,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

背景

学习代码审计,看到一些Java的漏洞,想要动手调试,复现漏洞搭建环境可以使用docker快速创建,了解到Java可以远程调试,本文记录学习Java远程调试环境搭建的过程。

远程调试的原理

如下图(图源:doc.oracle.com):

JPDA
首先需要明白上述些许名词的含义:

  • JDPA: Java Platform Debugger Architecture,直译Java平台调试架构,是Java为应用程序提供调试服务的一套框架。层次分明的结构提供了跨平台的特性,包含三层分别是JVM TI、JDWP、JDI
  • JVM TI: Java VM Tool Interface,Java虚拟机工具接口,是由VM(即Java虚拟机)实现的一组本地API,定义了VM必须提供的用于调试的服务。
  • JDWP: Java Debug Wire Protocol,Java调试线路协议,定义了后端与前端之间传输的信息和请求的格式。但JDWP没有定义传输机制
  • JDI: Java Debug Interface,Java调试接口,定义了用户代码级别的信息和请求。

也就是说,JDPA定义了一个框架,该框架包含三大模块,分别是后端的JVM TI、前端的JDI、以及定义了中间信息格式的JDWP。当我们调试某程序时,程序在VM(即Java虚拟机后续不在赘述)中运行,且VM实现了JVM TI,调试器后端即通过JVM TI与VM通信获取运行时的各种响应信息。

JDWP定义了调试器后端与调试器前端之间的通信格式,调试器后端将响应信息按照JDWP的规定包装后发送给调试器前端,还记得前面说的“JDWP没有定义传输机制”吗,这就意味着可以使用多种传输机制,例如可以是我们远程调试时使用的套接字。

现在已经明确了,JVM TI用于在VM运行时(调试时)收集调试关注的信息(响应),将响应按照JDWP打包后可以通过多种方式传输至调试器前端,包含套接字。调试器前端通过实现JDI,规定代码级别的请求,例如在何处断点,并将该信息同样打包成JDWP格式,以控制调试器后端。

像IDEA与Eclipse,都实现了JDI与自己UI界面来控制调试器的后端,进而操控VM,获取运行时的调试信息。

Demo:CVE-2024-4956远程调试

CVE-2024-4956是Nexus Repository 3的一个任意文件读取漏洞。我是用的docker镜像是官方的sonatype/nexus3:3.68.0-java8

首先创建一个IDEA空项目,创建一个远程JVM调试配置,IDEA实现了调试器的客户端,且会自动帮我们生成JVM的启动命令行参数,如下:

创建与配置JVM远程调试
这里调试器模式有两种,附加到远程JVM和;拷贝启动参数,-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

  • agentlib:jdwp:这是指定使用Java调试线程库的前缀。
  • transport=dt_socket:这表明调试数据将通过套接字(Socket)传输。
  • server=y:表示Java应用程序将作为调试服务器运行,调试器可以远程连接到这个服务器。
  • uspend=n:表示Java虚拟机(JVM)启动时不会暂停,即使调试器还未连接,程序也会继续运行。如果设置为suspend=y,则JVM会在启动时暂停,直到调试器连接后才继续执行。
  • address=5005:这是调试服务器监听的端口号,调试器需要连接到这个端口进行远程调试。这里设置为5005,也可以选择任何未被占用的端口。

第二步,修改VM的启动参数,添加启用远程调试。install4j是一个用于打包Java应用程序的工具,该镜像使用了install4j的环境变量INSTALL4J_ADD_VM_PARAMS,我们可以通过该环境变量修改启动参数。

docker inspect 镜像id

# 原始环境变量
INSTALL4J_ADD_VM_PARAMS=-Xms2703m -Xmx2703m -XX:MaxDirectMemorySize=2703m -Djava.util.prefs.userRoot=/nexus-data/javaprefs
# 修改后的环境变量(即将idea中copy出的参数附加)
INSTALL4J_ADD_VM_PARAMS=-Xms2703m -Xmx2703m -XX:MaxDirectMemorySize=2703m -Djava.util.prefs.userRoot=/nexus-data/javaprefs -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

docker通过-e选项指定启动的环境变量,于是得到容器的启动命令如下:

docker run -d -p 8081:8081 -p 5005:5005 --name nexus_3.68.0 -e INSTALL4J_ADD_VM_PARAMS="-Xms2703m -Xmx2703m -XX:MaxDirectMemorySize=2703m -Djava.util.prefs.userRoot=/nexus-data/javaprefs -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005" sonatype/nexus3:3.68.0-java8

第三步把jar包copy出来,附加到IDEA。要确保本地与远程的要调试部分的代码是一样的,这样我们在IDEA本地打断点,调试前端获取断点信息发送到调试后端,调试后端才能正确解析。

一开始我查到的文章,这里写的比较粗略,我一度一位本地是个空项目都能调试了,我在想,那怎么打断点呢?一些文章写要保证本地与远程的源码一样,看过文档我觉得“只要保证打断点部分的代码一样就可以了”,为验证该想法下面我做了实验:

实验:

  1. 在本地写一个web服务,打包成jar包,8090端口提供web服务
  2. 在服务器运行该jar包,为了方便我这里也使用了docker容器里的java环境,5006端口调试
  3. 本地配置IDEA调试客户端环境,连接服务器5006端口的调试端口
  4. 修改本地源码,下断点,访问web服务,观察是否还能正确触发断点
# Dockerfile
FROM vulhub/java:8u221-jdk
COPY ./apptest.jar /tmp/app.jar
EXPOSE 8090
ENTRYPOINT java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006 -jar /tmp/app.jar
// apptest.jar的源码
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;public class MainClass {public static void main(String[] args) throws Exception {HttpServer server = HttpServer.create(new InetSocketAddress(8090), 0);server.createContext("/test", new MyHandler());server.setExecutor(null); // creates a default executorserver.start();}static class MyHandler implements HttpHandler {@Overridepublic void handle(HttpExchange t) throws IOException {String response = "This is the response";t.sendResponseHeaders(200, response.length());OutputStream os = t.getResponseBody();os.write(response.getBytes());os.close();}}
}
# 构建镜像
docker build -t test:v1.0 .
# 启动容器
docker run -d -p 8090:8090 -p 5006:5006 test:v1.0
# 配置IDEA调试客户端

idea成功连接调试后端
随后修改了response的值,甚至是response变量的名称,发现在访问/test路径时,依旧可以触发断点,如下图所示;因此不需要保证本地源码与远程源码的“完全一致”,这点也很好理解,JDWP规定的信息也必然不是像“xx行xx变量有断点”此类的信息,源码被翻译为字节码,只要保证字节码时对应的,即可正确匹配(我觉得)。后续有深入研究再来探讨该问题。

修改变量名称仍可触发断点
手动设置值

CVE-2024-4956漏洞分析

如上配置好调试环境,把jar包copy出来,在IDEA中导入,项目结构=》模块=》依赖=》小加号“jar或目录”如下图:

导入jar包
我这里因为是看了别人的分析,知道漏洞点位于哪里,所以直接从docker容器里复制的特定jar包出来的。看了其他师傅的分析,get了一个小技巧:

# 将目录下的所有 jar 都复制到同一目录下, 方便 IDEA 添加依赖
mkdir ../all-lib
find . -name "*.jar" -exec cp {} ../all-lib/ \;

从官方给出的临时解决方案开始分析:

官方给出的临时解决方案
告诉我们要删除jetty.xml中的<Set name="resourceBase"><Property name="karaf.base"/>/public</Set>行,之后通过访问robots.txt来观察,若是404代表临时解决方案生效。

nexus对静态资源文件的获取有如下三种方法,优先级从1到3;目的都是获取路径,再检查请求的文件是否存在于这些路径中:

  1. getFileIfOnFileSystem,该方法从系统定义的环境变量或系统属性中获取路径,再从这些路径中get文件。默认为空。
  2. this.resourcePaths本身就是一个哈希表,是系统维护的一批路径,通过调试可以发现有2012条。
  3. this.servletContext,调用了Jetty的WebAppContext获取资源文件。
    获取资源文件
    2012条
    方法1默认为空,常规的静态资源文件通过方法2获取,在2中无法命中的交给3即jetty处理。问题即出在3处,即jetty的处理中。

访问不存在的a.txt

	// servletContext.getResource(path)public Resource getResource(String path) throws MalformedURLException {if (path != null && path.startsWith("/")) {if (this._baseResource == null) {return null;} else {try {Resource resource = this._baseResource.addPath(path);return this.checkAlias(path, resource) ? resource : null;} catch (Exception var3) {Exception e = var3;LOG.ignore(e);return null;}}} else {throw new MalformedURLException(path);}}// this._baseResource.addPath(path);public Resource addPath(String subPath) throws IOException {if (URIUtil.canonicalPath(subPath) == null) {throw new MalformedURLException(subPath);} else {return "/".equals(subPath) ? this : new PathResource(this, subPath);}}

如上是getResource与addPath的源码,首先会判断传入的path值是否为空,是否以/开头,之后与_baseResource“拼接”,即addPath方法,_baseResource的path属性为:“/opt/sonatype/nexus/public”,即将会从public路径下寻找匹配的文件。

addPath中为防止路径穿越的问题,做了处理,即canonicalPath函数,对传入的subPath进行“标准化”,具体逻辑如下:

// URIUtil.canonicalPath(subPath)public static String canonicalPath(String path) {if (path != null && !path.isEmpty()) {boolean slash = true;int end = path.length();int i;label68:for(i = 0; i < end; ++i) {char c = path.charAt(i);switch (c) {case '.':if (slash) {break label68;}slash = false;break;case '/':slash = true;break;default:slash = false;}}if (i == end) {return path;} else {StringBuilder canonical = new StringBuilder(path.length());canonical.append(path, 0, i);int dots = 1;++i;for(; i < end; ++i) {char c = path.charAt(i);switch (c) {case '.':if (dots > 0) {++dots;} else if (slash) {dots = 1;} else {canonical.append('.');}slash = false;continue;case '/':if (doDotsSlash(canonical, dots)) {return null;}slash = true;dots = 0;continue;}while(dots-- > 0) {canonical.append('.');}canonical.append(c);dots = 0;slash = false;}if (doDots(canonical, dots)) {return null;} else {return canonical.toString();}}} else {return path;}}// (doDotsSlash(canonical, dots))private static boolean doDotsSlash(StringBuilder canonical, int dots) {switch (dots) {case 0:canonical.append('/');break;case 1:return false;case 2:if (canonical.length() < 2) {return true;}canonical.setLength(canonical.length() - 1);canonical.setLength(canonical.lastIndexOf("/") + 1);return false;default:while(true) {if (dots-- <= 0) {canonical.append('/');break;}canonical.append('.');}}return false;}
  1. 先检查传入的路径path,既不是null也非空;
  2. 之后进入第一个label68循环,在该循环中对路径的每一个字符进行遍历,出现/.之前时跳出label68的循环。
  3. 下面判断循环是“正常结束”还是“提前跳出”,“正常结束”即i==end; “提前跳出”即遇到“出现/.之前”的情况。“正常结束”则返回path。
  4. 若是“提前跳出”,则维护一个dots变量标识点的数量并新建一个字符串,并将不包含该.在内的往前所有字符,保存至新字符串中,称之为标准字符串;接下来从下一个位置开始继续遍历字符串。
  5. dots一开始将被初始化为1,新的遍历将跳过该.,直接读取下一个字符,此时进入一个switch判断该字符:1.若为.:先判断dos,若dots大于0则dots自增并将slash变量置为false。若dots不大于0判断slash是否为true,若为true,dots置为1;2.若为/,判断doDotsSlash函数的值,若为true返回null,否则将slash置为true且清零dots。3.若为正常字符则将前面的点号都追加上,将此正常字符也追加。
  6. 下面来看doDotsSlash函数的逻辑,接受两个参数,标准字符串和点的数量dots,若点的数量为0,直接追加一个/,返回false;若点的数量为1,返回false;若点的数量为2,则将标准字符串长度减一(删掉最后一个字符),再寻找标准字符串中的最后一个/,将之后的都删掉。返回false;若点的数量大于2,则向标准字符串追加/,最后追加/,返回false。
  7. 只有一种情doDotsSlash会返回true,则标准化字符串返回null,即已经有两个/,且标准化字符串的长度小于2,即全是.

上面以“流水账”的形式走了一遍代码的流程,可以看出,标准化函数canonicalPath,已经在避免路径穿越的情况发生;第一次遍历path中的每个字符串,当遇到/之后有.时,跳过该.将前面的保存为新的标准化字符串,认为是没有问题的;对后续的字符串特殊处理,进入第二个遍历,遇到.前点的前面没有斜线也没有点时,认为时“良性”直接追加点号;否则将数量dots加1;

当再次遇到斜线后,前面无点、有一个点、有2个以上点时,都将点追加至标准字符串即可。只有当斜线前面点的数量恰好为2时,删除最后一个斜线后的所有字符,这两个连续的点也将被跳过。

写到这里的时候我在想这么严格的过滤,这怎么绕过?

但是addPath中,只是一个通过canonicalPath函数做了个判断,结果是否为null,并没有使用其返回值;之后将传入的路径与基础路径进行拼接,造成了路径穿越。
addPath函数
拼接
如果使用一些很明显的路径穿越payload是会被判null,从而抛出错误的。这就是前面doDotsSlash函数中,dots数量为2且标准化字符串长度小于2的情况。(测了下挺鸡肋的,两个.开头会抛出400错误,这是因为之前已经有了开头必须为/的判断了,因此这里的path前两个字符必定为"//")

第一次调,还有很多稀里糊涂的地方,有疑问评论区交流、多多批评

Reference

https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/architecture.html
https://blog.csdn.net/ywlmsm1224811/article/details/98611454
https://exp10it.io/2024/05/通过-java-fuzzing-挖掘-nexus-repository-3-目录穿越漏洞-cve-2024-4956/
https://xz.aliyun.com/t/14623
https://support.sonatype.com/hc/en-us/articles/29412417068819-Mitigations-for-CVE-2024-4956-Nexus-Repository-3-Vulnerability

这篇关于IDEA配置Java远程调试,以CVE-2024-4956为例的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1029184

相关文章

springboot健康检查监控全过程

《springboot健康检查监控全过程》文章介绍了SpringBoot如何使用Actuator和Micrometer进行健康检查和监控,通过配置和自定义健康指示器,开发者可以实时监控应用组件的状态,... 目录1. 引言重要性2. 配置Spring Boot ActuatorSpring Boot Act

使用Java解析JSON数据并提取特定字段的实现步骤(以提取mailNo为例)

《使用Java解析JSON数据并提取特定字段的实现步骤(以提取mailNo为例)》在现代软件开发中,处理JSON数据是一项非常常见的任务,无论是从API接口获取数据,还是将数据存储为JSON格式,解析... 目录1. 背景介绍1.1 jsON简介1.2 实际案例2. 准备工作2.1 环境搭建2.1.1 添加

Java实现任务管理器性能网络监控数据的方法详解

《Java实现任务管理器性能网络监控数据的方法详解》在现代操作系统中,任务管理器是一个非常重要的工具,用于监控和管理计算机的运行状态,包括CPU使用率、内存占用等,对于开发者和系统管理员来说,了解这些... 目录引言一、背景知识二、准备工作1. Maven依赖2. Gradle依赖三、代码实现四、代码详解五

C#读取本地网络配置信息全攻略分享

《C#读取本地网络配置信息全攻略分享》在当今数字化时代,网络已深度融入我们生活与工作的方方面面,对于软件开发而言,掌握本地计算机的网络配置信息显得尤为关键,而在C#编程的世界里,我们又该如何巧妙地读取... 目录一、引言二、C# 读取本地网络配置信息的基础准备2.1 引入关键命名空间2.2 理解核心类与方法

java如何分布式锁实现和选型

《java如何分布式锁实现和选型》文章介绍了分布式锁的重要性以及在分布式系统中常见的问题和需求,它详细阐述了如何使用分布式锁来确保数据的一致性和系统的高可用性,文章还提供了基于数据库、Redis和Zo... 目录引言:分布式锁的重要性与分布式系统中的常见问题和需求分布式锁的重要性分布式系统中常见的问题和需求

SpringBoot基于MyBatis-Plus实现Lambda Query查询的示例代码

《SpringBoot基于MyBatis-Plus实现LambdaQuery查询的示例代码》MyBatis-Plus是MyBatis的增强工具,简化了数据库操作,并提高了开发效率,它提供了多种查询方... 目录引言基础环境配置依赖配置(Maven)application.yml 配置表结构设计demo_st

在Ubuntu上部署SpringBoot应用的操作步骤

《在Ubuntu上部署SpringBoot应用的操作步骤》随着云计算和容器化技术的普及,Linux服务器已成为部署Web应用程序的主流平台之一,Java作为一种跨平台的编程语言,具有广泛的应用场景,本... 目录一、部署准备二、安装 Java 环境1. 安装 JDK2. 验证 Java 安装三、安装 mys

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

JAVA中整型数组、字符串数组、整型数和字符串 的创建与转换的方法

《JAVA中整型数组、字符串数组、整型数和字符串的创建与转换的方法》本文介绍了Java中字符串、字符数组和整型数组的创建方法,以及它们之间的转换方法,还详细讲解了字符串中的一些常用方法,如index... 目录一、字符串、字符数组和整型数组的创建1、字符串的创建方法1.1 通过引用字符数组来创建字符串1.2

SpringCloud集成AlloyDB的示例代码

《SpringCloud集成AlloyDB的示例代码》AlloyDB是GoogleCloud提供的一种高度可扩展、强性能的关系型数据库服务,它兼容PostgreSQL,并提供了更快的查询性能... 目录1.AlloyDBjavascript是什么?AlloyDB 的工作原理2.搭建测试环境3.代码工程1.