跳转至

利用编码进行混淆攻击

无论是客户端还是服务器,都在系统之间传输数据时使用各种不同的编码方式。当它们真正需要使用这些数据时,通常必须先进行解码处理。具体的解码步骤取决于数据出现的上下文环境。例如,查询参数通常需要在服务器端进行 URL 解码,而 HTML 元素的文本内容则可能在客户端进行 HTML 解码处理。

在构建攻击时,你应该考虑 payload 究竟会被注入到哪个位置。如果你能够根据这个上下文来推断输入数据是如何被解码的,那么你就有可能找到表示相同 payload 的其他方法。

注入攻击通常涉及注入含有可识别模式的 payloads,例如 HTML 标签、JavaScript 函数或 SQL 语句。由于这些 payloads 的输入几乎从不包含用户提供的代码或标记,因此网站通常会采取防御措施来阻止包含这些可疑模式的请求。

不过,这类输入过滤器也需要对输入数据进行解码,以判断其是否安全。从安全角度来看,当检查输入数据时,所执行的解码过程必须与后端服务器或浏览器在最终使用这些数据时执行的解码过程一致。任何差异都可能导致攻击者通过采用不同的编码方式,将有害载荷绕过过滤器,而这些编码差异之后会被自动清除。

URL编码混淆

在 URL 中,有一些被保留下来的字符具有特殊含义。例如,通配符( & )被用作分隔查询字符串中参数的分隔符。不过,基于 URL 的输入可能会因其他原因包含这些字符。假设有一个参数包含了用户的搜索查询,如果用户输入的是类似“Fish & Chips”这样的查询,会发生什么情况呢?

浏览器会自动对那些可能导致解析器产生歧义的字符进行编码处理。通常的做法是将这些字符替换为 % 符号,并加上它们的两位十六进制代码表示。

Text Only
[...]/?search=Fish+%26+Chips

这样就能确保使用特殊符号时不会将其误认为是分隔符。

Note

虽然空格字符可以被编码为 %20 ,但通常在实际情况中,它会被表示为一个加号( + ),就像上面的例子一样。

所有基于 URL 的输入在被赋值给相关变量之前,都会在服务器端进行自动的 URL 解码处理。这意味着,对于大多数服务器来说,查询参数中的类似 %22 、 %3C 和 %3E 这样的序列,实际上与 " 、 < 和 > 这些字符是等效的。换句话说,你可以通过 URL 注入 URL 编码的数据,这些数据通常仍能被后端应用程序正确解析。

偶尔,你会发现 WAF 等安全机制在解码用户输入的 URL 时存在不足。在这种情况下,你可以通过对黑名单中的字符或单词进行编码,从而将恶意代码悄悄植入后端应用程序中。例如,在 SQL 注入攻击中,你可以将关键词进行编码处理,使得 SELECT 变成 %53%45%4C%45%43%54 等等。

双重URL编码

由于某种原因,一些服务器会在接收到的任何 URL 上进行两轮解码操作。这本身并不一定是个问题,因为只要安全机制在验证输入时也能执行双重解码操作即可。不过,这种差异使得攻击者可以通过两次编码的方式将恶意输入悄悄传递到后端代码中。

假设您试图通过查询参数注入一种标准的 XSS 攻击代码,比如 <img src=x onerror=alert(1)> 。在这种情况下,URL 可能看起来像这样:

Text Only
[...]/?search=%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3E

在检查该请求时,如果 Web 应用防火墙采用标准的 URL 解码方式,那么它很容易识别出这个已知的恶意负载。这样一来,该请求就永远不会被传递到后端服务器。但是,如果采用双重编码的方式来注入恶意代码呢?实际上,这意味着 % 这样的字符会被替换为 %25 。

Text Only
[...]/?search=%253Cimg%2520src%253Dx%2520onerror%253Dalert(1)%253E

由于 WAF 只解码这一次,它可能无法识别出该请求具有危险性。如果后端服务器随后再次解码这个输入参数,那么 payload 就成功被注入到系统中了。

HTML编码

在 HTML 文档中,有一些字符需要被特殊处理或编码,以避免浏览器将其误认为是标记的一部分。实现这一目的的方法是:用一个引用符号来代替这些字符,该引用符号以&符号开头,并以分号结尾。在许多情况下,可以使用某个名称作为引用符号。例如,&colon;这样的序列就代表了一个冒号字符。

或者,也可以使用字符的十进制或十六进制代码点来表示引用,分别表示为&#58;和&#x3a;

在 HTML 的某些特定位置,比如元素的文本内容或属性的值,浏览器在解析文档时会自动解码这些引用。当在这些位置注入代码时,你可以利用这一点来混淆恶意代码,使其无法被服务器端的防御机制检测到。这样,恶意代码就能隐藏起来,从而实施客户端攻击。

如果你仔细查看之前例子中的 XSS 恶意代码,会注意到该恶意代码被嵌入到了一个 HTML 属性中,具体来说就是 onerror 事件处理程序。如果服务器端的检查是专门寻找 alert() 这个恶意代码的,那么如果你对其中一些字符进行了 HTML 编码处理,他们可能会忽略这个恶意代码。

Text Only
<img src=x onerror="&#x61;lert(1)">

当浏览器渲染页面时,会解码并执行注入到页面中的恶意代码。

