前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >[MYSQL] 自动收集统计信息意外阻止了SQL注入攻击?

[MYSQL] 自动收集统计信息意外阻止了SQL注入攻击?

原创
作者头像
大大刺猬
发布2024-11-27 14:53:27
发布2024-11-27 14:53:27
3900
举报
文章被收录于专栏:大大刺猬大大刺猬

背景/现象

业务反馈,有张表查询慢.(实际上查询不出来...), 登录数据库发现有条比较特殊的SQL(熟悉sql的小伙伴可能都猜出来这sql是干嘛的了), 其它均正常

注: 本次所有截图均为模拟环境. 包括相关SQL

show engine innodb status之类的也无异常信息

但就是查询数据慢,表也不大, 一查询该表就是Waiting for table flush

代码语言:sql
复制
(root@127.0.0.1) [(none)]> select * from information_schema.tables where table_schema='db1' and table_name='sbtest1'\G
*************************** 1. row ***************************
  TABLE_CATALOG: def
   TABLE_SCHEMA: db1
     TABLE_NAME: sbtest1
     TABLE_TYPE: BASE TABLE
         ENGINE: InnoDB
        VERSION: 10
     ROW_FORMAT: Dynamic
     TABLE_ROWS: 98156
 AVG_ROW_LENGTH: 358
    DATA_LENGTH: 35192832
MAX_DATA_LENGTH: 0
   INDEX_LENGTH: 2637824
      DATA_FREE: 37748736
 AUTO_INCREMENT: 100001
    CREATE_TIME: 2024-07-05 14:45:10
    UPDATE_TIME: NULL
     CHECK_TIME: NULL
TABLE_COLLATION: utf8_general_ci
       CHECKSUM: NULL
 CREATE_OPTIONS: 
  TABLE_COMMENT: 
1 row in set (0.00 sec)

(root@127.0.0.1) [(none)]> show processlist;
+-----+-------------+-----------------+------+---------+-------+----------------------------------+-------------------------------------------------------------------------+
| Id  | User        | Host            | db   | Command | Time  | State                            | Info                                                                    |
+-----+-------------+-----------------+------+---------+-------+----------------------------------+-------------------------------------------------------------------------+
|   1 | system user |                 | NULL | Connect | 17210 | Waiting for master to send event | NULL                                                                    |
| 305 | root        | localhost:45326 | NULL | Query   |     0 | starting                         | show processlist                                                        |
| 746 | root        | localhost:48086 | NULL | Query   |    54 | Waiting for table flush          | select * from db1.sbtest1 limit 1                                       |
| 750 | root        | localhost:48124 | db1  | Query   |   344 | User sleep                       | select  k from sbtest1 where id=2 or sleep(1) and database() like 'db%' |
+-----+-------------+-----------------+------+---------+-------+----------------------------------+-------------------------------------------------------------------------+
4 rows in set (0.01 sec)

分析

我们查询问题表的时候, 状态是Waiting for table flush, 我们查询官网可知该状态表示: 准备重新打开表, 正在等待其它线程关闭表. 也就是'有人'触发了FLUSH TABLES导致的, 或者是类似的SQL, 比如表ALTER TABLE,ANALYZE TABLE之类的, 而收集统计信息的话, 还涉及到自动收集. 我们分析binlog, 未找到相关的DDL, 但是发现了很多该表的INSERT操作, 再加上该表不大, 就很容易触发自动收集统计信息, 然后就需要重新打开表, 但是由于有SQL未跑完, 就无法重新打开表, 也就是对该表的任何查询都无法做(包括show create table).

The thread is executing FLUSH TABLES and is waiting for all threads to close their tables, or the thread got a notification that the underlying structure for a table has changed and it needs to reopen the table to get the new structure. However, to reopen the table, it must wait until all other threads have closed the table in question. This notification takes place if another thread has used FLUSH TABLES or one of the following statements on the table in question: FLUSH TABLES tbl_name, ALTER TABLE, RENAME TABLE, REPAIR TABLE, ANALYZE TABLE, or OPTIMIZE TABLE.

所以问题就来到了那条神奇的SQL.

代码语言:sql
复制
select  k from sbtest1 where id=2 or sleep(1) and database() like 'db%';

