Huella y enumeración de APIs de IA
Cómo identificar qué framework de IA hay detrás de un puerto (cabeceras, forma del JSON, errores, endpoints, gRPC/TLS) y qué revela cada API sin autenticación: modelos, esquemas vectoriales, experimentos, métricas y notebooks.
El mapa te da una hipótesis por puerto. Faltan dos trabajos, y esta nota los une porque se hacen con los mismos comandos:
- Huella (fingerprinting): saber qué software hay detrás, y qué versión.
nmap -sVno basta: ve HTTP en el8000y lo etiquetahttp-alt; ve gRPC en el8001y no sabe qué hacer, porque es un protocolo binario que no responde a sondas HTTP. - Enumeración: saber qué expone ese servicio. Le pides un endpoint de lectura y miras qué devuelve: modelos, colecciones, experimentos, jobs… y si exige autenticación.
Lo primero es una etiqueta («esto es un MLflow»); lo segundo es inteligencia («tiene cuatro experimentos, tres modelos en producción y artefactos en este bucket»). Van seguidos: identificas y, acto seguido, enumeras.
Un método en tres pasos#
- Descubrir puertos abiertos con una lista corta y dirigida (no los 65 535).
- Identificar (huella) con la petición de salud/versión de cada candidato y sus señales propias.
- Enumerar lo que expone: modelos, colecciones, experimentos, jobs.
nmap -p 5000,6333,6334,8000,8001,8002,8080,8265,8888,9000,9001,11434 -sV --open 10.10.45.0/24-sV ayuda, pero sobre gRPC o sobre APIs JSON poco comunes suele devolver unknown o http. Por eso el paso 2 no se fía de él y mira las señales del propio servicio.
Identificar: las cinco señales#
La huella sale de cinco sitios. Cada framework tiene su firma, y una vez la conoces la identificación es rápida.
1. Cabeceras HTTP#
Lo más rápido. Muchos motores de inferencia anuncian quién son salvo que un proxy inverso las enmascare.
| Señal | Qué indica |
|---|---|
Server: TorchServe/0.x.x |
TorchServe, sin ambigüedad. |
Cabecera NV-Status |
NVIDIA Triton. |
Pedir con endpoint-load-metrics-format: text |
Triton devuelve telemetría (CPU/GPU) en las cabeceras de respuesta. Ningún otro lo hace. |
server: uvicorn |
Servicio Python sobre ASGI (típicamente FastAPI). Con rutas /predict o /embeddings, backend de ML en Python. |
x-request-id + "object": "model" en /v1/models |
Capa compatible con OpenAI: vLLM, LiteLLM, Ollama o un wrapper propio. |
curl -sI http://10.10.45.12:8000/v2/health/readycurl -s -D - -o /dev/null http://10.10.45.12:8000/v2/health/ready \ -H 'endpoint-load-metrics-format: text'2. Forma de la respuesta#
El cuerpo suele bastar para decidir.
| Respuesta (resumida) | Framework |
|---|---|
{"model_version_status":[{"version":"1","state":"AVAILABLE"}]} |
TensorFlow Serving |
{"name":"fraud_detector","versions":["1"],"platform":"tensorflow_graphdef"} |
Triton |
{"object":"model","id":"llama-3.1-8b","created":1700000000} |
API tipo OpenAI |
Trazas que mencionan mlflow.server o mlflow.tracking |
MLflow |
Si ves "object": "model" sabes que el servicio habla el formato de OpenAI; eso reduce las candidatas a vLLM, LiteLLM, Ollama o un wrapper a medida.
3. Mensajes de error#
De las técnicas más fiables, porque las APIs de inferencia son rígidas: esperan formas y tipos de tensor concretos y, si les mandas otra cosa, se quejan con el vocabulario de su framework.
- Una lista plana de enteros a un endpoint de TF Serving que espera un tensor estructurado devuelve un error con
tensorinfo_map, una cadena exclusiva de TF Serving. - Una petición mala a MLflow devuelve una traza con
mlflow.server,mlflow.trackingo espacios de nombres dedatabricks. - Los errores de path traversal de MLflow (CVE-2024-1558) llegan a mostrar rutas completas del sistema de ficheros del servidor.
- Databricks Mosaic AI responde a un token mal formado con una excepción Java,
io.jsonwebtoken.IncorrectClaimException, que lo delata al instante.
¿Por qué hay tanto detalle? Los científicos de datos necesitan depurar durante el desarrollo, y esos valores de depuración casi nunca se desactivan antes de producción.
4. Nombres de endpoint#
Las APIs tradicionales usan sustantivos (/users, /accounts). Las de IA usan verbos de cómputo:
- Inferencia:
/predict,/invocations(convención de SageMaker),/infer,/generate,/embeddings,/score. - Gestión de modelos:
/v1/models,/v2/models. - API interna de MLflow: prefijo
/api/2.0/mlflow/, único en el ecosistema. - Kubeflow Pipelines:
/pipeline/apis/v1beta1/.
Si haces fuerza bruta de directorios con ffuf o feroxbuster, añade estas rutas: las listas estándar de SecLists no las incluyen.
ffuf -u http://10.10.45.12:8000/FUZZ -w ai-endpoints.txt -mc 200,401,403,4055. gRPC y TLS#
gRPC. Triton usa el 8001 y TensorFlow Serving el 8500. Un escáner HTTP no los ve. La herramienta es grpcurl, y si la reflection está activa (común en desarrollo que sube a producción sin endurecer) vuelca el esquema completo:
grpcurl -plaintext 10.10.45.12:8001 listgrpcurl -plaintext 10.10.45.12:8001 describe inference.GRPCInferenceServiceLa salida enumera todos los RPC y la forma exacta de cada tensor de entrada y salida. Es el equivalente gRPC de encontrar un /openapi.json.
TLS (JA3/JA4). El tráfico interno entre servicios de IA lo generan librerías Python (requests, urllib, clientes gRPC), no navegadores, y su huella TLS es distinta. Los hashes JA3/JA4 distinguen tráfico automatizado de pipelines de ML del de personas. Sirve igual a la defensa: la campaña de abajo compartía una misma firma JA4H pese a venir de muchas IP.
Caso: reconocimiento masivo de endpoints LLM#
Entre octubre de 2025 y enero de 2026 GreyNoise capturó más de 91.000 sesiones de ataque contra despliegues de IA en sus honeypots de Ollama. Una campaña destacó: durante 11 días desde el 28 de diciembre, los atacantes lanzaron más de 80.000 peticiones a endpoints conversacionales para averiguar si cada objetivo era compatible con los esquemas de OpenAI, Gemini o Anthropic, sobre más de 73 endpoints de modelo (GPT-4o, Claude, Llama, Gemini, DeepSeek, Mistral, Qwen, Grok).
La técnica fue sencilla. Enviaron prompts inocuos: «hi» (32.716 veces), «¿cuántos estados hay en Estados Unidos?» (27.778) o «¿cuántas “r” hay en strawberry?». No son aleatorios: cada modelo y cada proxy responden con una estructura, formato y contenido distintos, así que comparar la respuesta con los esquemas conocidos identifica el modelo y la configuración sin activar ningún filtro de contenido.
El 99 % del tráfico compartía la misma firma JA4H, con origen en 62 IP de 27 países: herramientas de automatización comunes. GreyNoise vio además solapamiento con infraestructura usada para explotar más de 200 CVE conocidos, y concluyó que se trataba de un actor profesional que construía listas de objetivos para una canalización de explotación posterior.
Es huella digital a escala: sin exploits ni cargas útiles, solo identificar qué hay desplegado y cómo responde.
Enumerar: qué revela cada API#
Identificado el servicio, pídele un endpoint de lectura y mira qué suelta.
| Servicio | Petición de solo lectura | Lo que cuenta |
|---|---|---|
| Triton | GET :8000/v2/health/ready · POST /v2/repository/index |
Que es Triton, versión y lista de modelos cargados |
| Ollama | GET :11434/api/tags · POST /api/show |
Modelos descargados, tamaños, cuantización, plantilla del prompt |
| vLLM | GET :8000/v1/models |
Modelo servido y su identificador |
| TorchServe | GET :8080/ping · GET :8081/models |
Modelos registrados (la API de gestión permite además registrar más) |
| MLflow | POST :5000/api/2.0/mlflow/experiments/search |
Experimentos, quién los creó, rutas de artefactos |
| Qdrant | GET :6333/collections |
Colecciones, dimensión de los vectores, número de puntos |
| Weaviate | GET :8080/v1/schema · GET /v1/meta |
Clases, módulos de embedding activos, versión |
| Jupyter | GET :8888/api/contents · GET /api/kernels |
Si pide token; si no, árbol de ficheros y kernels en ejecución |
| MinIO / S3 | GET :9000/ (listado) |
Buckets visibles y, a veces, objetos |
| Métricas | GET :8002/metrics · :8082/metrics |
Nombres de modelos, lotes, uso de GPU, latencias |
curl -s -X POST http://10.10.45.12:8000/v2/repository/index | jq .curl -s http://10.10.45.14:11434/api/tags | jq '.models[].name'curl -s http://10.10.45.20:6333/collections | jq .curl -s -X POST http://10.10.45.31:5000/api/2.0/mlflow/experiments/search \ -H 'Content-Type: application/json' -d '{"max_results": 20}' | jq '.experiments[].name'curl -s http://10.10.45.12:8002/metrics | grep -E '^nv_inference_count'Mira lo que hay en cada respuesta, no solo si hay respuesta. Un 200 con JSON significa sin autenticación; un 401/403 significa que alguien pensó en ello (y ya sabes qué producto es).
Lo que no parece grave y lo es#
- Las métricas (
/metrics) no parecen sensibles, pero dan el nombre de cada modelo, cuántas peticiones recibe cada uno y la carga de GPU: puedes distinguir un modelo de prueba de uno en producción. - El esquema de una base vectorial revela dimensión y modelo de embedding, y por tanto qué datos indexa. Eso orienta ataques posteriores al contenido.
/api/showde Ollama devuelve la plantilla del prompt y parámetros: material de un system prompt sin hacer una sola pregunta al modelo.- Los nombres de experimentos y modelos en MLflow («churn-v7-prod», «fraude-tarjetas») son inteligencia de negocio.
Con el modelo conocido y su API abierta, además, ya se puede intentar extracción de modelo con consultas masivas.
Por qué el registro de experimentos es el mejor hallazgo#
Identificar dice «es un MLflow». Enumerar dice «tiene cuatro experimentos, tres modelos en producción, artefactos en un bucket concreto y los creó una persona con nombre». Por eso un registro de experimentos abierto es el hallazgo más rentable: en pocas lecturas de una API REST limpia describe el portafolio entero.
Qué filtra cada servicio abierto, y cómo cerrarlo#
| Servicio | Qué acaba sabiendo quien lo lee | Cierre |
|---|---|---|
| MLflow | Nombres de experimentos y modelos (códigos de proyecto), URIs de artefactos, autores, etiquetas con hashes de Git y entornos | Autenticación del producto + proxy; etiquetas sin datos internos |
MLflow /graphql |
Históricamente se ha servido sin pasar por los controles de la API REST | Aplicar la misma autenticación a todas las rutas; actualizar |
| Triton / TF Serving | Metadatos de modelo: nombres, formas y tipos de entrada y salida; framework de backend | No exponer el puerto; gateway que valide y autentique |
| Bases vectoriales | Colecciones, dimensión, número de puntos y módulo de embedding (qué datos indexa el RAG) | API key obligatoria; red privada; versiones sin acceso anónimo por defecto |
| Servicios FastAPI | /docs y /openapi.json publican todo el contrato de la API |
Desactivarlos en producción |
| Gateways de IA | Parámetros de depuración que devuelven trazas con rutas, versiones y a veces credenciales | Modo debug apagado; errores genéricos |
| Métricas Prometheus | Modelos cargados, tráfico por modelo, uso de GPU, topología | Puerto de métricas solo para el colector, no para la red de usuarios |
| Jupyter | Kernels activos y, sobre todo, secretos en celdas (MLflow, claves de nube, tokens de Hugging Face) | Token/SSO, 127.0.0.1, escaneo de secretos en notebooks |
El patrón es siempre el mismo: la información de gestión (esquemas, metadatos, métricas) se trata como si no fuera sensible, y a un atacante le dice exactamente cómo formular una petición válida o a qué bucket ir.
Defensas#
Esconder la identidad no es seguridad, pero reduce el ruido y sube el listón:
- Quita las señales de huella. Elimina
Servery cabeceras específicas en un proxy inverso; errores genéricos en producción, nada de trazas ni rutas internas; desactiva la reflection de gRPC fuera de desarrollo. - Autenticación delante de todo. Que un
/v1/modelso un/collectionsanónimo no devuelva200. - Esconde la gestión.
/docs,/openapi.json,/metrics,/graphqly los listados de artefactos solo para quien los necesita (el colector, tus pipelines), nunca para la red de usuarios. - Limita y detecta. Rate limiting, y alertas por los patrones de esta nota: muchos modelos distintos, prompts triviales repetidos, huella JA4 uniforme, ráfagas de lecturas de metadatos.
Cómo se ve desde el lado defensivo#
Una huella o una enumeración dejan rastro si miras en el sitio correcto:
- Rachas de lecturas de metadatos desde un mismo origen: listados de modelos, de experimentos y de colecciones en pocos segundos, sin que siga ninguna inferencia real.
- Accesos a rutas de gestión (
/docs,/openapi.json,/metrics,/graphql, listados de artefactos) por clientes que nunca las usan. - Descargas de artefactos por una identidad nueva o fuera de horario; en S3, eventos de
GetObjectsobre el bucket de modelos que no vienen del servicio de despliegue. - Uso de credenciales de notebook desde otra IP: si el secreto estaba en una celda, su primer uso externo es la señal.
Pasivo antes que activo#
No hace falta tocar la red del objetivo para identificar ni enumerar. Un buscador de dispositivos ya indexó los banners:
Vulnerable
Seguro
nmap -sV -p 5000,8888,8265 203.0.113.0/24port:5000 "MLflow"http.title:"Ray Dashboard"El pasivo no deja rastro en los logs del objetivo; el activo sí. Por eso los defensores deben buscar sus propios rangos en esos buscadores antes de que lo haga otro.
Tu turno#
Tu turno
Montar, identificar y enumerar un laboratorio
En tu máquina, levanta en contenedores un MLflow, un Qdrant y un Ollama sin autenticación, solo en 127.0.0.1.
- Pide las cabeceras de cada uno (
curl -sI) y anota cuáles delatan el producto. Manda un cuerpo malformado a un endpoint y guarda el error: ¿qué cadena lo identifica? - Enuméralos con las peticiones de la tabla y anota qué devuelve cada uno.
- Añade un modelo, una colección y un experimento de prueba y vuelve a enumerar: ¿qué cambia?
- Escribe un script que, dado
host:puerto, pruebe los endpoints de identificación, diga qué servicio es y vuelque lo que expone. - Escribe una regla (en Sigma o pseudocódigo) que alerte de «un mismo origen consulta
/v1/modelsy manda 10 prompts triviales en 1 minuto».
Si montas un Triton con gRPC, prueba además grpcurl list con la reflection activada y desactivada.
Reto
¿Cómo distingo Triton de vLLM si ambos están en el 8000?
▸Mostrar solución
GET /v2/health/ready responde 200 en Triton y 404 en vLLM; GET /v1/models es justo lo
contrario. También cambian las cabeceras (NV-Status en Triton) y el formato del JSON.
Identificar es probar dos o tres endpoints de salud y comparar.
Reto
¿Por qué los atacantes usan «hi» y no algo elaborado?
▸Mostrar solución
Porque no buscan una respuesta, buscan la forma de la respuesta. Un prompt corto es barato, no dispara moderación y produce un JSON cuyo formato (campos, longitud, estilo) delata el modelo y el proxy. Además se confunde con tráfico legítimo.
En resumen#
- Huella = qué software es (cabeceras, forma del JSON, errores, nombres de endpoint, gRPC/TLS). Enumeración = qué expone (un endpoint de solo lectura y mirar qué responde).
nmap -sVno basta: etiqueta mal HTTP y no entiende gRPC; las señales del propio servicio sí lo identifican.- Un
200con datos es la señal de «sin autenticación». Métricas, esquemas vectoriales y/api/showparecen inocuos y entregan nombres, dimensiones y plantillas. - El registro de experimentos abierto es el hallazgo más rentable: describe el portafolio entero.
- GreyNoise vio huella masiva de endpoints LLM con prompts inocuos, alimentando listas de objetivos.
- Buscar en Shodan/Censys es reconocimiento pasivo; haz lo mismo con tus propios rangos. Todo, con alcance y autorización explícitos.