1. 簡介

導致遠端程式碼執行的預驗證鏈結很少源自單一的記憶體不安全操作(memory-unsafe primitive);在現代網頁應用程式核心中,它們更常從 parser 差異出現—即單一由攻擊者控制的值,被兩個獨立元件在不同語法下解讀 [1] 。報告檢視一個流行開源內容管理系統(open-source content management system)主題安裝(theme-installation)工作流程中的此類差異,漏洞被揭露並命名為 Click2Shell ,其中透過 GET 參數提供的主題 slug 由伺服器端目錄 API 進行標準化,但同時未經跳脫地重複用作管理員瀏覽器內的 CSS selector 語法 [1] 。產生的 selector injection 讓未經驗證的攻擊者能夠強制已登入的管理員 session 安裝官方但由攻擊者選擇的目錄主題(catalog theme),無需點擊任何安裝或啟用控制項。由於主題程式碼並非僅在成為「作用中」主題時才受限於執行,已安裝的套件暴露了第二個獨立介面:PHP 可透過預覽機制在主題啟用前存取 [1] 。將這兩個操作(primitive)鏈結起來,可從單一造訪的連結產生未經驗證的 遠端程式碼執行 。

選擇器注入是什麼?Click2Shell 教你一次看懂 nonce 的重要性! | 資訊安全新聞

2. 透過 Parser 差異的 Selector Injection

安裝程式路由 /wp-admin/theme-install.php?theme=THEME_SLUG 將 THEME_SLUG 轉發至遠端目錄查詢,並且獨立地在該查詢解析後,將相同的原始字串內嵌至 jQuery attribute selector 中 [1] 。由於目錄的 slug 標準化會在伺服器端查找執行前剝除標點符號,例如 twentytwenty"]>*>*>*/* 的值在伺服器端會被簡化為合法 slug twentytwenty ,而瀏覽器端的 selector 字串則保留每個注入的字元 [1] 。這是 source/sink mismatch 的直接 instance:URL 參數是攻擊者控制的 source,而 jQuery 的 $() function 是一個記錄在案的危险 sink,一旦未跳脫的字串到達該處—這類 sink 最常在 location.hash 的環境中被討論,但不限於此 [2] 。

  1. // --- Code 1: vulnerable selector construction (wp-admin/js/theme.js) ---
  2. request.theme = slug; // (1) raw, URL-derived slug is attached to the catalog query request object, unmodified
  3. self.view.collection.query( request ); // (2) the request is sent to the WordPress.org Themes API for server-side resolution
  4. self.view.collection.once( 'query:success', function() {
  5. // (3) callback fires once the catalog query resolves successfully (i.e. a real theme record was found)
  6. $( 'div[data-slug="' + slug + '"]' ).trigger( 'click' );
  7. // (4) the ORIGINAL unescaped "slug" variable (not the canonicalized API result) is concatenated
  8. // directly into a jQuery attribute-selector string
  9. // (5) .trigger('click') programmatically fires a click on whatever the resulting selector matches
  10. });

Code 1 — slug 的兩個消費者意見不一:目錄查詢將其標準化,selector 則否。

