Euskal Encounter 34 · Solve It 3
Solve It 3 — Retro Shell
- #ctf
- #ia
Algo se esconde en la siguiente shell.
Cómo se resolvió
-
El cliente solo manda espacios
No hay fichero que abrir: hay un terminal en el navegador. Su código cabe en una pantalla y solo escucha la barra espaciadora. Ni caja de texto, ni Enter, ni forma de escribir una palabra.
-
El duelo no es el puzzle
El servidor recibe con una frase del Sword Master y parece pedir la réplica de rigor. Probamos las canónicas y setenta y dos variantes de puntuación: contesta lo mismo a todas. Nunca compara texto.
-
Lo que mide es el ritmo
Si el contenido da igual, algo tendrá que mirar. Barriendo la cadencia aparece la ventana: cada dos segundos rechaza, cada segundo responde sostenido durante todo el minuto, en ráfaga vuelve a rechazar.
-
El nombre era el método
Pulsar al compás, no responder al insulto: eso es lo que pide, y lo que la contraseña dice con todas sus letras. La condición exacta de victoria no llegó a quedar en ningún log.
Contraseña t4pF1gHtINg1nINmELEEiSLAND
Para «Retro Shell» no te descargas nada. Abres una web y te encuentras un terminal viejo —blanco sobre negro, con el marco de un gestor de ventanas de los noventa— que parpadea y te insulta. Un reto sin fichero es un reto que vive en el otro extremo del cable: todo lo que hay que entender está en cómo se comporta el servidor, no en un binario que puedas abrir con calma en tu máquina.
Y lo primero que hace ese servidor es picarte. Nada más conectar suelta una frase con mala leche —«tienes los modales de un pordiosero»— y se queda esperando con un >. Cualquiera que haya jugado al primer Monkey Island la reconoce: es el duelo de espada, donde ganas no con el acero sino con la réplica más ingeniosa. Así que hicimos lo obvio, buscar la respuesta correcta. Y ahí perdimos el tiempo, porque el duelo era un decorado.