这种写法大概率是SQL注入了,一般业务逻辑是不会在where条件中加sleep的. 而sql注入, 通常还会包含or 1=1之类的条件的, 我这里是演示环境,就不写那么复杂了.

从该SQL逻辑来看是 database() like 'db%' 为True时, 就会走sleep(1), 也就是可以通过返回时间来判断后面的条件是否为真, 从而猜测出数据库名. 也就是SLEEP注入.

模拟验证

当我们生产环境遇到问题时, 别马上就干,或者下结论(除非之前就遇到过了). 一定要先在测试环境验证一下.

准备测试数据

代码语言:sql
复制
create table t20241127(id int primary key, name varchar(200));
insert into t20241127 values(1,'ddcw');
insert into t20241127 values(2,'https://github.com/ddcw');

模拟条件为真时:

我们看到返回时间为2秒(实际上会加上查询时间和延迟,我这里是本机就没得那么多了)

模拟条件为假时:

我们的sql马上就返回了, 耗时远远小于sleep的时间, 从而推断 数据库名字的第二个字母不是a, 然后就可以慢慢试来了.

继续分析

理论上通过上述方式我们就可以猜到数据库的一些基本信息,比如数据库名之类的. 该类SQL由于有sleep之类的操作, 所以能算是慢SQL,就可以在慢日志里面找到对应的SQL验证了. 通常会有试探性的sql, 发现没被禁用后,才会大规模的来试探. 但遗憾的是业务数据量变化太大触发了统计信息的自动收集, 从而要求重新打开表, 而之前试探的sql又查询得慢,没有跑完,于是后面的sql就无法重新打开表, 只能Waiting for table flush, 也就变相的导致了sql注入无法继续. (真离谱!)

处理方法

由于该sql导致了该表无法查询, 所以kill对应的线程即可. 但该接口会导致SQL注入, 临时处理方法是:禁用该接口.

后续再做调整.(比如验证用户输入,后端验证,别光前段验证). 如果应用能抓到执行该SQL的ip的话, 直接把它IP禁了也是不错的选择.

总结和预防

总结

本次的查询异常不是锁的问题(比如MDL),但也差不了多少了.但运气好的是自动收集统计信息间接阻断了后续的攻击,也让我们能发现存在该类攻击.

对于系统表的各种值要有一定的了解, 这就要多看官网了, 不要求全部记住, 只要记住官网有没有, 大概在哪一张之类的就行. 然后就是搜索了. 比如异常SQL的状态是User sleep, 我们查询官网可知是在执行sleep()函数

快速复现

如果有兴趣想要快速复现该现象的话, 可使用手动收集统计信息. 参考SQL如下.

代码语言:sql
复制
-- session 1 准备数据并模拟异常sql
create table test_sql_injection(id int, name varchar(200));
insert into test_sql_injection values(1,'ddcw'),(2,'https://github.com/ddcw');
select * from test_sql_injection where id=2 or sleep(100) and database() like 'd%'; -- 假设数据库第一个字母是d

-- session 2 手动收集统计信息 并查询该表
analyze table test_sql_injection;
select * from test_sql_injection;

-- session 3 观察现象
show processlist;

预防

关于SQL注入的预防, 通常是在业务层实现的.(数据库层不好做). 比如验证业务输入值的有效性,不要一股脑的扔给数据库. 尽量不要人工拼写sql, 可以使用PREPARE或者sqlarchemy之类的ORM.

再附个prepare放注入的例子吧

代码语言:sql
复制
PREPARE stmt_1 FROM 'select id,k from sbtest1 where id=?';
set @id_1 = "1 or sleep(2) and database() like 'd%'";
EXECUTE stmt_1 USING @id_1;

从逻辑讲相当于是select id,k from sbtest1 where id=1 or sleep(2) and database() like 'd%';, 但实际上是select id,k from sbtest1 where id=1 后面半截异常的被截断了.

参考

https://dev.mysql.com/doc/refman/8.0/en/general-thread-states.html

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景/现象
  • 分析
  • 模拟验证
  • 继续分析
  • 处理方法
  • 总结和预防
    • 总结
    • 快速复现
    • 预防
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档