1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991
|
---
-- This module takes care of the authentication used in SMB (LM, NTLM, LMv2, NTLMv2).
--
-- There is a lot to this functionality, so if you're interested in how it works, read
-- on.
-- In SMB authentication, there are two distinct concepts. Each will be dealt with
-- separately. There are:
-- * Stored hashes
-- * Authentication
--
-- What's confusing is that the same names are used for each of those.
--
-- Stored Hashes:
-- Windows stores two types of hashes: Lanman and NT Lanman (or NTLM). Vista and later
-- store NTLM only. Lanman passwords are divided into two 7-character passwords and
-- used as a key in DES, while NTLM is converted to unicode and MD4ed.
--
-- The stored hashes can be dumped in a variety of ways (pwdump6, fgdump, Metasploit's
-- <code>priv</code> module, <code>smb-psexec.nse</code>, etc). Generally, two hashes are dumped together
-- (generally, Lanman:NTLM). Sometimes, Lanman is empty and only NTLM is given. Lanman
-- is never required.
--
-- The password hashes can be given instead of passwords when supplying credentials;
-- this is done by using the <code>smbhash</code> argument. Either a pair of hashes
-- can be passed, in the form of Lanman:NTLM, or a single hash, which is assumed to
-- be NTLM.
--
-- Authentication:
-- There are four types of authentication. Confusingly, these have the same names as
-- stored hashes, but only slight relationships. The four types are Lanmanv1, NTLMv1,
-- Lanmanv2, and NTLMv2. By default, Lanmanv1 and NTLMv1 are used together in most
-- applications. These Nmap scripts default to NTLMv1 alone, except in special cases,
-- but it can be overridden by the user.
--
-- Lanmanv1 and NTLMv1 both use DES for their response. The DES mixes a server challenge
-- with the hash (Lanman hash for Lanmanv1 response and NTLMv1 hash for NTLM response).
-- The way the challenge is DESed with the hashes is identical for Lanmanv1 and NTLMv1,
-- the only difference is the starting hash (Lanman vs NTLM).
--
-- Lanmanv2 and NTLMv2 both use HMAC-MD5 for their response. The HMAC-MD5 mixes a
-- server challenge and a client challenge with the NTLM hash, in both cases. The
-- difference between Lanmanv2 and NTLMv2 is the length of the client challenge;
-- Lanmanv2 has a maximum client challenge of 8 bytes, whereas NTLMv2 doesn't limit
-- the length of the client challenge.
--
-- The primary advantage to the 'v2' protocols is the client challenge -- by
-- incorporating a client challenge, a malicious server can't use a precomputation
-- attack.
--
-- In addition to hashing the passwords, messages are also signed, by default, if a
-- v1 protocol is being used (I (Ron Bowes) couldn't get signatures to work on v2
-- protocols; if anybody knows how I'd love to implement it).
--
--@args smbusername The SMB username to log in with. The forms "DOMAIN\username" and "username@DOMAIN"
-- are not understood. To set a domain, use the <code>smbdomain</code> argument.
--@args smbdomain The domain to log in with. If you aren't in a domain environment, then anything
-- will (should?) be accepted by the server.
--@args smbpassword The password to connect with. Be cautious with this, since some servers will lock
-- accounts if the incorrect password is given. Although it's rare that the
-- Administrator account can be locked out, in the off chance that it can, you could
-- get yourself in trouble. To use a blank password, leave this parameter off
-- altogether.
--@args smbhash A password hash to use when logging in. This is given as a single hex string (32
-- characters) or a pair of hex strings (both 32 characters, optionally separated by a
-- single character). These hashes are the LanMan or NTLM hash of the user's password,
-- and are stored on disk or in memory. They can be retrieved from memory
-- using the fgdump or pwdump tools.
--@args smbtype The type of SMB authentication to use. These are the possible options:
-- * <code>v1</code>: Sends LMv1 and NTLMv1.
-- * <code>LMv1</code>: Sends LMv1 only.
-- * <code>NTLMv1</code>: Sends NTLMv1 only (default).
-- * <code>v2</code>: Sends LMv2 and NTLMv2.
-- * <code>LMv2</code>: Sends LMv2 only.
-- * <code>NTLMv2</code>: Doesn't exist; the protocol doesn't support NTLMv2 alone.
-- The default, <code>NTLMv1</code>, is a pretty decent compromise between security and
-- compatibility. If you are paranoid, you might want to use <code>v2</code> or
-- <code>lmv2</code> for this. (Actually, if you're paranoid, you should be avoiding this
-- protocol altogether!). If you're using an extremely old system, you might need to set
-- this to <code>v1</code> or <code>lm</code>, which are less secure but more compatible.
-- For information, see <code>smbauth.lua</code>.
--@args smbnoguest Use to disable usage of the 'guest' account.
local nmap = require "nmap"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local unicode = require "unicode"
local unittest = require "unittest"
_ENV = stdnse.module("smbauth", stdnse.seeall)
local have_ssl, openssl = pcall(require, "openssl")
-- Constants
local NTLMSSP_NEGOTIATE = 0x00000001
local NTLMSSP_CHALLENGE = 0x00000002
local NTLMSSP_AUTH = 0x00000003
local session_key = string.rep("\0", 16)
-- Types of accounts (ordered by how useful they are
local ACCOUNT_TYPES = {
ANONYMOUS = 0,
GUEST = 1,
USER = 2,
ADMIN = 3
}
local function account_exists(host, username, domain)
if(host.registry['smbaccounts'] == nil) then
return false
end
for i, j in pairs(host.registry['smbaccounts']) do
if(j['username'] == username and j['domain'] == domain) then
return true
end
end
return false
end
--- Try the next stored account for this host
-- @param host The host table
-- @param num If nil, the next account is chosen. If a number, the account at
-- that index is chosen
function next_account(host, num)
if(num == nil) then
if(host.registry['smbindex'] == nil) then
host.registry['smbindex'] = 1
else
host.registry['smbindex'] = host.registry['smbindex'] + 1
end
else
host.registry['smbindex'] = num
end
end
---Writes the given account to the registry.
--
-- There are several places where accounts are stored:
-- * registry['usernames'][username] => true
-- * registry['smbaccounts'][username] => password
-- * registry[ip]['smbaccounts'] => array of table containing 'username', 'password', and 'is_admin'
--
-- The final place, 'smbaccount', is reserved for the "best" account. This is
-- an administrator account, if one's found; otherwise, it's the first account
-- discovered that isn't <code>guest</code>.
--
-- This has to be called while no SMB connections are made, since it
-- potentially makes its own connection.
--
--@param host The host object.
--@param username The username to add.
--@param domain The domain to add.
--@param password The password to add.
--@param password_hash The password hash to add.
--@param hash_type The hash type to use.
--@param is_admin [optional] Set to 'true' the account is known to be an administrator.
function add_account(host, username, domain, password, password_hash, hash_type, is_admin)
-- Save the username in a global list -- TODO: restore this
-- if(nmap.registry.usernames == nil) then
-- nmap.registry.usernames = {}
-- end
-- nmap.registry.usernames[username] = true
--
-- -- Save the username/password pair in a global list
-- if(nmap.registry.smbaccounts == nil) then
-- nmap.registry.smbaccounts = {}
-- end
-- nmap.registry.smbaccounts[username] = password
-- Check if we've already recorded this account
if(account_exists(host, username, domain)) then
return
end
if(host.registry['smbaccounts'] == nil) then
host.registry['smbaccounts'] = {}
end
-- Determine the type of account, if it wasn't given
local account_type = nil
if(is_admin) then
account_type = ACCOUNT_TYPES.ADMIN
else
if(username == '') then
-- Anonymous account
account_type = ACCOUNT_TYPES.ANONYMOUS
elseif(string.lower(username) == 'guest') then
-- Guest account
account_type = ACCOUNT_TYPES.GUEST
else
-- We have to assume it's a user-level account (we just can't call any SMB functions from inside here)
account_type = ACCOUNT_TYPES.USER
end
end
-- Set some defaults
if(hash_type == nil) then
hash_type = 'ntlm'
end
-- Save the new account if this is our first one, or our other account isn't an admin
local new_entry = {}
new_entry['username'] = username
new_entry['domain'] = domain
new_entry['password'] = password
new_entry['password_hash'] = password_hash
new_entry['hash_type'] = string.lower(hash_type)
new_entry['account_type'] = account_type
-- Insert the new entry into the table
table.insert(host.registry['smbaccounts'], new_entry)
-- Sort the table based on the account type (we want anonymous at the end, administrator at the front)
table.sort(host.registry['smbaccounts'], function(a,b) return a['account_type'] > b['account_type'] end)
-- Print a debug message
stdnse.debug1("SMB: Added account '%s' to account list", username)
-- Reset the credentials
next_account(host, 1)
-- io.write("\n\n" .. nsedebug.tostr(host.registry['smbaccounts']) .. "\n\n")
end
---Retrieve the current set of credentials set in the registry.
--
-- If these fail, <code>next_account</code> should be called.
--
--@param host The host object.
--@return status true or false. If false, the next return value is an error
-- message and no other values are returned.
--@return username
--@return domain
--@return password
--@return password_hash
--@return hash_type
--@see next_account
function get_account(host)
if(host.registry['smbindex'] == nil) then
host.registry['smbindex'] = 1
end
local index = host.registry['smbindex']
local account = host.registry['smbaccounts'][index]
if(account == nil) then
return false, "No accounts left to try"
end
return true, account['username'], account['domain'], account['password'], account['password_hash'], account['hash_type']
end
---Initialize the host's account table.
--
-- Create the account table with the anonymous and guest users, as well as the
-- user given in the script's arguments, if there is one.
--
--@param host The host object.
function init_account(host)
-- Don't run this more than once for each host
if(host.registry['smbaccounts'] ~= nil) then
return
end
-- Create the list
host.registry['smbaccounts'] = {}
-- Add the anonymous/guest accounts
add_account(host, '', '', '', nil, 'none')
if(not stdnse.get_script_args( "smbnoguest" )) then
add_account(host, 'guest', '', '', nil, 'ntlm')
end
-- Add the account given on the commandline (TODO: allow more than one?)
local args = nmap.registry.args
local username = nil
local domain = ''
local password = nil
local password_hash = nil
local hash_type = 'ntlm'
-- Do the username first
if(args.smbusername ~= nil) then
username = args.smbusername
elseif(args.smbuser ~= nil) then
username = args.smbuser
end
-- If the username exists, do everything else
if(username ~= nil) then
-- Domain
if(args.smbdomain ~= nil) then
domain = args.smbdomain
end
-- Type
if(args.smbtype ~= nil) then
hash_type = args.smbtype
end
-- Do the password
if(args.smbpassword ~= nil) then
password = args.smbpassword
elseif(args.smbpass ~= nil) then
password = args.smbpass
end
-- Only use the hash if there's no password
if(password == nil) then
password_hash = args.smbhash
end
-- Add the account, if we got a password
if(password == nil and password_hash == nil) then
stdnse.debug1("SMB: Either smbpass, smbpassword, or smbhash have to be passed as script arguments to use an account")
else
add_account(host, username, domain, password, password_hash, hash_type)
end
end
end
---Generate the Lanman v1 hash (LMv1).
--
-- The generated hash is incredibly easy to reverse, because the input is
-- padded or truncated to 14 characters, then split into two 7-character
-- strings. Each of these strings are used as a key to encrypt the string,
-- "KGS!@#$%" in DES. Because the keys are no longer than 7-characters long,
-- it's pretty trivial to bruteforce them.
--
--@param password the password to hash
--@return true on success, or false on error
--@return The LMv1 hash
local function lm_create_hash(password)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
local str1, str2
local key1, key2
local result
-- Convert the password to uppercase
password = string.upper(password)
-- Encode the password in OEM code page
-- Supporting all the OEM code pages would be burdensome, so we try to
-- convert to CP437, the default for US-English Windows, which is
-- used for Alt+NumPad "unicode" entry in all versions of Windows.
-- https://en.wikipedia.org/wiki/Code_page_437
do
local buf = {}
for i, cp in ipairs(unicode.decode(password, unicode.utf8_dec)) do
local ch = unicode.cp437_enc(cp)
if ch == nil then
return false, "Couldn't encode password in CP437"
end
buf[i] = ch
end
password = table.concat(buf)
end
-- If password is under 14 characters, pad it to 14
password = password .. string.rep('\0', 14 - #password)
-- Take the first and second half of the password (note that if it's longer than 14 characters, it's truncated)
str1 = string.sub(password, 1, 7)
str2 = string.sub(password, 8, 14)
-- Generate the keys
key1 = openssl.DES_string_to_key(str1)
key2 = openssl.DES_string_to_key(str2)
-- Encrypt the string "KGS!@#$%" with each half, and concatenate it
result = openssl.encrypt("DES", key1, nil, "KGS!@#$%") .. openssl.encrypt("DES", key2, nil, "KGS!@#$%")
return true, result
end
---Generate the NTLMv1 hash.
--
-- This hash is quite a bit better than LMv1, and is far easier to generate.
-- Basically, it's the MD4 of the Unicode password.
--
--@param password the password to hash
--@return true on success, or false on error
--@return The NTLMv1 hash
function ntlm_create_hash(password)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
return true, openssl.md4(unicode.utf8to16(password))
end
---Create the Lanman response to send back to the server.
--
-- To do this, the Lanman password is padded to 21 characters and split into
-- three 7-character strings. Each of those strings is used as a key to encrypt
-- the server challenge. The three encrypted strings are concatenated and
-- returned.
--
--@param lanman The LMv1 hash
--@param challenge The server's challenge.
--@return true on success, or false on error
--@return The client challenge response, or an error message
function lm_create_response(lanman, challenge)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
local str1, str2, str3
local key1, key2, key3
local result
-- Pad the hash to 21 characters
lanman = lanman .. string.rep('\0', 21 - #lanman)
-- Take the first and second half of the password (note that if it's longer than 14 characters, it's truncated)
str1 = string.sub(lanman, 1, 7)
str2 = string.sub(lanman, 8, 14)
str3 = string.sub(lanman, 15, 21)
-- Generate the keys
key1 = openssl.DES_string_to_key(str1)
key2 = openssl.DES_string_to_key(str2)
key3 = openssl.DES_string_to_key(str3)
-- Print a warning message if a blank challenge is received, and create a phony challenge. A blank challenge is
-- invalid in the protocol, and causes some versions of OpenSSL to abort with no possible error handling.
if(challenge == "") then
stdnse.debug1("SMB: ERROR: Server returned invalid (blank) challenge value (should be 8 bytes); failing login to avoid OpenSSL crash.")
challenge = "AAAAAAAA"
end
-- Encrypt the challenge with each key
result = openssl.encrypt("DES", key1, nil, challenge) .. openssl.encrypt("DES", key2, nil, challenge) .. openssl.encrypt("DES", key3, nil, challenge)
return true, result
end
---Create the NTLM response to send back to the server.
--
-- This is actually done the exact same way as the Lanman hash,
-- so I call the <code>Lanman</code> function.
--
--@param ntlm The NTLMv1 hash
--@param challenge The server's challenge.
--@return true on success, or false on error
--@return The client challenge response, or an error message
function ntlm_create_response(ntlm, challenge)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
return lm_create_response(ntlm, challenge)
end
---Create the NTLM mac key, which is used for message signing.
--
-- For basic authentication, this is the md4 of the NTLM hash, concatenated
-- with the response hash; for extended authentication, this is just the md4 of
-- the NTLM hash.
--
--@param ntlm_hash The NTLM hash.
--@param ntlm_response The NTLM response.
--@param is_extended Should be set if extended security negotiations are being used.
--@return The NTLM mac key
function ntlm_create_mac_key(ntlm_hash, ntlm_response, is_extended)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
if(is_extended) then
return openssl.md4(ntlm_hash)
else
return openssl.md4(ntlm_hash) .. ntlm_response
end
end
---Create the LM mac key, which is used for message signing.
--
-- For basic authentication, it's the first 8 bytes of the lanman hash,
-- followed by 8 null bytes, followed by the lanman response; for extended
-- authentication, this is just the first 8 bytes of the lanman hash followed
-- by 8 null bytes.
--
--@param lm_hash The LM hash.
--@param lm_response The LM response.
--@param is_extended Should be set if extended security negotiations are being used.
--@return The LM mac key
function lm_create_mac_key(lm_hash, lm_response, is_extended)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
if(is_extended) then
return string.sub(lm_hash, 1, 8) .. string.rep('\0', 8)
else
return string.sub(lm_hash, 1, 8) .. string.rep('\0', 8) .. lm_response
end
end
---Create the NTLMv2 hash.
--
-- The NTLMv2 hash is based on the NTLMv1 hash (for easy upgrading), the
-- username, and the domain. Essentially, the NTLM hash is used as a HMAC-MD5
-- key, which is used to hash the unicode domain concatenated with the unicode
-- username.
--
--@param ntlm The NTLMv1 hash.
--@param username The username we're using.
--@param domain The domain.
--@return true on success, or false on error
--@return The NTLMv2 hash or an error message
function ntlmv2_create_hash(ntlm, username, domain)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
username = unicode.utf8to16(string.upper(username))
domain = unicode.utf8to16(string.upper(domain))
return true, openssl.hmac("MD5", ntlm, username .. domain)
end
---Create the LMv2 response, which can be sent back to the server.
--
-- This is identical to the <code>NTLMv2</code> function,
-- except that it uses an 8-byte client challenge.
--
-- The reason for LMv2 is a long and twisted story. Well, not really. The
-- reason is basically that the v1 hashes are always 24-bytes, and some servers
-- expect 24 bytes, but the NTLMv2 hash is more than 24 bytes. So, the only way
-- to keep pass-through compatibility was to have a v2-hash that was guaranteed
-- to be 24 bytes. So LMv2 was born -- it has a 16-byte hash followed by the
-- 8-byte client challenge, for a total of 24 bytes. And now you've learned
-- something
--
--@param ntlm The NVLMv1 hash.
--@param username The username we're using.
--@param domain The domain.
--@param challenge The server challenge.
--@return true on success, or false on error
--@return The LMv2 response, or an error message
function lmv2_create_response(ntlm, username, domain, challenge)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
return ntlmv2_create_response(ntlm, username, domain, challenge, 8)
end
---Create the NTLMv2 response, which can be sent back to the server.
--
-- This is done by using the HMAC-MD5 algorithm with the NTLMv2 hash as a key,
-- and the server challenge concatenated with the client challenge for the
-- data. The resulting hash is concatenated with the client challenge and
-- returned.
--
-- The "proper" implementation for this uses a certain structure for the client
-- challenge, involving the time and computer name and stuff (if you don't do
-- this, Wireshark tells you it's a malformed packet). In my tests, however, I
-- couldn't get Vista to recognize a client challenge longer than 24 bytes, and
-- this structure was guaranteed to be much longer than 24 bytes. So, I just
-- use a random string generated by OpenSSL. I've tested it on every Windows
-- system from Windows 2000 to Windows Vista, and it has always worked.
--
--@param ntlm The NVLMv1 hash.
--@param username The username we're using.
--@param domain The domain.
--@param challenge The server challenge.
--@param client_challenge_length number of random bytes of client challenge to use
--@return true on success, or false on error
--@return The NTLMv2 response, or an error message
function ntlmv2_create_response(ntlm, username, domain, challenge, client_challenge_length)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
local client_challenge = openssl.rand_bytes(client_challenge_length)
local status, ntlmv2_hash = ntlmv2_create_hash(ntlm, username, domain)
return true, openssl.hmac("MD5", ntlmv2_hash, challenge .. client_challenge) .. client_challenge
end
--- Generates the ntlmv2 session response.
-- It starts by generatng an 8 byte random client nonce, it is padded to 24 bytes.
-- The padded value is the lanman response. A session nonce is made by
-- concatenating the server challenge and the client nonce. The ntlm session hash
-- is first 8 bytes of the md5 hash of the session nonce.
-- The ntlm response is the lm response with session hash as challenge.
-- @param ntlm_password_hash The md4 hash of the utf-16 password.
-- @param challenge The challenge sent by the server.
function ntlmv2_session_response(ntlm_password_hash, challenge)
local client_nonce = openssl.rand_bytes(8)
local lm_response = client_nonce .. string.rep('\0', 24 - #client_nonce)
local session_nonce = challenge .. client_nonce
local ntlm_session_hash = openssl.md5(session_nonce):sub(1,8)
local status, ntlm_response = lm_create_response(ntlm_password_hash, ntlm_session_hash)
return status, lm_response, ntlm_response
end
---Generate the Lanman and NTLM password hashes.
--
-- The password itself is taken from the function parameters, the script
-- arguments, and the registry (in that order). If no password is set, then the
-- password hash is used (which is read from all the usual places). If neither
-- is set, then a blank password is used.
--
-- The output passwords are hashed based on the hash type.
--
--@param ip The ip address of the host, used for registry lookups.
--@param username The username, which is used for v2 passwords.
--@param domain The username, which is used for v2 passwords.
--@param password [optional] The overriding password.
--@param password_hash [optional] The overriding password hash. Shouldn't be
-- set if password is set.
--@param challenge The server challenge.
--@param hash_type The way in which to hash the password.
--@param is_extended Set to 'true' if extended security negotiations are being
-- used (this has to be known for the message-signing key to
-- be generated properly).
--@return lm_response, to be send directly back to the server
--@return ntlm_response, to be send directly back to the server
--@reutrn mac_key used for message signing.
function get_password_response(ip, username, domain, password, password_hash, hash_type, challenge, is_extended)
local status
local lm_hash = nil
local ntlm_hash = nil
local mac_key = nil
local lm_response, ntlm_response
-- Check for a blank password
if(password == nil and password_hash == nil) then
stdnse.debug2("SMB: Couldn't find password or hash to use (assuming blank)")
password = ""
end
-- The anonymous user requires a single 0-byte instead of a LANMAN hash (don't ask me why, but it doesn't work without)
if(hash_type == 'none') then
return '\0', '', nil
end
-- If we got a password, hash it
if(password ~= nil) then
status, lm_hash = lm_create_hash(password)
status, ntlm_hash = ntlm_create_hash(password)
else
if(password_hash ~= nil) then
if(string.find(password_hash, "^" .. string.rep("%x%x", 16) .. "$")) then
stdnse.debug2("SMB: Found a 16-byte hex string")
lm_hash = stdnse.fromhex(password_hash:sub(1, 32))
ntlm_hash = stdnse.fromhex(password_hash:sub(1, 32))
elseif(string.find(password_hash, "^" .. string.rep("%x%x", 32) .. "$")) then
stdnse.debug2("SMB: Found a 32-byte hex string")
lm_hash = stdnse.fromhex(password_hash:sub(1, 32))
ntlm_hash = stdnse.fromhex(password_hash:sub(33, 64))
elseif(string.find(password_hash, "^" .. string.rep("%x%x", 16) .. "." .. string.rep("%x%x", 16) .. "$")) then
stdnse.debug2("SMB: Found two 16-byte hex strings")
lm_hash = stdnse.fromhex(password_hash:sub(1, 32))
ntlm_hash = stdnse.fromhex(password_hash:sub(34, 65))
else
stdnse.debug1("SMB: ERROR: Hash(es) provided in an invalid format (should be 32, 64, or 65 hex characters)")
lm_hash = nil
ntlm_hash = nil
end
end
end
-- At this point, we should have a good lm_hash and ntlm_hash if we're getting one
if(lm_hash == nil or ntlm_hash == nil) then
stdnse.debug2("SMB: Couldn't determine which password to use, using a blank one")
return "", ""
end
-- Output what we've got so far
stdnse.debug2("SMB: Lanman hash: %s", stdnse.tohex(lm_hash))
stdnse.debug2("SMB: NTLM hash: %s", stdnse.tohex(ntlm_hash))
-- Hash the password the way the user wants
if(hash_type == "v1") then
-- LM and NTLM are hashed with their respective algorithms
stdnse.debug2("SMB: Creating v1 response")
status, lm_response = lm_create_response(lm_hash, challenge)
status, ntlm_response = ntlm_create_response(ntlm_hash, challenge)
mac_key = ntlm_create_mac_key(ntlm_hash, ntlm_response, is_extended)
elseif(hash_type == "lm") then
-- LM is hashed with its algorithm, NTLM is blank
stdnse.debug2("SMB: Creating LMv1 response")
status, lm_response = lm_create_response(lm_hash, challenge)
ntlm_response = ""
mac_key = lm_create_mac_key(lm_hash, lm_response, is_extended)
elseif(hash_type == "ntlm") then
-- LM and NTLM both use the NTLM algorithm
stdnse.debug2("SMB: Creating NTLMv1 response")
status, lm_response = ntlm_create_response(ntlm_hash, challenge)
status, ntlm_response = ntlm_create_response(ntlm_hash, challenge)
mac_key = ntlm_create_mac_key(ntlm_hash, ntlm_response, is_extended)
elseif(hash_type == "v2") then
-- LM and NTLM are hashed with their respective v2 algorithms
stdnse.debug2("SMB: Creating v2 response")
status, lm_response = lmv2_create_response(ntlm_hash, username, domain, challenge)
status, ntlm_response = ntlmv2_create_response(ntlm_hash, username, domain, challenge, 24)
elseif(hash_type == "lmv2") then
-- LM is hashed with its v2 algorithm, NTLM is blank
stdnse.debug2("SMB: Creating LMv2 response")
status, lm_response = lmv2_create_response(ntlm_hash, username, domain, challenge)
ntlm_response = ""
elseif(hash_type == "ntlmv2_session") then
stdnse.debug2("SMB: Creating nltmv2 session response")
status, lm_response, ntlm_response = ntlmv2_session_response(ntlm_hash, challenge)
else
-- Default to NTLMv1
if(hash_type ~= nil) then
stdnse.debug1("SMB: Invalid login type specified ('%s'), using default (NTLM)", hash_type)
else
stdnse.debug1("SMB: No login type specified, using default (NTLM)")
end
status, lm_response = ntlm_create_response(ntlm_hash, challenge)
status, ntlm_response = ntlm_create_response(ntlm_hash, challenge)
end
stdnse.debug2("SMB: Lanman response: %s", stdnse.tohex(lm_response))
stdnse.debug2("SMB: NTLM response: %s", stdnse.tohex(ntlm_response))
return lm_response, ntlm_response, mac_key
end
---Generate an NTLMSSP security blob.
--@param security_blob The server's security blob, or nil if this is the first
-- message
--@param ip The ip address of the host, used for registry lookups.
--@param username The username, which is used for v2 passwords.
--@param domain The username, which is used for v2 passwords.
--@param password [optional] The overriding password.
--@param password_hash [optional] The overriding password hash. Shouldn't be
-- set if password is set.
--@param hash_type The way in which to hash the password.
--@param flags The NTLM flags as a number
function get_security_blob(security_blob, ip, username, domain, password, password_hash, hash_type, flags)
local pos = 1
local new_blob
local flags = flags or 0x00008215 -- (NEGOTIATE_SIGN_ALWAYS | NEGOTIATE_NTLM | NEGOTIATE_SIGN | REQUEST_TARGET | NEGOTIATE_UNICODE)
if(security_blob == nil) then
-- If security_blob is nil, this is the initial packet
new_blob = string.pack("<zI4I4I8I8",
"NTLMSSP", -- Identifier
NTLMSSP_NEGOTIATE, -- Type
flags, -- Flags
0, -- Calling workstation domain
0 -- Calling workstation name
)
return true, new_blob, "", ""
else
-- Parse the old security blob
local identifier, message_type, domain_length, domain_max, domain_offset, server_flags, challenge, reserved = string.unpack("<I8I4I2I2I4I4c8c8", security_blob)
local lanman, ntlm, mac_key = get_password_response(ip, username, domain, password, password_hash, hash_type, challenge, true)
-- Convert the username and domain to unicode (TODO: Disable the unicode flag, evaluate if that'll work)
local hostname = unicode.utf8to16("nmap")
username = unicode.utf8to16(username)
domain = (#username > 0 ) and unicode.utf8to16(domain) or ""
ntlm = (#username > 0 ) and ntlm or ""
lanman = (#username > 0 ) and lanman or '\0'
local domain_offset = 0x40
local username_offset = domain_offset + #domain
local hostname_offset = username_offset + #username
local lanman_offset = hostname_offset + #hostname
local ntlm_offset = lanman_offset + #lanman
local sessionkey_offset = ntlm_offset + #ntlm
new_blob = string.pack("<zI4 I2I2I4 I2I2I4 I2I2I4 I2I2I4 I2I2I4 I2I2I4 I4",
"NTLMSSP",
NTLMSSP_AUTH,
#lanman,
#lanman,
lanman_offset,
( #ntlm > 0 and #ntlm - 16 or 0 ),
( #ntlm > 0 and #ntlm - 16 or 0 ),
ntlm_offset,
#domain,
#domain,
domain_offset,
#username,
#username,
username_offset,
#hostname,
#hostname,
hostname_offset,
#session_key,
#session_key,
sessionkey_offset,
flags)
.. domain
.. username
.. hostname
.. lanman
.. ntlm
.. session_key
return true, new_blob, mac_key
end
end
---
-- Host information for NTLM security
-- @class table
-- @name host_info
-- @field target_realm Target Name Data
-- @field netbios_computer_name Server name
-- @field netbios_domain_name Domain name
-- @field fqdn DNS server name
-- @field dns_domain_name DNS domain name
-- @field dns_forest_name DNS tree name
-- @field timestamp Timestamp
---
-- Gets host info from a security blob
-- @param security_blob The NTLM security blob
-- @return A host_info table containing the data in the blob.
-- @see host_info
function get_host_info_from_security_blob(security_blob)
local identifier, message_type, domain_length, domain_max, domain_offset, server_flags, challenge, hpos = string.unpack("<c8I4 I2I2I4 I4I8", security_blob)
-- Do some validation on the NTLMSSP message
if ( identifier ~= "NTLMSSP\0" ) then
stdnse.debug1("SMB: Invalid NTLM challenge message: unexpected signature." )
return false, "Invalid NTLM challenge message"
-- Per MS-NLMP, this field must be 2 for an NTLM challenge message
elseif ( message_type ~= 0x2 ) then
stdnse.debug1("SMB: Invalid NTLM challenge message: unexpected message type: %d.", message_type )
return false, "Invalid message type in NTLM challenge message"
end
local ntlm_challenge = {}
-- Parse the TargetName data (i.e. the server authentication realm)
if ( domain_length > 0 ) then
local length = domain_length
local pos = domain_offset + 1 -- +1 to convert to Lua's 1-based indexes
local target_realm
target_realm = string.unpack("c" .. length, security_blob, pos )
ntlm_challenge[ "target_realm" ] = unicode.utf16to8( target_realm )
end
if hpos + domain_length > #security_blob then
-- Context, Target Information, and OS Version structure are all omitted
-- Probably Win9x
return ntlm_challenge
end
local context, target_info_length, target_info_max, target_info_offset, hpos = string.unpack("<I8 I2I2I4", security_blob, hpos)
-- OS info is in the intervening 8 bytes, subtract 1 for lua 1-index
if target_info_offset >= hpos + 7 and domain_offset >= hpos + 7 then
local major, minor, build, reserved = string.unpack("<BBI2c4", security_blob, hpos)
if reserved == "\0\0\0\x0f" then
ntlm_challenge.os_major_version = major
ntlm_challenge.os_minor_version = minor
ntlm_challenge.os_build = build
else
stdnse.debug2("smbauth: Unknown OS info structure in NTLM handshake")
end
end
-- Parse the TargetInfo data (Wireshark calls this the "Address List")
if ( target_info_length > 0 ) then
-- Definition of AvId values (IDs for AV_PAIR (attribute-value pair) structures),
-- as defined by the NTLM Authentication Protocol specification [MS-NLMP].
local NTLM_AV_ID_VALUES = {
MsvAvEOL = 0x0,
MsvAvNbComputerName = 0x1,
MsvAvNbDomainName = 0x2,
MsvAvDnsComputerName = 0x3,
MsvAvDnsDomainName = 0x4,
MsvAvDnsTreeName = 0x5,
MsvAvFlags = 0x6,
MsvAvTimestamp = 0x7,
MsvAvRestrictions = 0x8,
MsvAvTargetName = 0x9,
MsvAvChannelBindings = 0xA,
}
-- Friendlier names for AvId values, to be used as keys in the results table
-- e.g. ntlm_challenge[ "dns_computer_name" ] -> "host.test.local"
local NTLM_AV_ID_NAMES = {
[NTLM_AV_ID_VALUES.MsvAvNbComputerName] = "netbios_computer_name",
[NTLM_AV_ID_VALUES.MsvAvNbDomainName] = "netbios_domain_name",
[NTLM_AV_ID_VALUES.MsvAvDnsComputerName] = "fqdn",
[NTLM_AV_ID_VALUES.MsvAvDnsDomainName] = "dns_domain_name",
[NTLM_AV_ID_VALUES.MsvAvDnsTreeName] = "dns_forest_name",
[NTLM_AV_ID_VALUES.MsvAvTimestamp] = "timestamp",
}
local length = target_info_length
local pos = target_info_offset + 1 -- +1 to convert to Lua's 1-based indexes
local target_info
target_info = string.unpack("c" .. length, security_blob, pos)
pos = 1 -- reset pos to 1, since we'll be working out of just the target_info
repeat
local value, av_id
av_id, value, pos = string.unpack( "<I2s2", target_info, pos )
local friendly_name = NTLM_AV_ID_NAMES[ av_id ]
if ( av_id == NTLM_AV_ID_VALUES.MsvAvEOL ) then
break
elseif ( av_id == NTLM_AV_ID_VALUES.MsvAvTimestamp ) then
-- this is a FILETIME value (see [MS-DTYP]), representing the time in 100-ns increments since 1/1/1601
ntlm_challenge[ friendly_name ] = string.unpack( "<I8", value )
elseif ( friendly_name ) then
ntlm_challenge[ friendly_name ] = unicode.utf16to8( value )
end
until ( pos >= #target_info )
end
return ntlm_challenge
end
---Create an 8-byte message signature that's sent with all SMB packets.
--
--@param mac_key The key used for authentication. It's the concatenation of the
-- session key and the response hash.
--@param data The packet to generate the signature for. This should be the
-- packet that's about to be sent, except with the signature slot
-- replaced with the sequence number.
--@return The 8-byte signature. The signature is equal to the first eight bytes
-- of md5(mac_key .. smb_data)
function calculate_signature(mac_key, data)
if(have_ssl) then
return string.sub(openssl.md5(mac_key .. data), 1, 8)
else
return string.rep('\0', 8)
end
end
if not unittest.testing() then
return _ENV
end
test_suite = unittest.TestSuite:new()
-- OpenSSL-dependent crypto tests.
if have_ssl then
test_suite:add_test(unittest.equal(
stdnse.tohex(select(-1, lm_create_hash("passphrase"))),
"855c3697d9979e78ac404c4ba2c66533"
),
"lm_create_hash"
)
test_suite:add_test(unittest.equal(
stdnse.tohex(select(-1, ntlm_create_hash("passphrase"))),
"7f8fe03093cc84b267b109625f6bbf4b"
),
"ntlm_create_hash"
)
test_suite:add_test(unittest.equal(
stdnse.tohex(select(-1, lm_create_hash("ÅÇÅÇ"))),
"1830f5732b438091aad3b435b51404ee"
),
"lm_create_hash"
)
test_suite:add_test(unittest.equal(
stdnse.tohex(select(-1, ntlm_create_hash("öäü"))),
"4848bcb81cf018c3b70ea1479bd1374d"
),
"ntlm_create_hash"
)
end
return _ENV;
|