7.5 KiB
Runner Ashfall
В runner_ashfall_case.zip собраны следы инцидента в CI/CD: комментарий к issue, действия агента, установка npm-пакета, облачная авторизация и деплой. Первые девять ответов восстанавливаем по журналам. Из них получаем ключ к финальной заметке с десятым флагом.
Решение
Распаковываем архив. Полезные артефакты удобно разделить по участкам цепочки:
| Каталог | Содержимое |
|---|---|
github/, agent/ |
Issue, вызовы инструментов, CI run и attestation |
repo/, npm/ |
Lockfile до и после, индекс кеша и копия preinstall.js |
runner/ |
События процессов и файлов |
cloud/ |
OIDC-токены, CloudTrail и закрывающая заметка |
registry/, kubernetes/ |
События образов и фактическое обновление deployment |
Комментарий и запуск CI (Q01–Q03)
В github/audit_log.jsonl ищем issue_comment.created с меткой release-triage и объектом agent_route. Его source_key — issue-1842-comment-7f31, первый ответ. Похожий комментарий с меткой docs дальше в эту цепочку не попадает.
В agent/triage_assistant.jsonl ищем событие tool_call, у которого input.from_issue совпадает с найденным source_key. У вызова deploy_plan.render читаем tool_call_id, равный tool.deploy_plan.render:6d91c0. Это второй ответ.
В github/actions/ ищем запуск, у которого source.tool_call_id совпадает с найденным вызовом. Подходит run_8716443.json, из которого берём tuple = run-8716443/check-314159265. С найденными идентификаторами запуска сверяем остальные события.
Пакет, кеш и раннер (Q04–Q06)
Сравниваем repo/package-lock.before.json и repo/package-lock.after.json. В новом lockfile выделяется зависимость с hasInstallScript = true: @northstar/ledger-seal@2.8.4-ash.3. Флаг hasInstallScript указывает на скрипт установки. Его имя проверяем по событиям раннера.
В npm/_cacache/index-v5/ledger-seal-entry.json сверяем имя и версию пакета, затем читаем cache_path. Получаем путь к содержимому кеша — ответ Q05.
По этому пути ищем процесс в runner/process_events.jsonl. У него должны совпасть run_id=8716443 и check_run_id=314159265, а команда содержит node ... preinstall.js. Поле host даёт ghr-prod-x64-ephemeral-0c8a7d, ответ Q06. Другой ephemeral-раннер относится к старому запуску и сюда не подходит.
Облачные права и деплой (Q07–Q09)
Из jobs[0].job_id в JSON запуска берём 9317. В cloud/oidc_tokens.jsonl выбираем токен по тройке run/check/job и сохраняем его sub целиком. Так получаем Q07 и связываем облачную авторизацию с конкретной работой CI.
В cloud/cloudtrail.jsonl находим PutObject, у которого x-amz-meta-run-id и x-amz-meta-check-run-id совпадают с исследуемым запуском. responseElements.object_locator содержит bucket, key и версию объекта — это Q08.
Остаётся выяснить, что действительно развернули. В kubernetes/audit.jsonl ищем patch deployment staging/recon-ledger. Аннотация northstar.io/source-object должна точно совпасть с Q08. Из requestObject.spec.template.spec.containers[0].image берём полный адрес образа с digest, ответ Q09.
Digest в attestation описывает результат сборки до подмены артефакта. Для ответа о деплое нужен образ из Kubernetes patch, связанный с конкретной версией объекта.
Финальная заметка (Q10)
cloud/final_closeout.note содержит nonce, ciphertext и mac. nonce и ciphertext переводим из hex в байты, mac оставляем hex-строкой. Ответы Q01–Q09 записываем строками в список raw_answers без оболочки caplag{...}, сохраняя порядок. Для ключа соединяем их разделителем |.
import hashlib
key = hashlib.sha256("|".join(raw_answers).encode()).digest()
expected_mac = hashlib.sha256(key + nonce + ciphertext).hexdigest()
assert expected_mac == mac
Сначала сверяем expected_mac с mac: ошибка хотя бы в одном ответе даст несовпадение. Затем получаем seed потока и его блоки:
seed = hashlib.sha256(key + nonce).digest()
stream = bytearray()
counter = 0
while len(stream) < len(ciphertext):
stream.extend(hashlib.sha256(seed + counter.to_bytes(4, "big")).digest())
counter += 1
plain = bytes(a ^ b for a, b in zip(ciphertext, stream))
Счётчик начинается с нуля. zip ограничивает XOR длиной шифротекста. Декодируем plain как UTF-8 и разбираем JSON, поле final_flag содержит последний ответ.
Получилась сквозная цепочка событий. Текст issue запустил инструмент агента, тот создал CI run, установка зависимости привела к действиям на раннере, а связанные облачные события — к подменённому деплою. Точные идентификаторы на каждом переходе позволяют отделить её от ложных следов.
Все этапы
| Этап | Флаг |
|---|---|
| 1 | caplag{issue-1842-comment-7f31} |
| 2 | caplag{tool.deploy_plan.render:6d91c0} |
| 3 | caplag{run-8716443/check-314159265} |
| 4 | caplag{@northstar/ledger-seal@2.8.4-ash.3} |
| 5 | caplag{sha512/6a/2d/6a2dc1e9b4f0d776a8e2b8fd47025703e617b1d9a61b88d5ad7b2c9f2e6a10f4} |
| 6 | caplag{ghr-prod-x64-ephemeral-0c8a7d} |
| 7 | caplag{repo:northstar-clearing/recon-ledger:ref:refs/heads/staging:workflow:release.yml:job_id:9317} |
| 8 | caplag{s3://nstar-stage-artifacts/releases/recon-ledger/2026.05.14/recon-ledger.tgz#v7d4b19e2} |
| 9 | caplag{registry.local/northstar/recon-ledger@sha256:9c2fd7f0d2b541e0b44267cc8d59a7cf508b1e3a7e422bcf1ac8a2d1377f1cc4} |
| 10 | caplag{runner_ashfall_ci_blast_radius} |