Init. Commit
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
<h1 align="center">Admission Drift</h1>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/category-Forensic-blueviolet" alt="Forensic"/>
|
||||
<img src="https://img.shields.io/badge/difficulty-medium-orange" alt="medium"/>
|
||||
</p>
|
||||
|
||||
В архиве `admission_drift_case.zip` находятся материалы инцидента с admission-контроллером ingress-nginx. Нужно восстановить последовательность событий от входящего AdmissionReview до чтения секрета. Флаг состоит из четырёх частей, которые находим по журналам.
|
||||
|
||||
## Решение
|
||||
|
||||
Проверяем переходы по файлам архива.
|
||||
|
||||
| Артефакт | Что подтверждает |
|
||||
|---|---|
|
||||
| `network/admission_service_access.jsonl` | Источник запроса и `review_uid` |
|
||||
| `admission/reviews/review-*.json` | Тело AdmissionReview |
|
||||
| `ingress-nginx/controller.log` | Обработку запроса контроллером |
|
||||
| `ingress-nginx/config_snapshots.tar` | Сгенерированный `nginx.conf` |
|
||||
| `runtime/falco_events.jsonl` | Фактическое обращение к файлу |
|
||||
| `rbac/secret_access.jsonl` | Успешное чтение секрета |
|
||||
|
||||
Начинаем с access log. Ищем `POST`, у которого `source_category == "workload-pod"` и `apiserver_proxy == false`. Запрос пришёл прямо от workload-пода, минуя API server. Из записи берём `review_uid`, начинающийся с `84f1d2`, и путь `review_file`.
|
||||
|
||||
Открываем указанный JSON и сверяем `request.uid`. В аннотациях объекта находим `validation-template`: именно она запускает обработку шаблона. Тот же UID встречается в логе контроллера.
|
||||
|
||||
Для поиска снимка берём первые шесть символов UID, то есть `84f1d2`. В `ingress-nginx/config_snapshots.tar` читаем соответствующий `snapshots/review-84f1d2/nginx.conf`. В конфиге есть путь вида `/proc/<pid>/fd/<n>` — обращение к файловому дескриптору процесса. Одного конфига мало, поэтому ищем в Falco событие с тем же `review_uid` и точно совпадающим `fd.name`. Из поля `marker` совпавшего события получаем `procfd`.
|
||||
|
||||
Переходим к журналу секретов. Выбираем запись с тем же UID, `verb=get`, `resource=secrets`, `response_code=200`. Хвост имени прочитанного секрета — `7c19`.
|
||||
|
||||
Собираем нормализованные компоненты:
|
||||
|
||||
| Компонент | Значение |
|
||||
|---|---|
|
||||
| Тип потока | `ad` |
|
||||
| Первые шесть символов UID | `84f1d2` |
|
||||
| Маркер из runtime-события | `procfd` |
|
||||
| Хвост имени секрета | `7c19` |
|
||||
|
||||
Соединяем их через `_`. Совпадение UID и пути дескриптора на промежуточных шагах подтверждает, что запрос, конфиг, runtime-событие и чтение секрета относятся к одному инциденту.
|
||||
|
||||
[Солвер](solve/solve.py).
|
||||
|
||||
## Флаг
|
||||
|
||||
`caplag{ad_84f1d2_procfd_7c19}`
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import tarfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def read_text(zf: zipfile.ZipFile, name: str) -> str:
|
||||
return zf.read(name).decode("utf-8")
|
||||
|
||||
|
||||
def read_json(zf: zipfile.ZipFile, name: str) -> Any:
|
||||
return json.loads(read_text(zf, name))
|
||||
|
||||
|
||||
def read_jsonl(zf: zipfile.ZipFile, name: str) -> list[dict[str, Any]]:
|
||||
rows = []
|
||||
for line in read_text(zf, name).splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
rows.append(json.loads(line))
|
||||
return rows
|
||||
|
||||
|
||||
def extract_snapshot_text(zf: zipfile.ZipFile, review_short: str) -> str:
|
||||
blob = zf.read("ingress-nginx/config_snapshots.tar")
|
||||
with tarfile.open(fileobj=io.BytesIO(blob), mode="r") as tf:
|
||||
member = tf.extractfile(f"snapshots/review-{review_short}/nginx.conf")
|
||||
if member is None:
|
||||
raise ValueError("missing matching nginx config snapshot")
|
||||
return member.read().decode("utf-8")
|
||||
|
||||
|
||||
def recover_components(bundle_path: str | Path) -> dict[str, str]:
|
||||
with zipfile.ZipFile(bundle_path) as zf:
|
||||
access_rows = read_jsonl(zf, "network/admission_service_access.jsonl")
|
||||
direct = next(
|
||||
row
|
||||
for row in access_rows
|
||||
if row.get("method") == "POST"
|
||||
and row.get("source_category") == "workload-pod"
|
||||
and not row.get("apiserver_proxy")
|
||||
)
|
||||
review_uid = direct["review_uid"]
|
||||
review_short = review_uid.split("-", 1)[0][:6]
|
||||
|
||||
review = read_json(zf, direct["review_file"])
|
||||
if review["request"]["uid"] != review_uid:
|
||||
raise ValueError("AdmissionReview file does not match access log UID")
|
||||
annotations = review["request"]["object"]["metadata"].get("annotations", {})
|
||||
if "validation-template" not in " ".join(annotations):
|
||||
raise ValueError("matched review lacks validation-template annotation")
|
||||
|
||||
controller_log = read_text(zf, "ingress-nginx/controller.log")
|
||||
if review_uid not in controller_log:
|
||||
raise ValueError("review UID missing from controller log")
|
||||
|
||||
snapshot = extract_snapshot_text(zf, review_short)
|
||||
proc_fd_match = re.search(r"/proc/\d+/fd/\d+", snapshot)
|
||||
if not proc_fd_match:
|
||||
raise ValueError("matching snapshot does not contain a proc fd marker")
|
||||
|
||||
falco_rows = read_jsonl(zf, "runtime/falco_events.jsonl")
|
||||
runtime = next(
|
||||
row
|
||||
for row in falco_rows
|
||||
if row.get("review_uid") == review_uid and row.get("fd.name") == proc_fd_match.group(0)
|
||||
)
|
||||
|
||||
secret_rows = read_jsonl(zf, "rbac/secret_access.jsonl")
|
||||
secret = next(
|
||||
row
|
||||
for row in secret_rows
|
||||
if row.get("review_uid") == review_uid
|
||||
and row.get("verb") == "get"
|
||||
and row.get("resource") == "secrets"
|
||||
and row.get("response_code") == 200
|
||||
)
|
||||
|
||||
return {
|
||||
"flow": "ad",
|
||||
"review_short": review_short,
|
||||
"fd_marker": runtime["marker"],
|
||||
"secret_tail": secret["name"].rsplit("-", 1)[-1],
|
||||
}
|
||||
|
||||
|
||||
def solve_bundle(bundle_path: str | Path) -> str:
|
||||
components = recover_components(bundle_path)
|
||||
return "caplag{" + "_".join(
|
||||
[
|
||||
components["flow"],
|
||||
components["review_short"],
|
||||
components["fd_marker"],
|
||||
components["secret_tail"],
|
||||
]
|
||||
) + "}"
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if len(argv) != 2:
|
||||
print(f"usage: {Path(argv[0]).name} public/admission_drift_case.zip", file=sys.stderr)
|
||||
return 2
|
||||
print(solve_bundle(argv[1]))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv))
|
||||
Reference in New Issue
Block a user