File Coverage

blib/lib/Authen/NTLM.pm
Criterion Covered Total %
statement 188 203 92.6
branch 22 40 55.0
condition 4 5 80.0
subroutine 27 32 84.3
pod 8 23 34.7
total 249 303 82.1


line stmt bran cond sub pod time code
1             #!/usr/local/bin/perl
2              
3             package Authen::NTLM;
4 4     4   194310 use strict;
  4         12  
  4         186  
5 4     4   4559 use Authen::NTLM::DES;
  4         13  
  4         851  
6 4     4   25622 use Authen::NTLM::MD4;
  4         11  
  4         655  
7 4     4   5793 use MIME::Base64;
  4         13068  
  4         349  
8 4     4   6661 use Digest::HMAC_MD5;
  4         41486  
  4         363  
9              
10 4     4   43 use vars qw($VERSION @ISA @EXPORT);
  4         10  
  4         1286  
11             require Exporter;
12              
13             =head1 NAME
14              
15             Authen::NTLM - An NTLM authentication module
16              
17             =head1 SYNOPSIS
18              
19             use Mail::IMAPClient;
20             use Authen::NTLM;
21             my $imap = Mail::IMAPClient->new(Server=>'imaphost');
22             ntlm_user($username);
23             ntlm_password($password);
24             $imap->authenticate("NTLM", Authen::NTLM::ntlm);
25             :
26             $imap->logout;
27             ntlm_reset;
28             :
29              
30             or
31              
32             ntlmv2(1);
33             ntlm_user($username);
34             ntlm_host($host);
35             ntlm_password($password);
36             :
37              
38             or
39              
40             my $ntlm = Authen::NTLM-> new(
41             host => $host,
42             user => $username,
43             domain => $domain,
44             password => $password,
45             version => 1,
46             );
47             $ntlm-> challenge;
48             :
49             $ntlm-> challenge($challenge);
50              
51              
52              
53             =head1 DESCRIPTION
54              
55             This module provides methods to use NTLM authentication. It can
56             be used as an authenticate method with the Mail::IMAPClient module
57             to perform the challenge/response mechanism for NTLM connections
58             or it can be used on its own for NTLM authentication with other
59             protocols (eg. HTTP).
60              
61             The implementation is a direct port of the code from F
62             which, itself, has based its NTLM implementation on F. As
63             such, this code is not especially efficient, however it will still
64             take a fraction of a second to negotiate a login on a PII which is
65             likely to be good enough for most situations.
66              
67             =head2 FUNCTIONS
68              
69             =over 4
70              
71             =item ntlm_domain()
72              
73             Set the domain to use in the NTLM authentication messages.
74             Returns the new domain. Without an argument, this function
75             returns the current domain entry.
76              
77             =item ntlm_user()
78              
79             Set the username to use in the NTLM authentication messages.
80             Returns the new username. Without an argument, this function
81             returns the current username entry.
82              
83             =item ntlm_password()
84              
85             Set the password to use in the NTLM authentication messages.
86             Returns the new password. Without an argument, this function
87             returns the current password entry.
88              
89             =item ntlm_reset()
90              
91             Resets the NTLM challenge/response state machine so that the next
92             call to C will produce an initial connect message.
93              
94             =item ntlm()
95              
96             Generate a reply to a challenge. The NTLM protocol involves an
97             initial empty challenge from the server requiring a message
98             response containing the username and domain (which may be empty).
99             The first call to C generates this first message ignoring
100             any arguments.
101              
102             The second time it is called, it is assumed that the argument is
103             the challenge string sent from the server. This will contain 8
104             bytes of data which are used in the DES functions to generate the
105             response authentication strings. The result of the call is the
106             final authentication string.
107              
108             If C is called, then the next call to C will
109             start the process again allowing multiple authentications within
110             an application.
111              
112             =item ntlmv2()
113              
114             Use NTLM v2 authentication.
115              
116             =back
117              
118             =head2 OBJECT API
119              
120             =over
121              
122             =item new %options
123              
124             Creates an object that accepts the following options: C, C,
125             C, C, C.
126              
127             =item challenge [$challenge]
128              
129             If C<$challenge> is not supplied, first-stage challenge string is generated.
130             Otherwise, the third-stage challenge is generated, where C<$challenge> is
131             assumed to be extracted from the second stage of NTLM exchange. The result of
132             the call is the final authentication string.
133              
134             =back
135              
136             =head1 AUTHOR
137              
138             David (Buzz) Bussenschutt - current maintainer
139             Dmitry Karasik - nice ntlmv2 patch, OO extensions.
140             Andrew Hobson - initial ntlmv2 code
141             Mark Bush - perl port
142             Eric S. Raymond - author of fetchmail
143             Andrew Tridgell and Jeremy Allison for SMB/Netbios code
144              
145             =head1 SEE ALSO
146              
147             L, L, L
148              
149             =head1 HISTORY
150              
151             1.09 - fix CPAN ticket # 70703
152             1.08 - fix CPAN ticket # 39925
153             1.07 - not publicly released
154             1.06 - relicense as GPL+ or Artistic
155             1.05 - add OO interface by Dmitry Karasik
156             1.04 - implementation of NTLMv2 by Andrew Hobson/Dmitry Karasik
157             1.03 - fixes long-standing 1 line bug L - released by David Bussenschutt 9th Aug 2007
158             1.02 - released by Mark Bush 29th Oct 2001
159              
160             =cut
161              
162             $VERSION = "1.09";
163             @ISA = qw(Exporter);
164             @EXPORT = qw(ntlm ntlm_domain ntlm_user ntlm_password ntlm_reset ntlm_host ntlmv2);
165              
166             my $domain = "";
167             my $user = "";
168             my $password = "";
169              
170             my $str_hdr = "vvV";
171             my $hdr_len = 8;
172             my $ident = "NTLMSSP";
173              
174             my $msg1_f = 0x0000b207;
175             my $msg1 = "Z8VV";
176             my $msg1_hlen = 16 + ($hdr_len*2);
177              
178             my $msg2 = "Z8Va${hdr_len}Va8a8a${hdr_len}";
179             my $msg2_hlen = 12 + $hdr_len + 20 + $hdr_len;
180              
181             my $msg3 = "Z8V";
182             my $msg3_tl = "V";
183             my $msg3_hlen = 12 + ($hdr_len*6) + 4;
184              
185             my $state = 0;
186              
187             my $host = "";
188             my $ntlm_v2 = 0;
189             my $ntlm_v2_msg3_flags = 0x88205;
190              
191              
192             # Domain Name supplied on negotiate
193 4     4   28 use constant NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED => 0x00001000;
  4         11  
  4         981  
