Skip to content
Merged
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
128 changes: 51 additions & 77 deletions mkchal/mkchal.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -25,13 +25,15 @@
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"
KLODD_YAML = "challenge.yml"
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"
Expand Down Expand Up @@ -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())
Expand All @@ -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"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

"""

Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions mkchal/templates/dev.sh
Original file line number Diff line number Diff line change
@@ -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}'
12 changes: 12 additions & 0 deletions mkchal/templates/docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Do not modify this
services:
chall:
ports: !reset []
networks:
- traefuck
restart: always
networks:
traefuck:
name: traefuck
external: true

12 changes: 2 additions & 10 deletions mkchal/templates/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
2 changes: 1 addition & 1 deletion mkchal/templates/pwn/build_dist.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 4 additions & 13 deletions mkchal/templates/pwn/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: {name}
services:
{name}:
chall:
container_name: {hash}
privileged: true # needed for redpwn jail to work
build:
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
- ../dist:/dist
13 changes: 13 additions & 0 deletions mkchal/templates/run.sh
Original file line number Diff line number Diff line change
@@ -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}'
14 changes: 2 additions & 12 deletions mkchal/templates/web/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

7 changes: 7 additions & 0 deletions mkchal/templates/web/klodd/run.sh
Original file line number Diff line number Diff line change
@@ -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

2 changes: 1 addition & 1 deletion mkchal/templates/web/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__":
Expand Down