MySQL 密码防暴力破解插件:Connection Control

Connection Control 是 MySQL 8.0 引入的一个安全功能插件,后移植到 MySQL 5.7.17 和 5.6.35 版本。

其核心功能是:当客户端因账号或密码错误连续多次登录失败时,服务端会对该客户端的后续请求进行延迟处理,且失败次数越多,延迟时间越长。这一机制能显著增加密码被暴力破解的耗时,从而有效遏制此类攻击。

适用场景:

  • 面向公网开放的 MySQL 服务器。
  • 合规性与安全性要求较高的环境。

插件效果

首先我们看看该插件的效果。

复制代码
`# time mysql -h10.0.0.108 -uroot -p123`
ERROR 1045 (28000): Access denied for user 'root'@'10.0.0.75' (using password: YES)
real    0m0.013s


         # time mysql -h10.0.0.108 -uroot -p123
ERROR 1045 (28000): Access denied for user 'root'@'10.0.0.75' (using password: YES)
real    0m0.013s


               # time mysql -h10.0.0.108 -uroot -p123
ERROR 1045 (28000): Access denied for user 'root'@'10.0.0.75' (using password: YES)
real    0m0.013s


                     # time mysql -h10.0.0.108 -uroot -p123
ERROR 1045 (28000): Access denied for user 'root'@'10.0.0.75' (using password: YES)
real    0m1.013s


                           # time mysql -h10.0.0.108 -uroot -p123
ERROR 1045 (28000): Access denied for user 'root'@'10.0.0.75' (using password: YES)
real    0m2.013s


                              # time mysql -h10.0.0.108 -uroot -p123
ERROR 1045 (28000): Access denied for user 'root'@'10.0.0.75' (using password: YES)
real    0m3.014s

前三次没有延迟,第四次开始延迟 1 秒,之后每次失败都会增加 1 秒的延迟。

当连接被延迟时,其在SHOW PROCESSLIST中的状态是Waiting in connection_control plugin:

复制代码
mysql> show processlist;
+------+-----------------+-----------------+------+---------+--------+--------------------------------------+------------------+
| Id   | User            | Host            | db   | Command | Time   | State                                | Info             |
+------+-----------------+-----------------+------+---------+--------+--------------------------------------+------------------+
|    5 | event_scheduler | localhost       | NULL | Daemon  | 418840 | Waiting on empty queue               | NULL             |
| 1179 | root            | localhost       | NULL | Query   |      0 | init                                 | show processlist |
| 1187 | root            | 10.0.0.75:40920 | NULL | Connect |      3 | Waiting in connection_control plugin | NULL             |
+------+-----------------+-----------------+------+---------+--------+--------------------------------------+------------------+

                              3 rows in set, 1 warning (0.00 sec)

接下来我们看看该插件的使用方法、相关参数、禁用方法、注意事项以及实现细节。

使用方法

安装插件

复制代码
`INSTALL` PLUGIN CONNECTION_CONTROL SONAME 'connection_control.so';

        INSTALL PLUGIN CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS SONAME 'connection_control.so';

其中,connection_control 是核心功能插件,connection_control_failed_login_attempts 提供记录客户端失败尝试次数的information_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS表。

卸载插件

复制代码
`UNINSTALL` PLUGIN CONNECTION_CONTROL;
UNINSTALL PLUGIN CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS;

从 MySQL 9.2 开始,引入了 Connection Control component 来替代 Connection Control plugin。

相关参数和状态变量

参数

复制代码
mysql> show variables like '%connection_control%';
+-------------------------------------------------+------------+
| Variable_name                                   | Value      |
+-------------------------------------------------+------------+
| connection_control_failed_connections_threshold | 3          |
| connection_control_max_connection_delay         | 2147483647 |
| connection_control_min_connection_delay         | 1000       |
+-------------------------------------------------+------------+
3 rows in set (0.03 sec)

其中,

  • connection_control_failed_connections_threshold:触发延迟的失败尝试次数阈值,默认为 3。
  • connection_control_min_connection_delay:首次触发延迟时的最小延迟时间,默认 1000 毫秒,即 1 秒。
  • connection_control_max_connection_delay:最大延迟时间,默认 2147483647 毫秒,相当于 24.8 天。

状态变量

Connection_control_delay_generated

显示触发延迟的总次数。

除此之外,还可以通过information_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS查询不同客户端的失败尝试次数,例如:

复制代码
mysql> select * from information_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS;
+----------------------+-----------------+
| USERHOST             | FAILED_ATTEMPTS |
+----------------------+-----------------+
| 'root'@'10.0.0.108'  |               5 |
| 'root'@'127.0.0.1'   |               4 |
| 'root1'@'10.0.0.108' |               7 |
+----------------------+-----------------+
3 rows in set (0.00 sec)

