Init. commit

This commit is contained in:
Caplag
2026-04-22 10:42:16 +03:00
commit 98e51ca58b
35 changed files with 2371 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
<h1 align="center">Пропавший коллега</h1>
<p align="center">
<img src="https://img.shields.io/badge/category-Forensic-blueviolet" alt="Forensic"/>
<img src="https://img.shields.io/badge/points-916-orange" alt="916 pts"/>
</p>
В руках — пачка артефактов сотрудника компании NordTech:
| Артефакт | Что содержит |
|---|---|
| `resume.pdf` | Резюме, ID сотрудника `NT-3893` |
| `business_card.png` | Визитка с ИНН |
| `commits.log` | Лог коммитов внутреннего репозитория |
| `profile_photo.jpg` | Фото с EXIF |
| `postal_codes.csv` | База почтовых индексов с координатами |
| `repo_snapshot.txt` | Снимок внешнего репозитория |
| `browser_render.png` | Скриншот из браузера |
Флаг собирается из четырёх частей, и каждая спрятана в своей цепочке.
## Решение
**Часть 1 — `br34d`.** В `resume.pdf` в профиле сотрудника указан ID `NT-3893`. В `business_card.png` — ИНН `7707083893`, и последние четыре цифры совпадают с ID. Сотрудник привязан к NordTech. Лезем в `commits.log`:
```text
feat(NT-3893): integrate module-br34d
```
Регуляркой `feat\(NT-3893\): integrate module-(\w+)` выдёргиваем кодовое слово — `br34d`.
**Часть 2 — `crumbs`.** Из EXIF `profile_photo.jpg` вытаскиваем GPS, конвертируем DMS в десятичные:
```text
55.7616 N, 37.6385 E → район Чистопрудного бульвара, Москва
```
Идём в `postal_codes.csv` и ищем ближайшую точку по манхэттенскому расстоянию. Находится запись:
```text
postal_code = 101000
sector_code = 6372756d6273
```
Декодируем hex → ASCII:
```text
63 72 75 6d 62 73 → c r u m b s
```
Вторая часть — `crumbs`.
**Часть 3 — `l34d` .** В `commits.log` кроме «нашего» коммита торчит отсылка на внешний репо вида `See commit <sha> in <repo>`. Вытаскиваем короткий SHA (7 символов) и имя репо регуляркой `See commit\s+(\w+)\s+in\s+([\w\-\.\/]+)`. Идём в `repo_snapshot.txt` и находим блок именно этого коммита (от нашего SHA до следующего полного 40-символьного). Внутри блока ищем base64-строки длиной от 20 символов — одна декодируется в:
```text
module-l34d-integration-v2.1.0
```
Первый сегмент после `module-` до дефиса — `l34d`.
**Часть 4 — `h0m3`.** Открываем картинку в RGBA через PIL, разворачиваем красный канал в одномерный массив, идём по пикселям и забираем младшие биты. Каждые 8 подряд складываются в байт (MSB первым). Нулевой байт — маркер конца. *Есть один мелкий нюанс*: сырой вывод начинается с трассировочного префикса вида `[N/4]`его срезаем регуляркой `\[\d/\d\](.*)`. Остаётся `h0m3`.
Склеиваем через `_`:
```text
br34d + crumbs + l34d + h0m3 = br34d_crumbs_l34d_h0m3
```
Готовый солвер — [`solve/solver.py`](solve/solver.py).
## Флаг
`caplag{br34d_crumbs_l34d_h0m3}`

View File

