VnutZ Domain
Copyright © 1996 - 2024 [Matthew Vea] - All Rights Reserved

Featured Article

Detecting Captive Portal Delivery on Hak5 Pineapple

[index] [3,084 page views]
Tagged As: Hacking, How To, Pineapple, and Security

I recently wrote about some fixes for the Captive Portal module on the Hak5 Pineapple to make sure redirection and credential capturing was effective. But what if you're trying to do something else - like use the Captive Portal as a means of delivering a message onto targets? Simply delivering the Captive Portal is easy, but it's hard to know when it's displayed and not interacted with.

My 2018 and 2019 information operations campaign for Army-Navy week.

Now, every red-blooded American should root for Army over Navy. For the most recent Army-Navy week, I walked around the Naval Academy utilizing a Pineapple Tetra to grab local mobile phones and deliver a "Beat Navy" message to the midshipmen and locals. It turns out the effort was largely fruitless as the Naval Academy does not make the midshipmen form up outside from late fall through early spring because it's "too cold." Pussies.

No matter what message you intend to deliver, there are a variety of small edits you need to make throughout the Pineapple to make it possible. In a nutshell, configure PineAP, configure EvilPortal, redirect all DNS back to the Pineapple, parse the logs, render a notification to the dashboard.

Craft The IO Message

In reality, only one file is actually necessary to edit, the index.php base file. However, giving a user a button to click, in this case allowing folks to choose "Go Army" will take them to a second page which was needed to have a PHP file run to log a notification onto the Pineapple dashboard. But in reality, people were highly unlikely to click the button.

To enable some tracking, though, you'll want to make sure the index.php file is loading an innocuous image. For this, I created a small, transparent, single-pixel .png image. Save it as portal.png and just drop it anywhere on the page.

<img src="portal.png">

The image is important for detecting if the captive portal page actually rendered. Mobile devices and browsers all utilize variations of web addresses to determine connectivity, looking for something simple like the word Success to be returned. The following are some sample URLs that various devices and browsers attempt to access for connectivity determination:

  • *

When the site is inaccessible, the device will render the captive portal login page for the user to authenticate for access. Images are only loaded in this case and will therefore be detectable in the logs to know the page was rendered. If you were to parse the logs just for page accesses, you may find the mobile devices and browses are doing connectivity checks at a high frequency which would flood the notification panel with false positives.

Redirecting the DNS Entries

While the CaptivePortal should automatically display a message, it doesn't always work. Inevitably, the target is going to use their mobile device for Internet browsing in which case you can redirect their web traffic to your desired page anyway by poisoning the DNS response. The Pineapple should already have dnsmasq installed as a dependency. If not, it can be installed from the modules interface.

Getting dnsmasq to redirect all addresses to the local Pineapple at is relatively easy and simply involves editing the /etc/dnsmasq.conf configuration file. SSH into the Pineapple and add the following line to the top of the file.


Then reload the dnsmasq service to get the updated configuration into play.

root@TETRA:/etc# service dnsmasq reload

Enabling NGINX Logging

