diff --git a/mkchal/mkchal.py b/mkchal/mkchal.py index 66150f0..0a88af1 100644 --- a/mkchal/mkchal.py +++ b/mkchal/mkchal.py @@ -1,12 +1,12 @@ from __future__ import annotations + +import os +import stat from enum import Enum -from re import match, sub -from pathlib import Path from json import dumps, loads +from pathlib import Path +from re import match, sub from secrets import token_hex -import os -import stat - # Infra constants ROOT_DOMAIN = os.getenv("ROOT_DOMAIN", "b01le.rs") # TODO: make it compliant with the testing workflow and VPS @@ -25,6 +25,7 @@ CHAL_JSON = "chal.json" DOCKERFILE = "Dockerfile" COMPOSE = "docker-compose.yml" +COMPOSE_PROD = "docker-compose.prod.yml" WRAPPER = "wrapper.sh" SAMPLE_PY = "sample.py" SAMPLE_C = "sample.c" @@ -32,6 +33,7 @@ BUILD_SH = "build.sh" DOCKERFILE_BUILD = "Dockerfile_build" BUILD_DIST = "build_dist.sh" +DEV_SH = "dev.sh" RUN_SH = "run.sh" README = "README.md" FLAG = "flag.txt" @@ -176,18 +178,24 @@ def __generate_defaults(challenge_obj: Challenge, challenge: Path) -> None: (challenge / CHAL_JSON).write_text(str(challenge_obj)) (challenge / FLAG).write_text(challenge_obj.flag) - - @staticmethod def __generate_deployments(challenge_obj: Challenge, challenge: Path) -> None: - from textwrap import dedent if challenge_obj.deploy == DeployType.NO_DEPLOY: return + (challenge / DEPLOY).mkdir(parents=True, exist_ok=DEBUG) (challenge / DEPLOY / DOCKERFILE).write_text(challenge_obj.gen_dockerfile()) + (challenge / DEPLOY / COMPOSE).write_text(challenge_obj.gen_docker_compose()) + (challenge / DEPLOY / COMPOSE_PROD).write_text( + ChallengeUtils.generate_file_content(TEMPLATES_DIR / COMPOSE_PROD, {}) + ) (challenge / DEPLOY / WRAPPER).write_text(challenge_obj.gen_wrapper()) + + (challenge / RUN_SH).write_text(challenge_obj.gen_run_sh()) + (challenge / DEV_SH).write_text(challenge_obj.gen_dev_sh()) + make_file_executable(challenge / RUN_SH) + make_file_executable(challenge / DEV_SH) - compose_command = "docker compose" if ChallengeUtils.has_docker_space_compose() else "docker-compose" if challenge_obj.type == ChallengeType.PWN: # special build Dockerfile and redpwn jail for pwn (challenge / SRC / SAMPLE_C).write_text(challenge_obj.gen_sample()) @@ -197,78 +205,16 @@ def __generate_deployments(challenge_obj: Challenge, challenge: Path) -> None: # for now pwn only support docker-compose assert challenge_obj.deploy == DeployType.DOCKER_COMPOSE - subdomain = ChallengeUtils.generate_service_name(challenge_obj.name) - (challenge / DEPLOY / COMPOSE).write_text(challenge_obj.gen_docker_compose()) (challenge / BUILD_DIST).write_text(challenge_obj.gen_pwn_build_dist()) make_file_executable(challenge / BUILD_DIST) - (challenge / RUN_SH).write_text( - dedent(f""" - #!/bin/sh - cd deploy && sudo {compose_command} up -d --build {challenge_obj.name} && echo ' - - - If you are testing locally:' && echo '> ncat localhost 1337' && echo ' - If you are on prod or testing server, here is how you connect:' && echo '> ncat --ssl {subdomain}.{ROOT_DOMAIN} {TCP_SEC_ENTRY}' - """) - ) - make_file_executable(challenge / RUN_SH) + return (challenge / SRC / SAMPLE_PY).write_text(challenge_obj.gen_sample()) - if challenge_obj.deploy == DeployType.DOCKER_COMPOSE: - subdomain = ChallengeUtils.generate_service_name(challenge_obj.name) - (challenge / DEPLOY / COMPOSE).write_text(challenge_obj.gen_docker_compose()) - if challenge_obj.type == ChallengeType.WEB: - (challenge / RUN_SH).write_text(dedent( - f"""#!/bin/sh - cd deploy && sudo {compose_command} up -d --build && echo ' - - - If you are testing locally:' && echo '> curl http://localhost:1337' && echo ' - If you are on prod or testing server, here is how you connect:' && echo '> curl https://{subdomain}.{ROOT_DOMAIN}' - """) - ) - else: - (challenge / RUN_SH).write_text(dedent( - f""" - #!/bin/sh - cd deploy && sudo {compose_command} up -d --build {challenge_obj.name} && echo ' - - - If you are testing locally:' && echo '> ncat localhost 1337' && echo ' - If you are on prod or testing server, here is how you connect:' && echo '> ncat --ssl {subdomain}.{ROOT_DOMAIN} {TCP_SEC_ENTRY}' - """) - ) - elif challenge_obj.deploy == DeployType.KLODD: + + if challenge_obj.deploy == DeployType.KLODD: #TODO: b01lers kube interface would be different, wait for vinh's decision (challenge / DEPLOY / KLODD_YAML).write_text(challenge_obj.gen_klodd_challenge()) - (challenge / RUN_SH).write_text(dedent( - f""" - #!/bin/sh - cd deploy && sudo docker build . -t{challenge_obj.name} && sudo -E docker push {challenge_obj.registry}/{challenge_obj.name} && kubectl create -f challenge.yml - """) - ) - - make_file_executable(challenge / RUN_SH) - - @staticmethod - def has_docker_space_compose() -> bool: - """Returns true if `docker compose` works.""" - import shutil - import subprocess - if shutil.which("docker-compose"): - return False - - try: - subprocess.run( - ["docker", "compose", "version"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=True, - ) - return True - except (subprocess.CalledProcessError, FileNotFoundError): - return False class Challenge: """Represents a challenge object""" @@ -335,6 +281,7 @@ def gen_readme(self) -> str: """ if self.deploy != DeployType.NO_DEPLOY: ret += """\n └── run.sh ── what will be run to deploy your challenge""" + ret += """\n └── dev.sh ── what you should use to test your challenge""" ret += """\n```""" ret += f"""\n## Quickstart to challenge development Make sure you develop your challenge on a new branch. You can create one with @@ -365,7 +312,7 @@ def gen_readme(self) -> str: - `./build_dist.sh` will build your challenge and copy the executable and libc to dist. - - `./run.sh` will run your challenge using the binary in dist. + - `./dev.sh` will run your challenge using the binary in dist. """ @@ -383,10 +330,10 @@ def gen_readme(self) -> str: Contains the challenge's writeup and solution scripts. A well-documented writeup is crucial for assessing challenge quality. ### {self.name}/src Contains the challenge source files. If deployment is required, the `Dockerfile` should use this folder to build the challenge. Ensure all necessary files are included for proper functionality. -### {self.name}/run.sh +### {self.name}/dev.sh **IMPORTANT**: If your challenge is not deployed via Klodd, ensure it can be fully deployed by running: ```bash -./run.sh +./dev.sh ``` ## Merging Once your challenge is complete, submit a **Pull Request (PR)**. The PR will be merged after a quality review on GitHub. @@ -493,6 +440,33 @@ def gen_pwn_build_dist(self) -> str: assert self.type == ChallengeType.PWN return ChallengeUtils.generate_file_content(PWN_TEMPLATE_DIR / BUILD_DIST, kwargs) + def gen_run_sh(self): + safe_name = ChallengeUtils.safe_name(self.name) + subdomain = ChallengeUtils.generate_service_name(safe_name) + kwargs = { + "name": safe_name, + "remote_command": ( + f"curl https://{subdomain}.{ROOT_DOMAIN}" + if self.type == ChallengeType.WEB + else f"ncat --ssl {subdomain}.{ROOT_DOMAIN} {TCP_SEC_ENTRY}" + ) + } + if self.type == ChallengeType.WEB and self.deploy == DeployType.KLODD: + return ChallengeUtils.generate_file_content(TEMPLATES_DIR / self.type.value / "klodd" / RUN_SH, kwargs) + return ChallengeUtils.generate_file_content(TEMPLATES_DIR / RUN_SH, kwargs) + + def gen_dev_sh(self): + safe_name = ChallengeUtils.safe_name(self.name) + kwargs = { + "name": safe_name, + "local_command": ( + "curl http://localhost:1337" + if self.type == ChallengeType.WEB + else "ncat localhost 1337" + ) + } + return ChallengeUtils.generate_file_content(TEMPLATES_DIR / DEV_SH, kwargs) + def create(self) -> bool: """Creates the challenge structure for a challenge""" return ChallengeUtils.generate(self) diff --git a/mkchal/templates/dev.sh b/mkchal/templates/dev.sh new file mode 100644 index 0000000..88ddd78 --- /dev/null +++ b/mkchal/templates/dev.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -e +cd -- "$(dirname -- "$0")/deploy" +sudo docker compose up -d --build chall +echo ' + + +If you are testing locally: +> {local_command}' diff --git a/mkchal/templates/docker-compose.prod.yml b/mkchal/templates/docker-compose.prod.yml new file mode 100644 index 0000000..3c59be4 --- /dev/null +++ b/mkchal/templates/docker-compose.prod.yml @@ -0,0 +1,12 @@ +# Do not modify this +services: + chall: + ports: !reset [] + networks: + - traefuck + restart: always +networks: + traefuck: + name: traefuck + external: true + diff --git a/mkchal/templates/docker-compose.yml b/mkchal/templates/docker-compose.yml index 1fd0ad8..269c25b 100644 --- a/mkchal/templates/docker-compose.yml +++ b/mkchal/templates/docker-compose.yml @@ -1,15 +1,12 @@ name: {name} services: - {name}: + chall: container_name: {hash} build: dockerfile: ./deploy/Dockerfile context: ../ logging: driver: "json-file" - restart: always - networks: - - traefuck expose: # This should be the port your challenge runs on inside the container. - "{port}" labels: # If you have a second service make sure the service name and port are replaced @@ -19,10 +16,5 @@ services: - "traefik.tcp.routers.${{COMPOSE_PROJECT_NAME}}.entrypoints=ncsecure" - "traefik.tcp.routers.${{COMPOSE_PROJECT_NAME}}.service=${{COMPOSE_PROJECT_NAME}}-svc" - "traefik.tcp.services.${{COMPOSE_PROJECT_NAME}}-svc.loadbalancer.server.port={port}" - ports: # please comment this whole ports field out when you are ready to make PR. + ports: - "1337:{port}" -networks: - traefuck: - name: traefuck - # please uncomment the below line when making a pr - # external: true \ No newline at end of file diff --git a/mkchal/templates/pwn/build_dist.sh b/mkchal/templates/pwn/build_dist.sh index 72325c2..15b4454 100644 --- a/mkchal/templates/pwn/build_dist.sh +++ b/mkchal/templates/pwn/build_dist.sh @@ -11,4 +11,4 @@ export CHALL_HASH='{hash}' # please include this envar in your final build # If you need to use sudo, you have to pass options to sudo to make sure # USER_ID and GROUP_ID env variables are passed into docker-compose # Otherwise outputed files in dist will be owned by root -cd deploy && sudo -E docker-compose up --build {name}_build +cd deploy && sudo -E docker-compose up --build build diff --git a/mkchal/templates/pwn/docker-compose.yml b/mkchal/templates/pwn/docker-compose.yml index 6ad842b..6475c49 100644 --- a/mkchal/templates/pwn/docker-compose.yml +++ b/mkchal/templates/pwn/docker-compose.yml @@ -1,6 +1,6 @@ name: {name} services: - {name}: + chall: container_name: {hash} privileged: true # needed for redpwn jail to work build: @@ -9,8 +9,6 @@ services: logging: driver: "json-file" restart: always - networks: - - traefuck expose: # This should be the port your challenge runs on inside the container. - "{port}" labels: # please do not delete these labels, if you have a second service make sure the service name and port are replaced @@ -20,10 +18,10 @@ services: - "traefik.tcp.routers.${{COMPOSE_PROJECT_NAME}}.entrypoints=ncsecure" - "traefik.tcp.routers.${{COMPOSE_PROJECT_NAME}}.service=${{COMPOSE_PROJECT_NAME}}-svc" - "traefik.tcp.services.${{COMPOSE_PROJECT_NAME}}-svc.loadbalancer.server.port={port}" - ports: # please comment this whole ports field out when you are ready to make PR. + ports: - "1337:{port}" - {name}_build: # build system for your challenge. + build: # build system for your challenge. user: "${{USER_ID}}:${{GROUP_ID}}" container_name: {hash}_build build: @@ -32,11 +30,4 @@ services: logging: driver: "json-file" volumes: - - ../dist:/dist - - -networks: - traefuck: - name: traefuck - # please uncomment the below line when making a pr - # external: true \ No newline at end of file + - ../dist:/dist \ No newline at end of file diff --git a/mkchal/templates/run.sh b/mkchal/templates/run.sh new file mode 100644 index 0000000..700f005 --- /dev/null +++ b/mkchal/templates/run.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -e +cd -- "$(dirname -- "$0")/deploy" +if [ -f docker-compose.prod.yml ]; then + docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build chall +else + docker compose up -d --build chall +fi +echo ' + + +If you are on prod or testing server, here is how you connect: +> {remote_command}' diff --git a/mkchal/templates/web/docker-compose.yml b/mkchal/templates/web/docker-compose.yml index aaa0c79..3088adb 100644 --- a/mkchal/templates/web/docker-compose.yml +++ b/mkchal/templates/web/docker-compose.yml @@ -1,15 +1,12 @@ name: {name} services: - {name}: + chall: container_name: {hash} build: dockerfile: ./deploy/Dockerfile context: ../ logging: driver: "json-file" - networks: # please keep this in - - traefuck - restart: always # please keep this in expose: # This should be the port your challenge runs on inside the container. - "{port}" labels: # please do not delete these labels, if you have a second service make sure the service name and port are replaced @@ -21,12 +18,5 @@ services: # - "traefik.http.middlewares.fluf.ratelimit.average=100" # - "traefik.http.routers.${{COMPOSE_PROJECT_NAME}}.middlewares=fluf" # - "traefik.http.middlewares.fluf.ratelimit.burst=100" - ports: # please comment the ports field out when you are done. + ports: - "1337:{port}" - -networks: # Do not delete this - traefuck: - name: traefuck - # please uncomment the below line when making a pr - # external: true - diff --git a/mkchal/templates/web/klodd/run.sh b/mkchal/templates/web/klodd/run.sh new file mode 100644 index 0000000..f163bbc --- /dev/null +++ b/mkchal/templates/web/klodd/run.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -e +cd deploy +sudo docker build . -t '{name}' +sudo -E docker push '{registry}/{name}' +kubectl create -f challenge.yml + \ No newline at end of file diff --git a/mkchal/templates/web/sample.py b/mkchal/templates/web/sample.py index 3a476a3..a988728 100644 --- a/mkchal/templates/web/sample.py +++ b/mkchal/templates/web/sample.py @@ -6,7 +6,7 @@ @app.route("/") def index(): with open("./flag.txt", "r") as f: - file = f.read() + file = f.read() return "Hello I am challenge: {name} and my flag is " + file if __name__ == "__main__":