194             # Workstation Name supplied on negotiate
195 4     4   30 use constant NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED => 0x00002000;
  4         11  
  4         217  
196             # Try to use NTLMv2
197 4     4   22 use constant NTLMSSP_NEGOTIATE_NTLM2 => 0x00080000;
  4         18  
  4         65776  
198              
199              
200             # Object API
201              
202             sub new
203             {
204 2     2 1 47 my ( $class, %opt) = @_;
205 2         8 for (qw(domain user password host)) {
206 8 100       28 $opt{$_} = "" unless defined $opt{$_};
207             }
208 2   100     15 $opt{version} ||= 1;
209 2         21 return bless { %opt }, $class;
210             }
211              
212             sub challenge
213             {
214 4     4 1 7980 my ( $self, $challenge) = @_;
215 4         11 $state = defined $challenge;
216 4         10 ($user,$domain,$password,$host) = @{$self}{qw(user domain password host)};
  4         31  
217 4 100       104 $ntlm_v2 = ($self-> {version} eq '2') ? 1 : 0;
218 4         17 return ntlm($challenge);
219             }
220              
221             eval "sub $_ { \$#_ ? \$_[0]->{$_} = \$_[1] : \$_[0]->{$_} }"
222 1 50   1 0 10 for qw(user domain password host version);
  0 0   0 0 0  
  1 50   1 0 15  
  1 50   1 0 65  
  0 0   0 0 0  
