1. 簡介

Hash-based Message Authentication Codes (HMAC) 被廣泛用於保護兩個共享 secret key 的各方之間的無狀態交換,其安全保證建立在一個隱含假設上:雙方必須簽署並驗證完全相同的位元組序列 [1] 。當簽署的訊息是透過串接數個邏輯上獨立的欄位所建立時,該假設便默默依賴於這些欄位如何被分隔。若系統簽署的是欄位的一種正規化形式,卻驗證另一種正規化形式,則能夠影響欄位邊界的攻擊者可能產生兩個在密文上等價、卻對應到不同應用層意義的訊息,即使底層的 secret key 從未被揭露。此報告分析此類缺陷的真實案例,由 Wordfence 揭露為 CVE-2026-76581,這是 WPMU DEV Dashboard plugin(版本 ≤ 5.0.1)中的 authentication bypass,讓未經驗證的攻擊者能夠在啟用 Hub Single Sign-On (SSO) 的網站上取得管理員 session [1] [2]

HMAC保護的SSO真的安全嗎? WPMU的訊息歧義漏洞給出答案! | 資訊安全新聞

2. 系統概觀

WPMU DEV Dashboard 將自行託管的安裝連接到供應商的 Hub 服務,並提供以兩個 AJAX actions 實作的 SSO 登入路徑: wdpsso_step1 wdpsso_step2 。由於訪客在 SSO 開始時依定義尚未通過驗證,因此這兩個動作都被註冊為未驗證( nopriv )端點,並位於 WPMUDEV_Dashboard_Ajax 類別之中:

  1. private array $nopriv_actions
  2. = array(
  3. 'wdpunauth',
  4. 'wdpsso_step1',
  5. 'wdpsso_step2',
  6. );

程式片段 1 — 兩個 SSO 階段皆可在未先進行 WordPress authentication 的情況下存取,使整個信任決策完全落在每個步驟內部執行的 HMAC 檢查上 [1]

此設計在功能上是必要的,但這意味著兩個 endpoints 必須獨立重建並對「簽署了什麼」達成一致。Step 1( authenticate_sso_access_step1() )發出一個新的 token、設定 state cookie,並對四個值——token、hashed state、redirect target 與 site domain——進行無分隔符或長度前綴的串接後產生外送 signature:

  1. $token = uniqid() . '-' . microtime( true );
  2. WPMUDEV_Dashboard::$settings->set( 'active_token', $token, 'sso' );
  3. $hashed_pre_sso_state = hash_hmac( 'sha256', $pre_sso_state, $api_key );
  4. $domain = $this->network_site_url();
  5. $outgoing_hmac = hash_hmac( 'sha256', $token . $hashed_pre_sso_state . $redirect . $domain, $api_key );

程式片段 2 — Step 1 簽署 token || state || redirect || domain ,並將產生的 HMAC 連同除了 API key 以外的所有明文輸入交還給呼叫者 [1]

3. 正規化落差

此漏洞並非 HMAC-SHA256 本身的弱點,也非 API key 保密性的問題——缺陷完全在於 step 1 簽署的內容與 step 2 驗證的內容之間的不一致。 authenticate_sso_access_step2() 僅從三個欄位重建預期的 signature,並默默捨棄 domain:

  1. $token = $sso_access_data['token'] ?? '';
  2. $pre_sso_state = $sso_access_data['pre_sso_state'] ?? '';
  3. $redirect = $sso_access_data['redirect'] ?? '';
  4. $verifying_hmac = hash_hmac( 'sha256', $token . $pre_sso_state . $redirect, $api_key );
  5. $is_valid = hash_equals( $incoming_hmac, $verifying_hmac );

程式片段 3 — Step 2 驗證 token || state || redirect ,這是刪去 domain 欄位後的串接,省略了 程式片段 2 中驗證的 domain 欄位 [1]

由於無分隔符的字串串接並非一對一(injective),同一個輸出位元組字串可以由欄位值的多種指派方式產生。若攻擊者在 step 1 請求時使用空的 redirect,簽署的訊息會塌縮為 token || state || "" || domain ,這在位元組上與 token || state || domain 完全相同。當攻擊者稍後將 step 1 以明文回傳的 domain 值作為 redirect 參數(而非原本綁定的 redirect 欄位)送出時,該位元組字串正是 step 2 所期望的內容。因此 HMAC 在對其自身輸入的語意不同解釋下成功驗證,攻擊者卻從未得知 API key;step 1 實際上成為 step 2 訊息空間的 signing oracle [1]

sequenceDiagram participant A as Unauthenticated Attacker participant S1 as wdpsso_step1 participant S2 as wdpsso_step2 participant WP as WordPress Session Layer A->>S1: Request SSO start (redirect = empty) S1->>S1: hmac = HMAC(token || state || "" || domain, key) S1-->>A: token, hashed state, cookie, domain, hmac Note over A: Attacker moves "domain" value
into the redirect field A->>S2: Submit token, state, hmac, redirect = domain S2->>S2: verify = HMAC(token || state || redirect, key) Note over S2: verify == hmac (same bytes,
different field meaning) S2->>WP: wp_set_auth_cookie(mapped_userid) WP-->>A: Authenticated session (admin, if Hub-mapped)

