diff --git a/.github/workflows/github_actions.yml b/.github/workflows/github_actions.yml index 5182fe894..880302da3 100644 --- a/.github/workflows/github_actions.yml +++ b/.github/workflows/github_actions.yml @@ -52,4 +52,4 @@ jobs: - name: 'BrowserStackLocal Stop' # Terminating the BrowserStackLocal tunnel connection uses: browserstack/github-actions/setup-local@master with: - local-testing: stop + local-testing: stop \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4322aed64..64282a1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,6 @@ node_modules/ # Generated files out/ doc/rdoc/ + +# Secrets for testing github actions locally +.secrets \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 573033f94..2213554ee 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -22,8 +22,8 @@ GEM addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) ansi (1.5.0) - archive-zip (0.12.0) - io-like (~> 0.3.0) + archive-zip (0.13.1) + io-like (~> 0.4.0) ast (2.4.3) async (1.32.1) console (~> 1.10) @@ -49,7 +49,7 @@ GEM coderay (1.1.3) concurrent-ruby (1.3.5) connection_pool (2.5.3) - console (1.29.2) + console (1.33.0) fiber-annotation fiber-local (~> 1.1) json @@ -71,7 +71,7 @@ GEM fiber-annotation (0.2.0) fiber-local (1.1.0) fiber-storage - fiber-storage (1.0.0) + fiber-storage (1.0.1) geckodriver-helper (0.24.0) archive-zip (~> 0.7) http-accept (1.7.0) @@ -80,9 +80,9 @@ GEM http_parser.rb (0.8.0) i18n (1.14.7) concurrent-ruby (~> 1.0) - io-console (0.8.0) - io-endpoint (0.15.1) - io-like (0.3.1) + io-console (0.8.1) + io-endpoint (0.15.2) + io-like (0.4.0) irb (1.15.2) pp (>= 0.6.0) rdoc (>= 4.0.0) @@ -91,20 +91,20 @@ GEM language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) - matrix (0.4.2) + matrix (0.4.3) maxmind-db (1.3.2) method_source (1.1.0) mime-types (3.7.0) logger mime-types-data (~> 3.2025, >= 3.2025.0507) - mime-types-data (3.2025.0520) + mime-types-data (3.2025.0819) mini_mime (1.1.5) minitest (5.25.5) mojo_magick (0.6.8) msfrpc-client (1.1.2) msgpack (~> 1) - msgpack (1.7.5) - mustermann (3.0.3) + msgpack (1.8.0) + mustermann (3.0.4) ruby2_keywords (~> 0.0.1) net-protocol (0.2.2) timeout @@ -112,8 +112,22 @@ GEM net-protocol netrc (0.11.0) nio4r (2.7.4) + nokogiri (1.18.9-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.9-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.9-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.9-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.9-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.9-x86_64-darwin) + racc (~> 1.4) nokogiri (1.18.9-x86_64-linux-gnu) racc (~> 1.4) + nokogiri (1.18.9-x86_64-linux-musl) + racc (~> 1.4) otr-activerecord (2.5.0) activerecord (>= 6.0, < 8.1) parallel (1.27.0) @@ -135,7 +149,7 @@ GEM psych (5.2.6) date stringio - public_suffix (6.0.1) + public_suffix (6.0.2) qr4r (0.6.2) mojo_magick (~> 0.6.5) rqrcode_core (~> 1.0) @@ -152,7 +166,7 @@ GEM erb psych (>= 4.0.0) regexp_parser (2.11.2) - reline (0.6.0) + reline (0.6.2) io-console (~> 0.5) rest-client (2.1.0) http-accept (>= 1.7.0, < 2.0) @@ -161,12 +175,12 @@ GEM netrc (~> 0.8) rexml (3.4.1) rqrcode_core (1.2.0) - rr (3.1.1) + rr (3.1.2) rspec (3.13.1) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.4) + rspec-core (3.13.5) rspec-support (~> 3.13.0) rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) @@ -174,7 +188,7 @@ GEM rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-support (3.13.4) + rspec-support (3.13.5) rubocop (1.80.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -208,12 +222,19 @@ GEM rack-protection (= 3.2.0) tilt (~> 2.0) slack-notifier (2.4.0) + sqlite3 (2.7.3-aarch64-linux-gnu) + sqlite3 (2.7.3-aarch64-linux-musl) + sqlite3 (2.7.3-arm-linux-gnu) + sqlite3 (2.7.3-arm-linux-musl) + sqlite3 (2.7.3-arm64-darwin) + sqlite3 (2.7.3-x86_64-darwin) sqlite3 (2.7.3-x86_64-linux-gnu) + sqlite3 (2.7.3-x86_64-linux-musl) stringio (3.1.7) sync (0.5.0) term-ansicolor (1.11.2) tins (~> 1.0) - test-unit (3.6.7) + test-unit (3.7.0) power_assert test-unit-context (0.5.1) test-unit (>= 2.4.0) @@ -234,10 +255,10 @@ GEM daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) - tilt (2.5.0) + tilt (2.6.1) timeout (0.4.3) timers (4.4.0) - tins (1.37.1) + tins (1.42.0) bigdecimal sync tzinfo (2.0.6) @@ -258,7 +279,14 @@ GEM nokogiri (~> 1.8) PLATFORMS - x86_64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES activerecord (~> 7.2) @@ -305,4 +333,4 @@ DEPENDENCIES xmlrpc (~> 0.3.3) BUNDLED WITH - 2.4.10 + 2.5.18 diff --git a/spec/beef/core/main/autorun_engine/autorun_engine_spec.rb b/spec/beef/core/main/autorun_engine/autorun_engine_spec.rb index aef99a958..02258042d 100644 --- a/spec/beef/core/main/autorun_engine/autorun_engine_spec.rb +++ b/spec/beef/core/main/autorun_engine/autorun_engine_spec.rb @@ -18,7 +18,10 @@ RSpec.describe 'AutoRunEngine Test', run_on_browserstack: true do print_info 'Loading database' db_file = @config.get('beef.database.file') print_info 'Resetting the database for BeEF.' - File.delete(db_file) if File.exist?(db_file) + + if ENV['RESET_DB'] + File.delete(db_file) if File.exist?(db_file) + end @config.set('beef.credentials.user', 'beef') @config.set('beef.credentials.passwd', 'beef') @@ -49,9 +52,13 @@ RSpec.describe 'AutoRunEngine Test', run_on_browserstack: true do OTR::ActiveRecord.establish_connection! end ActiveRecord::Migrator.migrations_paths = [File.join('core', 'main', 'ar-migrations')] - context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths) - ActiveRecord::Migrator.new(:up, context.migrations, context.schema_migration, context.internal_metadata).migrate if context.needs_migration? - + MUTEX.synchronize do + context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths) + if context.needs_migration? + ActiveRecord::Migrator.new(:up, context.migrations, context.schema_migration, context.internal_metadata).migrate + end + end + BeEF::Core::Migration.instance.update_db! # add AutoRunEngine rule @@ -63,16 +70,17 @@ RSpec.describe 'AutoRunEngine Test', run_on_browserstack: true do # Spawn HTTP Server print_info 'Starting HTTP Hook Server' http_hook_server = BeEF::Core::Server.instance - http_hook_server.prepare # Generate a token for the server to respond with @token = BeEF::Core::Crypto.api_token + # ***** IMPORTANT: close any and all AR/OTR connections before forking ***** + disconnect_all_active_record! + # Initiate server start-up - @pids = fork do - BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server) - end @pid = fork do + http_hook_server.prepare + BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server) http_hook_server.start end @@ -101,6 +109,7 @@ RSpec.describe 'AutoRunEngine Test', run_on_browserstack: true do after(:all) do server_teardown(@driver, @pid, @pids) + disconnect_all_active_record! end it 'AutoRunEngine is working' do diff --git a/spec/beef/core/main/handlers/browser_details_handler_spec.rb b/spec/beef/core/main/handlers/browser_details_handler_spec.rb index 4c54f2d37..c0c5fe9d7 100644 --- a/spec/beef/core/main/handlers/browser_details_handler_spec.rb +++ b/spec/beef/core/main/handlers/browser_details_handler_spec.rb @@ -16,7 +16,11 @@ RSpec.describe 'Browser Details Handler', run_on_browserstack: true do @config = BeEF::Core::Configuration.instance db_file = @config.get('beef.database.file') print_info 'Resetting the database for BeEF.' - File.delete(db_file) if File.exist?(db_file) + + if ENV['RESET_DB'] + File.delete(db_file) if File.exist?(db_file) + end + @config.set('beef.credentials.user', 'beef') @config.set('beef.credentials.passwd', 'beef') @username = @config.get('beef.credentials.user') @@ -50,24 +54,28 @@ RSpec.describe 'Browser Details Handler', run_on_browserstack: true do # Migrate (if required) ActiveRecord::Migrator.migrations_paths = [File.join('core', 'main', 'ar-migrations')] - context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths) - ActiveRecord::Migrator.new(:up, context.migrations, context.schema_migration, context.internal_metadata).migrate if context.needs_migration? - + MUTEX.synchronize do + context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths) + if context.needs_migration? + ActiveRecord::Migrator.new(:up, context.migrations, context.schema_migration, context.internal_metadata).migrate + end + end BeEF::Core::Migration.instance.update_db! # Spawn HTTP Server print_info 'Starting HTTP Hook Server' http_hook_server = BeEF::Core::Server.instance - http_hook_server.prepare # Generate a token for the server to respond with @token = BeEF::Core::Crypto.api_token + # ***** IMPORTANT: close any and all AR/OTR connections before forking ***** + disconnect_all_active_record! + # Initiate server start-up - @pids = fork do - BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server) - end @pid = fork do + http_hook_server.prepare + BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server) http_hook_server.start end @@ -97,6 +105,7 @@ RSpec.describe 'Browser Details Handler', run_on_browserstack: true do after(:all) do server_teardown(@driver, @pid, @pids) + disconnect_all_active_record! end it 'can successfully hook a browser' do diff --git a/spec/beef/core/main/network_stack/handlers/dynamic_reconstruction_spec.rb b/spec/beef/core/main/network_stack/handlers/dynamic_reconstruction_spec.rb index 458bc3d70..3cfd652fe 100644 --- a/spec/beef/core/main/network_stack/handlers/dynamic_reconstruction_spec.rb +++ b/spec/beef/core/main/network_stack/handlers/dynamic_reconstruction_spec.rb @@ -12,6 +12,10 @@ RSpec.describe 'BeEF Dynamic Reconsturction' do @server = Thin::Server.new('127.0.0.1', @port.to_s, @rackApp) trap("INT") { @server.stop } trap("TERM") { @server.stop } + + # ***** IMPORTANT: close any and all AR/OTR connections before forking ***** + disconnect_all_active_record! + @pid = fork do @server.start! end diff --git a/spec/beef/core/main/network_stack/handlers/redirector_spec.rb b/spec/beef/core/main/network_stack/handlers/redirector_spec.rb index 8333ab459..e17293127 100644 --- a/spec/beef/core/main/network_stack/handlers/redirector_spec.rb +++ b/spec/beef/core/main/network_stack/handlers/redirector_spec.rb @@ -12,6 +12,11 @@ RSpec.describe 'BeEF Redirector' do @server = Thin::Server.new('127.0.0.1', @port.to_s, @rackApp) trap("INT") { @server.stop } trap("TERM") { @server.stop } + + + # ***** IMPORTANT: close any and all AR/OTR connections before forking ***** + disconnect_all_active_record! + @pid = fork do @server.start! end diff --git a/spec/beef/extensions/websocket_hooked_browser_spec.rb b/spec/beef/extensions/websocket_hooked_browser_spec.rb index 6acf55641..34156645e 100644 --- a/spec/beef/extensions/websocket_hooked_browser_spec.rb +++ b/spec/beef/extensions/websocket_hooked_browser_spec.rb @@ -18,7 +18,11 @@ RSpec.describe 'Browser hooking with Websockets', run_on_browserstack: true do print_info 'Loading database' db_file = @config.get('beef.database.file') print_info 'Resetting the database for BeEF.' - File.delete(db_file) if File.exist?(db_file) + + if ENV['RESET_DB'] + File.delete(db_file) if File.exist?(db_file) + end + @config.set('beef.credentials.user', 'beef') @config.set('beef.credentials.passwd', 'beef') @config.set('beef.http.websocket.secure', false) @@ -49,21 +53,28 @@ RSpec.describe 'Browser hooking with Websockets', run_on_browserstack: true do end ActiveRecord::Migrator.migrations_paths = [File.join('core', 'main', 'ar-migrations')] - context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths) - ActiveRecord::Migrator.new(:up, context.migrations, context.schema_migration, context.internal_metadata).migrate if context.needs_migration? + MUTEX.synchronize do + context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths) + if context.needs_migration? + ActiveRecord::Migrator.new(:up, context.migrations, context.schema_migration, context.internal_metadata).migrate + end + end BeEF::Core::Migration.instance.update_db! # Spawn HTTP Server print_info 'Starting HTTP Hook Server' http_hook_server = BeEF::Core::Server.instance - http_hook_server.prepare # Generate a token for the server to respond with @token = BeEF::Core::Crypto.api_token + + + # ***** IMPORTANT: close any and all AR/OTR connections before forking ***** + disconnect_all_active_record! + # Initiate server start-up - @pids = fork do - BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server) - end @pid = fork do + http_hook_server.prepare + BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server) http_hook_server.start end @@ -86,12 +97,13 @@ RSpec.describe 'Browser hooking with Websockets', run_on_browserstack: true do sleep 1 until wait.until { @driver.execute_script('return window.beef.session.get_hook_session_id().length') > 0 } - @session = @driver.execute_script('return window.beef.session.get_hook_session_id().length') + @session = @driver.execute_script('return window.beef.session.get_hook_session_id()') end end after(:all) do server_teardown(@driver, @pid, @pids) + disconnect_all_active_record! end it 'confirms a websocket server has been started' do diff --git a/spec/beef/modules/debug/test_beef_debugs_spec.rb b/spec/beef/modules/debug/test_beef_debugs_spec.rb index 1bc89d813..075d13490 100644 --- a/spec/beef/modules/debug/test_beef_debugs_spec.rb +++ b/spec/beef/modules/debug/test_beef_debugs_spec.rb @@ -14,11 +14,16 @@ RSpec.describe 'BeEF Debug Command Modules:', run_on_browserstack: true do before(:all) do # Grab config and set creds in variables for ease of access @config = BeEF::Core::Configuration.instance + @pids = [] # ensure defined for teardown consistency # Grab DB file and regenerate if requested print_info 'Loading database' db_file = @config.get('beef.database.file') print_info 'Resetting the database for BeEF.' - File.delete(db_file) if File.exist?(db_file) + + if ENV['RESET_DB'] + File.delete(db_file) if File.exist?(db_file) + end + @username = @config.get('beef.credentials.user') @password = @config.get('beef.credentials.passwd') @@ -47,24 +52,29 @@ RSpec.describe 'BeEF Debug Command Modules:', run_on_browserstack: true do end ActiveRecord::Migrator.migrations_paths = [File.join('core', 'main', 'ar-migrations')] - context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths) - ActiveRecord::Migrator.new(:up, context.migrations, context.schema_migration, context.internal_metadata).migrate if context.needs_migration? + MUTEX.synchronize do + context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths) + if context.needs_migration? + ActiveRecord::Migrator.new(:up, context.migrations, context.schema_migration, context.internal_metadata).migrate + end + end BeEF::Core::Migration.instance.update_db! # Spawn HTTP Server print_info 'Starting HTTP Hook Server' http_hook_server = BeEF::Core::Server.instance - http_hook_server.prepare # Generate a token for the server to respond with @token = BeEF::Core::Crypto.api_token + # ***** IMPORTANT: close any and all AR/OTR connections before forking ***** + disconnect_all_active_record! + # Initiate server start-up - @pids = fork do - BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server) - end @pid = fork do + http_hook_server.prepare + BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server) http_hook_server.start end @@ -117,6 +127,7 @@ RSpec.describe 'BeEF Debug Command Modules:', run_on_browserstack: true do after(:all) do server_teardown(@driver, @pid, @pids) + disconnect_all_active_record! end it 'The Test_beef.debug() command module successfully executes' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4eb5c992d..aa23cbfbb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,6 +25,8 @@ require 'selenium-webdriver' require 'browserstack/local' require 'byebug' +MUTEX ||= Mutex.new + # Require supports Dir['spec/support/*.rb'].each do |f| require f @@ -67,6 +69,61 @@ if context.needs_migration? ActiveRecord::Migrator.new(:up, context.migrations, context.schema_migration, context.internal_metadata).migrate end +# ------------------------------------------------------------------- +# Console logger shims +# Some extensions may call Console.level= or BeEF::Core::Console.level= +# Ensure both are safe. +# ------------------------------------------------------------------- +module BeEF + module Core + module Console + class << self + attr_accessor :logger + def level=(val) + (self.logger ||= Logger.new($stdout)).level = val + end + def level + (self.logger ||= Logger.new($stdout)).level + end + # Proxy common logger methods if called directly (info, warn, error, etc.) + def method_missing(m, *args, &blk) + lg = (self.logger ||= Logger.new($stdout)) + return lg.public_send(m, *args, &blk) if lg.respond_to?(m) + super + end + def respond_to_missing?(m, include_priv = false) + (self.logger ||= Logger.new($stdout)).respond_to?(m, include_priv) || super + end + end + end + end +end +BeEF::Core::Console.logger ||= Logger.new($stdout) + +# Some code may reference a top-level ::Console constant (not namespaced) +unless defined?(::Console) && ::Console.respond_to?(:level=) + module ::Console + class << self + attr_accessor :logger + def level=(val) + (self.logger ||= Logger.new($stdout)).level = val + end + def level + (self.logger ||= Logger.new($stdout)).level + end + # Proxy to logger for typical logging calls + def method_missing(m, *args, &blk) + lg = (self.logger ||= Logger.new($stdout)) + return lg.public_send(m, *args, &blk) if lg.respond_to?(m) + super + end + def respond_to_missing?(m, include_priv = false) + (self.logger ||= Logger.new($stdout)).respond_to?(m, include_priv) || super + end + end + end +end + RSpec.configure do |config| config.disable_monkey_patching! config.bisect_runner = :shell @@ -85,17 +142,25 @@ RSpec.configure do |config| end def server_teardown(webdriver, server_pid, server_pids) - webdriver.quit - rescue StandardError => e - print_info "Exception: #{e}" - print_info "Exception Class: #{e.class}" - print_info "Exception Message: #{e.message}" - print_info "Exception Stack Trace: #{e.backtrace}" - exit 0 - ensure - print_info 'Shutting down server' - Process.kill('KILL', server_pid) - Process.kill('KILL', server_pids) + begin + webdriver&.quit + rescue => e + warn "[server_teardown] webdriver quit failed: #{e.class}: #{e.message}" + end + + begin + Process.kill('KILL', server_pid) if server_pid + rescue => e + warn "[server_teardown] kill failed: #{e.class}: #{e.message}" + end + + Array(server_pids).each do |pid| + begin + Process.kill('KILL', pid) if pid + rescue + # ignore + end + end end ######################################## @@ -139,6 +204,24 @@ require 'socket' BeEF::Modules.load if @config.get('beef.module').nil? end + # --- HARD fork-safety: disconnect every pool/adapter we can find --- + def disconnect_all_active_record! + # print_info "Entering disconnect_all_active_record!" + if defined?(ActiveRecord::Base) + # print_info "Disconnecting ActiveRecord connections" + handler = ActiveRecord::Base.connection_handler + if handler.respond_to?(:connection_pool_list) + # print_info "Using connection_pool_list" + handler.connection_pool_list.each { |pool| pool.disconnect! } + elsif handler.respond_to?(:connection_pools) + # print_info "Using connection_pools" + handler.connection_pools.each_value { |pool| pool.disconnect! } + end + else + print_info "ActiveRecord::Base not defined" + end + end + def start_beef_server configure_beef @port = @config.get('beef.http.port') @@ -158,6 +241,9 @@ require 'socket' File.delete(db_file) if File.exist?(db_file) end + # ***** IMPORTANT: close any and all AR/OTR connections before forking ***** + disconnect_all_active_record! + # Load up DB and migrate if necessary ActiveRecord::Base.logger = nil OTR::ActiveRecord.configure_from_hash!(adapter:'sqlite3', database: db_file) @@ -236,4 +322,4 @@ require 'socket' pid = nil end -end +end \ No newline at end of file