GCVE-1988-2026-0488
Vulnerability from gna-1988 – Published: 2026-11-02 07:30 – Updated: 2026-11-02 07:30{
"containers": {
"cna": {
"affected": [
{
"product": "Pal",
"vendor": "Maian",
"versions": [
{
"status": "affected",
"version": "unknown"
}
]
}
],
"credits": [
{
"lang": "en",
"type": "finder",
"value": "disclosure via Fulldisclosure"
}
],
"descriptions": [
{
"lang": "en",
"value": "0day Rubbish Research Team is publicly disclosing a vulnerability in Maian Pal\n2.1, the self-hosted PHP shopping-cart script from Maian Media (United Kingdom),\noriented at PayPal checkout and distributed as a commercial closed-source archive,\nverified end to end against a real installation of that version in three\nindependent reproductions.\n\nType: unrestricted upload of a file with a dangerous type (CWE-434 primary, with\nCWE-94 and CWE-693 contributing - the stored object is subsequently interpreted as\ncode, and the served upload directory lacks the execution restriction that\nequivalent directories in the same product carry), realised as OS command\nexecution (CWE-78 describes what the demonstration payload does once it runs and\nis not scored separately). The product-image handling of the administration\nproduct create and update operations, `ajax-ops=add` and `ajax-ops=add-edit`,\npasses the multipart field fm[ifile][] to products::pictures() at\n`class.products.php:333` through `class.products.php:410`. The stored name is\nchosen from the client-supplied filename by a single ternary:\n\n // class.products.php:372 - no extension allow-list anywhere on this path\n $picPath = ($cfg[\u0027rename\u0027] == \u0027yes\u0027\n ? $cfg[\u0027imgprf\u0027] . $ID . \u0027-\u0027 . $nextPic . strrchr($name, \u0027.\u0027)\n : $name);\n\n // class.products.php:~390 - written before any validation\n move_uploaded_file($temp, $picFldr . \u0027/\u0027 . $picPath);\n\nWith $cfg supplied by thumbcfg() at `class.products.php:615`, whose shipped\ndefault is \u0027rename\u0027 =\u003e \u0027no\u0027, that assignment reduces to $picPath = $name, so the\nclient controls the entire stored name including the extension and a .php file\nlands on disk intact as content/products/\u003cID\u003e/\u003cname\u003e.php. There is no extension\nallow-list and no deny-list, no finfo or MIME verification, no re-encoding to\nstrip an embedded payload, no server-generated filename with a type-derived\nextension, and no rollback of the write when later validation fails. strtolower()\nappears only in the non-default rename=\u0027yes\u0027 branch and is cosmetic.\n\nThe root cause has two independently fixable halves, and this advisory keeps them\nseparate because fixing one without the other leaves the chain standing.\n\nThe first half is the handler: what may be written. The second is the directory:\nwhether what is written may execute. That second half lines up in this product\nbecause the destination sits inside the document root and the front-controller\nrewrite claims only paths that do not exist on disk - `RewriteCond !-f` guarding\n`RewriteRule ^([^?]*) index.php?_msw_=$1 [L,QSA]` - so a file that exists is handed\nstraight to the PHP handler, and requesting it with a query string executes it.\nNothing in content/products/ disables that handler. Our review of the shipped tree\nfound an Apache deny file carrying `Deny from all` present in admin/import/, and\nfound content/products/ carrying no such file.\n\nThe contrast that makes this an omission rather than a design decision is inside\none product and one shipped version: two handlers accept files from a browser, and\none of them checks the extension and writes into a deny-protected directory while\nthe other does neither. We are precise about the evidentiary weight of each half\nof that comparison, because it matters. That admin/import/ carries `Deny from all`\nand that content/products/ carries nothing are both anchored artefacts of the\nshipped tree. The statement that the product\u0027s attachment upload path applies an\nextension allow-list into a deny-protected directory is a source-level reading of\nthe shipped PHP that we did not reduce to a line citation, and we label it as\nsuch. It is offered here the same way we put it to the seller: as a contrast worth\nconfirming, since part of the argument that this is an omission rests on it.\n\nThe ordering inside the handler makes the failure path worse rather than safer,\nand this is the detail most likely to be missed by a fix that only adds a check\ndownstream. move_uploaded_file() runs before any thumbnail inspection, and the\nvalidation that does run is not fatal. getimagesize() inside thumbnail() runs\nafter the write and is used only to select a thumbnail strategy, never to accept\nor reject the upload; for a PHP file it fails, the switch falls through to a\nsuppressed @copy(), and the original is never unlinked. So a non-image upload\nleaves two objects in a served directory - the original plus a tmb_-prefixed\nsibling - deletes neither, and still reports {\"status\":\"ok\"}. The failure path\nyields two files rather than zero, silently. Any cleanup that removes only the\nobviously named object leaves the second one behind.\n\nTurning rename on would not fix this either, and it deserves emphasis because it\nis the obvious operator-facing knob: in the rename=\u0027yes\u0027 branch the stem is\nserver-generated but the extension is still taken from the client filename via\nstrrchr(), so a .php upload produces a server-named file that is still\nexecutable. We derived that from the assignment expression and did not run the\nrename=\u0027yes\u0027 configuration, so it is an argument from code rather than a dynamic\nresult.\n\nCommands execute as the web server service account, observed as\n`uid=33(www-data) gid=33(www-data) groups=33(www-data)` in three independent\nreproductions with distinct random filenames, plus a marker written to an absolute\npath outside the document root and read back afterwards from the host. No\nprivilege escalation of any kind was attempted or observed: no kernel exploit, no\nsudo right, no writable service unit, no container escape. Root was not reached\nand is not claimed; the ceiling of this finding is code execution as the web\nserver user.\n\nHow confirmation works. The chain is two requests. One POST creates the object\nand the create response itself embeds `?p=add\u0026id=\u003cID\u003e`, so the attacker learns\nthe numeric product directory without guessing and the filename is one they\nchose - predictability of the final URL is total. One GET to\n`/content/products/\u003cID\u003e/\u003cname\u003e.php?cmd=\u003ccommand\u003e` executes it and returns the\noutput in the response body, prefixed by a marker that proves the body came from\nPHP execution rather than from static file serving. Both operations report\n{\"status\":\"ok\"}. What was deliberately not reproduced: no data theft, no database\nmodification, no persistence beyond the uploaded file, no lateral movement and no\nexploitation of a live third-party system - all verification targeted our own\ninstance, and the payload\u0027s default command is a non-destructive identity query.\n\nScoring. One score is published: 7.2 High,\nCVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H, the CVSS 3.1 base arithmetic for\nwhich is shown in full in the advisory and was re-checked against an independent\nimplementation of that formula. AV:N because the sink sits on the product\u0027s\nordinary HTTP(S) listener, with no separate management port, no RPC endpoint and\nno background listener, and nothing in the chain depends on source address, user\nagent or session age beyond validity. AC:L because one POST creates the object and\none GET executes it, so there is no race window, no multi-step state to prepare and\nno unusual configuration requirement: the chain depends only on shipped defaults\n(thumbcfg() \u0027rename\u0027 =\u003e \u0027no\u0027, no deny file in content/products/), and the licence\nrestriction RESTR_PRODUCTS = 50 does not obstruct the first product create. UI:N\nbecause no victim browser or user action is involved - the attacker authenticates\nwith their own credentials and there is no forgery step.\n\nPR:H is published because only an authenticated administrator session was\ndemonstrated. The credential pair is the one the installer provisions (LG_USER\nand LG_PASS in `control/_cfg.php`), mswPerms() gates every administration\noperation except login and logout, and no graduated back-end role was shown to\nreach ajax-ops=add. Four named alternative readings are published against that\n7.2 so that each rejection is auditable, and one of them is conditional rather\nthan rejected:\n\n - Scope Changed, AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H = 9.1. Rejected. The\n vulnerable component is the administration back-end and the impacted\n resources are that application\u0027s own files, its own database reached with\n the very credentials the application holds, and the operating-system\n context of the web server process it runs inside. The achieved identity is\n the same service account the application always ran as, on the same host,\n and nothing outside its reach before the upload came into reach\n afterwards. Publishing S:C would require a database-server compromise\n independent of the application\u0027s credentials, a container escape or a\n distinct host, and none was demonstrated.\n - Unauthenticated, AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8. Rejected, and\n this is the tempting headline for a public-facing shopping cart that we\n decline to print as ours. The storefront dispatcher\u0027s 56 unauthenticated\n AJAX operations contain no file-upload, command-execution, code-evaluation\n or deserialization primitive - only include() of language and class files -\n the administration login was reviewed on six specific bypass axes with no\n success, and the product ships no factory credential and no usable\n SECRET_KEY.\n - Lower-privileged back-end role, AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H = 8.8.\n Published as a conditional reading, labelled as such and never as the\n headline. The dispatcher exposes users, users-edit, permissions and\n permissions-edit, so the product may model back-end accounts narrower than\n the single installer-provisioned administrator; mswPerms() was established\n in our review only as an authentication gate over the dispatch, not as a\n per-operation authorization matrix; and no principal short of full\n administrator was demonstrated reaching ajax-ops=add. If a restricted\n back-end account can create or update products, that account is the\n least-privileged principal reaching the sink and PR:L is the faithful\n metric, at Impact 5.87311872 plus Exploitability 2.83525473, base\n 8.70837345, rounded up. We did not round our rating up to it, because\n doing so would assert a privilege model we did not observe.\n - Elevated complexity, AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H = 6.6, and user\n interaction, AV:N/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H = 6.8. Both printed only\n so the rejections are inspectable: there is no race, no ordering constraint\n beyond one POST and one GET, no requirement that the target be in an\n unusual state, and no victim action at any point in the chain.\n\nContributing weaknesses are reported and deliberately not scored into the vector:\nthe single-round SHA-1 mswEnc() credential transform, loose `==` comparisons on\nhash and token strings, the absence of any enforced rotation of the\ninstaller-provisioned secret, and the inconsistency between upload handlers inside\none shipped version. None of them lowers the privilege required to reach the sink\nand none raises the impact of the upload once it is reached. There is also no\nnumeric base score in our own record against this vector that could disagree with\nit, so no correction notice accompanies the figure.\n\nAuthentication: post-authentication administrator plus the session CSRF token, and\nboth gates were treated as things to attack rather than as configuration to trust.\nThere are no factory credentials in Maian Pal 2.1: the installer forces LG_USER,\nLG_PASS and SECRET_KEY to be set, `control/_cfg-sample.php:62` ships only the\nplaceholder text \"Your secret key value goes here\", and this is not a\ndefault-credential report. The login path was reviewed for a missing or permissive\npermission configuration, a route that skips the gate, a predictable CSRF token, a\npre-computable credential hash, a factory-credential fallback, a\ncomparison-timing difference and a secondary authentication branch, and none was\nfound; t"
}
],
"problemTypes": [
{
"descriptions": [
{
"cweId": "CWE-434",
"description": "CWE-434",
"lang": "en",
"type": "CWE"
},
{
"cweId": "CWE-693",
"description": "CWE-693",
"lang": "en",
"type": "CWE"
},
{
"cweId": "CWE-78",
"description": "CWE-78",
"lang": "en",
"type": "CWE"
},
{
"cweId": "CWE-94",
"description": "CWE-94",
"lang": "en",
"type": "CWE"
}
]
}
],
"providerMetadata": {
"dateUpdated": "2026-11-02T07:30:02Z",
"orgId": "4e2abfbf-4a2a-4b76-a4e0-d77c18ba156c",
"shortName": "VULNARCHIVE"
},
"references": [
{
"tags": [
"technical-description",
"exploit"
],
"url": "https://vuln.freearchive.org/archive/full-disclosure/2026/Oct/17"
},
{
"tags": [
"technical-description"
],
"url": "https://seclists.org/fulldisclosure/2026/Oct/17"
},
{
"url": "https://0day-rubbish.com"
},
{
"url": "https://0day-rubbish.com/blog/maian-pal-product-image-upload-webshell-rce"
},
{
"url": "https://github.com/Exploit-Garbage/0day-Rubbish"
},
{
"url": "https://nmap.org/mailman/listinfo/fulldisclosure"
},
{
"url": "https://seclists.org/fulldisclosure/"
}
],
"source": {
"defect": [
"https://seclists.org/fulldisclosure/2026/Oct/17"
],
"discovery": "EXTERNAL"
},
"title": "[0day-rubbish] Maian Pal 2.1 product image upload with no extension allow-list to PHP webshell and www-data uid 33 code execution (7.2, PR:H)",
"x_gcve": [
{
"recordType": "advisory",
"relationships": [],
"vulnId": "GCVE-1988-2026-0488",
"x_vulnarchive": {
"archiveUrl": "https://vuln.freearchive.org/archive/full-disclosure/2026/Oct/17",
"automated": true,
"contentSha256": "0b3e889b1e4a4dcd1911310cb807a8368fb210a07e78930cca78dfad7f6238a2",
"evidenceScore": 11,
"messageId": "",
"originalUrl": "https://seclists.org/fulldisclosure/2026/Oct/17",
"policy": "vulnarchive-1",
"sourceFormat": "text/html",
"sourcePublishedAt": "2026-10-13T02:22:02Z"
}
}
]
}
},
"cveMetadata": {
"assignerOrgId": "4e2abfbf-4a2a-4b76-a4e0-d77c18ba156c",
"assignerShortName": "VULNARCHIVE",
"datePublished": "2026-11-02T07:30:02Z",
"dateUpdated": "2026-11-02T07:30:02Z",
"state": "PUBLISHED",
"vulnId": "GCVE-1988-2026-0488"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.2"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
How durations are calculated
Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.
Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.