@@ -0,0 +1,204 @@
#!/usr/bin/env python3
import base64
import csv
import re
import os
from pathlib import Path
PUBLIC_DIR = Path(__file__).resolve().parent.parent / "public"
def extract_part1_br34d():
"""
Часть 1: 'br34d'
Цепочка: resume.pdf -> ID сотрудника 'NT-3893'
business_card.png -> ИНН 7707083893 (последние 4 цифры = 3893, подтверждает связь с NordTech)
commits.log -> тикет NT-3893 ссылается на 'module-br34d'
"""
print("[Шаг 1] Извлечение части 1: br34d")
employee_id = "NT-3893"
print(f" [1a] resume.pdf — ID сотрудника: {employee_id}")
print(" [1b] business_card.png — ИНН: 7707083893 (последние 4 цифры = 3893, совпадает)")
commits = (PUBLIC_DIR / "commits.log").read_text()
# Ищем коммит вида: feat(NT-3893): integrate module-<название>
pattern = rf"feat\({employee_id}\): integrate (module-\w+)"
match = re.search(pattern, commits)
if match:
module_name = match.group(1)
part1 = module_name.replace("module-", "") # Убираем префикс, оставляем только кодовое слово
print(f" [1c] commits.log: '{match.group(0)}'")
print(f" Часть 1: '{part1}'")
return part1
return None
def extract_part2_crumbs():
"""
Часть 2: 'crumbs' (метод второго раунда)
Цепочка: profile_photo.jpg GPS -> 55.7616N, 37.6385E
postal_codes.csv -> находим почтовый индекс 101000, соответствующий этим координатам
столбец sector_code = '6372756d6273' -> hex в ASCII = 'crumbs'
"""
print("\n[Шаг 2] Извлечение части 2: crumbs (через поиск по почтовому индексу)")
# Шаг 2a: Извлекаем GPS-координаты из EXIF-данных фотографии
import piexif
img_path = str(PUBLIC_DIR / "profile_photo.jpg")
exif = piexif.load(img_path)
def parse_gps_coord(coord_data, ref):
"""Конвертируем GPS из формата DMS (градусы/минуты/секунды) в десятичные градусы."""
degrees = coord_data[0][0] / coord_data[0][1]
minutes = coord_data[1][0] / coord_data[1][1]
seconds = coord_data[2][0] / coord_data[2][1]
result = degrees + minutes / 60 + seconds / 3600
if ref in [b'S', b'W']: # Южная широта и западная долгота — отрицательные
result = -result
return result
gps = exif.get("GPS", {})
lat = parse_gps_coord(gps[piexif.GPSIFD.GPSLatitude], gps[piexif.GPSIFD.GPSLatitudeRef])
lon = parse_gps_coord(gps[piexif.GPSIFD.GPSLongitude], gps[piexif.GPSIFD.GPSLongitudeRef])
print(f" [2a] GPS: {lat:.4f}N, {lon:.4f}E (район Чистопрудного бульвара)")
# Шаг 2b: Ищем ближайшую точку в таблице почтовых индексов по манхэттенскому расстоянию
csv_path = PUBLIC_DIR / "postal_codes.csv"
best_match = None
best_dist = float('inf')
with open(csv_path) as f:
reader = csv.DictReader(f)
for row in reader:
rlat = float(row['latitude'])
rlon = float(row['longitude'])
# Манхэттенское расстояние — достаточно для грубого геопоиска
dist = abs(rlat - lat) + abs(rlon - lon)
if dist < best_dist:
best_dist = dist
best_match = row
print(f" [2b] Ближайший индекс: {best_match['postal_code']} ({best_match['district']}, dist={best_dist:.4f})")
print(f" sector_code: {best_match['sector_code']}")
# Шаг 2c: Декодируем hex-строку sector_code в ASCII — это и есть часть флага
hex_str = best_match['sector_code']
decoded = bytes.fromhex(hex_str).decode('ascii')
print(f" [2c] hex -> ASCII: '{decoded}'")
return decoded
def extract_part3_l34d():
"""
Часть 3: 'l34d'
Цепочка: commits.log -> ссылка на коммит SHA 'a7c3e91' во внешнем репозитории
repo_snapshot.txt -> коммит a7c3e91 содержит base64-строку
base64-декодирование -> 'module-l34d-integration-v2.1.0'
"""
print("\n[Шаг 3] Извлечение части 3: l34d")
commits = (PUBLIC_DIR / "commits.log").read_text()
# Ищем отсылку к внешнему репозиторию вида: "See commit <sha> in <repo>"
ref_match = re.search(r"See commit\s+(\w+)\s+in\s+([\w\-\.\/]+)", commits, re.DOTALL)
if not ref_match:
return None
sha_prefix = ref_match.group(1) # Короткий SHA (7 символов)
repo = ref_match.group(2)
print(f" [3a] commits.log ссылается на {sha_prefix} в {repo}")
snapshot = (PUBLIC_DIR / "repo_snapshot.txt").read_text()
# Находим весь блок коммита — от нашего SHA до следующего коммита
commit_pattern = rf"commit {sha_prefix}\w*\n.*?(?=\ncommit [a-f0-9]{{40}}|\Z)"
commit_match = re.search(commit_pattern, snapshot, re.DOTALL)
if not commit_match:
return None
commit_block = commit_match.group(0)
# Ищем все потенциальные base64-строки длиной от 20 символов
b64_pattern = r'[A-Za-z0-9+/]{20,}={0,2}'
b64_matches = re.findall(b64_pattern, commit_block)
for b64_str in b64_matches:
try:
decoded = base64.b64decode(b64_str).decode("utf-8", errors="ignore")
if "module-" in decoded:
# Из строки вида "module-l34d-integration-v2.1.0" берём только кодовое слово
mod_match = re.search(r"module-(\w+)", decoded)
if mod_match:
part3 = mod_match.group(1).split("-")[0] # Только первый сегмент до дефиса
print(f" [3b] base64: {b64_str}")
print(f" decoded: {decoded}")
print(f" Часть 3: '{part3}'")
return part3
except Exception:
continue
return None
def extract_part4_h0m3():
"""
Часть 4: 'h0m3' (метод второго раунда)
Цепочка: browser_render.png -> LSB-стеганография в красном канале
Извлекаем LSB из первых пикселей -> 'h0m3'
"""
print("\n[Шаг 4] Извлечение части 4: h0m3 (через LSB-стеганографию)")
from PIL import Image
import numpy as np
img = Image.open(str(PUBLIC_DIR / "browser_render.png"))
pixels = np.array(img)
# Разворачиваем красный канал (индекс 0) в одномерный массив
flat_r = pixels[:, :, 0].flatten()
# Читаем байты: каждые 8 последовательных LSB пикселей = 1 символ
result = bytearray()
for byte_idx in range(100): # Ограничение на 100 байт во избежание зависания
byte_val = 0
for bit_idx in range(8):
pixel_idx = byte_idx * 8 + bit_idx
byte_val = (byte_val << 1) | (flat_r[pixel_idx] & 1)
if byte_val == 0:
break # Нулевой байт — признак конца скрытых данных
result.append(byte_val)
decoded = result.decode('utf-8', errors='replace')
print(f" [4a] LSB из красного канала (сырые байты): {result}")
print(f" [4b] Декодировано: '{decoded}'")
# Данные могут начинаться с маркера трассировки вида [N/4] — удаляем его
import re
m = re.match(r'\[\d/\d\](.*)', decoded)
if m:
decoded = m.group(1)
print(f" [4c] После удаления маркера трассировки: '{decoded}'")
return decoded
def main():
print("=" * 60)
print("OSINT-задание «Пропавший коллега» — Решение (R2)")
print("=" * 60)
print()
part1 = extract_part1_br34d()
part2 = extract_part2_crumbs()
part3 = extract_part3_l34d()
part4 = extract_part4_h0m3()
print("\n" + "=" * 60)
print("Сборка флага")
print("=" * 60)
print(f" Часть 1 (commits.log через NT-3893): {part1}")
print(f" Часть 2 (sector_code почтового индекса): {part2}")
print(f" Часть 3 (base64 из внешнего репозитория): {part3}")
print(f" Часть 4 (LSB-стего в browser_render.png): {part4}")
if __name__ == "__main__":
main()