TYPO3  7.6
NTLMAuthenticator.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of SwiftMailer.
5  * (c) 2004-2009 Chris Corbyn
6  *
7  * This authentication is for Exchange servers. We support version 1 & 2.
8  *
9  * For the full copyright and license information, please view the LICENSE
10  * file that was distributed with this source code.
11  */
12 
19 {
20  const NTLMSIG = "NTLMSSP\x00";
21  const DESCONST = 'KGS!@#$%';
22 
28  public function getAuthKeyword()
29  {
30  return 'NTLM';
31  }
32 
42  public function authenticate(Swift_Transport_SmtpAgent $agent, $username, $password)
43  {
44  if (!function_exists('mcrypt_module_open')) {
45  throw new LogicException('The mcrypt functions need to be enabled to use the NTLM authenticator.');
46  }
47 
48  if (!function_exists('openssl_random_pseudo_bytes')) {
49  throw new LogicException('The OpenSSL extension must be enabled to use the NTLM authenticator.');
50  }
51 
52  if (!function_exists('bcmul')) {
53  throw new LogicException('The BCMatch functions must be enabled to use the NTLM authenticator.');
54  }
55 
56  try {
57  // execute AUTH command and filter out the code at the beginning
58  // AUTH NTLM xxxx
59  $response = base64_decode(substr(trim($this->sendMessage1($agent)), 4));
60 
61  // extra parameters for our unit cases
62  $timestamp = func_num_args() > 3 ? func_get_arg(3) : $this->getCorrectTimestamp(bcmul(microtime(true), '1000'));
63  $client = func_num_args() > 4 ? func_get_arg(4) : $this->getRandomBytes(8);
64 
65  // Message 3 response
66  $this->sendMessage3($response, $username, $password, $timestamp, $client, $agent);
67 
68  return true;
69  } catch (Swift_TransportException $e) {
70  $agent->executeCommand("RSET\r\n", array(250));
71 
72  return false;
73  }
74  }
75 
76  protected function si2bin($si, $bits = 32)
77  {
78  $bin = null;
79  if ($si >= -pow(2, $bits - 1) && ($si <= pow(2, $bits - 1))) {
80  // positive or zero
81  if ($si >= 0) {
82  $bin = base_convert($si, 10, 2);
83  // pad to $bits bit
84  $bin_length = strlen($bin);
85  if ($bin_length < $bits) {
86  $bin = str_repeat('0', $bits - $bin_length).$bin;
87  }
88  } else {
89  // negative
90  $si = -$si - pow(2, $bits);
91  $bin = base_convert($si, 10, 2);
92  $bin_length = strlen($bin);
93  if ($bin_length > $bits) {
94  $bin = str_repeat('1', $bits - $bin_length).$bin;
95  }
96  }
97  }
98 
99  return $bin;
100  }
101 
109  protected function sendMessage1(Swift_Transport_SmtpAgent $agent)
110  {
111  $message = $this->createMessage1();
112 
113  return $agent->executeCommand(sprintf("AUTH %s %s\r\n", $this->getAuthKeyword(), base64_encode($message)), array(334));
114  }
115 
123  protected function parseMessage2($response)
124  {
125  $responseHex = bin2hex($response);
126  $length = floor(hexdec(substr($responseHex, 28, 4)) / 256) * 2;
127  $offset = floor(hexdec(substr($responseHex, 32, 4)) / 256) * 2;
128  $challenge = $this->hex2bin(substr($responseHex, 48, 16));
129  $context = $this->hex2bin(substr($responseHex, 64, 16));
130  $targetInfoH = $this->hex2bin(substr($responseHex, 80, 16));
131  $targetName = $this->hex2bin(substr($responseHex, $offset, $length));
132  $offset = floor(hexdec(substr($responseHex, 88, 4)) / 256) * 2;
133  $targetInfoBlock = substr($responseHex, $offset);
134  list($domainName, $serverName, $DNSDomainName, $DNSServerName, $terminatorByte) = $this->readSubBlock($targetInfoBlock);
135 
136  return array(
137  $challenge,
138  $context,
139  $targetInfoH,
140  $targetName,
141  $domainName,
142  $serverName,
143  $DNSDomainName,
144  $DNSServerName,
145  $this->hex2bin($targetInfoBlock),
146  $terminatorByte,
147  );
148  }
149 
157  protected function readSubBlock($block)
158  {
159  // remove terminatorByte cause it's always the same
160  $block = substr($block, 0, -8);
161 
162  $length = strlen($block);
163  $offset = 0;
164  $data = array();
165  while ($offset < $length) {
166  $blockLength = hexdec(substr(substr($block, $offset, 8), -4)) / 256;
167  $offset += 8;
168  $data[] = $this->hex2bin(substr($block, $offset, $blockLength * 2));
169  $offset += $blockLength * 2;
170  }
171 
172  if (count($data) == 3) {
173  $data[] = $data[2];
174  $data[2] = '';
175  }
176 
177  $data[] = $this->createByte('00');
178 
179  return $data;
180  }
181 
195  protected function sendMessage3($response, $username, $password, $timestamp, $client, Swift_Transport_SmtpAgent $agent, $v2 = true)
196  {
197  list($domain, $username) = $this->getDomainAndUsername($username);
198  //$challenge, $context, $targetInfoH, $targetName, $domainName, $workstation, $DNSDomainName, $DNSServerName, $blob, $ter
199  list($challenge, , , , , $workstation, , , $blob) = $this->parseMessage2($response);
200 
201  if (!$v2) {
202  // LMv1
203  $lmResponse = $this->createLMPassword($password, $challenge);
204  // NTLMv1
205  $ntlmResponse = $this->createNTLMPassword($password, $challenge);
206  } else {
207  // LMv2
208  $lmResponse = $this->createLMv2Password($password, $username, $domain, $challenge, $client);
209  // NTLMv2
210  $ntlmResponse = $this->createNTLMv2Hash($password, $username, $domain, $challenge, $blob, $timestamp, $client);
211  }
212 
213  $message = $this->createMessage3($domain, $username, $workstation, $lmResponse, $ntlmResponse);
214 
215  return $agent->executeCommand(sprintf("%s\r\n", base64_encode($message)), array(235));
216  }
217 
223  protected function createMessage1()
224  {
225  return self::NTLMSIG
226  .$this->createByte('01') // Message 1
227 .$this->createByte('0702'); // Flags
228  }
229 
241  protected function createMessage3($domain, $username, $workstation, $lmResponse, $ntlmResponse)
242  {
243  // Create security buffers
244  $domainSec = $this->createSecurityBuffer($domain, 64);
245  $domainInfo = $this->readSecurityBuffer(bin2hex($domainSec));
246  $userSec = $this->createSecurityBuffer($username, ($domainInfo[0] + $domainInfo[1]) / 2);
247  $userInfo = $this->readSecurityBuffer(bin2hex($userSec));
248  $workSec = $this->createSecurityBuffer($workstation, ($userInfo[0] + $userInfo[1]) / 2);
249  $workInfo = $this->readSecurityBuffer(bin2hex($workSec));
250  $lmSec = $this->createSecurityBuffer($lmResponse, ($workInfo[0] + $workInfo[1]) / 2, true);
251  $lmInfo = $this->readSecurityBuffer(bin2hex($lmSec));
252  $ntlmSec = $this->createSecurityBuffer($ntlmResponse, ($lmInfo[0] + $lmInfo[1]) / 2, true);
253 
254  return self::NTLMSIG
255  .$this->createByte('03') // TYPE 3 message
256 .$lmSec // LM response header
257 .$ntlmSec // NTLM response header
258 .$domainSec // Domain header
259 .$userSec // User header
260 .$workSec // Workstation header
261 .$this->createByte('000000009a', 8) // session key header (empty)
262 .$this->createByte('01020000') // FLAGS
263 .$this->convertTo16bit($domain) // domain name
264 .$this->convertTo16bit($username) // username
265 .$this->convertTo16bit($workstation) // workstation
266 .$lmResponse
267  .$ntlmResponse;
268  }
269 
277  protected function createBlob($timestamp, $client, $targetInfo)
278  {
279  return $this->createByte('0101')
280  .$this->createByte('00')
281  .$timestamp
282  .$client
283  .$this->createByte('00')
284  .$targetInfo
285  .$this->createByte('00');
286  }
287 
297  protected function getDomainAndUsername($name)
298  {
299  if (strpos($name, '\\') !== false) {
300  return explode('\\', $name);
301  }
302 
303  list($user, $domain) = explode('@', $name);
304 
305  return array($domain, $user);
306  }
307 
316  protected function createLMPassword($password, $challenge)
317  {
318  // FIRST PART
319  $password = $this->createByte(strtoupper($password), 14, false);
320  list($key1, $key2) = str_split($password, 7);
321 
322  $desKey1 = $this->createDesKey($key1);
323  $desKey2 = $this->createDesKey($key2);
324 
325  $constantDecrypt = $this->createByte($this->desEncrypt(self::DESCONST, $desKey1).$this->desEncrypt(self::DESCONST, $desKey2), 21, false);
326 
327  // SECOND PART
328  list($key1, $key2, $key3) = str_split($constantDecrypt, 7);
329 
330  $desKey1 = $this->createDesKey($key1);
331  $desKey2 = $this->createDesKey($key2);
332  $desKey3 = $this->createDesKey($key3);
333 
334  return $this->desEncrypt($challenge, $desKey1).$this->desEncrypt($challenge, $desKey2).$this->desEncrypt($challenge, $desKey3);
335  }
336 
345  protected function createNTLMPassword($password, $challenge)
346  {
347  // FIRST PART
348  $ntlmHash = $this->createByte($this->md4Encrypt($password), 21, false);
349  list($key1, $key2, $key3) = str_split($ntlmHash, 7);
350 
351  $desKey1 = $this->createDesKey($key1);
352  $desKey2 = $this->createDesKey($key2);
353  $desKey3 = $this->createDesKey($key3);
354 
355  return $this->desEncrypt($challenge, $desKey1).$this->desEncrypt($challenge, $desKey2).$this->desEncrypt($challenge, $desKey3);
356  }
357 
365  protected function getCorrectTimestamp($time)
366  {
367  // Get our timestamp (tricky!)
368  bcscale(0);
369 
370  $time = number_format($time, 0, '.', ''); // save microtime to string
371  $time = bcadd($time, '11644473600000'); // add epoch time
372  $time = bcmul($time, 10000); // tenths of a microsecond.
373 
374  $binary = $this->si2bin($time, 64); // create 64 bit binary string
375  $timestamp = '';
376  for ($i = 0; $i < 8; $i++) {
377  $timestamp .= chr(bindec(substr($binary, -(($i + 1) * 8), 8)));
378  }
379 
380  return $timestamp;
381  }
382 
394  protected function createLMv2Password($password, $username, $domain, $challenge, $client)
395  {
396  $lmPass = '00'; // by default 00
397  // if $password > 15 than we can't use this method
398  if (strlen($password) <= 15) {
399  $ntlmHash = $this->md4Encrypt($password);
400  $ntml2Hash = $this->md5Encrypt($ntlmHash, $this->convertTo16bit(strtoupper($username).$domain));
401 
402  $lmPass = bin2hex($this->md5Encrypt($ntml2Hash, $challenge.$client).$client);
403  }
404 
405  return $this->createByte($lmPass, 24);
406  }
407 
423  protected function createNTLMv2Hash($password, $username, $domain, $challenge, $targetInfo, $timestamp, $client)
424  {
425  $ntlmHash = $this->md4Encrypt($password);
426  $ntml2Hash = $this->md5Encrypt($ntlmHash, $this->convertTo16bit(strtoupper($username).$domain));
427 
428  // create blob
429  $blob = $this->createBlob($timestamp, $client, $targetInfo);
430 
431  $ntlmv2Response = $this->md5Encrypt($ntml2Hash, $challenge.$blob);
432 
433  return $ntlmv2Response.$blob;
434  }
435 
436  protected function createDesKey($key)
437  {
438  $material = array(bin2hex($key[0]));
439  $len = strlen($key);
440  for ($i = 1; $i < $len; $i++) {
441  list($high, $low) = str_split(bin2hex($key[$i]));
442  $v = $this->castToByte(ord($key[$i - 1]) << (7 + 1 - $i) | $this->uRShift(hexdec(dechex(hexdec($high) & 0xf).dechex(hexdec($low) & 0xf)), $i));
443  $material[] = str_pad(substr(dechex($v), -2), 2, '0', STR_PAD_LEFT); // cast to byte
444  }
445  $material[] = str_pad(substr(dechex($this->castToByte(ord($key[6]) << 1)), -2), 2, '0');
446 
447  // odd parity
448  foreach ($material as $k => $v) {
449  $b = $this->castToByte(hexdec($v));
450  $needsParity = (($this->uRShift($b, 7) ^ $this->uRShift($b, 6) ^ $this->uRShift($b, 5)
451  ^ $this->uRShift($b, 4) ^ $this->uRShift($b, 3) ^ $this->uRShift($b, 2)
452  ^ $this->uRShift($b, 1)) & 0x01) == 0;
453 
454  list($high, $low) = str_split($v);
455  if ($needsParity) {
456  $material[$k] = dechex(hexdec($high) | 0x0).dechex(hexdec($low) | 0x1);
457  } else {
458  $material[$k] = dechex(hexdec($high) & 0xf).dechex(hexdec($low) & 0xe);
459  }
460  }
461 
462  return $this->hex2bin(implode('', $material));
463  }
464 
475  protected function createSecurityBuffer($value, $offset, $is16 = false)
476  {
477  $length = strlen(bin2hex($value));
478  $length = $is16 ? $length / 2 : $length;
479  $length = $this->createByte(str_pad(dechex($length), 2, '0', STR_PAD_LEFT), 2);
480 
481  return $length.$length.$this->createByte(dechex($offset), 4);
482  }
483 
491  protected function readSecurityBuffer($value)
492  {
493  $length = floor(hexdec(substr($value, 0, 4)) / 256) * 2;
494  $offset = floor(hexdec(substr($value, 8, 4)) / 256) * 2;
495 
496  return array($length, $offset);
497  }
498 
506  protected function castToByte($v)
507  {
508  return (($v + 128) % 256) - 128;
509  }
510 
520  protected function uRShift($a, $b)
521  {
522  if ($b == 0) {
523  return $a;
524  }
525 
526  return ($a >> $b) & ~(1 << (8 * PHP_INT_SIZE - 1) >> ($b - 1));
527  }
528 
538  protected function createByte($input, $bytes = 4, $isHex = true)
539  {
540  if ($isHex) {
541  $byte = $this->hex2bin(str_pad($input, $bytes * 2, '00'));
542  } else {
543  $byte = str_pad($input, $bytes, "\x00");
544  }
545 
546  return $byte;
547  }
548 
556  protected function getRandomBytes($length)
557  {
558  $bytes = openssl_random_pseudo_bytes($length, $strong);
559 
560  if (false !== $bytes && true === $strong) {
561  return $bytes;
562  }
563 
564  throw new RuntimeException('OpenSSL did not produce a secure random number.');
565  }
566 
576  protected function desEncrypt($value, $key)
577  {
578  $cipher = mcrypt_module_open(MCRYPT_DES, '', 'ecb', '');
579  mcrypt_generic_init($cipher, $key, mcrypt_create_iv(mcrypt_enc_get_iv_size($cipher), MCRYPT_DEV_RANDOM));
580 
581  return mcrypt_generic($cipher, $value);
582  }
583 
592  protected function md5Encrypt($key, $msg)
593  {
594  $blocksize = 64;
595  if (strlen($key) > $blocksize) {
596  $key = pack('H*', md5($key));
597  }
598 
599  $key = str_pad($key, $blocksize, "\0");
600  $ipadk = $key ^ str_repeat("\x36", $blocksize);
601  $opadk = $key ^ str_repeat("\x5c", $blocksize);
602 
603  return pack('H*', md5($opadk.pack('H*', md5($ipadk.$msg))));
604  }
605 
615  protected function md4Encrypt($input)
616  {
617  $input = $this->convertTo16bit($input);
618 
619  return function_exists('hash') ? $this->hex2bin(hash('md4', $input)) : mhash(MHASH_MD4, $input);
620  }
621 
629  protected function convertTo16bit($input)
630  {
631  return iconv('UTF-8', 'UTF-16LE', $input);
632  }
633 
641  protected function hex2bin($hex)
642  {
643  if (function_exists('hex2bin')) {
644  return hex2bin($hex);
645  } else {
646  return pack('H*', $hex);
647  }
648  }
649 
653  protected function debug($message)
654  {
655  $message = bin2hex($message);
656  $messageId = substr($message, 16, 8);
657  echo substr($message, 0, 16)." NTLMSSP Signature<br />\n";
658  echo $messageId." Type Indicator<br />\n";
659 
660  if ($messageId == '02000000') {
661  $map = array(
662  'Challenge',
663  'Context',
664  'Target Information Security Buffer',
665  'Target Name Data',
666  'NetBIOS Domain Name',
667  'NetBIOS Server Name',
668  'DNS Domain Name',
669  'DNS Server Name',
670  'BLOB',
671  'Target Information Terminator',
672  );
673 
674  $data = $this->parseMessage2($this->hex2bin($message));
675 
676  foreach ($map as $key => $value) {
677  echo bin2hex($data[$key]).' - '.$data[$key].' ||| '.$value."<br />\n";
678  }
679  } elseif ($messageId == '03000000') {
680  $i = 0;
681  $data[$i++] = substr($message, 24, 16);
682  list($lmLength, $lmOffset) = $this->readSecurityBuffer($data[$i - 1]);
683 
684  $data[$i++] = substr($message, 40, 16);
685  list($ntmlLength, $ntmlOffset) = $this->readSecurityBuffer($data[$i - 1]);
686 
687  $data[$i++] = substr($message, 56, 16);
688  list($targetLength, $targetOffset) = $this->readSecurityBuffer($data[$i - 1]);
689 
690  $data[$i++] = substr($message, 72, 16);
691  list($userLength, $userOffset) = $this->readSecurityBuffer($data[$i - 1]);
692 
693  $data[$i++] = substr($message, 88, 16);
694  list($workLength, $workOffset) = $this->readSecurityBuffer($data[$i - 1]);
695 
696  $data[$i++] = substr($message, 104, 16);
697  $data[$i++] = substr($message, 120, 8);
698  $data[$i++] = substr($message, $targetOffset, $targetLength);
699  $data[$i++] = substr($message, $userOffset, $userLength);
700  $data[$i++] = substr($message, $workOffset, $workLength);
701  $data[$i++] = substr($message, $lmOffset, $lmLength);
702  $data[$i] = substr($message, $ntmlOffset, $ntmlLength);
703 
704  $map = array(
705  'LM Response Security Buffer',
706  'NTLM Response Security Buffer',
707  'Target Name Security Buffer',
708  'User Name Security Buffer',
709  'Workstation Name Security Buffer',
710  'Session Key Security Buffer',
711  'Flags',
712  'Target Name Data',
713  'User Name Data',
714  'Workstation Name Data',
715  'LM Response Data',
716  'NTLM Response Data',
717  );
718 
719  foreach ($map as $key => $value) {
720  echo $data[$key].' - '.$this->hex2bin($data[$key]).' ||| '.$value."<br />\n";
721  }
722  }
723 
724  echo '<br /><br />';
725  }
726 }