#!/usr/bin/env ruby # # MITM proxy. # # Author: et [at] metasploit.com 2009 # # openssl before rubygems mac os require 'openssl' require 'rubygems' require 'httpclient' require 'eventmachine' begin require 'sqlite3' rescue LoadError puts "Error: sqlite3-ruby not found" end msfbase = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__ $:.unshift(File.join(File.dirname(msfbase), '..', 'lib')) require 'rex' require 'msf/ui' require 'msf/base' # # Server properties # defaultport = 8080 defaultip = '0.0.0.0' $storedb = false $storedbpath = File.join(Msf::Config.get_config_root, 'sqlite3.db') $tmode = false $ttarget = "" $tssl = false # # Print request/response/connect # $printcon = false $printreq = false $printres = false $printstatus = false $modulepname = nil # # Used by modules for data storage # $modstore = {} module HttpProxy def initialize @isssl = false @sslconnectdone = false if $modulepname m = ::Module.new begin m.module_eval(File.read($modulepname, File.size($modulepname))) m.constants.grep(/^Tamper(.*)/) do mname = $1 klass = m.const_get("Tamper#{mname}") $modclass = klass.new() #puts("Loaded proxy module #{mname} from #{$modulepname}...") end rescue ::Exception => e puts("Proxy failed to load module: #{e.class} #{e}") exit end end end def post_init if $printcon client = Socket.unpack_sockaddr_in(get_peername) puts "Received a new connection from #{client.last}:#{client.first}" end # # Only for transparent mode # if $tmode and $tssl start_tls end end def get_first_line(data) # # Just the first line # firstline = "" data.each do |line| firstline = line.chomp break end return firstline end def parse_target_array(target,ssl) tarr = [] # # Clean garbage from target string and return [host,port,pathquery] # puri = target.sub(/^https:\/+|^http:\/+/,"") ppath = puri.scan(/\/.+|\//) tarr = puri.sub(/\/.+|\//,"").split(/:/) if !tarr[1] if ssl tarr[1] = 443 else tarr[1] = 80 end end if ppath[0] tarr[2] = ppath[0] else tarr[2] = "/" end return tarr end def receive_data(data) firstlinearray=[] # # Just for transparent mode # if $tmode @sslconnectdone = true @isssl = true end if !@sslconnectdone firstlinestr = get_first_line(data) firstlinearray = firstlinestr.split(" ") else @isssl = true end if !@isssl @targethost,@targetport,@targetpathquery = parse_target_array(firstlinearray[1], @usessl) if firstlinearray[0] and firstlinearray[0].include?("CONNECT") send_data "HTTP/1.0 200 Connection established\r\n\r\n" #start_tls(:verify_peer => false) start_tls @sslconnectdone = true else # # Adjust host:port/pathquery for /pathwuery on nonssl connection # data["#{firstlinestr}"] = "#{firstlinearray[0]} #{@targetpathquery} #{firstlinearray[2]}" if data.include? firstlinestr handle_connection(data,@isssl) end else # # Just for transparent mode # if $tmode dumbstr ="" @targethost,@targetport,dumbstr = parse_target_array($ttarget, $tssl) handle_connection(data,$tssl) else handle_connection(data,@isssl) end end end def handle_connection(request,usingssl) if $printreq p "REQUEST: #{request}" end # Use Rex::Proto::Http::Request to use # evasion techniques and allow to manipulate # request easily. modreq = Rex::Proto::Http::Request.new case modreq.parse(request) when Rex::Proto::Http::Packet::ParseCode::Completed # REQUEST INJECTION POINT if $modclass modreq = $modclass.tamper_request(modreq,usingssl) end # Done with user mods. if modreq.headers['Proxy-Connection'] modreq.headers['Connection'] = 'close' modreq.headers.delete('Proxy-Connection') end # Uncomment this line if you want to see clear text i.e. gzip #modreq.headers.delete('Accept-Encoding') # Adjust parsed request to httpclient method = modreq.method uri = "http://" if usingssl uri = "https://" end uritarget = "" uritarget << "#{@targethost}:#{@targetport}#{modreq.resource}" uri << uritarget query = modreq.qstring body = modreq.body #modreq.data? extheader = modreq.headers # # Using httpclient so not to deal with rebuilding a ruby http client # c = HTTPClient.new if usingssl c.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE end begin # Send Request resp = c.request(method, uri, query, body, extheader) respstr = "HTTP/#{resp.version} " respstr << resp.status.to_s respstr << " " respstr << resp.reason respstr << "\r\n" hr = resp.header.all headstr = "" hr.collect { |var, val| if var == "Transfer-Encoding" headstr << "Content-Length: #{resp.content.length}\r\n" else headstr << "#{var}: #{val.to_s}\r\n" end } headstr << "\r\n" respstr << headstr respstr << resp.content if $printstatus puts "[-] #{resp.status.to_s}\t#{@targethost}\t#{modreq.resource}\t#{modreq.method} #{resp.content.length}" end # # store request and response in Metasploit # sqlite3 db # if $storedb sslint = 0 if usingssl sslint = 1 end strq = "" modreq.qstring.each_pair do |k,v| if strq.empty? strq = k + "=" + v else strq = k + "=" + v + "&"+ strq end end # Using $db as connection Thread.new{ until !$db.transaction_active? puts "Waiting for db" #wait end $db.transaction $db.execute( "insert into wmap_requests values ( ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", nil, @targethost, @targethost, @targethost, @targetport, sslint, modreq.method, SQLite3::Blob.new(modreq.resource), SQLite3::Blob.new(modreq.headers.to_s), SQLite3::Blob.new(strq), SQLite3::Blob.new(modreq.body), resp.status.to_s, SQLite3::Blob.new(headstr), SQLite3::Blob.new(resp.content), Time.new ) $db.commit }.join end # # Response # # RESPONSE INJECTION POINT if $modclass respstr = $modclass.tamper_response(respstr,usingssl) end # Done with user mods. if $printres p "RESPONSE: #{respstr}" end # Send response to client send_data respstr rescue HTTPClient::ConnectTimeoutError => exc # Can configure connection timeout via HTTPClient#connect_timeout=. puts "Error: ConnectTimeoutError to #{@targethost}: #{exc.message}" rescue HTTPClient::ReceiveTimeoutError => exc # Can configure connection timeout via HTTPClient#receive_timeout=. puts "Error: ReceiveTimeoutError to #{@targethost}: #{exc.message}" end when Rex::Proto::Http::Packet::ParseCode::Error p "Parsing Error!!!" end unbind end def unbind self.close_connection_after_writing end end def usage $stderr.puts("\n" + " Usage: #{$0} \n" + $args.usage) exit end trap("INT") { exit() } $args = Rex::Parser::Arguments.new( "-a" => [ false, "Print requests: Default false" ], "-b" => [ false, "Print responses: Default false" ], "-c" => [ false, "Print connection message: Default false"], "-u" => [ false, "Print status: Default false"], "-v" => [ false, "Print requests and responses: Default false" ], "-i" => [ true, "Listening IP address. Default 0.0.0.0" ], "-p" => [ true, "Listening proxy port. Default 8080" ], "-d" => [ false, "Store requests to Metasploit database" ], "-w" => [ true, "Metasploit database path" ], "-t" => [ true, "Transparent mode. http(s)://host:port." ], "-m" => [ true, "Load module. path/module.rb."], "-h" => [ false, "Display this help information" ]) $args.parse(ARGV) { |opt, idx, val| case opt when "-a" $printreq = true when "-b" $printres = true when "-c" $printcon = true when "-u" $printstatus = true when "-v" $printreq = true $printres = true $printcon = true when "-d" $storedb = true puts "Storing requests in #{$storedbpath}." $db = SQLite3::Database.new($storedbpath) when "-w" $storedbpath = val $storedb = true puts "Storing requests in #{$storedbpath}." $db = SQLite3::Database.new($storedbpath) when "-i" defaultip = val when "-m" $modulepname = val when "-p" defaultport = val when "-t" $tmode = true $ttarget = val puts "Transparent mode: #{$ttarget}" if $ttarget.include?("https://") $tssl = true end when "-h" usage end } EventMachine::run { puts "SSL Support: #{EM.ssl?}." EM.epoll EM::start_server(defaultip, defaultport, HttpProxy) puts "Listening on #{defaultip} port #{defaultport}." }