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

「包含」本来是「把另一个文件的内容放进来」。 如果被包含的路径由用户控制,他就能包含你机器上的任意文件; 如果还能包含远程地址,那就是把攻击者的代码搬到你机器上执行。

📝 本节和前面几节的关系

上一节是「读」,这一节是「读进来当代码用」——差一步就是远程代码执行。

一、包含是什么

很多语言都有「包含」这个动作:PHP 的 include、JSP 的 <jsp:include>、 模板引擎的 {{ include 'x' }}。作用是把另一个文件的内容拼进当前文件。 它的本意是复用:页头、页脚、侧边栏各写一份,各处 include 一下。所以参数往往是「页面名」。 一旦这个页面名来自用户输入,问题就来了——用户能决定「包含哪个文件」,而包含不只是读, 如果那个文件里有可执行代码,它会被执行。

二、本地包含与远程包含

本地文件包含(LFI):能包含本机上的任意文件。 配上目录遍历(11.2.6)就能读到 /etc/passwd、日志、配置文件。 远程文件包含(RFI):能包含一个 http 地址。 这时候服务端会去攻击者的服务器上取一个文件回来当代码执行——这一步就直接是远程代码执行, 整台机器都归对方了。 RFI 能否成立取决于配置:PHP 里的 allow_url_include 默认是关的,关着就只能做本地包含。 所以这条攻击链的关键一环是「那个开关被打开了」。 Demo 只做了「路径能不能被控制」这一条:page=../../secret 在朴素版是 200, 白名单版是 400。远程那一半我不在这台机器上演示——取回远程文件再执行就是一句话拿到整台机器。

三、两头都要堵

白名单是第一头:page 只认 about 和 faq 两个字面量,不在表里的直接回 400。 Demo 里加了白名单之后,三行输入里有两行变成 400。 禁止远程包含是第二头:把 allow_url_include 关掉、或者用语言层面「只允许本地路径」的开关。 这一条是配置,改起来最便宜。 理解为什么白名单比黑名单强:黑名单要穷举「所有危险的写法」,白名单只要列「所有允许的值」。 前者是开放的,后者是封闭的。 还有一条工程上的做法:包含路径不要由请求参数决定,改成「参数是键,键去映射到固定路径」。 多一层映射,攻击面就小一圈。

四、动手 Demo:page 参数能跑多远

本机起一个「按名字包含页面」的接口:page=about 就返回 pages/about.txt 的内容。 两种实现,一种直接拼路径,一种只认白名单。各喂三个值。

if fixed:
    if page not in (u'about', u'faq'):
        return 400
    path = os.path.join(WEB, u'pages', page + u'.txt')
else:
    path = os.path.join(WEB, u'pages', page + u'.txt')

实跑输出:

--------------------------------------------------------------------
文件包含:被包含的那份文件从哪来
--------------------------------------------------------------------
  手法           page 参数                  朴素版      白名单版
  正常           about.txt                200      200
  逃到上层         ../../secret.txt         200      400
  编码过的斜杠       ..%2f..%2fsecret.txt     200      400

  第二、三行朴素版都拿到了 `secret.txt` 的内容——`page` 参数本来只该指向 `pages/` 里的几个固定页面,但它能往上层跑,编码过的斜杠也拦不住。

  这一节书里讲的是「远程文件包含」:把 `page` 换成一个远程地址,包含进来的就是攻击者服务器上的文件——如果那个文件是代码,就在你机器上执行了。
  本 Demo 不做那一步:远程取回再执行就是一句话拿到整台机器,我不在这台机器上演示它。能测的只有「路径能不能被控制」这一条,上面已经测了。

朴素版三行是 200/200/200:正常的 about 通了, ../../secret 和编码过的 ..%2f..%2fsecret 也通了,后两个读到了根目录外的文件。 白名单版三行是 200/400/400:只有 about 放行,另外两个连路径都没拼就被拒了。 注意白名单版的判断发生在拼路径之前——这一点很重要,因为一旦拼了路径,就又有了解析歧义的机会。 远程那一半没做:本 Demo 只测「路径能不能被控制」。 能控制路径 + 允许远程包含 = 远程代码执行,这一步我不演示。

30 秒实验

  1. 给白名单版的 page 传一个数组(page[]=about),看代码会不会报错。
  2. 查一下你手边项目里所有的 include/import/模板包含,看参数是不是可控。
  3. 想一想:为什么「日志文件」经常被当作 LFI 的跳板?

常见误区

  1. 「包含就是读文件,最多泄露点内容」——包含的内容会被当代码执行。
  2. 「allow_url_include 默认是开的」——PHP 里默认是关的,但有人会打开。
  3. 「黑名单里加几个危险词就够了」——白名单才是封闭的、可穷举的。
自测三问(点开对答案) 1. 本地文件包含和远程文件包含的区别是什么? —— 前者包含本机文件,后者包含远程地址上的文件,后者通常直接导致代码执行。 2. 为什么白名单比黑名单可靠? —— 白名单是封闭的(列出所有允许值),黑名单要穷举所有危险写法,永远漏。 3. 远程包含成立的关键配置是什么? —— 允许包含远程地址的那个开关(PHP 的 `allow_url_include`)。

注入这一大类完了。下一类更「便宜」:代码没错,是设置漏了。

相关笔记