19 失效的输入检测(上):攻击者有哪些绕过方案?

你好,我是王昊天。今天我们来学习失效的输入检测,看看攻击者有哪些绕过方案。

在现实生活中,我们在乘坐一些交通工具时,需要经过安检,以防止有人携带危险物品,避免一些危害公众安全的行为。但是这种安全检查也不是万能的,比如进地铁站的时候不会检查我们衣服口袋里的物品。

对于一个交互的系统来说,同样也需要对输入进行安全检查,也同样很难做到万无一失。

交互系统的输入,可能来自用户的输入或者其他系统的传递。在系统获得预期内的输入信息之后,就会将它们当作参数,运行相应的命令来实现自己想要的功能。那么问题来了,如果忽略了对输入的验证或者验证得不够充分,在攻击者的恶意操作下,系统就会接收到预期之外的数据,进而随着命令的运行就让攻击者实现了自己想要的目标,进而产生难以想象的后果。

这,其实就是失效的输入检测。

根据检测技术的不同,失效的输入检测可以分为6种,它们分别是:不安全的输入检查、中间件的输入输出、不安全的映射、编码及转义、编码及混淆、WAF及绕过。

在接下来的2讲内容中,我会带你学习这6种常见的失效输入检测,是如何产生的,以及应该如何应对。

不安全的输入检查

不安全的输入检查产生的原因,其实很好理解,就是当一个产品需要接收数据的输入时,却没有正确地对这些输入进行验证。

为了抵御这个问题,解决方案也比较简单,开发者只需要对一些输入数据进行安全性验证就可以。但其中的难点在于,如果安全性验证不够充分,攻击者就可以将输入构造成安全人员意料之外的形式,导致系统接收到意料之外的恶意输入。

这是非常危险的,因为攻击者甚至可以借此实现任意命令的执行。

我们来看一个关于消费行为的例子:

public static final double price = 20.00;
# 用户可以自由指定购买商品的数量
int quantity = currentUser.getAttribute(“quantity”);
# 计算总价
double total = price * quantity;
chargeUser(total);

在这段代码中,商品单价的值用户是无法修改的,但没有对购买数量的值进行限制。这时候,如果攻击者提供一个负值,那么他就不用进行消费,反而还能获得相应的收入。

接下来,我们开始学习失效的输入检测的第二种情况:中间件的输入输出。

中间件的输入输出

通常情况下,一个系统会由多个组件构成。上游组件在接收到外部输入后,会将它传给中间件来构建部分命令、数据结构或记录,然后就将它们发送给下游组件。

需要注意的是,中间件并不能正确地处理这些输入中的特殊元素。这种失效的输入检测问题,就是中间件的输入输出问题。

我们直接看个例子。

int main(int argc, char** argv) {
  char cmd[CMD_MAX] = "/usr/bin/cat";
  strcat(cmd, argv[1]);
  system(cmd);
}

如果这个程序是以root权限运行的,那么对system()的调用也会以root权限执行。如果用户传入的参数是标准的文件名,那么调用会按预期工作。

但是,如果攻击者传递了一个恶意输入,比如一个; rm -rf /形式的字符串,那么对system()的调用也会因为缺少参数而无法执行cat命令,从而运行恶意命令,递归删除根分区的内容。

这个示例就是中间件没有对用户传入的恶意输入进行处理导致的。首先,在输入检查就存在问题,导致该输入没有被拦截。其次,中间件没有对这部分信息进行过滤处理,直接将接收到的恶意参数当成了命令来执行,造成了严重的后果。

到这里,我们已经学习了两种最直接的失效的输入检测风险类型,接下来我们再来学习一种更加隐蔽的风险种类,也就是不安全的映射。

不安全的映射

不安全的映射发生的场景是:当应用程序需要使用带有映射的外部输入来选择要执行的代码时,却没有充分验证这些外部输入是否合法,这时候攻击者就可以将恶意文件上传到应用会执行的位置。

这对于应用来说是毁灭性的漏洞,非常危险。我们再通过一个例子,来理解下这种漏洞是怎么产生的吧。

下面这个例子,显示了一个不使用映射的命令调度程序,它的代码书写方式看起来并不十分优雅:

String ctl = request.getParameter(“ctl”);
Worker ao = null;

// 判断是否ctl参数中是Add字符串
if (ctl.equals("Add"))
{
  ao = new AddCommand();
}
// 判断是否ctl参数中是Modify字符串
else if (ctl.equals("Modify"))
{
  ao = new ModifyCommand();
}
else {
  throw new UnknownActionError();
}
ao.doAction(request);

我们品味一番,可以发现上述代码写得属实不够优雅,而优秀的开发人员可能会使用映射的方式来进行代码重构,如下所示:

String ctl = request.getParameter("ctl");
Class cmdClass = Class.forName(ctl + "Command");
Worker ao = (Worker)cmdClass.newInstance();
ao.doAction(request);

重构后的这段代码,确实提供了许多优势:代码更加简洁了;if/else块也消失了;在不修改命令调度程序的情况下,也可以添加新的命令类型。

