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 executor
        server.start();
    }

    static class MyHandler implements HttpHandler {
        @Override
        public 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/676481.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

无人值守设备远程运维,几个关键问题如何解决?

商用无人值守设备承载着很多企业的一线业务&#xff0c;它们分布广泛且数量众多&#xff0c;企业如何对这类设备实施有效的运维管理是一个重要的课题。 面对这一问题&#xff0c;很多企业选择了引入远程运维方案&#xff0c;以远程桌面为基础工具实施远程运维管理&#xff0c;…

安卓玩机搞机技巧综合资源----电脑控制手机 投屏操控的软件工具操作步骤解析【二十二】

接上篇 安卓玩机搞机技巧综合资源------如何提取手机分区 小米机型代码分享等等 【一】 安卓玩机搞机技巧综合资源------开机英文提示解决dm-verity corruption your device is corrupt. 设备内部报错 AB分区等等【二】 安卓玩机搞机技巧综合资源------EROFS分区格式 小米红…

1、Tomcat整体架构

1、Tomcat整体架构 Tomcat介绍Tomcat概述Tomcat目录结构web应用部署的三种方式 Tomcat整体架构分析Tomcat架构图Tomcat核心组件Server 组件Service组件连接器Connector组件容器Container组件结合Server.xml理解Tomcat架构请求定位 Servlet 的过程 Tomcat架构设计精髓Connector高…

netty LengthFieldBasedFrameDecoder 根据动态长度分包粘包

如下数据格式 在方法&#xff1a; // Integer.MAX_VALUE, // maxFrameLength: 最大允许的帧长度// 4, // lengthFieldOffset: 长度字段在帧中的偏移量&#xff0c;这里是在帧头之后// 4, // lengthFieldLength: 长度字段的长度&#xff0c;4字节表示32位整数// 0, // …

常见4种时间管理方法及实施步骤(收藏版)

有效的时间管理方法&#xff0c;不仅能够保证项目按时交付&#xff0c;还能提高开发效率&#xff0c;减少成本超支和质量风险。如果缺乏明确的时间规划&#xff0c;可能会导致任务延误&#xff1b;容易造成资源分配不当&#xff0c;导致整体效率低下和成本增加。 因此有效的时间…

go语言实现微信扫码登录,涵盖微信登录超详细流程并附带时序图

微信扫码登录 1. 简述&#xff1a;此文章目的主要是web网站进行微信扫码登录2. 微信登录过程时序图3. 全部微信登录组成元素3.1. 微信扫码登录后端总共只需要两个接口&#xff0c;3.2. 微信登录的各个对象&#xff1a;3.3. 微信登录的主要参数&#xff1a; 4. 流程解释&#xf…

基于Android Studio 垃圾分类助手App--原创

一、高质量源码&#xff08;非开源&#xff0c;白嫖低价勿扰&#xff09; 关注公众号&#xff1a;《编程乐学》 后台回复&#xff1a;24060301 二、项目演示视频 基于Android Studio 垃圾分类助手App--原创 三、开发环境 四、设计与实现 1.启动页 1.设置延迟三秒后执行 runna…

HDL-A/1-110VAC-2电流继电器 JOSEF约瑟 导轨安装

一. 应用 HDL系列电流继电器是静态型&#xff0c;不带方向性的、瞬动、交流电流继电器。可用于电力系统输电线,电机过负荷和短路保护中&#xff0c;作为启动元件。 继电器对短路电流中的直流分量不敏感&#xff0c;因此可用于要求哲态超小的线路中&#xff0c;改继电器由集成…

SpringBoot启动流程分析之设置系统属性spring.beaninfo.ignore、自定义banner图(五)

SpringBoot启动流程分析之设置系统属性spring.beaninfo.ignore、自定义banner图&#xff08;五&#xff09; 参考 目录 文章目录 SpringBoot启动流程分析之设置系统属性spring.beaninfo.ignore、自定义banner图&#xff08;五&#xff09;1、设置sping.beaninfo.ignore属性2、…

一文带你搞懂单模光纤和多模光纤的区别

单模光纤和多模光纤的区别及常见疑问解答 随着网络技术的飞跃&#xff0c;光纤因其高速传输与大容量特性&#xff0c;成为通信领域的佼佼者。光纤主要分为单模与多模&#xff0c;两者在几何与传输特性上迥异&#xff0c;实际应用中表现显著不同。本文将深入剖析两者的差异与应用…

记一次黑群晖折腾的过程

Tips&#xff1a; 建议先完整看完这篇文章&#xff0c;理解大致流程后再上手操作&#xff0c;其中有一些注意点需要事先了解 安装黑群晖的教程网上很多&#xff0c;我是参考了这篇&#xff1a; https://post.smzdm.com/p/am3epen4/前言在上一盘文章中组装了一台黑群晖&#…

Docker基础篇之将本地镜像发布到私有库

文章目录 1. Docker Registry简介2. 将本地镜像推送到私有库 1. Docker Registry简介 Docker Registry是官方提供的工具&#xff0c;可以用于构建私有镜像仓库。 2. 将本地镜像推送到私有库 下载Docker Registry docker pull registry现在我们可以从镜像中看到下载的Regist…

数据持久化第七课-URL重写与Ajax

数据持久化第七课-URL重写与Ajax 一.预习笔记 1.URL重写(对网页地址进行保护) 首先编写module,实现对网络地址的处理 其次就是module的配置 最后验证url重写技术 2.Ajax数据交互 编写后端响应数据 处理跨域的配置问题 运行项目得到后端响应数据的地址 编写前端ajax进行数据请…

基于VGG16的猫狗数据集分类

目录 1. 作者介绍2. VGG16介绍2.1 背景介绍2.2 VGG16 结构 3. Cat VS Dog数据集介绍4. 实验过程4.1 数据集处理4.2 训练部分设置4.3 训练结果4.4 问题分析4.5 单张图片测试 5.完整训练代码与权重参考文献 1. 作者介绍 孙思伟&#xff0c;男&#xff0c;西安工程大学电子信息学…

Accelerate 笔记:保存与加载文件

保存和加载模型、优化器、随机数生成器和 GradScaler 使用 save_state() 将上述所有内容保存到一个文件夹位置使用 load_state() 加载之前通过 save_state() 保存的状态通过使用 register_for_checkpointing()&#xff0c;可以注册自定义对象以便自动从前两个函数中存储或加载 …

epoll源码分析

epoll源码分析 主要数据结构epoll_create()函数实现ep_alloc()&#xff1a;初始化结构初始化eventpoll epoll_ctl()函数实现ep_insert回调函数的实现ep_ptable_queue_proc函数ep_poll_callback epoll_wait函数SYSCALL_DEFINE4(epoll_wait, ...)ep_pollep_send_events 主要数据结…

百度/迅雷/夸克,网盘免费加速,已破!

哈喽&#xff0c;各位小伙伴们好&#xff0c;我是给大家带来各类黑科技与前沿资讯的小武。 之前给大家安利了百度网盘及迅雷的加速方法&#xff0c;详细方法及获取参考之前文章&#xff1a; 刚刚&#xff01;度盘、某雷已破&#xff01;速度50M/s&#xff01; 本次主要介绍夸…

基于DeepLabv3+实现图像分割

目录 1. 作者介绍2. DeepLabv3算法2.1 DeepLabv3算法介绍2.2 DeepLabv3模型结构 3. 实验过程基于DeepLabv3实现图像分割3.1 VOC数据集介绍3.2 代码实现3.3 问题分析 4. 参考连接 1. 作者介绍 吴天禧&#xff0c;女&#xff0c;西安工程大学电子信息学院&#xff0c;2023级研究…

派派派森02

目录 1.容器 1.列表 2.元组 3.字符串 3.序列 4.集合 5.字典 2.数据容器通用操作 • max最大元素 • min最小元素 • 容器的通用转换功能 • 通用排序功能 3.字符串大小比较 4.函数中多个返回值 5.函数参数多种传递方式 1.位置参数 2.关键字参数 3.缺省参数 …

(函数)判断字符串元音字母(C语言)

一、运行结果&#xff1b; 二、源代码&#xff1b; # define _CRT_SECURE_NO_WARNINGS # include <stdio.h>//声明判断元音函数&#xff1b; void vowel(char a[100], char b[100]);int main() {//初始化变量值&#xff1b;char a[100] { 0 };char b[100] { 0 };//获取…