Java代码审计之命令注入

  • A+

漏洞成因

  JavaWeb中,在服务端主要通过调用Runtime.getRuntime.exec()方法或者ProcessBuilder.start()来执行命令。例如下面的代码,通过Runtime.getRuntime.exec()执行了pwd命令:

java
try {
Process process = Runtime.getRuntime().exec(new String[]{"/bin/sh","-c","pwd"});
InputStream is = process.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}

  在上面的前提下,满足如下条件的话很可能存在命令注入的风险:

  • 传入exec()或者ProcessBuilder()的参数用户前端可控
  • 服务端未对传入参数进行检查

常见业务场景

  业务逻辑处理时会直接拼接用户可控参数,然后去执行系统命令或者拼凑回调函数代码达到对应的业务效果。常见的场景如下:

  • 截屏操作(例如调用系统命令执行PhantomJs并对其传参实现截图)
  • 图片处理(例如通过命令进行文件的复制、剪切、删除等)
  • 大文件压缩解压(例如zip、unzip命令)
  • 文件转PDF
  • 网站监测(例如常见的curl、ping、telnet等)
  • 邮件发送(mail)
  • ......

审计要点

  Java中命令执行并不是通过shell实现的,若没有手动创建shell来执行命令,即使是对应命令参数部分可控时,也无法使用;、&&等特殊符号进行命令注入的。

图片.png

  当存在创建shell时,也需要根据相关可控输入点进行检查。只有满足特殊场景才能达到命令注入的效果。

ProcessBuilder

  ProcessBuilder的构造函数是一个字符串列表或者数组。列表中第一个参数是可执行命令程序,其他的是命令行执行是需要的参数。

  • 传入的参数是字符数组

  若部分可控且没有创建shell操作的话是没法进行命令注入的。例如:

java
String path = request.getParameter("path");
String[] cmd = new String[] {"ls","-lh",path};
ProcessBuilder builder= new ProcessBuilder(cmd);

  这里使用;闭合然后写入id命令,解析时会把;id也当成ls命令路径参数的一部分进行解析,最后会抛出No such file or directory异常。

  若存在创建shell操作且部分可控的话是可以命令注入的。例如:

java
String path = request.getParameter("path");
String[] cmd = new String[] {"/bin/sh","-c","ls -lh "+path};
ProcessBuilder builder= new ProcessBuilder(cmd);

  这里首先创建了一个/bin/sh shell,然后-c以后后面的ls -lh等均作为其参数传递。所以使用;闭合然后写入id命令是可以达到命令注入的效果的。

  • 传入的参数是List集合

  跟传入字符数组类似,若部分可控且没有创建shell操作的话是没法进行命令注入的。若创建shell且参数可控,则有可能存在命令注入问题:

java
String path = request.getParameter("path");
List<String> commands=new ArrayList();
commands.add("/bin/sh");
commands.add("-c");
commands.add("ls -lh "+path);
ProcessBuilder builder= new ProcessBuilder(commands);

  这里首先创建了一个/bin/sh shell,然后-c以后后面的ls -lh等均作为其参数传递。所以使用;闭合然后写入id命令是可以达到命令注入的效果的。

  • 传入的参数为字符

  直接传入字符串也是可以的,完全可控的情况下是可以进行命令注入的:

java
String command = request.getParameter("command");
ProcessBuilder builder= new ProcessBuilder(command);
builder.redirectErrorStream(true);
Process process = builder.start();

  但是只能执行不需要参数的命令,例如pwd、whoami等:

图片.png

command_inject2

  多个字符串参数的情况与字符数组的是类似的:

java
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", "myArg1");
//等价于传入了字符数组String[] cmd = new String[] {"/bin/sh","-c","myArg1"};若myArg1可控的情况下可以存在命令注入

Runtime.getRuntime.exec()

  其可接受一个单独的字符串,这个字符串是通过空格来分隔可执行命令程序和参数的;也可以接受字符串数组参数。本质上还是通过调用ProcessBuilder来真正执行操作的。

  • 若传入的参数为字符数组,情况与ProcessBuilder一致

例如下面手动创建shell的场景:

java
Process process = Runtime.getRuntime().exec(new String[]{"/bin/sh","-c","pwd"});

  • 传入的参数为字符

例如如下场景:

java
Process process = Runtime.getRuntime().exec(command);

  查看底层实现,也是通过切割字符串的方式转换成字符数组然后进行调用:

java
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);//通过tbrf空格等进行字符串分割
String[] cmdarray = new String[st.countTokens()];
for(int i=0;st.hasMoreTokens();i++){
cmdarrayp[i] = st.nextToken();
}
return exec(cmdarray,envp,dir);
}

  若传入的参数完全可控,肯定是可以进行命令注入的,例如:

java
String command = request.getParameter("command");
Process process = Runtime.getRuntime().exec(command);

  若部分可控且没有创建shell操作的话是没法进行命令注入的。

java
String url = request.getParameter("url");
Process process = Runtime.getRuntime().exec("curl "+url);

  若存在创建shell操作且部分可控的话情景与传入字符数组的情况类似(对传入的String进行了切割并且转换成了字符数组),也就是说情况与ProcessBuilder一致

java
String filename = request.getParameter("filename");
Process process = Runtime.getRuntime().exec("sh -c ./shell/"+filename);

  例如上述代码是通过前端传入的filename,调用shell去执行对应的sh脚本。正常的情况转换成字符数组如下:

sh -c ./shell/back-up.sh ==>["sh","-c","./shell/back-up.sh"]

  因为这里可控点在-c传入的参数命令后,所以可以进行命令注入。虽然有空格可能导致被截断,但是可以使用类似${IFS}代替空格进行深入利用。

总结

  • 若命令参数完全可控,可以注入任意命令执行(ProcessBuilder只能执行无参命令)
  • 不存在创建shell,无法结合;、&&等特殊符号进行命令注入
  • 存在创建shell
  • 存在参数注入,可以进行命令注入(例如可控点在-c传入的参数命令后)

修复建议

  • 实际业务场景中可以使用相关的API代替Runtime和ProcessBuilder执行命令来实现,例如文件的操作可以使用相关的API函数。
  • 对输入数据进行净化和检查,例如将任何引起参数或命令结束的字符进行转义,转义参数中所包含的特殊字符,使其无法对其当前执行进行截断,从而实现防范命令注入攻击的目的。
  • 使用白名单的方式限制对应的参数值,例如只允许输入字符数字,下划线_,或者仅仅只能是邮箱、IP等。

相关推荐: 浅谈短信业务中的格式化漏洞

关于短信业务   短信服务(Short Message Service)是指通过调用短信发送API,将指定短信内容发送给指定手机用户。短信的内容多用于企业向用户传递验证码、系统通知、会员服务等信息。大型网站的业务实现中都提供有手机短信业务功…