简介:这是一套面向网络安全初学者与CTF爱好者的Web方向练习源码环境,以备份形式打包,适合用来搭建本地靶场、熟悉常见Web漏洞的成因与利用方式。压缩包共13个文件,约135KB,以php脚本为主,辅以html页面、log日志、pcap流量包、txt文本与gif图片,分别对应题目入口、页面结构、运行记录、流量分析与线索素材,整体轻量、便于快速部署和反复调试。资源中预设了信息泄露、文件包含、编码线索等典型题型,读者可通过分析服务端配置信息、梳理日志与流量记录、追踪隐藏路径,逐步还原出题思路并完成解题。目前已有715人学习下载,适合作为入门级Web安全实战素材,帮助理解常见安全问题的排查与防御方法。
1. 拿到一套 CTF Web 源码环境,先别急着翻 flag
很多人第一次接触 CTF Web 方向,卡住的地方不是不会做题,而是环境跑不起来。你从题库或者别人手里拿到一个ctf_web源码环境.zip,解压出来一堆目录,有index.php、有flag.php、有docker-compose.yml,也可能只有一个孤零零的www文件夹,然后呢?直接双击打开?用浏览器访问本地文件?大概率是一片空白或者源码被当文本读出来。
这套东西的本质,是把一道 Web 题从「线上靶机」搬到「本地可控」。它解决的核心问题是:你不需要等比赛开放、不需要担心靶机被别人打崩、可以随便改源码看逻辑、可以反复重置数据库。适合两类人——刚入门 CTF Web 想搞懂题目怎么构造的新手,以及想自己出题、搭常态化训练环境的老手。
但「简单」两个字是陷阱。简单意味着它省略了部署文档,省略了依赖说明,甚至省略了入口提示。你得自己判断这是什么类型的题、用什么语言写的、需不需要数据库、跑在哪个端口。这一章先把判断思路立住,后面几章再动手。
2. 解压之后先做三件事:认语言、找入口、判依赖
拿到压缩包,不要上来就docker-compose up。我一般会先花五分钟做静态侦察,这三件事做完,后面能省掉大量翻车时间。
2.1 用文件后缀和目录结构判断技术栈
先看目录树。CTF Web 题常见的技术栈就那么几种,后缀基本能暴露一切:
| 文件特征 | 技术栈 | 常见题型 |
|---|---|---|
.php为主,有flag.php | PHP | 文件包含、反序列化、命令执行 |
.py+requirements.txt | Python(Flask/Django) | SSTI、反序列化、逻辑漏洞 |
.js+package.json | Node.js | 原型链污染、SSRF |
.java+pom.xml | Java | 反序列化、SpEL 注入 |
只有.html+.js | 纯前端 | 原型链、XSS、JS 逆向 |
判断完语言,再看有没有Dockerfile或docker-compose.yml。有的话说明作者已经帮你把运行环境定义好了,这是最省事的情况。没有的话,你得自己配 PHP 或者 Python 环境。
# 先看目录结构,两层足够 find . -maxdepth 2 -type f | head -50 # 找关键文件:入口、配置、flag 相关 find . -name "index.*" -o -name "flag*" -o -name "*.conf" -o -name "Dockerfile" -o -name "docker-compose*"这两条命令的逻辑很简单:第一条限制深度,避免在大项目里刷屏;第二条把「入口文件、flag 文件、配置文件、容器定义」一次性捞出来。参数-maxdepth 2可以根据实际情况调整,如果目录很深就加到 3。
2.2 找入口文件:从 Web 服务器配置反推
入口文件不一定是index.php。有些题故意把入口藏在admin/或者api/下面,甚至通过.htaccess或者 Nginx 配置做重定向。所以更可靠的做法是先找 Web 服务器配置。
如果压缩包里带了nginx.conf或者apache2.conf,直接看root和location指令。root指向哪个目录,那个目录就是 Web 根目录;location里的rewrite规则决定了 URL 怎么映射到文件。
# 找 Web 服务器配置文件 find . -name "*.conf" -o -name ".htaccess" -o -name "nginx*" -o -name "apache*" # 如果找到 nginx.conf,看 root 和 location grep -E "root|location|rewrite" nginx.conf如果没有任何配置文件,那就默认index.php或index.html在根目录。这时候直接起一个 PHP 内置服务器就能跑:
# 在源码根目录执行,-t 指定 Web 根目录,-S 指定监听地址和端口 php -S 0.0.0.0:8080 -t ./www-t ./www表示把www目录当作 Web 根目录,如果你的入口文件就在当前目录,改成-t .。0.0.0.0允许局域网访问,自己本机调试用127.0.0.1也行。
2.3 判断是否需要数据库和额外服务
很多 CTF Web 题不是纯文件就能跑的,它需要 MySQL 存用户数据、需要 Redis 存 session、甚至需要单独的 flag 服务。判断方法很简单:搜源码里的连接字符串。
# 搜数据库连接相关关键词 grep -rE "mysqli|PDO|mysql_connect|redis|mongodb" --include="*.php" --include="*.py" . # 搜配置文件里的 host、port、password grep -rE "DB_HOST|DB_PORT|REDIS_HOST|MYSQL_" .如果搜出来有DB_HOST=db这种,说明它原本是跑在 Docker 网络里的,db是容器名。你本地直接跑的话,要么改配置指向127.0.0.1,要么用docker-compose把数据库一起拉起来。
提示:如果源码里写死了
localhost但你的数据库没跑,页面会报连接错误。这时候先别改代码,优先看有没有docker-compose.yml,有的话直接用它,环境变量和网络都配好了。
3. 用 Docker 把环境跑起来:从 compose 文件到可访问页面
确认完技术栈和依赖,接下来就是让它跑起来。CTF Web 源码环境最标准的跑法就是 Docker,因为作者通常会把所有依赖打包好。这一章把 Docker 跑环境的完整流程拆开讲,包括端口映射、环境变量、常见启动失败的处理。
3.1 读懂 docker-compose.yml 里的三个关键字段
拿到docker-compose.yml,不要直接up,先看三个地方:services下面有哪些服务、每个服务的ports映射、environment里的变量。
# 一个典型的 CTF Web 题 compose 文件结构 services: web: build: ./web # 构建上下文,说明 web 目录里有 Dockerfile ports: - "8080:80" # 宿主机 8080 映射到容器 80 environment: - FLAG=flag{test_flag_here} # 环境变量注入 flag depends_on: - db # 依赖 db 服务,启动顺序有保证 db: image: mysql:5.7 environment: - MYSQL_ROOT_PASSWORD=root - MYSQL_DATABASE=ctfports的格式是宿主机端口:容器端口。上面这个例子,你访问http://127.0.0.1:8080就能打到容器的 80 端口。如果写的是"80:80",那就直接访问http://127.0.0.1。
environment里的FLAG很关键。很多题把 flag 通过环境变量注入,源码里用getenv('FLAG')读取。这意味着你改 compose 文件里的FLAG值,就能改题目的答案,自己出题或者改题的时候非常方便。
depends_on只保证启动顺序,不保证 db 已经初始化完成。如果 web 启动时报「数据库连接失败」,大概率是 db 还没初始化完,web 就急着连了。解决办法是在 web 的启动脚本里加等待逻辑,或者手动重启一次 web 容器。
3.2 启动、验证、重置的标准命令序列
# 1. 构建并后台启动所有服务 docker-compose up -d --build # 2. 看容器状态,确认没有 exited docker-compose ps # 3. 看 web 服务日志,找报错 docker-compose logs web # 4. 验证页面可访问 curl -I http://127.0.0.1:8080 # 5. 如果需要重置环境(比如数据库被改乱了) docker-compose down -v && docker-compose up -d--build强制重新构建镜像,适合你改了源码之后。-d是后台运行。down -v里的-v会删除数据卷,数据库会回到初始状态,这是「后悔药」级别的操作,改乱了就用它。
curl -I只拿响应头,用来快速判断服务是否活着。返回200或者302都算正常,返回Connection refused说明端口没映射对或者容器没起来。
3.3 端口冲突和权限问题的处理
最常见的启动失败是端口被占用。比如你本机已经跑了 Nginx 占了 80,compose 里又映射"80:80",就会报port is already allocated。解决办法是改宿主机端口,比如改成"8081:80",然后访问8081。
# 查看本机端口占用情况 ss -tlnp | grep -E "80|8080|3306" # 如果 80 被占用,改 compose 文件里的端口映射 # 把 "80:80" 改成 "8081:80",然后重新 up docker-compose up -d另一个坑是文件权限。有些题目的源码目录挂载到容器里,容器内用户和宿主机用户 UID 不一致,导致写文件失败。表现是页面报「Permission denied」或者「无法写入 session」。解决办法是在 compose 文件里加user: "1000:1000",或者直接chmod -R 777源码目录(仅限本地调试,别在生产环境这么干)。
注意:
docker-compose down -v会删除所有数据卷,包括数据库数据。如果你已经往数据库里插了测试数据,这个操作会全部清掉。确认不需要保留再执行。
4. 源码里找 flag 的四个切入点:从入口到敏感函数
环境跑起来之后,页面能访问了,接下来才是真正的 CTF Web 解题环节。这一章不讲具体某道题,而是讲一套通用的源码审计切入点。你拿到任何一套 CTF Web 源码,按这四个方向去搜,大概率能找到 flag 的藏身之处或者利用链的起点。
4.1 从入口文件顺着请求流往下追
入口文件是index.php的话,先把它完整读一遍。重点看它include或require了哪些文件,这些文件里有没有处理用户输入的逻辑。
// 典型的入口文件结构 <?php include 'config.php'; // 数据库配置 include 'functions.php'; // 公共函数 $action = $_GET['action'] ?? 'home'; switch ($action) { case 'login': include 'login.php'; break; case 'upload': include 'upload.php'; break; default: include 'home.php'; }这种结构下,action参数控制加载哪个文件。如果include的路径拼接了用户输入,就可能存在文件包含漏洞。比如include $_GET['page'] . '.php',你可以通过page=../../../../etc/passwd%00来读取任意文件(PHP 5.3 以下版本)。
追请求流的技巧是:从 URL 参数出发,看这个参数最终传到了哪个函数。用grep搜参数名,比如grep -rn "action" --include="*.php" .,把所有用到action的地方列出来,逐个看。
4.2 搜敏感函数:命令执行、文件读取、反序列化
CTF Web 题的核心利用点通常集中在几类敏感函数上。直接在源码目录里搜这些函数名,能快速定位到可疑代码。
# 命令执行相关 grep -rnE "system|exec|passthru|shell_exec|popen|proc_open" --include="*.php" . # 文件读取相关 grep -rnE "file_get_contents|fopen|readfile|include|require" --include="*.php" . # 反序列化相关 grep -rnE "unserialize|serialize|__wakeup|__destruct|__toString" --include="*.php" . # SQL 注入相关 grep -rnE "mysql_query|mysqli_query|->query|SELECT.*FROM" --include="*.php" .搜出来之后,重点看这些函数的参数是否可控。比如system($_GET['cmd'])就是典型的命令执行漏洞,你传?cmd=cat /flag就能拿到 flag。但实际题目不会这么直白,通常会加过滤,比如str_replace('cat', '', $cmd),这时候你就得用c\at或者tac绕过。
passthru这个函数在热词里出现过,它和system的区别是passthru直接输出二进制数据,适合执行会返回图片或者二进制内容的命令。用法上没区别,都是命令执行。
4.3 看配置文件和环境变量里的 flag
有些题目的 flag 不在源码里,而是通过环境变量注入。这时候你要看config.php或者.env文件里有没有getenv调用。
// config.php 里常见的 flag 读取方式 $flag = getenv('FLAG'); if ($flag === false) { $flag = 'flag{default_flag}'; }这种情况下,flag 的值在docker-compose.yml的environment里。你改那个值,页面显示的 flag 就跟着变。如果你是在做一道线上题,flag 在靶机的环境变量里,你需要通过漏洞读到环境变量,比如命令执行env或者文件包含/proc/self/environ。
还有一种情况是 flag 写在数据库里。这时候你需要先通过 SQL 注入或者弱口令进后台,然后查数据库。常见表名是flag、secrets、users,字段名是flag、value、secret。
4.4 用 diff 对比原始源码和修改后的源码
如果你自己改过源码,或者想确认题目有没有被改动过,可以用diff对比。把原始压缩包解压一份,把你正在跑的源码复制一份,然后对比。
# 对比两个目录的差异,-r 递归,-q 只显示有差异的文件名 diff -rq original/ running/ # 如果只想看具体差异内容,去掉 -q diff -r original/ running/ | head -100这个技巧在「题目环境被前人改过」或者「你想确认自己有没有改错」的时候特别有用。比如你改了FLAG环境变量,但页面显示的还是旧 flag,diff 一下就知道是不是改错了文件。
提示:
diff -rq只告诉你哪些文件不同,不告诉你具体哪里不同。要定位具体行,用diff -u original/file.php running/file.php。
5. 避坑与排查:环境跑不起来、flag 读不到、页面报错的常见原因
这一章集中讲踩坑记录。CTF Web 源码环境看起来简单,但实际跑的时候会遇到各种玄学问题。下面五条是我自己踩过或者见别人踩过的,每条按「现象 → 原因 → 解决」写。
5.1 页面返回 500 但日志里没有 PHP 错误
现象:浏览器访问显示500 Internal Server Error,但docker-compose logs web里只有 Nginx 的访问日志,没有 PHP 的报错信息。
原因:PHP 的错误显示被关掉了,或者错误日志输出到了容器内的其他文件。CTF 题为了「干净」,经常在php.ini里设置display_errors = Off。
解决:进容器改php.ini,或者直接在源码入口文件顶部加两行临时开启错误显示。
<?php ini_set('display_errors', 1); error_reporting(E_ALL); // 下面才是原来的代码如果不想改源码,可以进容器执行php -i | grep error_log找到错误日志路径,然后tail -f那个文件。
5.2 数据库连接失败但 db 容器明明是 Up 状态
现象:web 页面报SQLSTATE[HY000] [2002] Connection refused,但docker-compose ps显示 db 容器是Up。
原因:MySQL 容器启动后需要几秒钟初始化,depends_on只保证容器启动顺序,不保证服务就绪。web 容器在 db 还没准备好连接的时候就发起了连接。
解决:重启 web 容器,或者手动等几秒再访问。更彻底的办法是在 web 的启动脚本里加一个等待循环。
# 在 web 容器的启动脚本里加这段,等待 db 端口可连 until nc -z db 3306; do echo "waiting for db..." sleep 1 done # 然后再启动 web 服务5.3 flag 文件存在但读不出来,提示 Permission denied
现象:你知道flag.php在某个目录下,但通过文件包含或者命令执行读的时候报权限错误。
原因:flag 文件的权限设置成了400或者440,只有特定用户能读。Web 服务运行的用户(比如www-data)没有读权限。
解决:如果是本地环境,直接chmod 644 flag.php。如果是线上题,你需要通过提权或者找到以 root 运行的进程来读。CTF 里常见的做法是找 SUID 程序或者写定时任务。
5.4 命令执行没有回显,但命令确实执行了
现象:你通过漏洞执行了cat /flag,但页面上什么都没显示。
原因:system和passthru会直接输出,但exec和shell_exec只返回结果不输出。如果你用的是exec,需要echo出来。另外,有些题目的 PHP 配置了disable_functions,把system、exec都禁了。
解决:先确认用的是哪个函数。如果是exec,改成echo exec('cat /flag');。如果disable_functions禁了常用函数,试试pcntl_exec、imap_open或者LD_PRELOAD绕过。
# 查看当前 PHP 禁用了哪些函数 php -i | grep disable_functions5.5 端口映射正确但浏览器访问超时
现象:docker-compose ps显示端口映射是0.0.0.0:8080->80/tcp,但浏览器访问127.0.0.1:8080一直转圈最后超时。
原因:Web 服务在容器内监听的是127.0.0.1:80而不是0.0.0.0:80。Docker 的端口映射需要服务监听在0.0.0.0才能从宿主机访问。
解决:改 Web 服务器配置,把listen 127.0.0.1:80改成listen 0.0.0.0:80。Nginx 的话改nginx.conf,Apache 的话改ports.conf。
注意:如果你是在 macOS 或者 Windows 上跑 Docker,
127.0.0.1可能不通,试试host.docker.internal或者直接用localhost。
6. 把环境改造成自己的出题模板:改 flag、加漏洞、导出镜像
跑通别人的环境只是第一步。真正有价值的是把这套环境改造成你自己的出题模板,或者改造成一个可以反复练习的靶场。这一章讲三个进阶操作:改 flag 值、加一个自定义漏洞、把环境导出成镜像分发给别人。
6.1 改 flag 的三种方式
第一种是改docker-compose.yml里的environment。这是最干净的,因为源码不用动。
environment: - FLAG=flag{my_custom_flag_2025}第二种是改源码里的硬编码。搜flag{找到所有出现的地方,替换掉。
# 搜所有包含 flag{ 的文件 grep -rn "flag{" --include="*.php" --include="*.py" --include="*.js" . # 批量替换(先备份) sed -i 's/flag{old_flag}/flag{new_flag}/g' $(grep -rl "flag{old_flag}" .)第三种是改数据库里的 flag 记录。先进 db 容器,然后UPDATE对应的表。
# 进 db 容器 docker-compose exec db mysql -uroot -proot ctf # 在 MySQL 里执行 UPDATE flags SET value='flag{db_flag}' WHERE id=1;三种方式优先级是:环境变量 > 数据库 > 源码硬编码。因为环境变量在运行时注入,优先级最高。
6.2 加一个自定义漏洞:以文件包含为例
如果你想在这个环境里加一个文件包含漏洞用来练习,可以在入口文件里加一段代码。
// 在 index.php 的 switch 之前加 if (isset($_GET['page'])) { $page = $_GET['page']; // 故意不加过滤,制造文件包含漏洞 include $page . '.php'; exit; }加完之后,你就可以通过?page=../../../../etc/passwd来测试文件包含。如果想让它能读 flag,把 flag 文件放在 Web 目录下,然后?page=flag就能读到。
但要注意,PHP 的include默认会拼接.php后缀,所以?page=flag实际包含的是flag.php。如果你想包含非 PHP 文件,需要用%00截断(PHP 5.3 以下)或者用php://filter协议。
# 用 php://filter 读源码,base64 编码后输出 curl "http://127.0.0.1:8080/?page=php://filter/convert.base64-encode/resource=flag"这个 payload 会把flag.php的内容 base64 编码后输出,你解码就能看到源码。这是 CTF Web 里非常经典的读源码手法。
6.3 导出镜像和分发环境
如果你想把改造好的环境发给别人,有两种方式。一种是直接打包整个目录,别人拿到后docker-compose up就行。另一种是导出镜像。
# 导出镜像为 tar 文件 docker save -o ctf_web_env.tar ctf_web:latest # 别人导入镜像 docker load -i ctf_web_env.tar # 然后直接 docker run 或者用 compose 启动导出镜像的好处是别人不需要重新构建,坏处是文件大。一个 PHP + MySQL 的环境,镜像 tar 包大概 500MB 到 1GB。如果只是源码级别的分发,直接打包目录更轻量。
我自己的习惯是:源码目录 +docker-compose.yml一起打包,写一个README.md说明端口和 flag 位置。这样别人拿到就能跑,不需要问我任何问题。这个习惯帮我省了很多重复解释的时间,也让我的出题模板可以反复复用。
希望帮到你。
本文还有配套的精品资源,点击获取