如何有效防止MySQL Connection Control插件密码暴力破解?

摘要:Connection Control 是 MySQL 8.0 引入的一个安全功能插件,后移植到 MySQL 5.7.17 和 5.6.35 版本。 其核心功能是:当客户端因账号或密码错误连续多次登录失败时,服务端会对该客户端的后续请求进行延
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>showprocesslist; +------+-----------------+-----------------+------+---------+--------+--------------------------------------+------------------+ | 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 |showprocesslist| |1187| root |10.0.0.75:40920|NULL|Connect| 3| Waitinginconnection_controlplugin|NULL | +------+-----------------+-----------------+------+---------+--------+--------------------------------------+------------------+ 3rowsinset,1warning(0.00sec) 接下来我们看看该插件的使用方法、相关参数、禁用方法、注意事项以及实现细节。 使用方法 安装插件 INSTALLPLUGINCONNECTION_CONTROLSONAME'connection_control.so'; INSTALLPLUGINCONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTSSONAME'connection_control.so'; 其中,connection_control 是核心功能插件,connection_control_failed_login_attempts 提供记录客户端失败尝试次数的information_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS表。 卸载插件 UNINSTALLPLUGINCONNECTION_CONTROL; UNINSTALLPLUGINCONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS; 从 MySQL 9.2 开始,引入了 Connection Control component 来替代 Connection Control plugin。 相关参数和状态变量 参数 mysql>showvariableslike'%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 inset(0.03sec) 其中, 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*frominformation_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 inset(0.00sec) 如何禁用延迟功能 以下两种方法都可以: 卸载插件。 将 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。 如果某个用户的失败次数过多,但希望在输入正确密码时不再等待,可以通过以下命令清空其延迟状态: SETGLOBALconnection_control_failed_connections_threshold =DEFAULT; 3. 被延迟的连接依旧会占用连接数。 如果 connection_control_min_connection_delay 设置过大,且攻击端重试频率很高,可能导致连接数被耗尽,影响正常用户的连接。 实现细节 以下是 Connection Control plugin 被调用的时机: staticintcheck_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; } ... returnauth_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 触发延迟的实现逻辑。 boolConnection_delay_action::notify_event( MYSQL_THD thd, Connection_event_coordinator_services *coordinator, constmysql_event_connection *connection_event, Error_handler *error_handler){ DBUG_TRACE; boolerror =false; constunsignedintsubclass = connection_event->event_subclass; Connection_event_observer *self =this; // 只处理连接建立和用户切换事件 if(subclass != MYSQL_AUDIT_CONNECTION_CONNECT && subclass != MYSQL_AUDIT_CONNECTION_CHANGE_USER) returnerror; RD_lockrd_lock(m_lock); // 获取参数 connection_control_failed_connections_threshold 的值 constint64 threshold =this->get_threshold(); // 如果 connection_control_failed_connections_threshold 小于等于 0,则直接返回,相当于禁用了插件 if(threshold <= DISABLE_THRESHOLD)returnerror; int64 current_count =0; booluser_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) { // 根据失败次数计算延迟时间 constulonglong 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); } } returnerror; } 这个函数中需要注意的地方有两点: 延迟时间。 延迟时间 =(当前失败次数 + 1 - 阈值)秒,失败次数越多,延迟时间越长。 延迟时间受到最小值和最大值 (connection_control_min_connection_delay 和 connection_control_max_connection_delay) 的限制。 USERHOST 的构造规则。 如果连接的用户名在mysql.user表中存在,则 USERHOST 的 host 部分取自于mysql.user表中该用户的host字段值。 如果用户名不存在,则 host 部分会使用客户端的 IP 地址。 看下面这个示例。 mysql>selectuser,hostfrommysql.userwhereuser='root'andhost='%'; +------+------+ | user | host | +------+------+ | root | % | +------+------+ 1 row inset(0.00sec) # 用户 root 在 mysql.user 表中存在,且 host 为 %,所以生成的 USERHOST 是 'root'@'%'。 # mysql -h10.0.0.108 -uroot -p123 mysql: [Warning]Usingapasswordonthe command lineinterfacecan be insecure. ERROR1045(28000):Accessdeniedforuser'root'@'10.0.0.75'(usingpassword: YES) mysql>select*frominformation_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS; +------------+-----------------+ | USERHOST | FAILED_ATTEMPTS | +------------+-----------------+ | 'root'@'%' | 1 | +------------+-----------------+ 1 row inset(0.00sec) # 用户 root1 在 mysql.user 表中不存在,所以生成的 USERHOST 是 'root1'@'客户端IP'。 # mysql -h10.0.0.108 -uroot1 -p123 mysql: [Warning]Usingapasswordonthe command lineinterfacecan be insecure. ERROR1045(28000):Accessdeniedforuser'root1'@'10.0.0.75'(usingpassword: YES) mysql>select*frominformation_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS; +---------------------+-----------------+ | USERHOST | FAILED_ATTEMPTS | +---------------------+-----------------+ | 'root1'@'10.0.0.75' | 1 | +---------------------+-----------------+ 1 row inset(0.00sec) 参考资料 https://dev.mysql.com/doc/refman/8.4/en/connection-control-plugin-installation.html https://dev.mysql.com/doc/refman/9.2/en/connection-control-component.html https://dev.mysql.com/blog-archive/the-connection_control-plugin-keeping-brute-force-attack-in-check/