TIKTOUK leak: rotate your SMTP plugin credentials

A leaked attacker control panel enumerates roughly 50,000 server-side credentials across 37,000 WordPress domains, with credential-recovery routines pointed specifically at WP Mail SMTP, Easy WP SMTP, and FluentSMTP. The toolkit, labelled TIKTOUK and reported on 2 October by cyberpress, does not break any encryption. It uses whatever decryption material it can grep off the filesystem (a leaked wp-config.php, a database dump, a stray debug.log) against each plugin’s own decrypt routine. The researchers’ own controlled tests “did not demonstrate successful exploitation of either vulnerability” cited in the toolkit, so the panel enumerates opportunity rather than confirmed theft. Act anyway: if any of five canonical exposure files has ever been world-readable on your host, rotate your SMTP credentials tonight.

Why the toolkit works

The three plugins encrypt their SMTP credentials at rest, and they do it differently. That matters because it determines which leaked file compromises which plugin.

Plugin Cipher Key constant Default key location
WP Mail SMTP libsodium secretbox WPMS_CRYPTO_KEY wp_options (next to ciphertext)
Easy WP SMTP libsodium secretbox EASY_WP_SMTP_CRYPTO_KEY wp_options (next to ciphertext)
FluentSMTP OpenSSL aes-256-ctr FLUENTMAIL_ENCRYPT_KEY → LOGGED_IN_KEY wp-config.php (WordPress salts)

WP Mail SMTP encrypts credentials with libsodium (sodium_crypto_secretbox), using a 32-byte key that the plugin generates on first use and stores in wp_options under the key wp_mail_smtp_mail_key. The key can be moved out of the database by defining WPMS_CRYPTO_KEY in wp-config.php, but the default install keeps it next to the ciphertext.

Easy WP SMTP uses the same shape: libsodium (sodium_crypto_secretbox), with a key stored in wp_options as easy_wp_smtp_mail_key or lifted into wp-config.php via the EASY_WP_SMTP_CRYPTO_KEY constant. Default install: key and ciphertext share a database row set.

FluentSMTP is the odd one out. It encrypts with OpenSSL aes-256-ctr. The key is FLUENTMAIL_ENCRYPT_KEY if defined, otherwise WordPress’s LOGGED_IN_KEY salt from wp-config.php. The salt is FLUENTMAIL_ENCRYPT_SALT or, as fallback, LOGGED_IN_SALT. Default install: key material lives in wp-config.php, ciphertext lives in the database.

The practical blast radius follows from that split:

  • A leaked database dump alone (backup.sql, a dumped options array in debug.log) decrypts WP Mail SMTP and Easy WP SMTP, because both the ciphertext and the key sit in wp_options. It does not by itself decrypt FluentSMTP unless the attacker also has LOGGED_IN_KEY.
  • A leaked wp-config.php alone (wp-config.php.bak, .env that re-exports WordPress salts) decrypts FluentSMTP via the LOGGED_IN_KEY fallback. It does not decrypt WP Mail SMTP or Easy WP SMTP unless an operator has moved their crypto keys into wp-config.php via the *_CRYPTO_KEY constants.
  • Both files leaked together decrypt everything.

The toolkit’s recovery component (named wp2s_crack.py in cyberpress’s analysis) greps for all five canonical exposure files regardless, because the attacker who finds one often finds the next: wp-config.php.bak (and its editor cousins wp-config.php~, wp-config.old, wp-config.php.save), .env files that re-export WordPress salts, .git/config on a webroot-exposed repository, backup.sql dumps left inside the webroot, and wp-content/debug.log from a session where someone dumped the options array to debug a plugin.

The toolkit carries a second, no-file-exposure path: nested REST batch requests that chain author_exclude with UNION ALL SELECT expressions to pull option values out of wp_options directly, targeting CVE-2026-60137 (SQL injection via author__not_in) and CVE-2026-63030 (REST batch-route confusion). cyberpress cites these as patched in WordPress 7.0.2, 6.9.5, and 6.8.6. Any 7.1.x install is already covered: the fixes landed before 7.1 shipped. The researchers “did not demonstrate successful exploitation of either vulnerability” in their controlled testing, which bounds how much of the 50,000-credential figure represents reachable sites and how much represents only enumerated attempts.

Rotate per plugin

Each subsection follows the same shape: find the ciphertext, rotate at the provider, re-encrypt in WordPress, audit outbound mail. The ciphertext locations below reflect current trunk at the time of writing; the exact subkey holding the encrypted password has moved across major versions in each plugin, so confirm against your installed version at plugins.svn.wordpress.org/<slug>/trunk/ before scripting anything.

WP Mail SMTP