前导零字符

有趣的是,在使用十进制或十六进制格式的 HTML 编码时,可以选择在字符编码前添加任意数量的零。不过,一些 Web 应用防火墙和其他输入过滤器在处理这种情况时存在不足。

如果经过 HTML 编码后,你的有效载荷仍然被阻止了,那么你可以尝试在代码点前面加上几个零来规避过滤机制:

JavaScript
<a href="javascript&#00000000000058;alert(1)">Click me</a>

XML编码

XML 与 HTML 密切相关,两者都支持使用相同的数字转义序列来进行字符编码。这使得你可以在元素的文本内容中包含特殊字符,而不会破坏语法结构。这一点在通过基于 XML 的输入来测试 XSS 漏洞时非常有用。

即使您不需要对特殊字符进行编码以避免语法错误,您仍然可以利用这种特性来混淆 payload 内容,就像使用 HTML 编码一样。不同的是,这里的 payload 是由服务器自己进行解码的,而不是由浏览器在客户端进行解码。这一点对于绕过 Web 安全防护机制和其他过滤器非常有用,因为这些机制可能会检测到与 SQL 注入攻击相关的关键词,从而阻止您的请求。

XML
<stockCheck>
    <productId>
        123
    </productId>
    <storeId>
        999 &#x53;ELECT * FROM information_schema.tables
    </storeId>
</stockCheck>

Unicode转义

Unicode 转义序列由前缀 \u 和四位数的十六进制代码组成。例如, \u003a 表示冒号字符。ES6 还支持一种新的 Unicode 转义形式,即使用花括号形式: \u{3a} 。

在解析字符串时,大多数编程语言都会将这些 Unicode 转义符进行解码。这包括浏览器所使用的 JavaScript 引擎。在将恶意代码注入到字符串环境中时,你可以利用 Unicode 转义符来混淆客户端端的恶意负载,就像我们在上面的例子中通过 HTML 转义符所做的那样。

例如,假设您试图利用 DOM XSS 漏洞,其中您的输入作为字符串传递给 eval() sink。如果最初的尝试被阻止,可以尝试如下方式之一来转义其中一个字符:

Text Only
eval("\u0061lert(1)")

由于这部分内容是在服务器端进行编码处理的,因此可能直到浏览器再次解码时才会被注意到。

Note

在字符串内部,你可以像这样对字符进行转义。不过,在字符串外部,对某些字符进行转义会导致语法错误。例如,括号的开闭符号就属于需要转义的类型。

值得注意的是,ES6 风格的 Unicode 转义方式还允许使用前导零字符。因此,一些 Web 应用可能会利用我们用于 HTML 编码的相同技术被轻易欺骗。例如:

JavaScript
<a href="javascript:\u{00000000061}alert(1)">Click me</a>

16进制转义

在将内容注入字符串上下文时的另一种方法是使用十六进制转义字符。这种转义方式是通过在字符前面加上 \x 来表示特定的字符。例如,小写字母 a 实际上是由 \x61 来表示的。

就像 Unicode 转义字符一样,只要将输入视为字符串进行解析,这些字符就会在客户端被解码。

Text Only
eval("\x61lert")

请注意,有时也可以使用类似的方法对 SQL 语句进行混淆处理,比如使用前缀 0x 来编码某些字符。例如, 0x53454c454354 可能会被解码为 SELECT 这个关键词。

8进制转义

八进制转义方式与十六进制转义方式类似,只不过字符引用使用的是八进制编号系统,而非十六进制编号系统。这些前缀都以一个单独的反斜杠开头,例如,小写字母 a 实际上表示为 \141 。

Text Only
eval("\141lert(1)")

混合多种编码

需要注意的是,你可以结合使用不同的编码方式,将有效载荷隐藏在多层混淆处理之后。请看下面的示例中的 javascript: URL:

JavaScript
<a href="javascript:&bsol;u0061lert(1)">Click me</a>

浏览器会先对 &bsol; 进行 HTML 解码,结果会得到一个反斜杠字符。这样,原本可能是任意字符的 u0061 就被转换为 Unicode 转义字符 \u0061 了。

JavaScript
<a href="javascript:\u0061lert(1)">Click me</a>

接下来,这个编码内容会被进一步解码,从而形成一个有效的 XSS 攻击载荷。

JavaScript
<a href="javascript:alert(1)">Click me</a>

显然,要成功以这种方式注入负载,就必须清楚地了解对输入数据进行哪些解码操作,以及这些操作的执行顺序。

SQL中的CHAR函数

虽然这并不属于严格的编码方式,但在某些情况下,你可以利用 CHAR() 函数来混淆 SQL 注入攻击。该函数接受一个十进制或十六进制代码点,并返回相应的字符。十六进制代码必须以 0x 作为前缀。例如, CHAR(83) 和 CHAR(0x53) 都会返回大写字母 S 。

通过连接返回的值,可以使用这种方法来隐藏被屏蔽的关键词。例如,即使 SELECT 被列入了黑名单,以下的注入行为最初看起来并不危险:

Text Only
CHAR(83)+CHAR(69)+CHAR(76)+CHAR(69)+CHAR(67)+CHAR(84)

不过,当应用程序将这部分内容作为 SQL 语句进行处理时,就会动态生成 SELECT 关键字,并执行所插入的查询语句。