跳到内容

OS 命令注入防御速查表

简介

命令注入(或 OS 命令注入)是一种注入类型,当软件使用受外部影响的输入构造系统命令时,未能正确地将输入中可能修改初始预期命令的特殊元素进行中和处理。

例如,如果提供的值是

calc

在Windows命令提示符下输入时,会显示应用程序计算器

然而,如果提供的值已被篡改,现在是

calc & echo "test"

执行时,它会改变初始预期值的含义。

现在,计算器应用程序和值test都显示了

CommandInjection

如果受损进程不遵循最小权限原则,并且攻击者控制的命令最终以特殊系统权限运行,从而增加损害程度,问题将更加严重。

参数注入

每一次 OS 命令注入也是一次参数注入。在这种攻击中,用户输入可以作为参数在执行特定命令时被传递。

例如,如果用户输入通过转义函数来转义某些字符,如&|;等。

system("curl " . escape($url));

这将阻止攻击者运行其他命令。

但是,如果攻击者控制的字符串包含curl命令的附加参数

system("curl " . escape("--help"))

现在,当上述代码执行时,它将显示curl --help的输出。

根据所使用的系统命令,参数注入攻击的影响范围可以从信息泄露到关键的远程代码执行

主要防御措施

防御选项1:避免直接调用OS命令

主要的防御措施是避免直接调用OS命令。内置库函数是OS命令的绝佳替代品,因为它们不能被操纵来执行其预期之外的任务。

例如,使用mkdir()而不是system("mkdir /dir_name")

如果您使用的语言有可用的库或API,这是首选方法。

防御选项2:针对每个OS对添加到OS命令中的值进行转义

待完善。

例如,请参见PHP中的escapeshellarg()

escapeshellarg()会将用户输入用单引号括起来,因此如果格式错误的用户输入是& echo "hello",最终的输出将是calc '& echo "hello"',它将被解析为calc命令的一个单独参数。

尽管escapeshellarg()可以防止OS命令注入,但攻击者仍然可以向命令传递单个参数。

防御选项3:参数化结合输入验证

如果无法避免调用包含用户提供的系统命令,则应在软件内部使用以下两层防御来防止攻击

第一层

参数化: 如果可用,使用结构化机制自动强制数据和命令分离。这些机制可以帮助提供相关的引用和编码。

第二层

输入验证: 命令的值和相关参数都应该被验证。对实际命令及其参数的验证有不同的程度

  • 对于使用的命令,必须根据允许的命令列表进行验证。
  • 对于这些命令使用的参数,应使用以下选项进行验证
    • 正向或白名单输入验证:明确定义允许的参数。
    • 白名单正则表达式:定义了一系列良好、允许的字符和字符串的最大长度。确保元字符(如注A中指定的字符)和空白符不包含在正则表达式中。例如,以下正则表达式只允许小写字母和数字,且不包含元字符。长度也限制在3-10个字符:^[a-z0-9]{3,10}$
  • 根据POSIX准则10第一个非选项参数的--参数应作为分隔符,表示选项的结束。任何后续参数都应被视为操作数,即使它们以“-”字符开头。例如,curl -- $url将防止参数注入,即使$url格式错误并包含额外参数。

注 A

& |  ; $ > < ` \ ! ' " ( )

额外防御措施

在主要防御措施、参数化和输入验证之外,我们还建议采用所有这些额外的防御措施,以提供深度防御。

这些额外的防御措施是

  • 应用程序应使用完成必要任务所需的最低权限运行。
  • 如果可能,创建具有有限权限的独立账户,这些账户仅用于单一任务。

代码示例

Java

在Java中,使用ProcessBuilder,命令必须与其参数分开。

关于Java的Runtime.exec方法行为的注意事项

许多网站会告诉你Java的Runtime.exec与C语言的系统函数完全相同。这是不正确的。两者都允许你调用新的程序/进程。

然而,C语言的系统函数将其参数传递给shell (/bin/sh) 进行解析,而Runtime.exec试图将字符串分割成单词数组,然后执行数组中的第一个单词,其余单词作为参数。

Runtime.exec在任何时候都不会尝试调用shell,也不支持shell元字符.

关键区别在于,shell提供的许多可能用于恶意行为的功能(使用&&&|||等连接命令,重定向输入和输出)最终只会作为参数传递给第一个命令,很可能导致语法错误或被抛出为无效参数。

测试上述注意事项的代码

String[] specialChars = new String[]{"&", "&&", "|", "||"};
String payload = "cmd /c whoami";
String cmdTemplate = "java -version %s " + payload;
String cmd;
Process p;
int returnCode;
for (String specialChar : specialChars) {
    cmd = String.format(cmdTemplate, specialChar);
    System.out.printf("#### TEST CMD: %s\n", cmd);
    p = Runtime.getRuntime().exec(cmd);
    returnCode = p.waitFor();
    System.out.printf("RC    : %s\n", returnCode);
    System.out.printf("OUT   :\n%s\n", IOUtils.toString(p.getInputStream(),
                      "utf-8"));
    System.out.printf("ERROR :\n%s\n", IOUtils.toString(p.getErrorStream(),
                      "utf-8"));
}
System.out.printf("#### TEST PAYLOAD ONLY: %s\n", payload);
p = Runtime.getRuntime().exec(payload);
returnCode = p.waitFor();
System.out.printf("RC    : %s\n", returnCode);
System.out.printf("OUT   :\n%s\n", IOUtils.toString(p.getInputStream(),
                  "utf-8"));
System.out.printf("ERROR :\n%s\n", IOUtils.toString(p.getErrorStream(),
                  "utf-8"));

测试结果

##### TEST CMD: java -version & cmd /c whoami
RC    : 0
OUT   :

ERROR :
java version "1.8.0_31"

##### TEST CMD: java -version && cmd /c whoami
RC    : 0
OUT   :

ERROR :
java version "1.8.0_31"

##### TEST CMD: java -version | cmd /c whoami
RC    : 0
OUT   :

ERROR :
java version "1.8.0_31"

##### TEST CMD: java -version || cmd /c whoami
RC    : 0
OUT   :

ERROR :
java version "1.8.0_31"

##### TEST PAYLOAD ONLY: cmd /c whoami
RC    : 0
OUT   :
mydomain\simpleuser

ERROR :

不正确的使用方法

ProcessBuilder b = new ProcessBuilder("C:\DoStuff.exe -arg1 -arg2");

在这个例子中,命令和参数作为单个字符串传递,这使得操纵该表达式和注入恶意字符串变得容易。

正确的使用方法

这是一个启动进程并修改工作目录的示例。命令和每个参数都是单独传递的。这使得验证每个项变得容易,并降低了插入恶意字符串的风险。

ProcessBuilder pb = new ProcessBuilder("TrustedCmd", "TrustedArg1", "TrustedArg2");

Map<String, String> env = pb.environment();

pb.directory(new File("TrustedDir"));

Process p = pb.start();

.Net

请参见DotNet安全速查表中的相关详细信息

PHP

在PHP中,使用escapeshellarg()escapeshellcmd(),而不是exec()system()passthru()

命令注入漏洞描述

如何避免漏洞

如何审查代码

如何测试

外部参考