root@svr-app-prod01:~# sh -c 'printf '"'"'\n%s|%s\n'"'"' '"'"'check_cfa2dba9…_small_math'"'"' "$((314*738+96))"'
-bash: 314*738+96: command not found
La respuesta correcta era 231828. Cualquier sh de los últimos treinta años la calcula sin pensar, porque la expansión aritmética $(( )) es POSIX y está en la shell desde antes de que existieran los honeypots de SSH. Mi Cowrie no la calcula: recibe 314*738+96, lo toma por el nombre de un comando y dice que no lo encuentra. Esa línea la pidió un cliente Go desde un servidor dedicado en el AS47890, en una de las últimas sesiones que registró el sensor, como la segunda de treinta y ocho pruebas que mandó en poco más de dos segundos. No buscaba nada en el disco. Quería saber con qué estaba hablando.
En la entrega anterior conté el caso de un visitante que descubrió el señuelo a fuerza de tropezar con él y después lo confirmó con método. Era la excepción. Esta entrega es sobre la regla: todas las formas en que el corpus intentó averiguar si yo era real, cuántas IPs usaron cada una, qué les contestó el señuelo y —lo que más me interesa— qué hicieron con la respuesta.
1. Dos versiones del mismo dato#
python3 scripts/catalogo_antihoneypot.py .
── catálogo de pruebas de fidelidad ────────────────────────────────────────────────────── prueba IPs eventos ventana ASN principal check_<nonce>_small_* 17 38832 2026-10-24 → 2026-11-03 AS47890×13, AS48090×4 detector __hp_ (tokens, /proc) 2 5 2026-09-14 → 2026-09-21 AS8075×1, AS38731×1 echo -e "\x6F\x6B" (checker) 5 34147 2026-09-09 → 2026-10-15 AS45090×1, AS22927×1 echo -e "auth_ok" (RedTail) 3 78 2026-08-12 → 2026-10-29 AS6461×1, AS213790×1 echo xsec 10 812 2026-08-10 → 2026-10-16 AS26277×1, AS37263×1 echo SHELL_TEST 3 139 2026-08-11 → 2026-10-30 AS198364×2, AS396982×1 echo Hi | cat -n 47 53 2026-08-10 → 2026-11-01 AS4766×10, AS31898×4 ls -lh $(which ls) 114 126 2026-08-10 → 2026-11-03 AS38365×22, AS4811×19 /ip cloud print 47 54 2026-08-10 → 2026-11-01 AS4766×10, AS31898×4 locate D877F783D5D3EF8Cs 46 52 2026-08-10 → 2026-11-01 AS4766×10, AS31898×4 login con contraseña !root 18 89 2026-08-12 → 2026-11-03 AS47890×13, AS48090×4 canario web: Odin 34 68 2026-08-11 → 2026-11-03 AS14061×34 canario web: Nmap 7 36 2026-08-16 → 2026-11-02 AS63949×7 canario web: hex32 / __aws_leak_probe 20 40 2026-08-21 → 2026-10-31 AS396982×18, AS197170×2 canario web: *-404-probe 5 6 2026-09-07 → 2026-10-18 AS396982×2, AS218785×2 canario web: actor IL 1 6 2026-10-28 → 2026-10-30 AS31898×1
Un honeypot de interacción media es una mentira con muchas bocas. Cowrie no ejecuta nada: tiene un sistema de archivos serializado, un intérprete de shell escrito en Python y un centenar de comandos reimplementados uno por uno. Cada boca miente bien por separado. El problema es que no se ponen de acuerdo, porque nadie las escribió para que se consultaran entre sí. El prompt dice svr-app-prod01 y /etc/hostname dice svr04. ps muestra MySQL y ss no lo ve escuchando. which ls responde con dos rutas y ls -lh dice que no existe ninguna de las dos.
De todas las pruebas que vi, la más elegante entiende exactamente eso. El 14 de septiembre, una IP de Azure (20.80.86.196) entró como root con password, volvió a entrar un segundo después con __hp_0f03ed993d9cdbac0cf4c431 —una contraseña que nadie tiene, y menos un servidor real— y, cuando también funcionó, mandó en tres segundos esto:
echo ccbc05c3e509f73f1d60b098 > /tmp/.hp3cf79aed_a; echo 1ac94285246a782110490191 > /tmp/.hp3cf79aed_b
printf 'TOKENA:%s\n' "$(cat /tmp/.hp3cf79aed_a | tr -d '\n')" # ¿lo que escribo se puede leer?
{ __hp_1e8cded; } 2>&1; printf 'EXITCODE:%d\n' $? # ¿un comando inexistente da 127?
printf 'COWRIE_DIR:%s\n' "$(ls -d /home/cowrie /opt/cowrie /srv/cowrie | head -1)"
printf 'LOADAVG_RAW:%s\n' "$(cut -d' ' -f1-3 /proc/loadavg)" # la carga según el kernel…
printf 'LOAD_CMD:%s\n' "$(uptime | grep -oP 'load average: \K[\d., ]+')" # …y según uptime
SPID=$$; awk '/^Pid:/{print "STATUSPID:"$2}' /proc/$SPID/status # el PID según la shell y según /proc
printf 'TCP_LISTEN_22:%s\n' "$(grep -c ' 00000000:0016 ' /proc/net/tcp)" # ¿de verdad escucha el 22?
(Recortado: el store guarda los primeros 500 caracteres de cada comando y la sesión es de septiembre, antes de la ventana de los logs completos.)
No busca vulnerabilidades ni credenciales. Pide cada dato dos veces, a dos fuentes que en un Linux real son la misma cosa vista desde dos lados: la carga del sistema según /proc/loadavg y según uptime, el PID de la shell según $$ y según /proc/$$/status, el puerto 22 según el banner y según la tabla de sockets del kernel. Un emulador implementa una de las dos y deja la otra vacía, o las implementa con números que no cuadran. Cowrie, por ejemplo, escucha en el 2222 detrás de una redirección, así que /proc/net/tcp —si el emulador lo tuviera— no podría mostrar el 22 escuchando sin mentir de una tercera forma. El detector ni siquiera necesita interpretar los valores; le basta con compararlos.
El detalle que más me gusta es involuntario. Cowrie guarda como “descarga” todo archivo que el atacante crea, así que en mi inventario de 858 muestras hay dos de veinticinco bytes, 8e27eade… y 4d264ed9…, que son los tokens que el detector escribió en /tmp para comprobar si podía releerlos. Mi colección de malware incluye la prueba con la que me descubrieron.
Hubo una segunda IP con el mismo prefijo, 171.244.14.96 (Viettel, Vietnam), una semana después: un solo login con __hp_763b993a95439eab_x__ y nada más. No sé si es la misma herramienta en otra versión o una que solo prueba la contraseña. Dos IPs en ochenta y seis días: los detectores dedicados existen, pero son raros.
2. La batería#
El caso de la apertura es más interesante que el detector de Azure, aunque sea menos fino, porque viene en volumen y tiene una forma muy reconocible. Entre el 24 de octubre y el 3 de noviembre, que es donde termina el store (los logs completos de Cowrie la siguen viendo hasta el último día del sensor), diecisiete IPs —trece del AS47890 (Unmanaged Ltd) y cuatro del AS48090 (Techoff SRV)— mandaron 38.832 comandos con la misma estructura: un identificador check_ seguido de un MD5 nuevo en cada ronda, y un sufijo que dice qué se está probando.
hpintel tty 80.94.92.234
# sh -c 'printf … check_cfa2dba9…_small_control …' root@svr-app-prod01:~# sh -c 'printf … check_cfa2dba9…_small_math "$((314*738+96))"' -bash: 314*738+96: command not found root@svr-app-prod01:~# sh -c 'printf … check_cfa2dba9…_small_pid "$$"' root@svr-app-prod01:~# sh -c 'set -- …_arg1 …_arg2; printf … check_cfa2dba9…_small_positional "$#|$1|$2"' COLUMNS=240 HOME=/root LINES=24 LOGNAME=root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin SHELL=/bin/bash … root@svr-app-prod01:~# echo; echo 'check_64906f2cecce31f30907a446a2cd88e8_transport_done' check_64906f2cecce31f30907a446a2cd88e8_transport_done root@svr-app-prod01:~# echo 'check_679d008b…_small_math|'"$((604*140+45))" -bash: 604*140+45: command not found check_679d008b…_small_math| root@svr-app-prod01:~# echo 'check_679d008b…_small_pid|'"$$" check_679d008b…_small_pid|$$
Cada ronda prueba cinco capacidades de una shell POSIX: imprimir un marcador (control), hacer una cuenta (math), conocer su propio PID (pid), recibir parámetros posicionales con set -- (positional) y definir y llamar una función (function). Las cinco se prueban tres veces, por tres transportes: envueltas en sh -c, mandadas directamente y reescritas con echo en lugar de printf. Al final de cada transporte, un transport_done con otro nonce marca el cierre.
Lo que contestó mi señuelo no deja lugar a dudas. printf con formato no imprime nada. La aritmética es “command not found”. $$ sale tal cual, como texto, porque el intérprete de Cowrie no expande la variable. Y set --, que en una shell real reemplaza los parámetros posicionales sin decir nada, en Cowrie vuelca las variables de entorno, porque su set solo sabe hacer lo que hace set sin argumentos. De las cinco pruebas, solo pasan dos, control y function, y solo cuando van por echo. Un programa que compare cada salida con lo esperado (231828, un número de proceso, 2|…_arg1|…_arg2) tiene la respuesta antes de terminar la primera ronda.
Y aquí viene lo que no esperaba. De las diecisiete IPs de la batería, dieciséis subieron igual su minero por SFTP (/root/.16 o /dev/shm/.16) y lo intentaron ejecutar con sudo -S. La batería descubre el señuelo y la campaña sigue como si nada.
No creo que sea descuido. Leída con calma, la batería no está diseñada para decidir si atacar, sino cómo: los tres transportes y el transport_done dicen que lo que se elige es la forma de mandar los comandos siguientes, según qué construcciones de shell sobreviven en el destino. Es una herramienta pensada para routers, NAS y cajas embebidas con busybox recortados, donde printf o las funciones pueden faltar de verdad. Para esa herramienta, un Cowrie es solo un destino más con una shell rara. Descubrirlo no le cambia el plan; le cambia el transporte.
Un último detalle, que dejo como hipótesis: las diecisiete IPs entraron con la contraseña !root, y solo una IP más la usó en todo el corpus. En el formato de la base de usuarios de Cowrie, root:x:!root es la línea que significa “rechazar root como contraseña de root”. Como contraseña literal, !root no tiene sentido en un servidor real, pero Cowrie con la base de ejemplo la acepta sin problema. Puede ser coincidencia de diccionario. También puede ser alguien que leyó el userdb.example de Cowrie y armó su lista con él.
3. Aceptarlo todo#
Mi Cowrie no tenía base de usuarios propia y cargó la de ejemplo, que termina con *:x:*: cualquier usuario, cualquier contraseña. Las únicas contraseñas rechazadas en catorce días de logs completos fueron root:root y root:123456. Lo elegí así para maximizar la interacción, y es el delator más grande de todos, porque no requiere ningún comando.
El visitante de Jerusalén lo usó como prueba final: entró con veinticuatro caracteres aleatorios y se dio por respondido. El detector de Azure lo usó como primer paso. Pero el efecto más grande de aceptarlo todo no es sobre los que buscan señuelos, sino sobre los que no:
| IP | Red | Fecha | Logins exitosos | Único comando |
|---|---|---|---|---|
61.220.127.240 | AS3462 HiNet (TW) | 27-sep, 12:42 → 23:01 | 14.505 | echo -e "\x6F\x6B" |
93.170.12.102 | AS44546 Alfa Telecom (CZ) | 8-oct | 14.085 | echo -e "\x6F\x6B" |
179.41.10.172 | AS22927 Telefónica (AR) | 23-sep | 8.199 | echo -e "\x6F\x6B" |
154.66.109.229 | AS37642 Comnet (LS) | 15-oct | 2.398 | echo -e "\x6F\x6B" |
118.195.163.19 | AS45090 Tencent (CN) | 9-sep | 724 | echo -e "\x6F\x6B" |
\x6F\x6B es “ok”. Cinco IPs, casi todas de redes residenciales o de proveedores de acceso, hicieron en total 39.911 logins exitosos con usuarios y contraseñas distintos en cada intento, y después de cada uno mandaron un único comando: imprimir “ok” para confirmar que la credencial da una shell. Son validadores de listas: herramientas que reciben un archivo de credenciales, probablemente comprado o filtrado, y devuelven las que funcionan. Contra mi sensor, funcionaron todas.
En algún lugar hay un archivo con unas catorce mil quinientas credenciales “verificadas” de mi IP, y una buena parte del valor de una lista verificada es que lo esté de verdad. Si ese archivo se vende, el comprador recibe basura con sello de calidad. No sé si eso le hace daño a alguien en particular; sé que un honeypot que acepta todo no solo se delata, también envenena el mercado de credenciales a una escala que no buscaba. Lo cuento como efecto, no como mérito: no lo diseñé y no lo puedo medir.
El caso inverso es RedTail, que en la entrega IV aparece con todo su kit: desde tres IPs, la primera línea después de uname -a es echo -e "\x61\x75\x74\x68\x5F\x6F\x6B\x0A" —“auth_ok”—, y justo detrás escribe en /tmp una llave privada SSH. Es la misma pregunta que la del validador ("¿esto me da shell?"), hecha por quien sí piensa quedarse.
4. Preguntas que parecen pruebas#
Hay que desarmar una parte del catálogo, porque algunas de sus filas no son lo que parecen y contarlas como pruebas anti-honeypot inflaría el número.
ls -lh $(which ls) aparece en 114 IPs y suena a test de fidelidad clásico: pide el tamaño del binario de ls, que en un sistema real ronda los 140 KB y en un emulador puede no existir. En mi Cowrie no existía: which devolvía /usr/bin/ls y /bin/ls, y ls respondía que ninguno de los dos estaba. Pero las 114 IPs que lo mandaron son, sin excepción, parte de las 2.029 que instalaron la llave mdrfckr en authorized_keys. Es una línea más del reconocimiento de esa botnet: CPU, memoria, crontab, w, uname, el tamaño de ls. Lo recoge, no lo lee. Después de recibir una respuesta imposible, la sesión siguió exactamente igual que en cualquier otro destino.
echo Hi | cat -n, /ip cloud print y locate D877F783D5D3EF8Cs aparecen siempre juntas, en las mismas 46–47 IPs (diez de Korea Telecom, cuatro de Oracle). Son un solo script de diez comandos:
/ip cloud print ifconfig uname -a cat /proc/cpuinfo ps | grep '[Mm]iner' ps -ef | grep '[Mm]iner' ls -la ~/.local/share/TelegramDesktop/tdata /home/*/.local/share/TelegramDesktop/tdata /dev/ttyGSM* /dev/ttyUSB-mod* /var/spool/sms/* … locate D877F783D5D3EF8Cs echo Hi | cat -n
Leído entero, el script no busca un señuelo: clasifica el botín. /ip cloud print es un comando de RouterOS, así que pregunta si está en un MikroTik. Los grep '[Mm]iner' buscan mineros de la competencia. Las rutas de tdata, /dev/ttyGSM* y /var/spool/sms buscan sesiones de Telegram Desktop y módems GSM con los que mandar SMS. Y D877F783D5D3EF8Cs no es un canario aleatorio, como parece. Telegram Desktop nombra sus archivos con el MD5 de una palabra clave, con los nibbles de cada byte intercambiados, y D877F783D5D3EF8C es exactamente lo que sale de md5("data"): es el nombre de los archivos de sesión que guarda dentro de tdata. El locate los busca en todo el disco, por si la sesión no está en la ruta por defecto. De las diez líneas, la única que prueba la shell es cat -n, y la pasó: Cowrie sabe numerar líneas.
Lo mismo vale para echo xsec y echo SHELL_TEST: un eco de marcador sirve tanto para saber si hay shell como para delimitar salida, y con diez y tres IPs respectivamente no tengo forma honesta de decir cuál de las dos cosas era. La lista de pruebas anti-honeypot de verdad, una vez desagregada, es corta: el detector de Azure, la batería, los validadores (sin quererlo) y los canarios web. El resto es reconocimiento que, en un señuelo, también delata, pero que nadie lee.
5. El canario de veintiocho bytes#
La versión web de la misma pregunta es la más vieja y la más barata: pedir una ruta que no puede existir y mirar qué contesta el servidor. Un servidor real dice 404. Un señuelo que quiere atrapar cualquier exploit contesta que sí a todo, y entonces una ruta inventada recibe el mismo 200 que /wp-login.php.
Mi nginx era exactamente eso. Toda petición se reenviaba a un puerto local donde no escuchaba nadie, el error se convertía en una página de respaldo y la página de respaldo era siempre la misma:
location @decoy {
default_type text/html;
return 200 "<html><body>OK</body></html>";
}
Veintiocho bytes, el mismo cuerpo para cualquier ruta, en el puerto 80 y en el 443. Las 89 peticiones de canario que conservan los logs completos recibieron un 200 (y una, un 400 por malformada).
Quién lo preguntaba dice bastante sobre lo común que es la técnica. Treinta y cuatro IPs de DigitalOcean pidieron /odinhttpcall<número> o /Odin/http/call<número>, la firma de Odin, un buscador de infraestructura expuesta. Siete IPs de Linode pidieron /Nmap/folder/check<número>, que es lo que hacen los scripts http-* de Nmap para saber si el servidor devuelve 404 de verdad antes de interpretar resultados. Dieciocho IPs de Google Cloud pidieron rutas de treinta y dos caracteres hexadecimales, /admin<hex> y /__aws_leak_probe_<hex>__. Algunas herramientas ponen la intención en el nombre: scnr-probe-404-b7e21c9a4f, crusader-404-probe. El visitante de Jerusalén usó la suya con un nonce que se repetía en el User-Agent.
Ninguna de esas herramientas es exótica. El chequeo de soft-404 está en Nmap desde hace años, y cualquier escáner serio lo hace antes de reportar un hallazgo, por pura higiene: no quiere decirle a su usuario que /backup.zip existe solo porque el servidor contesta 200 a todo. En ese sentido, que mi nginx fuera descubierto por el canario de Odin no dice que Odin cazara honeypots. Dice que mi honeypot era indistinguible de un servidor mal configurado, y que los escáneres llevan años sin tomarse en serio a los servidores mal configurados.
6. Un contenedor para una sola cuenta#
En el sensor de servicios, la API de Docker la emulaba Beelzebub, y ahí apareció la versión más pura de la idea. El 26 de septiembre, desde 86.38.182.193 (Verda Cloud, Helsinki) y dos IPs de Cloudflare, alguien subió por /images/load una imagen propia, ccimp:1, que no contiene casi nada más que busybox, y creó con ella un primer contenedor:
{"Image": "ccimp:1", "Cmd": ["/bin/busybox", "sh", "-c", "echo KANIT=$((98765*4321))"],
"Tty": false, "AttachStdout": true, "AttachStderr": true,
"HostConfig": {"AutoRemove": false, "NetworkMode": "bridge"}}
KANIT es “prueba” en turco. Un Docker real crea el contenedor, ejecuta busybox sh y devuelve KANIT=426763565. Un emulador tiene que haber cargado de verdad la imagen, arrancado un proceso y calculado la cuenta, o tiene que mentir con un número; y para mentir con el número correcto tendría que calcularlo, que es lo mismo que no mentir. Es la batería de la sección 2 reducida a su esencia: una sola pregunta cuya respuesta solo puede dar algo que ejecute.
Qué contestó Beelzebub no lo sé. Los logs completos rotaban cada catorce días y el 26 de septiembre quedó fuera; el store solo conserva la ruta de la API, no la respuesta. Lo que sí sé es que la campaña siguió: el contrabando de ccimp:1 y lo que vino después ocupan una parte de la entrega IV.
7. Mis delatores#
Juntando lo que vio el visitante de Jerusalén con lo que preguntó el resto del corpus, la lista de lo que me delataba es esta. Ninguno es exótico y casi todos están en la configuración por defecto:
| Delator | Quién lo encontró | IPs que tropezaron con él |
|---|---|---|
| Acepta cualquier usuario y contraseña | detector __hp_, actor IL (y los validadores sin saberlo) | todas; lo pusieron a prueba 3 |
Aritmética $(( )), $$, set --, printf con formato | batería check_* | 17 |
Un archivo subido por SFTP a /dev/shm no existe un segundo después | campaña .16 (sin buscarlo) | 12 |
which devuelve rutas a binarios que ls no encuentra | mdrfckr (sin leerlo) | 114 |
| 200 con 28 bytes idénticos para cualquier ruta web | Odin, Nmap, escáneres en GCP, actor IL | 67 con canario explícito |
| SSH dice Debian 12 (OpenSSH 9.2p1); nginx, en la misma IP, dice 1.24.0, que es la versión de Ubuntu 24.04 | nadie, que yo sepa: lo muestra Shodan | — |
head -c, tail -c, awk -F:, type inexistentes; df con Exec format error | actor IL | 1–2 |
Usuario phil / Phil California, /etc/hostname = svr04, .dockerenv | actor IL | 1 |
ps muestra servicios que ss no ve; RSS imposible | actor IL | 1 |
| Docker que no ejecuta contenedores | KANIT | 3 |
La columna de la derecha la saqué de los 2.480 replays de los últimos catorce días y del store, y desordena la intuición. Los delatores finos que tanto me incomodaron en la entrega anterior —head -c, awk -F:, type, el df que no ejecuta— los encontraron una o dos IPs en dos semanas. El que más tropiezos produjo no lo buscaba nadie: la campaña .16 sube su minero por SFTP a /dev/shm, hace chmod +x ochocientos milisegundos después y Cowrie le contesta que el archivo no existe. Subido a /root, el mismo archivo sí aparece. Ese delator no necesita un atacante atento. Le rompe el flujo al bot más tonto, y de paso me deja sin ver qué habría hecho el minero.
Shodan, por su parte, no etiquetó ninguno de los dos sensores como honeypot. Lo que sí publica, a quien quiera leerlo, es que en la misma IP conviven un SSH de Debian 12 y un nginx que solo trae de fábrica Ubuntu 24.04. Ningún visitante del corpus hizo esa cuenta. Es gratis hacerla.
8. Un señuelo que dure#
Con esos números, la lista de recomendaciones que habría escrito antes de tener los datos sale bastante cambiada. La ordeno por cuántos tropiezos evita cada cambio y por lo que cuesta, no por lo elegante que suena.
Que lo que suben exista. Antes que cualquier otra cosa, comprobaría que un archivo subido por SFTP o scp aparece en el sistema de archivos virtual en todas las rutas donde los atacantes suelen dejarlo: /tmp, /dev/shm, /var/tmp, directorios ocultos relativos al cd actual. Se prueba en diez minutos con un cliente propio. Es el único delator del corpus que afecta a campañas que no miran, y además es el que más datos cuesta, porque corta la cadena justo antes de la ejecución.
Que todos los servicios cuenten la misma historia. El banner SSH, la versión de nginx (o server_tokens off), el kernel de uname, /etc/os-release, /etc/hostname, el prompt y el usuario de fábrica tienen que describir la misma máquina. Es una tarde de trabajo: generar el sistema de archivos desde una instalación real de la distribución que anuncia el banner y montar el resto del señuelo sobre esa misma distribución. Evita lo que encontró el actor IL y lo que publica Shodan.
No restringir las credenciales: rechazar solo las imposibles. Esta me sorprendió. La recomendación de siempre es dejar de aceptar todo. Medí qué pasaría con una base que solo acepte los pares usuario:contraseña más frecuentes del propio corpus: con los diez primeros, todavía entra el 67 % de las IPs que ejecutaron comandos, y con los mil primeros, el 72 %. Lo que se pierde no está repartido al azar. El visitante de Jerusalén entró con una credencial que está en el puesto 153.664 del ranking, y con cualquier base restrictiva me habría quedado sin el único caso adaptativo de los ochenta y seis días. Mientras tanto, los que buscan señuelos entran igual: la batería y los validadores también prueban admin:admin y compañía (sus mejores credenciales están entre los puestos 3 y 7), y el detector de Azure entró con root:password antes de probar la suya. La alternativa que los datos sostienen es la inversa: aceptar casi todo y rechazar solo lo que ningún servidor real tendría. Eso incluye las contraseñas con aspecto de generadas (longitud, entropía, letras y dígitos alternados, la misma regla con la que hpintel marca la señal random_password), los prefijos de detector como __hp_ y la sintaxis del propio userdb, como !root. Cowrie no trae esa clase; son pocas líneas sobre UserDB. Tiene una debilidad obvia: quien pruebe con una contraseña rara pero verosímil la esquiva. Es una carrera, no un cierre.
Responder 404 a lo que no existe. El nginx puede seguir registrando todo; lo que no puede es contestar 200 a todo. Un 404 por defecto, y 200 solo para las rutas de una aplicación creíble (y coherente con la distribución anunciada), elimina los 67 canarios explícitos del corpus. Cuesta poco y no pierde datos, porque los exploits se registran igual aunque reciban un 404.
El intérprete y la ejecución, solo si los necesitas. La batería check_* y KANIT piden algo que un emulador no tiene: ejecutar. Para Cowrie existen dos salidas. Una es el backend = proxy, que reenvía las sesiones a máquinas reales y desechables, con todo lo que eso implica de aislamiento, reseteo y riesgo. La otra es el backend llm, todavía experimental, que no probé: un modelo puede acertar 231828, pero no sé si sostiene que $$ coincida con /proc/$$/status y con el ps de dos comandos atrás. Para la API de Docker, la única respuesta correcta a KANIT es un dockerd de verdad dentro de una VM sin red. Las dos cosas convierten un honeypot de interacción media en uno de interacción alta. Vale la pena solo si lo que quieres estudiar son actores como el de Jerusalén. Para estudiar botnets y mineros no hace falta: dieciséis de diecisiete subieron el minero sabiendo, o pudiendo saber, que yo no era real.
Lista corta, en orden de rendimiento por hora de trabajo:
- Prueba tú mismo que lo que subes por SFTP/
scpexiste en/tmp,/dev/shmy rutas relativas. (12 IPs del corpus tropezaron aquí) - Una sola distribución en todo: banner SSH,
uname,/etc/os-release, hostname, prompt y versión del servidor web; fueraphil,svr04y.dockerenv. - 404 por defecto en el señuelo web, 200 solo para una aplicación creíble. (67 IPs con canario)
- Acepta casi cualquier credencial, pero rechaza las que parecen generadas,
__hp_*y la sintaxis del userdb (!root). No restrinjas a una lista: perderías al visitante que más importa. - Conserva los logs completos más de catorce días y vigila que el pull no se rompa: varias preguntas de esta entrega (qué contestó Beelzebub a
KANIT, el texto completo del detector de Azure) no tienen respuesta porque los logs ya habían rotado. - Solo si buscas actores adaptativos:
backend = proxyhacia VMs desechables o undockerdreal aislado. Si buscas volumen, no hace falta: te van a descubrir y van a seguir igual.
9. ¿Importa que te descubran?#
Hice la cuenta que me parece honesta. De las 4.103 IPs con actividad suficiente para evaluarlas, las que aplicaron una prueba anti-honeypot deliberada son unas pocas decenas: diecisiete de la batería, dos con __hp_, el visitante de Jerusalén, y los escáneres web que hacen el chequeo de soft-404 por higiene y no por sospecha. De esas, las que cambiaron de conducta al descubrirme son, hasta donde puedo ver, una: el visitante. La batería subió su minero igual. Los validadores entregaron catorce mil credenciales falsas a su dueño. mdrfckr recogió que ls no existía y siguió con la misma rutina desde dos mil IPs distintas.
La lectura ingenua de esto es que los atacantes son torpes. No lo creo. Para una operación de volumen, distinguir un señuelo cuesta tiempo por destino y no ahorra nada: el costo de tirar un minero en un Cowrie es un archivo perdido; el costo de saltarse un servidor real por un falso positivo de la detección es un servidor perdido. Con esa asimetría, la decisión racional es no mirar. Los honeypots de interacción media siguen siendo útiles por la misma razón por la que son fáciles de descubrir: a casi nadie le conviene descubrirlos.
10. Indicadores#
| Tipo | Valor | Contexto |
|---|---|---|
| IPv4 | 20.80.86.196 | AS8075 Microsoft (Azure) · detector __hp_ · 14-sep |
| IPv4 | 171.244.14.96 | AS38731 Viettel · login __hp_…_x__ · 21-sep |
| IPv4 ×17 | ver bloque de abajo | AS47890 / AS48090 · batería check_<md5>_small_* · 24-oct → 11-nov |
| IPv4 | 61.220.127.240, 93.170.12.102, 179.41.10.172, 154.66.109.229, 118.195.163.19 | validadores de credenciales (echo -e "\x6F\x6B") |
| IPv4 | 86.38.182.193, 104.28.254.18, 104.28.222.18 | creación del contenedor KANIT (Docker API) · 26-sep |
| Cadena | check_[0-9a-f]{32}_small_(control|math|pid|positional|function|done), check_[0-9a-f]{32}_transport_done | batería de fidelidad |
| Cadena | __hp_, /tmp/.hp<hex8>_a, COWRIE_DIR:, TCP_LISTEN_22: | detector de honeypots |
| Cadena | echo KANIT=$((98765*4321)) · imagen ccimp:1 | canario de ejecución en Docker |
| SHA-256 | 7f2bc9699faa7e5d5f1962dfe4c1baa5651efa8d38c523bae552a053f0e0e4c4 | cuerpo POST /containers/create con KANIT |
| Credencial | root:!root | login de la batería (17/17 IPs) |
| HASSH | 2ec37a7cc8daf20b10e1ad6221061ca5 | cliente SSH-2.0-Go de la sesión 1b626ba8 |
193.32.162.84 195.178.110.217 195.178.110.227 195.178.110.228 195.178.110.232
2.57.122.150 2.57.122.168 2.57.122.209 2.57.122.74 80.94.92.179 80.94.92.234
80.94.92.55 92.118.39.14 92.118.39.49 92.118.39.50 92.118.39.71 92.118.39.77
Ninguna de estas IPs es “un honeypot hunter” en sentido estricto, salvo quizá las dos del prefijo __hp_. Las de la batería, en particular, son las mismas que después subieron el minero: bloquearlas por la batería o por el minero da igual.
11. MITRE ATT&CK#
| Táctica | Técnica | Dónde |
|---|---|---|
| Evasión de defensas / Descubrimiento | T1497.001 Evasión de virtualización y sandbox: chequeos del sistema | batería check_*, detector __hp_, KANIT |
| Descubrimiento | T1082 Descubrimiento de información del sistema | /proc/version, uname, /proc/loadavg, lscpu |
| Descubrimiento | T1057 Descubrimiento de procesos | ps | grep '[Mm]iner', /proc/1/cmdline |
| Acceso inicial | T1078 Cuentas válidas | validadores de listas (39.911 logins) |
| Acceso a credenciales | T1110.004 Fuerza bruta: credential stuffing | validadores de listas |
| Acceso a credenciales | T1552.001 Credenciales en archivos | tdata de Telegram, D877F783D5D3EF8Cs |
| Reconocimiento | T1595.003 Escaneo activo: escaneo por diccionario | canarios web antes del barrido (Odin, Nmap, GCP) |
12. Detección#
En un servidor real, la batería y el detector dejan huellas muy específicas en la línea de comandos, y nadie con un trabajo legítimo escribe check_<md5>_small_math:
title: Batería de fidelidad de shell o detector de honeypots
id: 3f9c2a61-6b7e-4f0e-9d0a-5c1b8e2d4a17
status: experimental
logsource:
product: linux
category: process_creation
detection:
bateria:
CommandLine|re: 'check_[0-9a-f]{32}_(small_(control|math|pid|positional|function|done)|transport_done)'
detector:
CommandLine|contains:
- 'COWRIE_DIR:'
- 'TCP_LISTEN_22:'
- '/tmp/.hp'
condition: bateria or detector
level: high
tags:
- attack.defense_evasion
- attack.t1497.001
La API de Docker sin TLS habla HTTP en claro, así que el canario de KANIT se ve en la red:
alert http any any -> $HOME_NET [2375,2376] (msg:"Docker API: canario de ejecucion KANIT"; flow:to_server,established; http.method; content:"POST"; http.uri; content:"/containers/create"; http.request_body; content:"KANIT="; classtype:attempted-recon; sid:9115010; rev:1;)
alert http any any -> $HOME_NET [2375,2376] (msg:"Docker API: carga de imagen remota"; flow:to_server,established; http.method; content:"POST"; http.uri; content:"/images/load"; classtype:attempted-admin; sid:9115011; rev:1;)
La segunda regla es más ruidosa y más importante: que alguien de fuera cargue una imagen en tu Docker ya es un incidente, pruebe lo que pruebe después.
Eso no se aplica al único caso que leyó las respuestas, y ahí está la parte que me deja inquieto. La detección no cuesta lo mismo para todos. Para un script que recorre una lista, mirar es caro. Para un bucle que de todos modos lee cada salida para decidir el paso siguiente, notar que $((314*738+96)) no da 231828 es gratis: ya estaba leyendo. Cuanto más del ataque haga algo que lee, menos va a servir un señuelo que solo engaña a quien no mira.
La entrega siguiente cambia de lado: ya no los que me pusieron a prueba, sino los que vinieron a reclutarme para su botnet, y lo que se puede sacar de un binario ARM que su autor olvidó despojar de símbolos: Ochenta y seis días de señuelo · III — Botnets de alquiler.
commit 0x231828
Date: 2026-11-11 00:01 AM
test: 314*738+96 -> command not found; deploying anyway