CVE Hunting Made Easy

In just three Sunday afternoons, I discovered 14 CVEs - and you can too! CVE hunting is more accessible than many realise, and the methodology outlined here requires only a bit of coding knowledge.

CVE Hunting Made Easy

When most people think of CVE hunting, they picture 14-hour hacking sessions and sifting through binary firmware dumps. Indeed, many of the most impactful vulnerabilities are found this way, but it doesn’t have to be that way.

Vulnerable software is plentiful, and with access to source code, we can automate the discovery of high-impact CVEs by focusing on breadth rather than depth.

Obligatory video mention (warning headphone users).

The Idea

Here was the idea, how many serious security issues can we find just by running some SAST scans on every WordPress plugin?

Let's get cooking.

Downloading All Plugins

Challenge 1: How do we get a list of all plugins and download them reliably?

As it turns out, this was a non-issue. WordPress publishes a public API that provides information about all plugins in their catalog, and it even includes a download link for each plugin. Awesome!

If you want to take a look yourself, check out some of the raw output: WordPress Plugin API.

With a bit of python, we have part of a script that:

  • Retrieves the full list of Wordpress Plugins including information like active install count, and the last date it was updated.
  • Inserts this information into a database for later use.
  • Downloads and unzips each plugin that has been updated within the last 2 years.
$ python3 wordpress-plugin-audit.py --download --create-schema
Downloading plugins:   0%|               

RIP 30GB disk space.

Auditing All Plugins (and Staying Organised)

Challenge 2: SAST tools, in general, produce a lot of output. How do I triage and query this efficiently?

I figured the best way to handle all this output was to store it in a SQL database. This way, the results could be easily queried and searched as needed.

The next part of our script:

  • Runs Semgrep (unaffiliated) with the 'p/php' ruleset across each plugin.
  • Stores the raw Semgrep output in a JSON file in the plugin directory.
  • Parses this output and inserts it into a database.
$ python3 wordpress-plugin-audit.py --audit
Auditing plugins:  10%|█████████████▍      
The full process.

This script is published here if you want to follow along:

GitHub - prjblk/wordpress-audit-automation: https://projectblack.io/blog/cve-hunting-at-scale/
https://projectblack.io/blog/cve-hunting-at-scale/ - prjblk/wordpress-audit-automation

Now that we have all the output, I set myself a couple of rules for triaging the corpus so I wouldn't end up spending too much time on this project:

  1. I only looked at the output for LFI (Local File Inclusion) and SQL injection rules.
  2. The plugin must have a active install base greater than 0.
  3. Vulnerabilities only exploitable by administrators will be ignored.
  4. A maximum of 5 minutes will be spent triaging each Semgrep finding that looked interesting.
    1. If something seems possibly vulnerable, I’ll spend a maximum of 15 minutes attempting exploitation.

Triaging Output

With our database of findings in place, we can easily search for specific types of vulnerabilities and sort them by how many installations they affect - all with a SQL query.

USE SemgrepResults;
SELECT PluginResults.slug,PluginData.active_installs,PluginResults.file_path,PluginResults.start_line,PluginResults.vuln_lines 
FROM PluginResults INNER JOIN PluginData ON PluginResults.slug = PluginData.slug 
WHERE check_id = "php.lang.security.injection.tainted-sql-string.tainted-sql-string"
ORDER BY active_installs DESC

Hunting for SQLi

This results in rows of rows of interesting output.

But just because a line of code looks like it could be vulnerable doesn’t mean it actually is. Our next step is to double-check that the issues flagged by Semgrep are genuinely exploitable.

What we're effectively doing here is Sink -> Source code review.

Semgrep is identifying potentially vulnerable sinks, however there's no guarantee that these weak points are actually exploitable. The code might be safe for a couple of reasons:

  1. Input Sanitisation: User inputs might be cleaned up or filtered before they even reach the sink, making the finding a false alarm.
  2. Code Artifacts: The code might not even be used anymore. It could just be remnants from development with no real connection to any user inputs.

