Authenticating against an Active Directory Server, pt. 2

Posted on September 17, 2009 by Tommy McGuire
Let's talk about LDAP. (I feel like a character from Greater Tuna.) LDAP is the Lightweight Directory Access Protocol (and any protocol which includes the words "simple," or "lightweight" in its name is lying), and it is a scheme to (1) look up information in a particular, strange hierarchical database, and (2) provide authentication, by the way.

The second step in locating an Active Directory server is to "ping" the potential servers to determine which server to use. This ping is done using an LDAP query over UDP (quite aside from the fact that LDAPv3 is not defined over UDP; connectionless LDAP was available in version 1, but appears to have been dropped from the standards). Due to this, composing the ping seems to involve some custom hacking. The ping is a LDAP SearchRequest with a particular set of parameters, in effect limiting the query to specific parts of the AD server's configuration.

LDAP messages, including the SearchRequest, are defined using ASN.1, the Abstract Syntax Notation 1. This notation describes the structure of the message in a relatively readable format; the message on the wire is encoded based on the description using BER, the Basic Encoding Rules; these use a (T,L,V) format, breaking the message into a set of sub-messages each encoded with a Type of the sub-message, the Length of the sub-message in bytes, and the actual value. For the details of the message definition, see RFC2251. An example is the SearchRequest itself:

        SearchRequest ::= [APPLICATION 3] SEQUENCE {                baseObject      LDAPDN,                scope           ENUMERATED {                        baseObject              (0),                        singleLevel             (1),                        wholeSubtree            (2) },                derefAliases    ENUMERATED {                        neverDerefAliases       (0),                        derefInSearching        (1),                        derefFindingBaseObj     (2),                        derefAlways             (3) },                sizeLimit       INTEGER (0 .. maxInt),                timeLimit       INTEGER (0 .. maxInt),                typesOnly       BOOLEAN,                filter          Filter,                attributes      AttributeDescriptionList }        Filter ::= CHOICE {                and             [0] SET OF Filter,                or              [1] SET OF Filter,                not             [2] Filter,                equalityMatch   [3] AttributeValueAssertion,                substrings      [4] SubstringFilter,                greaterOrEqual  [5] AttributeValueAssertion,                lessOrEqual     [6] AttributeValueAssertion,                present         [7] AttributeDescription,                approxMatch     [8] AttributeValueAssertion,                extensibleMatch [9] MatchingRuleAssertion }        AttributeValueAssertion ::= SEQUENCE {                attributeDesc   AttributeDescription,                assertionValue  AssertionValue }        AttributeDescription ::= LDAPString        AssertionValue ::= OCTET STRING

Microsoft's documentation of the LDAP ping includes this block of data, purportedly the payload of the UDP datagram:

A0 84 00 00 00 A8 A3 84 00 00 00 25 04 09 446E 73 44 6F 6D 61 69 66 04 18 61 62 63 64 652E 63 6F 72 70 2E 6D 69 63 72 6F 73 6F 66 742E 63 6F 6D A3 84 00 00 00 14 04 04 48 6F 7374 04 0C 61 62 63 64 65 66 67 68 2D 64 65 76A3 84 00 00 00 15 04 04 55 73 65 72 04 0D 6162 63 64 65 66 67 68 2D 64 65 76 24 A3 84 0000 00 0B 04 03 41 41 43 04 04 80 00 00 00 A384 00 00 00 1E 04 0A 44 6F 6D 61 69 6E 47 7569 64 04 10 3B B0 21 CA D3 6D D1 11 8A 7D B8DF B1 56 87 1F A3 84 00 00 00 0D 04 05 4E 7456 65 72 04 04 06 00 00 00 30 84 00 00 00 0A04 08 6E 65 74 6C 6F 67 6F 6E

