範本引擎(Template Engine)負責動態產生網站、Email 等媒體內容——它在渲染時把佔位符替換成實際資料,方便開發者把應用邏輯呈現邏輯分離。例如使用者個資頁可能只用一個範本,由佔位符填入名稱、Email、年齡等欄位。範本引擎通常還提供輸入 sanitize 與 HTML 產生簡化等好處,但不代表它們本身免於漏洞

兩種範本注入#

範本注入(Template Injection)發生在引擎渲染使用者輸入時未正確 sanitize,嚴重時可導致遠端程式碼執行(RCE,第 12 章)

伺服器端範本注入(Server-Side Template Injection,SSTI)#

注入發生在伺服器端邏輯,可能藉由範本引擎所綁定的程式語言執行任意程式碼。能達到何種程度視引擎與站點防護而定:

  • Python Jinja2:曾允許任意檔案存取與 RCE
  • Ruby ERB(Rails 預設):同樣可被串成 RCE
  • Shopify Liquid:Ruby 引擎,刻意限制可呼叫的方法以防 RCE
  • 其他常見引擎:PHP 的 Smarty、Twig;Ruby 的 Haml、Mustache

各引擎語法不同,必須先確認站點使用什麼技術——可用 Wappalyzer BuiltWith 。常見的測試 payload:

  • Smarty:{{7*7}} → 期望輸出 49
  • ERB:<%= 7*7 %> → 期望輸出 49

客戶端範本注入(Client-Side Template Injection,CSTI)#

注入發生在客戶端 JavaScript 範本引擎。常見:Google AngularJS、Facebook ReactJS。

  • 無法直接 RCE,但可達到 XSS
  • ReactJS:預設保護完善;要找漏洞,看 JS 中是否有可控輸入餵給 dangerouslySetInnerHTML——這個函式會故意繞過 XSS 防護
  • AngularJS < 1.6:內建 Sandbox,但歷年陸續被研究者繞過。在開發者工具輸入 Angular.version 確認版本

AngularJS 1.3.0 ~ 1.5.7 的經典 Sandbox bypass:

{{a=toString().constructor.prototype;a.charAt=a.trim;$eval('a,alert(1),a')}}

更多版本對應的 bypass 可在 https://pastebin.com/xMXwsm0N https://jsfiddle.net/89aj1n7m/ 找到。

即便 {{4+4}} 被計算成 8,站點可能還有額外防禦:例如過濾 *()[],或限制最大字元數——這些都會讓 CSTI 看起來成立但難以實際利用。

案例 1:Uber AngularJS Template Injection#

漏洞描述#

PortSwigger(Burp Suite 開發商)首席研究員 James Kettle 在 Uber 開發者站找到 CSTI:

https://developer.uber.com/docs/deep-linking?q=wrtz{{7*7}}

頁面渲染後輸出 wrtz49,證實 AngularJS 解析了 7*7。Uber 用的版本含有可繞過的 Sandbox。Kettle 提交完整 Sandbox bypass:

?q=wrtz{{(_="".sub).call.call({}[$="constructor"].getOwnPropertyDescriptor(_.__proto__,$).value,0,"alert(1)")()}}zzzz

最終彈出 alert(1),證明 CSTI 可升級為 XSS,能威脅 Uber 開發者帳號與綁定的 App。

Takeaways#

找到 CSTI 後流程:

  1. Angular.version 確認版本
  2. 1.6+ → 直接送 payload
  3. < 1.6 → 找對應版本的 Sandbox bypass

案例 2:Uber Flask Jinja2 Template Injection#

背景#

Uber 公開了「treasure map」標示各子網域所用技術。riders.uber.com 用 Node.js + Express + Backbone.js,但 vault.uber.compartners.uber.comFlask + Jinja2——後者是有 SSTI 風險的伺服器端範本引擎。如果一處改名觸發了這些子網域的 Email 寄送,就有跨站注入機會。

漏洞描述#

