Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/network-services-pentesting/pentesting-web/servicenow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<sup>[[7]](#references)</sup>

### 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.<sup>[[7]](#references)</sup>

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:<sup>[[8]](#references)</sup>

```http
POST /xmlhttp.do
Content-Type: application/x-www-form-urlencoded

sysparm_processor=CredentialTestAjax&sysparm_scope=global&sysparm_name=retrieveData&sysparm_credSysId=<credential_sys_id>
```

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.<sup>[[7]](#references)[[8]](#references)</sup>

The tested default permission intersection is:<sup>[[7]](#references)</sup>

- 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.<sup>[[8]](#references)</sup>

```bash
git clone https://github.com/Tw1sm/servicenow-pocs
cd servicenow-pocs
python3 now_dumpcred_poc.py -i https://<instance>.service-now.com -u <user> -s <credential_sys_id>
```

### 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.<sup>[[7]](#references)[[9]](#references)</sup>

```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.<sup>[[7]](#references)[[9]](#references)</sup>

```bash
python3 now_ldap_dumpcred_poc.py -i https://<instance>.service-now.com -u <user>
# Limit extraction; repeat -s for multiple records
python3 now_ldap_dumpcred_poc.py -i https://<instance>.service-now.com -u <user> -s <ldap_config_sys_id>
```

## Detection / validation notes

From a defender or purple-team perspective, review logs for:<sup>[[1]](#references)</sup>
Expand All @@ -112,6 +168,13 @@ From a defender or purple-team perspective, review logs for:<sup>[[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:<sup>[[7]](#references)[[8]](#references)[[9]](#references)</sup>

- 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
Expand All @@ -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}}