That block of bytes is all well and good, but what does it actually mean? Certainly, if you pack it off to an AD server, you may get an error or more likely nothing at all. So, I spent a while manually decoding the message to see what its insides look like. (In the following example, the first few columns contain bars indicating the extents of sub-messages. These are followed by the value of a byte and either another vertical bar, indicating the byte is part of larger value described on a subsequent line, or an equal sign; the equal sign is typically followed by the eight binary digits of the byte and a text description of what it means.)

   A0 = 10100000  Context, constructed, #0 (LDAP AND filter)   84 = 10000100  4-byte length   00 |   00 |   00 |   A8 = 10101000  168 byte contents|  A3 = 10100011  Context, Constructed, #3 (LDAP equalityMatch filter)|  84 = 10000100  4-byte length|  00 ||  00 ||  00 ||  25 =           37-byte contents|| 04 = 00000100  Universal, OCTET STRING|| 09 =           9-byte contents|||44 ||||6E ||||73 ||||44 ||||6F ||||6D ||||61 ||||69 ||||66 =           "DnsDomaif" (?)|| 04 = 00000100  Universal, OCTET STRING|| 18 = 00011000  24-byte contents|||61 ||||62 ||||63 ||||64 ||||65 ||||2e ||||63 ||||6f ||||72 ||||70 ||||2e ||||6d ||||69 ||||63 ||||72 ||||6f ||||73 ||||6f ||||66 ||||74 ||||2e ||||63 ||||6f ||||6d =           "abcde.corp.microsoft.com"|  A3 = 10100011  Context, constructed #3 (LDAP equalityMatch filter)|  84 = 10000100  4-byte length|  00 ||  00 ||  00 ||  14 =           20-byte contents|| 04 = 00000100  Universal, OCTET STRING|| 04 =           4-byte contents|||48 ||||6f ||||73 ||||74 =           "Host"|| 04 = 00000100  Universal, OCTET STRING|| 0C =           12-byte contents|||61 ||||62 ||||63 ||||64 ||||65 ||||66 ||||67 ||||68 ||||2D ||||64 ||||65 ||||76 =           "abcdefgh-dev"|  A3 = 10100011  Context, constructed #3 (LDAP equalityMatch filter)|  84 = 10000100  4-byte length|  00 ||  00 ||  00 ||  15 =           21-byte contents|| 04 = 00000100  Universal, OCTET STRING|| 04 =           4-byte contents|||55 ||||73 ||||65 ||||72 =           "User"|| 04 = 00000100  Universal, OCTET STRING|| 0D =           13-byte contents|||61 ||||62 ||||63 ||||64 ||||65 ||||66 ||||67 ||||68 ||||2D ||||64 ||||65 ||||76 ||||24 =           "abcdefgh-dev\$"|  A3 = 10100011  Context, constructed #3 (LDAP equalityMatch filter)|  84 = 10000100  4-byte length|  00 ||  00 ||  00 ||  0B =           11-byte contents|| 04 = 00000100  Universal, OCTET STRING|| 03 =           3-byte contents|||41 ||||41 ||||43 =           "AAC"|| 04 = 00000100  Universal, OCTET STRING|| 04 =           4-byte contents|||80 = 10000000|||00 = 00000000|||00 = 00000000|||00 = 00000000|  A3 = 10100011  Context, constructed #3 (LDAP equalityMatch filter)|  84 = 10000100  4-byte length|  00 ||  00 ||  00 ||  1E =           30-byte contents|| 04 = 00000100  Universal, OCTET STRING|| 0A = 00001010  10-byte contents|||44 ||||6F ||||6D ||||61 ||||69 ||||6E ||||47 ||||75 ||||69 ||||64 =           "DomainGuid"|| 04 = 00000100  Universal, OCTET STRING|| 10 = 00010000  16-byte contents|||3B ||||B0 ||||21 ||||CA ||||D3 ||||6D ||||D1 ||||11 ||||8A ||||7D ||||B8 ||||DF ||||B1 ||||56 ||||87 ||||1F =           \3b,\b0,\21,\ca,\d3,\6d,\d1,\11,\8a,\7d,\b8,\df,\b1,\56,\87,\1f|  A3 = 10100011  Context, constructed #3 (LDAP equalityMatch filter)|  84 = 10000100  4-byte length|  00 ||  00 ||  00 ||  0D =           13-byte contents|| 04 = 00000100  Universal, OCTET STRING|| 05 = 00000101  5-byte contents|||4E ||||74 ||||56 ||||65 ||||72 =           "NtVer"|| 04 = 00000100  Universal, OCTET STRING|| 04 = 00000100  4-byte contents|||06 ||||00 ||||00 ||||00 =           \06\00\00\00   30 = 00110000  Universal, constructed #16 SEQUENCE   84 = 10000100  4-byte length   00 |   00 |   00 |   0A =           10-byte contents | 04 = 00000100  Universal, OCTET STRING | 08 = 00001000  8-byte contents ||6E | ||65 | ||74 | ||6C | ||6F | ||67 | ||6F | ||6E =           "netlogon"

There are a few things to be learned looking at this decoding. First, some joker has misspelled DnsDomain; the ASCII representation on Microsoft's page has the corrected text, but 0x66 is 'f', not 'n'. Second, this block of bytes is not the entire payload of the UDP datagram; instead it is an encoding of an LDAP query string from the LDAP Ping documentation page. Specifically, the overall structure is an and filter combining a sequence of equalityMatch filters.

