許多網站中會進行資料處理和檔案管理的功能,但如果這些功能的實作不夠謹慎,可能會引發嚴重的安全漏洞。Command Injection(指令注入)是其中一種高危險的漏洞,攻擊者能透過將惡意指令注入到應用程式中,使伺服器執行未經授權的指令。本篇文章將展示如何在 Node.js 環境中實作一個簡單的壓縮檔案功能,並分析 Command Injection 的潛在風險及其防範措施。
Command 是一種指令,透過作業系統的指令介面(CLI)執行特定的任務或操作。在類似 Unix 系統(如 Linux 或 macOS)和 Windows 系統中,指令可以用來操作檔案、資料夾、執行程式、進行系統設定等。
指令介面提供了比圖形介面更靈活的控制和自動化功能,因此經常被開發者、系統管理員以及資安專業人員使用。
在程式碼中,尤其是 Web 應用程式中,開發者有時需要呼叫系統指令來完成特定的任務,例如處理檔案壓縮、管理伺服器上的資源等。
Command Injection(指令注入)是一種應用程式漏洞,攻擊者可以透過不當處理的輸入,將惡意指令注入到應用程式中,並在伺服器上執行。
這類漏洞通常發生在應用程式直接執行系統指令且未對輸入進行適當的驗證或過濾時,攻擊者可藉此在伺服器上執行未經授權的操作,例如讀取敏感檔案、刪除資料,甚至取得系統控制權。
Command Injection 的危險在於,攻擊者可以利用這一漏洞執行任何他們想要的系統指令,導致嚴重的安全風險。
在指令介面中,連結詞(或稱為運算符號)允許使用者將多個指令組合在一起執行,根據不同的運算符號,這些指令之間的執行順序和行為會有所不同。
以下是常用的指令連結詞比較,請記得將表格中的|
改回半形|
。
連結詞 | 說明 | 範例 | 結果 |
---|---|---|---|
&& |
只有目前一個指令成功執行後才會執行下一個指令 | command1 && command2 |
若 command1 成功執行,則執行 command2 |
& |
同時執行多個指令,並將它們放在背景執行 | command1 & command2 |
command1 和 command2 會同時執行,且在背景中執行 |
|| |
目前一個指令失敗時執行下一個指令 | command1 || command2 |
若 command1 失敗,則執行 command2 |
| |
將前一個指令的輸出作為下一個指令的輸入,稱為管道(pipe) | command1 | command2 |
command1 的輸出作為 command2 的輸入 |
; |
順序執行多個指令,無論前一個指令是否成功 | command1; command2 |
依次執行 command1 和 command2 ,無論 command1 是否成功 |
> |
將指令的輸出寫入到檔案中,若檔案已存在,則覆蓋 | command1 > output.txt |
將 command1 的輸出寫入到 output.txt 中 |
>> |
將指令的輸出追加到檔案末尾,若檔案已存在,則不覆蓋 | command1 >> output.txt |
將 command1 的輸出追加到 output.txt 的末尾 |
2>&1 |
將標準錯誤(stderr)重定向到標準輸出(stdout) | command1 2>&1 |
將 command1 的錯誤輸出重定向到標準輸出 |
2> |
將標準錯誤輸出寫入到檔案中 | command1 2> error.txt |
將 command1 的錯誤輸出寫入 error.txt 中 |
這些連結詞提供了靈活的指令組合方式,使得複雜的任務可以透過單一指令完成。
但在 Command Injection 中,攻擊者可利用這些運算符號連結惡意指令,擴大攻擊效果,因此在處理指令時應特別小心。
在這次的實作中,我們將為伺服器新增一個壓縮檔案的功能,使用 zip
工具來壓縮指定的檔案,並將壓縮後的檔案提供給使用者下載。這個功能的實現依賴於 Node.js 的 child_process
模組中的 exec
函數,該函數可以執行系統指令,因此特別需要注意 Command Injection 的風險。
https://github.com/fei3363/ithelp_web_security_2024/commit/9dcfc8a1f9ee750e1eddf4d224c277dec2897143
首先,我們需要在 Dockerfile 中安裝 zip
工具,以便在伺服器上進行檔案壓縮操作。以下為修改的 Dockerfile
片段:
# 安裝 zip 工具
RUN apt-get update && apt-get install -y zip
這段指令確保我們的 Docker 環境中具備壓縮檔案的能力。
接下來,我們會在伺服器端建立一個 /compress
路由,允許使用者提供一個檔案名稱,並將該檔案壓縮為 .zip
格式供下載。以下為新增的程式碼:
app.post('/compress', async function (req, res) {
const { filename } = req.body;
// 如果 compress 資料夾不存在,則建立 compress 資料夾
if (!fs.existsSync('compress')) {
fs.mkdirSync('compress');
}
// 使用 exec 壓縮檔案
exec(`zip compress/${filename}.zip test.txt`, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
res.status(500).send(`Compression error: ${error}`);
return;
}
// 檢查壓縮檔案是否存在
const filePath = path.join(__dirname, 'compress', `${filename}.zip`);
if (fs.existsSync(filePath)) {
// 發送壓縮檔案給使用者
res.download(filePath, `${filename}.zip`, (err) => {
if (err) {
console.error(`Download error: ${err}`);
res.status(500).send('Download error');
} else {
// 清空 compress 資料夾中的檔案
exec('rm compress/*', (rmError) => {
if (rmError) {
console.error(`Error cleaning compress folder: ${rmError}`);
}
});
}
});
} else {
res.status(404).send(stdout + 'File not found');
}
});
});
這段程式碼實現了壓縮檔案的功能,並在完成後將檔案提供給使用者下載。當下載成功後,伺服器還會自動清除壓縮檔案,以防止資料佔用。
雖然這段程式碼看起來執行正常,但我們需要特別留意 exec
函數的使用。exec
函數允許我們執行系統指令,而這也為攻擊者創造了指令注入的可能性。
舉個例子,如果攻擊者發送一個惡意的檔案名稱,如:
filename="myfile && rm -rf /"
這樣會導致伺服器執行 rm -rf /
,刪除所有系統檔案,造成嚴重的安全問題。
在本次實作中,將展示如何透過 curl
指令與伺服器進行互動,並模擬合法的壓縮檔案下載操作與惡意的指令注入攻擊。這樣的實驗可以幫助我們理解應用程式如何應對不同類型的請求,並強化對 Command Injection 威脅的認識。
首先,我們將使用合法的請求來壓縮一個檔案,並下載該壓縮檔案。這裡我們使用 curl
指令發送 POST 請求,並指定檔名為 abc
。
curl -X POST http://nodelab.feifei.tw/compress -H "Content-Type: application/json" -d "{\"filename\":\"abc\"}" --output abc.zip
當指令執行後,伺服器會回傳一個名為 abc.zip
的壓縮檔案,並保存至本地。以下為成功操作的範例結果:
這裡可以看到 abc.zip
成功被下載,表示伺服器正確處理了合法的請求。
接下來,我們嘗試進行指令注入攻擊,藉此檢查伺服器是否存在 Command Injection 的漏洞。我們將在請求中輸入一個惡意指令:
curl -X POST http://nodelab.feifei.tw/compress -H "Content-Type: application/json" -d "{\"filename\":\"test; ls #\"}"
在這個例子中,我們嘗試透過 filename
傳遞 test; ls #
,其中 ; ls
是一個典型的指令注入手法,會列出伺服器目錄中的所有檔案,#
則為註解符號,避免之後的指令被處理。
攻擊結果如下:
zip error: Nothing to do! (compress/test.zip)
Dockerfile
compress
config
controllers
db
migrations
models
node_modules
package-lock.json
package.json
public
routes
server.js
test.txt
upload
views
File not found
可以看到,伺服器回傳了目錄列表,這表明伺服器在處理惡意輸入時,已經執行了 ls
指令,成功列出了當前目錄下的所有檔案與資料夾。這正是指令注入攻擊的一個典型結果,表示伺服器存在 Command Injection 漏洞。
這個實驗展示了應用程式如何在沒有充分保護的情況下,容易被惡意攻擊者利用執行未經授權的指令。
在本次實作中,我們嘗試了兩種不同的請求:合法的檔案壓縮請求與惡意的指令注入攻擊。結果顯示,當應用程式沒有進行充分的輸入驗證時,攻擊者可以藉由指令注入來操控伺服器的行為,甚至存取系統檔案或進行未授權的操作。這強調了在處理使用者輸入時,確保安全性和防範 Command Injection 的重要性。
為了避免這類攻擊,我們可以採取以下幾種防護措施:
execFile
,它不會經過 Shell 解析輸入,能有效防止指令注入。例如:
execFile('zip', [`compress/${filename}.zip`, 'test.txt'], (error, stdout, stderr) => {
// 更安全的執行方式
});
在今天的實作中,我們學習了如何使用 Node.js 實現檔案壓縮功能,同時也了解到在處理系統指令時,指令注入的風險不容忽視。透過適當的輸入檢查、使用更安全的函數,以及遵循最小權限原則,我們可以有效降低 Command Injection 的風險,保護應用程式免於被惡意利用。
什麼是 Command Injection?
A) 用來壓縮檔案的工具
B) 一種將惡意指令注入應用程式並讓伺服器執行的攻擊方式
C) 用來編輯伺服器檔案的指令
D) 一種壓縮檔案的格式
答案:B
在以下哪種情況下容易發生 Command Injection?
A) 應用程式將指令直接傳送給操作系統執行,且未對輸入進行驗證
B) 使用了 HTTPS 傳輸數據
C) 應用程式沒有接受外部輸入
D) 輸入數據受到嚴格檢查並過濾
答案:A
哪個選項能有效防範 Command Injection?
A) 完全不允許使用者輸入
B) 將指令執行換成更安全的函數如 execFile
並進行輸入驗證
C) 讓伺服器接受所有的系統
D) 使用外部指令來處理檔案系統
答案:B
哪個函數容易導致 Command Injection?
A) exec
B) fs.mkdirSync
C) path.join
D) fs.existsSync
答案:A
如何防範 Command Injection?
A) 避免使用 exec 函數
B) 使用正則表達式檢查輸入
C) 限制指令執行的檔案權限
D) 以上皆是
答案:D
安裝 zip 工具並設定壓縮檔案功能
zip
工具的安裝步驟。exec
函數壓縮檔案,並檢查壓縮檔案是否能夠成功回傳給使用者。合法的壓縮檔案測試
curl
請求壓縮檔案並下載,確認功能正常。指令:
curl -X POST http://nodelab.feifei.tw/compress -H "Content-Type: application/json" -d "{\"filename\":\"abc\"}" --output abc.zip
模擬指令注入攻擊測試
curl
請求嘗試進行 Command Injection,測試伺服器是否能夠防禦此類攻擊。指令:
curl -X POST http://nodelab.feifei.tw/compress -H "Content-Type: application/json" -d "{\"filename\":\"test; ls #\"}"
更新程式碼以防 Command Injection
exec
函數為更安全的 execFile
函數,並實施輸入檢查來過濾惡意字串。示例:
execFile('zip', [`compress/${filename}.zip`, 'test.txt'], (error, stdout, stderr) => {
// 更安全的執行方式
});
驗證輸入檢查與安全措施