ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SQL注入实战:双写绕过过滤与无information_schema信息获取

2026/8/8 10:53:24 拓冰建站 浏览量
SQL注入实战:双写绕过过滤与无information_schema信息获取 1. 项目概述一次关于SQL注入的深度实战复盘最近在BUUCTF平台上复现了一道名为“极客大挑战 2019 BabySQL”的WEB题目。这道题虽然名字里带个“Baby”听起来像是入门级但实际走下来发现它是一道非常经典的、融合了多种过滤与绕过技巧的SQL注入实战案例。它不像那些直接union select 1,2,3就能出结果的“签到题”而是需要你一步步分析过滤规则尝试各种绕过姿势最终才能拿到flag。整个过程就像是在和出题人进行一场“猫鼠游戏”非常考验对SQL注入底层原理和灵活运用能力的理解。今天我就把自己完整的解题思路、踩过的坑以及最终的通关方法从头到尾梳理一遍希望能给正在学习Web安全特别是对SQL注入感兴趣的朋友们提供一个清晰的参考路径。2. 环境搭建与初步信息收集2.1 题目环境启动与访问首先我们需要在BUUCTF平台上启动这道题目的环境。BUUCTF通常会提供一个链接和一个动态分配的端口。启动后我们使用浏览器访问对应的地址。映入眼帘的是一个非常简洁的登录界面通常包含用户名username和密码password的输入框以及一个提交按钮。页面本身没有多余的提示信息这很符合CTF题目的风格——一切线索都需要我们自己去挖掘。面对一个登录框我们的第一反应就是尝试是否存在SQL注入漏洞。最经典的测试方法就是使用万能密码admin or 11。于是我在用户名处输入admin密码随意输入点击登录。2.2 首次试探与错误信息分析提交后页面没有像预想中那样登录成功或者显示密码错误而是直接返回了一个SQL语句执行的报错信息。这是一个非常重要的信号它告诉我们两个关键点第一后端确实将我们的输入拼接到了SQL语句中执行第二网站开启了错误回显这能为我们提供极其宝贵的调试信息这种注入类型我们称之为“报错注入”。我收到的错误信息大致如下You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near admin at line 1这个报错信息非常标准。它告诉我们在admin附近出现了语法错误。我们来分析一下后端可能的SQL语句结构。当我们输入admin时它被拼接进去假设原语句是SELECT * FROM users WHERE username我们输入的用户名 AND password我们输入的密码那么拼接后就变成了SELECT * FROM users WHERE usernameadmin AND passwordxxx可以看到我们输入的单引号闭合了原本用于包裹用户名的前一个单引号然后后面多出了一个孤立的单引号导致语法错误。这直接证实了注入点的存在并且是字符型注入。注意错误回显是双刃剑。对于攻击者来说是绝佳的信息来源对于开发者则是严重的安全隐患。在生产环境中一定要关闭数据库的错误信息直接向前端输出应使用统一的、模糊的错误提示页面。2.3 判断注入类型与注释符使用既然存在注入下一步就是判断如何正确地闭合语句并执行我们想要的查询。我们输入admin ##在MySQL中是注释符尝试将后面的密码判断逻辑注释掉。这样理论上SQL语句会变成SELECT * FROM users WHERE usernameadmin # AND passwordxxx#之后的所有内容都被注释那么只要数据库中存在用户名为admin的记录就会登录成功。然而提交admin #后依然报错了。错误信息可能变成了关于#的语法错误。这说明#这个注释符很可能被后端过滤或转义了。这是题目设置的第一道障碍。我们换用另一个MySQL注释符--注意--后面必须跟一个空格。输入admin --。提交后奇迹发生了——我们成功登录了系统页面跳转到了一个显示Welcome!的界面。这说明--注释符没有被过滤我们成功绕过了密码验证。这也确定了注入类型为基于单引号的字符型注入。3. 核心过滤规则探测与绕过策略登录成功只是第一步我们的目标是获取数据库中的敏感信息flag。通常我们会使用union select进行联合查询注入。于是我尝试在用户名处输入admin union select 1,2,3 --3.1 发现关键字过滤结果页面返回了错误但这次的错误信息不再是SQL语法错误而是一个看起来像是被WAFWeb应用防火墙或自定义过滤函数拦截的提示比如直接显示“非法字符”或“SQL Injection Detect”。为了精确探测我尝试输入一些简单的payload。当我输入admin union --时正常登录。但输入admin select --时被拦截了。这说明select这个关键字被单独过滤了。我继续测试union发现union本身可以通过但union select一起出现就会被拦截。这揭示了过滤逻辑可能是一个黑名单里面包含了select、and、or等关键字当检测到这些关键字时请求被阻断。3.2 双写绕过Double Write Bypass这是一种非常经典的关键字绕过技巧。其原理是过滤函数通常是一次性匹配并删除或替换掉黑名单中的关键词。例如它会把select替换为空字符串。如果我们输入selselectect当中间的select被删除后剩下的部分select恰好又能拼接成新的select。我立刻开始测试测试select输入admin selselectect --如果成功登录说明select被过滤且双写绕过有效。测试结果成功登录证实了过滤规则和绕过方法。系统化测试其他关键字为了后续构造payload我们需要知道哪些关键字被过滤了。我设计了一个简单的测试流程在admin和--之间插入待测试的关键字。admin and 11 --被拦截。说明and被过滤。admin aandnd 11 --成功登录说明双写and有效。同理测试or、from、where、by、limit等常见SQL关键字。发现or,from,where,by均被过滤且均可通过双写oorr,frfromom,whwhereere,bbyy绕过。特别注意information_schema这是MySQL中获取元数据数据库名、表名、列名的关键库。测试发现information_schema也被整体过滤了。这增加了难度因为我们需要找到替代方法来获取表结构信息。3.3 确定列数与回显点在union select中前后两个select语句的列数必须相同。我们需要先确定当前查询的列数。通常有两种方法order by和union select null。由于order和by可能被过滤我们优先使用union select递增null的方法并配合双写绕过。 我尝试输入admin ununionion selselectect 1 --被拦截可能是因为union select 1作为一个整体触发了某种检测。我调整策略先注释掉后面确定原查询列数。但更直接的方法是在知道union和select可用需双写后直接尝试admin ununionion selselectect 1,2,3 --页面显示错误可能是列数不对。我不断增加null的数量admin ununionion selselectect 1,2,3,4 --当尝试到4个字段时页面没有报错并且显示了Welcome!同时在页面内容中我看到了数字2和3被显示了出来这是一个重大进展。实操心得寻找回显点即union后我们select的数据在页面哪个位置显示时不要只用1,2,3可以用一些独特的字符串如version、user()或者简单的‘a‘,‘b‘,‘c‘这样一旦页面内容出现变化能更直观地定位。这里我用了1,2,3,4页面显示了2和3说明第2和第3列是回显点。4. 信息获取与最终Flag提取确定了回显点是第2和第3列我们就可以通过这两个位置来输出我们想查询的信息了。标准的MySQL注入流程是查数据库名 - 查表名 - 查列名 - 查数据。4.1 查询当前数据库名由于information_schema被禁我们无法直接使用table_schema。但是我们可以使用database()函数来获取当前数据库名。 Payload构造如下admin ununionion selselectect 1, database(), 3, 4 --我将database()放在第2个回显点。执行后页面在2的位置显示了数据库名geek。4.2 查询表名不使用information_schema这是本题最大的难点。在MySQL中除了information_schema.tables还有另一种方式可以查询表名通过查询mysql.innodb_table_stats或sys.schema_table_statistics等系统库/视图。但这些视图的访问通常需要较高权限且在CTF环境中不一定可用或稳定。一个更通用的、不依赖information_schema的技巧是使用布尔盲注或时间盲注但本题有报错回显我们可以尝试报错注入。报错注入函数如extractvalue()或updatexml()它们能通过构造错误的XPath路径在报错信息中带出我们查询的数据。然而经过测试updatexml和extractvalue中的xml、xpath等关键词可能也被过滤了。我们需要另辟蹊径。回顾一下我们已知数据库名为geek。在CTF题目中表名和列名常常是flag、user、admin、password等。我们可以尝试用union select配合from语句双写直接猜解表名和列名。首先猜解表名。我尝试admin ununionion selselectect 1, group_concat(table_name), 3, 4 frfromom infoorrmation_schema.tables whwhereere table_schemadatabase() --这个payload试图从information_schema.tables中查询表名但前面说了information_schema被过滤。即使我们双写information_schema为infoorrmation_schema也可能因为字符串被整体匹配而失败。实测确实失败了。那么我们只能基于已知数据库geek进行暴力猜解。我假设存在一个flag表。构造payloadadmin ununionion selselectect 1,2,3,4 frfromom flag --如果flag表存在这个语句应该能执行尽管可能因为列数不对而报错但报错信息会不同。如果表不存在会直接提示表不存在。通过页面返回的差异我们可以判断。但这里我们有了报错信息可以更精确。输入后页面返回了关于flag表不存在的错误。说明没有flag表。我尝试另一个常见表名usersadmin ununionion selselectect 1,2,3,4 frfromom users --这次没有出现“表不存在”的错误而是出现了列数不匹配的语法错误。这强烈暗示users表是存在的因为如果表不存在错误会先于列数不匹配错误出现。4.3 查询users表的列名并获取数据既然users表存在我们就需要知道它有哪些列。同样没有information_schema.columns我们只能猜。常见的用户表列名有id,username,password。我们可以通过union select把猜到的列名选出来看是否报错。构造payloadadmin ununionion selselectect 1, username, password, 4 frfromom users --这里我假设users表有username和password两列分别放在回显点2和3。执行后页面成功显示了数据在Welcome!下方我看到了两行数据一行是admin和其对应的密码一串哈希值另一行则是一个看起来非常像flag的字符串flag{xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}。Bingo这就是我们要找的flag。4.4 最终Payload与完整流程梳理让我们把最终的、成功的payload再完整写一遍并梳理整个流程的逻辑探测注入点与注释符admin --成功登录确定为单引号字符型注入可用--注释。探测过滤规则发现select,union,from,where,and,or等关键字被过滤。采用双写绕过如selselectect,ununionion,frfromom。确定列数与回显点admin ununionion selselectect 1,2,3,4 --成功执行页面显示2和3确定共4列第2、3列为回显点。获取数据库名admin ununionion selselectect 1, database(), 3,4 --得到数据库名geek。猜解表名通过尝试admin ununionion selselectect 1,2,3,4 frfromom users --引发的错误类型差异推断出users表存在。猜解列名并提取数据基于经验猜测users表存在username和password列构造payloadadmin ununionion selselectect 1, username, password, 4 frfromom users --。获取Flag执行后在回显位置直接看到flag{...}字符串解题完成。5. 技术原理深度剖析与防御思考5.1 双写绕过过滤的原理与局限性这道题的核心绕过技巧是“双写”。它的生效完全依赖于过滤函数的实现逻辑。一个简单的、不安全的过滤函数可能像下面这样以PHP为例function blacklist_filter($input) { $blacklist array(select, union, from, where, and, or, information_schema); foreach ($blacklist as $word) { $input str_ireplace($word, , $input); // 不区分大小写地替换为空 } return $input; }当输入selselectect时函数首先找到中间的select并将其删除得到sel ect。由于函数只执行一次全局查找替换它不会对结果sel ect再进行一次扫描看是否能组成新的select。因此sel和ect被保留最终在SQL引擎中解析时它们被拼接成了select关键字。这种过滤方式的局限性非常明显递归删除问题如果过滤函数是递归执行的即删除一次后对结果字符串再次进行过滤那么双写绕过就会失效。正则表达式匹配如果使用正则表达式进行单词边界匹配如\bselect\b那么selselectect中的select因为前后没有单词边界它前后都是字母可能根本不会被匹配到从而导致双写绕过失败但union select这样的组合却可能被\bselect\b匹配到。本题的过滤可能更接近于简单的字符串替换而非严谨的正则匹配。过滤顺序如果过滤顺序有误也可能导致问题。例如先过滤union再过滤select对于ununionion selselectect可能得到不可预测的结果。5.2 为何不使用预编译语句这是最关键的防御点。SQL注入的根本原因在于“数据”和“代码”SQL指令被混在一起。预编译语句Prepared Statements通过将SQL语句的骨架如SELECT * FROM users WHERE username? AND password?与具体的参数值如admin和123456分开发送给数据库彻底解决了这一问题。数据库会先编译SQL骨架确定执行计划然后再将参数值作为纯数据处理无论参数里包含什么特殊字符、--、union等都不会改变原SQL语句的结构从而从根本上杜绝了注入。在PHP中使用PDO进行预编译的示例$stmt $pdo-prepare(SELECT * FROM users WHERE username :username AND password :password); $stmt-execute([username $input_username, password $input_password]); $user $stmt-fetch();在这种情况下即使$input_username是admin --它也会被整体作为一个字符串去和username字段比较而不会断开SQL语句。5.3 其他辅助防御措施除了预编译这道“铁闸”还可以采取以下措施纵深防御最小权限原则连接数据库的Web应用账号只授予其访问特定数据库、特定表的最小必要权限绝对不要使用root或拥有FILE、PROCESS等高级权限的账号。这样即使发生注入攻击者能造成的破坏也有限。关闭错误回显像本题这样详细的错误信息是攻击者的“指路明灯”。生产环境应配置PHP或相应语言和Web服务器如Nginx/Apache不向用户显示具体的数据库错误信息而是记录到日志中前端只返回统一的、友好的错误页面。输入验证与白名单对于已知固定格式的输入如手机号、邮箱、数字ID使用白名单正则进行严格验证。例如用户ID如果只能是数字那么就用is_numeric()或正则/^\d$/进行校验非数字直接拒绝。Web应用防火墙WAF部署WAF可以在网络层面拦截常见的攻击payload作为一道前置防线。但WAF可能存在绕过风险不能替代代码层面的安全开发。6. 常见问题与排查技巧实录在实战和教学过程中我总结了一些关于此类SQL注入题目的常见疑问和技巧Q1: 我用了双写为什么还是被拦截了A1: 可能有以下几种情况 -过滤函数是递归的尝试selselectect如果递归过滤第一次变成select第二次又被过滤成空。可以尝试三写甚至多写如seleselectlectct但成功率不高。 -过滤了空格有些WAF会过滤或编码空格。可以尝试用/**/MySQL注释符可充当空格、%0a换行符、%0d回车符、%09制表符来替代空格。本题中--后面的空格就很重要。 -大小写绕过失效如果过滤使用str_ireplace不区分大小写则大小写混合如SeLeCt无效。如果使用区分大小写的str_replace则可以尝试。 -整体字符串匹配像information_schema这种可能被作为一个整体字符串匹配双写infoorrmation_schema可能无效。需要尝试其他方法如使用mysql.innodb_table_stats或盲注。Q2: 如何判断是字符型还是数字型注入A2: 最经典的测试方法是 - 输入id1如果报错很可能是字符型。 - 输入id1 and 11和id1 and 12。如果前者正常后者异常是数字型。如果都异常可能是字符型且引号未闭合。 - 输入id1 and 11和id1 and 12。如果前者正常后者异常是字符型。 本题的报错信息直接显示了单引号不匹配是明显的字符型。Q3: 使用union select时一定要猜列数吗有没有其他方法A3:order by是更常用的方法通过order by N递增N直到报错N-1就是列数。例如admin order by 5 --报错order by 4正常则列数为4。但本题中order by可能被过滤所以采用了union select null递增的方法。在一些支持||字符串拼接的数据库如Oracle, PostgreSQL中还可以用union select null||null||...来试探。Q4: 报错注入函数updatexml和extractvalue被过滤了怎么办A4: 除了这两个还可以尝试 -floor(rand(0)*2)结合group by和count(*)触发重复键错误需要特定版本和条件。 -exp(~(select * from(select user())x))利用双精度溢出报错。 -geometrycollection()multipoint()等空间函数参数错误报错。 但这些方法往往有更严格的环境限制。在CTF中如果报错注入路径被堵死应立刻转向布尔盲注或时间盲注。Q5: 拿到一个注入点最系统的信息收集步骤是什么A5: 可以遵循以下流程 1.判类型字符型还是数字型用什么闭合 2.试注释--、#、/* */哪个能用 3.测过滤尝试常见关键字union,select,from,where,and,or,空格,逗号等观察是否被拦截思考绕过方式大小写、双写、编码、等价替换。 4.定列数用order by或union select null确定列数。 5.找回显用union select 1,2,3,...确定数据在页面哪个位置输出。 6.取信息通过回显点依次获取数据库版本version、当前用户user()、当前数据库database()。 7.爆结构通过information_schema或系统表/视图获取所有数据库名、表名、列名。如果被过滤尝试盲注或猜解。 8.拿数据从目标表中查询最终需要的数据如管理员密码、flag。这道“BabySQL”题目麻雀虽小五脏俱全它系统地考察了注入点判断、注释符使用、关键字过滤与绕过、列数判断、回显点定位以及在没有information_schema情况下的数据获取思路。通过这道题的练习你能对SQL注入有一个从理论到实战的深刻理解。记住安全的核心在于“不信任任何用户输入”而作为学习者理解攻击手法是为了更好地构建防御。在实际开发中请务必使用预编译语句。