223              
224             # Function API
225              
226             sub ntlm_domain
227             {
228 1 50   1 1 6 if (@_)
229             {
230 1         3 $domain = shift;
231             }
232 1         7 return $domain;
233             }
234              
235             sub ntlm_user
236             {
237 1 50   1 1 17 if (@_)
238             {
239 1         4 $user = shift;
240             }
241 1         8 return $user;
242             }
243              
244             sub ntlm_password
245             {
246 1 50   1 1 13 if (@_)
247             {
248 1         17 $password = shift;
249             }
250 1         5 return $password;
251             }
252              
253             sub ntlm_reset
254             {
255 0     0 1 0 $state = 0;
256             }
257              
258             sub ntlmv2
259             {
260 0 0   0 1 0 if (@_) {
261 0         0 $ntlm_v2 = shift;
262             }
263 0         0 return $ntlm_v2;
264             }
265              
266             sub ntlm_host {
267 0 0   0 0 0 if (@_) {
268 0         0 $host = shift;
269             }
270 0         0 return $host;
271             }
272              
273             sub ntlm
274             {
275 6     6 1 2817 my ($challenge) = @_;
276              
277 6         16 my ($flags, $user_hdr, $domain_hdr,
278             $u_off, $d_off, $c_info, $lmResp, $ntResp, $lm_hdr,
279             $nt_hdr, $wks_hdr, $session_hdr, $lm_off, $nt_off,
280             $wks_off, $s_off, $u_user, $msg1_host, $host_hdr, $u_host);
281 0         0 my $response;
282 6 100       23 if ($state)
283             {
284              
285 3         20 $challenge =~ s/^\s*//;
286 3         19 $challenge = decode_base64($challenge);
287 3         154 $c_info = &decode_challenge($challenge);
288 3         14 $u_user = &unicode($user);
289 3 100       15 if (!$ntlm_v2) {
    50          
290 2         10 $domain = substr($challenge, $c_info->{domain}{offset}, $c_info->{domain}{len});
291 2         11 $lmResp = &lmEncrypt($c_info->{data});
292 2         15 $ntResp = &ntEncrypt($c_info->{data});
293 2         14 $flags = pack($msg3_tl, $c_info->{flags});
294             }
295             elsif ($ntlm_v2 eq '1') {
296 1         22 $lmResp = &lmv2Encrypt($c_info->{data});
297 1         14 $ntResp = &ntv2Encrypt($c_info->{data}, $c_info->{target_data});
298 1         6 $flags = pack($msg3_tl, $ntlm_v2_msg3_flags);
299             }
300             else {
301 0         0 $domain = &unicode($domain);#substr($challenge, $c_info->{domain}{offset}, $c_info->{domain}{len});
302 0         0 $lmResp = &lmEncrypt($c_info->{data});
303 0         0 $ntResp = &ntEncrypt($c_info->{data});
304 0         0 $flags = pack($msg3_tl, $c_info->{flags});
305             }
306 3 50       22 $u_host = &unicode(($host ? $host : $user));
307 3         18 $response = pack($msg3, $ident, 3);
308              
309 3         7 $lm_off = $msg3_hlen;
310 3         10 $nt_off = $lm_off + length($lmResp);
311 3         6 $d_off = $nt_off + length($ntResp);
312 3         7 $u_off = $d_off + length($domain);
313 3         8 $wks_off = $u_off + length($u_user);
314 3         8 $s_off = $wks_off + length($u_host);
315 3         17 $lm_hdr = &hdr($lmResp, $msg3_hlen, $lm_off);
316 3         48 $nt_hdr = &hdr($ntResp, $msg3_hlen, $nt_off);
317 3         25 $domain_hdr = &hdr($domain, $msg3_hlen, $d_off);
318 3         11 $user_hdr = &hdr($u_user, $msg3_hlen, $u_off);
319 3         10 $wks_hdr = &hdr($u_host, $msg3_hlen, $wks_off);
320 3         12 $session_hdr = &hdr("", $msg3_hlen, $s_off);
321 3         23 $response .= $lm_hdr . $nt_hdr . $domain_hdr . $user_hdr .
322             $wks_hdr . $session_hdr . $flags .
323             $lmResp . $ntResp . $domain . $u_user . $u_host;
324             }
325             else # first response;
326             {
327 3         7 my $f = $msg1_f;
328 3 50       25 if (!length $domain) {
329 0         0 $f &= ~NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED;
330             }
331 3         9 $msg1_host = $user;
332 3 100 66     22 if ($ntlm_v2 and $ntlm_v2 eq '1') {
333 1         3 $f &= ~NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED;
334 1         2 $f |= NTLMSSP_NEGOTIATE_NTLM2;
335 1         2 $msg1_host = "";
336             }
337              
338 3         28 $response = pack($msg1, $ident, 1, $f);
339 3         7 $u_off = $msg1_hlen;
340 3         7 $d_off = $u_off + length($msg1_host);
341 3         15 $host_hdr = &hdr($msg1_host, $msg1_hlen, $u_off);
342 3         13 $domain_hdr = &hdr($domain, $msg1_hlen, $d_off);
343 3         12 $response .= $host_hdr . $domain_hdr . $msg1_host . $domain;
344 3         6 $state = 1;
345             }
346 6         106 return encode_base64($response, "");
347             }
348              
349             sub hdr
350             {
351 24     24 0 46 my ($string, $h_len, $offset) = @_;
352              
353 24         29 my ($res, $len);
354 24         32 $len = length($string);
355 24 100       53 if ($string)
356             {
357 20         55 $res = pack($str_hdr, $len, $len, $offset);
358             }
359             else
360             {
361 4         13 $res = pack($str_hdr, 0, 0, $offset - $h_len);
362             }
363 24         63 return $res;
364             }
365              
366             sub decode_challenge
367             {
368 6     6 0 2275 my ($challenge) = @_;
369              
370 6         11 my $res;
371 6         10 my (@res, @hdr);
372 6         11 my $original = $challenge;
373              
374 6 50       38 $res->{buffer} = $msg2_hlen < length $challenge
375             ? substr($challenge, $msg2_hlen) : '';
376 6         14 $challenge = substr($challenge, 0, $msg2_hlen);
377 6         73 @res = unpack($msg2, $challenge);
378 6         18 $res->{ident} = $res[0];
379 6         12 $res->{type} = $res[1];
380 6         21 @hdr = unpack($str_hdr, $res[2]);
381 6         21 $res->{domain}{len} = $hdr[0];
382 6         58 $res->{domain}{maxlen} = $hdr[1];
383 6         27 $res->{domain}{offset} = $hdr[2];
384 6         14 $res->{flags} = $res[3];
385 6         31 $res->{data} = $res[4];
386 6         15 $res->{reserved} = $res[5];
387 6         44 $res->{empty_hdr} = $res[6];
388 6         21 @hdr = unpack($str_hdr, $res[6]);
389 6         16 $res->{target}{len} = $hdr[0];
390 6         13 $res->{target}{maxlen} = $hdr[1];
391 6         14 $res->{target}{offset} = $hdr[2];
392 6         19 $res->{target_data} = substr($original, $hdr[2], $hdr[1]);
393              
394 6         20 return $res;
395             }
396              
397             sub unicode
398             {
399 12     12 0 32 my ($string) = @_;
400 12         26 my ($reply, $c, $z) = ('');
401              
402 12         20 $z = sprintf "%c", 0;
403 12         64 foreach $c (split //, $string)
404             {
405 48         90 $reply .= $c . $z;
406             }
407 12         39 return $reply;
408             }
409              
410             sub NTunicode
411             {
412 2     2 0 6 my ($string) = @_;
413 2         4 my ($reply, $c);
414              
415 2         14 foreach $c (map {ord($_)} split(//, $string))
  8         20  
416             {
417 8         21 $reply .= pack("v", $c);
418             }
419 2         6 return $reply;
420             }
421              
422             sub lmEncrypt
423             {
424 2     2 0 4 my ($data) = @_;
425              
426 2         8 my $p14 = substr($password, 0, 14);
427 2         6 $p14 =~ tr/a-z/A-Z/;
428 2         9 $p14 .= "\0"x(14-length($p14));
429 2         20 my $p21 = E_P16($p14);
430 2         12 $p21 .= "\0"x(21-length($p21));
431 2         13 my $p24 = E_P24($p21, $data);
432 2         18 return $p24;
433             }
434              
435             sub ntEncrypt
436             {
437 2     2 0 6 my ($data) = @_;
438              
439 2         10 my $p21 = &E_md4hash;
440 2         11 $p21 .= "\0"x(21-length($p21));
441 2         11 my $p24 = E_P24($p21, $data);
442 2         19 return $p24;
443             }
444              
445             sub E_md4hash
446             {
447 2     2 0 14 my $wpwd = &NTunicode($password);
448 2         17 my $p16 = mdfour($wpwd);
449 2         7 return $p16;
450             }
451              
452             sub lmv2Encrypt {
453 1     1 0 5 my ($data) = @_;
454              
455 1         3 my $u_pass = &unicode($password);
456 1         7 my $ntlm_hash = mdfour($u_pass);
457              
458 1         16 my $u_user = &unicode("\U$user\E");
459 1         4 my $u_domain = &unicode("$domain");
460 1         4 my $concat = $u_user . $u_domain;
461              
462 1         13 my $hmac = Digest::HMAC_MD5->new($ntlm_hash);
463 1         123 $hmac->add($concat);
464 1         15 my $ntlm_v2_hash = $hmac->digest;
465              
466             # Firefox seems to use this as its random challenge
467 1         24 my $random_challenge = "\0" x 8;
468              
469 1         3 my $concat2 = $data . $random_challenge;
470              
471 1         5 $hmac = Digest::HMAC_MD5->new($ntlm_v2_hash);
472 1         46 $hmac->add(substr($data, 0, 8) . $random_challenge);
473 1         9 my $r = $hmac->digest . $random_challenge;
474              
475 1         17 return $r;
476             }
477              
478             sub ntv2Encrypt {
479 1     1 0 2 my ($data, $target) = @_;
480              
481 1         12 my $u_pass = &unicode($password);
482 1         5 my $ntlm_hash = mdfour($u_pass);
483              
484 1         9 my $u_user = &unicode("\U$user\E");
485 1         6 my $u_domain = &unicode("$domain");
486 1         4 my $concat = $u_user . $u_domain;
487              
488 1         100 my $hmac = Digest::HMAC_MD5->new($ntlm_hash);
489 1         52 $hmac->add($concat);
490 1         11 my $ntlm_v2_hash = $hmac->digest;
491              
492 1         17 my $zero_long = "\000" x 4;
493 1         3 my $sig = pack("H8", "01010000");
494 1         8 my $time = pack("VV", (time + 11644473600) + 10000000);
495 1         6 my $rand = "\0" x 8;
496 1         4 my $blob = $sig . $zero_long . $time . $rand . $zero_long .
497             $target . $zero_long;
498              
499 1         4 $concat = $data . $blob;
500              
501 1         5 $hmac = Digest::HMAC_MD5->new($ntlm_v2_hash);
502 1         41 $hmac->add($concat);
503              
504 1         8 my $d = $hmac->digest;
505              
506 1         16 my $r = $d . $blob;
507              
508 1         7 return $r;
509             }
510              
511             1;