如何禁用延迟功能

以下两种方法都可以:

  1. 卸载插件。
  2. 将 connection_control_failed_connections_threshold 设置为 0。

注意事项

  1. 当用户连续多次登录失败后,即使后续输入正确密码,首次成功登录仍会被延迟(仅限首次成功登录,后续登录不受影响)。这种设计虽然可能不符合部分用户的习惯,但从防止暴力破解的角度来看是合理的。如果正确密码不触发延迟,攻击者便可通过是否存在延迟来判断密码是否正确,这样就会削弱防暴力破解的效果。

  2. 修改 connection_control_failed_connections_threshold 会:

  • 将 Connection_control_delay_generated 重置为 0。
  • 清空 information_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS。

如果某个用户的失败次数过多,但希望在输入正确密码时不再等待,可以通过以下命令清空其延迟状态:

复制代码
`SET` GLOBAL connection_control_failed_connections_threshold = DEFAULT;
  1. 被延迟的连接依旧会占用连接数。

如果 connection_control_min_connection_delay 设置过大,且攻击端重试频率很高,可能导致连接数被耗尽,影响正常用户的连接。

实现细节

以下是 Connection Control plugin 被调用的时机:

复制代码
static int check_connection(THD *thd) {
  ...

              if (!thd->m_main_security_ctx.host().length)  // If TCP/IP connection
  {
    ...
    if (acl_check_host(thd, thd->m_main_security_ctx.host().str,
                       main_sctx_ip.str)) { // 检查客户端IP是否允许连接到 MySQL 服务器
      /* HOST_CACHE stats updated by acl_check_host(). */
      my_error(ER_HOST_NOT_PRIVILEGED, MYF(0),
               thd->m_main_security_ctx.host_or_ip().str);
      return1;
    }
  } 
  ...
  auth_rc = acl_authenticate(thd, COM_CONNECT); // 这里验证用户密码是否正确


                              if (mysql_event_tracking_connection_notify(
          thd, AUDIT_EVENT(EVENT_TRACKING_CONNECTION_CONNECT))) { // 这里会调用 Connection Control 插件
    return1;
  }

  ...

                              return auth_rc;
}

首先检查客户端的 IP 是否被允许连接到 MySQL 服务器。如果 IP 地址不在允许的范围内,则提示ERROR 1130 (HY000): Host 'xxx' is not allowed to connect to this MySQL server 。

接着验证用户的密码是否正确。如果密码错误,则提示ERROR 1045 (28000): Access denied for user 'xxx'@'xxx' (using password: YES),错误在返回给客户端之前,会调用 Connection Control 插件,判断是否需要延迟连接。

这实际上提供了一个思路,如果担心延迟的连接占用过多的连接数,可以通过缩小用户账号的 host 范围,在密码验证之前拒绝不符合条件的连接,从而有效减少无效连接的占用。

此外,当 MySQL 实例可能暴露于公网时,切忌将 host 设置为%,否则数据库将面临较高的恶意攻击风险。

接下来我们看看 Connection Control 触发延迟的实现逻辑。

复制代码
bool Connection_delay_action::notify_event(
    MYSQL_THD thd, Connection_event_coordinator_services *coordinator,
    const mysql_event_connection *connection_event,
    Error_handler *error_handler) {
  DBUG_TRACE;

                    bool error = false;

                       constunsignedint subclass = connection_event->event_subclass;
  Connection_event_observer *self = this;

                            // 只处理连接建立和用户切换事件

                             if (subclass != MYSQL_AUDIT_CONNECTION_CONNECT &&
      subclass != MYSQL_AUDIT_CONNECTION_CHANGE_USER)
    return error;

RD_lock rd_lock(m_lock);

                              // 获取参数 connection_control_failed_connections_threshold 的值

                              const int64 threshold = this->get_threshold();


                              // 如果 connection_control_failed_connections_threshold 小于等于 0,则直接返回,相当于禁用了插件

                              if (threshold <= DISABLE_THRESHOLD) return error;

  int64 current_count = 0;

                              bool user_present = false;
  Sql_string userhost;

                              // 根据当前线程信息生成用户标识,格式为 user@host
  make_hash_key(thd, userhost);

  DBUG_PRINT("info", ("Connection control : Connection event lookup for: %s",
                      userhost.c_str()));


                              // 从哈希表中查找该用户的失败连接次数
  user_present = m_userhost_hash.match_entry(userhost, (void *)&current_count)
                     ? false
                     : true;

                              // 如果该用户的失败连接次数大于或等于阈值,则触发延迟

                              if (current_count >= threshold || current_count < 0) {
    // 根据失败次数计算延迟时间
    const ulonglong wait_time = get_wait_time((current_count + 1) - threshold);
    // 触发状态变量自增(Connection_control_delay_generated)
    if ((error = coordinator->notify_status_var(
             &self, STAT_CONNECTION_DELAY_TRIGGERED, ACTION_INC))) {
      error_handler->handle_error(
          ER_CONN_CONTROL_STAT_CONN_DELAY_TRIGGERED_UPDATE_FAILED);
    }
    // 执行延迟等待
    rd_lock.unlock();
    conditional_wait(thd, wait_time);
    rd_lock.lock();

    DBUG_EXECUTE_IF("delay_after_connection_delay", sleep(2););
  }


                              if (connection_event->status) {
    // 连接失败,更新哈希表中的失败计数
    if (m_userhost_hash.create_or_update_entry(userhost)) {
      error_handler->handle_error(
          ER_CONN_CONTROL_FAILED_TO_UPDATE_CONN_DELAY_HASH, userhost.c_str());
      error = true;
    }
  } else {
    // 连接成功,从 m_userhost_hash 表中删除该用户对应的记录
    if (user_present) {
      (void)m_userhost_hash.remove_entry(userhost);
    }
  }


                              return error;
}

