Decrypting ADWS Traffic
Table of Contents
Decrypting ADWS Traffic: Peeling Back the Layers of .NET Message Security
Active Directory Web Services (ADWS) is one of those protocols that security tooling increasingly relies on, yet few analysts know how to inspect at the wire level. Tools like SOAPHound use ADWS to enumerate Active Directory over port 9389, bypassing traditional LDAP monitoring. When you capture this traffic, you’ll find it wrapped in layer after layer of encoding and encryption.
If you’re lucky enough that the payload was encrypted using NTLMSSP then Wireshark can decrypt it with a password, but the output is a massive SOAP XML with base64 encoded binary SDDL strings and even finding which filter was used is a challenge:

However, even with a valid keytab, Wireshark doesn’t decrypt if Kerberos GSS API was used. And in practice, many environments use a mix of both - some connections authenticate with Kerberos, others with NTLM, sometimes within the same capture.
This post documents the full process of manually decrypting ADWS NMS (.NET Message Security) traffic from a pcap using Python, from opaque encrypted blobs to readable SOAP/XML containing AD enumeration queries and their results. It’s worth noting that you can get logs by enabling ADWS Debug mode that contain basically the same information and skip this process entirely, but where’s the fun in that?
The accompanying script (decrypt-adws on GitHub) handles both Kerberos (via keytab) and NTLM (via password or NT hash) authentication, decodes the binary XML and SDDL values, and generates an analyst summary report that flags attack patterns like SOAPHound and AS-REP roasting recon.
The Setup
I used an ExtraHop sensor to capture traffic generated by SOAPHound.exe between a workstation and a domain controller (dc01.lab.local) on port 9389. I then generated a keytab file using the AES Key for the dc01 machine account.
The keytab was generated from a simple Python script that packs the known key material into MIT keytab format:
kvno = 4
realm = 'LAB.LOCAL'
princ = 'host/DC01.LAB.LOCAL'
# AES-256 key (enctype 18)
key = '4fac4a6593a9413410601bbe21cc240cf82f7b46868f47534a1d50be0e97ba35'
Sadly, despite the Keytab matching KVNO and Host, Wireshark could not decrypt the Kerberos Payload.

Layer 1: Finding the Traffic
Parsing the TCP streams revealed 8 separate TCP connections from a single client to dc01.lab.local:9389, each with source ports in the 49297-49325 range. This is typical of ADWS - WCF creates multiple connections for different service endpoints (Resource, Enumeration, etc.).
Layer 2: .NET Message Framing (MC-NMF)
Each TCP stream starts with the .NET Message Framing protocol (documented in Microsoft’s [MC-NMF] open specification). The client-to-server preamble is:

