From 83de05b7af1794405c5760ddac568576fc1fbd8e Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Mon, 14 Sep 2026 18:28:45 +0000 Subject: [PATCH] Add content from: Cleartext Credential Recovery in ServiceNow --- .../pentesting-web/servicenow.md | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/network-services-pentesting/pentesting-web/servicenow.md b/src/network-services-pentesting/pentesting-web/servicenow.md index ec578e16c35..054685c155d 100644 --- a/src/network-services-pentesting/pentesting-web/servicenow.md +++ b/src/network-services-pentesting/pentesting-web/servicenow.md @@ -103,6 +103,62 @@ Useful behaviors: - Flags public `/stats.do` access - Supports JSON output for triage and replay +## Post-exploitation: cleartext credential recovery + +ServiceNow **Script Includes** are server-side JavaScript records. If a compromised identity can modify or create client-callable includes, it can expose secrets at the point where legitimate server workflows decrypt them. These are privileged post-exploitation techniques, not authentication bypasses.[[7]](#references) + +### Discovery credentials via `CredentialTestAjax` + +Discovery stores locally managed passwords, SSH keys, API secrets, and cloud credentials in `discovery_credentials`. The UI and Table API do not normally reveal the sensitive values, but the credential-test flow sends an AJAX request to `/xmlhttp.do`; `CredentialTestAjax.testCredential()` decrypts the selected record before passing it to `SNC.CredentialTest.test()`. An operator with write access to the include can save its source, return the decrypted JSON from this intermediate boundary, invoke the ordinary AJAX processor, and restore the original source. The UI may appear to hang because it expects a test result, while the plaintext is present in the raw HTTP response.[[7]](#references) + +The maintained PoC implements the same pattern by temporarily adding `ajaxFunction_retrieveData()`, resolving the supplied `sys_id` with `sn_cc.StandardCredentialsProvider().getCredentialByID()`, reading the attributes required for that credential type, and returning JSON through `/xmlhttp.do`. Its request shape is:[[8]](#references) + +```http +POST /xmlhttp.do +Content-Type: application/x-www-form-urlencoded + +sysparm_processor=CredentialTestAjax&sysparm_scope=global&sysparm_name=retrieveData&sysparm_credSysId= +``` + +The backend path is not limited by whether the record's form exposes **Test Credential**. In the tested instance it recovered Windows/AD passwords, SSH keys and passphrases, AWS keys, Entra ID service-principal secrets, and other locally stored Discovery credential types. External-vault-backed configurations such as CyberArk were not tested.[[7]](#references)[[8]](#references) + +The tested default permission intersection is:[[7]](#references) + +- Execute `CredentialTestAjax`: `discovery_admin` or `agent_admin`. +- Modify its `sys_script_include` record: `script_include_admin` (inherited by `discovery_admin` by default in the tested environment). +- Identify a target: enough `discovery_credentials` access to obtain its `sys_id`. + +For an authorized instance using direct portal authentication, the PoC backs up the include, patches it, requests one record, and restores the source in a `finally` block. It does not directly handle SSO-only or web-service-only identities.[[8]](#references) + +```bash +git clone https://github.com/Tw1sm/servicenow-pocs +cd servicenow-pocs +python3 now_dumpcred_poc.py -i https://.service-now.com -u -s +``` + +### LDAP Password2 fields + +LDAP connection credentials are separate: `ldap_server_config.password` is a reversibly encrypted **Password2** field. A client-callable Script Include can query that table with `GlideRecord`, skip empty values, call the field API `getDecryptedValue()`, and serialize the plaintext. Directly applying this generic API to sensitive `discovery_credentials` fields did not produce plaintext in the research; use the Discovery-specific path above instead.[[7]](#references)[[9]](#references) + +```javascript +var g = new GlideRecord("ldap_server_config"); +g.query(); +while (g.next()) { + var raw = g.getValue("password"); + if (!JSUtil.nil(raw)) { + var password = String(g.password.getDecryptedValue()); + } +} +``` + +The LDAP PoC creates a random global, public, client-callable `AbstractAjaxProcessor`, optionally restricts the query to supplied `sys_id` values, returns `sys_id`, name, DN, and password as JSON, then deletes the include. Creating/modifying and executing such an include required `script_include_admin` in the tested default configuration.[[7]](#references)[[9]](#references) + +```bash +python3 now_ldap_dumpcred_poc.py -i https://.service-now.com -u +# Limit extraction; repeat -s for multiple records +python3 now_ldap_dumpcred_poc.py -i https://.service-now.com -u -s +``` + ## Detection / validation notes From a defender or purple-team perspective, review logs for:[[1]](#references) @@ -112,6 +168,13 @@ From a defender or purple-team perspective, review logs for:[[1]](#referenc - Systematic variation of `t`, `f`, table names, field names, or `filterText` - Public access to `/stats.do` +For privileged credential-recovery activity, correlate script history with AJAX traffic:[[7]](#references)[[8]](#references)[[9]](#references) + +- Audit create, update, restore, and delete events for `sys_script_include`, especially short-lived changes to `CredentialTestAjax` or new client-callable includes. +- Inspect script versions for `_decryptCredentialData`, `sn_cc.StandardCredentialsProvider`, `getCredentialByID`, `getDecryptedValue()`, or code that serializes credential objects into an AJAX response. +- Hunt `/xmlhttp.do` requests invoking credential-related processors, unusual `sysparm_processor` values, `retrieveData`/`dump` methods, or repeated credential `sys_id` values. +- Review direct and inherited assignments of `discovery_admin`, `agent_admin`, `script_include_admin`, and `admin`; restoration or deletion of the malicious include does not erase version and request telemetry. + When validating impact, prefer **bounded evidence**: keep the total count, a minimal sample, and a reproducible request instead of bulk-exporting every accessible row. ## References @@ -122,5 +185,8 @@ When validating impact, prefer **bounded evidence**: keep the total count, a min - [4] [Varonis - Count(er) Strike: Data Inference Vulnerability in ServiceNow](https://www.varonis.com/blog/counter-strike-servicenow) - [5] [ServiceNow - Table API reference](https://www.servicenow.com/docs/r/api-reference/rest-apis/c_TableAPI.html) - [6] [ServiceNow - Service Portal widget API reference](https://www.servicenow.com/docs/r/platform-user-interface/service-portal/widget-api-reference.html) +- [7] [SpecterOps - Cleartext Credential Recovery in ServiceNow](https://specterops.io/blog/2026/08/27/cleartext-credential-recovery-servicenow/) +- [8] [Tw1sm - `now_dumpcred_poc.py`](https://github.com/Tw1sm/servicenow-pocs/blob/main/now_dumpcred_poc.py) +- [9] [Tw1sm - `now_ldap_dumpcred_poc.py`](https://github.com/Tw1sm/servicenow-pocs/blob/main/now_ldap_dumpcred_poc.py) {{#include ../../banners/hacktricks-training.md}}