ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

RCE命令注入从原理到实战:CTFHub通关与安全防御

RCE命令注入从原理到实战:CTFHub通关与安全防御 CTFHub技能树的RCE模块我前前后后刷了三遍每次都有新收获。第一遍是冲着过题去的第二遍开始琢磨绕过思路背后的原理第三遍则是在整理自己写代码时的防御经验。RCE这个词在CTF圈子里出现频率极高全称是Remote Code/Command Execution翻译过来就是远程代码执行或者远程命令执行。很多刚入门的朋友一看到RCE题目就发怵觉得要记一堆payload其实真不是这样。你只要把命令拼接的逻辑、常见过滤的绕过思路、以及业务代码为什么会把用户输入拼进命令这三个问题想清楚CTFHub技能树这套命令注入题基本就能顺畅过关而且这套思路放到真实代码审计里也一样管用。这篇文章我打算从最基础的原理讲起然后按CTFHub技能树的出题思路把无过滤、过滤关键字、过滤空格、前端JS验证这几种典型场景逐个拆解最后再从防御端聊聊开发时怎么避免写出这种漏洞。内容适合正在刷CTFHub的入门选手也适合搞Web开发想补充安全知识的朋友。所有演示都只在CTFHub、本地靶场这类授权环境中进行这点务必注意。1. RCE命令注入是个啥先看一道最基础的CTF题1.1 从CTFHub技能树的一道题目讲起CTFHub技能树的Web方向里RCE是一个独立章节下面又细分了命令注入、代码执行等多个子分类。其中命令注入最典型的入门题就是“无过滤命令注入”。题目通常长这样一个输入框让你提交目标IP地址后台代码大概就是执行ping命令来检测主机是否在线。这种题的要求很简单——找到办法让这台服务器执行你想要的系统命令。我第一次做这道题的时候第一反应是在输入框里填一个正常IP比如127.0.0.1然后加上一个分号再接一个ls。结果还真就把当前目录列出来了。这个操作看起来简单但背后涉及的恰恰是命令注入的核心逻辑后台代码把用户输入原封不动地拼接到了系统命令里没有做任何过滤和转义。分号在Linux Shell里是命令分隔符前面ping命令正常执行之后后面的ls同样会被当作新命令执行。这种“无过滤”类型的题目价值很高。它把所有干扰项都去掉只留最原始的命令注入形态目的就是让你直观地感受漏洞是怎么产生的。很多人在这一步栽跟头不是不会写payload而是压根没意识到输入框里的内容会被拼进系统命令。所以我把这个基础形态放第一小节后面所有花里胡哨的绕过本质上都是在这个基础上叠加了各种过滤条件。1.2 命令注入和代码执行的区别为什么经常被放一起讨论CTF题目里经常会看到RCE这个大帽子下面再细分命令注入Command Injection和代码执行Code Execution进阶一点还会遇到Java的表达式注入EL表达式、SpEL表达式、PHP的模板注入SSTI等。命令注入和代码执行一字之差但攻击结果和触发方式有明显差异。命令注入核心是把用户输入拼到操作系统命令里最终执行的是Shell命令比如ls、cat、id、whoami。武器库里主要是分号、竖线、与或符号这些Shell拼接符。而代码执行则是把用户输入拼到了后端语言的代码层比如PHP的eval函数可以直接把一段字符串当成PHP代码执行最终跑的是PHP语句像phpinfo()、system(id)这些。但这两者经常混在一起讨论因为在真实场景里它们的目标都一样——在目标服务器上执行任意命令拿到权限或敏感信息。很多PHP函数本身就具有双重性质比如system、exec、shell_exec既能作为命令执行的最终调用点也常被恶意用户通过代码执行链去调起来。CTFHub技能树把这几个场景放在同一个RCE章节里实际上是让你把“任意命令执行”的底层能力吃透遇到代码执行时也能自然地往命令执行的方向去打通。判断一道题到底属于哪种类型关键看触发点是命令拼接还是代码拼接这在后文的实操例子中会有更直观的体会。2. 命令注入的底层逻辑为什么一串payload能执行系统命令2.1 Shell拼接规则分号、管道、与或这些符号到底干了什么要理解命令注入首先要熟悉Linux Shell的几个命令连接符。这不是什么高深的知识点但在CTF题里经常用到因为每个符号的语义不同绕过场景也完全不同。分号;顺序执行多条命令不管前面是成功还是失败后面的命令都会执行。比如127.0.0.1;whoami先执行ping再执行whoami互相不影响。这是最常用的拼接方式。管道符|把前一个命令的输出作为后一个命令的输入。127.0.0.1|whoami会先执行ping然后把ping的输出交给whoami处理最终显示的是whoami的结果。这个符号还有一个变种||是逻辑或意思是前一条命令失败才执行后一条则是逻辑与前一条命令成功才执行后一条。这些组合在绕过过滤时经常用到。换行符%0a本质上也相当于命令分隔符URL编码后的换行符在HTTP请求中不会被过滤规则命中因为过滤逻辑可能只匹配了明文符号。用Burp Suite构造请求时%0a这类编码经常能绕开后端的字符串匹配。反引号和$()命令替换。这两个符号可以执行被包裹的命令并把输出替换到原命令中。比如127.0.0.1whoami Shell会先执行whoami再拼接到原命令里最终执行的是127.0.0.1 root这样的命令。这类符号在过滤器只拦截关键字、不拦截符号时特别好用。我在CTFHub上测试过无过滤题目里直接用分号就能通但遇到过滤规则时管道符、换行符、命令替换常常能给你意外的效果。总之理解这些符号的语义是第一个基本功因为后续的payload设计本质上就是“在满足过滤规则的前提下用Shell语法组合出合法的命令表达式”。2.2 PHP里那些危险的RCE函数system、exec、shell_exec、pcntl_execCTFHub技能树的RCE题目大部分基于PHP环境因为PHP是CTF命题最常用的语言之一。PHP里能执行系统命令的函数非常多这些函数既是漏洞成因也是我们解题时的“出口”。最常用的几个我在实践里统计过一个表函数名特性典型用法system()执行外部命令并输出结果system(whoami);exec()执行外部命令但只返回最后一行echo exec(ls -la);shell_exec()执行命令并把完整输出作为字符串返回echo shell_exec(id);反引号PHP中的命令执行运算符等价shell_exec$out ls;passthru()执行外部命令并直接输出原始结果passthru(cat /flag);popen()打开进程文件指针可读可写popen(whoami, r);proc_open()更底层的进程控制函数用法复杂常用于绕过禁用函数pcntl_exec()在进程空间执行指定程序一般配合其他函数用pcntl_exec(/bin/sh, [-c, $cmd]);其中pcntl_exec()在真实环境里常被用来绕过disable_functions。它的特点是会直接用新程序替代当前进程不会产生新的Shell子流程所以有些安全软件或函数禁用策略监视的是exec、system这类点却漏掉了它。CTFHub技能树里专门设置了相关的题目考察的就是“了解多少PHP命令执行函数”这个点。我第一次碰到pcntl_exec题目时尝试用常规的?cmdsystem(cat /flag);结果被禁了。后来注意到环境说明里提示启用了pcntl扩展改用pcntl_exec类的方式配合一个临时文件才把命令打出来。这个过程的收获就是做题不能只盯payload层面的过滤还要关注后端到底有哪些函数可用、哪些被禁用。2.3 一个payload的执行链路拆解拿CTFHub无过滤题目来说假设后端核心代码是?php $target $_REQUEST[ip]; system(ping -c 3 . $target); ?当用户在输入框提交127.0.0.1;ls时实际执行的就是ping -c 3 127.0.0.1;ls这里要补一个细节很多人以为system只是把字符串交给操作系统执行其实PHP的system函数内部会启动一个Shell在Linux下一般是/bin/sh -c然后由这个Shell去解析整条命令。正因为有这一层Shell解析字符串里的分号、管道、通配符等特殊字符才会被解释执行而不是被当成ping命令的普通参数。换句话说命令注入的本质是“Web服务端的开发语言把不可信数据传给了Shell解释器”。这也是为什么后端如果用的是exec且不经过Shell解析的命令数组命令注入的难度会大幅提升。但Web开发中很少有人会写那么严谨的代码多数人直接用字符串拼接这就是漏洞频出的根源。理解这一条链路后你就会明白做题的本质不是找“神奇payload”而是找“后端到底调用了哪个Shell解释器以及用户数据在拼接后处于什么位置”。3. CTFHub技能树实操从无过滤到各种绕过3.1 第一关无过滤命令注入直接把ls写进去CTFHub的“命令注入-无过滤”这道题是新手进入RCE世界的第一道门。题目页面通常只有一个输入框显示“Ping”字样让你输入IP地址。无过滤意味着空格、关键字、特殊符号都能直接用所以我们只需要考虑payload能不能被系统执行。我的通关步骤是先提交127.0.0.1观察回显。如果页面能显示ping结果说明后端确实执行了ping命令。提交127.0.0.1;ls看是否列出目录文件。用cat flag*或者cat flag_*之类的方式读取flag文件。这里有个最容易卡壳的地方flag文件的名字往往是未知的比如flag_123456.php这种随机串。如果你直接cat flag大概率会提示文件不存在。解决方法很简单先用ls看看目录里到底有什么再根据实际文件名去cat。这是最笨但最有效的方法没有任何技巧成分就是“先侦察后拿结果”。无过滤这道题的意义还在于它能让你体会到一个真实的Web应用全流程前端输入框、HTTP请求、PHP接收参数、命令拼接、Shell执行、结果回显。很多人在这一关用的是别人给的payload虽然过了题却不知道每一步发生了什么。我建议你在浏览器开发者工具里看看请求参数在Burp Suite里重放几次改改参数名观察一下响应变化。把这一套流程走顺了后面的题目才不至于靠猜。3.2 过滤flag关键字编码、拼接、通配符三件套过了无过滤这关下一题常见的是“过滤flag关键字”。也就是后台检测到你输入的内容里包含flag这个子串就拒绝执行。如果直接cat flag系统会拦截。但Shell语法本身给了我们很多变通手段。第一种思路是通配符。Linux的路径通配符*、?、[ ]都能帮我们绕过关键字匹配。比如cat flag*或者cat f*。当过滤规则只是简单判断字符串里有没有flag这两个连续字符时fla*显然能绕过因为提交的字符串里根本没有完整出现flag。第二种思路是Shell变量拼接。先声明一个变量或利用环境变量来组合出字符串。比如af;blag;cat $a$b或者利用两个已有的环境变量拼接但这种方式在CTF里更常用于绕过“字符串包含检测”。简单说过滤规则匹配的是你提交的整条payload而Shell在执行时才把变量解算成真实命令所以静态的字符串匹配规则拿它没办法。第三种思路是编码。比较经典的是Base64编码配合管道命令echo cat flag | base64 echo Y2F0IGZsYWc | base64 -d | bash第一步先在本地得到cat flag的Base64串第二步把这个串放进payload服务器执行时会先解码再交给bash执行。这样提交的payload里不但没有flag连cat都没出现只是过滤规则如果没有拦base64和echo这类方法就有效。我在做题时发现CTFHub有些题目对“短字符串”过滤比较宽容Base64方式几乎百试百灵。3.3 过滤空格$IFS家族的替代方案命令注入里另一个高频过滤项是空格。很多题目会检测你的输入里有没有空格字符有就拒绝。但Shell不会因为空格被过滤就无解因为Tab、换行、变量替换都可以充当参数分隔符。这里最经典的工具是$IFS变量。IFS是Shell里的内部字段分隔符Internal Field Separator默认包含空格、Tab、换行。你可以用它来替掉空格。比如cat$IFS/etc/passwd注意这里不能带空格cat和$IFS之间是紧挨着的Shell在展开$IFS后才会把它当作分隔符。还有一种更保险的写法是用花括号包裹${IFS}cat${IFS}/etc/passwd不过这个写法不一定在所有Shell下都生效因为${IFS}展开后是个变量值变量直接和命令拼接时有些Shell会解析成“命令名的一部分”。我在CTFHub实操时更常用的替代是$IFS$1或者${IFS}配合重定向。比如读flag文件时直接写cat${IFS}flag*在多个靶场环境里测试这条命中率最高。除了$IFSTab字符%09URL编码也能替代空格。有些过滤规则只匹配空格但如果同时过滤了Tab那再用Tab就失效了。要灵活一点把$IFS、$IFS$9、{cat,flag}这种花括号展开、重定向符当成备选方案的组合拳。我自己的习惯是建一个payload速查表记载这些替换方案刷题时一个个试而不是现场硬想。3.4 前端JS验证绕过之前先分清是前端卡你还是后端卡你CTFHub技能树里有一部分RCE题目会有“前端JS验证”这个标签指的是页面嵌入了JavaScript代码在浏览器端就拦截了某些关键字或符号。这种题目对小白来说特别喜欢卡人因为你在输入框里敲cat /flag还没到服务器就被浏览器弹窗或者提示拦住很多人就此以为后端也过滤得死死的。遇到JS验证第一步永远是绕过前端而不是去猜后端的过滤规则。最简单的办法是直接用Burp Suite抓包改包绕过浏览器直接给服务器发HTTP请求。或者用浏览器开发者工具把JS逻辑禁掉再重新提交表单。因为前端验证只影响浏览器端的交互体验不影响服务器端是否接收参数。但这里有个关键点前端JS验证题目里后端不一定没有过滤。CTFHub的题目命名带“JS前端验证”说明出题人故意只加了前端限制后端很可能是直接执行命令的。但你自己要养成习惯把前端绕过之后再测一遍基本的;ls确认后端到底过滤了什么。我在刷题时遇到过表面写了“JS前端验证”的题后端居然额外拦掉了flag和空格不测根本不知道最后绕了两层才过。所以正确顺序是先绕过前端然后按常规命令注入逐层探测不要因为题目只提“前端验证”就轻视后端策略。4. 实战中积累的绕过技巧与排查思路4.1 绕过技巧速查表粘贴到笔记里随时翻刷完CTFHub的RCE技能树后我把常用的绕过技巧整理成了一张速查表。以下内容可以复制到自己的笔记里做题时对照使用。被过滤的内容替代思路示例payload空格IFS变量、Tab、重定向符cat${IFS}flagflag关键字通配符、变量拼接、编码cat fla?、cat f*、echo Y2F0IGZsYWccat等命令关键字反斜杠转义、变量拼接c\at flag、cat flag/路径分隔符${PATH:0:1}、cd进目录cd ..;cat flag管道符和分号换行符%0a、逻辑与或、命令替换127.0.0.1%0als反引号被过滤$()命令替换$(cat /flag)数字被过滤极少见可用通配符匹配文件名cat fl*bash被过滤sh、dash、/bin/sh直接用sh -cIP参数本身校验看后端是否校验IP格式有些可以穿参数绕过尝试?ip127.0.0.1;ls或?ip[0]...这个表越往后做越完善。我后来在GitHub上也看到一些开源payload字典但总觉得它们太多太杂反而不如自己在做题过程中按题目类型整理的实用。因为每个CTF平台的出题风格不同过滤点也不同自己整理的速查表往往最能对应自己的知识盲区。4.2 在线靶场踩坑实录常见报错和排查方法在线靶场刷题和本地自己搭环境不一样有很多坑。我在CTFHub上遇到的几类典型问题值得单独拿出来说说。第一类提交后没反应/不显示结果。出现这种情况先检查payload是不是根本没执行成功。比如某些环境禁用了system、exec或者后端代码用了shell_exec但不打印结果。排查方法很简单分别测试;echo 1、;ls、;whoami看哪一步有回显。如果只回显了 “1” 而没有命令输出很可能结果是赋值给变量后没有输出逻辑或者是命令执行成功但输出被吞了。这时候可以考虑用;ls 1.txt把结果写到文件里再用另一个接口读取。第二类在线平台对并发和请求频率有限制。有的靶场反向代理会拦截短时间内的大量请求。我一开始写脚本批量跑payload时经常整段被WAF拦截页面直接返回403。这种时候不要硬刚把脚本延时调大每次请求之间间隔1到2秒或者改用手工测试效率反而更高。第三类payload里的特殊字符被在线编辑器转义。有些平台的前端页面会在提交时把#、%这类字符自动做URL编码导致你看到的payload和实际发出去的payload不一样。解决方法是用Burp Suite看真实请求亲自改HTTP报文而不是依赖页面输入框。第四类CTFHub的题目环境有时会过期或者数据被重置。如果你做一道题做到一半发现flag内容变了甚至ip变了可能是环境被重置了。这种情况只需要重新获取环境信息把IP、端口刷新一下重新提交即可。4.3 怎么系统化积累payload而不只是背题很多人在网上找一份“命令注入payload大全”背来背去一到新题目面前依然抓瞎。原因是payload只是结果没有形成“为什么这么写”的思维链路。我自己的经验是把每次成功绕过的题目拆成三个要素存进笔记过滤规则是什么拦了哪个字符、哪个命令、哪种编码绕过原理是什么利用了Shell的什么特性payload中最关键的那个符号或语法是哪一块比如我记录“用花括号IFS绕过空格”时会备注“原理是Shell变量展开发生在词法解析之后做静态字符串匹配的WAF往往只检查原始文本没检查展开后的执行结果。记住这个点比记住${IFS}更重要。” 这样积累几个月后你看新题目的方式会从“这个payload我没见过”变成“这个过滤点可以用Shell展开绕或者用编码绕或者用命令替换绕”。这种能力不是背面试题能得来的必须通过大量实践去沉淀。5. 从攻到防开发人员怎么避免自己的代码变成RCE入口5.1 编码层防御白名单、转义、禁用危险函数刷CTF题和真实开发是一体两面。我做过几年PHP后端开发在真实项目中见过不少同事写出和CTF题几乎一模一样的代码。比如拼接ping命令检测存活、拼接shell脚本备份数据库、拼接ffmpeg命令处理视频这些场景一旦用户可控参数被拼进去就是妥妥的命令注入。先说最简单有效的三层防御思路。第一层白名单校验。如果业务上只需要IP那就严格校验IP地址格式不合法直接拒绝。PHP可以用filter_var($input, FILTER_VALIDATE_IP)Java可以用InetAddress.getByName()再验证Python可以用ipaddress.ip_address()。白名单的好处是从根上杜绝了特殊符号进入命令的可能性因为连合法格式都不满足。第二层参数化调用避免字符串拼接进入Shell。PHP里如果必须执行外部命令优先用带参数数组的形式exec(/bin/ping, $output);在PHP 7.4以上还可以考虑用shell_exec配合escapeshellarg做转义。escapeshellarg会给参数加单引号并转义内部单引号这样用户输入即便包含分号也只会被当成一个普通参数字符串不会被Shell解释成命令分隔符。但注意这个方法有它自己的边界比如传入的参数本身需要特殊处理时它不一定满足所有场景所以白名单才是最稳的。第三层禁用危险函数。生产环境里如果实在用不到PHP的系统命令执行函数在php.ini里把system、exec、shell_exec、passthru、popen、proc_open加入disable_functions。这也是很多安全加固方案的常规操作成本低、见效快缺点是要提前和运维确认哪些业务真的需要这些函数。以上三层是经典的“本质安全”比单独装一个WAF或者写一堆过滤正则要可靠得多。因为过滤正则永远存在绕过空间CTFHub的那些绕过技巧说明了一切。5.2 代码审计视角如何快速定位命令注入点最后聊聊审计视角。不管你是甲方安全工程师、乙方渗透测试人员还是自己写代码想复盘命令注入点的定位基本有固定套路。第一是全局搜索危险函数。PHP项目搜system(、exec(、shell_exec(、passthru(、popen(、proc_open(、pcntl_exec(。把这些点全部列出来然后逐一判断参数是否由外部输入控制。很多自动化的代码扫描工具也能做这件事但我一直觉得人工确认不能省因为误报率很高。第二是追踪输入来源。搜到危险函数后往前看变量的传递链路。是$_GET、$_POST、$_REQUEST直接取值还是经过了某层封装后又拼接到命令字符串里有没有经过过滤函数过滤函数能不能被绕过审计时把这条链路画清楚漏洞基本就浮出水面了。第三是测试命令拼接形态。看到system(ping -c 3 . $target)这种形式第一时间应该意识到注入点就在$target。如果业务层不能改成白名单校验至少要用escapeshellarg包一层再拼。如果看到shell_exec(tar -czf . $backup . . $folder . )这种还需要防范目录遍历和命令注入两个维度的问题。我在Code Review时经常跟开发同事说一句话不要相信用户输入尤其是当它要进入操作系统命令的时候。所有用户可控参数都要默认当成恶意输入来处理即使当前业务场景看起来人畜无害。因为攻击者永远比你更了解你的代码里那些“边界”。做安全这件事攻防知识从来不是对立的。你在CTFHub上刷过的每一次绕过最终都可能变成你在真实系统上修补的一个漏洞。我希望这篇关于命令注入的文章能帮你在解题之外建立起更完整的知识框架。下一次遇到RCE题目先别急着找payload试着想想它过滤了什么、为什么能绕过、如果换成你写这段代码会怎么防——这三步走顺了RCE这道坎就算真正跨过去了。
返回列表