diff options
Diffstat (limited to '')
-rwxr-xr-x | examples/misc/cldap.pl | 491 |
1 files changed, 491 insertions, 0 deletions
diff --git a/examples/misc/cldap.pl b/examples/misc/cldap.pl new file mode 100755 index 0000000..c33fded --- /dev/null +++ b/examples/misc/cldap.pl @@ -0,0 +1,491 @@ +#!/usr/bin/perl -w + +# Copyright (C) Guenther Deschner <gd@samba.org> 2006 + +use strict; +use IO::Socket; +use Convert::ASN1 qw(:debug); +use Getopt::Long; + +# TODO: timeout handling, user CLDAP query + +################################## + +my $server = ""; +my $domain = ""; +my $host = ""; + +################################## + +my ( + $opt_debug, + $opt_domain, + $opt_help, + $opt_host, + $opt_server, +); + +my %cldap_flags = ( + ADS_PDC => 0x00000001, # DC is PDC + ADS_GC => 0x00000004, # DC is a GC of forest + ADS_LDAP => 0x00000008, # DC is an LDAP server + ADS_DS => 0x00000010, # DC supports DS + ADS_KDC => 0x00000020, # DC is running KDC + ADS_TIMESERV => 0x00000040, # DC is running time services + ADS_CLOSEST => 0x00000080, # DC is closest to client + ADS_WRITABLE => 0x00000100, # DC has writable DS + ADS_GOOD_TIMESERV => 0x00000200, # DC has hardware clock (and running time) + ADS_NDNC => 0x00000400, # DomainName is non-domain NC serviced by LDAP server +); + +my %cldap_samlogon_types = ( + SAMLOGON_AD_UNK_R => 23, + SAMLOGON_AD_R => 25, +); + +my $MAX_DNS_LABEL = 255 + 1; + +my %cldap_netlogon_reply = ( + type => 0, + flags => 0x0, + guid => 0, + forest => undef, + domain => undef, + hostname => undef, + netbios_domain => undef, + netbios_hostname => undef, + unk => undef, + user_name => undef, + server_site_name => undef, + client_site_name => undef, + version => 0, + lmnt_token => 0x0, + lm20_token => 0x0, +); + +sub usage { + print "usage: $0 [--domain|-d domain] [--help] [--host|-h host] [--server|-s server]\n\n"; +} + +sub connect_cldap ($) { + + my $server = shift || return undef; + + return IO::Socket::INET->new( + PeerAddr => $server, + PeerPort => 389, + Proto => 'udp', + Type => SOCK_DGRAM, + Timeout => 10, + ); +} + +sub send_cldap_netlogon ($$$$) { + + my ($sock, $domain, $host, $ntver) = @_; + + my $asn_cldap_req = Convert::ASN1->new; + + $asn_cldap_req->prepare(q< + + SEQUENCE { + msgid INTEGER, + [APPLICATION 3] SEQUENCE { + basedn OCTET STRING, + scope ENUMERATED, + dereference ENUMERATED, + sizelimit INTEGER, + timelimit INTEGER, + attronly BOOLEAN, + [CONTEXT 0] SEQUENCE { + [CONTEXT 3] SEQUENCE { + dnsdom_attr OCTET STRING, + dnsdom_val OCTET STRING + } + [CONTEXT 3] SEQUENCE { + host_attr OCTET STRING, + host_val OCTET STRING + } + [CONTEXT 3] SEQUENCE { + ntver_attr OCTET STRING, + ntver_val OCTET STRING + } + } + SEQUENCE { + netlogon OCTET STRING + } + } + } + >); + + my $pdu_req = $asn_cldap_req->encode( + msgid => 0, + basedn => "", + scope => 0, + dereference => 0, + sizelimit => 0, + timelimit => 0, + attronly => 0, + dnsdom_attr => $domain ? 'DnsDomain' : "", + dnsdom_val => $domain ? $domain : "", + host_attr => 'Host', + host_val => $host, + ntver_attr => 'NtVer', + ntver_val => $ntver, + netlogon => 'NetLogon', + ) || die "failed to encode pdu: $@"; + + if ($opt_debug) { + print"------------\n"; + asn_dump($pdu_req); + print"------------\n"; + } + + return $sock->send($pdu_req) || die "no send: $@"; +} + +# from source/libads/cldap.c : +# +#/* +# These seem to be strings as described in RFC1035 4.1.4 and can be: +# +# - a sequence of labels ending in a zero octet +# - a pointer +# - a sequence of labels ending with a pointer +# +# A label is a byte where the first two bits must be zero and the remaining +# bits represent the length of the label followed by the label itself. +# Therefore, the length of a label is at max 64 bytes. Under RFC1035, a +# sequence of labels cannot exceed 255 bytes. +# +# A pointer consists of a 14 bit offset from the beginning of the data. +# +# struct ptr { +# unsigned ident:2; // must be 11 +# unsigned offset:14; // from the beginning of data +# }; +# +# This is used as a method to compress the packet by eliminated duplicate +# domain components. Since a UDP packet should probably be < 512 bytes and a +# DNS name can be up to 255 bytes, this actually makes a lot of sense. +#*/ + +sub pull_netlogon_string (\$$$) { + + my ($ret, $ptr, $str) = @_; + + my $pos = $ptr; + + my $followed_ptr = 0; + my $ret_len = 0; + + my $retp = pack("x$MAX_DNS_LABEL"); + + do { + + $ptr = unpack("c", substr($str, $pos, 1)); + $pos++; + + if (($ptr & 0xc0) == 0xc0) { + + my $len; + + if (!$followed_ptr) { + $ret_len += 2; + $followed_ptr = 1; + } + + my $tmp0 = $ptr; #unpack("c", substr($str, $pos-1, 1)); + my $tmp1 = unpack("c", substr($str, $pos, 1)); + + if ($opt_debug) { + printf("tmp0: 0x%x\n", $tmp0); + printf("tmp1: 0x%x\n", $tmp1); + } + + $len = (($tmp0 & 0x3f) << 8) | $tmp1; + $ptr = unpack("c", substr($str, $len, 1)); + $pos = $len; + + } elsif ($ptr) { + + my $len = scalar $ptr; + + if ($len + 1 > $MAX_DNS_LABEL) { + warn("invalid string size: %d", $len + 1); + return 0; + } + + $ptr = unpack("a*", substr($str, $pos, $len)); + + $retp = sprintf("%s%s\.", $retp, $ptr); + + $pos += $len; + if (!$followed_ptr) { + $ret_len += $len + 1; + } + } + + } while ($ptr); + + $retp =~ s/\.$//; #ugly hack... + + $$ret = $retp; + + return $followed_ptr ? $ret_len : $ret_len + 1; +} + +sub dump_cldap_flags ($) { + + my $flags = shift || return; + printf("Flags:\n". + "\tIs a PDC: %s\n". + "\tIs a GC of the forest: %s\n". + "\tIs an LDAP server: %s\n". + "\tSupports DS: %s\n". + "\tIs running a KDC: %s\n". + "\tIs running time services: %s\n". + "\tIs the closest DC: %s\n". + "\tIs writable: %s\n". + "\tHas a hardware clock: %s\n". + "\tIs a non-domain NC serviced by LDAP server: %s\n", + ($flags & $cldap_flags{ADS_PDC}) ? "yes" : "no", + ($flags & $cldap_flags{ADS_GC}) ? "yes" : "no", + ($flags & $cldap_flags{ADS_LDAP}) ? "yes" : "no", + ($flags & $cldap_flags{ADS_DS}) ? "yes" : "no", + ($flags & $cldap_flags{ADS_KDC}) ? "yes" : "no", + ($flags & $cldap_flags{ADS_TIMESERV}) ? "yes" : "no", + ($flags & $cldap_flags{ADS_CLOSEST}) ? "yes" : "no", + ($flags & $cldap_flags{ADS_WRITABLE}) ? "yes" : "no", + ($flags & $cldap_flags{ADS_GOOD_TIMESERV}) ? "yes" : "no", + ($flags & $cldap_flags{ADS_NDNC}) ? "yes" : "no"); +} + +sub guid_to_string ($) { + + my $guid = shift || return undef; + if ((my $len = length $guid) != 16) { + printf("invalid length: %d\n", $len); + return undef; + } + my $string = sprintf "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X", + unpack("I", $guid), + unpack("S", substr($guid, 4, 2)), + unpack("S", substr($guid, 6, 2)), + unpack("C", substr($guid, 8, 1)), + unpack("C", substr($guid, 9, 1)), + unpack("C", substr($guid, 10, 1)), + unpack("C", substr($guid, 11, 1)), + unpack("C", substr($guid, 12, 1)), + unpack("C", substr($guid, 13, 1)), + unpack("C", substr($guid, 14, 1)), + unpack("C", substr($guid, 15, 1)); + return lc($string); +} + +sub recv_cldap_netlogon ($\$) { + + my ($sock, $return_string) = @_; + my ($ret, $pdu_out); + + $ret = $sock->recv($pdu_out, 8192) || die "failed to read from socket: $@"; + #$ret = sysread($sock, $pdu_out, 8192); + + if ($opt_debug) { + print"------------\n"; + asn_dump($pdu_out); + print"------------\n"; + } + + my $asn_cldap_rep = Convert::ASN1->new; + my $asn_cldap_rep_fail = Convert::ASN1->new; + + $asn_cldap_rep->prepare(q< + SEQUENCE { + msgid INTEGER, + [APPLICATION 4] SEQUENCE { + dn OCTET STRING, + SEQUENCE { + SEQUENCE { + attr OCTET STRING, + SET { + val OCTET STRING + } + } + } + } + } + SEQUENCE { + msgid2 INTEGER, + [APPLICATION 5] SEQUENCE { + error_code ENUMERATED, + matched_dn OCTET STRING, + error_message OCTET STRING + } + } + >); + + $asn_cldap_rep_fail->prepare(q< + SEQUENCE { + msgid2 INTEGER, + [APPLICATION 5] SEQUENCE { + error_code ENUMERATED, + matched_dn OCTET STRING, + error_message OCTET STRING + } + } + >); + + my $asn1_rep = $asn_cldap_rep->decode($pdu_out) || + $asn_cldap_rep_fail->decode($pdu_out) || + die "failed to decode pdu: $@"; + + if ($asn1_rep->{'error_code'} == 0) { + $$return_string = $asn1_rep->{'val'}; + } + + return $ret; +} + +sub parse_cldap_reply ($) { + + my $str = shift || return undef; + my %hash; + my $p = 0; + + $hash{type} = unpack("L", substr($str, $p, 4)); $p += 4; + $hash{flags} = unpack("L", substr($str, $p, 4)); $p += 4; + $hash{guid} = unpack("a16", substr($str, $p, 16)); $p += 16; + + $p += pull_netlogon_string($hash{forest}, $p, $str); + $p += pull_netlogon_string($hash{domain}, $p, $str); + $p += pull_netlogon_string($hash{hostname}, $p, $str); + $p += pull_netlogon_string($hash{netbios_domain}, $p, $str); + $p += pull_netlogon_string($hash{netbios_hostname}, $p, $str); + $p += pull_netlogon_string($hash{unk}, $p, $str); + + if ($hash{type} == $cldap_samlogon_types{SAMLOGON_AD_R}) { + $p += pull_netlogon_string($hash{user_name}, $p, $str); + } else { + $hash{user_name} = ""; + } + + $p += pull_netlogon_string($hash{server_site_name}, $p, $str); + $p += pull_netlogon_string($hash{client_site_name}, $p, $str); + + $hash{version} = unpack("L", substr($str, $p, 4)); $p += 4; + $hash{lmnt_token} = unpack("S", substr($str, $p, 2)); $p += 2; + $hash{lm20_token} = unpack("S", substr($str, $p, 2)); $p += 2; + + return %hash; +} + +sub display_cldap_reply { + + my $server = shift; + my (%hash) = @_; + + my ($name,$aliases,$addrtype,$length,@addrs) = gethostbyname($server); + + printf("Information for Domain Controller: %s\n\n", $name); + + printf("Response Type: "); + if ($hash{type} == $cldap_samlogon_types{SAMLOGON_AD_R}) { + printf("SAMLOGON_USER\n"); + } elsif ($hash{type} == $cldap_samlogon_types{SAMLOGON_AD_UNK_R}) { + printf("SAMLOGON\n"); + } else { + printf("unknown type 0x%x, please report\n", $hash{type}); + } + + # guid + printf("GUID: %s\n", guid_to_string($hash{guid})); + + # flags + dump_cldap_flags($hash{flags}); + + # strings + printf("Forest:\t\t\t%s\n", $hash{forest}); + printf("Domain:\t\t\t%s\n", $hash{domain}); + printf("Domain Controller:\t%s\n", $hash{hostname}); + + printf("Pre-Win2k Domain:\t%s\n", $hash{netbios_domain}); + printf("Pre-Win2k Hostname:\t%s\n", $hash{netbios_hostname}); + + if ($hash{unk}) { + printf("Unk:\t\t\t%s\n", $hash{unk}); + } + if ($hash{user_name}) { + printf("User name:\t%s\n", $hash{user_name}); + } + + printf("Server Site Name:\t%s\n", $hash{server_site_name}); + printf("Client Site Name:\t%s\n", $hash{client_site_name}); + + # some more int + printf("NT Version:\t\t%d\n", $hash{version}); + printf("LMNT Token:\t\t%.2x\n", $hash{lmnt_token}); + printf("LM20 Token:\t\t%.2x\n", $hash{lm20_token}); +} + +sub main() { + + my ($ret, $sock, $reply); + + GetOptions( + 'debug' => \$opt_debug, + 'domain|d=s' => \$opt_domain, + 'help' => \$opt_help, + 'host|h=s' => \$opt_host, + 'server|s=s' => \$opt_server, + ); + + $server = $server || $opt_server; + $domain = $domain || $opt_domain || undef; + $host = $host || $opt_host; + if (!$host) { + $host = `/bin/hostname`; + chomp($host); + } + + if (!$server || !$host || $opt_help) { + usage(); + exit 1; + } + + my $ntver = sprintf("%c%c%c%c", 6,0,0,0); + + $sock = connect_cldap($server); + if (!$sock) { + die("could not connect to $server"); + } + + $ret = send_cldap_netlogon($sock, $domain, $host, $ntver); + if (!$ret) { + close($sock); + die("failed to send CLDAP request to $server"); + } + + $ret = recv_cldap_netlogon($sock, $reply); + if (!$ret) { + close($sock); + die("failed to receive CLDAP reply from $server"); + } + close($sock); + + if (!$reply) { + printf("no 'NetLogon' attribute received\n"); + exit 0; + } + + %cldap_netlogon_reply = parse_cldap_reply($reply); + if (!%cldap_netlogon_reply) { + die("failed to parse CLDAP reply from $server"); + } + + display_cldap_reply($server, %cldap_netlogon_reply); + + exit 0; +} + +main(); |