NodeJS命令行注入:示例及预防
在本文中,我们将学习如何在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命令之前,首先要考虑我们是否一定需要它。毕竟,当你只需要一个凿子时,为什么要用电钻呢?