攻擊者提供的引號提前關閉了 data-slug attribute selector;後續的 child combinators >*>*>* 會從匹配到的主題卡片一路延伸到其巢狀的動作控制元件,而結尾的 /* 則被視為 CSS 註解的開頭,用來屏蔽 WordPress 在注入值之後附加的任何字串 [1] 。因此,selector 不再匹配單一的、無害的 <div> ;它到達其中嵌套的真實 Install 控制項,且 .trigger('click') 會觸發該控制項,完全如同管理員點擊了它一樣。攻擊者並未偽造任何 installation nonce 或 install_themes capability—受信任的、已繪製的管理頁面已持有兩者,而應用程式本身的腳本則代替攻擊者使用它們 [1] 。

  1. // --- Code 2: patched selector construction (post-fix) ---
  2. $( 'div.theme[data-slug="' + $.escapeSelector( slug ) + '"]' ).trigger( 'click' );
  3. // (1) the match is now constrained to an element that also carries the "theme" class,
  4. // not just any element that happens to expose a matching data-slug attribute
  5. // (2) slug is passed through jQuery's $.escapeSelector() before concatenation
  6. // (3) escapeSelector() converts CSS metacharacters (", >, /, etc.) into their literal,
  7. // non-structural escaped form, so they can no longer alter selector grammar
  8. // (4) .trigger('click') now only ever reaches the single intended theme-card element

Code 2 — 修復縮小了匹配目標,並在值進入 selector 語法的確切點進行跳脫。

Selector escaping 是針對接收 sink 特定語法的 contextual output encoding,而不是在網路邊界一次性應用的單一 sanitizer—這與一般其他 jQuery selector sinks 的文件記錄相同的 remediation pattern [2] 。

sequenceDiagram participant Atk as Attacker-crafted link participant Adm as Administrator browser (theme.js) participant API as Themes API (catalog) participant Core as Core admin page DOM Atk->>Adm: Administrator opens theme-install.php?theme=twentytwenty"]>*>*>*/* Adm->>API: Query catalog using the raw slug value API-->>Adm: Canonicalized record "twentytwenty" returned Note over Adm: Browser rebuilds the selector from the ORIGINAL unescaped slug, not the API's canonical value Adm->>Core: div[data-slug="twentytwenty"]>*>*>*/*"] .trigger('click') Note over Core: Injected combinators reach the genuine Install control Core-->>Core: Official theme ZIP fetched and installed (remains inactive)

Diagram 1 — 孤立狀態下的強制安裝操作。

3. 啟用前的執行介面

強制安裝本身並非程式碼執行:已安裝的套件保持非作用中狀態,且主題 PHP 通常預期僅在主題成為網站的作用中主題時才運行。一次 Customizer 即時預覽請求打破了該假設—當要求預覽某個主題時,會載入該主題的 functions.php ,以便 hooks 和 widgets 可以繪製,而這個載入過程不受資料庫仍記錄為啟用中的主題影響 [1] 。

  1. // --- Code 3: Customizer preview request (config / request line) ---
  2. /wp-admin/admin-ajax.php // (1) the core AJAX endpoint, reachable by any authenticated session
  3. ?wp_customize=on // (2) flag instructing Core to treat this request as a live Customizer preview
  4. &customize_theme=mobile-repair-zone
  5. // (3) names the INACTIVE theme whose functions.php should be loaded for preview
  6. // -- the database's "active theme" record is not changed by this request

Code 3 — 單一的 request line,在不啟用的情況下載入非作用中主題的 PHP。

到達該載入路徑讓任何目錄主題—透過上述原始機制強制載入的官方套件,或磁碟上已存在的第三方套件—有機會在管理員按下 Activate 之前註冊 hooks 和 handlers [1] 。

4. 未受保護的 AJAX Installer 提供執行能力

所檢視的特定目錄套件在其 functions.php 為預覽載入時,立即註冊了一個已驗證的 AJAX action:

  1. // --- Code 4: AJAX action registration (theme functions.php) ---
  2. add_action(
  3. 'wp_ajax_mobile_repair_zone_install_and_activate_plugin',
  4. // (1) hook name: fires for any logged-in user that posts action=mobile_repair_zone_install_and_activate_plugin
  5. 'mobile_repair_zone_install_and_activate_plugin'
  6. // (2) the callback function that will run when the hook fires -- registered the instant the theme's PHP loads
  7. );

Code 4 — handler 純粹透過載入主題的 PHP 註冊,而非透過啟用主題。

  1. // --- Code 5: vulnerable callback body (theme functions.php) ---
  2. $post_plugin_details = $_POST['plugin_details'];
  3. // (1) the entire plugin_details array is read directly from POST with no nonce field checked beforehand
  4. $plugin_text_domain = $post_plugin_details['plugin_text_domain'];
  5. // (2) attacker-chosen text domain used later to name the unpacked plugin directory
  6. $plugin_main_file = $post_plugin_details['plugin_main_file'];
  7. // (3) attacker-chosen filename identifying which file inside the archive is the plugin entry point
  8. $plugin_url = $post_plugin_details['plugin_url'];
  9. // (4) attacker-chosen URL the server will fetch, unpack, and later include as PHP -- no capability check precedes this

