📘 第十一章 Web的攻击技术
📄 一句话概括

HTTP 首部靠换行分隔。 用户输入里如果带 CRLF(%0d%0a),就能在响应里多造一行首部, 甚至多造一整段正文——这叫响应拆分。Demo 里服务端只想写一个 Location,结果写出了两个首部。

📝 本节和前面几节的关系

上一节的宿主是 shell,这一节的宿主是 HTTP 报文本身。换行在 HTTP 里是结构字符,不是空白。

一、换行就是结构

HTTP 报文长这样:一行状态行,若干行 名字: 值,一个空行,然后正文。 换行是唯一的分隔符——没有它,整个报文就是一坨没法解析的字符。 所以一旦用户输入里的换行能进入首部,他就会多出一行「首部」,因为解析器眼里那确实是一行首部。 常见的注入点:重定向的 Location、Set-Cookie 的值、Content-Type 里拼的文件名、 日志里回显的 User-Agent。凡是把输入写进首部的地方,都是候选。

二、一条 next 参数造出一行 Set-Cookie

Demo 里那个跳转接口直接写 Location: %s,next 来自 URL。正常传 /ok 没问题。 传 /ok%0d%0aSet-Cookie:%20role=admin,解出来的值是 /ok 加一个换行加 Set-Cookie: role=admin, 然后整段被写进 Location。 结果服务端回了 5 行(状态行加四个首部),第二条是 Set-Cookie: role=admin。 攻击者从「只能改一个跳转目标」变成了「能让服务器给我发任意 cookie」。 再补一个 %0d%0a%0d%0a,后面的字节就变成正文了——攻击者能往响应里塞任意 HTML。 这一招叫响应拆分,和老式缓存投毒是同一套思路:往共享缓存里塞一个假响应,让所有人都中。

三、修法:删掉换行,或者拒绝

修正版只做一件事:nxt.replace("\r", "").replace("\n", "")。 同一个请求,首部回到 4 行,Set-Cookie 消失了。 删掉是「尽量容错」的做法,更稳的是拒绝:输入里出现 CRLF 就回 400。 因为合法场景里永远不需要在首部值里放换行。 还有一层:现代框架大多在写首部时会自动拒绝 \r 和 \n,所以这类漏洞比以前少。 但自己拼字符串的地方,它还活着。 判断方法很朴素:搜代码里所有「把变量拼进首部」的地方,单个文件里通常不超过十处, 逐个看一遍就查完了。

四、动手 Demo:一个 next 参数,两个首部

本机起一个跳转接口,把 next 原样写进 Location。 先用正常值试一次,再把 %0d%0a 夹在中间试一次,最后加上过滤再试一次。

nxt = q.get(u'next', [u'/'])[0]
if clean:
    nxt = nxt.replace(u'\r', u'').replace(u'\n', u'')
head = (u'HTTP/1.1 302 Found\r\n'
        u'Location: %s\r\n'
        u'Content-Length: 0\r\n\r\n' % nxt)

实跑输出:

--------------------------------------------------------------------
HTTP 首部注入:一个换行就能多造一个首部
--------------------------------------------------------------------
  攻击者请求:GET /go?next=/ok%0d%0aSet-Cookie:%20role=admin

  【不处理换行】
    HTTP/1.1 302 Found
    Location: /ok
    Set-Cookie: role=admin
    Content-Length: 0
    Connection: keep-alive
      首部行数               5 行(含状态行)
      有没有 Set-Cookie      有

  【删掉换行】
    HTTP/1.1 302 Found
    Location: /okSet-Cookie: role=admin
    Content-Length: 0
    Connection: keep-alive
      首部行数               4 行(含状态行)
      有没有 Set-Cookie      没有

  第一段多出来一行 `Set-Cookie: role=admin`——它是被 `%0d%0a` 造出来的。
  服务端只想写一个 Location,结果写出了一个额外的首部。
  这一招叫 HTTP 首部注入,也叫响应拆分(CRLF injection)。

  同样的手法还能多造一整段正文:再补一个 `%0d%0a%0d%0a`,后面的字节就变成了响应体,页面内容直接被改写。

不处理时返回 5 行首部,Location: /ok 下面多出一行 Set-Cookie: role=admin, 并且被判定为「有 Set-Cookie」。 删掉换行之后回到 4 行,Location 变成 /okSet-Cookie: role=admin——拼成了一行,难看但安全。 载荷里的空格必须写成 %20,否则请求行会在空格处被切开,那半截参数根本到不了服务端。 这是个容易踩的细节。 这一节最值得记的一点:换行是 HTTP 的结构字符。 任何进入报文结构的用户输入,都必须先把换行处理掉。

30 秒实验

  1. 把载荷改成 %0d%0a%0d%0a<html>pwned</html>,看正文会不会被塞进去。
  2. 试试用 %0a(只有 LF)能不能绕过只删 \r 的过滤。
  3. 想一想:如果这个响应会被 CDN 缓存,攻击者能造成多大范围的危害?

常见误区

  1. 「只有 Location 能被注入」——任何写进首部的地方都算。
  2. 「URL 编码了就安全」——编码只是运输形式,服务端解出来还是换行。
  3. 「删掉 \r 就够了」——只认 LF(%0a)的解析器一样能被注入。
自测三问(点开对答案) 1. HTTP 首部注入的成因是什么? —— 用户输入里的 CRLF 被写进首部,解析器把换行当成结构,于是多出一行首部。 2. 响应拆分会造成什么后果? —— 攻击者能伪造任意首部甚至整段正文,配合缓存还能投毒。 3. 最稳的修法是什么? —— 拒绝含 CRLF 的输入(回 400),而不是只做删除。

HTTP 首部之外,邮件首部也是一样的毛病。

相关笔记