
Proof-of-concept exploit for CVE-2026-5076 demonstrating unauthenticated admin account takeover in ARMember Premium via SQL injection and plaintext password reset key storage.
Plaintext Password Reset Keys Stored in Database + SQL Injection = Complete Admin Takeover
| Item | Detail |
|---|---|
| CVE ID | CVE-2026-5076 |
| Plugin | ARMember – Membership Plugin & Content Restriction |
| Affected Version | Premium <= 7.3.1 |
| Patched Version | 7.3.2 |
| CVSS Score | 9.8 Critical |
| CWE | CWE-640: Weak Password Recovery |
| Type | Insecure Password Reset Mechanism → Plaintext Key Storage |
| Attack Vector | Network / Remote / Unauthenticated (via SQLi chain) |
| Active Installations | 30,000+ (Premium) |
| Discoverer | Wordfence Threat Intelligence |
| Publication Date | June 3, 2026 |
| CVE | Type | 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 |
Three critical vulnerabilities in the WordPress plugin ARMember Premium <= 7.3.1 that, when chained together, allow full administrator account takeover without authentication:
wp_usermeta (arm_reset_password_key), not hashed as per WordPress standardorder parameter of the AJAX handler arm_directory_paging_action()filter parameter of the AJAX handler arm_directory_paging_action()Direct consequence of CVE-2026-5076: Anyone with read access to the database (via SQLi, backup exposure, etc.) can read the password reset key in plaintext and immediately use it to reset any account's password — without needing to crack it.
Standard WordPress stores the password reset key in the user_activation_key column in hashed form using wp_hash(). ARMember stores a copy of the same key in wp_usermeta with meta_key arm_reset_password_key — but in 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
**Critical Issue**: `$wp_key` here is the key generated by `wp_generate_password(20, false)` — 20 alphanumeric characters. WordPress hashes this key before storing it in `user_activation_key`, but ARMember stores it **before hashing** or stores a separate **unhashed** copy.
### Root Cause #2: Key Persistence Bug
When `get_password_reset_key()` is called (WordPress core), a new key is generated and hashed. However, this function **does NOT update** `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!
As a result, the old plaintext key remains stored permanently in arm_reset_password_key even after the user performs a password reset. This key can be used repeatedly until the meta key is explicitly deleted.
AJAX handler arm_directory_paging_action() has a nonce check via arm_check_user_cap(), but the order and filter parameters go directly into the SQL query without sanitization:```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! }
**ORDER BY Exploitation**: The `order` parameter is inserted into the SQL `ORDER BY` clause. Because there is no sanitization, an attacker can inject a 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
This Oracle is immune to network latency because it distinguishes TRUE/FALSE based on error vs success, not response time.
╔══════════════════════════════════════════════════════════════════════════════════╗ ║ CVE-2026-5076 FULL CHAIN ATTACK ROADMAP ║ ║ ARMember Premium <= 7.3.1 → Unauthenticated Admin Takeover ║ ╚══════════════════════════════════════════════════════════════════════════════════╝