Files
validator/OmCTF-2025/checkers/jform/checker.py

371 lines
20 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)