diff --git a/extensions/proxy/api.rb b/extensions/proxy/api.rb index d14bfe5e8..294443f5b 100644 --- a/extensions/proxy/api.rb +++ b/extensions/proxy/api.rb @@ -24,14 +24,17 @@ module API BeEF::API::Registrar.instance.register(BeEF::Extension::Proxy::API::RegisterHttpHandler, BeEF::API::Server, 'mount_handler') def self.pre_http_start(http_hook_server) - proxy = BeEF::Extension::Proxy::HttpProxyZombie.instance - proxy.start - config = BeEF::Core::Configuration.instance + config = BeEF::Core::Configuration.instance + Thread.new{ + http_hook_server.semaphore.synchronize{ + BeEF::Extension::Proxy::Proxy.new + } + } print_success "HTTP Proxy: http://#{config.get('beef.extension.proxy.address')}:#{config.get('beef.extension.proxy.port')}" end def self.mount_handler(beef_server) - beef_server.mount('/proxy', false, BeEF::Extension::Requester::Handler) + beef_server.mount('/proxy', BeEF::Extension::Requester::Handler) end end diff --git a/extensions/proxy/base.rb b/extensions/proxy/base.rb deleted file mode 100644 index 2c085b8b9..000000000 --- a/extensions/proxy/base.rb +++ /dev/null @@ -1,43 +0,0 @@ -# -# Copyright 2011 Wade Alcorn wade@bindshell.net -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -module BeEF -module Extension -module Proxy - - class HttpProxyBase < WEBrick::HTTPProxyServer - - # call BeEF::HttpProxyZombie.instance - include Singleton - - attr_reader :config - - def initialize - @configuration = BeEF::Core::Configuration.instance - @config[:Logger] = WEBrick::Log.new($stdout, WEBrick::Log::ERROR) - @config[:ServerType] = Thread - super(@config) - end - - # remove beef hook if it exists - def remove_hook(res) - print_debug "[PROXY] Removing beef hook from page if present" - res.body.gsub!(%r'', '') - end - end - -end -end -end diff --git a/extensions/proxy/extension.rb b/extensions/proxy/extension.rb index 503383810..775de7edd 100644 --- a/extensions/proxy/extension.rb +++ b/extensions/proxy/extension.rb @@ -21,18 +21,12 @@ module Proxy @short_name = 'proxy' @full_name = 'proxy' - @description = 'The proxy allow to tunnel HTTP requests to the hooked domain through the victim browser' + @description = 'The tunneling proxy allow to tunnel HTTP requests to the hooked domain through the victim browser' end end end -require 'webrick/httpproxy' -require 'webrick/httputils' -require 'webrick/httprequest' -require 'webrick/httpresponse' require 'extensions/requester/models/http' -require 'extensions/proxy/base' -require 'extensions/proxy/zombie' -require 'extensions/proxy/api' -require 'extensions/proxy/handlers/zombie/handler' +require 'extensions/proxy/proxy' +require 'extensions/proxy/api' \ No newline at end of file diff --git a/extensions/proxy/handlers/zombie/handler.rb b/extensions/proxy/handlers/zombie/handler.rb deleted file mode 100644 index e78ae9666..000000000 --- a/extensions/proxy/handlers/zombie/handler.rb +++ /dev/null @@ -1,130 +0,0 @@ -# -# Copyright 2011 Wade Alcorn wade@bindshell.net -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -module BeEF -module Extension -module Proxy -module Handlers -module Zombie - - class Handler - - attr_reader :guard - @response = nil - H = BeEF::Core::Models::Http - - # This function will forward requests to the target and - # the browser will perform the request. Then the results - # will be sent back. - def forward_request(hooked_browser_id, req, res) - - # validate that the raw request is correct and can be used - req_parts = req.to_s.split(/ |\n/) # break up the request - verb = req_parts[0] - raise 'Only HEAD, GET, POST, OPTIONS, PUT or DELETE requests are supported' if not BeEF::Filters.is_valid_verb?(verb) #check verb - # antisnatchor: is_valid_url supposes that the uri is relative, while here we're passing an absolute one - #uri = req_parts[1] - #raise 'Invalid URI' if not BeEF::Filters.is_valid_url?(uri) #check uri - version = req_parts[2] - raise 'Invalid HTTP version' if not BeEF::Filters.is_valid_http_version?(version) # check http version - HTTP/1.0 - # antisnatchor: the following checks are wrong. the req_parts array can always contains elements at different postions. - # for example proxying Opera, the req_parts[3] is the User-Agent header... -# host_str = req_parts[3] -# raise 'Invalid HTTP host header' if not BeEF::Filters.is_valid_host_str?(host_str) # check host string - Host: -# host = req_parts[4] -# host_parts = host.split(/:/) -# hostname = host_parts[0] -# raise 'Invalid hostname' if not BeEF::Filters.is_valid_hostname?(hostname) #check the target hostname -# hostport = host_parts[1] || nil -# if !hostport.nil? -# raise 'Invalid hostport' if not BeEF::Filters.nums_only?(hostport) #check the target hostport -# end - - # Saves the new HTTP request to the db for processing by browser. - # IDs are created and incremented automatically by DataMapper. - http = H.new( - :request => req, - :method => req.request_method.to_s, - :domain => req.host, - :port => req.port, - :path => req.path.to_s, - :request_date => Time.now, - :hooked_browser_id => hooked_browser_id - ) - http.save - - # Starts a new thread scoped to this Handler instance, in order to minimize performance degradation - # while waiting for the HTTP response to be stored in the db. - print_info("[PROXY] Thread started in order to process request ##{http.id} to [#{req.path.to_s}] on domain [#{req.host}:#{req.port}]") - @response_thread = Thread.new do - while H.first(:id => http.id).has_ran != "complete" - sleep 0.5 - end - @response = H.first(:id => http.id) - end - - @response_thread.join - print_info("[PROXY] Response for request ##{http.id} to [#{req.path.to_s}] on domain [#{req.host}:#{req.port}] correctly processed") - - res.body = @response['response_data'] - - # set the original response status code - res.status = @response['response_status_code'] - - headers = @response['response_headers'] - #print_debug("====== original HTTP response headers =======\n#{headers}") - - # The following is needed to forward back some of the original HTTP response headers obtained via XHR calls. - # Original XHR response headers are stored in extension_requester_http table (response_headers column), - # but we are forwarding back only some of them (Server, X-.. - like X-Powered-By -, Content-Type, ... ). - # Some of the original response headers need to be removed, like encoding and cache related: for example - # about encoding, the original response headers says that the content-length is 1000 as the response is gzipped, - # but the final content-length forwarded back by the proxy is clearly bigger. Date header follows the same way. - headers_hash = Hash.new - if(res.status != -1 && res.status != 0) - headers.each_line do |line| - # stripping the Encoding, Cache and other headers - header_key = line.split(': ')[0] - header_value = line.split(': ')[1] - if !header_key.nil? && - header_key != "Content-Encoding" && - header_key != "Content-Length" && - header_key != "Keep-Alive" && - header_key != "Cache-Control" && - header_key != "Vary" && - header_key != "Pragma" && - header_key != "Connection" && - header_key != "Expires" && - header_key != "Accept-Ranges" && - header_key != "Date" - if header_value.nil? - #headers_hash[header_key] = "" - else - headers_hash[header_key] = header_value.gsub!(/[\n]+/,"") - end - end - end - - # note: override_headers is a (new) method of WebRick::HTTPResponse (the BeEF patch one: core\ruby\patches\webrick\httpresponse.rb) - res.override_headers(headers_hash) - end - res - end - end -end -end -end -end -end diff --git a/extensions/proxy/proxy.rb b/extensions/proxy/proxy.rb new file mode 100644 index 000000000..e0beb9bcd --- /dev/null +++ b/extensions/proxy/proxy.rb @@ -0,0 +1,151 @@ +# +# Copyright 2011 Wade Alcorn wade@bindshell.net +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +module BeEF + module Extension + module Proxy + class Proxy + + HB = BeEF::Core::Models::HookedBrowser + H = BeEF::Core::Models::Http + @response = nil + + # Multi-threaded Tunneling Proxy: listens on host:port configured in extensions/proxy/config.yaml + # and forwards requests to the hooked browser using the Requester component. + def initialize + @conf = BeEF::Core::Configuration.instance + @proxy_server = TCPServer.new(@conf.get('beef.extension.proxy.address'), @conf.get('beef.extension.proxy.port')) + + loop do + proxy = @proxy_server.accept + Thread.new proxy, &method(:handle_request) + end + end + + def handle_request socket + request_line = socket.readline + + method = request_line[/^\w+/] + url = request_line[/^\w+\s+(\S+)/, 1] + version = request_line[/HTTP\/(1\.\d)\s*$/, 1] + + # We're overwriting the URI::Parser UNRESERVED regex to prevent BAD URI errors when sending attack vectors (see tolerant_parser) + tolerant_parser = URI::Parser.new(:UNRESERVED => BeEF::Core::Configuration.instance.get("beef.extension.requester.uri_unreserved_chars")) + uri = tolerant_parser.parse(url) + + raw_request = request_line + content_length = 0 + + loop do + line = socket.readline + + if line =~ /^Content-Length:\s+(\d+)\s*$/ + content_length = $1.to_i + end + + if line.strip.empty? + # read data still in the socket, exactly bytes + if content_length >= 0 + raw_request += "\r\n" + socket.read(content_length) + end + break + else + raw_request += line + end + end + + # Saves the new HTTP request to the db. It will be processed by the PreHookCallback of the requester component. + # IDs are created and incremented automatically by DataMapper. + http = H.new( + :request => raw_request, + :method => method, + :domain => uri.host, + :port => uri.port, + :path => uri.path, + :request_date => Time.now, + :hooked_browser_id => self.get_tunneling_proxy + ) + http.save + print_debug("[PROXY] --> Forwarding request ##{http.id}: domain[#{http.domain}:#{http.port}], method[#{http.method}], path[#{http.path}]") + + # Wait for the HTTP response to be stored in the db. + # TODO: re-implement this with EventMachine or with the Observer pattern. + while H.first(:id => http.id).has_ran != "complete" + sleep 0.5 + end + @response = H.first(:id => http.id) + print_debug "[PROXY] <-- Response for request ##{@response.id} to [#{@response.path}] on domain [#{@response.domain}:#{@response.port}] correctly processed" + + response_body = @response['response_data'] + response_status = @response['response_status_code'] + headers = @response['response_headers'] + + # The following is needed to forward back some of the original HTTP response headers obtained via XHR calls. + # Original XHR response headers are stored in extension_requester_http table (response_headers column), + # but we are forwarding back only some of them (Server, X-.. - like X-Powered-By -, Content-Type, ... ). + # Some of the original response headers need to be removed, like encoding and cache related: for example + # about encoding, the original response headers says that the content-length is 1000 as the response is gzipped, + # but the final content-length forwarded back by the proxy is clearly bigger. Date header follows the same way. + response_headers = "" + if (response_status != -1 && response_status != 0) + headers.each_line do |line| + # stripping the Encoding, Cache and other headers + header_key = line.split(': ')[0] + header_value = line.split(': ')[1] + if !header_key.nil? && + header_key != "Content-Encoding" && + header_key != "Keep-Alive" && + header_key != "Cache-Control" && + header_key != "Vary" && + header_key != "Pragma" && + header_key != "Connection" && + header_key != "Expires" && + header_key != "Accept-Ranges" && + header_key != "Date" + if header_value.nil? + #headers_hash[header_key] = "" + else + # update Content-Length with the valid one + if header_key == "Content-Length" + header_value = response_body.size + response_headers += "Content-Length: #{header_value}\r\n" + else + response_headers += line + end + end + end + end + end + + res = "HTTP/#{version} #{response_status}\r\n#{response_headers}\r\n\r\n#{response_body}" + socket.write(res) + socket.close + end + + def get_tunneling_proxy + proxy_browser = HB.first(:is_proxy => true) + if (proxy_browser != nil) + proxy_browser_id = proxy_browser.id.to_s + else + proxy_browser_id = 1 + print_debug "[PROXY] Proxy browser not set. Defaulting to browser id #1" + end + proxy_browser_id + end + end + end + end +end + diff --git a/extensions/proxy/zombie.rb b/extensions/proxy/zombie.rb deleted file mode 100644 index 631d8e9cf..000000000 --- a/extensions/proxy/zombie.rb +++ /dev/null @@ -1,55 +0,0 @@ -# -# Copyright 2011 Wade Alcorn wade@bindshell.net -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -module BeEF -module Extension -module Proxy - - class HttpProxyZombie < BeEF::Extension::Proxy::HttpProxyBase - - HB = BeEF::Core::Models::HookedBrowser - - def initialize - @configuration = BeEF::Core::Configuration.instance - @config = {} - @config[:BindAddress] = @configuration.get('beef.extension.proxy.address') - @config[:Port] = @configuration.get('beef.extension.proxy.port') - @config[:ServerName] = "BeEF " + @configuration.get('beef.version') - @config[:ServerSoftware] = "BeEF " + @configuration.get('beef.version') - super - end - - def service(req, res) - proxy_browser = HB.first(:is_proxy => true) - if(proxy_browser != nil) - proxy_browser_id = proxy_browser.id.to_s - #print_debug "[PROXY] Current proxy browser id is #" + proxy_browser_id - else - proxy_browser_id = 1 - print_debug "[PROXY] Proxy browser not set. Defaulting to browser id #1" - end - - forwarder = BeEF::Extension::Proxy::Handlers::Zombie::Handler.new - res = forwarder.forward_request(proxy_browser_id, req, res) - res - # remove beef hook if it exists - #remove_hook(res) - - end - end - -end -end -end