最近在复现&分析 Java 领域历史相关漏洞,学习学习 Java 框架漏洞代码审计思路。分析原理少不了调试,然而每次都得搭建漏洞环境,又是构建maven, 又是引入依赖,挺繁琐的。Github 有很多其他同行利用 Docker 搭建好的漏洞环境镜像,一键拉取运行即可, 但是问题来了, 运行在 docker 容器里的程序我怎么断点调试呢?手上没有构建镜像时的源代码,该怎么办呢?
Docker 容器里面一般运行的是 java 打包的 jar 包,那么就等于要解决怎么远程调试正在运行的 jar 包?我们先看看本地项目市如何 Debug 的,仔细的你是否察觉到平时IDEA上Debug本地项目的时候都会出现的一行信息?
为什么 Debug 会出现这么一行信息呢?由于博主学过一些 JVM 虚拟机相关知识 (Java 开发必须了解 JVM),IDEA Debug 大致过程如下:
以上过程被称为 JPDA调用体系。
JPDA(Java Platform Debugger Architecture)是 sun 公司开发的 java平台调试体系, 它主要有三个层次组成,即 Java 虚拟机工具接口 (JVMTI) ,Java 调试线协议(JDWP)以及 Java 调试接口(JDI)。
- JVMTI (JVMDI): jdk1.4 之前称为JVMDI,之后改为了JVMTI,它是虚拟机的本地接口,其相当于 Thread 的 sleep、yield native 方法
- JDWP(Java Debug Wire Protocol):java调试网络协议,其描述了调试信息的格式,以及在被调试的进程(server)和调试器(client)之间传输的请求
- JDI:java调试接口,虚拟机的高级接口,调试器(client)自己实现 JDI 接口,比如 idea 等其他编译器。
综上我们知道了 IDEA 调试的原理大致如下:
1、先建立起了 socket 连接
2、将断点位置创建了断点事件通过 JDI 接口传给了 服务端(程序端)的 VM,VM 调用 suspend 将 VM 挂起
3、VM 挂起之后将客户端需要获取的 VM 信息返回给客户端,返回之后 VM resume 恢复其运行状态
4、客户端获取到 VM 返回的信息之后可以通过不同的方式展示给客户端
好了,讲了这么多,现在我们知道了本地调试其实也可以认为是远程调试,IDEA 通过 127.0.0.1:20256(端口随机)与 JVM 进行 socket 通信。那么这个端口到底怎么设置的?这就要搬出咱们的 jdk-8xxx-docs-all 官方完整文档 来查阅了。端口是由 JVM 创建的这毋庸置疑,所以直接点击最下层的 Java HotSpot Client and Server VM 进行查阅:
进入以后选择对应的操作系统 (当前需要运行的 Java 程序在什么操作系统上,以 Windows 为例):
跳转以后 Ctrl + f 搜索 Debug 关键字如下:
文档中详细描述了,启动 Java 程序之前,如果需要 Debug,需要添加参数:
-agentlib:jdwp=transport=dt_socket,server=y,address=xxxx
。adderss 填写自定义端口。
现在咱们知道 JVM 如何手动开通 Debug 端口了,但是又一个问题来了,IDEA 如何自定义连接 JVM 的呢 ?只要不会咱们就翻官方文档,官方文档往往会带来惊喜。具体如何连接,请自行前往查阅: Tutorial: Remote debug
由于咱们是引入的别人的docker镜像,咱们手上又没有构建docker镜像时的源代码,咱们最多只能提取 docker 容器中的 jar 包。突然想到我们平时 IDEA 引入第三方 jar 包,只要 Add as Library 操作,jar 包就被打开了,可以看到 “源代码”,并且 jar 包内的ClassName就可以被我们实例化调用,还可以在 jar 包的.class 文件里打断点进行调试。咱们不妨试试这样行不行得通? 事实上是可行的!
OK ,经过这一路上的思考过程,远程调试 Docker 中运行的 Java 程序也就 so easy 啦!
实现步骤:
1、在 docker-compose.yml 配置映射端口让 jvm debug 端口能外部访问。
2、在 docker-compose.yml 中使用 command 字段添加自定义启动命令:java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=xxxx -jar jar包名称.jar
。
3、容器启动后,从容器中把运行的 jar 包复制出来,新建一个文件夹 ,IDEA 点击 Open 打开这个文件夹, 复制粘贴 jar 包, 右键jar包,选择 Add as Lirary 添加到项目依赖库中,并在代码上打上几个断点。
4、 IDEA 配置 Remote Debug,点击 Debug 运行即可。
以 fastjson 反序列化漏洞docker镜像为例进行远程调试。
1、在 docker-compose.yml 配置映射端口让 jvm debug 端口能外部访问。
2、在 docker-compose.yml 中使用 command 字段添加自定义启动命令:java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=xxxx -jar jar包名称.jar
。
是不是突然发现自己不知道 jar 包名字叫啥? 咱们可以先启动容器,然后执行
docker ps --no-trunc
不截断输出完整的容器描述,就可以看到容器名称以及容器中的路径。
3、容器启动后,从容器中把运行的 jar 包复制出来,新建一个文件夹 (例:CVE-2017-18349),IDEA 点击 Open 打开 CVE-2017-18349 文件夹, 复制粘贴 fastjsondemo.jar, 右键jar包,选择 Add as Lirary 添加到项目依赖库中,并在代码上打上几个断点。
启动容器后,执行命令:
docker cp 容器id:jar包路径 目标路径
, 将 jar 复制出来。
4、 IDEA 配置 Remote Debug,点击 Debug 运行即可。
问题解决: 打开一个自己原先开发的项目,瞅一瞅引入的 jar 包结构是啥样的, 比较区别:
Project Structure,Modules->Dependencies 添加要调试的class文件的目录 BOOT-INF。
但是还是有问题,断点调试无法进入第三方依赖包中,我们需要把lib文件夹复制到根目录,并右键 Add as Library 。
最终远程断点调试的前期准备全部完成,可以快快乐乐的打断点了~
❗️❗️❗️如果你是一名Java开发人员,请注意如果生产环境下迫不得已需要远程调试,调试完一定要关闭JDWP服务,或者JDWP服务监听的端口不对公网开放。❗️❗️❗️
附一篇优秀的 JDWP 服务漏洞利用文章 ===> JDWP调试接口远程命令执行漏洞原理分析