Skip to content

BishopFox/snowpick

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

snowpick

snowpick is a ServiceNow exposure scanner that finds misconfigured widget ACLs, REST API access controls, and unauthenticated data exposure.

ServiceNow exposes data through two common public surfaces: Service Portal widgets and the Table REST API. When access controls on either surface are misconfigured, unauthenticated users may be able to query internal records such as incident tickets, user directories, knowledge base articles, and service catalog items. snowpick tests both surfaces, separates row exposure from count-only evidence, and produces reproduction details for authorized validation.

How it works

snowpick obtains a session token from the ServiceNow login page without any credentials, then uses it to query the platform's internal APIs. It checks two attack surfaces:

Widgets are small applications that power the ServiceNow Service Portal. Each one has its own access control settings. When those settings are too permissive, an unauthenticated session may be able to retrieve data from the widget. snowpick first asks the instance for a list of installed widgets where possible, then probes each one and checks whether it returns records or count-only evidence.

Table API is a REST interface at /api/now/table/ for querying ServiceNow tables directly. It has separate access controls from widgets, so a target can be locked down on one surface and open on the other. snowpick probes high-value tables like sys_user, incident, and oauth_entity.

Count oracle. Sometimes a widget or table confirms that matching records exist without returning the rows. snowpick flags this as a count_oracle — not full exposure, but not clean either, since counts can be probed with filters to infer data blindly.

Install

❯ go install github.com/BishopFox/snowpick@latest

Or build from source:

❯ git clone https://github.com/BishopFox/snowpick.git
❯ cd snowpick
❯ go build -o snowpick .

Requires Go 1.21 or later.

Usage

Basic scan

Point snowpick at a ServiceNow instance. No credentials needed.

❯ snowpick -target prod.service-now.com
snowpick v1.0.0 | 1 targets | 6 widgets | proxy direct
[!] prod.service-now.com
  | ticket-attachments 272 records [internal_operations:inferred]
[*] 1/1 targets complete, 1 with findings

RESULTS
1 targets scanned in 8.2s | 1 reachable | 1 with findings

  EXPOSED prod.service-now.com  272 records [internal_operations:inferred]
    | ticket-attachments       272 records [internal_operations:inferred]
    |   ServiceNow Overview and Open House 'How To.... Videos'
    |   How to - JIRA and Confluence Access Process
    |   Updating Your Personal Time Zone in ServiceNow

ATTACK SURFACE
  ticket-attachments           #################### 272 across 1 target

DATA CLASSIFICATION
  OPERATIONS          1 finding across 1 target

snowpick automatically discovers widgets installed on the instance and probes each one alongside its built-in defaults. Findings are ranked by severity.

Understanding the output

Progress markers (left of each host):
  [!] findings   [+] clean   [-] unreachable or no session   [*] progress

Summary buckets:
  EXPOSED  data or a confirmed count came back      CLEAN  nothing reachable
  SKIP     no public session (e.g. SSO-only login)  DOWN   host unreachable

Counts:
  272 records              total the instance reports as matching
  272 records, 10 sampled  total, plus the rows snowpick pulled as evidence

Tags:
  [pii:observed]   a hint, written as category:basis
                   basis  observed = value seen in returned data
                          inferred = from a table/field/widget name
                   category  pii, credentials, infrastructure,
                             internal_operations, review_required
  [blind oracle]   count confirmed but no rows returned (a count_oracle)
  [API]            finding came from the Table REST API, not a widget
  [stats.do]       the /stats.do diagnostic endpoint is publicly reachable

Scanning multiple targets

Pass a file with one hostname per line. snowpick scans them in parallel.

❯ snowpick -targets hosts.txt -concurrency 5
snowpick v1.0.0 | 6 targets | 6 widgets | proxy direct
[!] prod.service-now.com
  | ticket-attachments 272 records [internal_operations:inferred]
