第二面牆,遠看什麼都沒有。
不是「看起來很堅固」的那種什麼都沒有,是真的什麼都沒有——光滑、平整、乾淨,連接縫都對得很齊。那是一面蓋得很好的牆,好到讓人覺得蓋它的人很用心。
O1 照規矩勘了一遍。
七個孔,全部在規制上。邊緣整齊,沒有磨損,沒有多開的、沒有補起來的。他趴下去看牆根,牆根乾淨得像剛掃過。
他又勘了第二遍。一樣。
第三遍勘完,他站起來,往後退了幾步。
他沒有拿槌子。
這件事他自己注意到了,還在心裡記了一筆——上一次他退後的時候,手是往腰上摸的。這次不是。
他就只是站在那裡,看著那面很乾淨的牆。
然後他想到一個問題:
一面牆有兩面。我只看了其中一面。
繞到後面要花點工夫。那面牆的另一側不是給人走的,堆著雜物,地也不平。他踩著碎石走過去,一路上被自己的斗篷絆了兩次。
繞到背面之後,他才發現這面牆是空心的。
而且內壁上,刻滿了字。
那些字不是給人看的。
是砌牆的人留下的。施工的記號、尺寸的修正、某個位置被劃掉重寫過三次。有一行寫著「此處先照舊式,待第二期再改」——第二期顯然沒有來。有一段是兩個人輪流寫的,一個人寫「這裡的支撐不夠」,另一個人在下面回「先這樣」。
O1 蹲下來,一行一行讀。
他讀到一半的時候意識到一件事:他讀得懂這些字。
不是字面上讀得懂——是他認得這種字。那種寫在沒有人會看的地方的字,那種「先這樣、之後再說」的字,那種寫的時候不打算給誰看、也不指望誰回應的字。
他在公會的卷宗背面寫過很多年這種字。
往下幾行,他看到一個口。
那個口很小,開在內壁上,看起來像是通風用的。旁邊刻著一行字,字跡工整,顯然是正式標註的:
「此口僅通內院。」
O1 讀了兩遍,然後伸手探進去。
他的手穿過去了。
指尖碰到的不是內院的暖空氣。是外牆的風,很冷,帶著外面那條路上的灰。
那個口不通內院。它通外面。
他把手抽回來,蹲在那行字前面看了很久。
刻字的人沒有騙人。O1 幾乎可以確定——刻的時候,那個口大概真的只通內院。 他是照著當時的樣子刻的,刻得工整,甚至還特地標了出來,那是一個負責任的人才會做的事。
問題是後來。
後來有人動過那面牆,或者後來內院的隔牆被拆了,或者只是有人趕工的時候把順序做反了。而那行字還留在那裡,沒有人回來改,也沒有人回來看。
O1 在冊子上寫下那個口的位置,然後在旁邊補了一句:
牆上寫的,是打算,不是事實。
他寫完停了一下。
他想起來自己的編號後面也有一行字,寫在銓敘司那本名冊上。那行字是誰寫的、什麼時候寫的、寫的時候對不對——他不知道。
他只知道,沒有人回去看過。
那天他沒有進去。
他回去的時候屋子是暗的,他開燈,坐下,把那面牆內壁上所有的字重抄了一次——因為他在現場抄得太亂,怕過幾天自己看不懂。
抄完之後,他在冊子的最後一行寫:
「沒有東西的牆,是不存在的。」
Day 6 談的是怎麼找到門。這一篇談的是:門找到了,但那扇門後面是一整座建築。
只要目標有 80/443,工作量就會翻倍——而且大部分人在這裡漏掉最多東西。
你在瀏覽器裡看到的那個畫面,是網站願意呈現給你的樣子。它是正面。
列舉要做的是繞到背面:
| 正面 | 背面 |
|---|---|
| 渲染後的頁面 | HTML 原始碼與註解 |
| 按鈕與表單 | 背後真正呼叫的 API endpoint |
| 「登入失敗」 | 回應碼、回應長度、回應時間的差異 |
| 一個漂亮的首頁 | HTTP headers、cookie 屬性、框架指紋 |
Day 6 那三個目錄掃描工具是在敲牆面。這一篇講的是牆是空心的。
① HTML 註解與原始碼——測試帳號、已註解掉的功能、內部路徑、「TODO:之後要改」。
② 被遺忘的檔案
.bak、.old、.save、index.php~、config.php.txt
.git/ 目錄——沒關掉的話可以還原原始碼庫。我實際遇過幾次,次數不多但真的有,而且通常是拿來放舊版檔案的.svn/、.DS_Store、web.config、.env
README、install.php、phpinfo.php
③ HTTP headers 與錯誤訊息——Server、X-Powered-By、框架自訂 header、錯誤頁的堆疊訊息。
④ JavaScript——見第四節,這一塊我要講得誠實一點。
這是本篇最想講的一件事。
有一次的委託,我在設定檔裡看到資料庫的帳號密碼,明文。旁邊還有一行註解,寫得清清楚楚:
只開放 127.0.0.1
我從外面試著連了一次。
連得上。
因為那個服務實際綁定的是 0.0.0.0。監聽在所有介面上,外部連得進來。
寫那行註解的人沒有騙人。我幾乎可以確定,他寫的時候是真心那樣打算的,甚至還特地註記出來——那是一個負責任的人才會做的事。
問題是後來。後來設定被改過,或者環境被搬過,或者只是有人趕工時把順序做反了。而那行註解還留在那裡,沒有人回來改,也沒有人回來驗。
於是就有了列舉的第二原則(第一原則是「看完」):
所有的文件、註解、設定說明,都是待驗證的假設,不是事實。
在這一行,沒有人回去確認過的文件,跟錯的文件沒有分別。
這條原則在攻擊側和防禦側都成立。做委託的時候,客戶給你的網段清單、架構圖、「這台已經下線了」——全部都要驗。不是懷疑對方誠信,是因為文件的更新速度永遠追不上環境。
網路上談 JS 列舉,很愛講寫死的 API token 和沒關掉的 sourcemap。那些確實存在,但以我實際接案的比例來說,最常遇到的是另一件事:
老舊的前端函式庫。 jQuery 是最典型的,一個站用著好幾年前的版本,掃描器立刻跳出一串 CVE。
然後就是這一行最現實的部分:
版本過舊,不等於可以利用。
有 CVE 編號、版本也確實過舊,但實際要打的時候,卡住的往往不是漏洞本身——不是遇到權限問題,就是前面還有一層 WAF 擋著。條件湊不齊,利用性就低。
這裡就分出兩種人:
第二種才是滲透測試。
那不可利用的部分要不要寫進報告?要,但要分類分對。 它屬於「資訊性發現」或「安全強化建議」,不能跟一個真的能被打穿的漏洞放在同一個風險等級裡。
把不可利用的東西寫成高風險,會讓客戶把有限的修補資源用錯地方,而且下一次他們就不會那麼相信你的報告了。
(這一段會在 Day 28 完整展開。報告的可信度,是這一行最貴的資產。)
順序有意義:Proxy 和 Repeater 是用來「看」的,Intruder 是用來「量」的。 先看懂再去量,不然你只是在製造流量。
這一段接 Day 17。
每一個名字都要抄下來。 到了 AD 環境,那份名單就是密碼噴灑的輸入。
列舉的產出不是一堆路徑,是一段理解:
這個網站是誰做的?給誰用的?在做什麼?
一個內部管理後台和一個對外官網,即使跑出一模一樣的目錄結構,攻擊路徑也完全不同。不要先挑工具,再去找地方用它。
誠實說:我接的網站滲透案件,範圍通常很明確——就是那幾個網站、那幾個網域。子網域列舉除非客戶明確要求,否則不會去做。
這不是保守,是因為:
回到 Day 4 那句話:你做了什麼不重要,別人認定你做了什麼才重要。
如果列舉過程中意外碰到範圍邊緣的東西,做法是:停在「發現」,寫進報告。
要不要另外主動告知,看情況——列為高風險以上、或客戶事先要求即時回報的,才會另外通知;其餘就留在報告裡,由客戶決定要不要擴大範圍。
沒有東西的牆,是不存在的——只有你還沒繞到背面。
而繞到背面之後你會發現:牆上寫的,是打算,不是事實。