-
Notifications
You must be signed in to change notification settings - Fork 151
Add dojo Command-Line Application
#964
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
TheodorKitzenmaier
wants to merge
24
commits into
pwncollege:master
Choose a base branch
from
TheodorKitzenmaier:cli2
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
7c5b33b
Updated Container Auth Token
TheodorKitzenmaier b3286f6
Container Token Validation Returns UserID and ChallengeID
TheodorKitzenmaier 5c386bf
Added Container Token Authenticated API Endpoint
TheodorKitzenmaier a0c70f3
Opened CTFD to challenge containers.
TheodorKitzenmaier f340e42
Added note about Flask
TheodorKitzenmaier ba17663
Switched Container Communication to Nginx
TheodorKitzenmaier ce673ec
Convert tabs to spaces.
TheodorKitzenmaier ed9ae02
Added Dojo Application to Nix
TheodorKitzenmaier 707c87c
Added Dojo CLI Test
TheodorKitzenmaier 1a5d92c
Container Tokens Timeout After 6 Hours
TheodorKitzenmaier a9c631f
Fix Dojo-CLI path.
TheodorKitzenmaier e4f0445
Updated dojo-cli to use Urllib
TheodorKitzenmaier ed69d79
WIP fix for CLI
TheodorKitzenmaier bf34e66
Fix Integration WHOAMI Endpoint.
TheodorKitzenmaier c01db6a
Merge branch 'master' into cli2
TheodorKitzenmaier 7c531ed
Improved CLI App Error Handling
TheodorKitzenmaier e48dc27
Merge remote-tracking branch 'refs/remotes/origin/cli2' into cli2
TheodorKitzenmaier becb7f0
Merge branch 'master' into cli2
TheodorKitzenmaier cec2bfc
Add CLI again
TheodorKitzenmaier 93feeaf
Typo
TheodorKitzenmaier c876948
Handle URLErrors
TheodorKitzenmaier ade0da2
Test connection to main node
TheodorKitzenmaier bd52139
Add node_net docker network
TheodorKitzenmaier ffd49d4
No More Node_Net
TheodorKitzenmaier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| from typing import Any | ||
| from flask import request, session | ||
| from flask_restx import Namespace, Resource | ||
| from CTFd.plugins import bypass_csrf_protection | ||
| from CTFd.models import Users | ||
| from ...utils import validate_user_container, get_current_container | ||
|
|
||
| integration_namespace = Namespace( | ||
| "integration", | ||
| description="Endpoints for internal container integration", | ||
| decorators=[bypass_csrf_protection] | ||
| ) | ||
|
|
||
| def authenticate_container(token : str) -> tuple[Any, str | None, int | None]: | ||
| """ | ||
| Takes in a container token and returns the user if authentication succeeds. | ||
| Otherwise it will return `None`, an error message, and an error code. | ||
| """ | ||
|
|
||
| try: | ||
| userID, challengeID = validate_user_container(token) | ||
| except: | ||
| # validate user container (probably) raised BadSignature. | ||
| return None, "Failed to authenticate container token.", 401 | ||
|
|
||
| # Validate user. | ||
| user = Users.query.filter_by(id=userID).one() | ||
| if user is None: | ||
| return None, "Failed to authenticate container token", 401 | ||
|
|
||
| # Validate challenge matches. | ||
| container = get_current_container(user) | ||
| if container is None: | ||
| return None, "No active challenge container.", 403 | ||
| if container.labels["dojo.challenge_id"] != challengeID: | ||
| return None, "Token failed to authenticate active challenge container.", 403 | ||
|
|
||
| return user, None, None | ||
|
|
||
| # Idealy we would want to use before_request and teardown_request, | ||
| # however this is only supported at the application level. It is | ||
| # currently not possible to define this at the route or namespace | ||
| # level. | ||
| def authenticated(func): | ||
| """ | ||
| Function decorator. | ||
|
|
||
| Performs authentication of the request. Excepts | ||
| authentication information to be provided as part | ||
| of the request Headers. Temporarily creates an | ||
| authenticated session, then destroys it before | ||
| returning. | ||
| """ | ||
| def wrapper(*args, **kwargs): | ||
| # Authenticate. | ||
| token = request.headers.get("AuthToken", None) | ||
| if token is None: | ||
| return ({"success": False, "error": "Authentication token not provided."}, 400) | ||
| user, error, code = authenticate_container(token) | ||
| if user is None: | ||
| return ({"success": False, "error": error}, code) | ||
|
|
||
| try: | ||
| # Configure session and perform operation. | ||
| session["id"] = user.id | ||
| session["name"] = user.name | ||
| session["type"] = user.type | ||
| session["verified"] = user.verified | ||
| return func(*args, **kwargs) | ||
|
|
||
| except: | ||
| # FUBAR | ||
| return ({"success": False, "error": "An internal exception occured."}, 500) | ||
|
|
||
| finally: | ||
| # Make sure we destroy the session, no matter what. | ||
| session["id"] = None | ||
| session["name"] = None | ||
| session["type"] = None | ||
| session["verified"] = None | ||
| return wrapper | ||
|
|
||
| @integration_namespace.route("/whoami") | ||
| class whoami(Resource): | ||
| @authenticated | ||
| def get(self): | ||
| return ({ | ||
| "success": True, | ||
| "message": f"You are the epic hacker {session["name"]} ({session["id"]}).", | ||
| "user_id": session["id"] | ||
| }, 200) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import subprocess | ||
|
|
||
| from utils import workspace_run, start_challenge | ||
|
|
||
| def test_whoami(random_user, welcome_dojo): | ||
| """ | ||
| Tests the dojo application with the "whoami" command. | ||
|
|
||
| Likely reasons for failure are: | ||
| - Issue with the dojo application. | ||
| - Issue with the integrations api. | ||
| - Issue with container (challenge -> nginx) networking. | ||
| """ | ||
| # Make sure we have a running challenge container. | ||
| name, session = random_user | ||
| start_challenge(welcome_dojo, "welcome", "challenge", session=session) | ||
|
|
||
| try: | ||
| result = workspace_run("dojo whoami", user=name) | ||
| assert name in result.stdout, f"Expected hacker to be {name}, got: {(result.stdout, result.stderr)}" | ||
| except subprocess.CalledProcessError as error: | ||
| assert False, f"Exception in when running command \"dojo whoami\": {(error.stdout, error.stderr)}" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| { pkgs }: | ||
|
|
||
| pkgs.writeScriptBin "dojo" '' | ||
| #!${pkgs.python3}/bin/python3 | ||
|
|
||
| import urllib.request | ||
| import urllib.parse | ||
| import urllib.error | ||
| import argparse | ||
| import json | ||
| import sys | ||
| import os | ||
|
|
||
| DOJO_URL = "http://pwn.college:80" | ||
| DOJO_API = f"{DOJO_URL}/pwncollege_api/v1" | ||
|
|
||
| INCORRECT_USAGE = 1 | ||
| TOKEN_NOT_FOUND = 2 | ||
| API_ERROR = 3 | ||
|
|
||
| def get_token() -> str | None: | ||
| return os.environ.get("DOJO_AUTH_TOKEN") | ||
|
|
||
| def apiRequest(endpoint: str, method: str = "GET", args: dict[str, str] = {}) -> tuple[int, str | None, dict[str, str]]: | ||
| """ | ||
| Make a request to the given integration endpoint. | ||
| Will call `sys.exit` if the auth token is not specified in the environment. | ||
|
|
||
| Returns the http response code, an error message (or `None`), and a dictionary with the json response data. | ||
|
|
||
| Supports `GET` and `POST` methods. | ||
| """ | ||
| # Container authentication token required. | ||
| token = get_token() | ||
| if not token: | ||
| print("Failed to find authentication token (DOJO_AUTH_TOKEN). Did you change environment variables?") | ||
| sys.exit(TOKEN_NOT_FOUND) | ||
|
|
||
| # Append args to URL. | ||
| url = f"{DOJO_API}{endpoint}" | ||
| if (len(args) > 0 and method == "GET"): | ||
| url += f"?{urllib.parse.urlencode(args)}" | ||
|
|
||
| # Construct HTTP request. | ||
| match method: | ||
| case "GET": | ||
| request = urllib.request.Request( | ||
| url, | ||
| method="GET", | ||
| headers={ | ||
| "AuthToken":token | ||
| } | ||
| ) | ||
| case "POST": | ||
| data = json.dumps(args).encode() | ||
| request = urllib.request.Request( | ||
| url, | ||
| method="POST", | ||
| data=data, | ||
| headers={ | ||
| "AuthToken":token, | ||
| "Content-Type": "application/json; charset=utf-8", | ||
| } | ||
| ) | ||
| case _: | ||
| return 0, f"Unsupported method \"{method}\".", {} | ||
|
|
||
| # Make request, handle errors. | ||
| try: | ||
| response = urllib.request.urlopen(request, timeout=5.0) | ||
| except urllib.error.HTTPError as exception: | ||
| try: | ||
| return exception.code, json.loads(exception.read().decode())["error"], {} | ||
| except: | ||
| return exception.code, exception.reason, {} | ||
| except urllib.error.URLError as exception: | ||
| return 0, exception.reason, {} | ||
|
|
||
| # Parse response. | ||
| try: | ||
| response_json = json.loads(response.read().decode()) | ||
| except json.JSONDecodeError as exception: | ||
| return response.status, exception.msg, {} | ||
| except UnicodeDecodeError as exception: | ||
| return response.status, exception.reason, {} | ||
| except Exception as exception: | ||
| return response.status, "Exception while parsing reponse.", {} | ||
|
|
||
| if not response_json.get("success", False): | ||
| error = response_json.get("error", "No message provided.") | ||
| return response.status, error, response_json | ||
|
|
||
| return response.status, None, response_json | ||
|
|
||
| def whoami() -> int: | ||
| """ | ||
| Calls the WHOAMI integration api, printing information | ||
| about the current user such as userID and username. | ||
| """ | ||
|
|
||
| # Make request. | ||
| status, error, jsonData = apiRequest("/integration/whoami") | ||
| if error is not None: | ||
| print(f"WHOAMI request failed ({status}): {error}") | ||
| sys.exit(API_ERROR) | ||
|
|
||
| # Print who's hacking. | ||
| print(jsonData["message"]) | ||
| return 0 | ||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser( | ||
| prog="dojo", | ||
| description="Command-line application for interacting with the dojo from inside of the challenge environment." | ||
| ) | ||
|
|
||
| subparsers = parser.add_subparsers( | ||
| dest="command", | ||
| help="Dojo command to execute, not case sensitive." | ||
| ) | ||
|
|
||
| whoami_parser = subparsers.add_parser( | ||
| name="whoami", | ||
| help="Prints information about the current user (you!)." | ||
| ) | ||
|
|
||
| args = parser.parse_args() | ||
|
|
||
| if args.command is None: | ||
| parser.print_help() | ||
| return INCORRECT_USAGE | ||
|
|
||
| if args.command.lower() == "whoami": | ||
| return whoami() | ||
|
|
||
| parser.print_help() | ||
| return INCORRECT_USAGE | ||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) | ||
|
|
||
| '' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Challenge containers are now allowed to open NEW connections to
192.168.42.1with no port restriction. That address is the host-side bridge for the infrastructure stack, so accepting all traffic from every10.0.0.0/8container exposes every service bound on the host (docker daemon, databases, etc.), not just nginx. If the intent is to permit the CLI to reach the web frontend, this rule needs to narrow the destination ports or IP to the nginx container specifically; otherwise any compromise inside a challenge gains unrestricted network access to internal control planes.Useful? React with 👍 / 👎.