FEATURED · 精选文章

SQL注入进阶:无列名注入原理与实战绕过information_schema过滤

发布时间 / 2026/8/8 7:15:52
来源 / 创域科博编辑部
栏目 / 资讯中心
SQL注入进阶:无列名注入原理与实战绕过information_schema过滤 1. 从一道“广告发布系统”CTF题看无列名注入的实战演变最近在复盘一些经典的Web安全CTF题目发现一道来自SWPU2019的Web1题虽然题目本身叫“广告发布系统”听起来平平无奇但它的解题路径却非常典型地串联起了SQL注入从基础到高阶的几种关键手法。很多刚接触安全的朋友可能对union select、order by这些基础操作很熟悉但一旦遇到过滤了information_schema、甚至不知道表名和列名的情况就容易卡住。这道题恰好就是一个绝佳的教学案例它引导你一步步思考当常规的“地图”information_schema被没收后你该如何在数据库的迷宫里找到你要的“宝藏”flag这道题的核心考点就是无列名注入。这不仅仅是CTF里的炫技在真实的渗透测试中当管理员意识到information_schema是敏感信息并加以过滤时这种技术就成了突破的关键。今天我就结合这道题把无列名注入的原理、几种实战手法以及背后的思考逻辑彻底讲透。你会发现它本质上是一种基于SQL语言本身特性的“逻辑推导”而不是什么黑魔法。2. 环境初探与常规注入点的发现题目通常是一个简单的Web应用界面。第一步永远是信息收集。我们打开页面可能看到一个广告列表或者一个搜索框。用最基础的方式测试比如在搜索框输入一个单引号‘观察页面是否返回数据库错误信息或者页面内容/结构是否发生异常变化如部分内容消失。假设我们输入1‘后页面显示了SQL语法错误这基本确认了存在SQL注入漏洞。接下来我们需要判断注入的类型字符型还是数字型以及闭合方式。通过尝试1‘ and ‘1’‘1和1‘ and ‘1’‘2观察页面返回结果是否不同可以确认是字符型注入并且字符串是用单引号闭合的。注意在实际CTF或测试中错误信息可能被屏蔽。这时就需要依赖布尔盲注或时间盲注的技巧通过页面返回内容的真假如图片是否加载、文字是否存在或响应时间的差异来判断注入语句是否成功执行。这是基本功必须熟练掌握。确认注入点后我们会尝试获取数据库的基本信息。经典的步骤是判断字段数使用order by语句例如1‘ order by 1--逐渐增加数字直到页面返回错误从而确定select查询的字段数量。假设我们测试出字段数是2。判断回显点使用union select联合查询将我们可控的数据显示在页面上。例如-1‘ union select 1,2--。这里-1‘是为了让原查询不返回结果从而让我们的union select结果得以显示。页面可能会在某个位置显示数字“1”或“2”这就是我们可以利用的回显点。走到这一步很多新手会下意识地去查information_schema。通常会构造这样的payload-1‘ union select 1, group_concat(table_name) from information_schema.tables where table_schemadatabase()--目的是列出当前数据库中的所有表名。然而在这道SWPU2019的Web1题目中你会发现这条语句执行失败了。页面可能没有任何回显或者直接提示错误。这就是题目设置的第一个障碍过滤了information_schema库的访问。在真实环境中这可能是WAFWeb应用防火墙的规则也可能是开发人员手动过滤了相关关键字。当information_schema这条路被堵死我们就进入了“盲区”。你不知道表名更不知道列名union select似乎失去了方向。但这正是考验你对SQL理解深度的时候。我们需要转换思路。3. 核心突破理解无列名注入的基本原理无列名注入顾名思义就是在不知道表名和列名的情况下依然能够从数据库中提取数据。它的基石是SQL的**别名Alias和子查询Subquery**特性。让我们忘掉information_schema。假设我们已经通过某种方式比如错误提示、时间盲注的延时判断猜解或者推断出了一个疑似存放flag的表名例如flag、secret、users等。在CTF中表名常常是flag。我们假设目标表名就是flag。我们知道表名flag但不知道里面有什么列。传统的select * from flag需要知道列名才能指定输出。这里无列名注入的第一种手法登场了手法一利用子查询与别名进行逐位猜解我们可以把整个select * from flag查询作为一个子查询并为这个子查询结果集起一个别名比如a。同时我们还需要为这个子查询结果集的每一列起别名。因为我们不知道列数所以需要先猜列数。select * from (select * from flag) a;这条语句本身是合法的但它只是原样输出。关键在于我们可以通过union select来“拼接”这个子查询并利用数字作为列别名来探测数据。假设我们通过order by猜出flag表有1列实际情况可能更多这里简化。我们可以构造-1‘ union select 1, (select 1 from (select * from flag) a) --等等这里的1是什么这就是关键在MySQL中反引号可以用来包裹列名。当我们写1时它并不是数字1而是被当作一个名为“1”的列别名。我们来拆解这个payload(select * from flag) a 这是一个子查询它选取flag表的所有内容并给这个结果集起别名叫a。select1from (select * from flag) a 这是外层查询它试图从子查询结果集a中选取列名为1的列。由于a结果集本身有列我们假设有一列实际列名未知但它的列名并不是“1”。所以我们需要在子查询内部就为列定义别名。正确的构造应该是这样的-1‘ union select 1, (select 1 from (select 1, 2, 3 union select * from flag) a) --这看起来复杂了我们再拆解最内层select 1, 2, 3 union select * from flag。这个union创建了一个临时结果集前半部分是我们自定义的三列数据1,2,3后半部分是flag表的所有数据。union要求前后列数一致所以我们自定义的列数必须等于flag表的列数这里假设为3列。中间层(select 1, 2, 3 union select * from flag) a。给这个临时结果集起别名叫a。此时a这个结果集就有了明确的列名第一列叫“1”第二列叫“2”第三列叫“3”。因为select 1, 2, 3语句定义了这些列名。最外层select1from ...。现在我们就可以从a中名正言顺地选取列名为“1”的列了。如果flag表的第一列就是我们想要的flag数据那么它就会随着union查询被“附加”到a结果集的“1”列下面从而被我们选取出来。如果flag表不止一列而flag数据在第三列我们就需要选取列3-1‘ union select 1, (select 3 from (select 1, 2, 3 union select * from flag) a) --这个过程就像是你不知道仓库里箱子列的名字但你通过先搬进去几个自己贴好标签1,2,3的箱子和仓库里原有的箱子混在一起然后你就可以按照自己贴的标签把整个仓库里对应位置的箱子内容取出来看了。4. 实战升级利用JOIN与USING语法进行更优雅的探测上面这种方法需要猜列数并且构造的payload比较长。还有一种在MySQL中更巧妙的方法利用JOIN ... USING语法。这种方法可以直接爆出列名在某些情况下更加高效。它的原理是利用了JOIN语句的USING子句。USING(column_name)用于指定两个表连接时依据的相同列名。如果指定的列名不存在就会报错。我们可以利用这个报错信息来泄露列名。假设我们猜测存在一个flag表。我们可以构造如下语句1‘ and (select * from (select * from flag a join flag b)c)这条语句尝试将flag表自连接a和b是它的两个别名并将连接后的结果集再赋予别名c。如果flag表存在但连接没有指定条件会产生笛卡尔积可能返回大量数据导致报错如“The SELECT would examine more than MAX_JOIN_SIZE rows”但这至少能证明表存在。更精妙的是下面这个1‘ and (select * from (select * from flag a join flag b using(column_name))c)这里的column_name就是我们想要探测的列名。如果flag表中存在名为column_name的列那么USING(column_name)语句成立查询会执行可能因为数据量大而报其他错。如果column_name列不存在MySQL会直接返回一个错误“Unknown column ‘column_name’ in ‘from clause’”。看列名通过错误信息泄露出来了这本质上是一种基于错误信息的注入。我们可以写一个脚本暴力猜解列名。常见的列名可能是id,username,password,flag,value,secret等。在CTF中很可能列名就是flag。一旦通过报错确认了列名比如列名就是flag那么获取数据就非常简单了直接select flag from flag即可完全不需要复杂的无列名技巧了。所以这种方法是在“无列名”场景下一种“曲线救国”获取列名的方法。5. 盲注场景下的无列名数据提取在真实环境或更复杂的CTF题中可能没有显式的回显点即我们之前看到的数字1,2位置。所有注入结果都不会直接显示在页面上这就是盲注。在盲注下实施无列名注入思路是一致的但手段需要换成布尔逻辑或时间判断。假设我们通过时间盲注确认了flag表存在并且有我们关心的数据。我们想逐字符取出flag列的数据。在知道列名的情况下盲注payload是这样的以时间盲注为例1‘ and if(ascii(substr((select flag from flag),1,1))102, sleep(2), 1)--这条语句的意思是如果flag表flag列的第一个字符的ASCII码等于102即字母‘f’则让数据库睡眠2秒否则立即返回。通过观察页面响应时间就能判断字符是否正确。在无列名且不知道列名的情况下我们就需要把之前提到的子查询技巧融入到这个盲注框架中。例如我们猜测flag在flag表的第1列1‘ and if(ascii(substr((select 1 from (select 1 union select * from flag) a),1,1))102, sleep(2), 1)--这个payload看起来复杂但核心逻辑很清晰(select 1 union select * from flag) a创建临时结果集a其第一列的别名是“1”。select1from ...从a中选取别名为“1”的列这里就包含了flag表第一列的数据。将这个数据作为substr()和ascii()函数的输入进行字符判断。将整个判断嵌入if(..., sleep(2), 1)实现时间盲注。通过循环遍历每一个字符的位置和可能的ASCII值我们就能像挤牙膏一样把完整的flag数据提取出来。这个过程非常耗时必须借助自动化脚本如Python的requests库或直接使用sqlmap的--techniqueT时间盲注技术并搭配--tamper脚本绕过过滤。6. 工具辅助与自动化利用手动构造这些payload尤其是在盲注情况下是不现实的。我们必须借助工具。最强大的工具莫过于sqlmap。但对于这种过滤了information_schema和特定关键词的题目直接使用sqlmap可能无法成功。这就需要我们为sqlmap编写tamper脚本。Tamper脚本的作用是在payload发送前对其进行混淆、编码或改写以绕过WAF或过滤机制。例如题目可能过滤了information_schema这个词。我们可以写一个tamper脚本将information_schema拆分成information/*注释*/schema利用注释符绕过字符串匹配。或者对于无列名注入的payload我们可以编写一个专门的tamper当sqlmap试图查询列名时将其payload替换成我们前面提到的join using或子查询别名的方式。更高级的用法是我们可以使用sqlmap的--sql-shell参数在成功注入后获得一个交互式的SQL shell。然后在这个shell里手动输入我们精心构造的无列名注入语句直接读取数据。这要求我们已经通过其他方式如--dbs爆数据库名确认了注入点可用并且对当前数据库结构有基本猜测。除了sqlmap自己编写Python脚本是更灵活的方式。脚本的逻辑通常如下首先爆破当前数据库名有时database()函数未被过滤。然后通过join using报错法尝试爆破表名常见表名字典爆破。找到疑似表如flag后再用join using报错法爆破列名。最后使用布尔盲注或时间盲注结合substr()和ascii()函数逐位读取指定表、指定列的数据。这个过程就像侦探破案每一步都基于上一步的发现进行推理和验证。7. 防御视角开发者如何避免此类漏洞作为开发者了解攻击手法是为了更好地防御。针对这道题展现的SQL注入尤其是无列名注入这种“进阶”手段防御措施需要层层加固根本解决使用参数化查询预编译语句。这是唯一能从根本上杜绝SQL注入的方法。无论是使用PHP的PDO、Python的sqlalchemy、Java的PreparedStatement都应该将用户输入全部作为参数传递而不是拼接进SQL字符串。这样即使用户输入包含了union、select、join等关键字数据库也会将其视为纯粹的数据而非可执行的SQL代码。最小权限原则给Web应用使用的数据库账户分配最小的必要权限。比如只授予它对特定业务表的SELECT、INSERT、UPDATE权限并且坚决拒绝它对information_schema、mysql等系统库的访问权限。这样即使发生了SQL注入攻击者也无法通过information_schema来窥探数据库结构极大地增加了攻击难度。这道题模拟的就是这种环境。输入验证与过滤虽然这不是银弹但可以作为辅助手段。对用户输入进行严格的类型检查比如ID必须是数字、长度限制并使用安全的过滤函数如PHP的mysqli_real_escape_string但请注意它并非绝对安全尤其是在不同字符集下。对于无法使用参数化查询的复杂场景如动态表名、列名必须使用白名单机制只允许特定的、预定义的值。错误信息处理绝对不要将详细的数据库错误信息直接返回给前端用户。像“Unknown column ‘xxx’ in ‘from clause’”这样的错误正是攻击者梦寐以求的信息泄露。应该配置自定义的错误页面只返回通用的错误提示并将详细错误记录到后端日志中供管理员排查。使用Web应用防火墙WAFWAF可以识别和拦截常见的SQL注入攻击模式。它可以配置规则来过滤information_schema、union select、sleep()、benchmark()等敏感关键词和函数。但要注意WAF可能被绕过如通过注释、编码、等价函数替换它应该作为纵深防御的一环而不是唯一依赖。通过这道“广告发布系统”CTF题我们深入走完了一次完整的、面对过滤的SQL注入攻坚流程。从最初的发现注入点到遇到information_schema被过滤的障碍转而理解并运用无列名注入的核心原理子查询别名再到探索更巧妙的JOIN USING报错手法最后讨论了在盲注环境下如何应用以及如何自动化。整个思考链路其价值远超过获取一个flag本身。它训练的是在限制条件下如何灵活运用已有知识进行突破的安全思维。这种思维无论是在CTF赛场上还是在真实世界的渗透测试中都是至关重要的。下次当你再遇到一个看似“无路可走”的注入点时不妨想想我是不是还能从SQL语言本身的基本特性里找到另一条路
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