1#!/usr/bin/perl -w 2 3# Copyright (C) Guenther Deschner <gd@samba.org> 2006 4 5use strict; 6use IO::Socket; 7use Convert::ASN1 qw(:debug); 8use Getopt::Long; 9 10# TODO: timeout handling, user CLDAP query 11 12################################## 13 14my $server = ""; 15my $domain = ""; 16my $host = ""; 17 18################################## 19 20my ( 21 $opt_debug, 22 $opt_domain, 23 $opt_help, 24 $opt_host, 25 $opt_server, 26); 27 28my %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 41my %cldap_samlogon_types = ( 42 SAMLOGON_AD_UNK_R => 23, 43 SAMLOGON_AD_R => 25, 44); 45 46my $MAX_DNS_LABEL = 255 + 1; 47 48my %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 66sub usage { 67 print "usage: $0 [--domain|-d domain] [--help] [--host|-h host] [--server|-s server]\n\n"; 68} 69 70sub 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 83sub 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 173sub 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 238sub 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 264sub 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 286sub 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 350sub 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 383sub 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 431sub 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 491main(); 492