By default, the NGINX server on the Pineapple does not log. This was probably a decision to keep CPU usage down, IO bus consumption down on the SD cards, and file space considerations under control. Nevertheless, logging needs to be on to discern the difference between just accessing the index.php file and the rendering of the portal.png image. The configuration file can be found at /etc/nginx/nginx.conf.

  server {
    listen       80;
    server_name  www;
    error_page 404 =200 /index.php;
    error_log /tmp/log/nginx/error.log;
    access_log /tmp/log/nginx/access.log;

As with the DNS configuration, reload the NGINX service to get the updated configuration into play.

root@TETRA:/etc# service nginx reload

Now the logs will include lines such as: - - [20/Dec/2019:20:52:34 -0500] "GET / HTTP/1.1" 200 867 .... - - [20/Dec/2019:20:52:34 -0500] "GET /portal.png HTTP/1.1" 200 103 ....

Detecting A Page Render

Parsing the /tmp/log/nginx/access.log file is remarkably easy. A little usage of the grep and awk utilities will identify the image file, access times, and IP addresses.

# ensure the files exist
touch /tmp/ep_detected.log
touch /root/ep_notified.log
# parse the logs for relevant entries
cat /tmp/log/nginx/access.log | grep portal.png | awk '{print $4$5 " " $1}' > /tmp/ep_detected.log
# generate notifications

But while this is a start, it's still not entirely useful unless you're continuously interacting with the Pineapple on the terminal. (NOTE: Although you could simply save the log to a persistent folder instead of /tmp for later analysis - but that's not as fun). Save the commands to a script file and configure cron to execute it every five minutes.

Still, consecutive runs of the script will produce accesses you already knew about. Unfortunately, the Pineapple does not have the traditional diff or fc utilities to compare before and after logs. So, as Deadpool would say, this will require "Maximum Effort." Just utilize a simple Python script to compare the current extracted data - ep_detected.log - with the addresses already reported - ep_notified.log.

import subprocess

# Open Logged Items
fhl = open("/root/ep_notified.log", "r")
lines = fhl.readlines()
# Build a List of Already Reported Renders
notifications = []
for line in lines:

# Open Detected Items
fhd = open("/tmp/ep_detected.log", "r")
lines = fhd.readlines()
# Build a List of Known Renders
detections = []
for line in lines:
  # Test for Matches and Exclude
  if not line.rstrip() in notifications:

# Send Notifications
for line in detections:
  ip = line.split(" ")[1].rstrip()
  # build a command string
  command = "grep " + ip + " /tmp/dhcp.leases | awk '{print $4}'"
  # grab a hostname from the DHCP pool
  host = subprocess.check_output(command, shell=True)
  # build a command string
  command = "notify '[EP]' " + host.rstrip() + " " + ip
  # use the Pineapple "notify" command
  result = subprocess.check_output(command, shell=True)

# Append New Items to the Logged Items
fhl = open("/root/ep_notified.log", "a")
for line in detections:
  fhl.write(line + "\n")

Save this as and have it run as part of the cron script. The script utilizes two files, the ep_detected.log file generated from parsing the server log file and the ep_notified.log file which keeps track of which items were already displayed on the Pineapple Dashboard. The two lists are compared to determine any new items. It quickly checks the IP addresses from the log against the Pineapple's /tmp/dhcp.leases file to find hostnames. Then, the Pineapple's /usr/bin/pineapple/notify utility is used to actually submit a text string to the Dashboard for each new access.

Pure Shell Scripting

EDIT: The python solution worked, but it imposed too high a processing cost even when run every five minutes when the Pineapple was under a load. The following substitution can be made for the final line that called python

cat /tmp/ep_detected.log | while read line; 
  if grep -F "$line" /root/ep_notified.log; then
    printf "[*] Found - $line\n"
    printf "[!] Not Found - $line\n"
    ip=`echo $line | awk '{print $2}'`
    mac=`grep $ip /tmp/dhcp.leases | awk '{print $2}'`
    host=`grep $ip /tmp/dhcp.leases | awk '{print $4}'`
    /usr/bin/pineapple/notify "[EP] " $host " " $ip " " $mac
    echo $line >> /root/ep_notified.log

This modification will read the freshly created ep_detected.log file line by line and then use grep to locate whether the entry already exists in the ep_notified.log file. If its a new entry, the script will extract the IP address and use grep and awk commands to parse the Pineapple's DHCP leases for a hostname. The combination of hostname and IP address will be posted to the Pineapple's notification system. Lastly, the new entry, denoted by $line will be appended to the notification log. This should significantly reduce the processing burden on the Pineapple.

Notifications now appear in the Pineapple Dashboard.


  • The PineAP module captures nearby devices.
  • The EvilPortal module provides iptables traffic redirection to a locally hosted index.php page to render to targets.
  • The DNSmasq module redirects any devices that don't render the Captive Portal to the locally hosted webserver anyway.
  • The transparent pixel image allows detecting actual page renders.
  • The grep and awk utilities parse the web server logs for actual renderings.
  • The Python shell script handles de-duplication and pushing notifications to the Pineapple Dashboard.

All of these little components together allow you to know whether a Captive Portal page is rendered even if the targeted user does not interact with it. This allows you to know when things like a "Beat Navy" message, or other messaging campaign content, are in fact displayed on their screens.

More site content that might interest you:

So ... is NASA just recruiting crazy astronauts?

Try your hand at fate and use the site's continuously updating statistical analysis of the MegaMillions and PowerBall lotteries to choose "smarter" number. Remember, you don't have to win the jackpot to win money from the lottery!

Tired of social media sites mining all your data? Try a private, auto-deleting message bulletin board.

paypal coinbase marcus