|8 min read|Yvann Lièvre

SMB Sites: Scanning Joomla Plugin Upload CVEs with Nuclei (Webshell)

Joomla plugin upload vulnerability detection CVE: a KEV wave hits SP Page Builder, Joomlack, iCagenda, Balbooa Forms. A Nuclei method against false positives.

NucleiJoomlaCVEWebshell
SMB Sites: Scanning Joomla Plugin Upload CVEs with Nuclei (Webshell)

In early July, several entries landed on the CISA KEV catalog for Joomla extensions that sit on a large share of SMB showcase websites: SP Page Builder, Joomlack Page Builder, iCagenda and Balbooa Forms. The shared flaw is as simple as it is dangerous: a file upload endpoint that fails to enforce server-side extension checks, letting a .php file through where only an image was ever expected. The practical outcome: a webshell dropped in a single unauthenticated request, in several of these cases.

Attention still defaults to WordPress, which absorbs most of the research and most of the attacker tooling. Yet Joomla powers a meaningful share of French SMB showcase and institutional sites, usually installed once by an agency and left unmaintained for years afterward. A critical CVSS score on an upload plugin, on that kind of estate, is not an academic scenario: it is a backdoor waiting to be used.

The blind spot of SMB showcase sites

A Joomla showcase site is almost never monitored like a critical asset. No EDR, no active WAF, sometimes not even a recent backup. The agency that installed it rarely holds a running maintenance contract. That is exactly the profile mass scanning campaigns target the moment an upload CVE goes public: little detection noise, low odds a compromise gets noticed for months.

For a firm managing a portfolio of client sites, the question is no longer "was this site hit" but "which one, out of the twenty Joomla sites in the portfolio, has not been hit yet."

The July wave: four plugins, one shared flaw

The advisories published this month describe a recurring pattern across all four extensions: a Joomla component exposes an upload endpoint (tied to a contact form, a page builder, or an event module) and validates the file extension client-side only, or through a blacklist that is trivial to bypass (double extension, case variation, null byte).

The starting point for any detection stays the advisory itself, not a public exploit. A proof-of-concept circulating on a repository with no explicit license remains its author's property; the technical fact described in the advisory ("this path, this parameter, this response") belongs to no one and is the legitimate raw material for a template.

id: cve-2026-0001-joomla-pagebuilder-upload
# Illustrative CVE: the principle matters more than the chosen number.
 
info:
  name: "Joomla Page Builder Plugin < 5.x - Unrestricted File Upload"
  author: threatclaw
  severity: critical
  description: |
    The component's upload endpoint at /components/com_pagebuilder/ accepts
    a .php file with no server-side extension validation.
  classification:
    cve-id: CVE-2026-0001
    cvss-score: 9.8
  tags: cve,cve2026,joomla,upload,webshell

Fingerprinting Joomla and the plugin version without noise

Before attempting any upload, two passive checks are enough to qualify the target: confirm the site actually runs Joomla, then identify the exact version of the plugin in question.

http:
  - method: GET
    path:
      - "{{BaseURL}}/administrator/manifests/files/joomla.xml"
      - "{{BaseURL}}/language/en-GB/en-GB.xml"
    matchers:
      - type: word
        part: body
        words:
          - "<name>Joomla"

The plugin's version marker almost always sits in the manifest exposed under /components/, or in the public files served under /media/:

  - method: GET
    path:
      - "{{BaseURL}}/administrator/components/com_pagebuilder/pagebuilder.xml"
    matchers:
      - type: regex
        part: body
        regex:
          - '<version>([0-9.]+)</version>'
        internal: true

This fingerprint alone rules out every site that does not run the extension, without sending a single risky request. It is the first noise filter, well before any thought of testing the upload itself.

The Nuclei template shape: chained requests, not a bare GET

This is where most quickly-written templates fail: they check the plugin version with a single GET request and declare "vulnerable" on that basis alone. That kind of template ignores partial backports, configurations that disable public upload by default, and floods a team managing dozens of sites with unusable noise.

The correct approach chains two requests: a harmless upload POST, followed by a read-back that confirms the dropped file is actually reachable and interpreted.

