root@svr-app-prod01:~# df -h
-bash: /usr/bin/df: cannot execute binary file: Exec format error
Esa línea la vio alguien a las 04:09 UTC del 30 de octubre, en una terminal que creía de un servidor de aplicaciones Debian. Antes de ella había pedido once cosas y casi todas le habían salido raras: un head -c que el sistema no entendía, un /etc/os-release vacío, un proceso de MySQL que según ps ocupaba ciento treinta y siete mil billones de bytes de memoria. El df fue la que no se podía explicar con una distribución rara o un administrador perezoso. Un binario del sistema que no es un binario del sistema. Once minutos después volvió con otro cliente SSH, otra forma de mandar comandos y una lista de pruebas diseñadas para una sola pregunta: ¿esto es real? A las ocho de la mañana tenía la respuesta, y la obtuvo de la manera más limpia posible: entrando como root con una contraseña de veinticuatro caracteres aleatorios que nadie en el mundo podía conocer.
El servidor era un señuelo mío. Esta es la primera entrega de una serie sobre los ochenta y seis días que tuve dos honeypots expuestos en Oracle Cloud —un Cowrie con un nginx que lo acepta todo, y un Beelzebub haciéndose pasar por Redis, Docker, Elasticsearch, Postgres, MySQL y VNC—; el mapa completo, la infraestructura y los números están en la entrega cero. Empiezo por este caso porque es el único de los dieciséis mil que se comportó como si estuviera pensando, y porque me obliga a escribir sobre algo que no sé resolver.
1. Una IP de nube no es una persona#
hpintel ip 84.13.83.244 --deep
── 84.13.83.244 ────────────────────────────────────────────────────────── ipapi IL · Jerusalem · Oracle Corporation (AS31898 Oracle Corporation) · hosting abuseipdb score 100/100 · 63 reportes de 43 fuentes · BruteForce, SSH, Port Scan · último 2026-11-03 virustotal 4 malicious / 3 suspicious · rep -1 · Oracle Corporation greynoise unknown · noise=False · riot=False otx 1 pulsos · — urlhaus sin URLs de malware threatfox sin IOCs shodan puertos [22, 111, 3306, 6379, 27017] · vulns 0 · eol-product, database, cloud vt comunid. 1 comentarios 2026-10-28 Spring.Boot.Actuator.Unauthorized.Access
El título de la entrega es un apodo, y conviene desarmarlo antes de que alguien lo use como atribución. La IP pertenece al AS31898 de Oracle y se geolocaliza en Jerusalén porque ahí está una de las regiones de Oracle Cloud (il-jerusalem-1). Eso dice dónde corría la máquina virtual, no quién la manejaba ni desde dónde. Una VM en una región de nube se alquila con una tarjeta desde cualquier país, y una VM comprometida la maneja quien la comprometió.
Lo segundo pesa más de lo que parece. Según Shodan, la misma IP expone SSH, rpcbind, MySQL, Redis y MongoDB, con la etiqueta eol-product: software sin soporte escuchando en internet. Una máquina de ataque montada a propósito no suele publicar sus bases de datos al mundo; una máquina olvidada que alguien encontró, sí. No puedo probarlo desde afuera, y no lo afirmo: lo dejo como la hipótesis más barata. Si es correcta, el atacante cazaba credenciales de Oracle Cloud desde una instancia de Oracle Cloud que probablemente no era suya, que es una forma bastante literal de vivir de la tierra.
La reputación cuenta el resto. Para AbuseIPDB está quemada (100/100, 63 reportes de 43 fuentes distintas, mayormente fuerza bruta SSH), y el único comentario de la comunidad en VirusTotal la marca por sondear /actuator de Spring Boot el mismo 28 de octubre, el día que llegó a mi sensor. GreyNoise no la conoce como ruido de fondo: no era un escáner masivo. Era otra cosa.
2. Tres días, cinco servicios, un tema#
hpintel actor 84.13.83.244 --limit 0
── actor 84.13.83.244 ──────────────────────────────────────────────────── store 770 eventos · 3 días · 2026-10-28T13:29 → 2026-10-30T21:06 · clase unknown web 423 · vm1-web · 2026-10-28T13:29 → 2026-10-30T21:06 ssh 205 · vm1-cowrie · 2026-10-30T04:03 → 2026-10-30T20:40 docker 105 · vm2-services · 2026-10-28T16:18 → 2026-10-30T20:55 vnc 24 · vm2-services · 2026-10-30T08:37 → 2026-10-30T10:52 postgres 10 · vm2-services · 2026-10-30T01:55 → 2026-10-30T19:02 ssh-download 3 · vm1-cowrie · 2026-10-30T04:21 → 2026-10-30T04:23
Setecientos setenta eventos en tres sensores lógicos y dos máquinas distintas. Las dos VMs tenían IPs sin relación aparente y huellas complementarias (una era “el servidor de aplicaciones”, la otra “la caja con bases de datos expuestas por error”); una IP que pega en las dos ya está haciendo algo que el escaneo de plantilla casi nunca hace, que es tratar dos direcciones como partes del mismo objetivo.
gantt
title 84.13.83.244 · 28–30 de octubre (UTC)
dateFormat YYYY-MM-DD HH:mm
axisFormat %d %H:%M
section Web (80/443)
scope-check y primer sondeo :w1, 2026-10-28 13:29, 30m
Caza .oci + SSRF a IMDS (OCIcredscan) :w2, 2026-10-28 17:22, 5m
Segunda pasada con canario :w3, 2026-10-28 22:39, 5m
Terraform, actuator, cloud-init :w4, 2026-10-30 05:10, 120m
section Docker API
Enumeración (version, info, images) :d1, 2026-10-28 16:18, 5m
Enumeración profunda (events, volumes) :d2, 2026-10-29 23:50, 120m
section SSH (Cowrie)
paramiko · exec por lotes :s1, 2026-10-30 04:03, 6m
paramiko · PTY con delimitadores :s2, 2026-10-30 04:07, 4m
OpenSSH 10 · bash -s y base64 :s3, 2026-10-30 04:20, 4m
Llaves propias y contraseñas aleatorias :s4, 2026-10-30 07:54, 766m
section Otros
Postgres :p1, 2026-10-30 01:55, 1m
VNC :v1, 2026-10-30 08:37, 135mLo que une todo es el tema. En la web no buscó WordPress ni phpunit ni ninguno de los cuarenta CVEs que la plantilla promedio recita: buscó /.oci/config, /oci_api_key.pem, /home/opc/.oci/config (opc es el usuario por defecto de las imágenes de Oracle Linux), /root/.oci/config, ossutilconfig, terraform.tfstate, cloud-init.yaml. Contra Docker hizo la enumeración que hace un humano que quiere entender qué hay antes de tocar: versión, información del host, contenedores, imágenes, volúmenes, redes, system/df, events?since=0. Nunca intentó crear un contenedor. Nunca montó /. Quería saber, no ejecutar.
3. Lo que buscaba: la identidad de la máquina#
La parte más técnica del interés del visitante es también la más específica de Oracle, y explica todo lo demás.
En AWS, el famoso ataque de SSRF contra el servicio de metadatos de la instancia (169.254.169.254) sirve para robar las credenciales temporales del rol IAM. Oracle Cloud tiene su equivalente, con dos diferencias que el visitante conocía. La primera es la versión 2 de la API de metadatos, que exige la cabecera Authorization: Bearer Oracle: sin ella, /opc/v2/ responde 401, y una SSRF ciega que no controla cabeceras no sirve de nada. La segunda es lo que hay detrás de /opc/v2/identity/: el certificado, la clave privada y el intermedio que la instancia usa para autenticarse como instance principal. Con esos tres archivos, quien los tenga puede firmar peticiones a la API de OCI como si fuera la VM, con los permisos que el grupo dinámico de esa VM tenga en la tenancy.
Eso es lo que pidió, en todas las formas posibles. Por la web, metiendo la URL de metadatos en veintiséis nombres de parámetro distintos (?url=, ?next=, ?proxy=, ?webhook=, ?callback=…) y además como cuerpo de un POST en JSON y en formulario:
POST / HTTP/1.1
User-Agent: Mozilla/5.0 (compatible; OCIcredscan/1.0)
Content-Type: application/json
{"url": "http://169.254.169.254/opc/v2/instance/"}
Y una vez dentro de la shell, directo:
curl -s -m 5 -H 'Authorization: Bearer Oracle' http://169.254.169.254/opc/v2/identity 2>/dev/null
curl -s -m 5 -H 'Authorization: Bearer Oracle' http://169.254.169.254/opc/v2/instance/ 2>/dev/null
La razón por la que esto me importa más que cualquier otra cosa del caso es que el pentest que le hice a mis propios honeypots en agosto había llegado a la misma conclusión por el lado contrario: escapar del emulador hasta la VM real alcanzaba el plano de control de OCI a través del instance principal. Lo dejé anotado como hallazgo alto, pendiente de revisar los permisos IAM. Dos meses después, alguien vino específicamente a buscar esa puerta. No la encontró porque Cowrie no tiene red hacia 169.254.169.254 (el curl emulado no devuelve nada), pero lo que estaba buscando existía, a una capa de distancia.
El resto del inventario completa el perfil de un recolector de credenciales de nube y de escritorio, no de un minero: ~/.aws/credentials, rclone.conf, las credenciales de gcloud, /etc/oci-utils/config, ~/.docker/config.json, perfiles de Chrome, Chromium, Brave y Firefox (logins.json, key4.db, Login Data, Cookies), bóvedas de KeePass y Password Safe (*.kdbx, *.psafe3), repositorios git, archivos .env, docker-compose, inventarios de Ansible. Y un grep -rl sobre todo el disco buscando BEGIN OPENSSH PRIVATE KEY, ocid1.tenancy y ocid1.user. Los OCID son los identificadores de recursos de Oracle; alguien que los busca con grep sabe exactamente qué forma tiene lo que quiere.
4. Lo que vio#
Cowrie guarda un registro de cada sesión con lo que entró y lo que salió de la terminal. Escribí un lector para esos archivos (el formato es simple: una cabecera binaria por evento y los bytes detrás) y reconstruí lo que el visitante tuvo delante. No es un detalle de color: es la única forma de entender por qué hizo lo que hizo después.
hpintel tty 84.13.83.244
# id; hostname; whoami; uname -a; cat /etc/os-release 2>/dev/null | head -3 uid=0(root) gid=0(root) groups=0(root) svr-app-prod01 root Linux svr-app-prod01 6.1.0-21-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.90-1 (2024-05-03) x86_64 GNU/Linux head: invalid option -- '3' # echo '===OCI-CFG==='; cat ~/.oci/config 2>/dev/null; echo '===OCI-KEY==='; cat ~/.oci/oci_api_key.pem 2>/dev/null ===OCI-CFG=== ===OCI-KEY=== # find / -maxdepth 7 \( -iname '*.kdbx' -o -iname 'logins.json' -o -iname 'oci_api_key.pem' … \) | head -60 head: invalid option -- '6' # cat ~/.bash_history 2>/dev/null | tail -c 4000; echo; ls -la /root/.ssh 2>/dev/null tail: invalid option -- 'c' # ls -la /root /home 2>/dev/null; echo '== / =='; ls -la / 2>/dev/null
5 líneas más
drwxr-xr-x 1 phil phil 4096 2026-06-23 00:55 phil == / == -rwxr-xr-x 1 root root 0 2026-05-04 14:44 .dockerenv lrwxrwxrwx 1 root root 7 2024-06-12 00:00 bin -> usr/bin drwxr-xr-x 1 root root 4096 2024-01-28 21:20 boot
Cowrie no ejecuta nada: tiene un sistema de archivos falso y una colección de comandos reimplementados en Python, cada uno con las opciones que alguien se tomó el trabajo de programar. head sabe de -n, pero no de -3 ni de -c; tail tampoco sabe de -c; awk no entiende -F:. Para un bot que manda uname -a y se va, no importa. Para alguien que encadena | head -c 2000 para no ahogarse en la salida, cada pipeline se rompe de una forma que ningún Debian real produciría.
Ordené lo que el visitante fue acumulando en esa media hora, porque es la lista de delatores de mi propio señuelo, y la mitad no los había visto nunca:
| Lo que pidió | Lo que recibió | Por qué delata |
|---|---|---|
head -c 2000, head -3, tail -c | invalid option | coreutils real acepta las tres desde hace décadas |
cat /etc/os-release | nada | el banner y uname dicen Debian; el archivo no existe |
ls -la / | un .dockerenv | el servidor “de producción” parece un contenedor |
ps aux | MySQL con RSS de 137470771248103424 | ciento treinta y siete mil billones de bytes |
which head sort mount cat curl | cada ruta dos veces | /usr/bin/head y /bin/head, siempre en pareja |
df -h, getent hosts | Exec format error | binarios del sistema que no ejecutan |
cat /etc/hostname | svr04 | el prompt dice svr-app-prod01 |
ss -tlnp | solo SSH escuchando | ps mostraba MySQL y ejabberd |
echo $BASH_VERSION | vacío | $SHELL decía /bin/bash |
ls -la /home/phil | un directorio vacío | phil es el usuario de fábrica de Cowrie |
El último es el más feo, y es culpa mía: dejé el sistema de archivos por defecto. phil con el nombre completo Phil California en /etc/passwd es, para quien conoce Cowrie, lo mismo que un letrero.
5. Cómo se adaptó#
Lo que separa este caso de los otros dieciséis mil no es lo que buscaba —recolectores de credenciales hay muchos— sino que cada vez que el señuelo le respondía algo raro, la siguiente sesión era distinta. La secuencia completa, en hora UTC del 30 de octubre:
hpintel session 84.13.83.244
── sesiones ──────────────────────────────────────────────────────────────
2026-10-30 04:03:48 1978e4e609da 7 cmd mediana 0.84s cv 0.04
cliente SSH-2.0-paramiko_5.0.0 · hassh 6372ee695756 · root:qazwsx12A.
2026-10-30 04:06:42 6220d4e514bf 9 cmd mediana 0.84s cv 0.01
cliente SSH-2.0-paramiko_5.0.0 · hassh 6372ee695756 · root:qazwsx12A.
2026-10-30 04:07:25 a914e6def4cb 10 cmd mediana 3.31s cv 0.0
cliente SSH-2.0-paramiko_5.0.0 · hassh 6372ee695756 · root:qazwsx12A.
2026-10-30 04:08:20 3782a675d646 13 cmd mediana 3.92s cv 0.0
cliente SSH-2.0-paramiko_5.0.0 · hassh 6372ee695756 · root:qazwsx12A.
2026-10-30 04:21:46 81e3920c4e62 22 cmd mediana 0.0s cv 1.74
cliente SSH-2.0-OpenSSH_10.0p2 Debian-7+deb13u4 · hassh eeca2460550b · root:qazwsx12A
2026-10-30 07:54:57 a253817a3d4a 14 cmd mediana 3.3s cv 0.54
cliente SSH-2.0-paramiko_5.0.0 · hassh 6372ee695756 · postgres:qazwsx12A.
2026-10-30 08:00:20 be6c698c418a 0 cmd —
cliente SSH-2.0-paramiko_5.0.0 · hassh 6372ee695756 · root:LEMaqEWCOy9lbgC3Tmhsj3g4Primero, lotes por el canal de ejecución. Las dos primeras sesiones usan Paramiko, la librería SSH de Python, y mandan cada comando como una ejecución independiente (exec), a 0,84 segundos uno de otro con una variación de apenas el 1 %: un programa recorriendo una lista. Cada comando viene envuelto en su propio marcador (echo '===OCI===', luego echo '===OCI-CFG==='), y entre la primera sesión y la segunda los marcadores cambian de nombre y los find crecen de profundidad 6 a 7. Alguien leyó la salida de la primera y reescribió la segunda.
Después, una terminal interactiva con delimitadores. Los pipelines rotos con head -c le estaban arruinando la salida. A las 04:07 la estrategia cambia: abre una shell con PTY, como un humano, y empieza a envolver cada comando entre echo __B_id_S__ y echo __B_id_E__, __B_oci_S__ y __B_oci_E__, __B_imds_S__… En una terminal interactiva la salida llega mezclada con el eco y el prompt; los delimitadores son la forma de recortarla con un programa. Y el ritmo cambia a 3,31 y luego 3,92 segundos entre comandos, con variación cero: no es alguien escribiendo, es un bucle que manda un comando y espera un tiempo fijo a que aparezca el prompt.
Después, otro cliente y otro transporte. A las 04:20 la herramienta deja de ser Paramiko. El banner del cliente pasa a ser OpenSSH_10.0p2 Debian-7+deb13u4 —el cliente SSH de un Debian 13 actualizado—, con otra huella HASSH. Prueba echo SEATOK para confirmar que la ejecución vuelve. Manda un script entero por bash -s con un heredoc, se encuentra con que Cowrie lo procesa línea a línea y le devuelve el prompt pegado a cada salida, y en la sesión siguiente manda el mismo script codificado en base64 para esquivar el problema de las comillas:
echo ZWNobyAiIyMjSE9NRSIKbHMgLWxhIC9ob21lLyAyPiYxCmVjaG8gIiMjI1VTRVJTIgphd2sgLUY6IC… | base64 -d | bash
El script decodificado ya no se llama === home dirs ===: se llama ###HOME, ###USERS y, en una línea nueva que no estaba en la versión anterior, ###PHIL. Había visto el directorio de phil y lo agregó a la lista.
Después, la depuración. Las sesiones de las 04:22 a las 04:23 son las más reveladoras del caso, porque ya no buscan nada. Prueban las herramientas:
# printf "a\nb\nc\n" | head -2; echo "--"; echo a:b | awk -F: "{print \$1}"; echo "--"; type awk head; echo "--"; printf "a\nb\nc\n" | head -n 2
head: invalid option -- '2'
awk: invalid option -- ':'
Try 'awk --help' for more information.
-bash: type: command not found
a
bCuatro pruebas en una línea, cada una aislando una sospecha: si head -2 falla y head -n 2 funciona, el head no es el de GNU; si awk -F: falla, el awk tampoco; si type no existe, la shell no es bash. La siguiente sesión pide SHELL y BASH_VERSION, y lista con ls -l los binarios de head, awk, find y cut para ver si son enlaces, scripts o ejecutables. Es exactamente lo que haría yo frente a una máquina que se porta raro: no seguir empujando, sino averiguar qué es.
Al final, la prueba que no admite discusión. A las 07:54 vuelve con Paramiko y empieza a intentar entrar con llave como root y ubuntu, y desde las 08:05 también como opc y admin —los usuarios por defecto de las imágenes de Ubuntu, Oracle Linux y Debian en OCI—, usando dos llaves públicas propias, una ed25519 y una ECDSA. Cowrie las rechaza. Entonces entra con la contraseña que ya sabía que funcionaba, qazwsx12A., e inmediatamente después con LEMaqEWCOy9lbgC3Tmhsj3g4. Después postgres:WdQ8cHr0hGDx5DA1WV3EHFhK. Después postgres:EXWaiPBktluPwNI6zJ5uerjk, postgres:gjhs4X3SmNXrYQThqDlA2_yL, root:__g-OHcphf4ogsmdzwGmOmeX, root:JQNh8c8O_Vfu7hpHbMSnlmCT y, a las 20:40, root:zqb4c94d83b9cfd5c280.
Todas entraron. Mi Cowrie no tenía base de usuarios propia y cargó la de ejemplo, que acepta cualquier usuario con cualquier contraseña; en catorce días de logs las únicas contraseñas rechazadas fueron root:root y root:123456. Para el visitante, cada login exitoso con una cadena aleatoria fue una confirmación, no un acceso: un servidor real no deja entrar a root con veinticuatro caracteres que acaban de generarse. No ejecutó ningún comando en esas sesiones. No hacía falta.
6. ¿Una persona, un programa, o las dos cosas?#
Construí una heurística para la pregunta con la que nació el proyecto —cuánto del ataque es plantilla y cuánto es algo que mira lo que le responden— y la corrí sobre todo el corpus. Cuenta seis señales observables, cada una una huella de haber leído la respuesta: pedir una ruta inventada antes de barrer para saber si el servidor contesta 200 a todo (canario), poner a prueba la shell, entrar con una contraseña aleatoria cuando no se está rociando un diccionario, nombrar algo que solo existe dentro del señuelo, delimitar la salida para parsearla, y reescribir los scripts entre sesiones en vez de repetirlos.
hpintel agentic --decoy svr-app-prod01 --min-score 2 --min-events 5 --evidence
── agente vs plantilla ───────────────────────────────────────────────────
ip score sens eventos señales
84.13.83.244 6 3 770 canary, decoy_reaction, random_password, script_mutation, shell_probe, structured_output
canary GET /lwcym9x12n8ib2.ociprobe
decoy_reaction ls -la /home/phil
random_password login root:LEMaqEWCOy9lbgC3Tmhsj3g4 (success)
script_mutation 44 scripts distintos en 54 envíos
shell_probe id; ls /; echo RC=$?
structured_output echo '===OCI==='; cat ~/.oci/config 2>/dev/null; echo '===KEY==='; ca…
20.80.86.196 2 1 11 random_password, script_mutation
random_password login root:__hp_0f03ed993d9cdbac0cf4c431 (success)
script_mutation 4 scripts distintos en 4 envíosDe 4.103 IPs con actividad suficiente en SSH o web, una tiene seis de seis. La siguiente tiene dos, y es interesante por otra razón: entra con contraseñas que empiezan con __hp_ (de honeypot) para ver si las aceptan, que es una herramienta de detección de señuelos y no un operador adaptativo (la trato en la entrega II). La primera versión de la heurística marcaba también a media docena de bots de fuerza bruta: rociaban miles de contraseñas y alguna, como 1b3c6981abb574003277b6bf9751f9c2, parecía aleatoria. Una contraseña rara entre diez mil es ruido de la lista, no una prueba; la regla correcta cuenta contraseñas distintas, y por encima de cincuenta deja de considerar la señal. Lo cuento porque el número que sale de una heurística depende de decisiones como esa, y esconderlas sería vender precisión que no tengo.
Hay una señal que no entra en el puntaje y que, para mí, pesa más que las seis. En las capturas web completas, el visitante usó veintiún User-Agents distintos en tres días, y casi ninguno es un navegador:
2026-10-28T13:59:13 Mozilla/5.0 (X11; Linux x86_64) scope-check 2026-10-28T17:22:06 Mozilla/5.0 (compatible; OCIcredscan/1.0) 2026-10-28T22:39:39 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36 2026-10-29T23:40:15 Mozilla/5.0 2026-10-30T02:49:33 Mozilla/5.0 (compatible; recon) 2026-10-30T03:16:01 Mozilla/5.0 (probe) 2026-10-30T04:11:03 s3census/1.0 (read-only) 2026-10-30T05:10:59 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 probe/1.0 2026-10-30T06:01:10 scopeaudit/f775e1e6 2026-10-30T06:45:21 scopeaudit/a976d9bb 2026-10-30T07:52:04 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/126 Safari/537.36 2026-10-30T09:55:09 curl/8.5.0
9 líneas más
2026-10-30T11:23:35 Mozilla/5.0 (compatible; sec-audit) 2026-10-30T13:09:59 scopeaudit/087480c0 2026-10-30T17:16:33 Mozilla/5.0 (compatible; scopecheck/1.0) 2026-10-30T17:26:57 Python/3.13 aiohttp/3.14.5 2026-10-30T17:41:49 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124 Safari/537.36 2026-10-30T17:44:05 scopeaudit/1.0 2026-10-30T20:05:08 Mozilla/5.0 (compatible; read-only-recon) 2026-10-30T20:18:35 Mozilla/5.0 (compatible; scopeweaver-recon/1.0) 2026-10-30T20:59:44 curl/8.4.0
Una herramienta de ataque comprada o descargada tiene un User-Agent; a veces lo rota de una lista. Esto no es una lista: es código que se escribe de nuevo en cada iteración, y que en cada iteración se pone un nombre que describe lo que hace. scopeaudit/f775e1e6 lleva el mismo identificador que el canario de esa pasada (/zzqf775e1e6-absent-f775e1e6), así que el nombre se generaba junto con el script. Y el vocabulario es el de una auditoría autorizada: scope, audit, read-only, recon, census. Así se nombra el código cuando quien lo escribe cree, o necesita creer, que está haciendo un trabajo de solo lectura dentro de un alcance acordado.
Juntas, las piezas forman un dibujo coherente. Un bucle que planifica, genera un script, lo ejecuta en lote, lee la salida, se encuentra con un error y escribe el siguiente script corrigiéndolo. Dentro de cada sesión el ritmo es mecánico, porque lo ejecuta un programa; entre sesiones hay minutos, que es el tiempo de leer, decidir y reescribir. Los scripts nuevos llegan con nombres nuevos y con un léxico de auditoría que suena a instrucciones de sistema. Es exactamente lo que dejaría un agente de IA con herramientas de shell, puesto a “auditar” una lista de IPs.
Y sin embargo no lo afirmo, por una razón que no es modestia. Cada una de esas piezas la produce igual de bien una persona con un asistente de código abierto en otra ventana: escribe “dame un script que busque credenciales de OCI”, lo corre, pega el error, pide la corrección. El ritmo entre sesiones es el mismo, los nombres inventados son los mismos, y el vocabulario de auditoría lo pone el modelo aunque el humano no se lo pida, porque así le enseñaron a hablar del tema. La diferencia entre un agente autónomo y un humano que copia y pega lo que dice un agente no deja huella en un honeypot: en ambos casos el código que llega lo escribió un modelo y lo ejecutó alguien que no se tomó el trabajo de mirarlo con cuidado. Lo que sí puedo decir con los datos es más chico y más útil: el adversario adaptativo de este corpus no se distingue por su técnica, que es la de cualquier pentest de manual, sino por su forma de iterar. Y esa forma de iterar ya no requiere saber hacer las cosas, solo saber pedirlas.
Para medir el contraste no hace falta ir lejos. Entre el 30 de septiembre y el 17 de octubre, sesenta y tres IPs distintas —todas, sin excepción, del mismo AS31898 de Oracle— pidieron a mis sensores las mismas rutas de credenciales de OCI: /.oci/config, /.oci/oci_api_key.pem, /.oci/config.bak, setenta y seis variantes en total, diez o doce por IP, con el User-Agent WebSiphon/next-0.1. Las pidieron por HTTP incluso al puerto de VNC y al de la API de Docker, que no hablan HTTP de esa forma, porque la herramienta no mira qué hay del otro lado: dispara la lista y se va. Es el mismo objetivo que el del visitante, servido desde la misma nube, con la estrategia contraria. Si la pregunta del proyecto era cuánto del ataque es plantilla, la respuesta en este corpus es casi todo; lo interesante es que la excepción y la regla buscaban lo mismo.
7. Lo que habría pasado en un servidor real#
El ejercicio útil es invertir el caso: suponer que svr-app-prod01 hubiera sido de verdad un servidor de aplicaciones en OCI, con la contraseña de root que el visitante traía en el bolsillo.
La primera sesión ya habría vaciado ~/.oci/config y la clave de API si existían. La tercera línea habría pedido a los metadatos de la instancia el certificado y la clave del instance principal, y con ellos la API de OCI habría respondido como si las peticiones vinieran de la propia VM: listar buckets, leer secretos del Vault, crear instancias, según lo que el grupo dinámico tuviera permitido. Ningún binario malicioso habría tocado el disco. Ningún antivirus habría visto nada, porque todo lo que corrió son cat, find, grep y curl. El recorrido completo cabe en un par de minutos y no deja más rastro que unas líneas en auth.log y unas peticiones a 169.254.169.254 que casi nadie registra.
La contraseña, de paso, no es aleatoria: qazwsx12A. es un patrón de teclado (las dos primeras columnas del teclado QWERTY, de arriba abajo) con una mayúscula y un punto para cumplir la política de complejidad. El visitante la usó desde el primer intento y no probó otras, así que la trajo de algún lado: de una lista de contraseñas, de otra máquina comprometida o de una filtración. La entrada inicial fue la más vieja del oficio. Lo nuevo vino después.
Si tienes cargas en Oracle Cloud, lo que este caso pone a prueba es concreto:
- Metadatos: fuerza IMDSv2 en todas las instancias (deshabilita los legacy endpoints de v1). Con v2, una SSRF que no controla cabeceras no alcanza
/opc/v2/. - Instance principals: que los grupos dinámicos tengan el mínimo privilegio. El certificado de
/opc/v2/identity/es una credencial, aunque no se vea como una. - Archivos de configuración: que no exista
~/.oci/configcon una clave de API en servidores; para eso están los instance principals. - Detección: alerta ante procesos que no son el agente de OCI pidiendo
169.254.169.254conAuthorization: Bearer Oracle, y antegreprecursivos deocid1.tenancy.
8. Indicadores#
| Tipo | Valor | Contexto |
|---|---|---|
| IPv4 | 84.13.83.244 | AS31898 Oracle · geo IL · 28–30 oct 2026 |
| HASSH | 6372ee6957562199b2fb773b0be1bf34 | cliente SSH-2.0-paramiko_5.0.0 |
| HASSH | eeca2460550b9ded084ecf2f70a75356 | cliente SSH-2.0-OpenSSH_10.0p2 Debian-7+deb13u4 |
| Llave SSH | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINCFs41g/8kteeRhwL9Ssdq2KvznEAEx0ORuxdC1OMB3 | MD5 94:f2:38:4c:60:db:26:9e:cf:4f:ab:00:1f:bd:c8:c2 |
| Llave SSH | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPcZ5nPIRQUYshjqpcPPVdtJgAJskaKlzxAradEj/Va3EclaXdRVA/cwMROA7iYN8X7VqX27GRouUkHlpZHngog= | MD5 60:83:45:5c:ca:59:d9:7a:7d:24:ac:99:8f:48:01:e0 |
| Credencial | root:qazwsx12A. · postgres:qazwsx12A. | acceso inicial |
| SHA-256 | 232e568cf49363ffb11f1cbcffb79edb075daa3077969232a7a6d6cf6590929a | script de recon (=== home dirs ===) |
| SHA-256 | 56f86a7fec49c50d318bede8ec8597f1d5da823c2b5542da37966ed3315071f4 | script de recon v2 (###PHIL) |
| SHA-256 | 1f7c0ef32d88b95c88876108dd532f8aa1e13f09afbdbf00b5788c2e9785efaf | script de recon v3 (###PHILFULL, grep ocid1.tenancy) |
| User-Agent | OCIcredscan/1.0, scopeaudit/*, scopecheck/1.0, scope-check, s3census/1.0 (read-only), read-only-recon, scopeweaver-recon/1.0, sec-audit, probe/1.0 | herramientas generadas por iteración |
| Ruta canario | /lwcym9x12n8ib2.ociprobe, /dwwigzzgsicy-2341862423.absent, /zzqf775e1e6-absent-f775e1e6, /zzqa976d9bb-absent-a976d9bb | detección de soft-404 |
| Marcadores | __B_<nombre>_S__ / __B_<nombre>_E__, ===OCI-CFG===, ###PHILFULL, SEATOK | delimitadores de salida |
Los tres scripts de recon no estaban en VirusTotal ni en MalwareBazaar al momento de escribir esto. Las sesenta y tres IPs de WebSiphon/next-0.1 están en el paquete de IOCs de la entrega cero.
9. MITRE ATT&CK#
| Táctica | Técnica | Dónde |
|---|---|---|
| Reconocimiento | T1595.002 Escaneo de vulnerabilidades | barrido de rutas .oci, .env, terraform.tfstate |
| Acceso inicial | T1078 Cuentas válidas | root:qazwsx12A. traída de fuera |
| Ejecución | T1059.004 Unix Shell | lotes por exec, PTY, bash -s, base64 |
| Descubrimiento | T1082 / T1083 / T1057 / T1049 | uname, find, ps, ss |
| Descubrimiento | T1497.001 Evasión de virtualización y sandbox: chequeos del sistema | pruebas de coreutils, contraseñas aleatorias |
| Acceso a credenciales | T1552.005 Credenciales sin proteger: API de metadatos de la instancia | /opc/v2/identity/* con Bearer Oracle |
| Acceso a credenciales | T1552.001 Credenciales en archivos | ~/.oci/config, .aws/credentials, rclone.conf |
| Acceso a credenciales | T1555.003 Credenciales de navegadores web | logins.json, key4.db, Login Data |
| Acceso a credenciales | T1552.004 Claves privadas | grep -rl "BEGIN OPENSSH PRIVATE KEY" |
| Descubrimiento en nube | T1580 Descubrimiento de infraestructura de nube | Docker API, metadatos de instancia |
10. Detección#
Nada de lo que hizo el visitante requiere un exploit, así que nada se detecta por firma de exploit. Lo que sí deja forma es la búsqueda. Tres reglas que habrían disparado con este caso, escritas para que se puedan adaptar:
title: Proceso no autorizado consulta la identidad de la instancia OCI
id: 8765451d-dc68-4580-9ebf-04fb2224ae57
status: experimental
logsource:
product: linux
category: process_creation
detection:
imds:
CommandLine|contains: '169.254.169.254/opc/'
identidad:
CommandLine|contains:
- '/opc/v2/identity'
- '/opc/v1/identity'
- 'Bearer Oracle'
agente_oci:
Image|contains: 'oracle-cloud-agent' # ajustar a los agentes legítimos de cada imagen
condition: imds and identidad and not agente_oci
level: high
tags:
- attack.credential_access
- attack.t1552.005
alert http any any -> $HOME_NET any (msg:"SSRF a IMDS de OCI en parámetro o cuerpo"; flow:to_server,established; content:"169.254.169.254"; nocase; content:"/opc/"; distance:0; nocase; classtype:attempted-recon; sid:9115001; rev:1;)
alert http any any -> $HOME_NET any (msg:"Barrido de credenciales OCI (.oci/config, oci_api_key)"; flow:to_server,established; http.uri; pcre:"/\/\.?oci(\/|_api_key|_config)/i"; classtype:attempted-recon; sid:9115002; rev:1;)
alert ssh any any -> $HOME_NET any (msg:"Cliente SSH Paramiko 5.x"; flow:to_server,established; ssh.software; content:"paramiko_5"; startswith; classtype:policy-violation; sid:9115003; rev:1;)
La tercera es ruidosa a propósito: Paramiko lo usan también herramientas legítimas de administración. Sirve como contexto, no como alerta sola; combinada con un login de root por contraseña desde una IP de nube, deja de ser ruido.
La parte del caso que más me incomoda es la que funcionó a mi favor. El visitante descubrió el señuelo porque el señuelo era malo: un head incompleto, un usuario de fábrica, una contraseña que entra siempre. Si mi Cowrie hubiera sido mejor, habría seguido preguntando y yo tendría más datos. Si su servidor objetivo hubiera sido real, le habría bastado con la primera sesión. El método que lo llevó a sospechar de mí es el mismo que lo habría llevado al instance principal de alguien más: leer la respuesta, corregir, volver a preguntar. No sé si al otro lado había una persona. Sé que el bucle no se cansa, no se distrae y no necesita entender lo que hace para hacerlo bien, y que la próxima versión del script ya no va a tropezar con head -c.
La entrega siguiente es sobre el resto de los que intentaron averiguar si yo era real, y sobre lo poco que les costó: Ochenta y seis días de señuelo · II — Cómo te descubren.
commit 0xb34r3r0r4c13
Date: 2026-10-30 08:00 AM
note: it read every answer, fixed every error, and the last login was a question