圖 1 — 欄位轉移重放:step 1 回傳的 domain 值被重新送出為 step 2 的 redirect 值,因而在重新解釋的訊息上產生相同的 HMAC [1]

4. 為何輔助控制無法阻止攻擊

此 plugin 確實實作了 anti-replay 措施——綁定至 hashed 值的 state cookie,以及防止 token reuse 的 monotonic token-timestamp 檢查——但這兩項控制僅保護 token 與 state 欄位,而非歧義的 domain/redirect 邊界。由於 step 1 會向任何訪客發出這些值,並在同一回應中設定對應的 cookie,攻擊者使用完全新鮮、合法發出的材料即可滿足所有檢查;輔助邏輯中沒有任何部分能偵測到 redirect 欄位現在承載的是 domain 的位元組,而非實際的 redirect URL。

5. 修復

供應商的修復透過使兩個階段變為有狀態而非純粹計算式,消除了歧義性:step 1 產生的 HMAC 現在會在伺服器端持久化,而 step 2 會明確拒絕任何等於已儲存 step-1 值的 incoming signature,從而關閉兩個 endpoints 之間的 oracle 關係。

  1. // Step 1: persist the signature so step 2 can recognize a step-1 artifact.
  2. $outgoing_hmac = hash_hmac( 'sha256', $token . $hashed_pre_sso_state . $redirect . $domain, $api_key );
  3. WPMUDEV_Dashboard::$settings->set( 'step1_hmac', $outgoing_hmac, 'sso' );
  4. // Step 2: reject the request if the incoming signature is literally
  5. // the value step 1 produced — it cannot be a genuine Hub response.
  6. $step1_hmac = WPMUDEV_Dashboard::$settings->get( 'step1_hmac', 'sso', '' );
  7. if ( hash_equals( $step1_hmac, $incoming_hmac ) ) {
  8. wp_die( 'Invalid SSO authentication response.' );
  9. }

程式片段 4 — 此修補並未改變串接方案本身;它加入一個 out-of-band 檢查,使 step-1 產生的 HMAC 永遠無法被重放為 step-2 回應 [1]

這是務實的緩解措施,而非結構性修復:兩種訊息格式仍是無界的串接,仍可能彼此碰撞或與未來欄位碰撞,但阻止直接重放精確的 step-1 artifact,關閉了使利用成為可能的特定 oracle 關係。

6. 與先前發現的關聯

WPMU DEV Dashboard 先前曾釋出另一個獨立的 authentication-bypass 缺陷,於版本 5.0.1 修補,其中供應商遠端的 WDP-AUTH Hub handler 在未連線的安裝上接受以已知空 key 計算的偽造 signature [3] 。該缺陷與此處分析的缺陷僅在影響類別上相同;先前問題是不同程式碼路徑上的 key-management 失敗,而 CVE-2026-76581 之所以在第一個 bug 的修復後仍持續存在,正是因為它涉及已連線的網站、 wdpsso_* actions,以及正確保管的 secret key。同一 plugin 中兩個獨立程式碼路徑的重複發生,說明由數個未經驗證、鬆散耦合的 request handlers 組成的 authentication 邏輯難以全面推理,因為每個 handler 可能滿足其局部不變量,而 handlers 的組合卻不滿足。

7. 討論

底層弱點可推廣至此 plugin 之外:任何簽署 a || b || c 卻未使用固定長度編碼、長度前綴或 unambiguous 分隔符的 protocol,只要攻擊者能影響一個欄位的長度或內容並觀察或控制另一個欄位,就容易遭受 field-boundary shifting 攻擊。正規緩解措施包括為每個欄位加上長度前綴、使用保證不會出現在欄位內容中的分隔符,或先獨立雜湊每個欄位再組合 digests。同一邏輯操作的兩個驗證 routine 在欄位組成上出現分歧,也顯示出規格與程式碼審查的落差:若簽署與驗證共用單一正規的訊息建構函式,此差異在結構上便不可能發生,而僅會在安全稽核時被觀察到。

8. 結論

CVE-2026-76581 顯示,當簽署的訊息格式在每個建構或檢查它的各方之間未以正規且 unambiguous 的方式定義時,僅有 HMAC 正確性是不夠的。在 Hub SSO 對應至管理員的 WPMU DEV Dashboard 安裝上, wdpsso_step1 wdpsso_step2 之間四欄位對三欄位的不一致,讓未經驗證的攻擊者能夠取得完整的管理員 session,並且在存在如 plugin 或 theme 編輯器這類可寫入程式碼的介面時,進一步提升至 remote code execution。版本 5.0.2 透過將 step 2 綁定至 step 1 產生的特定 artifact,緩解了立即的重放路徑,但更廣泛的教訓——用於密碼學簽署的串接式訊息建構必須正規化一次並處處重用——遠超出此單一 plugin。