Files
2026-09-17 00:50:07 +03:00

7.5 KiB
Raw Permalink Blame History

Runner Ashfall

Forensic hard

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