Incident investigation

Clop's extortion site became an extortion target

Extortion group Clop had its website defaced and received a ransom demand from another group, exposing a Grav flaw that lingered for months in the older branch; sites still running that code need an update and a check for earlier compromise.

Warm-paper illustration of a website noticeboard with its original documents covered by a new red notice, beside a form and folders.
In this article

Clop is an extortion group that uses stolen company information to pressure victims. On September 18, its own leak site acquired a file uploaded by an outsider, followed by a replacement home page. BleepingComputer visited the site and confirmed both the downloadable file and the defacement. The other group, ShinyHunters, then demanded an eight-figure payment from Clop, threatening to release material it claimed to have taken. The group accustomed to posting extortion notices had received one of its own.

The most useful part of this dispute lies in the software running the site. Clop used Grav, a PHP content management system for publishing pages and handling forms, with much of its content stored directly in files. Follow-up reporting traced the entry point to temporary form-file handling. Grav's maintainers confirmed that the underlying flaw had been fixed in April. The older 1.7 branch received that repair only on September 24, after the incident became public.

That gives ordinary site owners a reason to pay attention. The attackers said Clop was running the older 1.7.43 release. Updating it to the then-latest 1.7.53.3 would still have left the relevant code vulnerable. The questions for an administrator are practical: which branch is serving the site, when did that branch actually receive the fix, and were any files changed before it arrived?

1 Find the code that is actually serving requests

The entry point involves Grav core's FormFlash, which preserves temporary form state and uploaded files. The old implementation inserted a form identifier into a directory path. An identifier with path semantics could send file operations outside the intended directory. In core 1.7.43, the constructor accepts the identifiers and getTmpDir() joins the path. The 1.7.53.4 repair restricts identifier characters at construction, after which the upload path rejects an empty identifier. Updating the Form plugin alone can leave the old core implementation in place.

On September 30, Grav recommends stable release 2.2.3. That is the deployment target for sites that can complete compatibility testing. Installations that must remain on 1.7 should first apply the 1.7.53.4 backport and then plan their migration. Release 2.0.0-beta.2 marks the first April fix; a deployment today should use the current stable release. The 1.7.53.4 backport closes the gap discussed here. Other dependencies and historical security fixes still need their own review.

If an immediate update is impractical, restrict external access to the affected forms and their upload and state-storage handlers, then verify enforcement on the server side. Hiding an upload button leaves direct submissions possible. Disabling an entire form can interrupt enquiries or attachment collection, so provide a working alternative. Before reopening it, use an isolated copy to check the installed version, a normal submission and upload, and the rejection of identifiers that could write outside the intended directory. Keep restrictions in place if the update fails. Any rollback package must retain the repair for its branch.

2 How a form identifier chooses a file's location

A website usually stages an attachment in a temporary directory before moving it to its final destination. Grav needs to associate that file with a session and a form, so it includes both identifiers in the path. The default layout is effectively tmp/forms/session-ID/form-ID. This is a convenient arrangement as long as each identifier remains a single directory name.

The problem begins with the form ID sent back by the browser. In the reported Form 7.3.0 version, form.php, lines 1191–1215, reads __unique_form_id__ and calls setUniqueId() after finding the matching form. A server-generated value is still under the request sender's control when it comes back. A hidden field changes the page's presentation; it does not keep the sender from changing the submitted value.

Core's FormTrait::getFlash() then passes that value to FormFlash as unique_id. Here, “flash” means short-lived form state retained between requests. Once the constructor has accepted the ID, getTmpDir() gives the filesystem an almost direct concatenation:

// Grav 1.7.43 · FormFlash::getTmpDir()
return $this->folder && $this->uniqueId
    ? "{$this->folder}/{$this->uniqueId}"
    : '';

A normal identifier such as form_42 selects a subdirectory with that name inside the session directory. Directory separators and parent-directory components change where a path leads. When PHP passes the assembled string to the filesystem, those components are interpreted as navigation. A value intended to distinguish forms has acquired the ability to choose a storage location.

