CTF Web 方向的 MySQL 基础:先认识数据库与 SQL 的基本概念,再到连接、库表操作、增删改查、SELECT 详解与常用查询指令。本文是 SQL 注入学习的前置知识。
1. 认识数据库与 SQL
在讲 SQL 之前,不妨先想想数据库是什么。
你每天打开 B 站刷视频、在淘宝下单、在微博发帖——这些看起来再普通不过的操作背后,其实都有一个数据库在默默运转。你的账号信息、浏览记录、订单数据、评论内容,全都存在数据库里:网站需要的时候就去查,你更新了就去写,你删了就去删。数据库,其实就是整个 Web 应用真正意义上的"记忆"。
没有数据库,网站不过是一张静态的纸;有了数据库,它才真正活起来。
1.1 数据库长什么样?
其实关系型数据库的结构没有想象中复杂,可以这样理解:
| 层级 | 说明 |
|---|---|
| 数据库(Database) | 最顶层的容器,一个网站通常会有一个或多个数据库 |
| 数据表(Table) | 数据库里的表格,比如用户表、订单表、文章表 |
| 字段(Column) | 表格的列,比如用户表里的 id、username、password、email |
| 记录(Row) | 表格的行,每一行就是一条完整的数据,比如某个用户的全部信息 |
拿一张简单的用户表举例,大概是这个样子:
| id | username | password | |
|---|---|---|---|
| 1 | 小明 | xxxxxx | xm@qq.com |
| 2 | 小红 | xxxxxx | xh@qq.com |
这张表里存着所有注册用户的信息。你登录的时候,网站要做的事其实很简单:去这张表里查一查,你输入的用户名和密码对不对。
1.2 SQL 是什么?
SQL,全称 Structured Query Language(结构化查询语言),就是用来跟数据库"对话"的工具。
它不属于哪一家公司,而是几乎所有关系型数据库通用的沟通方式。无论是 CTF 里最常见的 MySQL,还是企业级的 PostgreSQL、MSSQL,或者轻量级的 SQLite,背后都在用 SQL 跟数据打交道。
SQL 要做的事,说到底只有四件
| 操作 | 关键字 | 含义 |
|---|---|---|
| 增(Create) | INSERT |
往数据库里写新数据 |
| 查(Read) | SELECT |
从数据库里取数据 |
| 改(Update) | UPDATE |
修改已有的数据 |
| 删(Delete) | DELETE |
删掉数据 |
这四个操作合称 CRUD,也就是我们常说的"增删改查"——它们是所有数据库操作的基础,后面学到的注入手法,本质上也都是围绕这四件事做文章。
其中 SELECT(查)是我们打交道最多的一个,几乎所有的注入手法最终都是围绕着一句 SELECT 语句展开的。下面就从连接数据库开始,把这些指令逐一过一遍。
2. 连接与退出
# 连接数据库(-h 主机,-P 端口,-u 用户,-p 密码)
mysql -h 127.0.0.1 -P 3306 -u root -p
# 退出
exit;
quit;
3. 数据库操作
-- 查看所有数据库
SHOW DATABASES;
-- 创建数据库
CREATE DATABASE ctf;
-- 切换数据库
USE ctf;
-- 删除数据库
DROP DATABASE ctf;
-- 查看当前使用的数据库
SELECT DATABASE();
4. 表操作
-- 查看当前库中所有表
SHOW TABLES;
-- 创建表
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
password VARCHAR(50) NOT NULL,
email VARCHAR(50)
);
-- 查看表结构
DESC users;
-- 或
SHOW COLUMNS FROM users;
-- 删除表
DROP TABLE users;
5. 增删改查(CRUD)
对应第 1 节提到的四个基本操作,语法如下:
增:插入数据(INSERT)
INSERT INTO users (username, password, email) VALUES ('小刚', '123456', 'xg@qq.com');
往 users 表里插入一条新记录,对应的就是"注册"这个动作。
查:查询数据(SELECT)
-- 查询所有
SELECT * FROM users;
-- 条件查询
SELECT * FROM users WHERE id = 1;
-- 指定字段
SELECT username, password FROM users WHERE username = 'admin';
-- 排序与限制
SELECT * FROM users ORDER BY id DESC LIMIT 10;
-- 模糊查询
SELECT * FROM users WHERE username LIKE '%admin%';
SELECT 的用法远不止这些,第 6、7 节会单独展开细说。
改:更新数据(UPDATE)
UPDATE users SET email = 'new@qq.com' WHERE username = '小明';
把用户名是"小明"的那条记录,email 字段改成新的值。
删:删除数据(DELETE)
DELETE FROM users WHERE username = '小明';
把用户名是"小明"的那条记录删掉。
6. SELECT 查询详解
之所以把 SELECT 单独拎出来讲,是因为不管是正常的业务查询,还是后面要学的 SQL 注入,绝大部分场景都是在跟一句 SELECT 语句打交道。把这部分吃透,后面理解注入原理会顺畅很多。
基础查询词语:select / from / where
这三个词是 SELECT 语句的骨架,缺一不可。
SELECT username, email FROM users WHERE id = 1;
SELECT:告诉数据库"我要查哪些字段",比如这里的 username、email;想要全部字段就写*FROM:告诉数据库"去哪张表里查",这里是 users 表WHERE:告诉数据库"查询的条件是什么",这里是 id 等于 1
三者组合起来读,就是:从 users 表里,找出 id 等于 1 的那一行,把它的 username 和 email 取出来。网站的登录逻辑,背后往往就是一句类似的 SELECT:
SELECT * FROM users WHERE username = '小明' AND password = '123456';
能查到匹配的记录,就说明用户名密码正确。
子查询
子查询,简单说就是"查询里面还嵌着一个查询",外层查询要用到内层查询的结果。
SELECT * FROM users WHERE id = (SELECT id FROM users WHERE username = '小明');
括号里的 SELECT id FROM users WHERE username = '小明' 会先执行,查出小明的 id,然后外层再拿这个 id 去查完整信息。子查询在 SQL 注入里出现的频率很高,因为很多时候我们要在一条语句里,先"问"出一个信息,再拿这个信息去做下一步查询。
7. 常用查询指令进阶
以下指令在数据查询和筛选中非常常用,建议逐个动手试一试。为便于演示,假设有如下 users 表:
| id | username | password | age | city |
|---|---|---|---|---|
| 1 | admin | 123456 | 25 | 北京 |
| 2 | 小明 | xxxxxx | 20 | 上海 |
| 3 | 小红 | xxxxxx | 22 | 北京 |
| 4 | 小刚 | xxxxxx | 20 | 上海 |
AND / OR:组合条件
-- AND:两个条件同时满足
SELECT * FROM users WHERE age = 20 AND city = '上海';
-- 结果:小明、小刚
-- OR:满足任意一个条件即可
SELECT * FROM users WHERE city = '北京' OR age = 22;
-- 结果:admin、小红
-- 混合使用时建议加括号明确优先级(AND 优先级高于 OR)
SELECT * FROM users WHERE city = '上海' AND (age = 20 OR age = 25);
ORDER BY:排序
-- 按年龄升序(ASC 可省略,默认升序)
SELECT * FROM users ORDER BY age ASC;
-- 按年龄降序
SELECT * FROM users ORDER BY age DESC;
-- 多字段排序:先按城市升序,同城再按年龄降序
SELECT * FROM users ORDER BY city ASC, age DESC;
-- 也可以按列的位置排序(第 2 列即 username)
SELECT * FROM users ORDER BY 2;
GROUP BY:分组统计
-- 按城市分组,统计每个城市有多少用户
SELECT city, COUNT(*) FROM users GROUP BY city;
-- 结果:北京 2,上海 2
-- 按年龄分组,查看每个年龄的用户数
SELECT age, COUNT(*) AS num FROM users GROUP BY age ORDER BY num DESC;
-- 配合 HAVING 过滤分组结果(HAVING 作用于分组后,WHERE 作用于分组前)
SELECT age, COUNT(*) AS num FROM users GROUP BY age HAVING num > 1;
-- 结果:只有 age = 20(有 2 人)
[!tip]
CTF 里经常会用ORDER BY 数字或GROUP BY 数字来判断当前查询结果一共有几列,这是联合查询注入的关键一步。
LIMIT:限制返回行数
-- 只取前 2 条
SELECT * FROM users LIMIT 2;
-- LIMIT 偏移量, 条数:跳过前 1 条,取 2 条(即从第 0 条开始数的第 1、2 条)
SELECT * FROM users LIMIT 1, 2;
-- 结果:小明、小红
-- 常与 ORDER BY 配合:取年龄最大的 1 个人
SELECT * FROM users ORDER BY age DESC LIMIT 1;
GROUP_CONCAT:分组字段拼接
-- 把每个城市的所有用户名拼成一行
SELECT city, GROUP_CONCAT(username) FROM users GROUP BY city;
-- 结果:北京 → admin,小红 上海 → 小明,小刚
-- 自定义分隔符
SELECT GROUP_CONCAT(username SEPARATOR ' | ') FROM users;
-- 结果:admin | 小明 | 小红 | 小刚
如果不用 GROUP_CONCAT(),一次查询只能拿到一行结果;而在注入场景里,我们往往一次只有一个"出口"能把数据带出来,这时候 GROUP_CONCAT() 就能把原本分散在多行里的信息(比如所有表名、所有用户名)压缩进一行,一次性带出来,非常关键。
UNION:联合查询
-- 合并两次查询的结果(列数必须一致,类型尽量兼容)
SELECT username, city FROM users WHERE age = 20
UNION
SELECT username, city FROM users WHERE city = '北京';
-- UNION 默认去重,想保留重复行用 UNION ALL
SELECT city FROM users UNION ALL SELECT city FROM users;
-- 测试列数是否匹配的常用写法
SELECT username, password FROM users UNION SELECT 1, 2;
[!note] UNION 的要点
- 前后两个 SELECT 的列数必须相同,否则报错
- 可以用
UNION SELECT 1, 2, 3快速试探查询的列数- 显示位置不够时,可以用
NULL占位
DATABASE() / VERSION():常用内置函数
-- 当前正在使用的数据库名
SELECT DATABASE();
-- MySQL 版本号
SELECT VERSION();
-- 其他常见内置函数
SELECT USER(); -- 当前登录用户
SELECT NOW(); -- 当前时间
SELECT LENGTH('abc'); -- 字符串长度:3