Code 5 — 三個攻擊者控制的 POST fields 驅動 fetch-unpack-include 序列,無授權檢查。

Handler 使用 plugin_url 下載一個壓縮檔,將回傳的位元組寫入 plugins 目錄下,解壓縮後,再載入由 plugin_main_file 所指定的 entry file [1] 。實際上,一旦其註冊主題的 PHP 僅被載入,它就是一個設計上未經驗證的 installer—與該主題是否作用中無關。針對這種模式的標準防禦措施,是在每個動作中使用一次性隨機值(nonce),並透過 check_ajax_referer() 驗證,再搭配明確的權限檢查;該平台的開發者文件將這種組合視為任何會改變狀態的 AJAX 處理程序的基本要求 [3] ,且針對 plugin 和主題作者的指導方針對每個執行敏感、已驗證 action 的 hook 重複相同的要求 [4] 。Code 5 所示的 callback 中不存在任何這些檢查。

sequenceDiagram participant Atk as Attacker page participant Adm as Administrator browser participant Core as Core (admin-ajax / theme-install) participant Thm as Theme PHP (installed, inactive) Atk->>Adm: Lure click on crafted theme-install.php link Adm->>Core: Selector-injection forced-install primitive (Diagram 1) Core-->>Thm: Official theme written to disk, still inactive Atk->>Adm: Auto-submitted Customizer preview request Adm->>Core: GET admin-ajax.php?wp_customize=on&customize_theme=... Core->>Thm: Load functions.php for preview (pre-activation) Note over Thm: Unprotected AJAX action registered -- no nonce, no capability check Atk->>Adm: Auto-submitted stage-two POST with attacker plugin_url Adm->>Thm: action=..._install_and_activate_plugin (Code 5 fields) Thm->>Thm: Fetch archive, unpack, include entry file Thm-->>Core: Attacker-supplied PHP executes under the server process

Diagram 2 — 從單一連結到程式碼執行的完整鏈結。

5. 討論

此攻擊鏈是同一研究團隊在大約一個月內揭露的第二條 pre-authentication 從核心到 RCE 的攻擊鏈;在此之前,他們曾披露一個不相關的反射型 XSS ,該漏洞發生於登入頁面,並透過 DOM 覆寫升級成應用程式認證偽造 [5] 。這兩條攻擊鏈在整體結構上相似 — —核心中的 pre-authentication 原始機制提供了攻擊者的觸達能力,而核心信任邊界之外獨立發現的另一個漏洞則提供了程式碼執行 — 但它們的 root causes 完全不同。先前的攻擊鏈依賴於 strip_tags() 與平台自身 HTML-sanitization filter 在 tag-like input 上的處理差異 [5] ,而此處分析的攻擊鏈依賴於遠端目錄 API 的 slug 標準化與未跳脫 jQuery selector 之間的處理差異,並透過 Customizer preview load 被引導至程式碼執行,而非經由 credential-issuance 流程。core-adjacent JavaScript 中 parser-差異 bugs 的反覆出現,結合獨立維護的主題和 plugin 套件生態系統(每個都可提供 second-stage execution primitive),表明任何作為預覽繪製副作用可達的 install-but-not-yet-active code path 都應接受與其他 Core surfaces 已應用的相同系統性 selector- 和 output-escaping review。

6. 結論

這條攻擊鏈的兩個部分單獨來看並不特別:一個是經過設計的連結,用來安裝未啟用的主題;另一個是主題套件在 AJAX handler 上省略 nonce 驗證,而這個處理程序原本不被認為在啟用前可被觸及。結合起來,它們能在管理員一次點擊後觸發未經驗證的遠端程式碼執行,攻擊者不需要帳號,且整個過程中網站的啟用 theme 沒有任何可見變化。這個結構上的教訓不僅限於這個 core/theme 的組合:任何因預覽或安裝工具的副作用而變得可觸及的程式路徑,都應被視為應用程式受信任的『啟用前執行面』的一部分,並且必須以與完整啟用程式碼相同的 nonce 與權限檢查來防護。