From 6835e8ca3fe9f32353f8690041bf609a26c9cba3 Mon Sep 17 00:00:00 2001 From: mhavas Date: Thu, 16 Nov 2023 14:14:04 -0500 Subject: [PATCH 1/5] Check for non-null content If any of the display value fields is not null, the leak is more secure --- servicescan.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/servicescan.py b/servicescan.py index b6c4018..abc4e88 100755 --- a/servicescan.py +++ b/servicescan.py @@ -67,20 +67,25 @@ def check_vulnerability(url, g_ck_value, cookies, s, proxies, fast_check, displa if 'data' in response_json['result']: if 'count' in response_json['result']['data'] and response_json['result']['data']['count'] > 0: if response_json['result']['data']['list'] and len(response_json['result']['data']['list']) > 0: - print(f"{post_url} is EXPOSED, and LEAKING data. Check ACLs ASAP.") - if display: - try: - items = response_json['result']['data']['list'] - for item in items: - display_value = item['display_field']['display_value'] - sys_id = item['sys_id'] - if "sys_attachment" in table: - print(f'{url}/sys_attachment.do?sys_id={sys_id}#{display_value}') - else: - print(f'{display_value}') - print("") - except: - print('failed to extract display data') + if response_json['result']['data']['list'] and len(response_json['result']['data']['list']) > 0: + if any(d['display_field']['display_value'] is not None for d in response_json['result']['data']['list']): + print(f"{post_url} is EXPOSED, and REALLY LEAKING data. Check ACLs ASAP.") + else: + print(f"{post_url} is EXPOSED, and POTENTIALLY LEAKING data. Check ACLs ASAP.") + + if display: + try: + items = response_json['result']['data']['list'] + for item in items: + display_value = item['display_field']['display_value'] + sys_id = item['sys_id'] + if "sys_attachment" in table: + print(f'{url}/sys_attachment.do?sys_id={sys_id}#{display_value}') + else: + print(f'{display_value}') + print("") + except: + print('failed to extract display data') else: print(f"{post_url} is EXPOSED, but data is NOT leaking likely because ACLs are blocking. Mark Widgets as not Public.") vulnerable_urls.append(post_url) From 19379abeb4c23716a71735d86ea4919a61912e61 Mon Sep 17 00:00:00 2001 From: Michael Havas Date: Thu, 16 Nov 2023 19:40:42 +0000 Subject: [PATCH 2/5] Enabling table file and username/password authentication --- servicescan.py | 87 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/servicescan.py b/servicescan.py index abc4e88..87220bb 100755 --- a/servicescan.py +++ b/servicescan.py @@ -5,11 +5,12 @@ import requests import json import re +import getpass requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) -def check_vulnerability(url, g_ck_value, cookies, s, proxies, fast_check, display): +def check_vulnerability(url, g_ck_value, cookies, s, proxies, fast_check, display, tables): table_list = [ "t=cmdb_model&f=name", "t=cmn_department&f=app_name", @@ -39,6 +40,8 @@ def check_vulnerability(url, g_ck_value, cookies, s, proxies, fast_check, displa "t=sys_user&f=name", "t=customer_contact&f=name" ] + if tables is not None: + table_list = tables if fast_check: table_list = ["t=kb_knowledge"] @@ -67,25 +70,23 @@ def check_vulnerability(url, g_ck_value, cookies, s, proxies, fast_check, displa if 'data' in response_json['result']: if 'count' in response_json['result']['data'] and response_json['result']['data']['count'] > 0: if response_json['result']['data']['list'] and len(response_json['result']['data']['list']) > 0: - if response_json['result']['data']['list'] and len(response_json['result']['data']['list']) > 0: - if any(d['display_field']['display_value'] is not None for d in response_json['result']['data']['list']): - print(f"{post_url} is EXPOSED, and REALLY LEAKING data. Check ACLs ASAP.") - else: - print(f"{post_url} is EXPOSED, and POTENTIALLY LEAKING data. Check ACLs ASAP.") - - if display: - try: - items = response_json['result']['data']['list'] - for item in items: - display_value = item['display_field']['display_value'] - sys_id = item['sys_id'] - if "sys_attachment" in table: - print(f'{url}/sys_attachment.do?sys_id={sys_id}#{display_value}') - else: - print(f'{display_value}') - print("") - except: - print('failed to extract display data') + if any(d['display_field']['display_value'] is not None for d in response_json['result']['data']['list']): + print(f"{post_url} is EXPOSED, and REALLY LEAKING data. Check ACLs ASAP.") + else: + print(f"{post_url} is EXPOSED, and POTENTIALLY LEAKING data. Check ACLs ASAP.") + if display: + try: + items = response_json['result']['data']['list'] + for item in items: + display_value = item['display_field']['display_value'] + sys_id = item['sys_id'] + if "sys_attachment" in table: + print(f'{url}/sys_attachment.do?sys_id={sys_id}#{display_value}') + else: + print(f'{display_value}') + print("") + except: + print('failed to extract display data') else: print(f"{post_url} is EXPOSED, but data is NOT leaking likely because ACLs are blocking. Mark Widgets as not Public.") vulnerable_urls.append(post_url) @@ -93,9 +94,20 @@ def check_vulnerability(url, g_ck_value, cookies, s, proxies, fast_check, displa return vulnerable_urls -def check_url_get_headers(url, proxies): +def check_url_get_headers(url, proxies, username, password): # get the session + url = url.strip() + url = url.rstrip('/') + s = requests.Session() + if username is not None: + login_url = f"{url}/login.do" + payload = { + 'user_name': username, + 'user_password': password, + 'sys_action': 'sysverb_login' + } + s.post(login_url, payload, proxies=proxies) response = s.get(url, verify=False, proxies=proxies) cookies = s.cookies.get_dict() @@ -110,7 +122,7 @@ def check_url_get_headers(url, proxies): return None, cookies, s -def main(url, fast_check, proxy, display, headers): +def main(url, fast_check, proxy, display, headers, tables, username, password): if proxy: proxies = {'http': proxy, 'https': proxy} else: @@ -118,11 +130,11 @@ def main(url, fast_check, proxy, display, headers): url = url.strip() url = url.rstrip('/') - g_ck_value, cookies, s = check_url_get_headers(url, proxies) + g_ck_value, cookies, s = check_url_get_headers(url, proxies, username, password) if g_ck_value is None: print(f"{url} has no g_ck. Continuing test without X-UserToken header") - vulnerable_url = check_vulnerability(url, g_ck_value, cookies, s, proxies, fast_check, display) + vulnerable_url = check_vulnerability(url, g_ck_value, cookies, s, proxies, fast_check, display, tables) if vulnerable_url and headers : print("Headers to forge requests:") if g_ck_value is not None: @@ -143,20 +155,43 @@ def main(url, fast_check, proxy, display, headers): parser.add_argument('--proxy', help='Proxy server in the format http://host:port', default=None) parser.add_argument('--display', action='store_true', help='output display name for quick visual') parser.add_argument('--headers', action='store_true', help='print headers to forge request with any vulnerable systems') + parser.add_argument('--table-file', help='The specific tables to test') + parser.add_argument('--username', help='The username to login with') + parser.add_argument('--password', help='The password to login with') args = parser.parse_args() fast_check = args.fast_check display = args.display proxy = args.proxy headers = args.headers + table_file = args.table_file + username = args.username + password = args.password + + # Prompt for a password if required + if username is not None and password is None: + password = getpass.getpass("Enter password: ") + + # Collect tables to test from file if required + tables = None + if table_file is not None: + try: + table_file = args.table_file + with open(table_file, 'r') as file: + tables = file.read().splitlines() + except FileNotFoundError: + print(f"Could not find {url_file}") + except Exception as e: + print(f"Error occurred: {e}") + if args.url: - any_vulnerable = main(args.url, fast_check, proxy, display, headers) + any_vulnerable = main(args.url, fast_check, proxy, display, headers, tables, username, password) else: try: url_file = args.file with open(url_file, 'r') as file: url_list = file.readlines() for url in url_list: - if main(url, fast_check, proxy, display, headers): + if main(url, fast_check, proxy, display, headers, tables, username, password): any_vulnerable = True # At least one URL was vulnerable except FileNotFoundError: print(f"Could not find {url_file}") From 29672f1299743b38e9d72ab87044b4a9a15646ae Mon Sep 17 00:00:00 2001 From: Michael Havas Date: Thu, 16 Nov 2023 19:46:42 +0000 Subject: [PATCH 3/5] updated help --- servicescan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servicescan.py b/servicescan.py index 87220bb..0cc62b8 100755 --- a/servicescan.py +++ b/servicescan.py @@ -155,7 +155,7 @@ def main(url, fast_check, proxy, display, headers, tables, username, password): parser.add_argument('--proxy', help='Proxy server in the format http://host:port', default=None) parser.add_argument('--display', action='store_true', help='output display name for quick visual') parser.add_argument('--headers', action='store_true', help='print headers to forge request with any vulnerable systems') - parser.add_argument('--table-file', help='The specific tables to test') + parser.add_argument('--table-file', help='The specific tables to test. Sample sample_table_query.txt') parser.add_argument('--username', help='The username to login with') parser.add_argument('--password', help='The password to login with') args = parser.parse_args() From abe3a6bf257272ccde824214895dbab67376502e Mon Sep 17 00:00:00 2001 From: Michael Havas Date: Thu, 16 Nov 2023 19:46:57 +0000 Subject: [PATCH 4/5] Provide sample table-file --- sample_table_query.txt | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 sample_table_query.txt diff --git a/sample_table_query.txt b/sample_table_query.txt new file mode 100644 index 0000000..9b93be2 --- /dev/null +++ b/sample_table_query.txt @@ -0,0 +1,26 @@ +t=cmdb_model&f=name +t=cmn_department&f=app_name +t=kb_knowledge&f=text +t=licensable_app&f=app_name +t=alm_asset&f=display_name +t=sys_attachment&f=file_name +t=oauth_entity&f=name +t=cmn_cost_center&f=name +t=cmdb_model&f=name +t=sc_cat_item&f=name +t=sn_admin_center_application&f-name +t=cmn_company&f=name +t=customer_account&f=name +t=sys_email_attachment&f=email +t=sys_email_attachment&f=attachment +t=cmn_notif_device&f=email_address +t=sys_portal_age&f=display_name +t=incident&f=short_description +t=work_order&f=number +t=incident&f=number +t=sn_customerservice_case&f=number +t=task&f=number +t=customer_project&f=number +t=customer_project_task&f=number +t=sys_user&f=name +t=customer_contact&f=name From 3006dc8419e06f4d120bb87228c793a3cf706503 Mon Sep 17 00:00:00 2001 From: Michael Havas Date: Thu, 16 Nov 2023 19:53:42 +0000 Subject: [PATCH 5/5] Updated documentation --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index e6c98bb..de03dba 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,25 @@ Perform a fast check that only scans for the table `kb_knowledge` using the `--f python3 servicescan.py --url https://redacted.service-now.com --fast-check ``` +### Extended tests +To perform more tests than what is provided by default, you can specify a `--table-file TABLE_FILE` argument to query particular tables a fields. A sample file is provided in `sample_table_query.txt`: +```bash +python3 servicescan.py --url https://redacted.service-now.com --table-file sample_table_query.txt +``` + ### Using a Proxy To use a proxy server, use the `--proxy` option: ```bash python3 servicescan.py --url https://redacted.service-now.com --proxy http://host:port ``` +### Credentials +By default, the script will perform a scan without logging-in. If interested in performing a scan using a particular user +```bash +python3 servicescan.py --url https://redacted.service-now.com --user test.snc.internal +``` +You will be prompted to specify a password if not provided via the `--password` argument. + ### Example Output If the target instance is found to be vulnerable, you'll receive an output similar to the following: ```bash