这个函数中需要注意的地方有两点:

  1. 延迟时间。

    延迟时间 = (当前失败次数 + 1 - 阈值)秒,失败次数越多,延迟时间越长。

    延迟时间受到最小值和最大值 (connection_control_min_connection_delay 和 connection_control_max_connection_delay) 的限制。

  2. USERHOST 的构造规则。

    如果连接的用户名在mysql.user表中存在,则 USERHOST 的 host 部分取自于mysql.user表中该用户的host字段值。

    如果用户名不存在,则 host 部分会使用客户端的 IP 地址。

看下面这个示例。

复制代码
mysql> select user,host from mysql.user whereuser='root'and host='%';
+------+------+
| user | host |
+------+------+
| root | %    |
+------+------+
1 row in set (0.00 sec)


                          # 用户 root 在 mysql.user 表中存在,且 host 为 %,所以生成的 USERHOST 是 'root'@'%'。

                           # mysql -h10.0.0.108 -uroot -p123
mysql: [Warning] Using a passwordon the command line interface can be insecure.

                              ERROR1045 (28000): Access denied foruser'root'@'10.0.0.75' (usingpassword: YES)

mysql> select * from information_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS;
+------------+-----------------+
| USERHOST   | FAILED_ATTEMPTS |
+------------+-----------------+
| 'root'@'%' |               1 |
+------------+-----------------+
1 row in set (0.00 sec)


                              # 用户 root1 在 mysql.user 表中不存在,所以生成的 USERHOST 是 'root1'@'客户端IP'。

                              # mysql -h10.0.0.108 -uroot1 -p123
mysql: [Warning] Using a passwordon the command line interface can be insecure.

                              ERROR1045 (28000): Access denied foruser'root1'@'10.0.0.75' (usingpassword: YES)

mysql> select * from information_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS;
+---------------------+-----------------+
| USERHOST            | FAILED_ATTEMPTS |
+---------------------+-----------------+
| 'root1'@'10.0.0.75' |               1 |
+---------------------+-----------------+
1 row in set (0.00 sec)

参考资料

  1. https://dev.mysql.com/doc/refman/8.4/en/connection-control-plugin-installation.html
  2. https://dev.mysql.com/doc/refman/9.2/en/connection-control-component.html
  3. https://dev.mysql.com/blog-archive/the-connection_control-plugin-keeping-brute-force-attack-in-check/
相关推荐
Fleshy数模1 天前
CentOS7 安装配置 MySQL5.7 完整教程(本地虚拟机学习版)
linux·mysql·centos
az44yao1 天前
mysql 创建事件 每天17点执行一个存储过程
mysql
秦老师Q1 天前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
橘子131 天前
MySQL用户管理(十三)
数据库·mysql
Dxy12393102161 天前
MySQL如何加唯一索引
android·数据库·mysql
我真的是大笨蛋1 天前
深度解析InnoDB如何保障Buffer与磁盘数据一致性
java·数据库·sql·mysql·性能优化
怣501 天前
MySQL数据检索入门:从零开始学SELECT查询
数据库·mysql
人道领域1 天前
javaWeb从入门到进阶(SpringBoot事务管理及AOP)
java·数据库·mysql
千寻技术帮1 天前
10404_基于Web的校园网络安全防御系统
网络·mysql·安全·web安全·springboot
spencer_tseng1 天前
MySQL table backup
mysql