From 247e0e9a622ab8930342c5f57b4ee21facd323c6 Mon Sep 17 00:00:00 2001 From: antisnatchor Date: Fri, 11 May 2012 15:58:59 +0100 Subject: [PATCH] Issue 676. Now we use em-websocket for WebSocket server side. Instead of threads we use events with EventMachine. Faster and consumes less memory. --- Gemfile | 1 + beef | 1 + core/bootstrap.rb | 1 - core/main/handlers/modules/command.rb | 4 +- .../network_stack/websocket/lib/web_socket.rb | 592 ------------------ .../main/network_stack/websocket/websocket.rb | 78 ++- 6 files changed, 39 insertions(+), 638 deletions(-) delete mode 100644 core/main/network_stack/websocket/lib/web_socket.rb diff --git a/Gemfile b/Gemfile index 2c7b9651e..a4035422e 100644 --- a/Gemfile +++ b/Gemfile @@ -26,6 +26,7 @@ end gem "thin" gem "sinatra", "1.3.2" +gem "em-websocket", "~> 0.3.6" gem "ansi" gem "term-ansicolor", :require => "term/ansicolor" gem "dm-core" diff --git a/beef b/beef index 0c1cc740f..b99cc5672 100755 --- a/beef +++ b/beef @@ -115,6 +115,7 @@ print_info "RESTful API key: #{BeEF::Core::Crypto::api_token}" #@note Starts the WebSocket server if config.get("beef.http.websocket.enable") BeEF::Core::Websocket::Websocket.instance + print_info "Starting WebSocket server on port [#{config.get("beef.http.websocket.port")}], secure [#{config.get("beef.http.websocket.secure")}], timer [#{config.get("beef.http.websocket.alive_timer")}]" end diff --git a/core/bootstrap.rb b/core/bootstrap.rb index 747720849..0ae748c22 100644 --- a/core/bootstrap.rb +++ b/core/bootstrap.rb @@ -55,5 +55,4 @@ require 'core/main/rest/handlers/admin' require 'core/main/rest/api' ## @note Include Websocket -require 'core/main/network_stack/websocket/lib/web_socket' require 'core/main/network_stack/websocket/websocket' diff --git a/core/main/handlers/modules/command.rb b/core/main/handlers/modules/command.rb index 62d881378..e79516fd5 100644 --- a/core/main/handlers/modules/command.rb +++ b/core/main/handlers/modules/command.rb @@ -49,8 +49,8 @@ module BeEF build_missing_beefjs_components(command_module.beefjs_components) if not command_module.beefjs_components.empty? let= BeEF::Core::Websocket::Websocket.instance - #@todo radoen debug this one + #todo antisnatchor: remove this gsub crap adding some hook packing. if let.getsocket(hooked_browser.session) funtosend=command_module.output.gsub('// // Copyright 2012 Wade Alcorn wade@bindshell.net @@ -68,10 +68,8 @@ module BeEF // limitations under the License. //', "") let.sent(funtosend, hooked_browser.session) - #print_info("We are sending #{funtosend}") else @body << command_module.output + "\n\n" - end # @note prints the event to the console if BeEF::Settings.console? diff --git a/core/main/network_stack/websocket/lib/web_socket.rb b/core/main/network_stack/websocket/lib/web_socket.rb deleted file mode 100644 index e2cbcb56c..000000000 --- a/core/main/network_stack/websocket/lib/web_socket.rb +++ /dev/null @@ -1,592 +0,0 @@ -# Copyright: Hiroshi Ichikawa -# Lincense: New BSD Lincense -# Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75 -# Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 -# Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07 -# Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10 - -require "base64" -require "socket" -require "uri" -require "digest/md5" -require "digest/sha1" -require "openssl" -require "stringio" - - -class WebSocket - - class << self - - attr_accessor(:debug) - - end - - class Error < RuntimeError - - end - - WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - OPCODE_CONTINUATION = 0x00 - OPCODE_TEXT = 0x01 - OPCODE_BINARY = 0x02 - OPCODE_CLOSE = 0x08 - OPCODE_PING = 0x09 - OPCODE_PONG = 0x0a - - def initialize(arg, params = {}) - if params[:server] # server - - @server = params[:server] - @socket = arg - line = gets().chomp() - if !(line =~ /\AGET (\S+) HTTP\/1.1\z/n) - raise(WebSocket::Error, "invalid request: #{line}") - end - @path = $1 - read_header() - if @header["sec-websocket-version"] - @web_socket_version = @header["sec-websocket-version"] - @key3 = nil - elsif @header["sec-websocket-key1"] && @header["sec-websocket-key2"] - @web_socket_version = "hixie-76" - @key3 = read(8) - else - @web_socket_version = "hixie-75" - @key3 = nil - end - if !@server.accepted_origin?(self.origin) - raise(WebSocket::Error, - ("Unaccepted origin: %s (server.accepted_domains = %p)\n\n" + - "To accept this origin, write e.g. \n" + - " WebSocketServer.new(..., :accepted_domains => [%p]), or\n" + - " WebSocketServer.new(..., :accepted_domains => [\"*\"])\n") % - [self.origin, @server.accepted_domains, @server.origin_to_domain(self.origin)]) - end - @handshaked = false - - else # client - - @web_socket_version = "hixie-76" - uri = arg.is_a?(String) ? URI.parse(arg) : arg - - if uri.scheme == "ws" - default_port = 80 - elsif uri.scheme = "wss" - default_port = 443 - else - raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}") - end - - @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "") - host = uri.host + ((!uri.port || uri.port == default_port) ? "" : ":#{uri.port}") - origin = params[:origin] || "http://#{uri.host}" - key1 = generate_key() - key2 = generate_key() - key3 = generate_key3() - - socket = TCPSocket.new(uri.host, uri.port || default_port) - - if uri.scheme == "ws" - @socket = socket - else - @socket = ssl_handshake(socket) - end - - write( - "GET #{@path} HTTP/1.1\r\n" + - "Upgrade: WebSocket\r\n" + - "Connection: Upgrade\r\n" + - "Host: #{host}\r\n" + - "Origin: #{origin}\r\n" + - "Sec-WebSocket-Key1: #{key1}\r\n" + - "Sec-WebSocket-Key2: #{key2}\r\n" + - "\r\n" + - "#{key3}") - flush() - - line = gets().chomp() - raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n) - read_header() - if (@header["sec-websocket-origin"] || "").downcase() != origin.downcase() - raise(WebSocket::Error, - "origin doesn't match: '#{@header["sec-websocket-origin"]}' != '#{origin}'") - end - reply_digest = read(16) - expected_digest = hixie_76_security_digest(key1, key2, key3) - if reply_digest != expected_digest - raise(WebSocket::Error, - "security digest doesn't match: %p != %p" % [reply_digest, expected_digest]) - end - @handshaked = true - - end - @received = [] - @buffer = "" - @closing_started = false - end - - attr_reader(:server, :header, :path) - - def handshake(status = nil, header = {}) - if @handshaked - raise(WebSocket::Error, "handshake has already been done") - end - status ||= "101 Switching Protocols" - def_header = {} - case @web_socket_version - when "hixie-75" - def_header["WebSocket-Origin"] = self.origin - def_header["WebSocket-Location"] = self.location - extra_bytes = "" - when "hixie-76" - def_header["Sec-WebSocket-Origin"] = self.origin - def_header["Sec-WebSocket-Location"] = self.location - extra_bytes = hixie_76_security_digest( - @header["Sec-WebSocket-Key1"], @header["Sec-WebSocket-Key2"], @key3) - else - def_header["Sec-WebSocket-Accept"] = security_digest(@header["sec-websocket-key"]) - extra_bytes = "" - end - header = def_header.merge(header) - header_str = header.map(){ |k, v| "#{k}: #{v}\r\n" }.join("") - # Note that Upgrade and Connection must appear in this order. - write( - "HTTP/1.1 #{status}\r\n" + - "Upgrade: websocket\r\n" + - "Connection: Upgrade\r\n" + - "#{header_str}\r\n#{extra_bytes}") - flush() - @handshaked = true - end - - def send(data) - if !@handshaked - raise(WebSocket::Error, "call WebSocket\#handshake first") - end - case @web_socket_version - when "hixie-75", "hixie-76" - data = force_encoding(data.dup(), "ASCII-8BIT") - write("\x00#{data}\xff") - flush() - else - send_frame(OPCODE_TEXT, data, !@server) - end - end - - def receive() - if !@handshaked - raise(WebSocket::Error, "call WebSocket\#handshake first") - end - case @web_socket_version - - when "hixie-75", "hixie-76" - packet = gets("\xff") - return nil if !packet - if packet =~ /\A\x00(.*)\xff\z/nm - return force_encoding($1, "UTF-8") - elsif packet == "\xff" && read(1) == "\x00" # closing - close(1005, "", :peer) - return nil - else - raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'") - end - - else - begin - bytes = read(2).unpack("C*") - fin = (bytes[0] & 0x80) != 0 - opcode = bytes[0] & 0x0f - mask = (bytes[1] & 0x80) != 0 - plength = bytes[1] & 0x7f - if plength == 126 - bytes = read(2) - plength = bytes.unpack("n")[0] - elsif plength == 127 - bytes = read(8) - (high, low) = bytes.unpack("NN") - plength = high * (2 ** 32) + low - end - if @server && !mask - # Masking is required. - @socket.close() - raise(WebSocket::Error, "received unmasked data") - end - mask_key = mask ? read(4).unpack("C*") : nil - payload = read(plength) - payload = apply_mask(payload, mask_key) if mask - case opcode - when OPCODE_TEXT - return force_encoding(payload, "UTF-8") - when OPCODE_BINARY - raise(WebSocket::Error, "received binary data, which is not supported") - when OPCODE_CLOSE - close(1005, "", :peer) - return nil - when OPCODE_PING - raise(WebSocket::Error, "received ping, which is not supported") - when OPCODE_PONG - else - raise(WebSocket::Error, "received unknown opcode: %d" % opcode) - end - rescue EOFError - return nil - end - - end - end - - def tcp_socket - return @socket - end - - def host - return @header["host"] - end - - def origin - case @web_socket_version - when "7", "8" - name = "sec-websocket-origin" - else - name = "origin" - end - if @header[name] - return @header[name] - else - raise(WebSocket::Error, "%s header is missing" % name) - end - end - - def location - return "ws://#{self.host}#{@path}" - end - - # Does closing handshake. - def close(code = 1005, reason = "", origin = :self) - if !@closing_started - case @web_socket_version - when "hixie-75", "hixie-76" - write("\xff\x00") - else - if code == 1005 - payload = "" - else - payload = [code].pack("n") + force_encoding(reason.dup(), "ASCII-8BIT") - end - send_frame(OPCODE_CLOSE, payload, false) - end - end - @socket.close() if origin == :peer - @closing_started = true - end - - def close_socket() - @socket.close() - end - - private - - NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a() - - def read_header() - @header = {} - while line = gets() - line = line.chomp() - break if line.empty? - if !(line =~ /\A(\S+): (.*)\z/n) - raise(WebSocket::Error, "invalid request: #{line}") - end - @header[$1] = $2 - @header[$1.downcase()] = $2 - end - if !@header["upgrade"] - raise(WebSocket::Error, "Upgrade header is missing") - end - if !(@header["upgrade"] =~ /\AWebSocket\z/i) - raise(WebSocket::Error, "invalid Upgrade: " + @header["upgrade"]) - end - if !@header["connection"] - raise(WebSocket::Error, "Connection header is missing") - end - if @header["connection"].split(/,/).grep(/\A\s*Upgrade\s*\z/i).empty? - raise(WebSocket::Error, "invalid Connection: " + @header["connection"]) - end - end - - def send_frame(opcode, payload, mask) - payload = force_encoding(payload.dup(), "ASCII-8BIT") - # Setting StringIO's encoding to ASCII-8BIT. - buffer = StringIO.new(force_encoding("", "ASCII-8BIT")) - write_byte(buffer, 0x80 | opcode) - masked_byte = mask ? 0x80 : 0x00 - if payload.bytesize <= 125 - write_byte(buffer, masked_byte | payload.bytesize) - elsif payload.bytesize < 2 ** 16 - write_byte(buffer, masked_byte | 126) - buffer.write([payload.bytesize].pack("n")) - else - write_byte(buffer, masked_byte | 127) - buffer.write([payload.bytesize / (2 ** 32), payload.bytesize % (2 ** 32)].pack("NN")) - end - if mask - mask_key = Array.new(4){ rand(256) } - buffer.write(mask_key.pack("C*")) - payload = apply_mask(payload, mask_key) - end - buffer.write(payload) - write(buffer.string) - end - - def gets(rs = $/) - line = @socket.gets(rs) - $stderr.printf("recv> %p\n", line) if WebSocket.debug - return line - end - - def read(num_bytes) - str = @socket.read(num_bytes) - $stderr.printf("recv> %p\n", str) if WebSocket.debug - if str && str.bytesize == num_bytes - return str - else - raise(EOFError) - end - end - - def write(data) - if WebSocket.debug - data.scan(/\G(.*?(\n|\z))/n) do - $stderr.printf("send> %p\n", $&) if !$&.empty? - end - end - @socket.write(data) - end - - def flush() - @socket.flush() - end - - def write_byte(buffer, byte) - buffer.write([byte].pack("C")) - end - - def security_digest(key) - return Base64.encode64(Digest::SHA1.digest(key + WEB_SOCKET_GUID)).gsub(/\n/, "") - end - - def hixie_76_security_digest(key1, key2, key3) - bytes1 = websocket_key_to_bytes(key1) - bytes2 = websocket_key_to_bytes(key2) - return Digest::MD5.digest(bytes1 + bytes2 + key3) - end - - def apply_mask(payload, mask_key) - orig_bytes = payload.unpack("C*") - new_bytes = [] - orig_bytes.each_with_index() do |b, i| - new_bytes.push(b ^ mask_key[i % 4]) - end - return new_bytes.pack("C*") - end - - def generate_key() - spaces = 1 + rand(12) - max = 0xffffffff / spaces - number = rand(max + 1) - key = (number * spaces).to_s() - (1 + rand(12)).times() do - char = NOISE_CHARS[rand(NOISE_CHARS.size)] - pos = rand(key.size + 1) - key[pos...pos] = char - end - spaces.times() do - pos = 1 + rand(key.size - 1) - key[pos...pos] = " " - end - return key - end - - def generate_key3() - return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N") - end - - def websocket_key_to_bytes(key) - num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size - return [num].pack("N") - end - - def force_encoding(str, encoding) - if str.respond_to?(:force_encoding) - return str.force_encoding(encoding) - else - return str - end - end - - def ssl_handshake(socket) - ssl_context = OpenSSL::SSL::SSLContext.new() - ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context) - ssl_socket.sync_close = true - ssl_socket.connect() - return ssl_socket - end - -end - - -class WebSocketServer - - def initialize(params_or_uri, params = nil) - if params - uri = params_or_uri.is_a?(String) ? URI.parse(params_or_uri) : params_or_uri - params[:port] ||= uri.port - params[:accepted_domains] ||= [uri.host] - else - params = params_or_uri - end - @port = params[:port] || 80 - @accepted_domains = params[:accepted_domains] - if !@accepted_domains - raise(ArgumentError, "params[:accepted_domains] is required") - end - if params[:host] - @tcp_server = TCPServer.open(params[:host], @port) - else - @tcp_server = TCPServer.open(@port) - end - end - - attr_reader(:tcp_server, :port, :accepted_domains) - - def run(&block) - while true - Thread.start(accept()) do |s| - begin - ws = create_web_socket(s) - yield(ws) if ws - rescue => ex - print_backtrace(ex) - ensure - begin - ws.close_socket() if ws - rescue - end - end - end - end - end - - def accept() - return @tcp_server.accept() - end - - - def accepted_origin?(origin) - #domain = origin_to_domain(origin) - #return @accepted_domains.any?(){ |d| File.fnmatch(d, domain) } - - #todo antisnatchor -> we want to accept every origin - true - end - - def origin_to_domain(origin) - if origin == "null" || origin == "file://" # local file - return "null" - else - return URI.parse(origin).host - end - end - - def create_web_socket(socket) - ch = socket.getc() - if ch == ?< - # This is Flash socket policy file request, not an actual Web Socket connection. - send_flash_socket_policy_file(socket) - return nil - else - socket.ungetc(ch) - return WebSocket.new(socket, :server => self) - end - end - - private - - def print_backtrace(ex) - $stderr.printf("%s: %s (%p)\n", ex.backtrace[0], ex.message, ex.class) - for s in ex.backtrace[1..-1] - $stderr.printf(" %s\n", s) - end - end - - # Handles Flash socket policy file request sent when web-socket-js is used: - # http://github.com/gimite/web-socket-js/tree/master - def send_flash_socket_policy_file(socket) - socket.puts('') - socket.puts('') - socket.puts('') - for domain in @accepted_domains - next if domain == "file://" - socket.puts("") - end - socket.puts('') - socket.close() - end - -end - - -if __FILE__ == $0 - Thread.abort_on_exception = true - - if ARGV[0] == "server" && ARGV.size == 3 - - server = WebSocketServer.new( - :accepted_domains => [ARGV[1]], - :port => ARGV[2].to_i()) - puts("Server is running at port %d" % server.port) - server.run() do |ws| - puts("Connection accepted") - puts("Path: #{ws.path}, Origin: #{ws.origin}") - if ws.path == "/" - ws.handshake() - while data = ws.receive() - printf("Received: %p\n", data) - ws.send(data) - printf("Sent: %p\n", data) - end - elsif ws.path == "/badjs" - printf("Received: %s\n", ws.path) - #modules code here inject - - else - ws.handshake("404 Not Found") - end - puts("Connection closed") - end - - elsif ARGV[0] == "client" && ARGV.size == 2 - - client = WebSocket.new(ARGV[1]) - puts("Connected") - Thread.new() do - while data = client.receive() - printf("Received: %p\n", data) - end - end - $stdin.each_line() do |line| - data = line.chomp() - client.send(data) - printf("Sent: %p\n", data) - end - - else - - $stderr.puts("Usage:") - $stderr.puts(" ruby web_socket.rb server ACCEPTED_DOMAIN PORT") - $stderr.puts(" ruby web_socket.rb client ws://HOST:PORT/") - exit(1) - - end -end diff --git a/core/main/network_stack/websocket/websocket.rb b/core/main/network_stack/websocket/websocket.rb index e66487426..134ea4007 100644 --- a/core/main/network_stack/websocket/websocket.rb +++ b/core/main/network_stack/websocket/websocket.rb @@ -13,64 +13,59 @@ # See the License for the specific language governing permissions and # limitations under the License. # -#@todo STOP POLLING module BeEF module Core module Websocket require 'singleton' require 'json' require 'base64' + require 'em-websocket' class Websocket include Singleton include BeEF::Core::Handlers::Modules::Command - # @note obtain dynamic mount points from HttpHookServer - MOUNTS = BeEF::Core::Server.instance.mounts - @@activeSocket= Hash.new #empty at begin + + @@activeSocket= Hash.new @@lastalive= Hash.new @@config = BeEF::Core::Configuration.instance + def initialize port = @@config.get("beef.http.websocket.port") secure = @@config.get("beef.http.websocket.secure") - #todo antisnatchor: start websocket secure if beef.http.websocket.secure == true - server = WebSocketServer.new :accepted_domains => "127.0.0.1", - :port => port - print_info("Started WebSocket server: port [#{port.to_s}], secure [#{secure.to_s}]") - Thread.new { - server.run() do |ws| - begin - print_debug("Path requested #{ws.path} Origins #{ws.origin}") - if ws.path == "/" - ws.handshake() #accept and connect - while true - #command interpretation - message = ws.receive() - messageHash = JSON.parse("#{message}") - #@note messageHash[result] is Base64 encoded - if (messageHash["cookie"]!= nil) - print_info("Browser #{ws.origin} says helo! WebSocket is running") - #insert new connection in activesocket - @@activeSocket["#{messageHash["cookie"]}"] = ws - print_debug("In activesocket we have #{@@activeSocket}") - elsif messageHash["alive"] != nil - hooked_browser = BeEF::Core::Models::HookedBrowser.first(:session => messageHash["alive"]) + sleep 2 # prevent issues when starting at the same time the TunnelingProxy, Thin and Evented WebSockets + EventMachine.run { #todo antisnatchor: add support for WebSocket secure (new object with different config options, then start) + EventMachine::WebSocket.start(:host => "0.0.0.0", :port => port) do |ws| + begin + print_debug "New WebSocket channel open." + ws.onmessage { |msg| + msg_hash = JSON.parse("#{msg}") + #@note messageHash[result] is Base64 encoded + if (msg_hash["cookie"]!= nil) + print_debug("WebSocket - Browser says helo! WebSocket is running") + #insert new connection in activesocket + @@activeSocket["#{msg_hash["cookie"]}"] = ws + print_debug("WebSocket - activeSocket content [#{@@activeSocket}]") + elsif msg_hash["alive"] != nil + hooked_browser = BeEF::Core::Models::HookedBrowser.first(:session => msg_hash["alive"]) + unless hooked_browser.nil? hooked_browser.lastseen = Time.new.to_i hooked_browser.count! hooked_browser.save zombie_commands = BeEF::Core::Models::Command.all(:hooked_browser_id => hooked_browser.id, :instructions_sent => false) - zombie_commands.each{|command| add_command_instructions(command, hooked_browser)} - else - #json recv is a cmd response decode and send all to - #we have to call dynamicreconstructor handler camp must be websocket - #print_debug("Received from WebSocket #{messageHash}") - execute(messageHash) + zombie_commands.each { |command| add_command_instructions(command, hooked_browser) } end + else + #json recv is a cmd response decode and send all to + #we have to call dynamicreconstructor handler camp must be websocket + #print_debug("Received from WebSocket #{messageHash}") + execute(msg_hash) end - end - rescue Exception => e - print_error "Hooked browser from origin #{ws.origin} abruptly disconnected. #{e}" - end - end + } + rescue Exception => e + print_error "WebSocket error: #{e}" + end + end + } } end @@ -99,15 +94,14 @@ module BeEF command_results=Hash.new command_results["data"]=Base64.decode64(data["result"]) command_results["data"].force_encoding('UTF-8') - (print_error "BeEFhook is invalid"; return) if not BeEF::Filters.is_valid_hook_session_id?(data["bh"]) + hooked_browser = data["bh"] + (print_error "BeEFhook is invalid"; return) if not BeEF::Filters.is_valid_hook_session_id?(hooked_browser) (print_error "command_id is invalid"; return) if not BeEF::Filters.is_valid_command_id?(data["cid"]) (print_error "command name is empty"; return) if data["handler"].empty? (print_error "command results are empty"; return) if command_results.empty? - BeEF::Core::Models::Command.save_result(data["bh"], data["cid"], - @@config.get("beef.module.#{data["handler"].gsub("/command/","").gsub(".js","")}.name"), command_results) + BeEF::Core::Models::Command.save_result(hooked_browser, data["cid"], + @@config.get("beef.module.#{data["handler"].gsub("/command/", "").gsub(".js", "")}.name"), command_results) end - - end end end