[Version Record] 00 01 00 > Version 1.0
[Mode Record] 01 02 > Duplex
[Via Record] 02 4C net.tcp://dc01.lab.local:9389/ActiveDirectoryWebServices/Windows/Resource
[Known Encoding] 03 08 > Binary XML with session dictionary
[Upgrade Request] 09 15 application/negotiate
The application/negotiate upgrade request tells us the connection will use NegotiateStream for security - which means SPNEGO/Kerberos.
The server responds with:
[Preamble Ack] 0A
Layer 3: NegotiateStream (SPNEGO/Kerberos)
After the NMF preamble, both sides switch to the NegotiateStream protocol ([MS-NNS]). The frame format during handshake is:
Status(1 byte) | MajorVersion(1) | MinorVersion(1) | PayloadLength(2 BE) | Payload
Status values:
0x16= Handshake In Progress0x14= Handshake Done0x15= Handshake Error
The client sends 0x16 (In Progress) with a SPNEGO negTokenInit containing a Kerberos AP-REQ. The server responds with 0x14 (Done) containing a SPNEGO negTokenResp with a Kerberos AP-REP.
Extracting the Kerberos Keys
The AP-REQ (ASN.1 tag 0x6E = APPLICATION 14) contains an encrypted service ticket. Using the host key from the keytab, we decrypt it:
# Key usage 2 = ticket enc-part
ticket_plaintext = aes256_decrypt(host_key, usage=2, ciphertext=ticket_cipher)
enc_ticket = EncTicketPart.load(ticket_plaintext)
session_key = enc_ticket['key'] # AES-256 session key
The ticket reveals the client principal: aragorn@LAB.LOCAL.
Inside the AP-REQ is also an encrypted Authenticator (decrypted with the session key, key usage 11). The Authenticator contains a client subkey - an AES-256 key that the client proposes for protecting the session.
# Key usage 11 = AP-REQ authenticator
auth_plaintext = aes256_decrypt(session_key, usage=11, auth_cipher)
authenticator = Authenticator.load(auth_plaintext)
client_subkey = authenticator['subkey'] # AES-256
The AP-REP from the server (ASN.1 tag 0x6F = APPLICATION 15) contains the server’s acceptance and its own server subkey:
# Key usage 12 = AP-REP enc-part
aprep_plaintext = aes256_decrypt(session_key, usage=12, aprep_cipher)
server_subkey = EncAPRepPart.load(aprep_plaintext)['subkey'] # AES-256
Each of the 8 TCP connections has its own Kerberos authentication exchange and unique set of subkeys.
Layer 4: GSS-Wrap CFX Tokens
After the NegotiateStream handshake completes, data flows as NegotiateStream data frames: a 4-byte little-endian length prefix followed by the payload. Each payload is a GSS-Wrap CFX token (RFC 4121).
The CFX token header (16 bytes):
Offset Field Value
0-1 TOK_ID 05 04 (Wrap token)
2 Flags 07 (S2C) or 06 (C2S)
3 Filler FF
4-5 EC 00 00 (extra count)
6-7 RRC 00 1C (right rotation count = 28)
8-15 SND_SEQ sequence number (big-endian)
The flags tell us:
- Bit 0 (0x01): SentByAcceptor - set for server>client
- Bit 1 (0x02): Sealed - the payload is encrypted
- Bit 2 (0x04): AcceptorSubkey - the server’s subkey is the encryption key
Both directions have the AcceptorSubkey flag set, meaning the server subkey is used for encryption in both directions. The key usage differs by direction:
- Server > Client (acceptor seal): key usage 22
- Client > Server (initiator seal): key usage 24
The RRC Rotation
The ciphertext after the 16-byte header has been right-rotated by RRC bytes (28 in our case). To decrypt:
- Strip the 16-byte header
- Left-rotate the remaining bytes by 28 positions (undo the rotation)
- Decrypt using AES-256-CTS-HMAC-SHA1-96
- The plaintext ends with the 16-byte header (for integrity verification) - strip it
ciphertext = token[16:]
# Undo rotation
r = rrc % len(ciphertext)
ciphertext = ciphertext[r:] + ciphertext[:r]
# Decrypt (confounder + plaintext + HMAC handled by the cipher)
plaintext = aes256_cts_decrypt(server_subkey, key_usage, ciphertext)
# Strip appended header
message_data = plaintext[:-16]
The RRC value of 28 is not a coincidence - it equals the AES-256 confounder (16 bytes) plus the HMAC-SHA1-96 checksum (12 bytes). This rotation places the integrity checksum at a predictable position for hardware-accelerated verification.
Layer 5: .NET Message Framing Records (Inner)
After decrypting all the GSS-Wrap tokens in order and concatenating the plaintext, we get the inner .NET Message Framing stream. This starts with familiar NMF records:
C2S: 0C > PreambleEnd
06 F2 07 > Sized Envelope (1010 bytes)
06 AB 04 > Sized Envelope (555 bytes)
S2C: 0B > PreambleAck
06 87 03 > Sized Envelope (391 bytes)
06 A0 03 > Sized Envelope (416 bytes)
The PreambleEnd/PreambleAck handshake inside the encrypted channel mirrors the outer handshake - confirming to both sides that security is established.
Each Sized Envelope record contains the actual SOAP message in Binary XML format.
Layer 6: MC-NBFS String Table
Before the Binary XML in each Sized Envelope is a session string table (MC-NBFS). This is where we hit an undocumented implementation detail.
The Microsoft specification ([MC-NBFS] Section 2.2.5.5) describes the string table as having a “count” field followed by that many strings. However, in practice the first field is a byte count (the total size of the string table in bytes), not a string count.
Byte 0: 4D (77) > 77 bytes of string table data
Byte 1: 4C (76) > First string is 76 bytes
Bytes 2-77: > "net.tcp://dc01.lab.local:9389/.../Enumeration"
Byte 78: 56 > Start of Binary XML (PrefixDictionaryElement 's')
If you interpret 0x4D as “77 strings” (as the spec suggests), parsing breaks immediately - the second “string length” would be 0x56 (86), and the “string” would contain binary XML opcodes. The byte-count interpretation gives exactly one string (76 bytes + 1 byte length prefix = 77) and the Binary XML starts cleanly at the expected <s:Envelope> element.
Layer 7: MC-NBFX Binary XML
The Binary XML format ([MC-NBFX]) uses single-byte opcodes for XML elements, attributes, and text values, combined with a static dictionary of well-known strings (SOAP namespaces, WS-Addressing terms, etc.) and the session-specific strings from the table above.
Key record type ranges:
0x01 EndElement
0x04-0x0B Attribute records
0x0C-0x25 PrefixDictionaryAttribute (a-m, sequential)
0x26-0x3F PrefixAttribute (a-m, sequential)
0x40-0x43 Element records (Short, Full, Dictionary variants)
0x44-0x5D PrefixDictionaryElement (a-z, sequential - 26 records)
0x5E-0x77 PrefixElement (a-z, sequential - 26 records)
0x80-0xBF Text records (even=inline, odd=inline+EndElement)
A critical detail: PrefixDictionaryElement records are sequential (one per letter), not paired even/odd. 0x44 = prefix ‘a’, 0x45 = prefix ‘b’, …, 0x5D = prefix ‘z’. Some documentation and third-party parsers get this wrong, treating them as paired (even = open, odd = close), which corrupts the decode.
The DictionaryString Even/Odd Split
Both element and attribute records reference strings via DictionaryString wire values - multi-byte base-128 (mb32) integers. The critical encoding detail that isn’t obvious from the spec: even wire values map to the static dictionary, and odd wire values map to the session dictionary.
The static dictionary contains 487 entries sourced from the WCF ServiceModelStringsVersion1.cs in the .NET Framework reference source. These are keyed by even wire values from 0x000 to 0x3CC:
0x000 > mustUnderstand 0x002 > Envelope
0x004 > http://www.w3.org/2003/05/soap-envelope
0x006 > http://www.w3.org/2005/08/addressing
0x008 > Header 0x00A > Action
0x00C > To 0x00E > Body
...
0x234 > EnumerateResponse 0x236 > Pull
Session strings from the MC-NBFS string table (Layer 6) are assigned odd IDs starting at 1, incrementing by 2 (1, 3, 5, 7, …). When a subsequent Sized Envelope on the same connection adds new session strings, they continue the numbering from where the previous message left off.
So the byte sequence 56 04 decodes as:
0x56= PrefixDictionaryElement for prefix ‘s’ (0x56 - 0x44 = 18th letter)0x04= even wire value > static dictionary >http://www.w3.org/2003/05/soap-envelope- Result:
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
Text Record Types
The text record range (0x80-0xBF) is where we hit the most insidious problem. The MC-NBFX open specification documents one ordering of text record types; the actual .NET WCF runtime uses a completely different one.
The published spec places UniqueIdText at 0x9E and DictionaryText at 0xA8. The real implementation (XmlBinaryNodeType.cs in dotnet/runtime) puts Bytes8Text at 0x9E, DictionaryText at 0xAA, and UniqueIdText at 0xAC. The shifts cascade across the entire range:
| Opcode | MC-NBFX Spec Says | .NET Actually Does |
|---|---|---|
0x9E | UniqueIdText (16-byte UUID) | Bytes8Text (1-byte len + data) |
0xA0 | TimeSpanText (8-byte int) | Bytes16Text (2-byte len + data) |
0xA2 | UInt64Text (8-byte uint) | Bytes32Text (4-byte len + data) |
0xA8 | DictionaryText (mb32 index) | EmptyText (no data) |
0xAA | StartListText | DictionaryText (mb32 index) |
0xAC | EndListText | UniqueIdText (16-byte UUID) |
Getting this wrong is catastrophic. If a server response contains a 0xAD byte (UniqueIdTextWithEndElement in reality), interpreting it as the spec’s “EndListTextWithEndElement” means you read zero bytes instead of the correct 16 - and the parser immediately derails into consuming binary XML opcodes as text data, producing garbled output.
As far as I can tell, this discrepancy hasn’t been publicly documented before. Existing third-party MC-NBFX parsers (including several open-source ones) follow the published spec and silently produce corrupt output for any message containing text records above 0x9E. The correct mapping has to be extracted from XmlBinaryNodeType.cs in the dotnet/runtime repository:
0x80 ZeroText > literal "0"
0x82 OneText > literal "1"
0x84 FalseText > literal "false"
0x86 TrueText > literal "true"
0x88 Int8Text > 1-byte signed int
0x8A Int16Text > 2-byte signed int
0x8C Int32Text > 4-byte signed int
0x8E Int64Text > 8-byte signed int
0x90 FloatText > 4-byte IEEE float
0x92 DoubleText > 8-byte IEEE double
0x94 DecimalText > 16-byte .NET decimal
0x96 DateTimeText > 8-byte .NET ticks
0x98 Chars8Text > 1-byte len + UTF-8
0x9A Chars16Text > 2-byte len + UTF-8
0x9C Chars32Text > 4-byte len + UTF-8
0x9E Bytes8Text > 1-byte len + raw (base64)
0xA0 Bytes16Text > 2-byte len + raw (base64)
0xA2 Bytes32Text > 4-byte len + raw (base64)
0xA4 StartListText > (no data)
0xA6 EndListText > (no data)
0xA8 EmptyText > (no data)
0xAA DictionaryText > mb32 dictionary index
0xAC UniqueIdText > 16-byte UUID (urn:uuid:...)
0xAE TimeSpanText > 8-byte ticks
0xB0 GuidText > 16-byte GUID
0xB2 UInt64Text > 8-byte unsigned int
0xB4 BoolText > 1-byte boolean
0xB6 UnicodeChars8Text > 1-byte len + UTF-16LE
0xB8 UnicodeChars16Text> 2-byte len + UTF-16LE
0xBA UnicodeChars32Text> 4-byte len + UTF-16LE
0xBC QNameDictText > 1-byte prefix + mb32 name
Odd opcodes (0x81, 0x83, etc.) encode the same text type plus an implicit EndElement - a compression that saves one byte per leaf value.
What the Traffic Revealed
After peeling back all seven layers, the decrypted ADWS traffic shows the SOAPHound enumeration session:

