www.enumerated.ie Open in urlscan Pro
198.49.23.144  Public Scan

Submitted URL: https://t.co/P7fDLRkVsb
Effective URL: https://www.enumerated.ie/servicenow-data-exposure
Submission: On October 18 via manual from US — Scanned from DE

Form analysis 0 forms found in the DOM

Text Content

0
Skip to Content


Homepage
Enumerated

Open Menu Close Menu

Homepage
Enumerated

Open Menu Close Menu

Homepage



DATA EXPOSURE AND SERVICENOW: THE ELEPHANT IN THE ITSM ROOM

Oct 14
Written By Aaron Costello

This research is written and discovered by Aaron Costello (Twitter
@ConspiracyProof). Daniel Miessler has had absolutely no part in the research
nor this article. His sole link to the research is taking statements from this
very article and reposting them on Twitter. Please provide proper accreditation
if choosing to share.


BACKGROUND AND INTRODUCTION

The purpose of this tutorial is to share my knowledge of how a built-in
capability within ServiceNow could potentially be leveraged to extract data from
records as an unauthenticated user. This is extremely similar to my piece on
Salesforce Data Exposure, in that the vector being discussed in this article is
out-of-the-box (OOB) functionality that exists by default within all versions of
ServiceNow. The good news, I have been observing this vector since 2021 and have
yet to find evidence of exploitation in the wild. The bad news, it has likely
existed since 2015 which is the creation date of the component, and this long
predates the inception of my surveillance.

It is entirely improper for both myself, and any individuals reading this
article, to claim that this is a vulnerability or 0day. Because of the
open-source nature of the offending component, it is recorded quite clearly that
the underlying JS code which powers the component was modified by at least one
individual from ServiceNow on 3rd March 2023 to make it less dangerous.
Additionally, through this method one can only retrieve data that they are
explicitly authorised to see based on access controls.

Lastly, there are dangerously incorrect assumptions being made about the
security of large-scale SaaS platforms that have any concept of public access to
data. Whether the basis of these assumptions are from providing blind trust in
vendors, or SaaS practitioners rooted in the ego of their own expertise, or
something else, I don’t know. But it is dangerous, and it should stop. It is my
one and only ask of you all to approach SaaS products that have the capability
to make resources public with utmost skepticism and care. It is my knowledge,
not a guess, that near-identical vectors exist across other popular SaaS
applications, not only ServiceNow and Salesforce.




WHAT IS SERVICENOW?

ServiceNow, unlike some of it’s competitors, is both considered to be a
Platform-as-a-Service (PaaS) and Software-as-a-Service (SaaS). It is considered
PaaS because it provides a platform for creating custom applications, and SaaS
because it offers a suite of ready-to-use software applications accessible over
the internet ‘as a service’. As multi-faceted as it is, there are clear popular
use cases.

 * IT Service Management (ITSM): The core use-case is ITSM, where it helps
   organizations manage and automate IT services, incidents, problems, changes,
   and service requests.

 * IT Operations Management (ITOM): Managing and optimizing IT infrastructure
   and operations, including network, cloud, and server management.

 * IT Asset Management (ITAM): Organizations can efficiently track and manage
   their IT assets, ensuring compliance and cost optimization.

 * Service Desk and Customer Support: It serves as a platform for managing
   service desks and customer support, enabling efficient incident resolution
   and support ticket handling.

 * Employee Self-Service: The self-service portal empowers employees to request
   services, access information, and solve common issues without the need for
   direct IT support.

These are just to name a few. Knowing these use-cases is important, as they will
give you an idea as to the kind of data that ServiceNow is typically used to
store and process. They are just the tip of the iceberg when it comes to a
platform as large as this, and if you cannot tell already, a potential gold-mine
of data for hackers.


TECHNICAL ANALYSIS

ServiceNow’s widgets are an incredibly powerful but often overlooked component
of the base platform, serving as an API for the Service Portal. They are
open-source, written in JS, and are equally as powerful as Scripted REST APIs.
It’s likely that their perceived limitation as small components of the Service
Portal has allowed them to largely go unnoticed as a potential concern. Even
more so, their access control is not governed by ACLs. As a result cloud
practitioners who are searching for exposed endpoints will always miss them when
performing routine checks for public ACLs on non-record components. Instead,
their access control is dictated by fields on the individual widget record
itself.