Attempting Exploitation

Once we’ve identified a plugin that looks like it might be exploitable, the next step is to validate it through actual exploitation.

WPScan's Wordpress test bench makes this step easier.

GitHub - Automattic/wpscan-vulnerability-test-bench: Standardised setup for researching WordPress plugin- and theme vulnerabilities.
Standardised setup for researching WordPress plugin- and theme vulnerabilities. - Automattic/wpscan-vulnerability-test-bench

Once setup, plugins can be installed and activated using their slug name.

$ ddev wp plugin install woocommerce --activate-network

If we’ve done our triage right, this step is mostly about figuring out how to use the plugin to hit the vulnerability source.

Sometimes, you’ll need to dig around the plugin menus to figure out how certain features can be enabled. Other times, the vulnerabilities might be directly accessible through the WordPress REST API or AJAX interface, which can make things a bit easier.

*A full worked example is provided at the end.

The Findings

So, what did we uncover just by triaging the Semgrep results? Here’s the full list of CVEs disclosed to the WPScan CNA.

Unauthenticated .php LFI/Exeuction

Authenticated .php LFI/Execution

  • still coordinating disclosure - CVE-2024-6228

Unauthenticated SQLi

Authenticated SQLi

Takeaways

This was a fun project with a few takeaway learnings:

  • Unsurprisingly, most of plugins we disclosed issues for had a small install base, around 200-2,000 users, with the largest being quiz-master-next, which had over 40,000 installs at the time of writing.
  • There's more than one way to go CVE hunting. Usually, the focus is on depth—picking one product or codebase and reverse-engineering it thoroughly. Here, we took the opposite approach, going for breadth rather than depth.
  • The importance of shifting security left. If these plugin developers had run Semgrep or any other SAST tools on their own plugins, they likely would have found the same vulnerabilities I did.
  • If you're a pentester working on a WordPress site in your next test, take a closer look at the plugins installed on your customer's site. Just because there aren't any disclosed issues doesn't mean there aren't any easily discoverable vulnerabilities waiting to be found.
  • This method is definitely replicable (also why stop at Wordpress plugins). I didn’t spend a lot of time on each plugin, nor did I look at every vulnerability class, so there’s undoubtedly stuff I missed.
    • Do you want to give it a go? The full SQL dataset is published here.
      • If there's enough interest, I can keep my computer busy and publish a new dataset every other month.

Worked Example

push-notification-for-post-and-buddypress <=1.93 SQLi

Scrolling through the corpus, this particular line caught my eye.

A PHP variable $onesignal_externalid is inserted directly into a SQL query.

⚠️
It's also important that the variable is not wrapped with quotes. Wordpress implements it's own 'magic quotes' functionality which automatically escapes user input.

e.g. Your input ' or 1=1 becomes \' or 1=1.

Opening the plugin's source code we can quickly spot the vulnerable Sink that Semgrep has highlighted and the Source for the variable.

pnfpb_update_deviceid_ajax.php

Although it appears that the user input is sanitised using sanitize_text_field, a quick trip the developer docs shows that this function looks like it's intended to protect against XSS and won't help with preventing SQLi.

Triage complete. ✅

Let's install the plugin and figure out how to call this code and exploit it.

Searching through the code base, we can spot that the affected file is included as a part of this function - PNFPB_icpushcallback_callback.

pnfpb_push_notification.php

Tracing further upstream we find that this callback function can be reached via Wordpress's AJAX interface and to top it off, this can be reached without authentication!

pnfpb_push_notification.php
💡
To reach this code all we have to do is make a POST request against the AJAX interface at /wp-admin/admin-ajax.php with an action name of icpushcallback.

Now with what we know we can craft a simple POC to add a 1 second sleep to the SQL query while providing the relevant parameters to reach our desired code path.

🎉