1. 簡介

Redis 是一套開放原始碼的記憶體內資料結構儲存系統,長期以來透過整合的 Lua 腳本環境提供擴充性。雖然這項功能藉由允許原子操作來提升效能,但也帶來了記憶體管理與狀態同步方面顯著的攻擊面。這份報告分析了 CVE-2026-23631 ,也稱為 DarkReplica ,這是在 Redis 複製子系統中發現的一個嚴重權限驗證後 Use-After-Free (UAF) 漏洞。該漏洞源於 Redis 在 Lua 函數執行期間處理同步事件時的一個邏輯缺陷,導致 Lua 解譯器狀態被過早摧毀。

一個 Lua UAF 漏洞如何毀掉 Redis?揭開 DarkReplica 的致命複製攻擊! | 資訊安全新聞

2. 背景:Redis Lua 引擎與執行模型

現代 Redis 版本維護兩個不同的 Lua 執行環境:傳統的腳本引擎 (EVAL/SCRIPT LOAD) 以及 7.0 版引入的新函數引擎 (FUNCTION LOAD/FCALL)。函數引擎的設計目的在提供更好的持久性與叢集同步。然而,由於 Redis 運行在單執行緒的事件迴圈上,任何長時間執行的 Lua 腳本都可能阻塞整個伺服器。為了解決此問題,Redis 實作了一套 「Slow Script」檢測機制

當腳本執行時間超過設定的閾值 (預設為 5 秒) 時,Redis 會觸發一個逾時旗標。為了讓伺服器保持足夠的反應能力來處理 FUNCTION KILL SHUTDOWN 指令,Redis 利用 Lua hooks ( lua_sethook ) 透過 processEventsWhileBlocked() 函數定期將控制權交還給伺服器的事件迴圈 [1] 。這樣的設計讓伺服器即使在腳本技術上「blocking」主執行緒時,仍能處理 I/O 事件。

3. DarkReplica 邏輯缺陷的技術分析

DarkReplica 漏洞的核心在於 processEventsWhileBlocked() 機制與 Redis 複製協定之間的互動。在正常情況下,當伺服器「busy」執行腳本時, processCommand() 函數會拒絕來自一般用戶端的大多數指令,以防止狀態損壞。然而,對於複製架構中來自主要伺服器 (Master) 的指令,此保護機制則會被繞過。

當 Redis 執行個體使用 SLAVEOF 指令設定為複本 (Slave) 時,它會與主要伺服器建立一個持久連線。如果主要伺服器發起 FULLRESYNC ,複本就必須清除其現有資料並載入一個新的 RDB (Redis Database) 檔案。關鍵在於,如果這個 FULLRESYNC 發生在一個 Lua 函數於 processEventsWhileBlocked() 中被暫停的時候,伺服器會繼續清除全域函數環境,包括 lua_State 物件本身,而被暫停的函數仍然持有一個指向它的 Pointer。

這種行為與其他 Lua 相關的漏洞 (例如 CVE-2025-49844 ) 形成對比,CVE-2025-49844 涉及 Lua 解析器內因垃圾回收對 TString 物件保護不足所導致的 UAF [2] 。CVE-2025-49844 是解析過程中低階的記憶體管理錯誤,而 CVE-2026-23631 則是更高階的架構缺陷,其中整個解譯器的生命週期並未與複製狀態機同步。

4. 系統架構與漏洞流程

下方的循序圖說明了 DarkReplica 攻擊的生命週期,突顯了腳本執行與複製同步之間的 Race Condition。

sequenceDiagram participant Attacker participant Slave as Redis Slave (Victim) participant Master as Attacker Master participant Lua as Lua Engine Attacker->>Slave: FUNCTION LOAD (Register Slow Script) Attacker->>Slave: SLAVEOF Master Attacker->>Slave: FCALL SlowScript Note over Slave,Lua: Script runs for > 5s Lua->>Slave: luaMaskCountHook() Slave->>Slave: processEventsWhileBlocked() Master->>Slave: FULLRESYNC (RDB with new functions) Slave->>Slave: functionsLibCtxClearCurrent() Note right of Slave: lua_State is freed here! Slave->>Lua: Resume Execution Note over Lua: Use-After-Free (Crash/RCE)

5. 程式碼分析

以下來自 Redis 原始碼的片段展示了有漏洞的路徑。

5.1. 中斷機制

Redis 使用一個 hook 從 Lua 引擎取回控制權。 scriptInterrupt 函數決定腳本是否應該被終止,或者伺服器是否應該處理事件。

  1. /*
  2. * Snippet 1: The Lua hook mechanism
  3. * This function is called every 100,000 Lua instructions.
  4. */
  5. int scriptInterrupt(scriptRunCtx *run_ctx) {
  6. if (run_ctx->flags & SCRIPT_TIMEDOUT) {
  7. /*
  8. * If the script has timed out, we enter this block.
  9. * processEventsWhileBlocked() is the danger zone where
  10. * replication events are handled.
  11. */
  12. processEventsWhileBlocked();
  13. /* Returns whether to kill the script or continue */
  14. return (run_ctx->flags & SCRIPT_KILLED) ? SCRIPT_KILL : SCRIPT_CONTINUE;
  15. }
  16. return SCRIPT_CONTINUE;
  17. }

5.2. 指令處理漏洞

