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:

Spot the filter

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.

Wireshark failed to decrypt with valid keytab

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:

Client-server preamble

[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 Progress
  • 0x14 = Handshake Done
  • 0x15 = 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:

  1. Strip the 16-byte header
  2. Left-rotate the remaining bytes by 28 positions (undo the rotation)
  3. Decrypt using AES-256-CTS-HMAC-SHA1-96
  4. 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:

OpcodeMC-NBFX Spec Says.NET Actually Does
0x9EUniqueIdText (16-byte UUID)Bytes8Text (1-byte len + data)
0xA0TimeSpanText (8-byte int)Bytes16Text (2-byte len + data)
0xA2UInt64Text (8-byte uint)Bytes32Text (4-byte len + data)
0xA8DictionaryText (mb32 index)EmptyText (no data)
0xAAStartListTextDictionaryText (mb32 index)
0xACEndListTextUniqueIdText (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:

Raw and Soapy

SOAP Operations

OperationCountPurpose
Transfer/Get4RootDSE queries (capabilities, naming contexts)
Enumeration/Enumerate4Start LDAP searches
Enumeration/Pull8Retrieve 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 nTSecurityDescriptor ACLs (base64-encoded)
  • supportedCapabilities OIDs (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 (0xA1B2C3D4 for pcap, 0x0A0D0D0A for 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 ActiveDirectoryWebServices in the Via record, with --port override

Decryption Tool Output

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:

SDDL decoded from nTSecurityDescriptor

<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-z prefix, 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_ACE types (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:

SeverityPatternTrigger
HIGHAS-REP RoastingLDAP filter contains userAccountControl:1.2.840.113556.1.4.803:=4194304
HIGHSOAPHoundFilter contains soaphound marker, or bulk enum requesting nTSecurityDescriptor + member + SPN with 15+ attributes
MEDIUMKerberoasting ReconLDAP filter targets servicePrincipalName
MEDIUMSensitive Attribute HarvestingRequesting msDS-AllowedToDelegateTo, userPassword, or unixUserPassword
INFOBulk AD EnumerationConnection 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 annotations
  • decrypted_adws_summary.txt - analyst summary report with attack detection
  • decrypted_adws_raw.bin - raw decrypted binary streams with structured headers
  • decrypted_raw/ - individual binary stream files per connection/direction