After determining the structure of the sample data, I got busy trying to get a valid, good response from an AD server. Then, I lowered my sights and just tried to get any response. Finally, I broke down and wrote some ASN.1 encoding code; I opened a UDP socket and manually sent the request and waited for a response. Boy, did I get one.

   30 = 00110000  Universal, constructed #16 SEQUENCE   84 = 10000100  4-byte length   00 |   00 |   00 |   a7 =           167-byte contents|  02 = 00000010  Universal, primitive INTEGER|  01 =           1-byte content|  01 =           Value 1 (Message Id 1)|  65 = 01100101  Application, constructed #5 (LDAP SearchResultDone)|  84 = 10000100  4-byte length|  00 ||  00 ||  00 ||  9e =           158-byte contents|| 0a = 00001010  Universal, primitive #10 (ENUMERATED)|| 01 = 00000001  1-byte content|| 01 =           Value 1 (LDAP LDAPResult resultCode operationsError)|| 04 = 00000100  Universal, primitive OCTET STRING|| 00 = 00000000  0-byte content|| 04 = 00000100  Universal, primitive OCTET STRING|| 84 = 10000100  4-byte length|| 00 ||| 00 ||| 00 ||| 93 =           147-byte content|||30 =           "0"|||30 =           "0"|||30 =           "0"|||30 =           "0"|||30 =           "0"|||30 =           "0"|||30 =           "0"|||30 =           "0"|||3a =           ":"|||20 =           " "|||4c =           "L"|||64 =           "d"         |||61 =           "a"|||70 =           "p"|||45 =           "E"|||72 =           "r"|||72 =           "r"|||3a =           ":"|||20 =           " "|||44 =           "D"|||53 =           "S"|||49 =           "I"|||44 =           "D"|||2d =           "-"|||30 =           "0"|||43 =           "C"|||30 =           "0"|||39 =           "9"|||30 =           "0"|||36 =           "6"|||32 =           "2"|||37 =           "7"|||2c =           ","|||20 =           " "|||63 =           "c"|||6f =           "o"|||6d =           "m"|||6d =           "m"|||65 =           "e"|||6e =           "n"|||74 =           "t"|||3a =           ":"|||20 =           " "|||49 =           "I"|||6e =           "n"|||20 =           " "|||6f =           "o"|||72 =           "r"|||64 =           "d"|||65 =           "e"|||72 =           "r"|||20 =           " "|||74 =           "t"          |||6f =           "o"|||20 =           " "|||70 =           "p"|||65 =           "e"|||72 =           "r"|||66 =           "f"|||6f =           "o"|||72 =           "r"|||6d =           "m"|||20 =           " "|||74 =           "t"|||68 =           "h"|||69 =           "i"|||73 =           "s"|||20 =           " "|||6f =           "o"|||70 =           "p"|||65 =           "e"|||72 =           "r"|||61 =           "a"|||74 =           "t"|||69 =           "i"|||6f =           "o"|||6e =           "n"|||20 =           " "|||61 =           "a"|||20 =           " "|||73 =           "s"|||75 =           "u"|||63 =           "c"|||63 =           "c"|||65 =           "e"|||73 =           "s"|||73 =           "s"|||66 =           "f"|||75 =           "u"|||6c =           "l"|||20 =           " "|||62 =           "b"|||69 =           "i"|||6e =           "n"|||64 =           "d"|||20 =           " "|||6d =           "m"|||75 =           "u"|||73 =           "s"|||74 =           "t"|||20 =           " "|||62 =           "b"|||65 =           "e"|||20 =           " "|||63 =           "c"|||6f =           "o"|||6d =           "m"|||70 =           "p"|||6c =           "l"|||65 =           "e"|||74 =           "t"|||65 =           "e"|||64 =           "d"|||20 =           " "|||6f =           "o"|||6e =           "n"|||20 =           " "        |||74 =           "t"|||68 =           "h"|||65 =           "e"|||20 =           " "|||63 =           "c"|||6f =           "o"|||6e =           "n"|||6e =           "n"|||65 =           "e"|||63 =           "c"|||74 =           "t"|||69 =           "i"|||6f =           "o"|||6e =           "n"|||2e =           "."|||2c =           ","|||20 =           " "|||64 =           "d"|||61 =           "a"|||74 =           "t"|||61 =           "a"|||20 =           " "|||30 =           "0"|||2c =           ","|||20 =           " "|||76 =           "v"|||65 =           "e"|||63 =           "c"|||65 =           "e"|||00...

The string of the error message is, "00000000: LdapErr: DSID-0C090627, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, vece". (vece?) This was followed by enough 0x00 bytes to fill out the 167 byte length. What the error actually means is that the request I have made does not match what AD wants from a ping request; since AD does not respond to anonymous requests, the request I sent was not authenticated, and the message I sent did not satisfy the ping, it refused to even try to perform the query.

(Just as an aside, speaking as an old internet protocol guy, ASN.1 is an gargantuan abomination upon the face of the earth; a gigantic bag of ugly that really does nothing to make protocols easier; a tedious piece of kit that does not even satisfy its own marketing. You will notice that, in those two examples, the encoded message includes lengths of sub-messages in two formats: a byte containing a length and a byte containing a flag bit and the length of an integer that specifies the length of the sub-message. In Microsoft's implementation, the second form is always at least four bytes for the integer, although in these examples three of those bytes are zero. Given that and the sub-message types serves to increase the size of the message unnecessarily. Further, ASN.1 is described as being "self-documenting"; given the type-length-value format, it should be possible to simply read the structure of the message without a detailed understanding of that structure such as would be required to understand a TCP or UDP header. However, it is not actually possible to do that: the and filter of the first block is an ASN.1 sequence, but it has a tag which is context-dependent #3, indicating that the value is a sub-message construction but providing no other interpretation of the structure. Likewise, a SearchResultDone is also an ASN.1 sequence; but it has a tag of application-specific, constructed #0. The "constructed" flag should be a good hint, but actually parsing the sub-message is going to require an application-specific understanding of what kind of structure the sub-message represents.)

Next, I will present some actual Java code to search for the SRV records and perform the LDAP ping to identify an Active Directory server.
Site proudly generated by Hakyll.