1 | #!/usr/bin/perl -w
|
---|
2 |
|
---|
3 | # Copyright (C) Guenther Deschner <gd@samba.org> 2006
|
---|
4 |
|
---|
5 | use strict;
|
---|
6 | use IO::Socket;
|
---|
7 | use Convert::ASN1 qw(:debug);
|
---|
8 | use Getopt::Long;
|
---|
9 |
|
---|
10 | # TODO: timeout handling, user CLDAP query
|
---|
11 |
|
---|
12 | ##################################
|
---|
13 |
|
---|
14 | my $server = "";
|
---|
15 | my $domain = "";
|
---|
16 | my $host = "";
|
---|
17 |
|
---|
18 | ##################################
|
---|
19 |
|
---|
20 | my (
|
---|
21 | $opt_debug,
|
---|
22 | $opt_domain,
|
---|
23 | $opt_help,
|
---|
24 | $opt_host,
|
---|
25 | $opt_server,
|
---|
26 | );
|
---|
27 |
|
---|
28 | my %cldap_flags = (
|
---|
29 | ADS_PDC => 0x00000001, # DC is PDC
|
---|
30 | ADS_GC => 0x00000004, # DC is a GC of forest
|
---|
31 | ADS_LDAP => 0x00000008, # DC is an LDAP server
|
---|
32 | ADS_DS => 0x00000010, # DC supports DS
|
---|
33 | ADS_KDC => 0x00000020, # DC is running KDC
|
---|
34 | ADS_TIMESERV => 0x00000040, # DC is running time services
|
---|
35 | ADS_CLOSEST => 0x00000080, # DC is closest to client
|
---|
36 | ADS_WRITABLE => 0x00000100, # DC has writable DS
|
---|
37 | ADS_GOOD_TIMESERV => 0x00000200, # DC has hardware clock (and running time)
|
---|
38 | ADS_NDNC => 0x00000400, # DomainName is non-domain NC serviced by LDAP server
|
---|
39 | );
|
---|
40 |
|
---|
41 | my %cldap_samlogon_types = (
|
---|
42 | SAMLOGON_AD_UNK_R => 23,
|
---|
43 | SAMLOGON_AD_R => 25,
|
---|
44 | );
|
---|
45 |
|
---|
46 | my $MAX_DNS_LABEL = 255 + 1;
|
---|
47 |
|
---|
48 | my %cldap_netlogon_reply = (
|
---|
49 | type => 0,
|
---|
50 | flags => 0x0,
|
---|
51 | guid => 0,
|
---|
52 | forest => undef,
|
---|
53 | domain => undef,
|
---|
54 | hostname => undef,
|
---|
55 | netbios_domain => undef,
|
---|
56 | netbios_hostname => undef,
|
---|
57 | unk => undef,
|
---|
58 | user_name => undef,
|
---|
59 | server_site_name => undef,
|
---|
60 | client_site_name => undef,
|
---|
61 | version => 0,
|
---|
62 | lmnt_token => 0x0,
|
---|
63 | lm20_token => 0x0,
|
---|
64 | );
|
---|
65 |
|
---|
66 | sub usage {
|
---|
67 | print "usage: $0 [--domain|-d domain] [--help] [--host|-h host] [--server|-s server]\n\n";
|
---|
68 | }
|
---|
69 |
|
---|
70 | sub connect_cldap ($) {
|
---|
71 |
|
---|
72 | my $server = shift || return undef;
|
---|
73 |
|
---|
74 | return IO::Socket::INET->new(
|
---|
75 | PeerAddr => $server,
|
---|
76 | PeerPort => 389,
|
---|
77 | Proto => 'udp',
|
---|
78 | Type => SOCK_DGRAM,
|
---|
79 | Timeout => 10,
|
---|
80 | );
|
---|
81 | }
|
---|
82 |
|
---|
83 | sub send_cldap_netlogon ($$$$) {
|
---|
84 |
|
---|
85 | my ($sock, $domain, $host, $ntver) = @_;
|
---|
86 |
|
---|
87 | my $asn_cldap_req = Convert::ASN1->new;
|
---|
88 |
|
---|
89 | $asn_cldap_req->prepare(q<
|
---|
90 |
|
---|
91 | SEQUENCE {
|
---|
92 | msgid INTEGER,
|
---|
93 | [APPLICATION 3] SEQUENCE {
|
---|
94 | basedn OCTET STRING,
|
---|
95 | scope ENUMERATED,
|
---|
96 | dereference ENUMERATED,
|
---|
97 | sizelimit INTEGER,
|
---|
98 | timelimit INTEGER,
|
---|
99 | attronly BOOLEAN,
|
---|
100 | [CONTEXT 0] SEQUENCE {
|
---|
101 | [CONTEXT 3] SEQUENCE {
|
---|
102 | dnsdom_attr OCTET STRING,
|
---|
103 | dnsdom_val OCTET STRING
|
---|
104 | }
|
---|
105 | [CONTEXT 3] SEQUENCE {
|
---|
106 | host_attr OCTET STRING,
|
---|
107 | host_val OCTET STRING
|
---|
108 | }
|
---|
109 | [CONTEXT 3] SEQUENCE {
|
---|
110 | ntver_attr OCTET STRING,
|
---|
111 | ntver_val OCTET STRING
|
---|
112 | }
|
---|
113 | }
|
---|
114 | SEQUENCE {
|
---|
115 | netlogon OCTET STRING
|
---|
116 | }
|
---|
117 | }
|
---|
118 | }
|
---|
119 | >);
|
---|
120 |
|
---|
121 | my $pdu_req = $asn_cldap_req->encode(
|
---|
122 | msgid => 0,
|
---|
123 | basedn => "",
|
---|
124 | scope => 0,
|
---|
125 | dereference => 0,
|
---|
126 | sizelimit => 0,
|
---|
127 | timelimit => 0,
|
---|
128 | attronly => 0,
|
---|
129 | dnsdom_attr => $domain ? 'DnsDomain' : "",
|
---|
130 | dnsdom_val => $domain ? $domain : "",
|
---|
131 | host_attr => 'Host',
|
---|
132 | host_val => $host,
|
---|
133 | ntver_attr => 'NtVer',
|
---|
134 | ntver_val => $ntver,
|
---|
135 | netlogon => 'NetLogon',
|
---|
136 | ) || die "failed to encode pdu: $@";
|
---|
137 |
|
---|
138 | if ($opt_debug) {
|
---|
139 | print"------------\n";
|
---|
140 | asn_dump($pdu_req);
|
---|
141 | print"------------\n";
|
---|
142 | }
|
---|
143 |
|
---|
144 | return $sock->send($pdu_req) || die "no send: $@";
|
---|
145 | }
|
---|
146 |
|
---|
147 | # from source/libads/cldap.c :
|
---|
148 | #
|
---|
149 | #/*
|
---|
150 | # These seem to be strings as described in RFC1035 4.1.4 and can be:
|
---|
151 | #
|
---|
152 | # - a sequence of labels ending in a zero octet
|
---|
153 | # - a pointer
|
---|
154 | # - a sequence of labels ending with a pointer
|
---|
155 | #
|
---|
156 | # A label is a byte where the first two bits must be zero and the remaining
|
---|
157 | # bits represent the length of the label followed by the label itself.
|
---|
158 | # Therefore, the length of a label is at max 64 bytes. Under RFC1035, a
|
---|
159 | # sequence of labels cannot exceed 255 bytes.
|
---|
160 | #
|
---|
161 | # A pointer consists of a 14 bit offset from the beginning of the data.
|
---|
162 | #
|
---|
163 | # struct ptr {
|
---|
164 | # unsigned ident:2; // must be 11
|
---|
165 | # unsigned offset:14; // from the beginning of data
|
---|
166 | # };
|
---|
167 | #
|
---|
168 | # This is used as a method to compress the packet by eliminated duplicate
|
---|
169 | # domain components. Since a UDP packet should probably be < 512 bytes and a
|
---|
170 | # DNS name can be up to 255 bytes, this actually makes a lot of sense.
|
---|
171 | #*/
|
---|
172 |
|
---|
173 | sub pull_netlogon_string (\$$$) {
|
---|
174 |
|
---|
175 | my ($ret, $ptr, $str) = @_;
|
---|
176 |
|
---|
177 | my $pos = $ptr;
|
---|
178 |
|
---|
179 | my $followed_ptr = 0;
|
---|
180 | my $ret_len = 0;
|
---|
181 |
|
---|
182 | my $retp = pack("x$MAX_DNS_LABEL");
|
---|
183 |
|
---|
184 | do {
|
---|
185 |
|
---|
186 | $ptr = unpack("c", substr($str, $pos, 1));
|
---|
187 | $pos++;
|
---|
188 |
|
---|
189 | if (($ptr & 0xc0) == 0xc0) {
|
---|
190 |
|
---|
191 | my $len;
|
---|
192 |
|
---|
193 | if (!$followed_ptr) {
|
---|
194 | $ret_len += 2;
|
---|
195 | $followed_ptr = 1;
|
---|
196 | }
|
---|
197 |
|
---|
198 | my $tmp0 = $ptr; #unpack("c", substr($str, $pos-1, 1));
|
---|
199 | my $tmp1 = unpack("c", substr($str, $pos, 1));
|
---|
200 |
|
---|
201 | if ($opt_debug) {
|
---|
202 | printf("tmp0: 0x%x\n", $tmp0);
|
---|
203 | printf("tmp1: 0x%x\n", $tmp1);
|
---|
204 | }
|
---|
205 |
|
---|
206 | $len = (($tmp0 & 0x3f) << 8) | $tmp1;
|
---|
207 | $ptr = unpack("c", substr($str, $len, 1));
|
---|
208 | $pos = $len;
|
---|
209 |
|
---|
210 | } elsif ($ptr) {
|
---|
211 |
|
---|
212 | my $len = scalar $ptr;
|
---|
213 |
|
---|
214 | if ($len + 1 > $MAX_DNS_LABEL) {
|
---|
215 | warn("invalid string size: %d", $len + 1);
|
---|
216 | return 0;
|
---|
217 | }
|
---|
218 |
|
---|
219 | $ptr = unpack("a*", substr($str, $pos, $len));
|
---|
220 |
|
---|
221 | $retp = sprintf("%s%s\.", $retp, $ptr);
|
---|
222 |
|
---|
223 | $pos += $len;
|
---|
224 | if (!$followed_ptr) {
|
---|
225 | $ret_len += $len + 1;
|
---|
226 | }
|
---|
227 | }
|
---|
228 |
|
---|
229 | } while ($ptr);
|
---|
230 |
|
---|
231 | $retp =~ s/\.$//; #ugly hack...
|
---|
232 |
|
---|
233 | $$ret = $retp;
|
---|
234 |
|
---|
235 | return $followed_ptr ? $ret_len : $ret_len + 1;
|
---|
236 | }
|
---|
237 |
|
---|
238 | sub dump_cldap_flags ($) {
|
---|
239 |
|
---|
240 | my $flags = shift || return;
|
---|
241 | printf("Flags:\n".
|
---|
242 | "\tIs a PDC: %s\n".
|
---|
243 | "\tIs a GC of the forest: %s\n".
|
---|
244 | "\tIs an LDAP server: %s\n".
|
---|
245 | "\tSupports DS: %s\n".
|
---|
246 | "\tIs running a KDC: %s\n".
|
---|
247 | "\tIs running time services: %s\n".
|
---|
248 | "\tIs the closest DC: %s\n".
|
---|
249 | "\tIs writable: %s\n".
|
---|
250 | "\tHas a hardware clock: %s\n".
|
---|
251 | "\tIs a non-domain NC serviced by LDAP server: %s\n",
|
---|
252 | ($flags & $cldap_flags{ADS_PDC}) ? "yes" : "no",
|
---|
253 | ($flags & $cldap_flags{ADS_GC}) ? "yes" : "no",
|
---|
254 | ($flags & $cldap_flags{ADS_LDAP}) ? "yes" : "no",
|
---|
255 | ($flags & $cldap_flags{ADS_DS}) ? "yes" : "no",
|
---|
256 | ($flags & $cldap_flags{ADS_KDC}) ? "yes" : "no",
|
---|
257 | ($flags & $cldap_flags{ADS_TIMESERV}) ? "yes" : "no",
|
---|
258 | ($flags & $cldap_flags{ADS_CLOSEST}) ? "yes" : "no",
|
---|
259 | ($flags & $cldap_flags{ADS_WRITABLE}) ? "yes" : "no",
|
---|
260 | ($flags & $cldap_flags{ADS_GOOD_TIMESERV}) ? "yes" : "no",
|
---|
261 | ($flags & $cldap_flags{ADS_NDNC}) ? "yes" : "no");
|
---|
262 | }
|
---|
263 |
|
---|
264 | sub guid_to_string ($) {
|
---|
265 |
|
---|
266 | my $guid = shift || return undef;
|
---|
267 | if ((my $len = length $guid) != 16) {
|
---|
268 | printf("invalid length: %d\n", $len);
|
---|
269 | return undef;
|
---|
270 | }
|
---|
271 | my $string = sprintf "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X",
|
---|
272 | unpack("I", $guid),
|
---|
273 | unpack("S", substr($guid, 4, 2)),
|
---|
274 | unpack("S", substr($guid, 6, 2)),
|
---|
275 | unpack("C", substr($guid, 8, 1)),
|
---|
276 | unpack("C", substr($guid, 9, 1)),
|
---|
277 | unpack("C", substr($guid, 10, 1)),
|
---|
278 | unpack("C", substr($guid, 11, 1)),
|
---|
279 | unpack("C", substr($guid, 12, 1)),
|
---|
280 | unpack("C", substr($guid, 13, 1)),
|
---|
281 | unpack("C", substr($guid, 14, 1)),
|
---|
282 | unpack("C", substr($guid, 15, 1));
|
---|
283 | return lc($string);
|
---|
284 | }
|
---|
285 |
|
---|
286 | sub recv_cldap_netlogon ($\$) {
|
---|
287 |
|
---|
288 | my ($sock, $return_string) = @_;
|
---|
289 | my ($ret, $pdu_out);
|
---|
290 |
|
---|
291 | $ret = $sock->recv($pdu_out, 8192) || die "failed to read from socket: $@";
|
---|
292 | #$ret = sysread($sock, $pdu_out, 8192);
|
---|
293 |
|
---|
294 | if ($opt_debug) {
|
---|
295 | print"------------\n";
|
---|
296 | asn_dump($pdu_out);
|
---|
297 | print"------------\n";
|
---|
298 | }
|
---|
299 |
|
---|
300 | my $asn_cldap_rep = Convert::ASN1->new;
|
---|
301 | my $asn_cldap_rep_fail = Convert::ASN1->new;
|
---|
302 |
|
---|
303 | $asn_cldap_rep->prepare(q<
|
---|
304 | SEQUENCE {
|
---|
305 | msgid INTEGER,
|
---|
306 | [APPLICATION 4] SEQUENCE {
|
---|
307 | dn OCTET STRING,
|
---|
308 | SEQUENCE {
|
---|
309 | SEQUENCE {
|
---|
310 | attr OCTET STRING,
|
---|
311 | SET {
|
---|
312 | val OCTET STRING
|
---|
313 | }
|
---|
314 | }
|
---|
315 | }
|
---|
316 | }
|
---|
317 | }
|
---|
318 | SEQUENCE {
|
---|
319 | msgid2 INTEGER,
|
---|
320 | [APPLICATION 5] SEQUENCE {
|
---|
321 | error_code ENUMERATED,
|
---|
322 | matched_dn OCTET STRING,
|
---|
323 | error_message OCTET STRING
|
---|
324 | }
|
---|
325 | }
|
---|
326 | >);
|
---|
327 |
|
---|
328 | $asn_cldap_rep_fail->prepare(q<
|
---|
329 | SEQUENCE {
|
---|
330 | msgid2 INTEGER,
|
---|
331 | [APPLICATION 5] SEQUENCE {
|
---|
332 | error_code ENUMERATED,
|
---|
333 | matched_dn OCTET STRING,
|
---|
334 | error_message OCTET STRING
|
---|
335 | }
|
---|
336 | }
|
---|
337 | >);
|
---|
338 |
|
---|
339 | my $asn1_rep = $asn_cldap_rep->decode($pdu_out) ||
|
---|
340 | $asn_cldap_rep_fail->decode($pdu_out) ||
|
---|
341 | die "failed to decode pdu: $@";
|
---|
342 |
|
---|
343 | if ($asn1_rep->{'error_code'} == 0) {
|
---|
344 | $$return_string = $asn1_rep->{'val'};
|
---|
345 | }
|
---|
346 |
|
---|
347 | return $ret;
|
---|
348 | }
|
---|
349 |
|
---|
350 | sub parse_cldap_reply ($) {
|
---|
351 |
|
---|
352 | my $str = shift || return undef;
|
---|
353 | my %hash;
|
---|
354 | my $p = 0;
|
---|
355 |
|
---|
356 | $hash{type} = unpack("L", substr($str, $p, 4)); $p += 4;
|
---|
357 | $hash{flags} = unpack("L", substr($str, $p, 4)); $p += 4;
|
---|
358 | $hash{guid} = unpack("a16", substr($str, $p, 16)); $p += 16;
|
---|
359 |
|
---|
360 | $p += pull_netlogon_string($hash{forest}, $p, $str);
|
---|
361 | $p += pull_netlogon_string($hash{domain}, $p, $str);
|
---|
362 | $p += pull_netlogon_string($hash{hostname}, $p, $str);
|
---|
363 | $p += pull_netlogon_string($hash{netbios_domain}, $p, $str);
|
---|
364 | $p += pull_netlogon_string($hash{netbios_hostname}, $p, $str);
|
---|
365 | $p += pull_netlogon_string($hash{unk}, $p, $str);
|
---|
366 |
|
---|
367 | if ($hash{type} == $cldap_samlogon_types{SAMLOGON_AD_R}) {
|
---|
368 | $p += pull_netlogon_string($hash{user_name}, $p, $str);
|
---|
369 | } else {
|
---|
370 | $hash{user_name} = "";
|
---|
371 | }
|
---|
372 |
|
---|
373 | $p += pull_netlogon_string($hash{server_site_name}, $p, $str);
|
---|
374 | $p += pull_netlogon_string($hash{client_site_name}, $p, $str);
|
---|
375 |
|
---|
376 | $hash{version} = unpack("L", substr($str, $p, 4)); $p += 4;
|
---|
377 | $hash{lmnt_token} = unpack("S", substr($str, $p, 2)); $p += 2;
|
---|
378 | $hash{lm20_token} = unpack("S", substr($str, $p, 2)); $p += 2;
|
---|
379 |
|
---|
380 | return %hash;
|
---|
381 | }
|
---|
382 |
|
---|
383 | sub display_cldap_reply {
|
---|
384 |
|
---|
385 | my $server = shift;
|
---|
386 | my (%hash) = @_;
|
---|
387 |
|
---|
388 | my ($name,$aliases,$addrtype,$length,@addrs) = gethostbyname($server);
|
---|
389 |
|
---|
390 | printf("Information for Domain Controller: %s\n\n", $name);
|
---|
391 |
|
---|
392 | printf("Response Type: ");
|
---|
393 | if ($hash{type} == $cldap_samlogon_types{SAMLOGON_AD_R}) {
|
---|
394 | printf("SAMLOGON_USER\n");
|
---|
395 | } elsif ($hash{type} == $cldap_samlogon_types{SAMLOGON_AD_UNK_R}) {
|
---|
396 | printf("SAMLOGON\n");
|
---|
397 | } else {
|
---|
398 | printf("unknown type 0x%x, please report\n", $hash{type});
|
---|
399 | }
|
---|
400 |
|
---|
401 | # guid
|
---|
402 | printf("GUID: %s\n", guid_to_string($hash{guid}));
|
---|
403 |
|
---|
404 | # flags
|
---|
405 | dump_cldap_flags($hash{flags});
|
---|
406 |
|
---|
407 | # strings
|
---|
408 | printf("Forest:\t\t\t%s\n", $hash{forest});
|
---|
409 | printf("Domain:\t\t\t%s\n", $hash{domain});
|
---|
410 | printf("Domain Controller:\t%s\n", $hash{hostname});
|
---|
411 |
|
---|
412 | printf("Pre-Win2k Domain:\t%s\n", $hash{netbios_domain});
|
---|
413 | printf("Pre-Win2k Hostname:\t%s\n", $hash{netbios_hostname});
|
---|
414 |
|
---|
415 | if ($hash{unk}) {
|
---|
416 | printf("Unk:\t\t\t%s\n", $hash{unk});
|
---|
417 | }
|
---|
418 | if ($hash{user_name}) {
|
---|
419 | printf("User name:\t%s\n", $hash{user_name});
|
---|
420 | }
|
---|
421 |
|
---|
422 | printf("Server Site Name:\t%s\n", $hash{server_site_name});
|
---|
423 | printf("Client Site Name:\t%s\n", $hash{client_site_name});
|
---|
424 |
|
---|
425 | # some more int
|
---|
426 | printf("NT Version:\t\t%d\n", $hash{version});
|
---|
427 | printf("LMNT Token:\t\t%.2x\n", $hash{lmnt_token});
|
---|
428 | printf("LM20 Token:\t\t%.2x\n", $hash{lm20_token});
|
---|
429 | }
|
---|
430 |
|
---|
431 | sub main() {
|
---|
432 |
|
---|
433 | my ($ret, $sock, $reply);
|
---|
434 |
|
---|
435 | GetOptions(
|
---|
436 | 'debug' => \$opt_debug,
|
---|
437 | 'domain|d=s' => \$opt_domain,
|
---|
438 | 'help' => \$opt_help,
|
---|
439 | 'host|h=s' => \$opt_host,
|
---|
440 | 'server|s=s' => \$opt_server,
|
---|
441 | );
|
---|
442 |
|
---|
443 | $server = $server || $opt_server;
|
---|
444 | $domain = $domain || $opt_domain || undef;
|
---|
445 | $host = $host || $opt_host;
|
---|
446 | if (!$host) {
|
---|
447 | $host = `/bin/hostname`;
|
---|
448 | chomp($host);
|
---|
449 | }
|
---|
450 |
|
---|
451 | if (!$server || !$host || $opt_help) {
|
---|
452 | usage();
|
---|
453 | exit 1;
|
---|
454 | }
|
---|
455 |
|
---|
456 | my $ntver = sprintf("%c%c%c%c", 6,0,0,0);
|
---|
457 |
|
---|
458 | $sock = connect_cldap($server);
|
---|
459 | if (!$sock) {
|
---|
460 | die("could not connect to $server");
|
---|
461 | }
|
---|
462 |
|
---|
463 | $ret = send_cldap_netlogon($sock, $domain, $host, $ntver);
|
---|
464 | if (!$ret) {
|
---|
465 | close($sock);
|
---|
466 | die("failed to send CLDAP request to $server");
|
---|
467 | }
|
---|
468 |
|
---|
469 | $ret = recv_cldap_netlogon($sock, $reply);
|
---|
470 | if (!$ret) {
|
---|
471 | close($sock);
|
---|
472 | die("failed to receive CLDAP reply from $server");
|
---|
473 | }
|
---|
474 | close($sock);
|
---|
475 |
|
---|
476 | if (!$reply) {
|
---|
477 | printf("no 'NetLogon' attribute received\n");
|
---|
478 | exit 0;
|
---|
479 | }
|
---|
480 |
|
---|
481 | %cldap_netlogon_reply = parse_cldap_reply($reply);
|
---|
482 | if (!%cldap_netlogon_reply) {
|
---|
483 | die("failed to parse CLDAP reply from $server");
|
---|
484 | }
|
---|
485 |
|
---|
486 | display_cldap_reply($server, %cldap_netlogon_reply);
|
---|
487 |
|
---|
488 | exit 0;
|
---|
489 | }
|
---|
490 |
|
---|
491 | main();
|
---|