[*] 1/6 targets complete, 1 with findings
[+] staging.service-now.com clean
[*] 2/6 targets complete, 1 with findings
[!] dev.service-now.com [stats.do]
  | ticket-attachments 61 records [internal_operations:inferred]
[*] 3/6 targets complete, 2 with findings
[+] sandbox.service-now.com clean
[*] 4/6 targets complete, 2 with findings
[-] internal.service-now.com bootstrap failed: /login.do returned 302
[+] test.service-now.com clean
[*] 6/6 targets complete, 2 with findings

REST API probing

ServiceNow also exposes a Table REST API at /api/now/table/ for querying backend tables directly. This is a separate access control surface from widgets. Use -table-api to check both.

❯ snowpick -target prod.service-now.com -table-api
snowpick v1.0.0 | 1 targets | 6 widgets | proxy direct
[!] prod.service-now.com
  | [API] cmn_department 1,234 records, 10 sampled [infrastructure:inferred]
[*] 1/1 targets complete, 1 with findings

For Table API findings, record_count represents the total exposed records when ServiceNow returns a total count. sample_count shows how many records snowpick collected as bounded evidence.

Table discovery

By default, snowpick checks a curated set of high-value tables (incident tickets, user records, attachments, etc.). With -discover, it tries to enumerate table names from the instance and checks those too.

❯ snowpick -target prod.service-now.com -discover -discover-limit 200

Version detection

Use -fingerprint to identify which ServiceNow release the target is running. This helps determine what security patches are in place.

❯ snowpick -target prod.service-now.com -fingerprint
snowpick v1.0.0 | 1 targets | 6 widgets | proxy direct
[!] prod.service-now.com [Zurich (glide-zurich-07-01-2025__patch7-02-19-2026)] [stats.do]
  | ticket-attachments 61 records [internal_operations:inferred]
[*] 1/1 targets complete, 1 with findings

Proxy and rate limiting

Route traffic through Burp, ZAP, or a SOCKS proxy. Use -rate to throttle requests on production instances.

❯ snowpick -target prod.service-now.com -proxy socks5://127.0.0.1:1080 -insecure -rate 200ms

Pipeline mode

Suppress progress output with -quiet. JSON goes to stdout by default, or to a file with -output. Exit code 2 means findings exist, 1 means one or more targets could not be tested, and 0 means the scan completed without findings.

❯ snowpick -target prod.service-now.com -quiet -output results.json
❯ echo $?
2

Working with results

The JSON output contains finding details, including impact hints, request details, bounded sample records when rows are returned, and reproduction material where applicable.

JSON structure

{
  "target": "acme.service-now.com",
  "reachable": true,
  "token_obtained": true,
  "stats_do": false,
  "widgets": [
    {
      "id": "ff26f2b1db12220099f93691f0b8f5ce",
      "name": "ticket-attachments",
      "status": "exposed",
      "record_count": 847,
      "sample_count": 10,
      "hints": [
        {
          "category": "pii",
          "basis": "observed",
          "reason": "email pattern in sample content",
          "signals": ["content:admin@example.com"]
        },
        {
          "category": "internal_operations",
          "basis": "inferred",
          "reason": "widget name associated with internal operations",
          "signals": ["widget:ticket-attachments"]
        }
      ],
      "reproduction": {
        "method": "POST",
        "url": "https://acme.service-now.com/api/now/sp/widget/ff26f2b1db12220099f93691f0b8f5ce",
        "headers": {"X-UserToken": "...", "Cookie": "JSESSIONID=..."},
        "body": "{\"payload\":{\"start\":0,\"end\":1}}",
        "status_code": 200,
        "curl": "curl -i -sS -X 'POST' ..."
      }
    }
  ]
}

Table enumeration findings appear under table_findings, and Table REST API findings appear under table_api_findings. They use the same hints structure. Table API findings also include both record_count and sample_count when a total count is available.

Common jq queries

List all targets with findings:

