adding validated services? patching forcad_local.py
This commit is contained in:
370
OmCTF-2025/checkers/jform/checker.py
Executable file
370
OmCTF-2025/checkers/jform/checker.py
Executable file
@@ -0,0 +1,370 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import requests
|
||||
from typing import Optional
|
||||
|
||||
from checklib import *
|
||||
from jform_lib import CheckMachine
|
||||
|
||||
|
||||
class Checker(BaseChecker):
|
||||
vulns: int = 1
|
||||
timeout: int = 15
|
||||
uses_attack_data: bool = True
|
||||
|
||||
def __init__(self, host: str):
|
||||
super().__init__(host)
|
||||
self.m = CheckMachine(self)
|
||||
|
||||
def _new_session(self) -> requests.Session:
|
||||
return get_initialized_session()
|
||||
|
||||
def _require_json(self, r: requests.Response, where: str) -> dict:
|
||||
self.assert_in("application/json", r.headers.get("Content-Type", ""), f"{where}: Content-Type is not JSON")
|
||||
return self.get_json(r, f"{where}: malformed JSON")
|
||||
|
||||
def _extract_form_id(self, obj: dict) -> str:
|
||||
form_id = obj.get("id") or obj.get("formId") or obj.get("_id")
|
||||
self.assert_neq(None, form_id, "Service returned empty form id")
|
||||
form_id = str(form_id)
|
||||
self.assert_neq("", form_id, "Service returned empty form id")
|
||||
return form_id
|
||||
|
||||
def _register_and_login(self, s: requests.Session):
|
||||
username = rnd_username()
|
||||
password = rnd_password()
|
||||
|
||||
r = self.m.register(s, username, password)
|
||||
self.assert_eq(200, r.status_code, "Service /signup returned unexpected status code")
|
||||
_ = self._require_json(r, "/signup")
|
||||
|
||||
r = self.m.login(s, username, password)
|
||||
self.assert_eq(200, r.status_code, "Service /login returned unexpected status code")
|
||||
_ = self._require_json(r, "/login")
|
||||
|
||||
r = self.m.me(s)
|
||||
self.assert_eq(200, r.status_code, "Service /me returned unexpected status code")
|
||||
me = self._require_json(r, "/me")
|
||||
self.assert_in("username", me, "Service /me missing username")
|
||||
self.assert_eq(me["username"], username, "Service /me returned wrong username")
|
||||
self.assert_in("userId", me, "Service /me missing userId")
|
||||
user_id = str(me["userId"])
|
||||
self.assert_neq("", user_id, "Service /me returned empty userId")
|
||||
|
||||
return username, password, user_id
|
||||
|
||||
def _form_catalog(self):
|
||||
return [
|
||||
{
|
||||
"title": "Обратная связь",
|
||||
"description": "Короткий опрос о качестве сервиса",
|
||||
"fields": [
|
||||
{"name": "email_1", "type": "email", "label": "Email", "placeholder": "you@example.ru", "required": True},
|
||||
{"name": "number_1", "type": "number", "label": "Оценка (1-10)", "placeholder": "10", "required": True},
|
||||
{"name": "textarea_1", "type": "textarea", "label": "Комментарий", "placeholder": "Напишите пару слов", "required": False},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Заявка на работу",
|
||||
"description": "Расскажите о себе",
|
||||
"fields": [
|
||||
{"name": "text_1", "type": "text", "label": "ФИО", "placeholder": "Иванов Иван", "required": True},
|
||||
{"name": "email_1", "type": "email", "label": "Почта", "placeholder": "you@example.ru", "required": True},
|
||||
{"name": "number_1", "type": "number", "label": "Опыт (лет)", "placeholder": "3", "required": True},
|
||||
{"name": "textarea_1", "type": "textarea", "label": "Резюме (кратко)", "placeholder": "Ключевые навыки...", "required": False},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Регистрация на событие",
|
||||
"description": "Подтвердите участие",
|
||||
"fields": [
|
||||
{"name": "text_1", "type": "text", "label": "Имя", "placeholder": "Ваше имя", "required": True},
|
||||
{"name": "email_1", "type": "email", "label": "Email", "placeholder": "you@example.ru", "required": True},
|
||||
{"name": "number_1", "type": "number", "label": "Количество гостей", "placeholder": "0", "required": False},
|
||||
{"name": "text_2", "type": "text", "label": "Дата", "placeholder": "2025-10-05", "required": True},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Сообщить об ошибке",
|
||||
"description": "Помогите нам стать лучше",
|
||||
"fields": [
|
||||
{"name": "text_1", "type": "text", "label": "Заголовок", "placeholder": "Коротко о проблеме", "required": True},
|
||||
{"name": "select_1", "type": "select", "label": "Серьёзность", "options": ["Низкая", "Средняя", "Высокая"], "required": True},
|
||||
{"name": "textarea_1", "type": "textarea", "label": "Шаги воспроизведения", "placeholder": "1) ... 2) ...", "required": True},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Заказ пиццы",
|
||||
"description": "Соберите свою пиццу",
|
||||
"fields": [
|
||||
{"name": "select_1", "type": "select", "label": "Размер", "options": ["Маленькая", "Средняя", "Большая"], "required": True},
|
||||
{"name": "text_1", "type": "text", "label": "Адрес", "placeholder": "Город, улица, дом", "required": True},
|
||||
{"name": "select_2", "type": "select", "label": "Соус", "options": ["Томатный", "Сливочный", "Барбекю"], "required": False},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Опрос о цветах",
|
||||
"description": "Пара вопросов о вкусе",
|
||||
"fields": [
|
||||
{"name": "number_1", "type": "number", "label": "Возраст", "placeholder": "25", "required": False},
|
||||
{"name": "text_1", "type": "text", "label": "Любимый цвет", "placeholder": "синий", "required": True},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Мини-викторина",
|
||||
"description": "Два коротких вопроса",
|
||||
"fields": [
|
||||
{"name": "text_1", "type": "text", "label": "Имя", "placeholder": "Ваше имя", "required": True},
|
||||
{"name": "number_1", "type": "number", "label": "Сколько будет 2+2?", "placeholder": "4", "required": True},
|
||||
{"name": "number_2", "type": "number", "label": "Сколько дней в неделе?", "placeholder": "7", "required": True},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Заявка в поддержку",
|
||||
"description": "Опишите проблему",
|
||||
"fields": [
|
||||
{"name": "email_1", "type": "email", "label": "Email", "placeholder": "you@example.ru", "required": True},
|
||||
{"name": "text_1", "type": "text", "label": "Тема", "placeholder": "Коротко", "required": True},
|
||||
{"name": "textarea_1", "type": "textarea", "label": "Описание", "placeholder": "Что произошло?", "required": True},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Командировка",
|
||||
"description": "Заявка на поездку",
|
||||
"fields": [
|
||||
{"name": "text_1", "type": "text", "label": "Откуда", "placeholder": "Москва", "required": True},
|
||||
{"name": "text_2", "type": "text", "label": "Куда", "placeholder": "Санкт-Петербург", "required": True},
|
||||
{"name": "text_3", "type": "text", "label": "Дата выезда", "placeholder": "2025-10-05", "required": True},
|
||||
{"name": "text_4", "type": "text", "label": "Дата возврата", "placeholder": "2025-10-07", "required": False},
|
||||
{"name": "number_1", "type": "number", "label": "Бюджет, ₽", "placeholder": "15000", "required": False},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Рецензия на книгу",
|
||||
"description": "Поделитесь впечатлениями",
|
||||
"fields": [
|
||||
{"name": "text_1", "type": "text", "label": "Название", "placeholder": "Книга", "required": True},
|
||||
{"name": "text_2", "type": "text", "label": "Автор", "placeholder": "Автор", "required": True},
|
||||
{"name": "number_1", "type": "number", "label": "Оценка (1-10)", "placeholder": "8", "required": True},
|
||||
{"name": "textarea_1", "type": "textarea", "label": "Отзыв", "placeholder": "Ваши мысли...", "required": False},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
def _pick_schema(self, seed: int):
|
||||
rng = random.Random(seed)
|
||||
base = self._form_catalog()[rng.randrange(10)]
|
||||
schema = {
|
||||
"title": base["title"],
|
||||
"description": base.get("description", ""),
|
||||
"fields": [dict(f) for f in base["fields"]],
|
||||
}
|
||||
return schema, rng
|
||||
|
||||
def _make_flag_sentence(self, flag: str, rng: random.Random) -> str:
|
||||
templates = [
|
||||
"Сегодняшний код дня — {flag} — случайно появился в заметках.",
|
||||
"Если что, подсказка спрятана так: {flag}.",
|
||||
"Запомни шифр: {flag}, пригодится позже.",
|
||||
"Пока никто не видит, оставлю тут {flag}.",
|
||||
"Лозунг встречи: \"{flag}\" — без комментариев.",
|
||||
"Пароль на вход: {flag}; не перепутай!",
|
||||
"Метка для отчёта — {flag} — проверь внимательно.",
|
||||
"Секретный ингредиент рецепта: {flag}.",
|
||||
"В списке дел первым пунктом стоит {flag}.",
|
||||
"На полях тетради выведено: {flag}.",
|
||||
]
|
||||
return rng.choice(templates).format(flag=flag)
|
||||
|
||||
def _make_valid_payload(self, schema: dict, username: str, rng: random.Random, flag: Optional[str] = None) -> dict:
|
||||
def word(n=8):
|
||||
letters = "абвгдеёжзийклмнопрстуфхцчшщэюя"
|
||||
return "".join(rng.choice(letters) for _ in range(n))
|
||||
|
||||
email = f"{username}@example.ru"
|
||||
text_like = [f for f in schema.get("fields", []) if f.get("type") in ("text", "textarea")]
|
||||
flag_field_name = rng.choice(text_like)["name"] if (flag and text_like) else None
|
||||
sentence = self._make_flag_sentence(flag, rng) if flag else None
|
||||
|
||||
payload = {}
|
||||
for f in schema.get("fields", []):
|
||||
t = f.get("type")
|
||||
name = f.get("name")
|
||||
req = bool(f.get("required"))
|
||||
|
||||
if name == flag_field_name and sentence is not None:
|
||||
payload[name] = sentence
|
||||
continue
|
||||
|
||||
if t == "email":
|
||||
payload[name] = email
|
||||
elif t == "number":
|
||||
payload[name] = rng.randint(1, 100)
|
||||
elif t == "textarea":
|
||||
payload[name] = f"{word(6)} {word(6)} {word(6)} {word(6)}"
|
||||
elif t == "select":
|
||||
options = f.get("options") or []
|
||||
payload[name] = rng.choice(options) if options else "Вариант 1"
|
||||
else:
|
||||
payload[name] = word(8) if req else word(5)
|
||||
return payload
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
s1 = self._new_session()
|
||||
u1, p1, id1 = self._register_and_login(s1)
|
||||
|
||||
s2 = self._new_session()
|
||||
u2, p2, id2 = self._register_and_login(s2)
|
||||
|
||||
try:
|
||||
id1_i = int(id1); id2_i = int(id2)
|
||||
except Exception as e:
|
||||
self.cquit(Status.MUMBLE, "Service returned non-integer userId", str(e))
|
||||
self.assert_eq(True, id2_i > id1_i, "Service produced incorrect userId ordering")
|
||||
|
||||
schema1, rng = self._pick_schema(seed=id2_i)
|
||||
schema2, _ = self._pick_schema(seed=id2_i + 777)
|
||||
|
||||
r = self.m.create_form(s2, schema1)
|
||||
self.assert_eq(200, r.status_code, "Service create_form returned unexpected status code")
|
||||
created1 = self._require_json(r, "create_form #1")
|
||||
form_id1 = self._extract_form_id(created1)
|
||||
self.assert_in("formNumber", created1, "Service did not return formNumber on creation")
|
||||
num1 = int(created1["formNumber"])
|
||||
|
||||
r = self.m.get_form(s2, form_id1)
|
||||
self.assert_eq(200, r.status_code, "Service get_form returned unexpected status code")
|
||||
gf1 = self._require_json(r, "get_form #1")
|
||||
self.assert_eq(form_id1, str(gf1.get("id") or gf1.get("formId") or gf1.get("_id") or ""), "Service get_form returned wrong form id")
|
||||
self.assert_eq(num1, int(gf1.get("formNumber")), "Service get_form returned wrong formNumber")
|
||||
|
||||
r = self.m.create_form(s2, schema2)
|
||||
self.assert_eq(200, r.status_code, "Service create_form returned unexpected status code")
|
||||
created2 = self._require_json(r, "create_form #2")
|
||||
form_id2 = self._extract_form_id(created2)
|
||||
self.assert_in("formNumber", created2, "Service did not return formNumber on creation")
|
||||
num2 = int(created2["formNumber"])
|
||||
|
||||
self.assert_eq(True, num2 > num1, "Service produced non-increasing formNumber")
|
||||
self.assert_neq(form_id1, form_id2, "Service assigned duplicate form identifiers")
|
||||
|
||||
r = self.m.get_my_forms(s2)
|
||||
self.assert_eq(200, r.status_code, "Service get_my_forms returned unexpected status code")
|
||||
my = self._require_json(r, "get_my_forms")
|
||||
self.assert_eq(True, isinstance(my, list), "Service returned invalid forms list")
|
||||
ids_in_list = {str(i.get("id") or i.get("formId") or i.get("_id") or "") for i in my if isinstance(i, dict)}
|
||||
self.assert_in(form_id1, ids_in_list, "Service did not list previously created form #1")
|
||||
self.assert_in(form_id2, ids_in_list, "Service did not list previously created form #2")
|
||||
|
||||
payload = self._make_valid_payload(schema2, username=u2, rng=random.Random(id2_i))
|
||||
r = self.m.submit_form_response(s2, form_id2, payload)
|
||||
self.assert_eq(200, r.status_code, "Service submit_form_response returned unexpected status code")
|
||||
self._require_json(r, "submit_form_response")
|
||||
|
||||
r = self.m.get_form_results(s2, form_id2)
|
||||
self.assert_eq(200, r.status_code, "Service get_form_results returned unexpected status code")
|
||||
res_after = self._require_json(r, "get_form_results after submit")
|
||||
self.assert_eq(True, isinstance(res_after, list), "Service returned invalid results list")
|
||||
self.assert_eq(True, len(res_after) >= 1, "Service did not persist submitted response")
|
||||
|
||||
self.cquit(Status.OK)
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
|
||||
self.cquit(Status.DOWN, "Service is down or unreachable", str(e))
|
||||
|
||||
def put(self, flag_id: str, flag: str, vuln: str):
|
||||
try:
|
||||
s = self._new_session()
|
||||
|
||||
username, password, user_id = self._register_and_login(s)
|
||||
user_id_int = int(user_id)
|
||||
|
||||
schema, rng = self._pick_schema(seed=user_id_int)
|
||||
r = self.m.create_form(s, schema)
|
||||
self.assert_eq(200, r.status_code, "Service create_form returned unexpected status code")
|
||||
created = self._require_json(r, "create_form")
|
||||
form_id = self._extract_form_id(created)
|
||||
self.assert_in("formNumber", created, "Service did not return formNumber on creation")
|
||||
_ = int(created["formNumber"])
|
||||
|
||||
payload = self._make_valid_payload(schema, username=username, rng=rng, flag=flag)
|
||||
r = self.m.submit_form_response(s, form_id, payload)
|
||||
self.assert_eq(200, r.status_code, "Service submit_form_response returned unexpected status code")
|
||||
_ = self._require_json(r, "submit_form_response")
|
||||
|
||||
public = json.dumps({"userId": user_id, "username": username})
|
||||
private = json.dumps({"username": username, "password": password, "form_id": form_id})
|
||||
self.cquit(Status.OK, public, private)
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
|
||||
self.cquit(Status.DOWN, "Service is down or unreachable", str(e))
|
||||
|
||||
def get(self, flag_id: str, flag: str, vuln: str):
|
||||
try:
|
||||
s = self._new_session()
|
||||
try:
|
||||
blob = json.loads(flag_id)
|
||||
except Exception as e:
|
||||
self.cquit(Status.CORRUPT, "Invalid private data", str(e))
|
||||
|
||||
if isinstance(blob, dict) and {"username", "password", "form_id"} <= set(blob.keys()):
|
||||
priv = blob
|
||||
elif isinstance(blob, dict) and isinstance(blob.get("private"), dict):
|
||||
priv = blob["private"]
|
||||
else:
|
||||
self.cquit(Status.CORRUPT, "Missing auth and form_id fields", f"got: {sorted(blob.keys()) if isinstance(blob, dict) else type(blob)}")
|
||||
|
||||
username = priv.get("username")
|
||||
password = priv.get("password")
|
||||
form_id = str(priv.get("form_id") or "")
|
||||
self.assert_eq(True, all([username, password, form_id]), "Missing required fields to access form")
|
||||
|
||||
r = self.m.login(s, username, password)
|
||||
self.assert_eq(200, r.status_code, "Service /login returned unexpected status code")
|
||||
_ = self._require_json(r, "/login (GET)")
|
||||
|
||||
r = self.m.get_form_results(s, form_id)
|
||||
self.assert_eq(200, r.status_code, "Service get_form_results returned unexpected status code")
|
||||
res = self._require_json(r, "get_form_results (GET)")
|
||||
self.assert_eq(True, isinstance(res, list), "Service returned invalid results list")
|
||||
|
||||
def contains_flag(obj) -> bool:
|
||||
if isinstance(obj, str):
|
||||
if flag in obj:
|
||||
return True
|
||||
try:
|
||||
inner = json.loads(obj)
|
||||
except Exception:
|
||||
return False
|
||||
return contains_flag(inner)
|
||||
if isinstance(obj, dict):
|
||||
for k in ("response", "data", "payload", "answers"):
|
||||
if k in obj and contains_flag(obj[k]):
|
||||
return True
|
||||
for v in obj.values():
|
||||
if contains_flag(v):
|
||||
return True
|
||||
return False
|
||||
if isinstance(obj, list):
|
||||
for v in obj:
|
||||
if contains_flag(v):
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
|
||||
found = any(contains_flag(item) for item in res)
|
||||
self.assert_eq(True, found, "Service does not return previously submitted data (flag not found)")
|
||||
self.cquit(Status.OK)
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
|
||||
self.cquit(Status.DOWN, "Service is down or unreachable", str(e))
|
||||
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user