Continuing my write-up series from BSides SF's CTF, today I'll be looking at a "pwn" challenge, Steel Mountain: Sensors.
The challenge starts with a link, and a cryptic comment:
Steel Mountain's environmental control systems have some flaws. What's going on with the sensors?
Navigating to the URL, we are greeted by a blueprint of Steel Mountain (points deducted BSidesSF for not using comic sans :D), with a number of sensors running:
If we look at the web requests/responses driving the sensor stats, we see a HTTPS request to the following URL:
Reviewing the URL, and testing for common vulnerabilities, we find that the following URL's are equivalent and return the same response:
This means there is a good chance that the sensor parameter is being used to build a path. Interestingly, we notice that the following URL results in a 403 Forbidden error being returned:
This could be due to directory permissions, but we can rule this out with the following request:
This should result in a non-valid path, and yet we still receive a 403 Forbidden error, smells like a WAF.
Let's do some information gathering on the site itself. A quick look at "/robots.txt" shows the following:
Disallow: /firmware Disallow: /flag.txt
Obviously /flag.txt is what we will be attempting to extract (and no, https://steel-mountain-d2fcf1e0.ctf.bsidessf.net/flag.txt doesn't provide the flag :D), but what is in the /firmware/ directory:
This gives us access to 2 firmware files, "sensor" and "setpoint". For this challenge we are looking at "sensor", so we need to fire up radare2 and start disassembling.
Listing the available symbols, we see references to functions prefixed with "cgi...", for example: "cgiMain". A quick Google search shows a framework named cgic which exposes the same functionality, so there is a good chance that this binary was built using this framework. Reviewing the documentation, we find the following:
Since all CGI applications must perform certain initial tasks, such as parsing form data and examining environment variables, the cgic library provides its own main() function. When you write applications that use cgic, you will begin your own programs by writing a cgiMain() function, which cgic will invoke when the initial cgi work has been successfully completed.
Knowing this, we will start our analysis at cgiMain() and see what the sensor CGI application is doing with our passed data.
A symbol stands out as being interesting, "loadSensorConfig", and upon disassembling this we see that we were right about the WAF:
A limitation of this filter however is the search for the prefix "/", which means we can actually bypass this and jump into the parent directory by using "../". This will become important later.
Next, we see that our provided parameter is involved in a snprintf() call with the following format string:
Knowing this, we can check our directory traversal by making a CURL request:
Cool, we get the same result, so now we can access files outside of the directory. Unfortunately however, as we see in the format string, ".cfg" will be appended to our path. We will need to bypass this to grab the flag. Attempting the usual %00 NULL byte doesn't seem to work, so we need to dig further.
Just below the snprintf() call, we see a strcspn() call, which has the following signature:
The "size_t" returned is the offset of the first occurrence of str2 within str2. The address of 0x4044cc is being used as the "str2" parameter, let's see what this call is searching for:
So we now know that a "\r\n" (0x0d0a) is being searched for, and a NULL character is being added to terminate the string if found. With another CURL request, we can make sure that this is the case:
Awesome, now lets have a go at grabbing the flag.txt:
Close, but we receive a HTTP 400 Bad Request response. We know that we are able to request the file as looking for a non-existent file will return a HTTP 404 response. Let's go back to our disassembly.
After more searching, we find a function named debug_printf(), which is used to output debug strings from the application. Within this function, we see this:
So it appears there is a "debug" parameter that can be passed to trigger this functionality. Let's combine everything and give it a try: