Saltar al contenido

Temas

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 -sV no basta: ve HTTP en el 8000 y lo etiqueta http-alt; ve gRPC en el 8001 y 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#

  1. Descubrir puertos abiertos con una lista corta y dirigida (no los 65 535).
  2. Identificar (huella) con la petición de salud/versión de cada candidato y sus señales propias.
  3. Enumerar lo que expone: modelos, colecciones, experimentos, jobs.
descubrir
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.
cabeceras de un candidato
curl -sI http://10.10.45.12:8000/v2/health/ready
curl -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.tracking o espacios de nombres de databricks.
  • 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 con wordlist propia de IA
ffuf -u http://10.10.45.12:8000/FUZZ -w ai-endpoints.txt -mc 200,401,403,405

5. 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:

gRPC con reflection
grpcurl -plaintext 10.10.45.12:8001 list
grpcurl -plaintext 10.10.45.12:8001 describe inference.GRPCInferenceService

La 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
identificar y enumerar
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/show de 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 Server y 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/models o un /collections anónimo no devuelva 200.
  • Esconde la gestión. /docs, /openapi.json, /metrics, /graphql y 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 GetObject sobre 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

activo: tú sondeas
nmap -sV -p 5000,8888,8265 203.0.113.0/24
pasivo: ya está indexado
port: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.

  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?
  2. Enuméralos con las peticiones de la tabla y anota qué devuelve cada uno.
  3. Añade un modelo, una colección y un experimento de prueba y vuelve a enumerar: ¿qué cambia?
  4. Escribe un script que, dado host:puerto, pruebe los endpoints de identificación, diga qué servicio es y vuelque lo que expone.
  5. Escribe una regla (en Sigma o pseudocódigo) que alerte de «un mismo origen consulta /v1/models y 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 -sV no basta: etiqueta mal HTTP y no entiende gRPC; las señales del propio servicio sí lo identifican.
  • Un 200 con datos es la señal de «sin autenticación». Métricas, esquemas vectoriales y /api/show parecen 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.

Referencias#

Notas relacionadas

ESC
↑↓ para navegar↵ para abriresc para cerrar