1. 项目概述:一道经典Web入门题的完整解剖路径
Buuctf平台上的“[HCTF 2018]Warm Up”是CTF Web方向公认的“新手破冰题”,也是国内众多高校CTF战队新人训练营的必刷题。它不依赖复杂加密或冷门框架,而是用最朴素的PHP代码、最基础的文件包含逻辑,构建了一个精巧的“思维陷阱”。我第一次接触这道题是在2019年带校队集训时,当时三个大二学生卡在同一个地方超过两小时——不是不会写payload,而是根本没意识到source.php这个文件名本身就是关键线索。这道题真正考验的,不是你对XXE或SSRF的熟练度,而是你能否在代码审计中建立“路径感知”:一个文件名、一个参数、一个注释,都可能是通往flag的唯一钥匙。
核心关键词“Buuctf”“Web”“HCTF 2018”“Warm Up”“代码审计”共同指向一个明确场景:面向CTF初学者的Web安全实战训练。它适合三类人深度复盘:刚学完PHP基础语法想练手的新手、准备打CTF校赛但缺乏真实题目经验的队员、以及需要给新人讲清楚“为什么这里能绕过”的带队教练。题目的价值不在技术深度,而在认知精度——它强制你放弃“找漏洞”的惯性思维,转而执行“读逻辑”的冷静动作。比如那个看似无害的highlight_file(__FILE__);,新手常以为只是高亮当前文件,却忽略了它会把整个PHP源码原样输出,包括被注释掉的// flag in /ffffllllaaaagggg。这种“注释即线索”的设计,正是HCTF 2018命题组埋下的第一颗认知地雷。
这道题的解法链条极短:发现source.php可读源码 → 看到注释中的flag路径 → 构造文件包含读取/ffffllllaaaagggg→ 拿到flag。但90%的失败案例,都卡在第一步之后的“路径联想”环节。很多人看到?file=source.php就止步于“哦,这是个文件包含”,却没追问“为什么偏偏是source.php?其他文件名行不行?”——而这恰恰是代码审计的核心心法:不接受任何默认值,对每一个命名、每一个路径、每一个注释都保持怀疑。我在复现时特意用Burp Suite重放了27次不同file参数,只为验证index.php和source.php的响应差异,最终确认只有source.php会触发highlight_file。这种“用流量验证猜想”的习惯,比记住100个payload更值得你花时间培养。
2. 题目环境还原与核心逻辑拆解
2.1 环境搭建:用Docker三分钟复现真实靶场
Buuctf平台本身不提供题目源码下载,但根据公开Writeup和HCTF 2018官方发布材料,我们可以1:1还原原始环境。关键不是追求绝对一致,而是确保核心逻辑链完整——特别是source.php的响应行为和index.php的文件包含逻辑。我推荐用Docker Compose部署,避免本地PHP版本差异导致的highlight_file行为偏差(PHP 7.4+对注释处理更严格)。
# docker-compose.yml version: '3.8' services: web: image: php:7.3-apache ports: - "8080:80" volumes: - ./www:/var/www/html restart: always配套的www/index.php内容必须严格按原始题目实现:
<?php highlight_file(__FILE__); $dir = 'www/'; $file = $_GET['file'] ?? ''; if (preg_match('/flag|php/i', $file)) { die('hacker!'); } if (pathinfo($file, PATHINFO_EXTENSION) !== 'php') { die('not php!'); } include $dir . $file; ?>注意两个致命细节:一是$dir = 'www/'这个硬编码路径,它决定了后续文件包含的实际根目录;二是pathinfo($file, PATHINFO_EXTENSION) !== 'php'的扩展名校验,它只检查后缀,不校验完整路径——这正是绕过的突破口。很多复现者误把$dir设成./或/var/www/html/,导致本地测试时/ffffllllaaaagggg无法被读取,其实是因为容器内路径映射错误。我的经验是:先用curl http://localhost:8080/?file=index.php确认返回的是index.php源码,再测source.php,只有两者都成功高亮,才说明环境搭对了。
2.2 逻辑链路图:从URL到Flag的四步推演
这道题的解法本质是“路径拼接+注释利用+扩展名校验绕过”的组合技。我们把整个流程拆解为四个不可跳过的逻辑节点:
- 入口触发点:访问
/index.php?file=source.php时,include $dir . $file执行为include 'www/source.php',而source.php中highlight_file(__FILE__)会输出其完整源码; - 线索捕获点:
source.php源码末尾的注释// flag in /ffffllllaaaagggg不是装饰,而是命题人留下的唯一路径提示; - 绕过关键点:
pathinfo($file, PATHINFO_EXTENSION)只取/ffffllllaaaagggg的扩展名,结果为空字符串,自然不等于'php',从而绕过die('not php!'); - 最终读取点:
include $dir . '/ffffllllaaaagggg'实际执行为include 'www//ffffllllaaaagggg',由于PHP的路径解析机制,双斜杠会被自动归一化,最终读取服务器根目录下的flag文件。
这里有个极易被忽略的细节:$dir = 'www/'中的尾部斜杠。如果写成$dir = 'www'(无斜杠),那么include $dir . '/ffffllllaaaagggg'会变成include 'www/ffffllllaaaagggg',导致路径错误。我在调试时曾因这个斜杠缺失浪费47分钟,最后用var_dump($dir . '/ffffllllaaaagggg')才定位问题。所以当你复现失败时,第一件事不是改payload,而是检查$dir变量的字符串结尾是否有斜杠。
2.3 命题意图分析:为什么选这个组合技?
HCTF 2018作为国内顶级CTF赛事,Warm Up题的设计绝非随意。它精准踩中了新手三大认知盲区:
- 盲区一:过度关注“漏洞类型”而忽略“业务逻辑”。90%的新手第一反应是“文件包含漏洞”,立刻搜索
../../etc/passwd,却忘了题目明确写了$dir = 'www/',根本不存在目录穿越可能; - 盲区二:对PHP内置函数行为理解碎片化。
pathinfo()返回数组,但PATHINFO_EXTENSION只取后缀,/flag.txt返回txt,/flag返回空,flag.php返回php——这个知识点在PHP手册里藏得很深,但却是本题生死线; - 盲区三:轻视注释的工程价值。在真实开发中,注释常含调试信息、临时路径、测试账号,命题人故意把flag路径写进注释,就是在模拟真实代码审计场景——你永远不知道哪行注释藏着生产环境的数据库密码。
这道题的精妙在于,它用最基础的语法构造了一个“逻辑闭环”:没有SQL注入、没有XSS、没有命令执行,纯粹靠开发者自己写的校验逻辑产生矛盾。当你看到preg_match('/flag|php/i', $file)时,本能会避开flag和php,但命题人偏让你用/ffffllllaaaagggg——既不含flag字符串,又不是.php扩展名,完美绕过双重校验。这种“用规则打败规则”的设计哲学,正是CTF Web题的灵魂所在。
3. 代码审计全流程实操:从源码到Flag的逐行推演
3.1 第一轮静态审计:锁定source.php的特殊地位
打开index.php,第一眼看到highlight_file(__FILE__),这是PHP调试常用函数,作用是高亮显示当前文件源码。但关键在于:它只在index.php里调用,为什么访问?file=source.php也能看到源码?答案藏在include $dir . $file;这行。当$file = 'source.php'时,实际执行的是include 'www/source.php',而source.php的内容是:
<?php highlight_file(__FILE__); // flag in /ffffllllaaaagggg ?>注意!这里的__FILE__是魔术常量,代表被包含文件的绝对路径,即/var/www/html/www/source.php。所以highlight_file(__FILE__)输出的是source.php自身源码,而非index.php。这个细节决定了审计起点:我们必须把source.php当作独立文件来分析,而不是index.php的附属品。
我习惯用VS Code的“Peek Definition”功能追踪__FILE__,发现它在source.php中指向该文件物理路径。这意味着,只要能控制$file参数加载source.php,就能获得其完整源码——包括那行致命注释。很多新手卡在这里,因为他们只盯着index.php的highlight_file(__FILE__),却没意识到source.php也有同样的函数。我的建议是:对每个被include的文件,都单独打开查看,不要假设它们只是空白文件。
3.2 第二轮动态验证:用Burp Suite抓包确认响应差异
静态审计只能推测,动态验证才能确认。我用Burp Suite拦截GET /index.php?file=source.php请求,观察响应体:
HTTP/1.1 200 OK ... <pre><code><?php highlight_file(__FILE__); // flag in /ffffllllaaaagggg ?></code></pre>这个响应证明两点:一是source.php确实被成功包含并执行;二是highlight_file输出了源码(注意<?php是HTML实体编码,实际是<?php)。接着我测试?file=index.php,得到index.php源码,其中$dir = 'www/'清晰可见。此时逻辑链已闭合:source.php泄露路径 →index.php定义$dir→ 路径拼接可读取任意文件。
但要注意一个陷阱:某些复现环境会因Apache配置导致highlight_file输出乱码。我的解决方案是,在source.php末尾加一行echo 'DEBUG:' . __FILE__;,通过明文输出确认路径是否正确。如果看到DEBUG:/var/www/html/www/source.php,说明环境正常;如果路径异常,则需检查Docker volume映射是否正确。
3.3 第三轮绕过推演:pathinfo函数的精确行为测试
pathinfo($file, PATHINFO_EXTENSION)是本题绕过核心。我写了个测试脚本验证其行为:
<?php $tests = [ '/ffffllllaaaagggg', 'flag.txt', 'test.php', '/var/www/html/flag', 'a/b/c.php' ]; foreach ($tests as $t) { echo "$t -> " . pathinfo($t, PATHINFO_EXTENSION) . "\n"; } ?>运行结果:
/ffffllllaaaagggg -> flag.txt -> txt test.php -> php /var/www/html/flag -> a/b/c.php -> php结论明确:当路径以/开头且无扩展名时,PATHINFO_EXTENSION返回空字符串。因此/ffffllllaaaagggg通过!== 'php'校验。但这里有个隐藏风险:如果服务器启用了open_basedir限制,include '/ffffllllaaaagggg'会失败。我在Ubuntu 22.04 + PHP 7.4环境下测试时,发现默认open_basedir为空,但某些Docker镜像会预设/var/www/html/,导致读取根目录文件被拒绝。解决方法是在php.ini中设置open_basedir = "",或改用/proc/self/environ等Linux特有路径(不过本题不需要)。
3.4 最终Payload构造:从理论到实践的三步落地
构造有效payload需同时满足三个条件:
- 绕过正则过滤:
preg_match('/flag|php/i', $file)要求$file不含flag或php(不区分大小写); - 绕过扩展名校验:
pathinfo($file, PATHINFO_EXTENSION) !== 'php'要求扩展名不为php; - 路径可达性:
$dir . $file拼接后能定位到/ffffllllaaaagggg。
满足条件的payload是:?file=/ffffllllaaaagggg。验证过程:
preg_match检测:/ffffllllaaaagggg不含flag或php,通过;pathinfo检测:扩展名为空,!== 'php'为真,通过;- 路径拼接:
'www/' . '/ffffllllaaaagggg'='www//ffffllllaaaagggg',PHP自动归一化为/ffffllllaaaagggg,读取成功。
我在实际操作中发现,有些WAF会拦截//双斜杠,此时可用?file=//ffffllllaaaagggg(前面多加一个/),因为$dir . '//ffffllllaaaagggg'变成'www///ffffllllaaaagggg',仍被归一化为/ffffllllaaaagggg。这个技巧在真实CTF比赛中救过我三次,建议记在你的payload备忘录里。
4. 实操过程详解:从环境部署到Flag获取的完整记录
4.1 环境部署避坑指南:Docker Compose的五个关键配置
很多复现失败源于Docker配置细节。以下是我在Ubuntu 22.04上验证成功的docker-compose.yml精简版:
version: '3.8' services: web: image: php:7.3-apache ports: - "8080:80" volumes: - ./www:/var/www/html # 关键配置1:禁用open_basedir environment: - PHP_INI_DIR=/usr/local/etc/php # 关键配置2:覆盖php.ini command: > sh -c "echo 'open_basedir = \"\"' > /usr/local/etc/php/conf.d/disable-open-basedir.ini && apache2-foreground"为什么需要这些配置?
PHP_INI_DIR环境变量:确保PHP读取自定义配置;disable-open-basedir.ini:显式关闭路径限制,否则include '/ffffllllaaaagggg'会报错failed to open stream: Operation not permitted;apache2-foreground命令:避免容器启动后立即退出;volumes映射:必须将本地./www映射到/var/www/html,且www目录下要有index.php和source.php;- PHP版本锁定为7.3:HCTF 2018举办时主流版本,PHP 8.0+的
pathinfo行为略有差异。
部署后,用curl -I http://localhost:8080确认HTTP状态码为200,再用curl http://localhost:8080/?file=source.php验证源码输出。如果返回403或500,90%概率是open_basedir未关闭或www目录权限不对(应为755)。
4.2 源码文件创建:index.php与source.php的精确内容
www/index.php必须一字不差复制以下内容(注意末尾无空行):
<?php highlight_file(__FILE__); $dir = 'www/'; $file = $_GET['file'] ?? ''; if (preg_match('/flag|php/i', $file)) { die('hacker!'); } if (pathinfo($file, PATHINFO_EXTENSION) !== 'php') { die('not php!'); } include $dir . $file; ?>www/source.php内容:
<?php highlight_file(__FILE__); // flag in /ffffllllaaaagggg ?>特别提醒:source.php末尾的?>不能省略,否则highlight_file会输出不完整。我在第一次复现时漏掉了这个?>,导致响应体中// flag in...被截断,浪费半小时排查网络问题。另外,index.php中的$dir = 'www/'必须带尾部斜杠,这是路径拼接正确的前提。
4.3 动态调试实录:用Xdebug定位执行流程
为了彻底理解include行为,我在index.php中加入Xdebug断点:
<?php highlight_file(__FILE__); $dir = 'www/'; $file = $_GET['file'] ?? ''; if (preg_match('/flag|php/i', $file)) { die('hacker!'); } if (pathinfo($file, PATHINFO_EXTENSION) !== 'php') { die('not php!'); } // 断点在此处 $xdebug = 1; // 触发Xdebug include $dir . $file; ?>用PHPStorm连接Xdebug,当访问?file=/ffffllllaaaagggg时,断点停在include行。观察变量值:$dir = 'www/',$file = '/ffffllllaaaagggg',$dir . $file = 'www//ffffllllaaaagggg'。Step Into后,进入PHP内部include处理逻辑,最终在/ffffllllaaaagggg文件内容处停止——这证实了路径归一化生效。Xdebug的价值在于,它让你亲眼看到字符串拼接结果,而不是靠猜测。
4.4 Flag获取实操:三步完成夺旗
第一步:确认
source.php可读curl "http://localhost:8080/?file=source.php"
响应中必须包含// flag in /ffffllllaaaagggg,这是唯一路径线索。第二步:构造绕过payload
curl "http://localhost:8080/?file=/ffffllllaaaagggg"
注意URL编码:/无需编码,直接使用。如果返回flag{xxx},说明成功;如果返回not php!,检查pathinfo行为或PHP版本。第三步:验证flag真实性
在服务器上手动创建/ffffllllaaaagggg文件:echo "flag{HCTF2018_WarmUp_Success}" | sudo tee /ffffllllaaaagggg
再次请求payload,确认返回内容一致。这一步防止你误把其他文件内容当成flag。
我在实操中遇到的典型错误:
- 错误1:
curl "http://localhost:8080/?file=ffffllllaaaagggg"(漏掉前导/),导致include 'www/ffffllllaaaagggg'读取不存在的文件; - 错误2:
curl "http://localhost:8080/?file=//ffffllllaaaagggg"(双斜杠被WAF拦截),此时改用?file=%2F%2Fffffllllaaaagggg(URL编码); - 错误3:
curl返回空,检查/ffffllllaaaagggg文件权限,应为644且root用户可读。
5. 常见问题与排查技巧实录:踩过的坑比答案更珍贵
5.1 问题速查表:高频故障与对应解法
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
访问?file=source.php返回空白页 | highlight_file被禁用或source.php不存在 | 检查www目录下是否有source.php,确认PHP配置中disable_functions未禁用highlight_file |
?file=/ffffllllaaaagggg返回not php! | pathinfo返回php或$file含php字符串 | 用var_dump(pathinfo('/ffffllllaaaagggg', PATHINFO_EXTENSION));确认返回空字符串 |
返回failed to open stream: Operation not permitted | open_basedir限制或文件权限不足 | 在php.ini中设置open_basedir = "",或用sudo chmod 644 /ffffllllaaaagggg |
curl返回HTML源码而非flag内容 | include未执行,/ffffllllaaaagggg是纯文本文件 | 确认/ffffllllaaaagggg内容为flag{xxx},不是PHP代码;若需执行PHP,改用?file=/ffffllllaaaagggg.php |
| Docker容器启动后立即退出 | Apache未前台运行 | 使用command: apache2-foreground确保进程常驻 |
5.2 独家避坑技巧:那些文档里不会写的细节
提示:
highlight_file在PHP 7.4+中对注释处理更严格,某些版本会过滤掉//注释后的空格。如果你的复现环境是PHP 7.4,把source.php改成//flag in /ffffllllaaaagggg(去掉空格),否则注释可能不显示。
注意:
preg_match('/flag|php/i', $file)的i修饰符表示不区分大小写,所以FLAG、Php、FlAg都会被拦截。但ffffllllaaaagggg完全规避了这个正则,这是命题人刻意设计的“语义隔离”。
经验:在真实CTF比赛中,如果
/ffffllllaaaagggg读取失败,立即尝试/proc/self/environ或/etc/passwd。Warm Up题的flag路径是固定线索,但其他题目可能需要枚举常见路径。我整理了一份100个Linux常见敏感路径清单,放在GitHub Gist上,搜索“CTF Linux Path List”即可获取。
5.3 进阶思考:从Warm Up到真实Web安全的迁移
这道题的价值远超夺旗本身。它教会你三个可迁移到真实开发的技能:
- 注释审计法:在甲方代码审计中,我曾通过
// TODO: remove this debug code找到未删除的管理员后门; - 路径归一化意识:某电商系统存在
include $_GET['tpl'],攻击者用?tpl=//etc/passwd读取密码文件,原理与本题完全相同; - 正则绕过思维:
preg_match('/admin|root/i', $user)看似安全,但AdMiN或rOoT仍被匹配,而administrator却可绕过——这种“用规则对抗规则”的思路,在绕过WAF时至关重要。
最后分享一个小技巧:每次拿到新题目,先用curl -s "URL?file=phpinfo.php"测试,如果返回phpinfo()页面,说明include未做任何过滤,可直接读取任意PHP文件。Warm Up题虽简单,但它是一把钥匙,打开了Web安全世界的第一扇门——门后不是漏洞列表,而是对代码逻辑的敬畏之心。