flow: |
  http(1)
  http(2)
 
http:
  - method: POST
    path:
      - "{{BaseURL}}/components/com_pagebuilder/upload.php"
    multipart:
      - name: file
        filename: "tc-check-{{randstr}}.php"
        content: "<?php echo 'tc-marker-{{randstr}}'; ?>"
    matchers:
      - type: status
        status:
          - 200
 
  - method: GET
    path:
      - "{{BaseURL}}/media/com_pagebuilder/uploads/tc-check-{{randstr}}.php"
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200
      - type: word
        part: body
        words:
          - "tc-marker-{{randstr}}"

The dropped file executes nothing beyond a plain echo of a unique, on-the-fly marker. The goal is not to exploit the flaw for access, it is to obtain the lightest, most reliable signal that it actually exists: the file was accepted, stored at a predictable path, and its content got executed by the server. The matcher fires on a confirmed upload response, never on the mere presence of a vulnerable version string.

Checking whether a webshell is already sitting there

A portfolio that has never been audited may already host a compromise that predates the CVE's publication. A second pass, separate from the exploitation test, looks for traces of an upload that already happened: suspiciously named .php files in the usual upload directories, and responses containing patterns typical of a webshell (eval calls, base64-encoded blobs, system(, shell_exec( strings).

http:
  - method: GET
    path:
      - "{{BaseURL}}/media/com_pagebuilder/uploads/"
      - "{{BaseURL}}/images/icagenda/uploads/"
    matchers:
      - type: word
        part: body
        words:
          - ".php"
 
  - method: GET
    path:
      - "{{BaseURL}}/media/com_pagebuilder/uploads/{{1}}"
    matchers:
      - type: word
        part: body
        words:
          - "eval("
          - "base64_decode"
          - "system("
        condition: or

This verification scan exploits nothing: it reads what is already publicly exposed. A positive result here is not a vulnerability alert, it is an incident alert, to be handled with absolute priority ahead of any plugin update.

Against false positives: prove the upload, do not infer it

The costliest mistake is concluding a site is vulnerable from the plugin's mere presence, or from a version string that looks older than the patch. Two traps come up repeatedly: the backported fix (the displayed version stays old even though a targeted patch was applied upstream), and configurations that disable public upload by default on certain custom installs.

The discipline applied here fits in one sentence: never declare "vulnerable" without proof of an actual upload, confirmed by a read-back. Before a template is folded into a shipped pack, it is validated against both scenarios: a deliberately vulnerable install of the affected version (the template must fire) and the same install once patched (the template must stay silent). A template that fails the second test never ships as-is.

Framing the practice: a periodic sweep across the portfolio, remediation

For a firm running a portfolio of client sites, the right cadence is a periodic sweep of the entire estate, not a one-off check when the CVE first breaks. New installs, plugin migrations, and fixes applied by third parties keep shifting the surface continuously.

nuclei -l client-sites-portfolio.txt \
  -t cve-2026-0001-joomla-pagebuilder-upload.yaml \
  -t cve-2026-0002-joomlack-upload.yaml \
  -t cve-2026-0003-icagenda-upload.yaml \
  -t cve-2026-0004-balbooa-forms-upload.yaml \
  -severity critical,high \
  -o joomla-scan-results.json

When a positive result comes back, remediation follows a strict order: update the plugin immediately to the fixed version, then run the compromise check above before considering the site clean. Updating without first checking for a webshell is like locking a door with the intruder still inside.

In summary

The July KEV wave on SP Page Builder, Joomlack, iCagenda and Balbooa Forms points to an overlooked reality: SMB Joomla showcase sites carry critical file upload CVEs, and nobody is watching them. The method that holds up over time starts from the advisory, not a copied exploit, fingerprints the version without noise, chains a harmless upload request with its read-back rather than concluding from a bare version number, and clearly separates the vulnerability scan from the scan for a compromise that is already in place.

That is exactly the rigor built into the ThreatClaw Nuclei feed: templates validated on the real engine, prioritized against the KEV catalog, ready to run across an entire client site portfolio the moment a critical CVE breaks.

Related articles