Authenticating against an Active Directory Server, pt. 2

Posted on September 17, 2009 by Tommy McGuire
Labels: notation, authentication, protocols, http, ldap, active directory, java
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 44
6E 73 44 6F 6D 61 69 66 04 18 61 62 63 64 65
2E 63 6F 72 70 2E 6D 69 63 72 6F 73 6F 66 74
2E 63 6F 6D A3 84 00 00 00 14 04 04 48 6F 73
74 04 0C 61 62 63 64 65 66 67 68 2D 64 65 76
A3 84 00 00 00 15 04 04 55 73 65 72 04 0D 61
62 63 64 65 66 67 68 2D 64 65 76 24 A3 84 00
00 00 0B 04 03 41 41 43 04 04 80 00 00 00 A3
84 00 00 00 1E 04 0A 44 6F 6D 61 69 6E 47 75
69 64 04 10 3B B0 21 CA D3 6D D1 11 8A 7D B8
DF B1 56 87 1F A3 84 00 00 00 0D 04 05 4E 74
56 65 72 04 04 06 00 00 00 30 84 00 00 00 0A
04 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.
active directory applied formal logic ashurbanipal authentication books c c++ comics conference continuations coq data structure digital humanities Dijkstra eclipse virgo electronics emacs goodreads haskell http java job Knuth ldap link linux lisp math naming nimrod notation OpenAM osgi parsing pony programming language protocols python quote R random REST ruby rust SAML scala scheme shell software development system administration theory tip toy problems unix vmware yeti
Member of The Internet Defense League
Site proudly generated by Hakyll.