processCommand 函數通常在腳本執行期間會阻擋指令,但複製流量會繞過這些檢查。

  1. /*
  2. * Snippet 2: Logic in processCommand
  3. * Regular clients are blocked, but master-replica traffic is not.
  4. */
  5. int processCommand(client *c) {
  6. // ...
  7. /* Check if the server is busy with a script */
  8. if (isInsideYieldingLongCommand() && !(c->cmd->flags & CMD_ALLOW_BUSY)) {
  9. /*
  10. * Rejects commands from normal clients.
  11. * However, commands from the Master do not always
  12. * trigger this rejection, especially during RDB loading.
  13. */
  14. rejectCommand(c, shared.slowscripterr);
  15. return C_OK;
  16. }
  17. // ...
  18. }

5.3. 複製環境清除

當收到 FULLRESYNC 時,從屬伺服器會呼叫 functionsLibCtxClearCurrent 來為新資料做準備。

  1. /*
  2. * Snippet 3: Clearing the function context
  3. * This is the root of the UAF.
  4. */
  5. void functionsLibCtxClearCurrent(int async) {
  6. if (async) {
  7. /* Handle asynchronous clearing */
  8. functionsLibCtx *old_l_ctx = curr_functions_lib_ctx;
  9. dict *old_engines = engines;
  10. freeFunctionsAsync(old_l_ctx, old_engines);
  11. } else {
  12. /*
  13. * Synchronous clearing: Immediately frees the engines dict.
  14. * This includes the lua_State currently executing the script.
  15. */
  16. functionsLibCtxFree(curr_functions_lib_ctx);
  17. dictRelease(engines); // <--- CRITICAL: lua_State is destroyed here
  18. }
  19. functionsInit(); // Re-initializes a fresh context
  20. }

5.4. 深入了解環境銷毀

functionsLibCtxFree 函數會遞迴清理所有相關資源。

  1. /*
  2. * Snippet 4: Freeing the function library context
  3. * Detailed cleanup of libraries and engines.
  4. */
  5. void functionsLibCtxFree(functionsLibCtx *functions_lib_ctx) {
  6. /* Clear all registered functions in the library */
  7. functionsLibCtxClear(functions_lib_ctx);
  8. /* Release the dictionaries holding function and library metadata */
  9. dictRelease(functions_lib_ctx->functions);
  10. dictRelease(functions_lib_ctx->libraries);
  11. /* Release engine statistics */
  12. dictRelease(functions_lib_ctx->engines_stats);
  13. /* Finally, free the context structure itself */
  14. zfree(functions_lib_ctx);
  15. }

5.5. 從 RDB 載入新函數

FULLRESYNC 的 RDB 載入階段,新函數會被註冊,但在載入完成前它們會使用一個不同的環境。

  1. /*
  2. * Snippet 5: RDB Function Loading
  3. * Part of the rdbLoad process.
  4. */
  5. if (type == RDB_OPCODE_FUNCTION2) {
  6. sds err = NULL;
  7. /*
  8. * Loads the function library from the RDB file.
  9. * If this happens while a script is yielding, the old
  10. * engine state has already been marked for destruction.
  11. */
  12. if (rdbFunctionLoad(rdb, rdbver, rdb_loading_ctx->functions_lib_ctx, rdbflags, &err) != C_OK) {
  13. serverLog(LL_WARNING,"Failed loading library, %s", err);
  14. sdsfree(err);
  15. goto eoferr;
  16. }
  17. continue;
  18. }

6. 攻擊情境

要觸發此漏洞,攻擊者必須擁有 Redis 執行個體的驗證存取權限。攻擊遵循以下步驟:

  1. 準備: 攻擊者註冊一個包含無限迴圈或非常長執行路徑的 Lua 函數。
  2. 設定: 攻擊者使用 CONFIG SET slave-read-only no 確保即使伺服器進入複本狀態, FCALL 仍然可以被執行。
  3. 執行: 攻擊者透過 FCALL 執行這個慢速函數。
  4. 觸發複製: 當函數正在執行並進入逾時狀態 (觸發 processEventsWhileBlocked ) 時,攻擊者使用 SLAVEOF 指令將受害者指向一個由攻擊者控制的主要伺服器。
  5. Payload 傳遞: 攻擊者的主要伺服器發送一個 FULLRESYNC 回應,其中包含一個特製的 RDB 檔案。受害者的伺服器在處於「running」Lua 腳本的暫停狀態時,會處理該複製事件並銷毀使用中的 Lua 引擎。
  6. UAF 執行: 一旦複製事件處理完畢, processEventsWhileBlocked 函數就會回傳,而 Lua 引擎會嘗試使用一個已被釋放的 lua_State Pointer 來恢復原始腳本的執行,導致任意程式碼執行。

7. 與先前 Lua 漏洞的比較

將 CVE-2026-23631 與較早的漏洞如 CVE-2025-46817 ( unpack() 中的整數溢位) [2] 進行比較是很有啟發性的。在 unpack() 漏洞的情況中,缺陷是缺乏對索引的邊界檢查,導致堆疊損壞。然而,DarkReplica 代表了一個更複雜的 「State Machine」漏洞 。它不是由單一有問題的程式碼行所造成,而是兩個複雜功能(讓出 Lua 執行權與完整複製重新同步)之間未能預見的互動所導致的。

8. 結論與修復建議

CVE-2026-23631 突顯了在單執行緒架構中進行多狀態管理的風險。一個「blocking」的腳本能有效地停止所有狀態變更操作的假設,已被複製子系統的優先權所推翻。Redis 已透過確保在腳本讓出執行權時無法清除函數環境來解決這個問題。

強烈建議使用者升級到以下已修復的版本:Redis 7.2.14、7.4.9、8.2.6、8.4.3 或 8.6.3。此外,一個安全最佳實務是透過 Redis ACL 限制 SLAVEOF CONFIG 指令的使用,以防止已驗證但非特權的使用者重新設定伺服器的複製狀態。