资讯动态

SQL注入实战:无列名注入结合group_concat与注释符绕过过滤拿flag

发布时间:2026/10/21 15:17:37 来源:尧图企业网站定制
周末翻出某公开CTF题库平台上的一道老题2019年某高校赛的Web1重新跑了一遍。这题从登录后台、找注入点、爆表名一路都很常规真正卡人的是最后一步明明知道flag表存在却拿不到列名。于是把无列名注入、group_concat、/**/注释符这三样东西串起来才解出来。今天就把这条完整链路拆开写一遍适合已经会基础SQL注入、但对无列名注入还停留在“见过没用过”状态的读者。经常刷Web题的人应该都有这种感觉SQL注入的常规套路背得滚瓜烂熟information_schema三步走就能拿数据但一旦过滤规则把某个关键环节掐死就容易卡住。这题就是典型——列名信息直接被过滤规则干掉了逼着你绕开information_schema.columns改从联合查询的临时列名下手。说白了无列名注入就是“知道表名、不知道列名”时取数据的最后一根稻草。1. 无列名注入到底在解决什么问题1.1 常规注入流程里最容易被卡的一环平时做SQL注入思路基本固定先确认注入点再判断列数然后从information_schema.schemata拿库名从information_schema.tables拿表名从information_schema.columns拿列名最后拼接查询拿数据。这套流程在绝大多数题目里都成立但有一个前提——information_schema相关查询能被正常执行。一旦过滤规则把columns这个关键词干掉了或者当前数据库用户对information_schema没有读取权限整个流程就会断在“拿列名”这一步。我见过不少题目会把information_schema或column直接置空就是为了逼选手换思路。还有一类场景是出题人故意不给列名比如直接告诉你数据在flag表里但列名叫什么就是不让你通过常规手段查。无列名注入解决的就是这个问题我不去查列名而是利用联合查询时系统自动生成的临时列名按列的位置顺序直接取数。这就好比你要找一个陌生人的手机号正常做法是翻通讯录目录按姓名查但目录被锁了你只能拿到整本通讯录那就按“第几页第几行”去找——虽然笨但确实能找到。1.2 联合查询的隐藏规则临时列名是可以“借用”的无列名注入的核心原理藏在MySQL和MariaDB对union select的处理规则里。执行select 1,2,3 union select * from flag时第一行结果集的三个常量会被自动当成列名也就是临时列名变成了1、2、3。这个行为在MariaDB和MySQL里是一致的属于SQL解析的默认规则。有了这层规则就能做一件有意思的事把select 1,2,3 union select * from flag整个包成一个子查询再给这个子查询起个别名就可以用别名.数字去引用对应位置的列。比如select a.2 from (select 1,2,3 union select * from flag) a这段SQL的意思是先通过联合查询构造一个临时表a这个表有三列列名分别是1、2、3然后从里面取第二列的数据。这里的a.2不是小数而是“表别名a的名为2的列”SQL解析器能正确区分。用无列名注入有前提条件第一你得知道目标表大概有几列联合查询两边列数不一致会直接报错第二你得知道目标表名第三目标表的列不能太多因为构造联合查询时你得把列数补齐。CTF里常见的flag表一般就两到三列用select 1,2,3或者select 1,2,3,4完全够用。1.3 group_concat与/**/这两个“老伙计”能干什么group_concat在注入里的作用很简单把多行结果合并成一行默认用逗号分隔。这个函数在联合查询只显示一行结果的时候几乎是必备的——CTF注入点的输出位往往只有一个如果不把多行数据拼成一行limit 0,1只能一条条翻效率低不说还容易触发各种过滤。用group_concat可以直接把所有表名或所有数据一次性拉出来。/**/则是一个SQL注释符在MySQL和MariaDB里/*和*/之间的内容会被当作注释忽略。但它有个特殊用法在SQL语法里可以代替空格作为分隔符。也就是说当过滤规则把空格删掉时select 1,2,3会变成select1,2,3导致语法错误但写成select/**/1,2,3就能正常执行。不少过滤规则只处理空格字符不处理注释符所以/**/就成了绕过空格过滤最稳定的手段。它比%0a换行符、%09制表符更通用因为有些题目的过滤规则会把%0a也干掉但过滤/**/的题目反而少。而且/**/的可读性最好payload写出来自己能看懂排查问题也方便。无列名注入和这两个工具凑在一起就是一个完整的组合拳group_concat负责把多行数据压成一行/**/负责让payload在空格被过滤的环境里正常执行无列名注入负责绕过列名获取。三者结合就能在被过滤规则逼到墙角的时候硬生生把数据抠出来。2. 题目环境还原过滤与注入点2.1 登录后台与功能梳理这道题的环境大致是这样的进入站点后有一个普通的登录页面注册一个账号进去即可。登录之后是广告管理后台可以发布广告广告有标题和内容两个字段列表页有一个排序功能排序的参数从URL传入。整个功能不多但注入点就藏在看起来无害的排序功能里。我在复现的时候先按常规流程走了一遍打开广告列表页URL末尾有个sort参数试着改成sort1页面正常改成sort1页面直接报错说明存在SQL注入。再往深处试发现很多输入会被“处理”掉具体表现是某些字符传进去之后在SQL语句里消失了页面要么空白要么报错。注册登录这个环节没什么好说的重点是登录之后的功能面。广告列表的排序参数虽然不起眼但这类参数在后台SQL里往往直接拼接进order by子句本身就比较危险。如果开发者再偷懒不加过滤那注入就是白送。这道题确实是白送了注入点但过滤规则把后续的常规路堵死了。2.2 过滤规则探测过程我在实际探测时习惯先列一个测试清单把所有可能被过滤的字符和关键词依次试一遍。先测空格sort1 order by 3页面报错用sort1/**/order/**/by/**/3页面正常。这就基本确定空格被过滤/**/可用。再测注释符sort1#和sort1-- -都不行说明#和--这类行注释也被处理了。然后测关键词union能用select能用and和or被过滤需要靠和||或者直接改用其他语法绕。最后试information_schema.columns发现columns这个词被置空整个查询报废。我整理了一份过滤规则表方便对照输入内容执行结果说明sort1 order by 3报错空格被过滤sort1/**/order/**/by/**/3正常/**/可替代空格sort1#报错#注释被过滤sort1-- -报错--注释被过滤sort1 union select 1,2,3正常union可用sort1 and 11报错关键字and被过滤sort11information_schema.columns结果为空column关键词被置空这组过滤规则看起来零零散散但组合起来就把常规注入的路堵得差不多了空格没了注释符没了列名信息拿不到。唯一的好消息是/**/和union还能用这就给无列名注入留了口子。2.3 为什么说这是MariaDB环境判断数据库类型在注入里是个关键步骤。这道题的报错信息和函数行为都指向MariaDB比如version()函数返回的字符串带MariaDB标识information_schema的表结构也和标准MySQL有些细节差异。虽然MariaDB和MySQL在很多语法上完全兼容但在注入场景里有些行为差异值得留意。无列名注入在MariaDB下完全可用这跟它是MySQL的分支有关系。联合查询生成临时列名的规则、派生表必须起别名的规则MariaDB都继承了。我在本地用一个MariaDB 10.x的靶场环境验证过select a.2 from (select 1,2,3 union select * from user)a这种写法能正常返回数据说明这个技巧在MariaDB环境里没有兼容性坑。还有一个细节MariaDB的information_schema里columns表同样存在但这题的过滤规则把column关键词直接置空了所以即使MariaDB本身没做限制你也查不到列名。这也是出题人刻意为之——环境用MariaDB考点却是无列名注入两者搭配得刚刚好。3. 完整解题链路与关键payload拆解3.1 用/**/绕过空格拿库名和表名确定/**/可用之后第一步是获取当前数据库名。我比较推荐用联合查询因为输出位明确调试方便。先判断列数用order by逐级递增sort1/**/order/**/by/**/1 sort1/**/order/**/by/**/2 sort1/**/order/**/by/**/3 sort1/**/order/**/by/**/4从1试到4order by 4时报错说明查询结果一共三列。然后构造联合查询sort-1/**/union/**/select/**/1,2,3把sort改成负数让前面查询结果为空这样联合查询的第二部分就能显示出来了。页面显示第二列有回显那2就是输出位。接下来把2替换成database()就能看到库名。拿到库名后用一个子查询把表名全列出来sort-1/**/union/**/select/**/1,group_concat(table_name/**/separator/**/0x7e),3/**/from/**/information_schema.tables/**/where/**/table_schemadatabase()这里有个细节过滤规则没有动information_schema.tables所以表名信息还能拿到。返回结果里有一个明显的flag表还有一个ads表。第一步到此完成知道库名知道目标表名是flag。3.2 列名被过滤后怎么判断该用无列名注入按照常规流程下一步应该查flag表的列名也就是访问information_schema.columns。但前面已经测试过column关键词会被置空所以这个查询基本没戏。我不死心又试了大小写混写、注释符分隔等方式全部碰壁。这里有一个判断逻辑值得展开当常规拿列名的路被堵死时先别急着放弃可以先确认目标表的列数和数据分布。列数可以通过order by的无列名版本来探测也可以直接用构造联合查询的方式来试。比如sort-1/**/union/**/select/**/1,(select/**/1,2,3/**/union/**/select/**/*/**/from/**/flag),3如果flag表恰好是三列1,2,3和select *两边列数一致嵌套查询就不会报错。如果报错就调整数字的个数。这一步的目的不是直接拿数据而是确认“这个表能用无列名注入搞定”。确认列数后还有关键一步确认这个表有数据而且数据可读。我试着直接select * from flag看回显但联合查询的语法限制导致这个操作得包在子查询里才能继续处理。不管怎样到这里已经能确定flag表就是最后的目标列名拿不到必须走无列名注入。3.3 最终的无列名payload逐段拆解最终解题payload长这样sort-1/**/union/**/select/**/1,(select/**/group_concat(a.2)/**/from/**/(select/**/1,2,3/**/union/**/select/**/*/**/from/**/flag)a),3这个payload看起来长其实拆开就三层。最内层是select 1,2,3 union select * from flag作用是构造一个三列的临时表临时表的列名是1、2、3数据行是flag表里的内容。注意这一步必须保证两边的列数一致所以前面的列数探测是必要的。中间层是(select 1,2,3 union select * from flag)a包在括号里并起别名a。这个别名绝对不能省MariaDB和MySQL都强制要求派生表必须有自己的别名不写会在执行时报错Every derived table must have its own alias。我当时在这上面卡了十分钟一直以为payload写错了其实是漏了别名。外层是select group_concat(a.2) from (...)a作用是把临时表a的第二列数据用group_concat拼成一行。之所以套group_concat是因为flag表里可能有多行数据不合并的话联合查询只回显一行超过一行的数据就丢了。a.2就是取第二列如果flag的第二列才是真正的flag字符串这一步就能直接读出来。我实际执行后回显位置吐出了一串字符串隔一段就出现~分隔符这就是用separator/**/0x7e设置的。仔细看能看到flag{...}的片段但被分隔符截开了。这说明flag表不止一列数据于是我把a.2换成a.1、a.3一分列拉出来后拼完整。3.4 拿到flag后的数据清洗CTF题目拿到flag只是“万里长征走完一半”很多时候数据是分散的、被截断的、带分隔符的。我就遇到过group_concat拼接结果太长被默认的group_concat_max_len截断的情况。虽然实际输出没爆但最好有准备。清洗方法有几个实操要点。第一用separator指定一个不容易混淆的分隔符比如0x7e就是~的十六进制比默认逗号好辨认得多。第二如果数据里本来就有逗号或波浪号换一个更冷门的分隔符比如0x23是#。第三数据太长被截断时不要硬拼改用limit一行一行取或者用substring分段截取。还有一个容易被忽略的地方flag表如果有多列不要把希望全押在一列上。我第一轮只看到第二列有东西以为大功告成结果拿到的只是字符串的一部分。后来把三列都拉出来才发现真正的flag分散在第一列和第三列里需要拼接才完整。多列数据可以这样一次取多列select group_concat(concat(a.1,0x7e,a.2,0x7e,a.3)) from (select 1,2,3 union select * from flag)a这样每行数据都会带上分隔符一眼就能看出哪列有货。4. 实战中的坑与排查速查表4.1 子查询必须要有别名这是无列名注入最容易踩的坑。派生表是SQL标准里一个特殊结构MySQL和MariaDB都强制要求它必须带别名。写from (select 1,2,3 union select * from flag)不加别名执行时直接报错。我排查这类问题有一个习惯如果payload本身写得没错但一直报错先从语法细节找优先看有没有漏掉别名。报错信息里出现Every derived table must have its own alias那就是别名问题老实加个a在后面就行。这个坑不仅出现在无列名注入里任何子查询套嵌都可能遇到记住了能省不少时间。4.2 数字列名的引号问题无列名注入里用a.2引用第二列这里的2不是字符串是标识符。在大多数情况下a.2这种写法可以直接用但某些场景下2会被解析器当成数字而不是列名导致报错Unknown column 2 in field list这时候需要给列名加反引号select group_concat(a.2) from (select 1,2,3 union select * from flag)a反引号在MySQL和MariaDB里是标识符引用符加上之后解析器就明确知道2是列名而不是常量。我问过不少朋友他们都在这里栽过跟头。实际使用建议是先不加反引号直接试报错了再加省得画蛇添足。4.3 过滤逗号时的替代方案有些题目把逗号也过滤了那union select 1,2,3这步就麻烦。解决办法是用join语法代替逗号分隔比如用union select 1 join 2 join 3严格来说select 1 join 2不是标准写法实际更常用的是利用from后面的多个表或join关键字来传多个拼接内容。不过如果只是group_concat要指定分隔符那本身不需要逗号group_concat(a.2 separator 0x7e)就能搞定。如果是limit 0,1被逗号卡住改成limit 1 offset 0就行。这些变体都属于“过滤规则逼出来的语法体操”建议提前记熟遇到类似的题直接套用。4.4 无列名注入与报错注入的混搭如果题目环境不显示联合查询结果只显示报错信息无列名注入也能配合报错函数使用。典型写法是用updatexml或extractvalue触发报错并在报错内容里带上无列名子查询的结果sort1/**/and/**/updatexml(1,concat(0x7e,(select/**/a.2/**/from/**/(select/**/1,2,3/**/union/**/select/**/*/**/from/**/flag)a),1)这个payload把无列名子查询的结果放进了concat报错信息会显示~数据。我测试后发现这种方式在order by注入场景下尤其好用因为排序参数后面接and和表达式是合法的报错回显又能绕过显示位限制。可见无列名注入不是只能配合联合查询它是一个通用取数工具只要有任何一种回显渠道就能把数据带出来。5. 事后复盘我踩过的坑和一些个人体会这道题真正有价值的地方不在于难而在于它把几个知识点串得恰到好处。空格过滤逼你用/**/列名信息被禁逼你走无列名注入输出行数限制逼你用group_concat。三个过滤点没有任何一个单独看是致命的但合在一起就把常规思路全堵死了逼着你换角度思考。我个人在复现时最有收获的一点是意识到“无列名注入”不是一个专属于CTF的奇技淫巧它本质上是对SQL联合查询机制的理解。只要你理解了“联合查询自动生成临时列名”这个规则很多变体都能推导出来。比如flag表只有一列时select 1 union select * from flag后取a.1有三列时取a.1、a.2、a.3数据多行时套group_concat数据拖太长时用limit分段取。这些都是同一个原理的排列组合不需要死记硬背。如果你也想练这类题建议本地搭一个MariaDB环境把过滤规则模拟出来然后手写payload一遍遍地试。我试到第三遍才彻底理解a.2的引用逻辑和group_concat的位置关系。纸上得来终觉浅SQL注入这种东西真的得自己打一遍才能形成直觉。最后再分享一个小技巧无列名注入里如果flag表列数很多不要急着猜直接用二分法通过order by 5、order by 10这种递增方式快速圈定列数范围再用联合查询嵌套验证比自己一个一个猜数字高效得多。这题虽然到这里就结束了但这个思路换个过滤规则、换道题照样能用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