
trouver les données sensibles qui fuient des instances ServiceNow.
snowpick est un scanner d'exposition ServiceNow qui détecte les ACL de widgets mal configurés, les contrôles d'accès à l'API REST et l'exposition de données sans authentification.
ServiceNow expose des données via deux surfaces publiques courantes : les widgets Service Portal et l'API REST Table. Lorsque les contrôles d'accès sur l'une ou l'autre surface sont mal configurés, des utilisateurs non authentifiés peuvent être en mesure d'interroger des enregistrements internes tels que les tickets d'incident, les annuaires d'utilisateurs, les articles de la base de connaissances et les éléments du catalogue de services. snowpick teste les deux surfaces, distingue l'exposition de lignes de la simple preuve de comptage et produit des détails de reproduction pour une validation autorisée.
snowpick obtient un jeton de session depuis la page de connexion ServiceNow sans aucun identifiant, puis l'utilise pour interroger les API internes de la plateforme. Il vérifie deux surfaces d'attaque :
Widgets sont de petites applications qui alimentent le Service Portal de ServiceNow. Chacune possède ses propres paramètres de contrôle d'accès. Lorsque ces paramètres sont trop permissifs, une session non authentifiée peut être en mesure de récupérer des données à partir du widget. snowpick demande d'abord à l'instance la liste des widgets installés lorsque c'est possible, puis sonde chacun d'eux et vérifie s'il renvoie des enregistrements ou une simple preuve de comptage.
Table API est une interface REST située à /api/now/table/ permettant d'interroger directement les tables ServiceNow. Elle possède des contrôles d'accès distincts de ceux des widgets, si bien qu'une cible peut être verrouillée sur une surface et ouverte sur l'autre. snowpick sonde des tables à forte valeur comme sys_user, incident et oauth_entity.
Oracle de comptage. Parfois, un widget ou une table confirme l'existence d'enregistrements correspondants sans renvoyer les lignes. snowpick signale cela comme un count_oracle — ce n'est pas une exposition complète, mais ce n'est pas non plus sain, car les compteurs peuvent être sondés avec des filtres pour déduire les données à l'aveugle.
❯ go install github.com/BishopFox/snowpick@latest
Ou construisez à partir des sources :
❯ git clone https://github.com/BishopFox/snowpick.git
❯ cd snowpick
❯ go build -o snowpick .
Nécessite Go 1.21 ou ultérieur.
Pointez snowpick vers une instance ServiceNow. Aucun identifiant n'est nécessaire.
❯ 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 découvre automatiquement les widgets installés sur l'instance et sonde chacun d'eux en plus de ses valeurs par défaut intégrées. Les résultats sont classés par gravité.
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
Passez un fichier avec un nom d'hôte par ligne. snowpick les analyse en parallèle.
❯ 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
ServiceNow expose également une API REST Table à /api/now/table/ pour interroger directement les tables backend. Il s'agit d'une surface de contrôle d'accès distincte de celle des widgets. Utilisez -table-api pour vérifier les deux.
❯ 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
Pour les résultats de l'API Table, record_count représente le nombre total d'enregistrements exposés lorsque ServiceNow renvoie un compte total. sample_count indique le nombre d'enregistrements que snowpick a collectés comme preuve bornée.
Par défaut, snowpick vérifie un ensemble sélectionné de tables à forte valeur (tickets d'incident, enregistrements utilisateurs, pièces jointes, etc.). Avec -discover, il tente d'énumérer les noms de tables depuis l'instance et vérifie également ceux-ci.
❯ snowpick -target prod.service-now.com -discover -discover-limit 200
Utilisez -fingerprint pour identifier la version de ServiceNow exécutée par la cible. Cela aide à déterminer quels correctifs de sécurité sont en 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
Acheminez le trafic via Burp, ZAP ou un proxy SOCKS. Utilisez -rate pour limiter les requêtes sur les instances de production.
❯ snowpick -target prod.service-now.com -proxy socks5://127.0.0.1:1080 -insecure -rate 200ms
Supprimez la sortie de progression avec -quiet. Le JSON sort sur stdout par défaut, ou dans un fichier avec -output. Le code de sortie 2 signifie que des résultats ont été trouvés, 1 qu'une ou plusieurs cibles n'ont pas pu être testées, et 0 que l'analyse s'est terminée sans résultat.
❯ snowpick -target prod.service-now.com -quiet -output results.json
❯ echo $?
2
La sortie JSON contient les détails des résultats, y compris les indices d'impact, les détails de requête, des échantillons d'enregistrements bornés lorsque des lignes sont renvoyées, et du matériel de reproduction le cas échéant.
{
"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:[email protected]"]
},
{
"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' ..."
}
}
]
}
Les résultats d'énumération de tables apparaissent sous table_findings, et les résultats de l'API REST Table apparaissent sous table_api_findings. Ils utilisent la même structure hints. Les résultats de l'API Table incluent également record_count et sample_count lorsqu'un compte total est disponible.
Liste de toutes les cibles avec des résultats :
❯ 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
Obtenez les commandes curl de reproduction pour les résultats exposés (copier-coller pour reproduire) :
❯ 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' ...
Affichez les résultats PII avec cible, vecteur, statut, compte, échantillons et base de l'indice :
❯ 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
Obtenez les titres d'échantillons pour un résultat :
❯ 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
Comptez le nombre total d'enregistrements exposés :
❯ 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
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
snowpick est livré avec 6 widgets par défaut et 26 paires table/champ par défaut. Vous pouvez en ajouter sans modifier le code à l'aide de fichiers JSON :
Candidats de widgets (-widgets-file) :
[{"id": "WIDGET_ID", "name": "custom-widget"}]
Candidats de tables (-tables-file) :
[{"table": "TABLE_NAME", "field": "FIELD_NAME"}]
Ils sont fusionnés avec les valeurs par défaut intégrées. Les doublons sont supprimés automatiquement.
Cet outil est fourni uniquement à des fins de tests de sécurité autorisés et de recherche. Vous êtes seul responsable de vous assurer que vous disposez d'une autorisation explicite pour tester toute instance ServiceNow que vous ciblez. L'accès non autorisé à des systèmes informatiques est illégal. Les auteurs ne sont pas responsables d'un mauvais usage.