主题
SQL 基础:从增删改查到注入的起点
上一课我们让服务器记住了"你是谁",这一课去看它把这些信息存在哪里——数据库。 你会写出人生第一条
SELECT,也会第一次看到' OR '1'='1。我们只讲原理,绝不教打站:所有实验都在你自己主机上的 MySQL 里做。
一、数据库的最小概念
用一张表就能解释清楚:
| 概念 | 英文 | 类比 |
|---|---|---|
| 数据库 | database | 一个 Excel 文件 |
| 表 | table | 文件里的一个工作表 |
| 列(字段) | column | 表头,比如 username、password |
| 行(记录) | row | 一行数据,比如"张三、某个哈希值" |
| 主键 | primary key | 唯一编号,通常是 id |
你主机上就装着一个 MySQL 服务在跑,CentOS 7 虚拟机里也可以再装一个。SQL 的特别之处在于:数据库会严格按语法解析你提交的每一条语句。这既是它的能力,也是注入能发生的前提——一旦"用户输入"被拼进语句,数据库就再也分不清哪部分是代码、哪部分是数据。
二、增删改查四件套
sql
SELECT id, username, email FROM users; -- 查
INSERT INTO users (username, password, email) -- 增
VALUES ('alice', 'hashed_password_here', 'alice@example.com');
UPDATE users SET email = 'new@example.com' WHERE id = 1; -- 改
DELETE FROM users WHERE id = 1; -- 删先记住一条工程常识:UPDATE 和 DELETE 一定先写 WHERE,甚至先把它 SELECT 一遍确认范围。 忘记 WHERE 会改掉/清空整张表,这是真实事故的高发点,生产库里少写一个 WHERE,老板的电话马上就到。
三、WHERE、ORDER BY、LIMIT
sql
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE username LIKE 'a%'; -- 以 a 开头
SELECT * FROM users ORDER BY id DESC; -- 排序:ASC 升序(默认)/ DESC 降序
SELECT username FROM users ORDER BY id DESC LIMIT 5; -- 最新 5 条
SELECT username FROM users LIMIT 0, 5; -- 分页:偏移 0,取 5 条四、聚合与 JOIN
sql
SELECT COUNT(*) FROM users; -- 一共几个用户
-- users 有 id、username;orders 有 id、user_id、amount
SELECT u.username, o.amount
FROM users AS u
JOIN orders AS o ON o.user_id = u.id
WHERE u.id = 1;JOIN 是后面读 UNION 类注入文章绕不开的基础:它证明一条查询可以同时从多张表取数据。 注入里用 UNION SELECT 拼出别的表内容,思路和这里的 JOIN 是同一件事——让一条语句读到本不该读的列。
五、动手建表:可直接复制的 SQL
在 MySQL 客户端里(Windows 上用 mysql -u root -p,或图形化的 MySQL Workbench)依次执行:
sql
CREATE DATABASE lab_sql CHARACTER SET utf8mb4; -- 专门用来练习的库,绝不碰业务库
USE lab_sql;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, -- 只存哈希,不存明文
email VARCHAR(100),
role VARCHAR(20) NOT NULL DEFAULT 'user'
);
INSERT INTO users (username, password, email, role) VALUES
('alice', 'hash_aaa', 'alice@example.com', 'user'),
('bob', 'hash_bbb', 'bob@example.com', 'user'),
('admin', 'hash_ccc', 'admin@example.com', 'admin');⚠️
hash_aaa只是占位符。真实系统里password列存的必须是 bcrypt/Argon2 之类的加盐哈希,永远不要存明文——这一点在 Cookie、Session 与身份认证 里讲过。
六、后端是怎么把用户输入拼进 SQL 的
错误写法(很多老教程真的这么写):
python
# ❌ 危险写法:f-string 拼接(Python + PyMySQL)
uid = request.args.get('id') # 用户完全可控
sql = f"SELECT id, username, email FROM users WHERE id = {uid}"
cursor.execute(sql)正确写法(参数化查询,也叫预处理语句):
python
# ✅ 安全写法:输入作为参数单独传给驱动,永远只是"值"
uid = request.args.get('id')
sql = "SELECT id, username, email FROM users WHERE id = %s"
cursor.execute(sql, (uid,))PHP 里对应关系是 "WHERE id = $id"(危险)对比 mysqli_prepare + mysqli_stmt_bind_param(安全),Servlet 里则是 Statement 对比 PreparedStatement。两种写法只差一点,区别却是本质性的:
| 对比项 | 拼接字符串 | 参数化查询 |
|---|---|---|
| 输入的作用 | 变成 SQL 语句的一部分 | 永远只是数据 |
| 输入含引号/关键字时 | 改变语句结构 | 原样当作字符串值 |
| 能否被注入 | 能 | 不能(前提是别在 SQL 里再拼接表名/列名) |
七、为什么拼字符串就会注入
机制只有一句:数据库在解析"语句结构"时,不区分哪部分是你写的、哪部分是用户塞进来的。
假设用户提交的 id 是下面这一串(这是我们第一次见到经典 payload,请只在你自己刚建的 lab_sql 库里理解它):
text
1 OR 1=1拼接之后,进入数据库的完整语句变成 SELECT id, username, email FROM users WHERE id = 1 OR 1=1;——OR 1=1 永远为真,WHERE 条件对整个表都成立,原本只想查一条记录,结果整张表都被返回。
再看带引号、更经典的版本 ' OR '1'='1(当代码把输入包在单引号里时):拼接结果是 WHERE username = '' OR '1'='1',条件同样恒真。关键动作是那个单引号——它提前结束了字符串,让后面的 OR '1'='1 从"字符串内容"变成了"SQL 语法"。它把数据越界成了代码,这就是单引号成为 SQL 注入标志性字符的原因。
到这里你只需要掌握原理:可控输入改变了语句结构 = 注入。 怎么绕过过滤、怎么用 UNION 读别的表,等你在 第四阶段 的本地靶场 DVWA 里再动手,那才是有授权的正确场所。
八、ORDER BY 为什么常被用来探测列数
ORDER BY 有个特殊之处:它后面跟的是列的位置或名字,这个位置通常无法用参数占位符替代,于是很多程序只能拼接。
sql
SELECT id, username, email FROM users ORDER BY 1; -- 合法:按第 1 列排序
SELECT id, username, email FROM users ORDER BY 3; -- 合法:按第 3 列排序
SELECT id, username, email FROM users ORDER BY 4; -- 报错:Unknown column '4' in 'order clause'从 1 往上试,一报错就说明超出了列数,从而推断出这条查询返回几列——这是做 UNION SELECT 类注入的第一步准备工作。这也能让你看懂为什么防御清单里有"不要拼接排序列":确实需要用户选择排序时,正确做法是白名单映射(asc/desc → 固定字符串),而不是校验后拼接。
九、防御三件事
| 措施 | 具体做法 | 挡住了什么 |
|---|---|---|
| 1. 参数化查询 | 用户输入全部用占位符绑定;表名/列名/排序方向改不了参数化的,就用白名单枚举 | 从根本上让输入无法变成语法 |
| 2. 最小权限数据库账号 | 网站用专用账号连库,只给需要的权限;业务账号不要有 DROP、FILE、INTO OUTFILE | 一旦被注入,损失被限制在最小范围 |
| 3. 错误信息不外泄 | 对外统一模糊报错,详细错误只写服务器本地日志 | 不让攻击者用报错反推表结构和列数 |
配一个最小权限账号,可以直接抄:
sql
CREATE USER 'web_lab'@'localhost' IDENTIFIED BY '换成你自己的强密码';
GRANT SELECT, INSERT, UPDATE, DELETE ON lab_sql.* TO 'web_lab'@'localhost';
FLUSH PRIVILEGES;
SHOW GRANTS FOR 'web_lab'@'localhost'; -- 检查:不该出现 ALL PRIVILEGES 或 *.*💡 动手实验 1:在本地 MySQL 建表并查询
💡 动手实验 1:打开 PowerShell,用
mysql -u root -p登录你本机的 MySQL(密码是你自己设置的 root 密码)。 把第五节的 SQL 整段复制粘贴进去,看到Query OK和 alice/bob/admin 三条记录,就说明你的第一个练习库建好了。 然后自己写三条查询:查所有role = 'admin'的用户、按id倒序取前 2 条、统计每种角色的人数,写完再对照下面参考答案。
bash
mysql -u root -p # Windows 主机上登录本地 MySQLsql
-- 下面是小练习的参考答案,做完再看
SELECT username FROM users WHERE role = 'admin';
SELECT username FROM users ORDER BY id DESC LIMIT 2;
SELECT role, COUNT(*) AS total FROM users GROUP BY role;💡 动手实验 2:亲手制造一次"注入",看结果差异
💡 动手实验 2:只在你自己刚建的
lab_sql库上做。 依次执行下面两条查询,用眼睛确认结果差异:第一条只返回 alice,第二条把 alice、bob、admin 全部返回了。 这就是"条件恒真"的效果——第二条模拟的正是"程序把用户输入拼进 SQL"之后,数据库实际收到的语句。
sql
SELECT id, username, email FROM users WHERE username = 'alice';
SELECT id, username, email FROM users WHERE username = '' OR '1'='1'; -- payload:' OR '1'='1💡 动手实验 2 加练:用第八节的方法猜列数,依次执行
ORDER BY 1/2/3/4,看哪一条开始报Unknown column '4' in 'order clause'。 再想一个问题:如果换成参数化查询,上面那种输入会返回什么? 答案是一条也返回不了——数据库会把整串' OR '1'='1当作一个普普通通的用户名字符串去比对,语法结构根本没被改变。这就是防御的意义。 做完执行DROP DATABASE lab_sql;收拾现场,养成清理习惯。
⚠️ 再次强调边界:以上 payload 只在你自己主机、你自己创建的
lab_sql库里有意义。对任何不属于你的系统尝试同样的输入,性质就从"学习"变成了"攻击",可能触犯《网络安全法》和《刑法》第 285、286 条。授权与合规的详细讨论在 四、工具与靶场。
小结
- 数据库 → 表 → 行 → 列,SQL 就是操作它们的语言;核心动作是增删改查。
WHERE决定"操作哪些行",ORDER BY决定排序,LIMIT做分页;UPDATE/DELETE前先确认WHERE。JOIN和聚合能把多张表的数据合起来看——这正是注入里UNION想做的事。- 拼接字符串 = 把用户输入变成 SQL 语法,这是注入的唯一根因;参数化查询让输入永远只是数据。
- 单引号是"数据越界成代码"的标志性字符;
1 OR 1=1、' OR '1'='1能让WHERE条件恒真。 ORDER BY后面的数字报错与否能反推列数,所以排序字段必须用白名单而不是拼接。- 防御三件事:参数化查询、最小权限数据库账号、错误信息不外泄。
到这里,你已经具备了读懂 第三阶段 后续漏洞文章的最小储备:请求怎么进来(服务端如何接收请求)、身份怎么被记住(Cookie、Session 与身份认证)、数据怎么被查询(本课)。下一站就是去本地靶场把这些串起来实战。
📎 本课对应知识库阅读:《SQL 注入》