A browser-supplied form ID reaches unique_id and enters a directory path; the fix checks characters and empties invalid IDs before that path is used.
Figure 1. An identifier enters a directory path. Gates and folders represent character checks and storage locations; the code below gives the exact allowlist. Reachable functionality and server permissions still constrain actual writes.

The next question is which operation uses the path. The plugin's uploadFiles() calls the flash object's upload method. The compatibility class used in 1.7.43, Grav\Common\Form\FormFlash::uploadFile(), obtains the temporary path, creates the directory, and calls move_uploaded_file(). At this point, the assembled string determines where bytes are written to disk.

That explains the limits of checking an upload's extension or size, or giving the file a random name. Those measures constrain content, volume, or the final filename. This flaw affects its parent directory. The plugin does perform type and size checks before the upload call, and a particular installation is further constrained by form configuration, reachable functionality, and the PHP process's write permissions. The source explains how directory traversal becomes possible. Public material does not provide the complete requests, files, and subsequent execution history from Clop's server.

3 The April fix reached the old branch in September

The repair is small enough to follow directly. April's d904efc commit introduced strict character restrictions for path identifiers and shipped in 2.0.0-beta.2. On September 24, backport a39fe54 brought corresponding handling to 1.7, released as 1.7.53.4. In the follow-up interview, the maintainers confirmed that the fix had not previously been backported.

A backport brings a repair from a newer development line to an older one, allowing users to fix the same error without immediately migrating a major version. These intervening months show why that work matters. For 1.7 users, the April repair had not yet become an installable fix on their branch. Public records do not explain the internal decision that left the gap. They do establish that the two release lines acquired the repair at substantially different times.

In 1.7.53.4, sanitizeId() checks the identifier with the expression below and returns an empty string when the match fails. The function discards the whole invalid identifier. Removing only its troublesome characters could quietly turn different inputs into the same directory name.

// Grav 1.7.53.4 · sanitizeId() acceptance rule
preg_match('/^[A-Za-z0-9,_-]{1,64}$/', $id) ? $id : '';

Following the same upload path shows why the empty value matters. getTmpDir() returns an empty path for an empty identifier, and the compatibility class's uploadFile() returns failure before creating a directory or moving the file. Form-state saving has its own empty-value checks. These branches describe the actual change in behavior. Other callers, plugins, and custom forms still need validation along their own paths; a patch comment cannot stand in for checking every file operation on a site.

The older branch also retains a deliberate difference. The 2.x implementation restricts three fields: id, session_id, and unique_id. The 1.7 backport restricts the latter two. Its id comes from form configuration and can be a path-like lookup identifier for media forms. A new regression test specifically requires that configuration value to survive unchanged. A useful backport must stop untrusted requests while preserving legitimate calls in the older system. Following each field's origin explains why the change does not simply copy three assignments.

There is another distinction in the vulnerability record. The original advisory discusses __form-flash-id reaching session_id, with directory creation and index.yaml writes. September's incident account describes __unique_form_id__ reaching unique_id. Both encounter the same class of unsafe directory construction, through different request parameters and calling conditions. A detector that recognizes only the parameter in the original advisory can miss the later reported route.

4 What was taken after the home page changed?

The code explains how control of website content could be lost. Establishing the amount of data taken requires incident evidence. The reporter directly observed the uploaded file and replaced home page, and Grav's maintainers validated the vulnerability description sent to them. ShinyHunters additionally claimed to have obtained logs, source code, and onion-service private keys, and invoked alleged payment records in its threats. The reporter did not independently verify that material.

Clop acknowledged that its software was not fully updated and subsequently moved to a new onion address, while denying the theft of important financial or operational information. The address change and public dispute can be recorded. Determining what the server held and who read it would require verifiable files, logs, or a server image. Clop denied being in negotiations, and public reporting has not confirmed a ransom payment. “Extortion target” here refers to the public demand and threat.

For an ordinary business, those distinctions determine the recovery scope. A modified page calls for investigation of content and code integrity. Confirmed access to credentials or private keys expands that work to revocation, rotation, and connected systems. Treating every allegation as established can misdirect the response; simply restoring the home page leaves the question of continuing access unanswered. Recovery should expand as the evidence warrants.

