Skip to content

Commit 71d5b6c

Browse files
committed
pwnshop: add challenge.yml support for privileged containers
1 parent 5d46f77 commit 71d5b6c

5 files changed

Lines changed: 102 additions & 7 deletions

File tree

tools/pwnshop/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ dependencies = [
99
"click",
1010
"jinja2",
1111
"pyastyle",
12+
"pyyaml",
1213
"rich",
1314
]
1415

tools/pwnshop/src/pwnshop/commands/run.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@
3232
@click.argument("command", nargs=-1, default=("/bin/bash",))
3333
def run_command(challenge_path, user, volumes, command):
3434
"""Run interactive shell for a challenge."""
35+
config = lib.load_challenge_config(challenge_path)
3536
try:
3637
image_id = lib.build_challenge(challenge_path)
3738
except RuntimeError as error:
3839
raise click.ClickException(str(error)) from error
3940
resolved_volumes = [path.resolve() for path in volumes]
4041
logger.info("running %s as uid=%d, command=%s", challenge_path, user, list(command))
41-
with lib.run_challenge(image_id, volumes=resolved_volumes) as (container, flag):
42+
with lib.run_challenge(image_id, volumes=resolved_volumes, privileged=config["privileged"]) as (container, flag):
4243
subprocess.run(["docker", "exec", f"--user={user}", "-it", container, *command])

tools/pwnshop/src/pwnshop/commands/test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def test_challenge(challenge_path):
6464
challenge_path = pathlib.Path(challenge_path)
6565
rendered = None
6666
try:
67+
config = lib.load_challenge_config(challenge_path)
6768
rendered = lib.render_challenge(challenge_path)
6869
image_id = lib.build_challenge(challenge_path)
6970
tests = sorted(rendered.rglob("test*/test_*"))
@@ -75,7 +76,7 @@ def test_challenge(challenge_path):
7576
for test in tests:
7677
test_name = test.relative_to(rendered)
7778
logger.debug("running test %s in %s", test_name, challenge_path)
78-
with lib.run_challenge(image_id, volumes=[test]) as (container, _):
79+
with lib.run_challenge(image_id, volumes=[test], privileged=config["privileged"]) as (container, _):
7980
try:
8081
run = subprocess.run(
8182
["docker", "exec", "--user=1000:1000", container, f"{test}"],

tools/pwnshop/src/pwnshop/lib/__init__.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,32 @@
88
import shutil
99
import subprocess
1010
import tempfile
11-
from typing import Iterable, Iterator, List, Optional, Sequence
11+
from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence
1212

1313
import black
1414
import jinja2
1515
import pyastyle
16+
import yaml
1617

1718
logger = logging.getLogger(__name__)
1819

1920
CHALLENGE_SEED = int(os.environ.get("CHALLENGE_SEED", "0"))
2021

22+
CHALLENGE_CONFIG_DEFAULTS: Dict[str, Any] = {
23+
"privileged": False,
24+
}
25+
26+
27+
def load_challenge_config(challenge_path: pathlib.Path) -> Dict[str, Any]:
28+
config = dict(CHALLENGE_CONFIG_DEFAULTS)
29+
config_file = challenge_path / "challenge.yml"
30+
if config_file.is_file():
31+
logger.debug("loading challenge config from %s", config_file)
32+
with open(config_file) as f:
33+
user_config = yaml.safe_load(f) or {}
34+
config.update(user_config)
35+
return config
36+
2137

2238
class _NoSelfExtendLoader(jinja2.FileSystemLoader):
2339
"""FileSystemLoader that prevents templates from extending/including themselves.
@@ -136,7 +152,7 @@ def ignore_git_crypt(current, names):
136152

137153
@contextlib.contextmanager
138154
def run_challenge(
139-
challenge_image: str, *, volumes: Optional[Sequence[pathlib.Path]] = None
155+
challenge_image: str, *, volumes: Optional[Sequence[pathlib.Path]] = None, privileged: bool = False
140156
) -> Iterator[tuple[str, str]]:
141157
flag = "pwn.college{" + base64.b64encode(os.urandom(32)).decode() + "}"
142158
env_options = []
@@ -149,6 +165,8 @@ def run_challenge(
149165
logger.info("starting container for image %s", challenge_image)
150166
if volumes:
151167
logger.debug("mounting volumes: %s", volumes)
168+
if privileged:
169+
logger.debug("running container in privileged mode")
152170
container = (
153171
subprocess.check_output(
154172
[
@@ -159,8 +177,7 @@ def run_challenge(
159177
"--detach",
160178
"--init",
161179
"--user=0:0",
162-
"--device=/dev/kvm",
163-
"--cap-add=SYS_PTRACE",
180+
*(["--privileged"] if privileged else ["--device=/dev/kvm", "--cap-add=SYS_PTRACE"]),
164181
*env_options,
165182
*[f"--volume={volume}:{volume}:ro" for volume in (volumes or [])],
166183
challenge_image,

0 commit comments

Comments
 (0)