mirror of
https://github.com/beefproject/beef
synced 2026-06-08 13:15:56 +00:00
TEST: hookerbrowser and browserdetails spec
This commit is contained in:
@@ -413,5 +413,158 @@ RSpec.describe BeEF::Core::Handlers::BrowserDetails do
|
||||
expect(BeEF::Core::Models::BrowserDetails).to receive(:set).with(session_id, 'network.proxy.server', 'proxy.example.com')
|
||||
described_class.new(proxy_data)
|
||||
end
|
||||
|
||||
context 'filter failures (err_msg branches)' do
|
||||
# Full data with optional keys so later filters (e.g. battery, capabilities) don't trigger err_msg
|
||||
let(:full_data) do
|
||||
data.merge('results' => data['results'].merge(
|
||||
'hardware.battery.level' => '50%',
|
||||
'browser.name.reported' => 'Mozilla/5.0',
|
||||
'browser.engine' => 'Gecko',
|
||||
'browser.window.cookies' => 'session=abc',
|
||||
'host.os.name' => 'Windows',
|
||||
'host.os.family' => 'Windows',
|
||||
'host.os.version' => '10',
|
||||
'browser.capabilities.vbscript' => 'yes'
|
||||
))
|
||||
end
|
||||
|
||||
def stub_all_filters_valid_except(except_key = nil)
|
||||
%i[
|
||||
is_valid_hook_session_id? is_valid_browsername? is_valid_browserversion? is_valid_ip?
|
||||
is_valid_browserstring? is_valid_cookies? is_valid_osname? is_valid_hwname?
|
||||
is_valid_date_stamp? is_valid_pagetitle? is_valid_url? is_valid_pagereferrer?
|
||||
is_valid_hostname? is_valid_port? is_valid_browser_plugins? is_valid_system_platform?
|
||||
nums_only? is_valid_yes_no? is_valid_memory? is_valid_gpu? is_valid_cpu? alphanums_only?
|
||||
].each do |m|
|
||||
allow(BeEF::Filters).to receive(m).and_return(except_key == m ? false : true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'calls err_msg when browser name is invalid' do
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
|
||||
stub_all_filters_valid_except(:is_valid_browsername?)
|
||||
allow(BeEF::Core::Models::BrowserDetails).to receive(:set)
|
||||
allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name)
|
||||
zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1')
|
||||
allow(zombie).to receive(:firstseen=)
|
||||
allow(zombie).to receive(:domain=)
|
||||
allow(zombie).to receive(:port=)
|
||||
allow(zombie).to receive(:httpheaders=)
|
||||
allow(zombie).to receive(:httpheaders).and_return('{}')
|
||||
allow(zombie).to receive(:save!)
|
||||
allow(JSON).to receive(:parse).with('{}').and_return({})
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
|
||||
err_msg_calls = []
|
||||
allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last }
|
||||
described_class.new(full_data)
|
||||
expect(err_msg_calls).to include(a_string_matching(/Invalid browser name/))
|
||||
end
|
||||
|
||||
it 'calls err_msg when IP is invalid' do
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
|
||||
stub_all_filters_valid_except(:is_valid_ip?)
|
||||
allow(BeEF::Core::Models::BrowserDetails).to receive(:set)
|
||||
allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox')
|
||||
zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1')
|
||||
allow(zombie).to receive(:firstseen=)
|
||||
allow(zombie).to receive(:domain=)
|
||||
allow(zombie).to receive(:port=)
|
||||
allow(zombie).to receive(:httpheaders=)
|
||||
allow(zombie).to receive(:httpheaders).and_return('{}')
|
||||
allow(zombie).to receive(:save!)
|
||||
allow(JSON).to receive(:parse).with('{}').and_return({})
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
|
||||
err_msg_calls = []
|
||||
allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last }
|
||||
described_class.new(full_data)
|
||||
expect(err_msg_calls).to include(a_string_matching(/Invalid IP address/))
|
||||
end
|
||||
|
||||
it 'calls err_msg when browser version is invalid' do
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
|
||||
stub_all_filters_valid_except(:is_valid_browserversion?)
|
||||
allow(BeEF::Core::Models::BrowserDetails).to receive(:set)
|
||||
allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox')
|
||||
zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1')
|
||||
allow(zombie).to receive(:firstseen=)
|
||||
allow(zombie).to receive(:domain=)
|
||||
allow(zombie).to receive(:port=)
|
||||
allow(zombie).to receive(:httpheaders=)
|
||||
allow(zombie).to receive(:httpheaders).and_return('{}')
|
||||
allow(zombie).to receive(:save!)
|
||||
allow(JSON).to receive(:parse).with('{}').and_return({})
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
|
||||
err_msg_calls = []
|
||||
allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last }
|
||||
described_class.new(full_data)
|
||||
expect(err_msg_calls).to include(a_string_matching(/Invalid browser version/))
|
||||
end
|
||||
|
||||
it 'calls err_msg when browser.name.reported is invalid' do
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
|
||||
allow(BeEF::Filters).to receive(:is_valid_browsername?).and_return(true)
|
||||
allow(BeEF::Filters).to receive(:is_valid_browserversion?).and_return(true)
|
||||
allow(BeEF::Filters).to receive(:is_valid_ip?).and_return(true)
|
||||
allow(BeEF::Filters).to receive(:is_valid_browserstring?).and_return(false)
|
||||
stub_all_filters_valid_except(nil)
|
||||
allow(BeEF::Filters).to receive(:is_valid_browserstring?).and_return(false)
|
||||
allow(BeEF::Core::Models::BrowserDetails).to receive(:set)
|
||||
allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox')
|
||||
zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1')
|
||||
allow(zombie).to receive(:firstseen=)
|
||||
allow(zombie).to receive(:domain=)
|
||||
allow(zombie).to receive(:port=)
|
||||
allow(zombie).to receive(:httpheaders=)
|
||||
allow(zombie).to receive(:httpheaders).and_return('{}')
|
||||
allow(zombie).to receive(:save!)
|
||||
allow(JSON).to receive(:parse).with('{}').and_return({})
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
|
||||
err_msg_calls = []
|
||||
allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last }
|
||||
described_class.new(full_data)
|
||||
expect(err_msg_calls).to include(a_string_matching(/browser\.name\.reported/))
|
||||
end
|
||||
|
||||
it 'calls err_msg when cookies are invalid' do
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
|
||||
stub_all_filters_valid_except(:is_valid_cookies?)
|
||||
allow(BeEF::Core::Models::BrowserDetails).to receive(:set)
|
||||
allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox')
|
||||
zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1')
|
||||
allow(zombie).to receive(:firstseen=)
|
||||
allow(zombie).to receive(:domain=)
|
||||
allow(zombie).to receive(:port=)
|
||||
allow(zombie).to receive(:httpheaders=)
|
||||
allow(zombie).to receive(:httpheaders).and_return('{}')
|
||||
allow(zombie).to receive(:save!)
|
||||
allow(JSON).to receive(:parse).with('{}').and_return({})
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
|
||||
err_msg_calls = []
|
||||
allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last }
|
||||
described_class.new(full_data)
|
||||
expect(err_msg_calls).to include(a_string_matching(/Invalid cookies/))
|
||||
end
|
||||
|
||||
it 'calls err_msg when host.os.name is invalid' do
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
|
||||
stub_all_filters_valid_except(:is_valid_osname?)
|
||||
allow(BeEF::Core::Models::BrowserDetails).to receive(:set)
|
||||
allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox')
|
||||
zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1')
|
||||
allow(zombie).to receive(:firstseen=)
|
||||
allow(zombie).to receive(:domain=)
|
||||
allow(zombie).to receive(:port=)
|
||||
allow(zombie).to receive(:httpheaders=)
|
||||
allow(zombie).to receive(:httpheaders).and_return('{}')
|
||||
allow(zombie).to receive(:save!)
|
||||
allow(JSON).to receive(:parse).with('{}').and_return({})
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
|
||||
err_msg_calls = []
|
||||
allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last }
|
||||
described_class.new(full_data)
|
||||
expect(err_msg_calls).to include(a_string_matching(/operating system name/))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,6 +10,131 @@ RSpec.describe BeEF::Core::Handlers::HookedBrowsers do
|
||||
# .new returns Sinatra::Wrapper; use allocate to get the real class instance for unit testing
|
||||
let(:handler) { described_class.allocate }
|
||||
|
||||
describe "GET '/'" do
|
||||
let(:config) { BeEF::Core::Configuration.instance }
|
||||
# Use a host permitted by Router's host_authorization (.localhost, .test, or config public host)
|
||||
let(:rack_env) { { 'REMOTE_ADDR' => '192.168.1.1', 'HTTP_HOST' => 'localhost' } }
|
||||
|
||||
def app
|
||||
described_class
|
||||
end
|
||||
|
||||
before do
|
||||
allow(BeEF::Core::Logger.instance).to receive(:register)
|
||||
allow(config).to receive(:get).and_call_original
|
||||
allow(config).to receive(:get).with('beef.http.restful_api.allow_cors').and_return(false)
|
||||
end
|
||||
|
||||
it 'returns 404 when permitted_hooking_subnet is nil' do
|
||||
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(nil)
|
||||
get '/', {}, rack_env
|
||||
expect(last_response.status).to eq(404)
|
||||
end
|
||||
|
||||
it 'returns 404 when permitted_hooking_subnet is empty' do
|
||||
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return([])
|
||||
get '/', {}, rack_env
|
||||
expect(last_response.status).to eq(404)
|
||||
end
|
||||
|
||||
it 'returns 404 when client IP is not in permitted subnet' do
|
||||
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['10.0.0.0/8'])
|
||||
get '/', {}, rack_env
|
||||
expect(last_response.status).to eq(404)
|
||||
end
|
||||
|
||||
it 'returns 404 when client IP is in excluded_hooking_subnet' do
|
||||
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['0.0.0.0/0'])
|
||||
allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return(['192.168.1.0/24'])
|
||||
get '/', {}, rack_env
|
||||
expect(last_response.status).to eq(404)
|
||||
end
|
||||
|
||||
it 'returns 200 and hook body when IP permitted, not excluded, no session (new browser)' do
|
||||
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['192.168.0.0/16'])
|
||||
allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return([])
|
||||
allow(config).to receive(:get).with('beef.http.hook_session_name').and_return('beefhook')
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
|
||||
allow(BeEF::Filters).to receive(:is_valid_hostname?).with('localhost').and_return(true)
|
||||
allow(config).to receive(:get).with('beef.http.websocket.enable').and_return(false)
|
||||
allow_any_instance_of(described_class).to receive(:confirm_browser_user_agent).and_return(false)
|
||||
allow_any_instance_of(described_class).to receive(:legacy_build_beefjs!).with('localhost')
|
||||
get '/', {}, rack_env
|
||||
expect(last_response.status).to eq(200)
|
||||
expect(last_response.headers['Content-Type']).to include('javascript')
|
||||
end
|
||||
|
||||
it 'uses multi_stage_beefjs when websocket disabled and confirm_browser_user_agent true' do
|
||||
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['0.0.0.0/0'])
|
||||
allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return([])
|
||||
allow(config).to receive(:get).with('beef.http.hook_session_name').and_return('beefhook')
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
|
||||
allow(BeEF::Filters).to receive(:is_valid_hostname?).with('localhost').and_return(true)
|
||||
allow(config).to receive(:get).with('beef.http.websocket.enable').and_return(false)
|
||||
allow_any_instance_of(described_class).to receive(:confirm_browser_user_agent).and_return(true)
|
||||
allow_any_instance_of(described_class).to receive(:multi_stage_beefjs!).with('localhost')
|
||||
get '/', {}, { 'REMOTE_ADDR' => '127.0.0.1', 'HTTP_HOST' => 'localhost' }
|
||||
expect(last_response.status).to eq(200)
|
||||
end
|
||||
|
||||
it 'returns early with empty body when hostname is invalid' do
|
||||
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['0.0.0.0/0'])
|
||||
allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return([])
|
||||
allow(config).to receive(:get).with('beef.http.hook_session_name').and_return('beefhook')
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
|
||||
# Use permitted host so request reaches handler; stub hostname validation to fail
|
||||
allow(BeEF::Filters).to receive(:is_valid_hostname?).with('localhost').and_return(false)
|
||||
get '/', {}, { 'REMOTE_ADDR' => '127.0.0.1', 'HTTP_HOST' => 'localhost' }
|
||||
expect(last_response.status).to eq(200)
|
||||
expect(last_response.body).to eq('')
|
||||
end
|
||||
|
||||
context 'when session exists (existing browser path)' do
|
||||
let(:hooked_browser) do
|
||||
double('HookedBrowser',
|
||||
id: 1,
|
||||
ip: '192.168.1.1',
|
||||
lastseen: Time.new.to_i - 120,
|
||||
session: 'existing_session',
|
||||
count!: nil,
|
||||
save!: true).tap do |d|
|
||||
allow(d).to receive(:lastseen=)
|
||||
allow(d).to receive(:ip=)
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['192.168.0.0/16'])
|
||||
allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return([])
|
||||
allow(config).to receive(:get).with('beef.http.hook_session_name').and_return('beefhook')
|
||||
allow(config).to receive(:get).with('beef.http.allow_reverse_proxy').and_return(false)
|
||||
relation = double('Relation', first: hooked_browser)
|
||||
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).with(session: 'existing_session').and_return(relation)
|
||||
allow(BeEF::Core::Models::Command).to receive(:where).with(hooked_browser_id: 1, instructions_sent: false).and_return([])
|
||||
allow(BeEF::Core::Models::Execution).to receive(:where).with(is_sent: false, session_id: 'existing_session').and_return([])
|
||||
allow(BeEF::API::Registrar.instance).to receive(:fire)
|
||||
end
|
||||
|
||||
it 'returns 200 and updates lastseen' do
|
||||
get '/', { 'beefhook' => 'existing_session' }, rack_env
|
||||
expect(last_response.status).to eq(200)
|
||||
expect(hooked_browser).to have_received(:save!)
|
||||
end
|
||||
|
||||
it 'logs zombie comeback when lastseen was more than 60 seconds ago' do
|
||||
get '/', { 'beefhook' => 'existing_session' }, rack_env
|
||||
expect(BeEF::Core::Logger.instance).to have_received(:register).with('Zombie', /appears to have come back online/, '1')
|
||||
end
|
||||
|
||||
it 'calls add_command_instructions for each pending command' do
|
||||
command = double('Command', id: 1, command_module_id: 1)
|
||||
allow(BeEF::Core::Models::Command).to receive(:where).with(hooked_browser_id: 1, instructions_sent: false).and_return([command])
|
||||
expect_any_instance_of(described_class).to receive(:add_command_instructions).with(command, hooked_browser)
|
||||
get '/', { 'beefhook' => 'existing_session' }, rack_env
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#confirm_browser_user_agent' do
|
||||
it 'returns true when user_agent suffix matches a legacy UA string' do
|
||||
allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return(['IE 8.0'])
|
||||
|
||||
Reference in New Issue
Block a user