SOAP Operations
| Operation | Count | Purpose |
|---|---|---|
| Transfer/Get | 4 | RootDSE queries (capabilities, naming contexts) |
| Enumeration/Enumerate | 4 | Start LDAP searches |
| Enumeration/Pull | 8 | Retrieve search results |
LDAP Queries
The Enumerate requests contain LDAP filters that betray the tool:
<Filter>(!soaphound=*)</Filter>
<BaseObject>DC=lab,DC=local</BaseObject>
<Scope>Subtree</Scope>
<Filter>(trustType=*)</Filter>
<BaseObject>DC=lab,DC=local</BaseObject>
<Scope>Subtree</Scope>
The (!soaphound=*) filter is a characteristic signature - it matches all objects (since no object has a soaphound attribute), while leaving a breadcrumb that identifies the tool.
Requested AD Attributes
The enumeration targeted a comprehensive set of security-relevant attributes:
- Identity:
sAMAccountName,cn,distinguishedName,objectSid,objectGUID - Security:
nTSecurityDescriptor,userAccountControl,adminCount - Credentials:
userPassword,unixUserPassword,servicePrincipalName - Authentication:
pwdLastSet,lastLogon,lastLogonTimestamp - Group membership:
member,primaryGroupID - GPO:
gPLink,gPCFileSysPath - Trust:
trustDirection,trustAttributes,securityIdentifier,name - Other:
operatingSystem,dNSHostName,description,title,homeDirectory
Server Responses
The PullResponse messages contained full AD data including:
- Domain trust relationships with
nTSecurityDescriptorACLs (base64-encoded) supportedCapabilitiesOIDs (1.2.840.113556.1.4.800, etc.)supportedSASLMechanisms(GSSAPI, GSS-SPNEGO, EXTERNAL, DIGEST-MD5)msDS-Behavior-Version: 7(Windows Server 2016+ functional level)
The Complete Protocol Stack
To summarise the full onion:
┌─────────────────────────────────────────────┐
│ 7. SOAP/XML (AD Enumerate, Pull, Get) │ < What we want
├─────────────────────────────────────────────┤
│ 6. MC-NBFX Binary XML encoding │ < Binary-encoded XML
├─────────────────────────────────────────────┤
│ 5. MC-NBFS Session String Table │ < Per-message dictionary
├─────────────────────────────────────────────┤
│ 4. .NET Message Framing (Sized Envelopes) │ < Inner NMF records
├─────────────────────────────────────────────┤
│ 3. GSS-Wrap CFX (RFC 4121, AES-256) │ < Kerberos encryption
├─────────────────────────────────────────────┤
│ 2. NegotiateStream (SPNEGO handshake) │ < Key exchange
├─────────────────────────────────────────────┤
│ 1. .NET Message Framing (outer preamble) │ < Connection setup
├─────────────────────────────────────────────┤
│ 0. TCP (IPv6, port 9389) │ < Transport
└─────────────────────────────────────────────┘
Gotchas and Lessons Learned
The MC-NBFX spec lies about text record types: This was the most damaging issue. The MC-NBFX open specification ([MS-NBFX]) documents a text record ordering that does not match the actual .NET WCF implementation. The correct mapping comes from XmlBinaryNodeType.cs in the dotnet/runtime repository. The shifts are not minor - UniqueIdText is 14 bytes apart between spec and reality (0x9E vs 0xAC), which means every text record above 0x9E parses the wrong number of bytes, cascading into total corruption of server response bodies.
DictionaryString uses even/odd wire values: The static dictionary and session dictionary share the same mb32 index space but are distinguished by parity. Even values are static dictionary lookups; odd values are session dictionary lookups. The static dictionary (487 entries from ServiceModelStringsVersion1.cs) uses keys 0x000, 0x002, 0x004, … while session strings use 1, 3, 5, 7, … Getting this wrong maps every dictionary lookup to the wrong string - elements named with SOAP namespace URIs instead of short names like “Envelope”.
The string table byte-count vs string-count ambiguity: The MC-NBFS spec says “count of strings” but the implementation sends byte count. Interpreting 0x4D as “77 strings” consumed 1,584 bytes (way past the 1,010-byte payload), while interpreting it as “77 bytes of string table” gave exactly one valid string and left the Binary XML starting at the correct offset.
PrefixDictionaryElement is sequential, not paired: Many references (and my first implementation) treat 0x44-0x5D as paired records where even = one letter and odd = another. In reality, each byte maps to one letter: 0x44=‘a’, 0x45=‘b’, …, 0x5D=‘z’. Getting this wrong shifts all prefix letters by one position, producing garbled element names.
RRC=28 is not arbitrary: The right rotation count in GSS-Wrap CFX equals the AES-256 encryption overhead (16-byte confounder + 12-byte HMAC-SHA1-96). This places the HMAC at the end of the rotated ciphertext, enabling stream processing.
Each TCP connection has independent Kerberos state: The 8 connections each perform separate AP-REQ/AP-REP exchanges with different session keys and subkeys. You cannot use keys from one connection to decrypt another.
The inner NMF PreambleEnd/Ack: After the NegotiateStream handshake, the decrypted stream doesn’t immediately contain SOAP messages - there’s a PreambleEnd (0x0C) from the client and PreambleAck (0x0B) from the server before the Sized Envelope records begin.
The Decryption Tool
The Python script (decrypt_adws.py) handles the full pipeline from pcap to readable XML. Key capabilities:
- Pcap/pcapng auto-detection - reads the file magic bytes (
0xA1B2C3D4for pcap,0x0A0D0D0Afor pcapng) and selects the right dpkt reader - IPv4 and IPv6 support - parses both address families, with manual extension header walking for IPv6
- MIT keytab v2 parsing - reads keytab files directly, tries all key entries against ticket enctypes
- TCP reassembly - tracks byte ranges per direction, deduplicates retransmissions, detects and zero-fills gaps
- ADWS port auto-detection - scans reassembled streams for NMF preambles containing
ActiveDirectoryWebServicesin the Via record, with--portoverride

