Very unique CTF! Alongside challenges that got you to work with provided files, there was an OSCP-virtual-labs-style network with both external-facing and internal developer and admin subnets. Network reconnaissance and pivoting had to be performed. Unfortunately, I solved little challenges as I only started seriously on the second day :) The delays and infrastructure issues doused the flames of my enthusiasm…
|CTFtime.org Event Page||https://ctftime.org/event/1301|
Triskel 1: First contact
Coronavirus affected our airport so much that our dev team developed an app to keep track of it! I mean they didn’t have much time to make it, but what could go wrong?
After discovering the two IPs pinging us in the initial
Discovery challenge, we find a web service running on one of them —
10.35.2.134 — on port
8100 after running a nmap scan for all ports.
The website appears to track medical news in and outside of Brittany. There is a login field but it is of no use to us at the moment.
What’s interesting is that on the ‘Outside of Brittany’ page, the news is fetched by an API call to an endpoint on an internal subnet
10.0.42.0/24 that we do not have direct access to.
Perhaps we can utilize this SSRF vulnerability to first scan the internal subnet and see what kind of other endpoints exist, as suggested by my teammate zeyu2001.
Thus, we use Burp Intruder with a ‘Sniper’ payload that loops from 0 to 255 for the last octet of the IP address.
Our scan results returned us 3 IP addresses that did not give a “Cant connect to API” error.
.2 is the website itself,
.100 is the API endpoint we saw earlier, and
.200 is what seems to be a chat page containing the flag.
Triskel 2 : Going in
What did you do? You shouldn’t have access to this chat, but you can’t do anything from it right?
On the internal chat page there is a username search at the top which basically filters the chat by username.
Unlike the message-sending form at the bottom that does not work, the search is functioning and is done through a GET request to
I decided to fuzz the search parameter with Burp Intruder to see if it was processed in any strange way.
For payloads that did not match any usernames in the chat, they were simply reflected unsanitized in the output.
Upon closer inspection, I noticed that payloads containing a single quote
' additionally returned a
No Data :( error message.
This seemed like an SQL injection vulnerability where generic error messages are returned.
But before I was able to test bigger queries, I also noticed that whitespaces (represented with
+ when URL-encoded) were blacklisted since they abnormally gave a
400 Bad Request message despite the server responding with
Bypassing this with inline comments, we can demonstrate a successful injection with an
OR query that returns all chat rows since the condition
'1'='1' is always true:
My teammate zeyu2001 then went on to retrieve admin credentials from the database which was
admin_richard_lauren:D.!uTra+b+wrUVH5s?^AE3a~X. After login, the flag is displayed in the admin management page.
Triskel 3 : Dead end
You are admin now… Anyway now you can’t access any information or have more privileges so I guess it’s the end of your journey haha!
Clicking on the
Not available link tries to load a document with the URL parameter
?document=1 but the contents returned are also
Inspecting this under the hood, we see that a
confidential_documents cookie is set with value
kontammadur_klanvour.prod.local%3A5001%2Fdocuments, which URL-decodes to
kontammadur_klanvour.prod.local:5001/documents. Thus, it is reasonable to conclude that the documents are fetched from this internal production endpoint.
However, as we have seen on the admin management page, it is currently “Temporary down” and “Not available”. This is likely because a dev “closed the prod” as written on the internal chat page
10.0.42.200 from Triskel 2 : Going in. So maybe we can modify the cookie to point to the internal dev endpoint instead? Changing the cookie to
kontammadur_klanvour.dev.local:5001/documents successfully returns the list of documents.
From there, the important document we should view is
?document=1 which complains about the disabling of the dev console PIN.
With the green-highlighted text
API Powered by Werkzeug so prominently plastered on the admin management page, some of us will immediately make the connection that document’s content refers to the Werkzeug debug console which is usually accessible at
/console if enabled. We modify the cookie to be:
and we are greeted with the Werkzeug Debugger Console.
Looking at the debugger’s source code here https://github.com/pallets/werkzeug/blob/main/src/werkzeug/debug/shared/debugger.js, we can see that the PIN is disabled through the
EVALEX_TRUSTED = true option.
I didn’t know about this ssrf_proxy tool during the CTF, so I just thought of setting the proper debugger URL parameters directly in the cookie url like so:
This loaded the
debugger.js successfully, meaning that we can proceed to use this method to send console commands without using the UI.
With reference to the
handleConsoleSubmit function source code:
and a truncated sample of this Werkzeug RCE POC https://github.com/its-arun/Werkzeug-Debug-RCE/blob/master/werkzeug.py :
we can figure out, that to send a console command and achieve RCE through Python, we just need to format the cookie with 4 URL parameters to
/console this way:
cmd contains the Python code,
frm can be set to
s is the
SECRET from the debugger option variables seen earlier. Something to take note of is that more complicated system commands ran under
popen() should be double-URL-encoded.
Anyway, our above request returns with the output of
root, which proves high-privilege RCE.
ls -la / and spotting a
/, we grab the flag’s contents with:
That’s all folks!