攻防實(shí)戰(zhàn):從原理到修復(fù)的完整指南)
1. 從模板引擎說(shuō)起為什么“填坑”填出了安全風(fēng)險(xiǎn)模板引擎這個(gè)東西做Web開(kāi)發(fā)的人幾乎天天打交道。它的初衷特別樸素——把數(shù)據(jù)和頁(yè)面結(jié)構(gòu)分開(kāi)讓后端只負(fù)責(zé)算數(shù)據(jù)前端只負(fù)責(zé)擺位置。你給模板一個(gè)占位符它幫你把變量替換進(jìn)去最后吐出一段完整的HTML。聽(tīng)起來(lái)人畜無(wú)害但問(wèn)題恰恰出在“替換”這個(gè)動(dòng)作上如果替換進(jìn)去的內(nèi)容不是普通數(shù)據(jù)而是一段能被引擎再次解析的表達(dá)式那整個(gè)邏輯就變味了。我最早接觸這類(lèi)問(wèn)題時(shí)是在一個(gè)內(nèi)部管理系統(tǒng)的代碼審計(jì)里。當(dāng)時(shí)看到一個(gè)頁(yè)面把用戶(hù)提交的昵稱(chēng)直接拼進(jìn)了模板渲染流程直覺(jué)告訴我這里不對(duì)勁。后來(lái)花了一個(gè)下午做驗(yàn)證確認(rèn)了模板注入確實(shí)存在而且能讀到一些不該讀的東西。從那以后我對(duì)“用戶(hù)輸入進(jìn)模板”這件事就格外敏感。這篇內(nèi)容想聊的就是模板注入這類(lèi)問(wèn)題的來(lái)龍去脈——它是什么、為什么會(huì)產(chǎn)生、怎么一步步驗(yàn)證、實(shí)際環(huán)境中怎么排查、以及開(kāi)發(fā)側(cè)怎么從根上堵住。適合有一定Web基礎(chǔ)、想理解服務(wù)端模板安全的朋友也適合做代碼審計(jì)和滲透測(cè)試方向的同學(xué)當(dāng)作思路參考。文中涉及的操作均在本地搭建的模擬環(huán)境中完成目的是理解原理和防御方法請(qǐng)勿對(duì)任何未授權(quán)的系統(tǒng)做嘗試。先把這個(gè)概念說(shuō)清楚模板注入業(yè)內(nèi)常簡(jiǎn)稱(chēng)SSTIServer-Side Template Injection指的是攻擊者能夠把惡意表達(dá)式注入到服務(wù)端模板中并讓模板引擎在服務(wù)端執(zhí)行它。它和常見(jiàn)的XSS不是一回事——XSS是在瀏覽器里跑腳本SSTI是在服務(wù)器上跑代碼危害量級(jí)完全不同。一個(gè)配置不當(dāng)?shù)哪0邃秩军c(diǎn)輕則泄露配置信息重則直接拿到服務(wù)器權(quán)限。為什么模板引擎會(huì)“執(zhí)行”用戶(hù)輸入關(guān)鍵在于模板引擎的解析機(jī)制。大多數(shù)模板引擎在渲染時(shí)會(huì)先對(duì)模板字符串做一次語(yǔ)法解析識(shí)別出其中的表達(dá)式片段比如雙大括號(hào)包裹的內(nèi)容然后對(duì)這些片段求值。如果用戶(hù)輸入被當(dāng)成了模板字符串的一部分而不是作為“數(shù)據(jù)”傳進(jìn)去那么用戶(hù)寫(xiě)的表達(dá)式就會(huì)被當(dāng)成代碼來(lái)執(zhí)行。這就是整個(gè)問(wèn)題的根子。2. 漏洞成因拆解數(shù)據(jù)與代碼的邊界被模糊了2.1 正常用法與危險(xiǎn)用法的分水嶺要理解這個(gè)問(wèn)題得先看清楚“正確姿勢(shì)”長(zhǎng)什么樣。模板引擎的標(biāo)準(zhǔn)用法是模板文件固定變量通過(guò)上下文傳入。比如模板里寫(xiě){{ username }}渲染時(shí)把username作為一個(gè)鍵值對(duì)傳進(jìn)去引擎只負(fù)責(zé)把值填進(jìn)去不會(huì)去解析這個(gè)值本身的內(nèi)容。這種情況下哪怕用戶(hù)名是{{ 7*7 }}渲染出來(lái)也就是原樣的字符串不會(huì)變成49。危險(xiǎn)用法則是另一回事把用戶(hù)輸入直接拼接到模板字符串里然后再交給引擎渲染。這時(shí)候用戶(hù)輸入就不再是“數(shù)據(jù)”而是“模板源碼”的一部分。引擎解析時(shí)看到{{ 7*7 }}會(huì)老老實(shí)實(shí)算成49。這個(gè)差別看起來(lái)很小但性質(zhì)完全變了——你等于把代碼解釋器的入口開(kāi)放給了外部輸入。我用一個(gè)簡(jiǎn)化的對(duì)比來(lái)說(shuō)明用法代碼形態(tài)用戶(hù)輸入{{7*7}}的結(jié)果風(fēng)險(xiǎn)安全模板固定變量走上下文原樣輸出{{7*7}}無(wú)危險(xiǎn)用戶(hù)輸入拼進(jìn)模板字符串輸出49高這個(gè)表格看著簡(jiǎn)單但實(shí)際項(xiàng)目中“危險(xiǎn)用法”往往藏得很深。比如某些框架提供了“動(dòng)態(tài)模板”能力允許在運(yùn)行時(shí)根據(jù)字符串生成模板又比如某些代碼為了圖方便用字符串格式化把用戶(hù)輸入塞進(jìn)模板再渲染。這些寫(xiě)法在功能測(cè)試時(shí)一切正常只有在安全審視時(shí)才會(huì)暴露問(wèn)題。2.2 為什么這類(lèi)問(wèn)題容易被忽視我觀(guān)察下來(lái)這類(lèi)問(wèn)題被忽視有幾個(gè)典型原因。第一模板渲染通常發(fā)生在服務(wù)端前端看不到任何異常功能測(cè)試完全通過(guò)。第二很多開(kāi)發(fā)者對(duì)模板引擎的“表達(dá)式求值”能力缺乏認(rèn)知以為它只是簡(jiǎn)單的字符串替換。第三部分框架的默認(rèn)配置比較寬松動(dòng)態(tài)模板能力開(kāi)箱即用開(kāi)發(fā)者順手就用了根本沒(méi)意識(shí)到自己在開(kāi)放一個(gè)執(zhí)行入口。還有一個(gè)更隱蔽的點(diǎn)模板注入的入口不一定在顯眼的位置。它可能藏在郵件模板、報(bào)表生成、PDF導(dǎo)出、甚至錯(cuò)誤頁(yè)面的自定義消息里。這些地方往往不是核心業(yè)務(wù)邏輯測(cè)試覆蓋度低代碼review也容易漏掉。我在一次審計(jì)中就遇到過(guò)一個(gè)“自定義歡迎語(yǔ)”的功能把用戶(hù)填的文案直接拼進(jìn)了模板渲染結(jié)果成了一個(gè)穩(wěn)定的注入點(diǎn)。注意判斷是否存在風(fēng)險(xiǎn)不能只看“有沒(méi)有用模板引擎”而要看“用戶(hù)輸入有沒(méi)有機(jī)會(huì)變成模板源碼的一部分”。前者是正常技術(shù)選型后者才是問(wèn)題所在。2.3 影響范圍到底有多大影響范圍取決于模板引擎的能力和運(yùn)行環(huán)境的權(quán)限。最輕的情況是信息泄露比如通過(guò)表達(dá)式讀取環(huán)境變量、配置文件內(nèi)容。中等情況是能讀取應(yīng)用內(nèi)部的敏感數(shù)據(jù)比如數(shù)據(jù)庫(kù)連接串、密鑰。嚴(yán)重情況是能通過(guò)引擎的某些特性執(zhí)行系統(tǒng)命令直接拿到服務(wù)器控制權(quán)。具體能走多遠(yuǎn)取決于引擎本身提供了哪些“能力接口”以及應(yīng)用運(yùn)行在什么權(quán)限下。這里要強(qiáng)調(diào)一點(diǎn)不要因?yàn)椤爸皇亲x了個(gè)變量”就低估危害。信息泄露往往是進(jìn)一步利用的起點(diǎn)攻擊者拿到配置信息后可以橫向擴(kuò)展。所以在評(píng)估時(shí)應(yīng)該把這類(lèi)問(wèn)題當(dāng)作高危來(lái)處理而不是“小問(wèn)題”。3. 實(shí)操驗(yàn)證在模擬環(huán)境里把鏈路走通3.1 環(huán)境搭建與目標(biāo)確認(rèn)驗(yàn)證這類(lèi)問(wèn)題第一步是搭一個(gè)可控的模擬環(huán)境。我用的是一個(gè)最簡(jiǎn)單的Web應(yīng)用后端用某常見(jiàn)語(yǔ)言框架模板引擎選了一個(gè)支持表達(dá)式求值的常見(jiàn)引擎頁(yè)面提供一個(gè)輸入框用戶(hù)提交的內(nèi)容會(huì)被拼進(jìn)模板渲染。整個(gè)環(huán)境跑在本地容器里與外部網(wǎng)絡(luò)隔離。搭建時(shí)要注意兩點(diǎn)一是確保環(huán)境完全隔離避免驗(yàn)證過(guò)程中產(chǎn)生意外影響二是記錄好基線(xiàn)行為也就是正常輸入時(shí)的輸出方便后續(xù)對(duì)比。我一般會(huì)先用一個(gè)普通字符串測(cè)試確認(rèn)渲染流程正常再逐步升級(jí)測(cè)試載荷。環(huán)境確認(rèn)之后先做“無(wú)害探測(cè)”。提交一個(gè)簡(jiǎn)單的算術(shù)表達(dá)式比如兩個(gè)數(shù)相乘觀(guān)察返回結(jié)果。如果返回的是計(jì)算結(jié)果而不是原樣字符串說(shuō)明表達(dá)式被求值了注入點(diǎn)成立。這一步是整個(gè)驗(yàn)證的基石必須確認(rèn)清楚避免把“字符串被原樣回顯”誤判成“表達(dá)式被執(zhí)行”。3.2 逐步升級(jí)的探測(cè)思路確認(rèn)基礎(chǔ)注入成立后下一步是摸清引擎的能力邊界。我的習(xí)慣是從“讀環(huán)境”開(kāi)始因?yàn)檫@類(lèi)操作相對(duì)溫和能快速判斷引擎暴露了哪些對(duì)象。不同引擎的語(yǔ)法差異很大有的用點(diǎn)號(hào)訪(fǎng)問(wèn)屬性有的用方括號(hào)有的支持函數(shù)調(diào)用。探測(cè)時(shí)需要根據(jù)返回的報(bào)錯(cuò)信息或輸出內(nèi)容來(lái)判斷引擎類(lèi)型再針對(duì)性調(diào)整語(yǔ)法。這里有個(gè)經(jīng)驗(yàn)報(bào)錯(cuò)信息是寶藏。很多引擎在表達(dá)式非法時(shí)會(huì)拋出帶有引擎名稱(chēng)或語(yǔ)法特征的錯(cuò)誤這能幫你快速定位引擎類(lèi)型。但要注意生產(chǎn)環(huán)境通常會(huì)關(guān)閉詳細(xì)報(bào)錯(cuò)所以這一步主要在本地環(huán)境做。定位到引擎類(lèi)型后就可以查閱該引擎的文檔了解它暴露了哪些內(nèi)置對(duì)象和方法。探測(cè)過(guò)程中要保持耐心不要一上來(lái)就用復(fù)雜載荷。先用最簡(jiǎn)單的表達(dá)式確認(rèn)求值再逐步嘗試訪(fǎng)問(wèn)對(duì)象屬性最后才考慮更深的能力。每一步都要記錄輸入和輸出形成清晰的鏈路。我在實(shí)際操作中會(huì)用一個(gè)表格來(lái)跟蹤階段測(cè)試內(nèi)容預(yù)期觀(guān)察判斷依據(jù)基礎(chǔ)算術(shù)表達(dá)式返回計(jì)算結(jié)果表達(dá)式被求值進(jìn)階訪(fǎng)問(wèn)內(nèi)置對(duì)象返回對(duì)象信息引擎暴露能力深入調(diào)用對(duì)象方法返回方法執(zhí)行結(jié)果能力邊界確認(rèn)3.3 從表達(dá)式到實(shí)際影響的鏈路把鏈路走通的關(guān)鍵是理解“表達(dá)式求值”如何一步步轉(zhuǎn)化為實(shí)際影響?;A(chǔ)表達(dá)式只能做算術(shù)和字符串操作本身危害有限。真正的風(fēng)險(xiǎn)在于引擎暴露的對(duì)象模型——如果某個(gè)內(nèi)置對(duì)象能訪(fǎng)問(wèn)到運(yùn)行時(shí)環(huán)境那么通過(guò)屬性鏈就可能觸達(dá)敏感信息。再進(jìn)一步如果某個(gè)對(duì)象的方法能執(zhí)行外部指令那就到了最嚴(yán)重的層級(jí)。我在模擬環(huán)境里驗(yàn)證時(shí)刻意控制了深度只驗(yàn)證到“能讀取環(huán)境變量”這一層就停止目的是確認(rèn)風(fēng)險(xiǎn)存在而不做進(jìn)一步擴(kuò)展。這個(gè)分寸很重要驗(yàn)證的目的是證明問(wèn)題、推動(dòng)修復(fù)而不是追求“打穿”。在實(shí)際工作中超出必要范圍的驗(yàn)證既不負(fù)責(zé)任也容易引發(fā)不必要的麻煩。驗(yàn)證完成后要把整個(gè)鏈路整理成可復(fù)現(xiàn)的記錄包括環(huán)境配置、輸入載荷、輸出結(jié)果、影響判斷。這份記錄是后續(xù)修復(fù)和回歸測(cè)試的依據(jù)。我一般會(huì)把它寫(xiě)成簡(jiǎn)短的報(bào)告重點(diǎn)說(shuō)清楚“問(wèn)題在哪、能造成什么影響、建議怎么修”而不是堆砌技術(shù)細(xì)節(jié)。4. 排查與防御從代碼層把口子堵上4.1 代碼審計(jì)中的快速定位方法做代碼審計(jì)時(shí)我找這類(lèi)問(wèn)題的思路是“逆向追蹤”先找模板渲染的調(diào)用點(diǎn)再看渲染的模板字符串是怎么來(lái)的。如果模板字符串是常量或者來(lái)自受控的模板文件基本安全如果模板字符串里拼接了外部輸入就要重點(diǎn)看。常見(jiàn)的危險(xiǎn)信號(hào)包括字符串格式化后傳給渲染函數(shù)、動(dòng)態(tài)生成模板內(nèi)容、把請(qǐng)求參數(shù)直接拼進(jìn)模板路徑或內(nèi)容。具體操作上我會(huì)先全局搜索渲染相關(guān)的函數(shù)名列出所有調(diào)用點(diǎn)然后逐個(gè)看參數(shù)來(lái)源。對(duì)于每個(gè)調(diào)用點(diǎn)問(wèn)三個(gè)問(wèn)題模板內(nèi)容從哪來(lái)用戶(hù)能不能控制控制的部分是作為數(shù)據(jù)還是作為模板源碼這三個(gè)問(wèn)題回答完風(fēng)險(xiǎn)基本就清楚了。這個(gè)方法看起來(lái)笨但覆蓋率高不容易漏。還有一類(lèi)容易漏的點(diǎn)是“間接拼接”。比如用戶(hù)輸入先存進(jìn)數(shù)據(jù)庫(kù)后來(lái)又被讀出來(lái)拼進(jìn)模板。這種跨請(qǐng)求的鏈路單看一處代碼是發(fā)現(xiàn)不了的需要結(jié)合數(shù)據(jù)流分析。我在審計(jì)時(shí)會(huì)特別關(guān)注那些“用戶(hù)可寫(xiě)、后續(xù)又被渲染”的字段這類(lèi)字段往往是隱藏的注入點(diǎn)。4.2 修復(fù)方案的分層設(shè)計(jì)修復(fù)這類(lèi)問(wèn)題核心原則只有一條讓用戶(hù)輸入永遠(yuǎn)只作為數(shù)據(jù)不進(jìn)入模板源碼。具體落地可以分幾層來(lái)做。第一層是用法糾正。所有模板渲染都走“固定模板上下文傳參”的標(biāo)準(zhǔn)模式禁止把外部輸入拼進(jìn)模板字符串。這一層是根本改完之后大部分風(fēng)險(xiǎn)就消失了。改的時(shí)候要注意有些框架的“動(dòng)態(tài)模板”API要顯式禁用或限制避免被誤用。第二層是輸入約束。對(duì)于確實(shí)需要用戶(hù)自定義內(nèi)容的地方比如自定義文案做嚴(yán)格的白名單校驗(yàn)或轉(zhuǎn)義處理確保內(nèi)容里不包含模板語(yǔ)法字符。這一層是兜底不能替代第一層但能降低誤用帶來(lái)的風(fēng)險(xiǎn)。第三層是運(yùn)行環(huán)境加固。模板引擎運(yùn)行在最小權(quán)限下關(guān)閉不必要的內(nèi)置對(duì)象訪(fǎng)問(wèn)能力生產(chǎn)環(huán)境關(guān)閉詳細(xì)報(bào)錯(cuò)。這一層是縱深防御萬(wàn)一前面有漏網(wǎng)之魚(yú)能把影響控制在較小范圍。層級(jí)措施作用優(yōu)先級(jí)用法固定模板上下文傳參消除注入入口最高輸入白名單校驗(yàn)/轉(zhuǎn)義降低誤用風(fēng)險(xiǎn)高環(huán)境最小權(quán)限關(guān)閉報(bào)錯(cuò)控制影響范圍中4.3 上線(xiàn)前的自查清單修復(fù)完成后上線(xiàn)前建議過(guò)一遍自查清單。我常用的清單包括所有渲染調(diào)用點(diǎn)的模板來(lái)源是否受控是否存在動(dòng)態(tài)拼接模板的代碼用戶(hù)可寫(xiě)字段是否有可能流入模板模板引擎配置是否關(guān)閉了不必要的動(dòng)態(tài)能力生產(chǎn)環(huán)境報(bào)錯(cuò)是否已收斂。這幾項(xiàng)過(guò)完基本能覆蓋主要風(fēng)險(xiǎn)面。另外建議把這類(lèi)檢查納入代碼review的常規(guī)項(xiàng)。模板渲染相關(guān)的改動(dòng)review時(shí)多問(wèn)一句“模板內(nèi)容從哪來(lái)”能攔住不少問(wèn)題。我在團(tuán)隊(duì)里推過(guò)這個(gè)習(xí)慣一開(kāi)始大家覺(jué)得啰嗦后來(lái)有一次真在review階段攔下一個(gè)拼接寫(xiě)法之后就沒(méi)人抱怨了。5. 常見(jiàn)問(wèn)題與踩坑記錄5.1 誤判與漏判的典型場(chǎng)景實(shí)際操作中誤判和漏判都很常見(jiàn)。誤判的典型是“看到回顯就以為有注入”——有些應(yīng)用會(huì)把用戶(hù)輸入原樣顯示在頁(yè)面上這看起來(lái)像表達(dá)式被執(zhí)行了其實(shí)只是普通的字符串回顯。區(qū)分方法很簡(jiǎn)單用算術(shù)表達(dá)式測(cè)試如果返回的是原字符串而不是計(jì)算結(jié)果那就是回顯不是注入。漏判的典型是“只測(cè)了顯眼入口”。很多注入點(diǎn)藏在次要功能里比如自定義錯(cuò)誤頁(yè)、郵件模板、導(dǎo)出功能。這些地方測(cè)試覆蓋低容易被跳過(guò)。我的建議是把“所有涉及模板渲染的功能”列一個(gè)清單逐個(gè)過(guò)不要憑感覺(jué)挑著測(cè)。還有一種漏判是“語(yǔ)法沒(méi)對(duì)上”。不同引擎語(yǔ)法差異大用一種引擎的載荷去測(cè)另一種可能完全沒(méi)反應(yīng)讓人誤以為安全。所以探測(cè)前先確認(rèn)引擎類(lèi)型很重要確認(rèn)不了就用多種語(yǔ)法分別試觀(guān)察哪種有反應(yīng)。5.2 排查時(shí)的實(shí)用技巧排查這類(lèi)問(wèn)題時(shí)有幾個(gè)技巧能提高效率。第一善用報(bào)錯(cuò)。本地環(huán)境打開(kāi)詳細(xì)報(bào)錯(cuò)非法表達(dá)式往往能直接暴露引擎類(lèi)型和語(yǔ)法特征。第二做對(duì)照實(shí)驗(yàn)。同樣的輸入分別走“拼接渲染”和“上下文傳參”兩條路徑對(duì)比輸出差異能快速確認(rèn)問(wèn)題性質(zhì)。第三記錄基線(xiàn)。每次測(cè)試前先記錄正常行為避免把環(huán)境本身的特性誤認(rèn)為漏洞。還有一個(gè)技巧是“最小化載荷”。不要一上來(lái)就用復(fù)雜表達(dá)式先用最簡(jiǎn)單的能區(qū)分“求值”和“回顯”的載荷確認(rèn)基礎(chǔ)事實(shí)后再逐步加復(fù)雜度。這樣即使中間某步?jīng)]反應(yīng)也能清楚知道卡在哪一層而不是一團(tuán)亂麻。提示所有驗(yàn)證操作都應(yīng)在自己搭建的隔離環(huán)境中進(jìn)行驗(yàn)證記錄要妥善保存用于修復(fù)后的回歸對(duì)比。不要在任何非授權(quán)環(huán)境做測(cè)試。5.3 修復(fù)后的回歸驗(yàn)證修復(fù)上線(xiàn)前一定要做回歸驗(yàn)證。驗(yàn)證思路是用之前確認(rèn)有效的載荷重新測(cè)試確認(rèn)不再被執(zhí)行同時(shí)測(cè)試正常功能確認(rèn)修復(fù)沒(méi)有影響業(yè)務(wù)。這兩方面都要覆蓋只測(cè)前者可能漏掉功能回歸只測(cè)后者可能漏掉修復(fù)不徹底?;貧w時(shí)我習(xí)慣把之前的測(cè)試記錄拿出來(lái)逐條對(duì)照每條載荷的預(yù)期結(jié)果從“被執(zhí)行”變成“被當(dāng)作普通數(shù)據(jù)處理”就算通過(guò)。如果某條載荷仍然有異常反應(yīng)說(shuō)明修復(fù)不徹底需要回到代碼層繼續(xù)排查。這個(gè)過(guò)程看起來(lái)繁瑣但比上線(xiàn)后出問(wèn)題再回滾要省事得多。6. 我個(gè)人的幾點(diǎn)體會(huì)做這類(lèi)安全問(wèn)題的排查和修復(fù)最大的感受是“邊界意識(shí)”比“技術(shù)技巧”更重要。模板引擎本身沒(méi)有問(wèn)題問(wèn)題出在“數(shù)據(jù)”和“代碼”的邊界被模糊了。只要守住這條邊界——用戶(hù)輸入永遠(yuǎn)只當(dāng)數(shù)據(jù)——大部分風(fēng)險(xiǎn)自然就消失了。反過(guò)來(lái)如果邊界意識(shí)缺失再多的檢測(cè)工具也只是事后補(bǔ)救。另一個(gè)體會(huì)是這類(lèi)問(wèn)題的修復(fù)成本其實(shí)很低但發(fā)現(xiàn)成本不低。標(biāo)準(zhǔn)用法改起來(lái)就是幾行代碼的事難的是把散落在各處的危險(xiǎn)用法找出來(lái)。所以我在實(shí)際工作中更看重“預(yù)防”在代碼規(guī)范里明確模板渲染的標(biāo)準(zhǔn)寫(xiě)法在review清單里加上相關(guān)檢查項(xiàng)在框架層面限制動(dòng)態(tài)模板能力。這些前置動(dòng)作做扎實(shí)了后面排查的壓力會(huì)小很多。最后分享一個(gè)小習(xí)慣每次看到“字符串拼接后渲染”的代碼我都會(huì)多停留幾秒問(wèn)一句“這個(gè)拼接的內(nèi)容用戶(hù)能不能碰到”。這個(gè)習(xí)慣幫我攔下過(guò)好幾個(gè)潛在問(wèn)題也讓我對(duì)模板渲染的邊界越來(lái)越敏感。安全這件事很多時(shí)候就是多問(wèn)一句、多看一眼的事。