在本文中,我們將學習如何在NodeJS中使用命令行函數進行注入漏洞攻擊。
現代網站可以是一個複雜的軟件,它由許多分佈在不同環境中的部分組成。如果你的應用程序沒有得到有效的保護,那麼分佈在這些環境中的每一個組成部分都有可能受到命令行注入漏洞的攻擊。
本文將介紹如何在NodeJS中使用shell命令行函數時進行注入漏洞攻擊。同時我們還將探討一些有關如何防範這些攻擊的技術。
下面讓我們開始吧!
什麼是命令行注入漏洞?
簡單來講,當你的應用程序接受不安全的用戶輸入並將輸入的內容作爲執行操作系統命令的參數時,就有可能產生這樣的漏洞。命令行注入攻擊的目的是通過合法的命令,以便攻擊者在目標系統中執行任意命令。這些輸入可以來自任意用戶可修改的源,例如網頁表單、cookies、HTTP頭等。
注意命令行注入攻擊與代碼注入攻擊不同。後者主要破壞應用程序本身,例如前端JavaScript代碼注入攻擊可能會在用戶的瀏覽器上執行一些惡意代碼。但攻擊通常會被限制在瀏覽器範圍內。而命令行注入攻擊的目標則是底層操作系統,因此更具破壞性。
攻擊過程解析
讓我們看一下上面這張圖。系統接受用戶的輸入並組成一個完整的命令。假設該輸入是有效的,並且用戶沒有輸入惡意的命令,那麼系統將執行命令並返回正確的結果。
但是,如果惡意用戶想要嘗試系統的漏洞,那麼他們可以在命令的後面附加自己的命令來重載應用程序的系統命令。例如,在Windows中的DOS命令行,你可以使用&符號附加額外的命令。在Linux系統中,你可以使用;符號達到相同的效果。此時應用程序將執行多個命令並將結果返回給用戶。
攻擊者甚至都不用關心命令的返回結果。如果由於某種原因應用程序沒有返回結果,那麼攻擊者可以發送curl之類的命令來ping被攻擊的服務器,以確認命令行注入攻擊是否成功。這使得通過自動化工具來查找和攻擊具有這種類型漏洞的應用程序和網站變得更加容易。
下面讓我們通過一個實際的例子來了解更多有關命令行注入漏洞的內容。
一個存在漏洞的網站示例
我們將創建一個小的有風險的網站來演示這個漏洞。我們的網站是衆多介紹shell命令的網站中的一個,它教你學習shell命令並幫助你如何通過命令列出系統目錄中的內容。它接受用戶輸入目錄的完整路徑,然後返回目錄中的內容。
讓我們開始setup。
設置
首先確保你已經安裝了需要的NodeJS和npm。如果沒有的話,需要先安裝。
接下來,運行以下命令來初始化項目。該示例假設你在Mac或Linux環境中運行命令,或者在Windows WSL2上運行。
mkdir nodejs-command-injection cd nodejs-command-injection npm init -y npm install express npm install pug
這些命令將創建項目文件夾並安裝Express和Pug。我們將使用Express的web服務器功能來加載網站,然後使用Pug來實現一些快速模板功能。
添加功能
現在讓我們創建一個非常簡單的頁面,該頁面接受用戶輸入目錄的路徑。創建文件nodejs-command-injection/server.js,並複製下面的代碼:
const express = require('express'); const {exec} = require('child_process'); const app = express(); const port = 3000; const pug = require('pug'); // Listen in on root app.get('/', (req, res) => { const folder = req.query.folder; if (folder) { // Run the command with the parameter the user gives us exec(`ls -l ${folder}`, (error, stdout, stderr) => { let output = stdout; if (error) { // If there are any errors, show that output = error; } res.send( pug.renderFile('./pages/index.pug', {output: output, folder: folder}) ); }); } else { res.send(pug.renderFile('./pages/index.pug', {})); } }); app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`); });
接下來,添加模板文件以顯示錶單。創建文件nodejs-command-injection/pages/index,並複製下面的代碼:
html
head
title="NodeJS Command Injection Example"
body
h1="List Folders"
form(action="/" method="GET")
input(type="text" name="folder" value=folder)
button(type="submit") Submit
code
pre #{output}
很好。我們使用Pug簡潔的標記風格創建了一個簡單的HTML頁面。
現在我們可以通過運行這個命令來啓動應用程序:node server.js。如果運行正常,在瀏覽器中輸入http://localhost:3000/,你應該可以看到以下頁面。
如果一切進展順利,接下來讓我們詳細查看漏洞。
探查漏洞
提醒一下,我們假設你是在基於Mac、Linux或Windows WSL2的環境中運行該網站。這一點很重要,因爲我們將在接下來的示例中使用相應的命令在文件夾中查找內容。
現在,讓我們來測試一下我們的網站。輸入/usr並點擊Submit按鈕。你應該看到文件夾中的內容輸出到列表中。
很好,看起來我們的網站實現了預期的功能。如果你在輸入框中輸入不同的命令,例如ps,則無法正常工作,它只列出了文件夾。但真的是這樣嗎?試試在輸入框中輸入/usr;ps。
嗯,出問題了!我們列出了託管我們網站的服務器上的所有用戶。這完全有可能,因爲我們使用分號作爲命令的分隔符,這將告訴shell執行兩個單獨的命令。
如果你查看下面的代碼,可以看到ls命令首先執行,然後命令與用戶的輸入連接起來。代碼原本期望只執行一個命令,但是如果用戶破壞了這個過程,那麼就可以在服務器上執行他想要的任何命令了。
... exec(`ls -l ${folder}`, (error, stdout, stderr) => { ....
那如何改進我們的代碼呢?接下來我們將介紹幾個可用的選項。
防止命令行注入
如上所述,這個漏洞存在的原因是我們爲惡意用戶打開了一扇可以輸入任意內容的大門。理想情況下,不應該將用戶輸入的任意內容傳遞到shell命令。這種做法是非常糟糕的,應該絕對避免。
說到這裏,假設在極少數的情況你恰好需要以上示例中所演示的這個功能,那如何才能避免命令行注入漏洞呢?
驗證輸入
不僅是對於我們的示例網站而言,在多數情況下我們都應該遵循:永遠不要相信用戶輸入的內容。應該始終對用戶輸入的內容進行過濾處理,從而避免惡意用戶將其用作破壞性命令的傳遞方法。
至少,你應該做到以下幾點:
- 使用白名單以確保只有允許的命令和參數進入系統執行。
- 對輸入的內容進行驗證,並確保不允許某些特殊字符進入系統,而僅允許你認爲有效的字符可以被輸入。
使用execFile()函數代替exec()函數
NodeJS有一個非常方便的函數execFile,它允許你直接調用一個可執行的文件,而不是使用原始的shell訪問。使用這個函數會將整個執行過程更加安全,因爲用戶不能運行任何額外的命令以及以參數的方式傳遞給命令行來執行。
如果我們更新代碼,它看起來像這樣:
const {exec, execFile} = require('child_process'); ... app.get('/', (req, res) => { const folder = req.query.folder; ... execFile(`/usr/bin/ls`, ['-l', folder], (error, stdout, stderr) => { ... } }
使用API級別的函數代替Shell命令
避免命令行注入漏洞最安全的方法之一是通過使用編程環境自帶的函數來替換Shell命令——本例中我們使用NodeJS編程環境。NodeJS已經內置了用來列出目錄內容的函數,我們只需要在代碼中導入文件系統模塊就可以使用這些函數。
下面的代碼顯示了一個使用fs模塊的示例。
const fs = require('fs'); const folder = req.query.folder; if (folder) { // Read the files in the folder using the fs module fs.readdir(folder, function (err, files) { //handling error if (err) { return console.log('Unable to scan directory: ' + err); } let fileOutput = ''; files.forEach(function (file) { fileOutput += `${file}\n`; }); res.send( pug.renderFile('./pages/index.pug', { output: fileOutput, folder: folder, }) ); }); }
這種方法比我們一開始使用的方法要安全得多。在完成核心功能之前,探索編程環境本身提供了哪些功能總是一個不錯的主意!
總結
在本文中,我們創建了一個帶有命令行注入漏洞的示例網站。我們還研究了一些可選項,以便更好地保護自己的網站免受這些類型的攻擊。
接下來,如果有興趣我們可以繼續瞭解命令行注入漏洞如何影響其它的編程環境,比如Rust和Java。另外一個有趣的點是NodeJS中的SQL注入指南。
Shell訪問是一種強大的工具,同時它也是一把雙刃劍。在使用Shell命令之前,首先要考慮我們是否一定需要它。畢竟,當你只需要一個鑿子時,爲什麼要用電鑽呢?