研究者 Orange Tsai 在 riders.uber.com 改名為 {{1+1}}收到的通知 Email 把名字顯示為 2——說明 Email 那端用 Jinja2 並評估了表達式。

Figure 8-1: Orange 收到的 Email——名字欄位執行了他注入的程式碼

進一步驗證:

{% for c in [1,2,3]%} {{c,c,c}} {% endfor %}

收到的 Email 名稱被展開成九個數字。

Figure 8-2: Orange 注入更複雜程式碼後收到的 Email

為避免越權操作,Orange 只示範執行能力,並未進一步利用 Jinja2 的 introspection(運行時查詢物件屬性)來組成 RCE。要繼續探索影響時,先和計畫方確認——這既保護自己也讓對方能正確評估風險。

Takeaways#

即使主站不用某技術,子網域之間的互動也可能讓注入跨站發酵。本案的 payload 不是在 riders 主站觸發,而是在 Email 寄出時由另一個子網域處理。

案例 3:Rails Dynamic Render(CVE-2016-0752)#

漏洞描述#

Rails 採 MVC 架構。Controller 中常以 render 渲染指定範本:

render params[:template]

如果 template 參數是使用者可控,Rails 會遞迴搜尋:先找 /app/views,再 application root,再 server root。在 CVE-2016-0752 修補前,可送:

template=%2fetc%2fpasswd

伺服器最終會找到 /etc/passwd 並印出。同樣的搜尋鏈可被用來達成任意程式碼執行:

<%25%3d`ls`%25>

解碼後是 ERB 的 <%= \ls` %>`——直接執行 shell 指令。

即使官方 Rails 修補,仍要留意開發者把使用者輸入直接餵給 render inline: 的情況——這條路依舊把字串當 ERB 解析。

Takeaways#

讀懂框架的「慣例優於設定(convention over configuration)」設計細節,能找出依賴慣例放大的攻擊面。

案例 4:Unikrn Smarty Template Injection#

漏洞描述#

作者本人在 Unikrn(電競投注網站)的初步偵測時,Wappalyzer 標出 AngularJS。試 {{7*7}} 在 profile 沒中,改在「邀請朋友」功能;送出後收到的邀請信夾帶了 Smarty 錯誤堆疊——表示 Email 端是 Smarty。

Figure 8-3: 作者收到 Unikrn 的 Email,內含 Smarty 錯誤堆疊

進一步驗證:

  • 把名字改成 {$smarty.version} → 收到的邀請信顯示 2.6.18(Smarty 版本)
  • 名字改成 {php}print "Hello"{/php} → 邀請者顯示為 Hello

最後構造完整 PoC:

{php}$s=file_get_contents('/etc/passwd',NULL,NULL,0,100);var_dump($s);{/php}

收到的邀請信中出現 /etc/passwd 前 100 個字元(用 0, 100 分段是因為 Unikrn 對名稱長度有限制)。回報後一小時內修復。

Figure 8-4: Unikrn 邀請信顯示 /etc/passwd 檔案內容

Takeaways#

看到錯誤堆疊就追下去——「Where there’s smoke, there’s fire」。發現潛在 SSTI 後,讀引擎的官方文件找保留變數與標籤是高 CP 值步驟(如 Smarty 的 {$smarty.version}{php}{/php})。

章末總結#

  • 注入測試先確認技術棧(Wappalyzer / BuiltWith / 頁面源碼觀察 ng- 等屬性)
  • SSTI 可達 RCE,CSTI 一般只能達 XSS
  • 各引擎語法不同,先用簡單算式({{7*7}})測是否有評估
  • 範本引擎的 Sandbox 可能被繞過,遇到舊版(如 AngularJS < 1.6、Jinja2)尤其值得試
  • 漏洞不一定在送出時觸發;Email、PDF 匯出、後台頁面都是常被忽略的渲染點
  • 找到 SSTI 後優先做能力證明(如 introspection),不要直接組 RCE,讓計畫方評估或主動詢問