Captive Portal

#White Hacking: Wemos & Captive Portal using MicroPython

At the beginning of October I wrote a blog article called White Hacking: WeMos and SquirelCrawl!. I used the WeMos (ESP32) and the firmware provided by Hacker Arsenal to do a captive portal. As you known, a captive portal is a web page which is displayed to newly connected users before they are granted broader access to network resources [

This time, I've implemented the captive portal (system) using MicroPython on the ESP32. This implementation allows you to upload more than one file. This means, e.g. you can upload CSS and JavaScript files for better web page style and more interaction with the user (e.g. using jquery). However, you should not forget that the WiFi speed of the ESP32 as an access point is very limited. You should always optimize the web page!

Python Code:

Portal optimization is required! This is only a sample and a very beta code. The portal is far away from stable :S, but it works.


I assume no responsibility for the usage of this code and post. I repeat again, the book "The Hacker Playbook 2: Practical Guide to Penetration Testing - Peter Kim" says

Just remember, ONLY test systems on which you have written permission. Just Google the term “hacker jailed” and you will see plenty of different examples where young teens have been sentenced to years in prison for what they thought was a “fun time.” There are many free platforms where legal hacking is allowed and will help you further educate yourself.

How does a Captive Portal work?

DNS implementation

There is more than one way to implement a captive portal. I used the "redirect by DNS". Every time a client requests a website, a DNS query is started. I implemented a DNS server that answers all the DNS lookups returning the IP address of the ESP32 and thus, as a result, the captive portal page.

How to get the "connect to a network" message?

Every time that you connect to a hotspot, you get usually the "connect to a network" message. This is triggered in different ways depending on the OS of your device. I used Wireshark to see the package communication (I connected an external WiFi interface to my computer and I configured the interface as an access point) and I saw the following:

Windows searches for the following web addresses and expects the answers with the described texts.

Address                                           Answer Code             Text
*                HTTP/1.1 200 OK         Microsoft NCSI
*         HTTP/1.1 200 OK         Microsoft Connect Test
*                HTTP/1.1 302 Redirect

All other traffic should be redirected to the captive portal IP.

Android searches for the following web addresses, if the answer code is 204 (no content), there is no captive portal between the device and Internet. That means, if you want to have a "connect to a network" message, you can answer these requests with a code 200 (ok). The gen_204 needs a 302 (redirect) to your captive portal web page.

Address                                                      Answer Code (>6.0)            HTTP/1.1 204 No Content           (4.4)             HTTP/1.1 204 No Content      (>6.0)            HTTP/1.1 204 No Content

iOS works as Windows, it searches for a web address and expects the answer Success.

Address                                      Answer Code             Text        HTTP/1.1 200 OK         Success

Using picoweb, a web micro-framework that works on the ESP32, it is possible to respond to the described requests and simulate that the ESP32 is connected to the Internet and acts as a hotspot. The devices that connect to the ESP32 think that it is a normal Hotspot and the "connect to a network" message appears. The "evil" portal is then opened, when the user clicks on the message.

Hardware & Software Requirements

WeMos WeMos WiFi ESP32 Development Tool x 1
INR18650 INR18650 3.7v Battery x 1
Python MicroPython
Python uPyPortal

Installing packages: upip, picoweb & notes-pico

I wrote the captive portal code inspired and using code of the notes-pico package. This package is amazing! You need to test it on the ESP32. If you want to see it live, install MicroPython on the board and then, type the following lines on the PyMark console

import upip

Installing notes-pico using upip installs also the following dependencies


Using upip requires that you board has access to the Internet! That means, you need to connect the board to a WiFi node. That can be done using the following code

import network

ssid_ = <#your_ssid#>
wp2_pass = <#your_wpa2_pass#>

sta_if = []

def do_connect():
    global sta_if
    sta_if = network.WLAN(network.STA_IF)
    if not sta_if.isconnected():
        print('connecting to network...')
        sta_if.connect(ssid_, wp2_pass)
        while not sta_if.isconnected():
    print('network config:', sta_if.ifconfig())

# connecting to WiFi

After the installation is finished, you can run the notes-pico application using the following lines

import notes_pico.__main__
notes_pico.__main__.main(host=sta_if.ifconfig()[0], port=80)

Then, you can access the note application using your browser and the IP and port that are reported on the console, e.g.

Running on

If you want to read more about this application, click here.

DIY: Captive Portal on ESP32

  1. Get the required hardware.

  2. Install MicroPython on the ESP32. You need a nightly version greater than esp32-20171031 (31 Oct. 2017)! Versions older than this one have a problem with the DNS address of the access point (see here). A tutorial to install MicroPython on the ESP32 can be found here.

  3. Using the PyMark console, connect the WeMos to the Internet (code above or, and install the captive-portal dependencies (picoweb, micropython-logging, utemplate, micropython-pkg_resources, micropython-btreedb). I included the file You need to change the <your_ssid>, and the <wpa2_password> to the corresponding of your network and execute the file on the PyMark console (click on the run button).

  4. Check the configuration variables in and modified them if you want to.

    The configuration variables are located in this file. The logged data will be save on a file on the WeMos. To do that, you can choose between 3 possibilities (btree, filedb or sqlite). I tested only btree ;). This can be selected using the variable DB_BACKEND. The variable DB_PASSWORD is the admin password. I included an admin section to read the database. The user is admin, the password is defined using DB_PASSWORD. The DNS service only responds to the requests that contain at least of the words from the array DNS_ANSWERS. This reduces the CPU workload. The words listed per default are the required in order to get the "connect to a network" message (you should not remove any of them). You can add others to this array. The problems with sockets that I described in the Wemos-Alexa blog article has also consequences here. The number of opened sockets is limited. You can expect some fatal error from the picoweb service.

  5. Modify the captive portal webpage included in the template folder.

    The homepage.html is the web page that the users see after clicking on the "connect to a network" message. You can include some JavaScript files, images and CSS style files in the static folder too. The WiFi speed is slow. The portal should be not to big (I included one sample portal, but it should be a lighter one - optimization is required here!). If you are an advanced user, you can also modified the admin.html and login.html pages. These two files are only for you to see the logged data (administrator section).

  6. Load the captive_portal folder using the FTP server included (, or using the ampy.

  7. Configure the name of the access point in the file and load this file to the root folder.

  8. Reset the Wemos.

  9. Connect to the access point network and wait for the "connect to a network" message. Click on it and wait for the portal.

  10. Go to http://<ip wemos>/admin.html enter the admin as user and the password from and you will see the logged data.


  • For the logging data, I took some of the files from notes-pico and modified them.
  • For the DNS fake code, I wrote a file using info from here (mirror: here).
{{ message }}

{{ 'Comments are closed.' | trans }}