
Configure your Pi Zero 2W to be a BadUSB
A programmable BadUSB / HID attack platform built on a Raspberry Pi Zero 2 W. The Pi enumerates as a USB composite device (keyboard plus an optional read-only mass-storage volume) and executes Ducky-Script-style payloads against the host it's plugged into. Designed for authorised red-team engagements, penetration tests, and CTFs.
⚠️ Legal notice. Use only on systems you own or have explicit written permission to test. Unauthorised access is illegal in most jurisdictions, and pretending you didn't know won't help.
configured state — no
spurious payload fires at boot.LAYOUT.IF / ELSE, WHILE, HOLD / RELEASE, INJECT_MOD,
RANDOM_* generators, STRING_BLOCK / STRINGLN_BLOCK, DEFINE.ExecStop that tears the gadget down
cleanly. No leftover configfs state, restart works without rebooting./dev/hidg0 — no world-writable
device nodes.+----------------+ USB cable +-------------+
| Raspberry Pi | ===================>>> | Host PC |
| Zero 2 W | (Pi emulates HID + | (target) |
| (this device) | optional drive) | |
+----------------+ +-------------+
|
| /home/pi/pi-badusb/
|
+-- badusb.service ----> monitor_and_run.py
|
| on `state == configured`:
v
run_payload.py
|
v
/dev/hidg0 (USB HID gadget)
badusb.service systemd unit runs gadget_setup.sh,
which uses configfs/libcomposite to compose a USB gadget with an
HID keyboard function and (optionally) a mass-storage LUN backed by
a flat image file at /var/badusb/storage.img.monitor_and_run.py. It polls
/sys/class/udc/<udc>/state and waits for configured — the
USB-spec state that means a host has successfully enumerated the
gadget. (We do not use /dev/hidg0's existence as a signal:
that's true the moment the gadget binds to the UDC at boot,
regardless of whether anything is plugged in.)run_payload.py,
which parses payload.txt and writes HID reports to /dev/hidg0.configured transition.| Component | Note |
|---|---|
| Raspberry Pi Zero 2 W | Tested on 2026-05 hardware revision. Older Pi Zero W with the BCM2835 dwc_otg driver also works in principle, but this README assumes 2 W with dwc2. |
| micro-USB to USB-A cable | Or a "USB stick" form-factor adapter that lets the Pi plug straight into a host port. |
| Optional: separate charger | If you want to power the Pi from a non-host source so the listener can boot before being plugged into a target (otherwise the host port supplies the power). |
The Pi Zero 2 W has two micro-USB ports:
PWR IN — power only, doesn't expose USB data lines to the dwc2 OTG block.USB — the OTG data port; this is where you plug into the target.| Requirement | Why |
|---|---|
| Raspberry Pi OS (Debian Bookworm or Trixie, 64-bit Lite recommended) | The install script writes to /boot/firmware/... on Bookworm+ and falls back to /boot/... on older images. |
| Python 3 | Comes with Pi OS. |
mkfs.vfat | For formatting the mass-storage backing image on first run. Skip if you disable mass storage. |
| Root access for setup | Touches systemd, udev, and /boot/firmware/config.txt. |
Clone or copy the repo into the Pi, then run the installer:
git clone http://your-gitea/admin/Pi-Zero-2W-Bad-USB.git /home/pi/pi-badusb
cd /home/pi/pi-badusb
sudo ./install.sh
sudo reboot
After reboot, enable and start the service:
sudo systemctl enable --now badusb.service
journalctl -u badusb -f
install.sh is idempotent — re-run it whenever you change project
files. It:
/boot/firmware (Bookworm+) vs /boot (older).dtoverlay=dwc2,dr_mode=otg is active under an [all]
block in config.txt. Raspberry Pi Imager defaults put this line
inside a [cm5] filter that doesn't apply on Pi Zero 2 W; the
installer appends a sentinel-marked override so re-runs don't
duplicate it.modules-load=dwc2 is in cmdline.txt.g_ether is still present in cmdline.txt (it steals the
UDC from libcomposite and breaks gadget mode)./etc/systemd/system/badusb.service./etc/udev/rules.d/99-badusb-hidg.rules
so /dev/hidg0 is group-writable by plugdev.pi user to plugdev./var/badusb/ for the mass-storage backing image.# Start / stop / restart
sudo systemctl start badusb
sudo systemctl stop badusb
sudo systemctl restart badusb # safe to do while plugged in
# Watch live
journalctl -u badusb -f
# Disable autostart on boot
sudo systemctl disable badusb
# Tune timings (creates an override drop-in)
sudo systemctl edit badusb
# (paste an [Service] block with Environment="BADUSB_REARM_COOLDOWN_S=8" etc)
sudo systemctl restart badusb
The service depends on sys-kernel-config.mount and the presence of a
UDC, so it can't fire payloads before the gadget is actually ready.
Editing the payload doesn't require a restart — payload.txt is
read fresh on every plug-in.
payload.txt lives in the install directory. The full command
reference is in payload_commands.md.
REM Open Run dialog and type a greeting via Notepad
LAYOUT US
GUI r
DELAY 1500
STRING notepad
ENTER
DELAY 2500
STRINGLN Hello from the Pi Zero 2 W
VAR $USER="alice"
VAR $COUNT=0
WHILE $COUNT < 3
STRINGLN Hello $USER (iteration $COUNT)
VAR $COUNT = $COUNT + 1
END_WHILE
IF $USER == "alice"
STRINGLN matched
ELSE
STRINGLN missed
END_IF