Initial commit
This commit is contained in:
commit
60fc3dc835
|
@ -0,0 +1,6 @@
|
|||
*[~%]
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
.idea/
|
||||
build/
|
|
@ -0,0 +1,86 @@
|
|||
import os
|
||||
from configparser import ConfigParser
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
from requests import Session, get
|
||||
|
||||
|
||||
class CTFdClient:
|
||||
"""Client implementation of the CTFd API"""
|
||||
|
||||
def __init__(self, baseurl: str, token: str, verify_ssl: bool = True):
|
||||
self._base_url = baseurl.rstrip("/")
|
||||
self._session = Session()
|
||||
self._session.headers["Authorization"] = f"Token {token}"
|
||||
self._session.headers["Content-Type"] = "application/json"
|
||||
self._verify_ssl = verify_ssl
|
||||
|
||||
@classmethod
|
||||
def from_config(
|
||||
cls,
|
||||
config_file: str = "~/.config/ctftool/ctftool.conf",
|
||||
section: Optional[str] = None,
|
||||
) -> "CTFdClient":
|
||||
cfg = ConfigParser()
|
||||
cfg.read(os.path.expanduser(config_file))
|
||||
if section is None:
|
||||
sections = [x for x in cfg.sections() if x != "DEFAULT"]
|
||||
section = sections[0]
|
||||
section = cfg[section]
|
||||
return cls(section["url"], section["token"], section.getboolean("verify", True))
|
||||
|
||||
def download_file(self, url: str, out: str = None) -> str:
|
||||
if out is None:
|
||||
out = os.curdir
|
||||
out = os.path.realpath(out)
|
||||
|
||||
full_url = urljoin(self._base_url, url)
|
||||
file_name = urlparse(full_url).path.rsplit("/", 1)[-1]
|
||||
|
||||
with get(full_url, stream=True) as r:
|
||||
r.raise_for_status()
|
||||
disposition = r.headers.get("Content-Disposition").strip()
|
||||
if disposition:
|
||||
if disposition.lower().startswith("attachment;"):
|
||||
_, rest = disposition.split(";", 1)
|
||||
rest = rest.strip()
|
||||
if rest.lower().startswith("filename="):
|
||||
_, file_name = rest.split("=", 1)
|
||||
file_name = file_name.strip()
|
||||
file_path = os.path.join(out, file_name)
|
||||
with open(file_path, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
return file_path
|
||||
|
||||
def api_url(self, url: str) -> str:
|
||||
return f"{self._base_url}/api/v1{url}"
|
||||
|
||||
def get(self, url: str, **kwargs) -> dict[str, Any]:
|
||||
response = self._session.get(
|
||||
self.api_url(url), verify=self._verify_ssl, **kwargs
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def post(self, url: str, data=None, json=None, **kwargs) -> dict[str, Any]:
|
||||
response = self._session.post(
|
||||
self.api_url(url), data, json, verify=self._verify_ssl, **kwargs
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def get_challenge_list(self) -> dict[str, Any]:
|
||||
return self.get("/challenges")
|
||||
|
||||
def get_challenge(self, challenge_id: int) -> dict[str, Any]:
|
||||
return self.get(f"/challenges/{challenge_id}")
|
||||
|
||||
def post_challenge_attempt(
|
||||
self, challenge_id: int, submission: str
|
||||
) -> dict[str, Any]:
|
||||
return self.post(
|
||||
"/challenges/attempt",
|
||||
json=dict(challenge_id=challenge_id, submission=submission),
|
||||
)
|
|
@ -0,0 +1,85 @@
|
|||
import os
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
from rich.table import Table
|
||||
|
||||
from ctftool.api import CTFdClient
|
||||
|
||||
|
||||
def _make_table(rows: list[dict[str, Any]], fields: list[str]) -> Table:
|
||||
table = Table()
|
||||
types = {}
|
||||
for row in rows:
|
||||
for field, value in [(x, row.get(x)) for x in fields]:
|
||||
ft = type(value)
|
||||
if field in types:
|
||||
if value is not None and types[field] != ft:
|
||||
types[field] = "mixed"
|
||||
else:
|
||||
types[field] = ft
|
||||
for field, field_type in [(x, types[x]) for x in fields]:
|
||||
if field_type in (int, float):
|
||||
table.add_column(field, justify="right")
|
||||
else:
|
||||
table.add_column(field, justify="left")
|
||||
for row in rows:
|
||||
table.add_row(*(str(row.get(x)) for x in fields))
|
||||
return table
|
||||
|
||||
|
||||
class CLI:
|
||||
"""CLI tool to interact with a CTFd service"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_file: str = "~/.config/ctftool/ctftool.conf",
|
||||
event: Optional[str] = None,
|
||||
):
|
||||
self.ctfd = CTFdClient.from_config(config_file, event)
|
||||
self.env = Environment(
|
||||
loader=PackageLoader("ctftool"), autoescape=select_autoescape()
|
||||
)
|
||||
self.con = Console()
|
||||
|
||||
def list(self):
|
||||
"""List challenges"""
|
||||
r = self.ctfd.get_challenge_list()["data"]
|
||||
self.con.print(
|
||||
_make_table(
|
||||
r, ["id", "value", "category", "name", "solves", "solved_by_me", "tags"]
|
||||
)
|
||||
)
|
||||
|
||||
ls = list
|
||||
|
||||
def show(self, challenge_id: int, save: bool = False, render: bool = False):
|
||||
"""Display a specific challenge and optionally download it"""
|
||||
challenge = self.ctfd.get_challenge(challenge_id)["data"]
|
||||
attached_file_urls = challenge.get("files", [])
|
||||
attached_files = [urlparse(x).path.split("/")[-1] for x in attached_file_urls]
|
||||
|
||||
md = self.env.get_template("challenge.md").render(
|
||||
challenge=challenge, attached_files=attached_files
|
||||
)
|
||||
|
||||
if save:
|
||||
out = os.path.realpath(os.curdir)
|
||||
with open(os.path.join(out, "challenge.md"), "w") as fd:
|
||||
fd.write(md)
|
||||
for cfile in attached_file_urls:
|
||||
self.ctfd.download_file(cfile, out)
|
||||
|
||||
if render:
|
||||
self.con.print(Markdown(md))
|
||||
else:
|
||||
self.con.print(md)
|
||||
|
||||
cat = show
|
||||
|
||||
def submit(self, challenge_id: int, flag: str):
|
||||
"""Submit the flag for a challenge"""
|
||||
self.con.print(self.ctfd.post_challenge_attempt(challenge_id, flag))
|
|
@ -0,0 +1,14 @@
|
|||
# {{ challenge.name }}
|
||||
|
||||
{{ challenge.category }} - {{ challenge.value }} points
|
||||
|
||||
{{ challenge.description | wordwrap(78) | indent(width='> ', first=True, blank=True) }}
|
||||
{%- if attached_files %}
|
||||
|
||||
|
||||
## Attached files
|
||||
|
||||
{% for item in attached_files -%}
|
||||
- {{ item }}
|
||||
{% endfor -%}
|
||||
{%- endif -%}
|
|
@ -0,0 +1,4 @@
|
|||
fire
|
||||
Jinja2
|
||||
requests
|
||||
rich
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
import fire
|
||||
|
||||
from ctftool.cli import CLI
|
||||
|
||||
if __name__ == "__main__":
|
||||
fire.Fire(CLI)
|
|
@ -0,0 +1,18 @@
|
|||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="ctftool",
|
||||
version="0.1.0",
|
||||
description="Command line tool to interact with CTFd",
|
||||
author="Maurizio Porrato",
|
||||
author_email="maurizio.porrato@mailbox.org",
|
||||
scripts=["scripts/ctf"],
|
||||
packages=find_packages(),
|
||||
package_data={"ctftool": ["templates/*"]},
|
||||
install_requires=[
|
||||
"fire",
|
||||
"Jinja2",
|
||||
"requests",
|
||||
"rich",
|
||||
],
|
||||
)
|
Loading…
Reference in New Issue