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

跳转接口如果不校验目标,就是一个「用你的域名发出去的通行证」。 Demo 里 next=https://evil.example/steal 直接回了 302,Location 指向外站。 而 //evil.example 这种写法能绕过「必须 http 开头」的黑名单。

📝 本节和前面几节的关系

上一节泄露的是内部信息,这一节借的是你的域名的可信度。

一、为什么这算漏洞

登录成功之后跳回原来要去的页面,这是个很常见的需求,所以接口往往写成 ?next=<目标地址>。 如果服务端不校验,这个接口就变成了「你给我一个地址,我让你的浏览器跳过去」。 听起来没什么,但关键在于:跳转是从你的域名发起的。 钓鱼链接写成 your-site.com/go?next=https://evil.example,用户看到域名是你,就不会怀疑。 点下去之后落到一个假的登录页,输的账号密码直接进了攻击者的库。 它还常被用来绕过邮件和聊天软件的外链检测——那些系统看到域名可信就放行。

二、两种绕过写法

最直白的载荷是 https://evil.example/steal,Demo 里朴素版回 302,Location 就是这个地址。 稍微聪明一点的过滤器会要求「必须以 http 开头」或者「必须包含本站域名」。 于是有了第二种写法://evil.example/steal。 它不以 http 开头,所以过了「http 开头」这条检查; 但浏览器解析它时,// 表示「同协议、另一个主机」——照样跳到外站。 还有更绕的:https://[email protected]/(用户信息那段)、 https://your-site.com.evil.example/(子域名伪装)。 黑名单永远列不全,因为这些写法是浏览器解析规则的一部分。

三、白名单,而且只认相对路径

Demo 的修正版只做一件事:目标必须以 / 开头、不能以 // 开头、不能含冒号。三条件一起,才允许跳。 结果三行里第一行 /dashboard 还是 302, 后两行 https://evil.example 和 //evil.example 都变成 400。 更稳的做法是根本不让外部传地址:参数只给一个「页面标识」,服务端自己映射到路径。 这样攻击者连拼接的机会都没有。 最后一条:如果确实需要跳到外站(比如第三方登录回跳),就维护一份明确的外站白名单, 并且跳转前给用户看一个确认页。

四、动手 Demo:三种目标,两种被放行

本机起一个跳转接口,把 next 直接写进 Location。 分别传站内路径、外站地址、双斜杠写法,看回什么;再加一版只许站内的校验。

nxt = q.get(u'next', [u'/'])[0]
if whitelist:
    ok = (nxt.startswith(u'/') and not nxt.startswith(u'//')
          and u':' not in nxt)
    if not ok:
        return 400
return 302, [(u'Location', nxt)]

实跑输出:

--------------------------------------------------------------------
开放重定向:你的域名帮别人发了一张通行证
--------------------------------------------------------------------
  目标                 next 参数                        不校验        只许站内
  站内相对路径             /dashboard                     302        302
  外站绝对地址             https://evil.example/steal     302        400
  双斜杠协议相对            //evil.example/steal           302        400

  外站绝对地址 跳去了        https://evil.example/steal
  双斜杠协议相对 跳去了      //evil.example/steal

  第二、三行都是 302,`Location` 直接指向 evil.example——
  浏览器会真的跳过去。可用户看到的链接是 `your-site.com/go?next=...`,域名是你的,他不会怀疑。
  这一招常被用来做钓鱼的第一跳:链接看起来正规,落地页却是假的登录页。

  第三行 `//evil.example` 要特别注意:它**不以 http 开头**,所以「只允许 http 开头」那种黑名单挡不住它——浏览器会把它当成「同协议、另一个主机」。

朴素版三行全是 302。 第二、 三行打出来的 Location 分别是 https://evil.example/steal 和 //evil.example/steal—— 都指向外站。 修正版第一行还是 302(站内的正常跳转),后两行 400。三个条件各挡一种写法。 第三行 //evil.example 是最容易漏的那一种:它看起来像站内路径(以 / 开头), 实际是「另一个主机」。 所以判断「是不是站内」不能只看开头有没有斜杠,还要专门排掉双斜杠和带冒号的形式。

30 秒实验

  1. 试试 https://[email protected]/,看浏览器的地址栏显示的是什么。
  2. 试试 /%2f%2fevil.example(编码过的双斜杠)能不能绕过。
  3. 想一想:为什么「跳转前给用户一个确认页」能降低危害?

常见误区

  1. 「只是跳转,又不执行代码,不算漏洞」——它借的是你的域名信用。
  2. 「限制必须 http 开头就够了」——//evil.example 不以 http 开头。
  3. 「只允许本站域名出现在参数里就够了」——域名可以写成子域名或用户信息段。
自测三问(点开对答案) 1. 开放重定向的危害是什么? —— 攻击者能用你的域名发出看起来可信的链接,把用户导向钓鱼页面。 2. 为什么 `//evil.example` 能绕过「必须 http 开头」的检查? —— 它以 `/` 开头,但浏览器把 `//` 解析成「同协议、另一个主机」。 3. 最稳的修法是什么? —— 不让外部传地址,参数只给页面标识,由服务端映射到路径。

设置这一类讲完了。最后一类最阴:用户什么都没做错。

相关笔记