It’s important to note that I have yet to discover any ServiceNow documentation
that mentions the fact they have placed a public endpoint for possible data
disclosure as an OOB component of the core platform, even though, as you will
shortly see, they are aware of it and its potential for security risks.

The widget in scope for this particular piece is known plainly as Simple List.
It can be found by navigating to Service Portal > Widgets. It has a UUID
(sys_id) of 5b255672cb03020000f8d856634c9c28. Its function is simple, to return
record data that is readable by the caller when provided a table and field as
input. If you find the widget’s record, and open it, you will see the below:

Simple List with default RBAC settings

The first thing that should catch your eye is that it’s set to Public without
any roles defined. This is by default.

Within this record is also the Server Script and Client Script. The purpose of
the Server Script is to provide backend functionality, and this is normally
called by the Client Script which is what our browsers interact with. However,
in the case of this widget, the Server Script reads directly from request
parameters so it may be queried directly.

Before I dig in to the meat of the Server Script, you will be seeing small
snippets of very human-readable JS code. So yes, I am aware that this portion of
the article is very discriminatory against those on LinkedIn with ‘PhD x CISSP x
Threat Modelling Guru x SBOM empath’ in their LinkedIn titles, but ChatGPT can
explain it for them. Anyway, let me show you what are the most relevant pieces
of the Server Script code.

LINE 7: options.table = $sp.getParameter('t') || options.table;

Takes in the value of GET parameter t using $sp.getParameter, a member of the
GlideSPScriptable class. A really interesting thing here is the alternative
value if the GET parameter t is not provided. In most cases widgets do not
accept input directly from a HTTP request. Instead data is passed to widgets via
the Client Script or Widget Instances through the options global variable as you
see above. This knowledge may come in handy for you later.

LINE 11: if (!gs.getSession().isLoggedIn() && !new SNCACLUtil().hasPublicAccess(options.table)) {
LINE 12:		gs.warn("Deny access to table which is not public: " + options.table);
LINE 13:		data.isValid = false;
LINE 14:		return;
LINE 15:	}

Remember I mentioned that ServiceNow made an addition to this code on 3rd March
2023? This was it. In effect, it checks if the table explicitly has the public
role. If it does not, access is denied. Within ServiceNow, resources that rely
on ACLs for access control can cause a resource to be public through several
ways. We know that one must satisfy the Role, Condition, and Scripted parts of
an ACL. If public is not defined as a role on the ACL, an unauthenticated user
might still pass the condition or scripted parts and thus be granted access.
Even more likely is the ACL is entirely empty of a defined Role, Condition, or
Script; allowing an unauthenticated user access to the resource.

Taking the above statement into account, can you find an OOB ACLs that prior to
the date of this addition, were exposing sensitive PII information? If you
can’t, I’ll be telling you later on in the article.

LINE 17: var gr = new GlideRecordSecure(options.table); // does ACL checking for us

Above we are priming for an permission-respecting call to the DB using the table
provided from the earlier GET request, which is really what shifts the majority
of data exposures through this widget from a vulnerability to a
misconfiguration.

LINE 30: options.display_field = $sp.getParameter('f') || options.display_field;
LINE 31: if (!options.display_field || !grTemp.isValidField(options.display_field))
LINE 32:		options.display_field = gr.getDisplayName();

Similarly to how the t value was fetched for the table, the f GET parameter’s
value is fetched for the field. If not provided, the script will automatically
select whatever the table’s display column is.

LINE 34: if (input && input.filterText)
LINE 35:		gr.addEncodedQuery(options.display_field + "LIKE" + input.filterText)

Similarly to option, input is another global variable but takes inputs from POST
data. In this case, the caller can provide a parameter filterText with a value
that is a valid encoded filter, which will allow filtering on the returned
result set using a LIKE operation. This is really good when testing against
kb_knowledge table’s text field, as I have commonly found passwords and tokens
by filtering for them in the article’s content.

LINE 38: options.secondary_fields = options.secondary_fields || "";
LINE 39: options.secondary_fields = options.secondary_fields.split(",");

