你注意到了以下HTTP响应中的某些内容吗?
HTTP/1.1 200 OK
Server: Some Server
Content-Type: text/html
Content-Length: 1337
<!DOCTYPE html>
<html>
<head><title>Some Page</title></head>
<body>
...
基于这段HTTP响应的小部分内容,可以假设这个web应用可能容易受到XSS漏洞攻击。
这怎么可能呢?你注意到什么了吗?
如果你对Content-Type
头有疑问,那你是对的。这里有一个小小的不完美:这个头缺少一个charset
属性。这听起来不是什么大问题,然而,这篇博文将解释攻击者如何利用这一点,通过有意识地改变浏览器假设的字符集,在网站中注入任意的JavaScript代码。
这篇博文的内容也在TROOPERS24会议[1]上展示过。我们会在录音可用时添加一个链接,并在X/Twitter[2]和Mastodon[3]上通知你。
字符编码[4]
HTTP响应中的常见Content-Type
头如下所示:
HTTP/1.1 200 OK
Server: Some Server
Content-Type: text/html; {% mark yellow %}charset=utf-8{% mark %}
...
charset
属性告诉浏览器使用UTF-8对HTTP响应体进行了编码。像UTF-8这样的字符编码定义了字符和字节之间的映射。当web服务器提供一个HTML文档时,它将文档的字符映射到相应的字节并在HTTP响应体中传输。这一过程将字符变为字节(编码):
当浏览器在HTTP响应体中接收到这些字节时,它可以将它们转换回HTML文档的字符。这一过程将字节变为字符(解码):
UTF-8只是现代浏览器必须支持的许多字符编码之一,根据HTML规范[5],还有许多其他编码,如UTF-16
、ISO-8859-xx
、windows-125x
、GBK
、Big5
等。浏览器必须知道服务器使用的是哪种编码,否则它无法正确解码HTTP响应体中的字节。
但如果在Content-Type
头中没有charset
属性或该属性无效,会发生什么?
在这种情况下,浏览器会在HTML文档本身中查找<meta>
标签。这个标签也可以有一个charset
属性,指示字符编码(例如,<meta charset="UTF-8">
)。这对于浏览器来说已经是一种平衡行为:为了读取HTML文档,它需要解码HTTP响应体。因此,它需要假设某种编码,解码HTTP体,查找<meta>
标签,并可能使用指示的字符编码重新解码体。
另一种较不常见的指示字符编码的方法是字节顺序标记[6]。这是一个特定的Unicode字符(U+FEFF
),可以放在字符串前面以指示字节的字节序和字符编码。它主要用于文件中,但由于这些文件可能通过web服务器提供,现代浏览器支持它。HTML文档开头的字节顺序标记甚至优先于Content-Type
头中的charset
属性和<meta>
标签。
总而言之,有三种常见的方法,浏览器用于确定HTML文档的字符编码,按优先级排序:
-
HTML文档开头的字节顺序标记 -
Content-Type
头中的charset
属性 -
HTML文档中的 <meta>
标签
缺少字符集信息[7]
字节顺序标记通常非常少见,Content-Type
头中的charset
属性并不总是存在或可能无效。此外 – 特别是对于部分HTML响应 – 通常没有指示字符编码的<meta>
标签。在这些情况下,浏览器没有任何关于使用哪种字符集的信息:
你见过这个错误信息吗?可能没有,因为它不存在。
类似于有缺陷的HTML语法,当从web服务器解析内容时,浏览器会尝试从缺少的字符集信息中恢复,并尽力而为。这种不严格的行为有助于提供良好的用户体验,但也可能为利用技术(如mXSS[8])打开大门。
对于缺少字符信息,浏览器会根据内容尝试做出有根据的猜测,这称为自动检测[9]。这类似于MIME类型嗅探[10],但在字符编码层面上进行操作。例如,Chromium的渲染引擎Blink使用紧凑编码检测(CED)库[11]来自动检测字符编码。从攻击者的角度来看,自动检测功能非常强大,正如我们将看到的那样。
到目前为止,我们已经熟悉了浏览器可能用来确定HTML文档字符编码的不同机制。但是攻击者如何利用这一点呢?
编码差异[12]
字符编码的目的是将字符转换为计算机可处理的字节序列。这些字节可以通过网络传输,并由接收方解码回字符。这样,发送方意图传输的完全相同的字符得以恢复:
这种方法只有在发送方和接收方同意使用相同的字符编码时才有效。如果在编码和解码时使用的字符编码之间存在不匹配,接收方可能会看到不同的字符:
这里我们所说的编码和解码之间的这种不匹配就是所谓的编码差异。
对于web应用程序来说,当用户控制的数据经过消毒以防止跨站脚本(XSS)漏洞时,这一点变得至关重要。如果浏览器假设的字符编码与web服务器意图的不一致,理论上可能会破坏消毒过程,导致XSS漏洞。
这本身并不是大新闻,甚至Google在2005年[13]也曾遇到过类似的问题。Google的404页面没有提供字符集信息,这可以通过插入UTF-7[14] XSS载荷来利用。在UTF-7中,像尖括号这样的HTML特殊字符与ASCII编码不同,可以利用这一点来绕过消毒:
+ADw-script+AD4-alert(1)+ADw-+AC8-script+AD4-
这很好地展示了这种编码的危险性,随后几年为了防止这样的安全问题,UTF-7被弃用。如今,HTML规范甚至明确禁止使用UTF-7[15]以防止XSS漏洞。
虽然仍然支持许多其他字符编码,但从攻击者的角度来看,这些大多数并不太有用。所有HTML特殊字符如尖括号和引号都是仅ASCII的,因为大多数字符编码与ASCII兼容,这些字符没有区别。即使对于UTF-16,由于其每个字符固定为两个字节,它与ASCII不兼容,通常也无法偷渡ASCII字符,因为它们的相应字节表示是相同的,只是带有尾随(小端)或前导(大端)零字节。
然而,有一个特别有趣的编码:ISO-2022-JP。
ISO-2022-JP[16]
ISO-2022-JP是一种日文字符编码,在RFC 1468[17]中定义。根据HTML标准[18],它是用户代理必须支持的官方字符编码之一。特别有趣的是,这种编码支持某些转义序列来在不同字符集之间切换。
例如,如果字节序列包含字节0x1b
、0x28
、0x42
,这些字节不会解码为字符,而是指示所有后续字节应使用ASCII解码。总共有四种不同的转义序列可用于在字符集ASCII、JIS X 0201 1976、JIS X 0208 1978和JIS X 0208 1983之间切换:
ISO-2022-JP的这个特性不仅提供了极大的灵活性,还可以破坏基本假设。还有一个问题:在撰写本文时,Chrome(Blink)和Firefox(Gecko)会自动检测这种编码。通常只需一次出现这些转义序列之一,就足以让自动检测算法认为HTTP响应体是用ISO-2022-JP编码的。
以下部分解释了两种不同的利用技术,当攻击者能够使浏览器假设ISO-2022-JP字符集时可以使用这些技术。根据攻击者的能力,例如,可以通过直接控制Content-Type
头中的charset
属性或通过HTML注入漏洞插入一个<meta>
标签来实现。如果web服务器提供了无效的charset
属性或根本没有提供,通常没有其他前提条件,因为攻击者可以轻松地通过自动检测将字符集切换到ISO-2022-JP。
技术1:否定反斜杠转义[19]
这种技术的场景是用户控制的数据被放置在JavaScript字符串中:
假设一个网站接受两个查询参数,分别是search
和lang
。第一个参数在纯文本上下文中反映,第二个参数(lang
)插入到JavaScript字符串中:
search
参数中的HTML特殊字符经过HTML编码,而lang
参数通过转义双引号("
)和反斜杠()进行适当的消毒。因此,不可能跳出字符串上下文并注入JavaScript代码:
ISO-2022-JP的默认模式是ASCII。这意味着所有接收到的HTTP响应体字节都使用ASCII解码,生成的HTML文档看起来像我们预期的那样:
现在,假设攻击者在search
参数中插入了切换到JIS X 0201 1976字符集的转义序列(0x1b
、0x28
、0x4a
):
浏览器现在会使用JIS X 0201 1976解码此转义序列后的所有字节:
如我们所见,这仍然会导致与之前相同的字符,因为JIS X 0201 1976主要与ASCII兼容。然而,如果我们仔细查看其代码表[20],我们会注意到有两个例外(以黄色突出显示):
字节0x5c
映射到日元符号(¥
),字节0x7e
映射到上划线(‾
)。这与ASCII不同,在ASCII中,0x5c
映射到反斜杠(),
0x7e
映射到波浪号(~
)。
这意味着,当web服务器尝试在lang
参数中用反斜杠转义双引号时,浏览器不再看到反斜杠,而是看到日元符号:
因此,插入的双引号实际上指定了字符串的结束,允许攻击者注入任意JavaScript代码:
虽然这种技术非常强大,但它仅限于在JavaScript上下文中绕过消毒,因为反斜杠字符在HTML中没有特殊含义。下一节解释了一种更高级的技术,可以在纯HTML上下文中应用。
技术2:破坏HTML上下文[21]
这种技术的场景是攻击者可以控制两个不同HTML上下文中的值。一个常见的用例是支持markdown的网站。例如,让我们考虑以下markdown文本:
生成的HTML代码如下所示:
这种技术的关键在于攻击者可以控制两个不同HTML上下文中的值。在本例中,这些上下文是:
-
属性上下文(图像描述/来源) -
纯文本上下文(图像周围的文本)
默认情况下,ISO-2022-JP处于ASCII模式,浏览器看到的HTML文档如预期:
现在,假设攻击者在第一个图像描述中插入了切换字符集到JIS X 0208 1978的转义序列:
这使得浏览器使用JIS X 0208 1978解码所有后续字节。这个字符集使用固定的每个字符2个字节,并且不与ASCII兼容。这有效地破坏了HTML文档:
然而,可以在两幅图像之间的纯文本上下文中插入第二个转义序列,将字符集切换回ASCII:
这样,所有后续字节再次使用ASCII解码:
然而,检查HTML语法时,我们会注意到一些变化。第二个img
标签的开始现在是alt
属性值的一部分:
原因是两次转义序列之间的4个字节使用JIS X 0208 1978解码。这也消耗了属性值的结束双引号:
此时,第二个图像的src
属性值不再是属性值。因此,攻击者可以用JavaScript错误处理程序替换此值:
这再次允许攻击者注入任意JavaScript代码。
总结
在这篇博客文章中,我们强调了在提供HTML文档时提供字符集信息的重要性。缺乏字符集信息可能导致严重的XSS漏洞,特别是当攻击者能够改变浏览器假定的字符集时。
我们详细说明了浏览器如何确定用于解码HTTP响应体的字符集,并解释了攻击者可能利用ISO-2022-JP字符编码将任意JavaScript代码注入网站的两种不同技术。
虽然我们认为缺少字符集信息是实际的漏洞,但浏览器的自动检测机制大大增加了其影响。因此,我们希望浏览器能够按照我们的建议禁用自动检测机制——至少对于ISO-2022-JP字符编码。
原文始发于微信公众号(3072):通杀所有服务端XSS Sanitizer的bypass技术分析