In the compressed archive there are 4 important files that we must understand to solve this challenge
run.sh contains the following lines:
exec socat "TCP-LISTEN:80,reuseaddr=1,fork" "EXEC:./fw.rb simple ./serve_file,su=nobody,nofork" 2> >(tee -a ../www.log)
We can completely ignore the '2>>(tee -a ../www.log)' portion of this script. This script calls socat to handle the server socket code, and tells it to pass the data to fw.rp, with the arguments simple and ./serve_file.
As socat calls fw.rb, lets examine that next.
fw.rb contains 4 classes
The first two behave as expected. The HTTPRequest class basically parses out the HTTP Request, and each of the HTTP headers. The first design flaw is in this class. See if you can find it before moving on.
The Firewall class is a simple framework that requires a plugin in order to be fully functional. The plugin replaces/extends the test method, with some logic to determine if a request should pass through. If the test method doesn't say the method was acceptable, we'll see a 'HTTP/1.0 403 Forbidden\r\n\r\nForbidden' message. This will be useful for debugging out attacks to determine wither the firewall blocked them, or if they failed for some other reason.
The rest of the script takes the first argument, which contains the plugin and evals it. Eval is a nasty call in any language, but this eval doesn't involve user input, so its safe. The script then runs the firewall.test() and if successful rewinds stdin and executes serve_file.
The plugin, simple.rb, essentially performs a regex check on the request and hostname to make sure there's nothing funny going on. The fact that it's only checking these two areas is a clue. Here we find the second design flaw. Again see if you can find it before moving on.
Finally we come to serve_file.c, it contains transmit(), read_line(), and log_request().functions, along with a main() function. The helper functions behave mundanely, with the meat of the logic being performed in the main() function.
The main() function reads the request, and then searches for a host header, upon finding a host header it stops reading the HTTP header, changes to the www-data directory, then a "host" directory as defined by the host header, and opens the file defined by the request. If the file doesn't exist or is empty, it returns a 404 error. If the file exists, it happily replies with the contents. In this function contains the multiple design flaws. See if you can spot them before proceeding.
Hopefully you've found all three design flaws which are as follows:
- HTTPRequest doesn't check if a header value is already set before assigning another value to a header. In other words, we can double up headers and overwrite previous header information.
- Simple.rb doesn't check for multiple Host headers. Either to examine, or fail on the event of there being more than one. Of course Simple.rb would need HTTPRequest to store multiple entries of headers.
- serve_file.c trusts the host header and doesn't chroot the environment.
- serve_file.c doesn't read every header line, and stops processing on the first instance of the Host header.
Had the first and second flaws been designed differently, or the third, or the fourth, we would not be able to attack in this manner. Multiple points of failure had to occur for this.
For building the request, we need two host headers. The first one will be a directory traversal, tricking serve_file.c to open a much different directory. The second one will be clean, to overwrite the bad header in fw.rb.
Now, what file should we go for? As seen in the run.sh we should be running with nobody privileges. I went directly for /etc/passwd, because passwords, but it could have been /flag.txt or something involving 31c3, I got lucky. The following is my request.
GET /passwd HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36
User flag at the end of the file contains the flag.
I prefer to use ncat (which comes with nmap), instead of normal netcat due to the additional features included, The -C option used here overrides the OS return convention with CRLF, as the normal LF method didn't appear to work.