5 Checking an ordinary site through to recovery

Start by confirming that the update reached the installation handling requests. Multiple replicas, old containers, and a release directory that never became active can all separate a version shown in an administration screen from the code serving visitors. Check Grav core and the FormFlash file on each instance, then record the Form plugin version, enabled file fields, and temporary-directory configuration. This tells you which handlers need containment and which workflows need testing after the update.

Upstream's existing boundaries provide useful test cases. The 1.7.53.4 regression tests cover separators, parent-directory components, stream-style paths, encoded forms, excessive length, and control characters. They also retain ordinary identifiers, commas, and valid 64-character values. SOSEC read those assertions and the related source; we did not run them as a live-product test. Installation acceptance should exercise the corresponding checks in an isolated copy, alongside a legitimate attachment upload and normal form submission, while inspecting actual write locations.

A rejected malformed request alone is insufficient to exercise the repair. An earlier configuration check may have stopped it before it reached the changed code. Confirm that a normal request traverses the same form path, then check the identifier restriction and the continued operation of custom media forms. The backport's explicit test preserving configuration id is a useful reminder to include that legitimate workflow.

The earlier exposure requires different evidence. Preserve website and proxy logs, relevant file timestamps, and trusted backups. Look for unexpected files outside form-staging directories and changes to pages, plugins, or configuration. Logs without request bodies may never have recorded the submitted form identifier; a keyword search covers only what was retained. After confirmed or strongly suspected writes, rebuild from trusted packages, inspect content and configuration before restoring them, and handle secrets according to what the process could access. Avoid copying suspect files into the replacement installation.

Finish with the visitor's ordinary tasks: can an enquiry be submitted, can an attachment be retrieved, does media management work, and has every replica switched to repaired code? Remove temporary restrictions only as these checks complete. If the upgrade fails, keep the instance isolated and recover with a repaired package whose compatibility has been tested. A version number at or above 1.7.53.4 says nothing by itself about whether data and configuration can survive a major-version rollback.

The useful question to take away is easily missed in a routine update process: which branch received the fix? Clop's identity made the story striking; the software errors were familiar. A browser-supplied identifier reached the filesystem with path semantics intact. An existing repair then spent months outside a release line that people were still using. Following both issues through running code, actual writes, and a verified recovery tells an administrator what the update has really accomplished.

Research basis

Research basisReviewed public interviews, pinned Grav and Form source, fixes on both branches, and upstream regression assertions. No access to the affected onion site, server image, or live-product exploit reproduction. Sources checked through 2026-09-30 10:42 UTC.

SourceGrav source and advisories; BleepingComputer observations and interviews

Evidence confidence Medium

6Evidence and sources

6.1Sources and material

  1. BleepingComputer: September 19 observations, updated September 21https://www.bleepingcomputer.com/news/security/shinyhunters-hacks-clop-leak-site-threatens-to-extort-ransomware-gang/
  2. BleepingComputer: September 25 technical account and maintainer interviewhttps://www.bleepingcomputer.com/news/security/shinyhunters-hacked-clop-leak-site-using-grav-cms-path-traversal-flaw/
  3. Grav: CVE-2026-42608 / GHSA-hmcx-ch82-3fv2https://github.com/getgrav/grav/security/advisories/GHSA-hmcx-ch82-3fv2
  4. Grav: April repair d904efchttps://github.com/getgrav/grav/commit/d904efc33e03ebb597afde8d3368b28cf0423632
  5. Grav: 1.7 backport and regression tests a39fe54https://github.com/getgrav/grav/commit/a39fe54c6ffc6c20189bd8099a754a8f2c74daa6
  6. Grav: 1.7.53.4 release noteshttps://github.com/getgrav/grav/releases/tag/1.7.53.4
  7. Grav: current stable 2.2.3 release noteshttps://github.com/getgrav/grav/releases/tag/2.2.3
  8. Grav: current official download recommendationhttps://getgrav.org/downloads