Reconstrucción local del front del reto —el servicio original ya no está en pie—. El insulto de bienvenida, el prompt y la hilera de respuestas son los que devolvió el servidor real, tap a tap.
El título ya lo avisaba, aunque tardamos en oírlo. «Retro Shell»: una shell. Una shell que, cuando lees su cliente, resulta que solo entiende una tecla. Ese detalle —que parece una limitación cutre— era el reto entero.
El cliente solo sabe enviar espacios
Antes de tocar el servicio, lee su cliente. El app.js que sirve la web cabe en una pantalla y lo dice todo:
function sendTap() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(' ');
}
}
window.addEventListener('keydown', (e) => {
if (e.code === 'Space' || e.key === ' ') {
e.preventDefault();
sendTap();
}
});
El único evento que existe es la barra espaciadora, y lo único que viaja por el socket es un carácter: un espacio. No hay campo de texto, no hay Enter, no hay nada más. La página recibe lo que llega y lo pega en pantalla; no lo interpreta.
Eso no es una carencia, es una decisión de diseño. Cuando el cliente oficial de un reto está restringido a un único tipo de input, ese canal es el mecanismo. Lo anotamos y, como se verá, tardamos de más en volver a leerlo.
Para conectarse desde fuera del navegador hay un detalle práctico: el servidor valida el header Origin y responde 403 sin él. Con el header correcto, un WebSocket normal entra.
import asyncio, websockets
async def main():
hdrs = {"Origin": "http://hackit.party.eus:7826"}
async with websockets.connect("ws://hackit.party.eus:7826/ws",
additional_headers=hdrs) as ws:
...
Los insultos son el decorado
El banner de bienvenida dice «EE34 HAL terminal», que apunta a 2001. Pero la frase que viene detrás no: es un insulto del duelo de espada de Monkey Island. Abriendo conexiones sucesivas recolectamos el pool: dentro de una misma conexión el insulto es fijo, y cambia de una conexión a otra. Son las frases canónicas del Sword Master del primer juego.
La lectura se impone sola: HAL te insulta, tú respondes con la réplica ingeniosa que toca. Y por ahí atacamos a fondo. Montamos el solver del duelo —duel.py, con los 31 pares de insulto y réplica— y le mandamos las respuestas correctas. El servidor contestó ?. Sospechamos que el problema fuera de puntuación o de formato, así que generamos las variantes de cada réplica con y sin ., ! y ? —72 strings en total, en brute.py— y las probamos sobre el insulto fijo de una conexión, con distintos finales de línea. Todas: ?.
De ahí sale la conclusión que desatasca el reto, y conviene decirla sin rodeos: el servidor nunca compara strings. El ? no significa «réplica incorrecta»; significa «no reconozco tu entrada», y lo devuelve para cualquier texto que le mandes. Los insultos, el Sword Master, el HAL: todo eso es la carcasa. No hay nada que teclear.
Lo que mide es el ritmo
Si el texto da igual, algo tendrá que estar mirando. Bastó con barrer la cadencia: dos probes idénticos salvo por el intervalo entre pulsaciones. Sus logs son los de abajo, capturados contra el servicio real.
A un espacio cada 2,0 s (rawtap.log) el servidor rechaza sin descanso:
0.00 [T] 'EE34 HAL terminal\r\n'
0.00 [T] '\r\nYou have the manners of a beggar.\r\n> '
2.56 [T] '?'
4.56 [T] '?'
6.57 [T] '?'
8.57 [T] '?'
A un espacio cada 1,0 s (slowtap.log) cambia por completo: responde con una a sostenida, estable durante todo el minuto que dura el probe:
0.00 'EE34 HAL terminal\r\n'
0.00 '\r\nIf your brother is like you, better to marry a pig.\r\n> '
2.46 'a'
3.21 ' '
4.47 'a'
5.22 ' '
6.48 'a'
7.23 ' '
Y en ráfaga, rechaza otra vez. Puestas en una tabla, las tres cadencias:
| Cadencia de pulsación | Respuesta del servidor |
|---|---|
| un espacio cada 2,0 s | ? |
| un espacio cada 1,0 s | a sostenida |
| flood (ráfaga de espacios) | ? |
O sea: hay una ventana. Ni rápido ni lento, al tempo. El servidor no lee lo que escribes: cronometra cada cuánto pulsas y te dice si mantienes el ritmo. Es un watchdog de cadencia disfrazado de shell.
Uno de nuestros probes es instructivo por lo que hace mal: ata cada envío a la respuesta del servidor y, como el servidor contesta rápido, acaba mandando espacios casi sin pausa. Se floodea a sí mismo sin querer y solo ve ?. El bug del script imitaba justo el error de medición que nos costó el reto: confundir el ruido de nuestro propio probe con la respuesta del servidor.
Y ahí encaja todo hacia atrás. El duelo de Monkey Island nunca fue el puzzle: era la pista sobre el método. Esto es un tap fight, un pulso a la barra. «Retro Shell» tampoco mentía: una shell que solo entiende pulsaciones. El mecanismo estaba en el cómo —pulsar al ritmo—, no en el qué —el tema de los insultos—, y la contraseña lo nombra literalmente.
El hueco que no maquillamos
Aquí toca ser explícitos, porque el writeup se queda sin final limpio. No sacamos la contraseña de estos probes. Caracterizamos el canal, encontramos la ventana de ritmo y llegamos a la hipótesis correcta —esto es un cronómetro de cadencia, no un duelo—, pero el cierre lo puso un compañero por su cuenta, fuera de los probes que quedaron guardados, y el servicio ya no está en pie para reproducirlo.
Lo que sí está verificado y se lee en los logs: el cliente solo manda espacios, el servidor no compara texto, el ? es su rechazo genérico, y una cadencia de un segundo produce respuesta sostenida mientras que dos segundos y la ráfaga no. Lo que no podemos documentar es la condición exacta de victoria: cuántas pulsaciones hay que sostener, con qué tolerancia, y cómo se emite la clave una vez que aciertas el ritmo. No está en ningún log y no es comprobable hoy. Preferimos decirlo a inventar un final coherente.
Solución:
t4pF1gHtINg1nINmELEEiSLAND
Leet de tap fighting in Melee Island: pelear a base de pulsaciones al ritmo, en la isla del primer Monkey Island. La contraseña describe el método, no el tema. Como todo lo demás en este reto.
El material — los probes, los logs de las sesiones contra el servidor y el writeup técnico completo de este nivel están en euskal34/solve-it/3.