news 2026/10/1 9:16:51

HCTF 2018 Warm Up题深度解析:PHP文件包含与代码审计实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HCTF 2018 Warm Up题深度解析:PHP文件包含与代码审计实战

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的四步推演

这道题的解法本质是“路径拼接+注释利用+扩展名校验绕过”的组合技。我们把整个流程拆解为四个不可跳过的逻辑节点:

  1. 入口触发点:访问/index.php?file=source.php时,include $dir . $file执行为include 'www/source.php',而source.php中highlight_file(__FILE__)会输出其完整源码;
  2. 线索捕获点:source.php源码末尾的注释// flag in /ffffllllaaaagggg不是装饰,而是命题人留下的唯一路径提示;
  3. 绕过关键点:pathinfo($file, PATHINFO_EXTENSION)只取/ffffllllaaaagggg的扩展名,结果为空字符串,自然不等于'php',从而绕过die('not php!');
  4. 最终读取点: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>&lt;?php highlight_file(__FILE__); // flag in /ffffllllaaaagggg ?&gt;</code></pre>

这个响应证明两点:一是source.php确实被成功包含并执行;二是highlight_file输出了源码(注意&lt;?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需同时满足三个条件:

  1. 绕过正则过滤:preg_match('/flag|php/i', $file)要求$file不含flag或php(不区分大小写);
  2. 绕过扩展名校验:pathinfo($file, PATHINFO_EXTENSION) !== 'php'要求扩展名不为php;
  3. 路径可达性:$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获取实操:三步完成夺旗

  1. 第一步:确认source.php可读
    curl "http://localhost:8080/?file=source.php"
    响应中必须包含// flag in /ffffllllaaaagggg,这是唯一路径线索。

  2. 第二步:构造绕过payload
    curl "http://localhost:8080/?file=/ffffllllaaaagggg"
    注意URL编码:/无需编码,直接使用。如果返回flag{xxx},说明成功;如果返回not php!,检查pathinfo行为或PHP版本。

  3. 第三步:验证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 permittedopen_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安全世界的第一扇门——门后不是漏洞列表,而是对代码逻辑的敬畏之心。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 9:16:38

Switch初代升级Atmosphere 1.5.5适配v18.x全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 9:16:23

JumpServer 502/504故障根因:OpenLDAP依赖链深度排障指南

1. 502/504不是“页面打不开”&#xff0c;而是服务链路在无声断裂JumpServer页面访问报502 Bad Gateway或504 Gateway Timeout&#xff0c;绝大多数人第一反应是“Nginx挂了”或者“JumpServer没起来”。我去年在三个不同客户现场处理过同类问题&#xff0c;结果发现&#xff…

作者头像 李华
网站建设 2026/10/1 9:15:02

GPM 2.0:移动端崩溃可归因、可联动、可预判的质量治理新范式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 9:14:50

yolov8剪枝源码实战:结构化剪枝与RK3588部署优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 9:14:37

多云 GPU 算力纳管与混合调度落地实录

多云 GPU 算力纳管与混合调度落地实录在企业自建 AI 智能体与大模型私有化推理集群的演进中&#xff0c;算力成本与 GPU 资源调度是摆在云原生基础设施团队面前最棘手的现实难题。 随着业务扩张&#xff0c;企业通常会采购多家公有云的 GPU 算力券&#xff0c;同时机房里还散落…

作者头像 李华
网站建设 2026/10/1 9:14:34

AI日报制作全流程:从信息筛选到知识库构建的实操指南

1. 一份“AI日报”到底在记录什么每天早上打开电脑&#xff0c;我做的第一件事不是看邮件&#xff0c;而是花二十分钟把过去二十四小时里AI圈发生的事过一遍。这个习惯坚持了快三年&#xff0c;从最开始只是自己记备忘录&#xff0c;到后来整理成固定的格式发给团队&#xff0c…

作者头像 李华