❯ jq -r '.findings[] | select(any(.widgets[]?; .status == "exposed" or .status == "count_oracle") or ((.table_findings // []) | length > 0) or ((.table_api_findings // []) | length > 0)) | .target' results.json
prod.service-now.com
dev.service-now.com

Get reproduction curl commands for exposed findings (copy-paste to reproduce):

❯ jq -r '.findings[] | (.widgets[]?, .table_findings[]?, .table_api_findings[]?) | select(.status == "exposed") | .reproduction.curl' results.json
curl -i -sS -X 'POST' 'https://acme.service-now.com/api/now/sp/widget/ff26f...' -H 'X-UserToken: MOCK_TOKEN' ...

Show PII findings with target, vector, status, count, samples, and hint basis:

❯ jq -r '.findings[] as $f | ($f.widgets[]?, $f.table_findings[]?, $f.table_api_findings[]?) | select(any(.hints[]?; .category=="pii")) | [$f.target, (.name // (.table + (if .field then "." + .field else "" end))), .status, ((.record_count // .count // 0)|tostring), ((.sample_count // 0)|tostring), ([.hints[]? | select(.category=="pii") | .basis] | unique | join(","))] | @tsv' results.json
acme.service-now.com	ticket-attachments	exposed	847	10	observed
acme.service-now.com	sys_user	exposed	250	10	inferred

Get sample titles for a finding:

❯ jq -r '.findings[] | select(.target == "acme.service-now.com") | .widgets[] | select(.name == "ticket-attachments") | .samples[]?.title' results.json
Synthetic ticket attachment title
Example onboarding guide

Count total exposed records:

❯ jq '[.findings[] | (.widgets[]? | select(.status == "exposed") | .record_count), (.table_findings[]? | select(.status == "exposed") | .count), (.table_api_findings[]? | select(.status == "exposed") | .record_count)] | add // 0' results.json
12896

Flags

TARGETS
  -target string          Single ServiceNow hostname
  -targets string         File with one hostname per line

OUTPUT
  -output string          JSON output file (default: stdout)
  -quiet                  Suppress informational output
  -version                Print version and exit

CONNECTION
  -proxy string           Proxy URL (http, https, socks5, socks5h)
  -timeout duration       HTTP timeout per request (default: 15s)
  -insecure               Skip TLS certificate verification
  -rate duration          Minimum interval between requests (default: unlimited)
  -vendor string          X-Vendor header for engagement tracking

SCANNING
  -concurrency int        Max parallel targets (default: 3)
  -table-concurrency int  Max parallel table probes per target (default: 5)
  -samples int            Sample records per exposed finding (default: 10)

WIDGET PROBING
  -widgets string         Comma-separated widget IDs (overrides defaults)
  -widgets-file string    JSON file with additional widget candidates

TABLE ENUMERATION
  -tables                 Enumerate via widget-simple-list (default: true)
  -tables-file string     JSON file with additional table/field candidates
  -table-api              Probe /api/now/table/ REST API

DISCOVERY
  -discover               Discover tables dynamically from the instance
  -discover-limit int     Max tables from discovery (default: 100)
  -discover-widgets-limit int
                          Max widgets from discovery (default: 50)
  -fingerprint            Detect ServiceNow version and release

Extending coverage

snowpick ships with 6 default widgets and 26 default table/field pairs. You can add more without modifying the code using JSON files:

Widget candidates (-widgets-file):

[{"id": "WIDGET_ID", "name": "custom-widget"}]

Table candidates (-tables-file):

[{"table": "TABLE_NAME", "field": "FIELD_NAME"}]

These merge with the built-in defaults. Duplicates are removed automatically.

Disclaimer

This tool is provided for authorized security testing and research purposes only. You are solely responsible for ensuring you have explicit permission to test any ServiceNow instance you target. Unauthorized access to computer systems is illegal. The authors are not liable for misuse.

License

MIT

About

find sensitive data leaking from ServiceNow instances.

Resources

License

Contributing

Security policy

Stars

6 stars

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages