DEFCON CTF Quals
Yes… DEFCON is really cancelled this year, nevertheless I did not miss the chance to play the DEFCON CTF Quals. I participated with the team Sauercloud and spent most of the time with the pooot web challenge. Unfortunately, we were not successful in solving the challenge before the end of the Quals, but I continued and eventually figured the solution out. To document the solution for myself and hopefully help anyone who did not solve the challenge, here is my writeup.
What do we have?
The web is becoming more and more dangerous everyday. Our secure pooot proxy allows you to continue your browsing securely and hide your IP address from your visited websites! Give it a swing here: pooot.challenges.ooo
When we open the website, we see a field to enter a URL and surf anonymously. After submitting, the website renders the corresponding website. The first interesting hint is the comment in the HTML that leads us to the source of the website which I uploaded here.
The website uses Flask and provides the following endpoints:
The domain/path URL uses python requests to
GET the requested website and modifies all links to work with the proxy. Afterwards it renders the content. The feedback path is used to give feedback about a broken page. It uses a Redis queue to schedule the tasks. The whole service is hidden behind a proxy.
What are we looking for?
The first idea that we had was to use SSRF to communicate with Redis. So, we set up a nginx server which replied with a redirect to pooot.challenges.ooo/<my server> and it worked. We received two requests. However, as soon as we tried to redirect to an internal IP, we received the error “Internal IP address […] not allowed”. The server checks if the request is sent from 172.25.0.100, a local IP, and since our origin address is from our own PC, the request is blocked.
client_ip = request.headers.getlist("X-Forwarded-For")
client_ip = request.remote_addr
protocol = “http”
if client_ip != “172.25.0.100”:
app.logger.error(f”Internal IP address [...] not allowed.” )
return “Internal IP address not allowed”, 400
Next, we wondered what the workers job is.
from worker import task
url = re.sub(r'http[s]*://', '', form.url.data)
job = q.enqueue(task,url)
When we submitted our server domain in the form, a request appeared in our nginx access log. Next, we tested by replying an html document (below) executing JS to request our server — XSS. And yes, we received two requests. The html is called by a headless chrome that executes the JS. From this point, we have a request initiated by the internal browser. Consequently, we can call /<IP> and would pass the internal IP check. Now it was time to find and speak to Redis… Well, I will skip this part, as it led us to a dead-end from which we would never escape...
After the CTF ended, someone in Discord suggested combining the XSS in the workers task with a service worker to check other URLs that are visited. This led me to research service workers, a concept I was previously unaware of. Service workers are essentially a proxy on your website/in your browser that can intercept requests from your browser and pull them from the cache. What it can also do is, it can send the request or information about the request to another server!
I crafted a service worker, registered it for the task and immediately received another request. In the access log below, we can see that the headless chrome also visits 172.25.0.102:3000. Using the method above, I requested this IP and I finally had the flag!!!
The challenge was great fun and I learned a lot. And that is for me the most important part — I love to learn new things when playing CTFs. Thanks to Order of the Overflow for organizing the challenges and thanks to my team who showed me many cool tricks. Keep on hacking!