但是,重构后的代码有个漏洞。攻击者可以先利用Worker接口创建一个类,然后使用它们。这里创建的类是没有限制的,它是由攻击者控制的参数ctl所决定。攻击者可以利用创建的这个类去执行恶意命令。

编码及转义

软件为了与另一个组件通信,会准备要发送的消息。它的结构需要符合通信协议的要求,如果数据的编码或转义过程中发生丢失或者执行错误,就可能会导致消息的结构发生变化。

不正确的编码或转义,可能允许攻击者将发送的正常命令更改为恶意命令。大多数软件都会遵循双方规定的协议进行通信。通信消息可以为带有控制信息的原始数据。

这么说有些抽象,我们看一个具体的示例:

“GET/index.html HTTP/1.1”是一个结构化消息,其中包含一个命令(“GET”)和一个参数(“/index.html”)和有关正在使用的协议版本(“HTTP/1.1”)。

如果应用程序使用攻击者提供的输入来构建结构化消息,而没有正确编码或转义,那么攻击者就可以在这条消息中插入特殊字符,导致数据被解释为控制信息。因此,接收输出的组件,就将会执行错误的操作。

我们再通过一个示例,来看看涉及编码及转义的攻击方式是如何发生的。

现在有这么一个聊天应用程序,它的前端Web应用程序与后端服务器之间要进行通信。因为后端是不执行身份验证或授权的遗留代码,所以我们必须在前端必须实现这个功能。聊天协议规定只支持两个命令SAY和BAN,而且BAN命令只有管理员才可以使用。每个参数必须由一个空格分隔,原始输入经过URL编码,消息协议允许在一行中执行多个以分隔的命令。

我们先看后端的代码:

$inputString = readLineFromFileHandle($serverFH);
# generate an array of strings separated by the "|" character.
@commands = split(/\|/, $inputString);

foreach $cmd (@commands) {
  # separate the operator from its arguments based on a single whitespace
  ($operator, $args) = split(/ /, $cmd, 2);
  
  $args = UrlDecode($args);
  if ($operator eq "BAN") {
    ExecuteBan($args);
  }
  else if ($operator eq "SAY") {
    ExecuteSay($args);
  }
}

前端Web应用程序接收命令后,对其进行编码,然后发送到权限查看服务器执行授权的检查,然后再将命令发送给后端。

$inputString = GetUntrustedArgument("command");
($cmd, $argstr) = split(/\s+/, $inputString, 2);

/# removes extra whitespace and also changes CRLF's to spaces/
$argstr =~ s/\s+/ /gs;

$argstr = UrlEncode($argstr);
if (($cmd eq "BAN") && (! IsAdministrator($username))) {
die "Error: you are not the admin.\n";
}

/# communicate with file server using a file handle/
$fh = GetServerFileHandle("myserver");

print $fh "$cmd $argstr\n";

我们可以发现一个很明显的问题,虽然协议和后端都允许在一个请求中发送多个命令,但前端只打算发送一个命令。可是UrlEncode函数可能会留下字符。

也就是说,如果攻击者提供SAY hello world|BAN user12,前端会看到这是一个SAY命令,$argstr 就为hello world | BAN user12。由于命令是SAY,对BAN命令的检查会失败。前端会向后端发送URL编码的命令:

SAY hello%20world|BAN%20user12

后端就会把这个解析为如下两条命令来运行:

SAY hello world
BAN user12

但是请注意,如果前端使用正确的编码将 编码为%7C ,那么后端将只处理一个命令。

这就是一个典型的编码错误导致的输入验证失效的例子。

编码及混淆

除了编码及转义,攻击者还可以通过编码混淆攻击来逃避输入的检查,为攻击区注入有害负载。这种攻击方式,就叫做编码及混淆。

客户端和服务器会使用各种不同的编码在系统之间传递数据,而当它们想要使用数据时就需要首先对其进行解码。

在构建攻击时,我们需要考虑有害负载的注入位置。如果可以根据关联环境推断出输入是如何被解码的,那么我们就可以知道,要用什么方式对有害负载进行编码。

在URL中,有一系列具有特殊含义的保留字符。例如,&用作分隔符,它可以分隔查询字符串中的参数。基于URL的输入可能包含这些字符,比如用户搜索Fish & Chips之类的内容会发生什么呢?

浏览器会自动对任何可能导致解析器歧义的字符进行URL编码。这意味着,用%字符和它们的二位十六进制代码替换它们,成为这样[…]/?search=Fish+%26+Chips,来确保& 不会被误认为是分隔符。

任何基于URL的输入在分配给相关变量之前,都会在服务器端自动进行URL解码。这意味着,就大多数服务器而言,查询参数中的%22%3D%3E等序列分别与<>字符同义。也就是说,我们可以通过URL注入URL编码的数据,它通常仍会被后端应用程序正确解释。

有时,我们可能会发现,WAF等在检查你的输入时,无法正确地对你的输入进行 URL解码。在这种情况下,我们只需对列入黑名单的任何字符或单词进行编码,就可以将有害负载绕过检测,发送给后端应用程序,实现攻击行为。

在XSS注入中,我们经常会需要输入