Respuesta rápida
Un endpoint GraphQL de "sincronización de contactos" en una plataforma de IA conversacional aceptaba arrays arbitrarios de teléfonos por petición, sin rate limiting y sin verificar que pertenecieran a la agenda del usuario. PoC: enumeración del prefijo +34 636 (1 millón de candidatos) en 1 hora, 93 perfiles reales extraídos con UID, handle, nombre completo, bio y fotos. Cero detecciones.
1. Contexto — la función de sincronización de contactos
La app móvil de la plataforma incluye una funcionalidad de sincronización: el usuario puede subir los números de su agenda y la plataforma devuelve qué números corresponden a cuentas registradas. Esta funcionalidad está expuesta como un endpoint GraphQL en la API.
El endpoint es:
POST https://api.target.tld/api/gql_POST
Operation: PoeDeviceContactForPhoneNumbersQuery
El fallo es que este endpoint:
- No tiene control de acceso más allá de tener una sesión autenticada.
- No verifica que los números enviados pertenezcan a la agenda del dispositivo.
- No tiene rate limiting.
- Acepta arrays de cientos de números por petición.
2. La petición vulnerable
POST /api/gql_POST HTTP/2
Host: api.target.tld
Cookie: m-b=...; m-login=1; m-s=...; m-uid=...
Content-Type: application/json
{
"extensions": {
"hash": "c56e7237b4ac9258dc9dd8633a4165ba386052e8e0ed964b5c9515003e58139c"
},
"operationName": "PoeDeviceContactForPhoneNumbersQuery",
"variables": {
"phoneNumbers": ["+34636138342", "+34636180219"]
}
}
El campo phoneNumbers acepta un array de teléfonos arbitrarios — no tienen que estar en la agenda del dispositivo ni tener relación con el usuario autenticado.
3. La respuesta — PII devuelta por cada match
Por cada número que corresponde a una cuenta registrada, el endpoint devuelve:
{
"data": {
"poeDeviceContactForPhoneNumbers": [
{
"poeUser": {
"uid": 3140577186,
"handle": "exampleuser",
"fullName": "John Doe",
"bio": "...",
"profilePhotoURLSmall": "https://...",
"profilePhotoURLMedium": "https://..."
},
"contactPhoneNumber": "+34600000001"
}
]
}
}
| Campo | Descripción |
|---|---|
uid | ID único del usuario en la plataforma |
handle | Nombre de usuario público |
fullName | Nombre completo real |
bio | Biografía del perfil |
profilePhotoURLSmall | URL de foto de perfil (baja resolución) |
profilePhotoURLMedium | URL de foto de perfil (media resolución) |
contactPhoneNumber | Teléfono confirmado como vinculado a esa cuenta |
El resultado es un mapeo directo y confirmado de número de teléfono → identidad real.
4. Características que amplifican el impacto
Sin rate limiting. Durante el PoC se enviaron peticiones de forma continua durante horas contra el prefijo +34 636 (1.000.000 de números candidatos) sin encontrar ningún throttling, bloqueo, CAPTCHA ni retraso progresivo. Cada petición devolvió respuesta con éxito.
Batching masivo. El campo phoneNumbers acepta arrays de hasta 200 números por petición (probado en el PoC). Esto permite enumerar grandes rangos con muy pocas peticiones HTTP.
Sin verificación de propiedad. El endpoint no comprueba que los números enviados existan en la agenda del dispositivo del usuario que hace la petición. Cualquier número arbitrario es válido como input.
Sin relación previa requerida. No hace falta interacción anterior entre el atacante y las víctimas cuyos datos se extraen.
5. El script de automatización
Para demostrar el impacto a escala, se desarrolló un script Python que automatiza la enumeración completa de cualquier prefijo telefónico de cualquier país.
Funcionamiento:
- Itera sobre un rango configurable de números (secuencial o aleatorio).
- Agrupa los números en batches del tamaño configurado.
- Envía cada batch al endpoint GraphQL con las cookies de sesión.
- Registra en tiempo real todos los matches encontrados en un CSV.
Parámetros principales:
| Parámetro | Descripción | Default |
|---|---|---|
--mode | sequential (escaneo completo) o random (muestreo) | sequential |
--prefix-range | Rango de prefijos a enumerar, ej. 636 636 | — |
--batch | Números de teléfono por petición | 50 |
--delay | Segundos entre peticiones | 1.0 |
--output | Nombre del CSV de salida | users_spain.csv |
--resume | Retoma desde el último estado guardado | false |
Comando del PoC:
python3 phone_enum.py --mode sequential --prefix-range 636 636 --batch 200
CSV de salida: columnas phone, uid, handle, fullName, bio, profilePhotoURLSmall, profilePhotoURLMedium, timestamp.
6. Resultados reales del PoC
Enumeración contra el prefijo español +34 636 (1.000.000 de candidatos):
- Tiempo de ejecución: ~1 hora.
- Números candidatos enviados: 1.000.000.
- Perfiles de usuario reales extraídos: 93.
- Rate limiting encontrado: ninguno en ningún momento.
- Bloqueos o detecciones: ninguno.
El CSV de resultados incluye los 93 perfiles con teléfono, UID, handle, nombre completo, bio y fotos. El perfil del propio investigador aparece en los resultados, confirmando la cobertura correcta del escaneo.
+34 636 es uno de los 190 prefijos disponibles solo en España. El script es directamente aplicable a cualquier país del mundo cambiando el parámetro de prefijo.
7. Impacto observado
- Extracción de 93 perfiles reales con PII completa en 1 hora de escaneo sobre un único prefijo.
- Mapeo confirmado teléfono → nombre real, UID, handle, bio y fotos de perfil.
- Cero mecanismos defensivos activados durante todo el proceso.
- El mismo script funciona contra cualquier prefijo de cualquier país sin modificaciones.
8. Clasificación técnica
| Campo | Valor |
|---|---|
| Tipo de vulnerabilidad | IDOR + Missing Rate Limiting + Missing Input Validation |
| CWE | CWE-359 — Exposure of Private Personal Information |
| Endpoint afectado | POST https://api.target.tld/api/gql_POST |
| Operación GraphQL | PoeDeviceContactForPhoneNumbersQuery |
| Autenticación requerida | Sí — cuenta gratuita |
| Interacción de la víctima | Ninguna |
| Complejidad de explotación | Muy baja — petición HTTP con array de teléfonos |
9. Notas técnicas
- El endpoint usa un hash fijo para identificar la operación GraphQL:
c56e7237b4ac9258dc9dd8633a4165ba386052e8e0ed964b5c9515003e58139c. Este hash es estático y reproducible. - La sesión necesaria se obtiene de las cookies de la app móvil capturadas con un proxy (Burp Suite en el PoC).
- El batch de 200 números por petición fue el tamaño testeado en el PoC — es posible que el límite superior sea mayor.
- La respuesta solo devuelve datos de números que efectivamente tienen cuenta. Los números sin cuenta no generan error, simplemente no aparecen en el array de respuesta.
Labs relacionados
Practica enumeración de endpoints GraphQL, batching y caza de PII a escala: labs de GraphQL.
Practica esto en un lab
Graphql
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Artículos relacionados
Account enumeration via LinkedIn + phone — exposure de millones de usuarios
Caso real: account enumeration en API combinada con scraping de LinkedIn + reverse phone lookups para correlacionar identidades reales con accounts privados. Privacy impact crítico.
Acortador de URL como PII leak masivo — extracción de ~300 personas/hora
Códigos de baja entropía + sin rate limiting + ticket sin auth = enumeración de teléfonos, tarjetas (BIN+últimos 4) y compras de clientes reales.
IDOR en API de newsletters — la UI lo oculta, la API lo regala
Un endpoint sin comprobación de autorización en servidor. Un parámetro público en la URL. Acceso a la lista de suscriptores de cualquier newsletter de una red social profesional.