nTSecurityDescriptor SDDL Decoding
The decrypted XML contains nTSecurityDescriptor values as raw base64-encoded SECURITY_DESCRIPTOR_RELATIVE binary blobs - completely unreadable without further decoding. The script automatically parses these into SDDL (Security Descriptor Definition Language) strings and injects them as XML comments alongside the original base64:

<addata:nTSecurityDescriptor><ad:value xsi:type="xsd:base64Binary">AQAEhKwQ...</ad:value></addata:nTSecurityDescriptor>
<!-- SDDL: O:DAG:DAD:PAI(A;;RPWPCCDCLCSWRCWDWOGA;;;WD)(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;DA) -->
The SDDL converter is pure Python (no impacket or other AD libraries) and handles:
- Binary SID parsing - revision, 6-byte big-endian authority, N little-endian sub-authorities
- Well-known SID abbreviation - maps ~40 SIDs to their SDDL shorthand (SY, BA, DA, AU, WD, etc.)
- Domain SID auto-detection - scans the first batch of security descriptors to find the most common
S-1-5-21-x-y-zprefix, enabling domain-relative RID resolution (DA for -512, EA for -519, etc.) - Access mask decomposition - generic rights (GA, GR, GW, GX), standard rights (SD, RC, WD, WO), and DS-specific rights (CC, DC, LC, SW, RP, WP, DT, LO, CR)
- Object ACEs with GUIDs - parses
ACCESS_ALLOWED_OBJECT_ACEtypes (0x05, 0x06, 0x07) including conditional object type and inherited object type GUIDs in mixed-endian format - ACL control flags - P (protected), AI (auto-inherited), AR (auto-inherit-required)
- 60+ AD GUID lookups - maps common extended rights and property set GUIDs (
00299570-246d-11d0-a768-00aa006e0529>User-Force-Change-Password,1131f6ad-9c07-11d1-f79f-00c04fc2dcd2>DS-Replication-Get-Changes-All, etc.)
This makes it possible to directly read the AD ACLs from the wire capture. For a full domain dump like SOAPHound produces, the SDDL output is honestly overwhelming - hundreds of ACEs across hundreds of objects. Where it becomes more useful is targeted investigations: checking whether a specific account’s delegation permissions were queried, or spotting ACL modifications in a more surgical ADWS session. Mostly though, I just wanted to convert the base64 binary to SDDL for learning purposes.
Dependencies
- dpkt: Packet parsing (IPv4/IPv6, TCP)
- minikerberos: Kerberos ASN.1 structures and AES-256-CTS-HMAC-SHA1-96 decryption
- pycryptodome: AES primitives used by minikerberos
NTLM Support
The tool also handles NTLM-authenticated ADWS sessions. If the traffic used NTLMSSP instead of Kerberos, you can supply an NT hash or plaintext password:
python decrypt_adws.py capture.pcapng --password "Summer2026!"
python decrypt_adws.py capture.pcapng --nthash 32ed87bdb5fdc5e9cba88547376818d4
The script extracts NTLM Type 2 (challenge) and Type 3 (authenticate) messages from the NegotiateStream handshake, derives the session key, and decrypts the RC4-sealed payloads. You can combine both - provide a keytab for Kerberos connections and --password/--nthash for NTLM connections in the same pcap.
Analyst Summary Report
The raw XML output is thorough but verbose - a SOAPHound scan against a small lab domain produces a 2.2 MB XML file with 50 SOAP messages. Most of that bulk is repeated namespace declarations, base64-encoded security descriptors, and RootDSE capability lists. Reading through it to answer basic analyst questions (“who authenticated?”, “what was queried?”, “does this look malicious?”) is painful.
The script now generates a decrypted_adws_summary.txt alongside the raw XML that provides a structured overview:
Connection summary - a table showing each TCP connection with its port, authentication method (inferred from principal format: DOMAIN\user = NTLM, user@REALM = Kerberos), the authenticated principal, message count, and SOAP actions observed.
Attack pattern detection - rules that flag known offensive techniques:
| Severity | Pattern | Trigger |
|---|---|---|
| HIGH | AS-REP Roasting | LDAP filter contains userAccountControl:1.2.840.113556.1.4.803:=4194304 |
| HIGH | SOAPHound | Filter contains soaphound marker, or bulk enum requesting nTSecurityDescriptor + member + SPN with 15+ attributes |
| MEDIUM | Kerberoasting Recon | LDAP filter targets servicePrincipalName |
| MEDIUM | Sensitive Attribute Harvesting | Requesting msDS-AllowedToDelegateTo, userPassword, or unixUserPassword |
| INFO | Bulk AD Enumeration | Connection returned 20+ AD objects |
ATTACK PATTERN DETECTION
──────────────────────────────────────────────────────────────────────────────
[!] HIGH: SOAPHound (Conn 2, LAB\fsmith)
Filter contains "soaphound" marker string
[!] HIGH: AS-REP Roasting (Conn 9, lab.local\fsmith)
LDAP filter targets DONT_REQ_PREAUTH accounts
[*] MEDIUM: Sensitive Attribute Harvesting (Conn 2, LAB\fsmith)
Requesting: msDS-AllowedToDelegateTo, unixUserPassword, userPassword
These are basic at best and mostly added for proof of concept purposes.
Query details - for each Enumerate request: the LDAP filter, base DN, scope, requested attributes, and how many objects the server returned.
Extracted AD objects - deduplicated tables of users, computers, and groups pulled from the response data. User entries include decoded userAccountControl flags, which makes it immediately obvious which accounts have DONT_REQ_PREAUTH set (AS-REP roastable) or TRUSTED_FOR_DELEGATION (unconstrained delegation):
Users (37 unique):
sAMAccountName DN (CN) admin UAC Flags
──────────────────────────────────────────────────────────────────────────
hh hh 1 NORMAL_ACCOUNT, DONT_REQ_PREAUTH
fsmith FSmith NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, DONT_REQ_PREAUTH
frodo Frodo Baggins 1 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, DONT_REQ_PREAUTH
Objects are deduplicated by sAMAccountName with group memberships merged across multiple appearances, so repeated scans (e.g. SOAPHound querying the same objects from different connections) don’t bloat the output.
Usage
python decrypt_adws.py capture.pcapng dc01.keytab [--port 9389]
python decrypt_adws.py capture.pcapng --password "Password1" [--port 9389]
python decrypt_adws.py capture.pcapng dc01.keytab --password "Password1"
The script outputs:
decrypted_adws.xml- all SOAP messages with SDDL annotationsdecrypted_adws_summary.txt- analyst summary report with attack detectiondecrypted_adws_raw.bin- raw decrypted binary streams with structured headersdecrypted_raw/- individual binary stream files per connection/direction