前言
在当今数字化飞速发展的时代,企业越来越依赖复杂的云服务平台来推动业务创新与增长。然而,这些平台的广泛使用也伴随着潜在的安全风险,尤其是在处理敏感数据时。近期,关于ServiceNow的一系列关键漏洞(CVE-2024-4879、CVE-2024-5178 和 CVE-2024-5217)的披露,再次引发了业界对网络安全的关注。
这些漏洞不仅可能使攻击者轻易访问敏感信息,还能让他们完全控制整个数据库。在这个始终在线的环境中,安全研究人员和企业 IT 团队必须时刻保持警惕,确保系统的防护能够跟得上不断演变的威胁。
本文将深入探讨这三项漏洞的技术细节及其潜在影响,揭示如何通过了解并应对这些风险,以保护企业数据的完整性和安全。无论是安全专家、开发者,还是对信息安全感兴趣的普通读者,本文将为提供丰富的见解、实用参考的建议,以及对未来网络安全态势的深刻洞察。让我们一起揭开这些安全隐患的面纱,探索如何在数字化转型的浪潮中,维护企业的安全堡垒。
ServiceNow介绍
ServiceNow 是一个庞大的 Java 单体应用,其单个 .jar 文件大小竟超过了 20GB。其设计宗旨在于实现高度的可定制性,许多配置选择都通过数据库完成。这与典型的 Java 应用程序形成鲜明对比,后者通常在 web.xml 文件中注册多个 servlet,并将端点硬编码于程序内。而在 ServiceNow 的世界中,实例会咨询一组数据库表来决定大多数请求的路由位置。
为了更好地理解 ServiceNow,需要深入探讨几个核心概念:
-
表
ServiceNow 的基本构建块是表。几乎所有的数据都被存储在表中,其中包括用户信息 (sys_users)、页面 (sys_pages) 和配置信息 (sys_properties)。这些表与底层数据库之间呈现出 1:1 的映射关系。例如,数据库中存在一个名为 sys_users 的表。
ServiceNow 为用户提供了便捷的数据库更新机制,只需在 URL 中输入特定的路径即可实现。例如,若想查看用户列表,可以直接访问 /sys_users_list.do;而若需创建新用户,则可浏览至 /sys_users.do。当然,允许任何用户随意修改所有内容显然非常不安全,因此 ServiceNow 构建了一个复杂的访问控制列表 (ACL) 系统来限制访问权限,用户可以被授权访问整个表、单个行或甚至特定字段。
-
处理器
另一种处理请求的方式是通过处理器。这些处理器可以视为最接近 API 端点的组件。ServiceNow 提供了基于 Rhino 的 JavaScript 引擎,使得用户在设计自定义端点时能够拥有更大的灵活性。同时,他们还提供了一系列主要使用 Java 编写的辅助类,使几乎可以配置平台的任意部分。
以下是一个随机抽取的示例处理器,以便了解其实现方式:
redirectBasedOnTheDevice();
function redirectBasedOnTheDevice() {
var userId = g_request.getParameter("sysparm_id");
var requestId = g_request.getParameter("sysparm_request_id");
var token = g_request.getParameter("sysparm_token");
var redirectUrl = g_request.getParameter("sysparm_redirect_url");
gs.getSession().putProperty('pwd_redirect_url', redirectUrl);
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);
if (GlideMobileExtensions.getDeviceType() == 'm' || GlideMobileExtensions.getDeviceType() == 'mobile') {
gs.debug("从移动设备发送的密码重置请求。将 URL 更改为移动兼容");
resetPasswordURL = this.getInstanceURL() + '/$pwd_new.do?sysparm_id=' + userId + '&sysparm_request_id=' + requestId + '&sysparm_nostack=true&sysparm_token=' + token;
}
g_response.sendRedirect(resetPasswordURL);
}
在大多数实例都托管于共享租户设置中,ServiceNow 提供了多个层次的沙盒机制,以确保底层机器的安全。JavaScript 的执行被沙盒化,任何涉及底层文件系统的辅助类都受到严格控制,仅限于允许的目录。此外,Java 的 SecurityManager 作为最后一道防线,以防止访问和写入超出租户目录的内容。
即使在这些保护措施下,未经授权的 JavaScript 执行在 ServiceNow 服务上仍可能引发严重的安全问题。
-
UI 页面
最常见的请求类型往往会过滤到 UI 页面。UI 页面是基于 Apache Jelly 库构建,使用 XML 模板来渲染内容。以下是一个简单的 UI 页面示例:
<?xml version="1.0" encoding="utf-8" ?><j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<g:evaluate var="jvar_product_name">
gs.getProperty('glide.product.name')
</g:evaluate>
<div style="font-size: 36px">Hello from ${jvar_product_name}, your query param is ${sysparm_foo}</div>
<g2:evaluate var="jvar_time">
new GlideDate().getByFormat("HH:mm:ss");
</g2:evaluate>
<div style="font-size: 36px">The time is $[jvar_time]</div></j:jelly>
如果保存该模板为名为 test 的 UI 页面并访问 /test.do?sysparm_foo=abc,则可以看到相应的输出。这个简单示例体现了几个重要的特点:
- Jelly 模板可以执行 JavaScript,与处理器类似。这是通过 ServiceNow 提供的自定义标签 g:evaluate 和 g2:evaluate 来实现的。
- Jelly 还有自己独特的模板表达式语言,称为 JEXL,采用 ${…} 或 $[…] 的格式进行表示,并以类似于 JavaScript 的方式进行沙盒化。
- 查询字符串中的 URL 参数会自动作为变量传递给模板。
- Jelly 模板默认对字符串进行转义,以防止跨站脚本攻击 (XSS),因而通过传递 sysparm_foo= 是安全的。
- 若要禁用转义,必须手动使用 no_escape 标签。
UI 页面存储在两个地方:一个是存放基于 ‘base’ 的 UI 页面模板,这些模板位于本地文件系统中的 ui.jforms/ 文件夹下;另一个则是在 sys_ui_pages 表中,可在此添加任何所需的页面。
问题迹象
对于特别敏锐的读者而言,可能会对为什么存在两种不同的前缀(g: 和 g2:)以及两种不同的模板表达式语法(${} 和 $[])感到疑惑。答案在于 UI 渲染过程分为两个阶段。整体流程大致如下:
首先,ServiceNow 会渲染模板,处理 g: 和 j: 标签,同时忽略 g2: 和 j2: 标签。在这一阶段,它使用 ${} 作为表达式的分隔符。这种过程被称为“第一阶段”,在此阶段,任何用户提供的值都将被直接插入到模板中。
接着,ServiceNow 会再次评估模板,这次它会处理 g2: 和 j2: 标签,并使用 $[] 作为模板的分隔符,这一过程称为“第二阶段”。
这种双重评估的结构意味着,在第一阶段中所发生的任何内容注入都可能引发模板注入的问题。从根本上来说,这一设计潜藏风险,因为某些出口可能会导致模板注入,而这种情况并不总是显而易见。
当然,开发人员在设计时考虑到了这一点。对于最明显的注入向量,已经实施了多种缓解措施——例如,用户输入中的 $[ 和 ${ 以及 <j2: 和 <g2: 都会被转义,以防止恶意注入。
我们将在后面的讨论中更加详细地介绍这些缓解策略。不过,值得注意的是,如果我们能够在身份验证之前的 UI 中找到一个允许我们在第一阶段注入 XML 标签的地方,那么就有可能实现模板注入。
漏洞1: 标题注入
在 ServiceNow 中,可以在身份验证之前访问的页面和处理器的列表存储在 sys_public 表中。考虑到模板的双重评估原理,我们开始探查可能导致第一阶段标签注入的位置。
最明显的情况出现在 <g:no_escape> 标签的使用上,因此我们开始利用 grep 工具查找该标签。我们很快在文件系统中的 ui.jtemplates/html_page_title.xml 找到了这个模板,它作为每个页面头部的一部分被包含:
<g:evaluate var="jvar_page_title" jelly="true">
var pageTitle = jelly.jvar_page_title;
if (JSUtil.nil(pageTitle)) {
var productName = gs.getProperty('glide.product.name', 'ServiceNow'),
description = gs.getProperty('glide.product.description');
if (gs.getProperty('glide.ui.title.use_product_name_only', 'false') == 'true')
pageTitle = productName;
else
pageTitle = productName + ' ' + description;
}
SNC.GlideHTMLSanitizer.sanitizeWithConfig('HTMLSanitizerConfig', pageTitle);
</g:evaluate>
<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=aaa。令我们惊讶的是,页面标题实际上被成功注入了!这似乎完全不是开发人员的本意。
潜在问题
这种情况表明,在处理用户输入时存在一定的风险。由于可以直接通过 URL 参数注入 HTML 标签,攻击者可能会利用此漏洞进行跨站脚本攻击 (XSS) 或其他注入攻击。对于 Web 应用程序,尤其是涉及用户输入的部分,必须对输入进行严格验证和清理,以避免潜在的安全问题。
但是
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)