WP Mail SMTP serialises its settings into a single row in wp_options under the option key wp_mail_smtp. To see what the plugin holds today, run wp option get wp_mail_smtp --format=json | jq; the mailer-specific credential field (pass under the smtp group for the SMTP mailer, api_key for the API-based mailers, client_secret for OAuth providers) carries the ciphertext.

  1. Rotate at the provider first. For SMTP, generate a new password and revoke the old. For an API mailer (SendGrid, Mailgun, Postmark, SES), issue a new API key and delete the compromised one. The old secret stops working before the plugin forgets about it.
  2. In WordPress admin, open WP Mail SMTP → Settings, paste the new credential, save. The plugin re-encrypts under the current wp_mail_smtp_mail_key and writes the new ciphertext back to wp_options.
  3. Confirm. wp option get wp_mail_smtp --format=json | jq '.mail.mailer' should show the mailer still routing where it was before, and the ciphertext in the credential field should differ from the pre-rotation value.
  4. Audit. The provider dashboard’s sent-message record for the last 30 days is the universal audit surface. WP Mail SMTP’s own Email Log (a Pro feature) is the fastest cross-check when it is enabled; without it, go straight to the provider.

Easy WP SMTP

Easy WP SMTP stores its settings under easy_wp_smtp in wp_options (the pre-2.0 swpsmtp_options key is gone from current trunk). The password field is a libsodium ciphertext keyed on easy_wp_smtp_mail_key, which sits in the same wp_options table unless an operator has lifted the key into wp-config.php via EASY_WP_SMTP_CRYPTO_KEY. The rotation flow is identical to WP Mail SMTP’s: rotate at the provider, re-save in Easy WP SMTP → Settings, confirm the row changed, audit the provider dashboard. Easy WP SMTP has carried its own history of security incidents separate from this one; the nanoPost entity review summarises those at the link above.

FluentSMTP

FluentSMTP stores each connection as an array under fluentmail-settings in wp_options, with the credential field encrypted per-connection using OpenSSL and (by default) WordPress’s LOGGED_IN_KEY salt. FluentSMTP’s author, Shahjahan Jewel, has stated on the wordpress.org support forum that changing the WordPress salt keys invalidates stored credentials. In practice, the plugin reads only LOGGED_IN_KEY and LOGGED_IN_SALT, so rotating those two is the nuclear option that invalidates every stored FluentSMTP credential. Rotating AUTH_KEY or SECURE_AUTH_KEY logs out every logged-in user instead; rotating NONCE_KEY invalidates in-flight nonces (form submissions, AJAX actions) rather than sessions. Neither touches FluentSMTP ciphertext.

Per-connection rotation runs the same four steps as above: rotate at the provider, open FluentSMTP → Settings → the connection, re-enter the credential, save. For a multi-connection setup (a transactional provider plus a Gmail connection for a shared mailbox, say), rotate each one and audit each one against its own dashboard.

Harden the five files the toolkit greps for

The toolkit’s grep list is also a hardening checklist. For each file, the question is the same: does the public internet get a response body or a 404?

  • wp-config.php.bak, wp-config.php~, wp-config.old, wp-config.php.save: editor and migration leftovers. curl -I https://your-site/wp-config.php.bak from an off-host machine should return 404 for each variant. Delete the backups themselves if they live anywhere web-reachable; move them above the webroot if you need to keep them.
  • .env: framework or tooling leftover. Same curl test. If the file holds secrets at all, it belongs above the webroot.
  • .git/config: deployment leftover. The whole /.git/ tree should return 403 or 404. On nginx, location ~ /\.git { deny all; return 404; } covers it; on Apache, <DirectoryMatch "/\.git"> with Require all denied does the same.
  • backup.sql, backup.sql.gz, *.sql, dated variants: no database dump belongs inside the webroot. Full stop. This is the file that compromises WP Mail SMTP and Easy WP SMTP on its own.
  • wp-content/debug.log: set WP_DEBUG_LOG to an absolute path above the webroot (not to the boolean true, which lands the log at wp-content/debug.log by default). Even with the file inside the webroot, nginx and Apache should refuse .log by location block.

Run every curl check from an off-host machine, not from the server itself. A local request bypasses the public-facing config in several common setups and will tell you the server allows a file that the public does not actually reach, or vice versa.

Patch the core too

Even without a file leak, the toolkit’s REST-request path is a reachable alternative on unpatched cores. CVE-2026-60137 and CVE-2026-63030 are the two cyberpress names, patched in WordPress 7.0.2, 6.9.5, and 6.8.6. wp core version reports what you have; wp core update or your managed host’s update flow resolves it. Anything on 7.1 already includes the fix. A site pinned on an older major that cyberpress does not name has no clean patch path, so the LTS backports are the fallback worth planning the upgrade around.

The news is operational, not novel

The recipe here has been documented in theory since each plugin shipped encrypted-at-rest storage. The news is that somebody built the tooling, ran it against 37,000 sites, and left the control panel on the open internet. Treat it as a prompt, not a surprise.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *