Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/lib/rex/proto/ldap/auth.rb
Views: 11704
require 'net/ldap'1require 'net/ldap/dn'23module Rex4module Proto5module LDAP6class Auth7SUPPORTS_SASL = %w[GSS-SPNEGO NTLM]8NTLM_CONST = Rex::Proto::NTLM::Constants9NTLM_CRYPT = Rex::Proto::NTLM::Crypt10MESSAGE = Rex::Proto::NTLM::Message1112#13# Initialize the required variables14#15# @param challenge [String] NTLM Server Challenge16# @param domain [String] Domain value used in NTLM17# @param server [String] Server value used in NTLM18# @param dnsname [String] DNS Name value used in NTLM19# @param dnsdomain [String] DNS Domain value used in NTLM20def initialize(challenge, domain, server, dnsname, dnsdomain)21@domain = domain.nil? ? 'DOMAIN' : domain22@server = server.nil? ? 'SERVER' : server23@dnsname = dnsname.nil? ? 'server' : dnsname24@dnsdomain = dnsdomain.nil? ? 'example.com' : dnsdomain25@challenge = [challenge.nil? ? Rex::Text.rand_text_alphanumeric(16) : challenge].pack('H*')26end2728#29# Process the incoming LDAP login requests from clients30#31# @param user_login [OpenStruct] User login information32#33# @return auth_info [Hash] Processed authentication information34def process_login_request(user_login)35auth_info = {}3637if user_login.name.empty? && user_login.authentication.empty? # Anonymous38auth_info = handle_anonymous_request(user_login, auth_info)39elsif !user_login.name.empty? # Simple40auth_info = handle_simple_request(user_login, auth_info)41elsif sasl?(user_login)42auth_info = handle_sasl_request(user_login, auth_info)43else44auth_info = handle_unknown_request(user_login, auth_info)45end4647auth_info48end4950#51# Handle Anonymous authentication requests52#53# @param user_login [OpenStruct] User login information54# @param auth_info [Hash] Processed authentication information55#56# @return auth_info [Hash] Processed authentication information57def handle_anonymous_request(user_login, auth_info = {})58if user_login.name.empty? && user_login.authentication.empty?59auth_info[:user] = user_login.name60auth_info[:pass] = user_login.authentication61auth_info[:domain] = nil62auth_info[:result_code] = Net::LDAP::ResultCodeSuccess63auth_info[:auth_type] = 'Anonymous'64end65auth_info66end6768#69# Handle Unknown authentication requests70#71# @param user_login [OpenStruct] User login information72# @param auth_info [Hash] Processed authentication information73#74# @return auth_info [Hash] Processed authentication information75def handle_unknown_request(user_login, auth_info = {})76auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported77auth_info[:error_msg] = 'Invalid LDAP Login Attempt => Unknown Authentication Format'78auth_info79end8081#82# Handle Simple authentication requests83#84# @param user_login [OpenStruct] User login information85# @param auth_info [Hash] Processed authentication information86#87# @return auth_info [Hash] Processed authentication information88def handle_simple_request(user_login, auth_info = {})89domains = []90names = []91if !user_login.name.empty?92if user_login.name =~ /@/93pub_info = user_login.name.split('@')94if pub_info.length <= 295auth_info[:user], auth_info[:domain] = pub_info96else97auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials98auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}"99end100elsif user_login.name =~ /,/101begin102dn = Net::LDAP::DN.new(user_login.name)103dn.each_pair do |key, value|104if key == 'cn'105names << value106elsif key == 'dc'107domains << value108end109end110auth_info[:user] = names.join('')111auth_info[:domain] = domains.empty? ? nil : domains.join('.')112rescue Net::LDAP::InvalidDNError => e113auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}"114raise e115end116elsif user_login.name =~ /\\/117pub_info = user_login.name.split('\\')118if pub_info.length <= 2119auth_info[:domain], auth_info[:user] = pub_info120else121auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials122auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}"123end124else125auth_info[:user] = user_login.name126auth_info[:domain] = nil127auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials128end129auth_info[:private] = user_login.authentication130auth_info[:private_type] = :password131auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported if auth_info[:result_code].nil?132auth_info[:auth_type] = 'Simple'133auth_info134end135end136137#138# Handle SASL authentication requests139#140# @param user_login [OpenStruct] User login information141# @param auth_info [Hash] Processed authentication information142#143# @return auth_info [Hash] Processed authentication information144def handle_sasl_request(user_login, auth_info = {})145case user_login.authentication[1]146when /NTLMSSP/147message = Net::NTLM::Message.parse(user_login.authentication[1])148if message.is_a?(::Net::NTLM::Message::Type1)149auth_info[:server_creds] = generate_type2_response(message)150auth_info[:result_code] = Net::LDAP::ResultCodeSaslBindInProgress151elsif message.is_a?(::Net::NTLM::Message::Type3)152auth_info = handle_type3_message(message, auth_info)153auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported154end155else156auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported157auth_info[:error_msg] = 'Invalid LDAP Login Attempt => Unsupported SASL Format'158end159auth_info[:auth_type] = 'SASL'160auth_info161end162163private164165#166# Determine if the supplied request is formatted for SASL auth167#168# @param user_login [OpenStruct] User login information169#170# @return [bool] True if the request can be processed for SASL auth171def sasl?(user_login)172if user_login.authentication.is_a?(Array) && SUPPORTS_SASL.include?(user_login.authentication[0])173return true174end175176false177end178179#180# Generate NTLM Type2 response from NTLM Type1 message181#182# @param message [Net::NTLM::Message::Type1] NTLM Type1 message183#184# @return server_hash [String] NTLM Type2 response that is sent as server credentials185def generate_type2_response(message)186dom = message.domain187ws = message.workstation188domain = dom.empty? ? @domain : dom189server = ws.empty? ? @server : ws190server_hash = MESSAGE.process_type1_message(message.encode64, @challenge, domain, server, @dnsname, @dnsdomain)191Rex::Text.decode_base64(server_hash)192end193194#195# Handle NTLM Type3 message196#197# @param message [Net::NTLM::Message::Type3] NTLM Type3 message198# @param auth_info [Hash] Processed authentication information199#200# @return auth_info [Hash] Processed authentication information201def handle_type3_message(message, auth_info = {})202arg = {203domain: message.domain,204user: message.user,205host: message.workstation206}207208domain, user, host, lm_hash, ntlm_hash = MESSAGE.process_type3_message(message.encode64)209nt_len = ntlm_hash.length210211if nt_len == 48212arg[:ntlm_ver] = NTLM_CONST::NTLM_V1_RESPONSE213arg[:lm_hash] = lm_hash214arg[:nt_hash] = ntlm_hash215216if arg[:lm_hash][16, 32] == '0' * 32217arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE218end219elsif nt_len > 48220arg[:ntlm_ver] = NTLM_CONST::NTLM_V2_RESPONSE221arg[:lm_hash] = lm_hash[0, 32]222arg[:lm_cli_challenge] = lm_hash[32, 16]223arg[:nt_hash] = ntlm_hash[0, 32]224arg[:nt_cli_challenge] = ntlm_hash[32, nt_len - 32]225else226auth_info[:error_msg] = "Unknown hash type from #{host}, ignoring ..."227end228auth_info.merge(process_ntlm_hash(arg)) unless arg.nil?229end230231#232# Process the NTLM Hash received from NTLM Type3 message233#234# @param arg [Hash] authentication information received from Type3 message235#236# @return arg [Hash] Processed NTLM authentication information237def process_ntlm_hash(arg = {})238ntlm_ver = arg[:ntlm_ver]239lm_hash = arg[:lm_hash]240nt_hash = arg[:nt_hash]241unless ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE || ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE242lm_cli_challenge = arg[:lm_cli_challenge]243nt_cli_challenge = arg[:nt_cli_challenge]244end245domain = Rex::Text.to_ascii(arg[:domain])246user = Rex::Text.to_ascii(arg[:user])247host = Rex::Text.to_ascii(arg[:host])248249case ntlm_ver250when NTLM_CONST::NTLM_V1_RESPONSE251if NTLM_CRYPT.is_hash_from_empty_pwd?({252hash: [nt_hash].pack('H*'),253srv_challenge: @challenge,254ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE,255type: 'ntlm'256})257arg[:error_msg] = 'NLMv1 Hash correspond to an empty password, ignoring ... '258return259end260if lm_hash == nt_hash || lm_hash == '' || lm_hash =~ /^0*$/261lm_hash_message = 'Disabled'262elsif NTLM_CRYPT.is_hash_from_empty_pwd?({263hash: [lm_hash].pack('H*'),264srv_challenge: @challenge,265ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE,266type: 'lm'267})268lm_hash_message = 'Disabled (from empty password)'269else270lm_hash_message = lm_hash271end272273hash = [274lm_hash || '0' * 48,275nt_hash || '0' * 48276].join(':').gsub(/\n/, '\\n')277arg[:private] = hash278when NTLM_CONST::NTLM_V2_RESPONSE279if NTLM_CRYPT.is_hash_from_empty_pwd?({280hash: [nt_hash].pack('H*'),281srv_challenge: @challenge,282cli_challenge: [nt_cli_challenge].pack('H*'),283user: user,284domain: domain,285ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE,286type: 'ntlm'287})288arg[:error_msg] = 'NTLMv2 Hash correspond to an empty password, ignoring ... '289return290end291if (lm_hash == '0' * 32) && (lm_cli_challenge == '0' * 16)292lm_hash_message = 'Disabled'293elsif NTLM_CRYPT.is_hash_from_empty_pwd?({294hash: [lm_hash].pack('H*'),295srv_challenge: @challenge,296cli_challenge: [lm_cli_challenge].pack('H*'),297user: user,298domain: domain,299ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE,300type: 'lm'301})302lm_hash_message = 'Disabled (from empty password)'303else304lm_hash_message = lm_hash305end306307hash = [308lm_hash || '0' * 32,309nt_hash || '0' * 32310].join(':').gsub(/\n/, '\\n')311arg[:private] = hash312when NTLM_CONST::NTLM_2_SESSION_RESPONSE313if NTLM_CRYPT.is_hash_from_empty_pwd?({314hash: [nt_hash].pack('H*'),315srv_challenge: @challenge,316cli_challenge: [lm_hash].pack('H*')[0, 8],317ntlm_ver: NTLM_CONST::NTLM_2_SESSION_RESPONSE,318type: 'ntlm'319})320arg[:error_msg] = 'NTLM2_session Hash correspond to an empty password, ignoring ... '321return322end323324hash = [325lm_hash || '0' * 48,326nt_hash || '0' * 48327].join(':').gsub(/\n/, '\\n')328arg[:private] = hash329else330return331end332arg[:domain] = domain333arg[:user] = user334arg[:host] = host335arg[:private_type] = :ntlm_hash336arg337end338end339end340end341end342343344