Previously on line (30), I showed how to specify a single field to return from
the query. I also previously discussed that the options global variable is often
not controllable by the user since it does not take information from a HTTP
request. However, there does exist other public proxy-like widgets that allow an
unauthenticated user to call this widget and control the values of the options
global, granting the caller the ability to specify additional fields. Think of
it as a fun exercise to figure it out!

LINE 56: data.maxCount = 500;
LINE 57: gr.setLimit(data.maxCount);
LINE 58: gr.query();
         ...
LINE 64: while (gR.next()){
            ...

Prior to eventually querying the DB, a limit of 500 maximum returned records can
be returned. After the query is made, it will begin iterating over the results
set.

LINE 68: var record = {};
         ...
LINE 79: record.sys_id = gr.getValue('sys_id');


An empty Object is instantiated for each record, with the first key-pair
assigned to it being the sys_id (UUID) of the record.

LINE 87: if (options.display_field)
LINE 88:	record.display_field = getField(gr, options.display_field);
LINE 89:
LINE 90: record.secondary_fields = [];
LINE 91: options.secondary_fields.forEach(function(f) {
LINE 92:	record.secondary_fields.push(getField(gr, f));
LINE 93:  });

The value provided to the f GET parameter, or if not provided, the calculated
display field name, is passed to the getField function. This function returns
both the value and also the display value that the record contains in that
field. The difference between the value and the display value is
straightforward. In the event that the options.displayfield is of a ‘reference
field’ type, then the value will be the reference ID (sys_id) and the display
value will be the resolved readable value that is normally shown within the
ServiceNow UI.

This is the same function called for each of the secondary_fields, if provided.

LINE 108: data.list.push(record);


Lastly, the record object is added to the data global variable which is
responsible for returning data to the client.


EXAMPLE PAYLOAD

Apart from the parameters discussed in the previous section, there are two
things worth mentioning when crafting the payload.

Firstly, even though unauthenticated, one must populate various session related
headers. To obtain these session values unauthenticated, navigate to the
ServiceNow login page.

 * The first required header, Cookie, will be populated for you automatically.
   You can simply copy it and its values from your request.

 * The second is requirement is to craft an X-UserToken header. This can be
   populated by taking the value of the g_ck JS variable from the login page
   HTTP response body.

It’s important to understand that in some cases, organizations may be using SSO
and instead of a ServiceNow login page you will receive a 201 redirect to the
IdP. When this happens, refer to your HTTP proxy and you will see that the
redirect occurred from ServiceNow’s oauth_redirect.do page. You may take both
the cookies from the request to this page, and the g_ck value in the response,
in order to obtain / craft a valid Cookie and X-UserToken header.

Secondly, you will notice that the POST method must be used even in the absence
of any POST data. Even if one chooses not to filter the query results using the
filterText POST parameter, the request must still use the POST method and a
Content-Type value of application/json.

This example request is querying the incident table, and does not provide an f
parameter value. So, the widget will default to retrieving the display field for
the table which is number. For the purposes for the PoC, I have set the public
role on the ACL.

POST /api/now/sp/widget/widget-simple-list?t=incident HTTP/1.1
Host: example.service-now.com
Cookie: glide_user_route=glide.2a12d7af3d7d455e312f7e86b22564e7; glide_node_id_for_js=634d231b1c48aefac83fc3383d156040cc484385b028f4b4edcea4c8e3d996c1; BIGipServerpool_ven04337=363f9ef47179748e8b0ebda002c7c371; JSESSIONID=D9D761781699C065ECE575DDF5363A00; __CJ_g_startTime=%221697203063167%22
X-UserToken:d4e3deea1bf1bd1008e154e4604bcb1fe636d0a2e7f6380e8b9f79a037a543fe8fb59dba
Content-Type: application/json
Accept: application/json
Connection: close



It’s extremely easy to determine a response that contains results by doing two
things:

 1. Ensure that the result object is not empty

 2. Ensure that result.data.count > 0

If result is just an empty object, you’ve malformed the request somehow. If
result.data.count is 0, then you do not have permission to read any records from
within this table. Below, a successful response with a single record returned:

...
"data":{
         "viewAllMsg":"View all",
         "isValid":true,
         "count":1,
         "list":[
            {
               "sys_id":"<example sys_id>",
               "className":"incident",
               "display_field":{
                  "display_value":"INC0010021",
                  "label":"Number",
                  "type":"string",
                  "value":"INC0010021"
               },
               "secondary_fields":[
                  {
                     "display_value":null,
                     "value":null
                  }
               ]
            },
         ],
         ...
}
...

In some cases, you might see that display_field.display_value is null. Don’t
panic, this just means that the f value provided isn’t a field you have read
permissions for. In reality, this null return value is a blessing . It’s telling
us ‘Hey there’s an exposed field here, it’s just not this one!’. So, which one
is it?

Well no one is expected to memorise every field name in the schema, and luckily
you don’t have to. Simply do the following:

 1. Login to your own ServiceNow instance

 2. In the Application Navigator search, enter Tables & Columns, select it

 3. CTRL+F your table name. Select it from the list.

 4. On the right-hand side, you’ll see every field within the table. Click on
    the individual field labels to see their actual name. Simply iterate over
    the most interesting ones using the f parameter to see if they’re exposed


TESTING EN MASSE

In order to reliably test this at scale against a large number of tables, one
could export a list of all tables and fields from the ServiceNow dictionary
table and use them as key-pairs in requests. The only downside to this is you’ll
get about 125k results of mostly rubbish.

Instead, for a quick and meaningful test, I would suggest getting a short list
of meaningful (but admittedly lazily filtered) tables.

If you’re performing a blackbox test and don’t have the exact schema of the site
you’re testing against, you will unfortunately be missing out on custom tables
or tables specific to any installed ServiceNow applications. On the plus side,
the list of tables that I provided are sure to exist on every platform. With
this list alone, you can still get incredibly good results against any site
you’re authorized to test against.


IDENTIFYING EXPLOITATION ATTEMPTS

Through analysis of the Transaction Log, one can investigate potential attempts
at exploiting both the specific widget within this article and other public
widgets by an unauthenticated threat actor. I’ve found the easiest way to do so
is by creating a report, below I have outlined the steps on how to do so.

 1. By default, ServiceNow does not allow reporting on the syslog_transaction
    table. Enter sys_properties.list in the Application Navigator and hit Enter.
    Search for glide.ui.permitted_tables and open the record. Append
    syslog_transaction to the value and click Update.

 2. Navigate to Reports > Create New. In the Data sub-menu, enter the
    information as follows:
    
    * Report name: <select a name for the report>
    
    * Source type: Table
    
    * Table: Transaction Log Entry (syslog_transaction)

 3. Select the Configure sub-menu. Click Choose columns. Select the columns you
    wish to see, personally I would choose the following:
    
    * Created
    
    * Output Length (can assist identifying successful exploitation based on
      response size)
    
    * URL
    
    * IP Address

 4. Click the filter icon, as seen in the below screenshot and add the following
    filters:
    
    * URL starts with /api/now/sp/widget/widget-simple-list
      
      * OR
    
    * URL starts with /api/now/sp/widget/5b255672cb03020000f8d856634c9c28
    
    * AND
    
    * Created at or after <select look-back period>
      
      * AND
    
    * Created by is guest

 5. It’s worth noting that the rotation of the syslog_transaction table is
    around 8 weeks, so the look-back period may be quite limited. Click the Save
    button to save the report for reuse. Click Run.

The results will appear on the page. The PDF export will truncate the results
significantly to 1000 rows maximum. It is recommended instead to export to CSV.
To do so, right-click any of the report column headers and select Export > CSV.

Exporting the report as CSV


MITIGATION & REMEDIATION

Whilst the only true remediation for this issue is to address the underlying
access controls that cause information to be public, there are several temporary
mitigations that can bide your organization time whilst the aforementioned
access control issues are being addressed.

INBOUND IP ADDRESS RESTRICTION

Implementing IP restrictions for inbound traffic will entirely prevent public
data exposure, as anyone attempting to access any platform resource from outside
of a defined IP whitelist will be unable to do so. Those which do not have any
legitimate external facing content should already be looking to ensure all
inbound traffic is coming from a trusted network, and this article provides you
another reason to pursue a corporate VPN or similar. On the other hand, those
who do have intentionally public resources, such as a knowledgebase, are
recommended to opt out of this one.

For implementation details and more information about IP restrictions, refer to
the official documentation.

DISABLE PUBLIC WIDGETS

Widgets as a specific vector for retrieving public data while unauthenticated
can be entirely prevented by unchecking the public flag within a widget’s
record. Prior to removing public access, ensure that any widget that is
regularly used by employees of your organization has the proper roles set on the
widget record that will allow them to continue to access the widget during
regular business usage. It is trivial to adjust the report filters within the
Identifying Exploitation Attempts section to look for legitimate usage which can
assist with deciding which roles to secure the widget with.

Making widgets non-public is an effective measure, but don’t forget to take into
account that a widget may also be used by external integrations or external
login-capable users also.

SECURE ACLS WITH A ROLE / EXPLICIT ROLES PLUGIN

Within the Technical Analysis, I alluded to the fact that it’s not only ACLs /
User Criteria with the public role that are at risk, but also empty ACLs. The
widget used for the PoC in this article cannot retrieve data that is not
explicitly assigned the public role, but other vectors I know of can. The
easiest way to secure these ACLs without adjusting the ACL conditions or the ACL
script is to assign a role not possessed by the ‘guest’ user to each ACL.
Implementation is easy, simply create a role and mass-assign it to every user
(except ‘guest’), then subsequently assign it to every ACL that you wish to be
available to all authenticated users. If you do not wish to do this yourself,
you can install the Explicit Roles plugin.

Extensive prep should be made prior to turning this plugin on, as it can break
functionality if implemented hastily and incorrectly. In effect, it will assign
the snc_internal role to all existing users (except guest) and assign the
snc_internal role to all ACLs without a role. This still means you must validate
ACLs that contain the public role, and remove it if necessary. Additionally, be
careful with this since any ‘external integration’ users will also receive the
role. If this occurs, re-assign these integrations the snc_external role and
work with the integration vendor on ensuring its proper functionality.


A HAT TIP

As I’ve insinuated within this article, this is not the only vector in my
arsenal. In fact, I even made ServiceNow aware of it 173 days ago. This vector
functions the exact same as the widget used to prior to ServiceNow’s ‘explicitly
public’ mitigation they introduced to it earlier this year.

In other words, if you’ve got any ACLs that allow for public access through not
only the public role, but also via conditions, a scripted ACL check, or even a
blank ACL, you are still to this day leaking data. Unfortunately, the sys_user
table which contains every user on the platform has an OOB blank ACL for the
name field, therefore satisfying ServiceNow’s definition of ‘public’. In other
words, the table’s not leaking data through the widget discussed in this
article, but it still is via a separate vector.

Therefore, anyone who has not modified that ACL directly or indirectly (such as
through installing the Explicit Roles plugin), is leaking the full names of all
internal users, which is PII. Fortunately, my moral compass is pointing north
and the thought of most instances leaking PII by default is irking me without
doing something about it. So this is a polite nudge to add the little piece of
code below to the scripted ACL part of the read ACL sys_user.name.

gs.getUserName() != "guest";


CONCLUSION

Honestly, I think this entire article illustrated the point I was making pretty
well in the introduction. The fact that this widget disaster is known by the
vendor, as proven this year by their modifications, yet has existed since 2015
without any publicly facing documentation, is appalling. So I’ll parrot what I
said in my introduction. Stop blindly trusting vendors, take security of your
data into your own hands by look intensely where you’re storing it.

Seriously, think about the fact that these SaaS products give you the ability to
publicly expose data through legitimate means. Such as a public knowledge base /
shop / site, give some thought as to how that data is appearing on the page.
What APIs are being used? If you ever even see the concept of public mentioned
anywhere on a SaaS platform, through a permission, role, module, plugin, or
otherwise, you should be raising your eyebrows as to how that’s implemented.

The shared responsibility model becomes broken when there is not clear honesty
between the vendor and the customer. This is not the last article of its kind, I
have many more that I am eager to publish. Hopefully this article will inspire
organisations to take their SaaS security seriously, by employing SaaS security
experts to routinely review not only customer-made configurations but also the
vendor’s architecture of the product and their implementation of an RBAC model.

Thank you for bearing with me throughout this piece. If you have read this
article and decided to implement a process to validate your ServiceNow RBAC
controls along with some mitigatory techniques against the vectors I have
discussed, then you deserve to consider yourself a ‘slay baddy’.

Aaron Costello

Next
Next


SALESFORCE LIGHTNING - TINTING THE WINDOWS



Made with Squarespace