Merge pull request #3386 from beefproject/red/dev

Fix forked SQLite connection handling to improve test accuracy (#3249)
This commit is contained in:
zinduolis
2025-09-07 19:33:19 +10:00
committed by GitHub
10 changed files with 232 additions and 65 deletions

View File

@@ -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

3
.gitignore vendored
View File

@@ -131,3 +131,6 @@ node_modules/
# Generated files
out/
doc/rdoc/
# Secrets for testing github actions locally
.secrets

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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