В этой статье я немного расскажу о том, как в Java осуществляется вызов команд уровня операционной системы. Также поговорим про OS Command и Argument Injections и про то, как мы делали диагностики, которые могут помочь в их обнаружении.

Погружение
Приветствую читающих! В этой статье мы рассмотрим, какие в Java есть средства для вызова ОС команд и какими уязвимостями может быть чревато их неаккуратное использование. Вам будет представлена своеобразная выжимка информации, собранная в процессе создания диагностик для обнаружения вышеописанных потенциальных уязвимостей.
Эти диагностики были выбраны не просто так. Java-анализатор PVS-Studio взял курс на становление полноценным SAST-решением. Для этого мы реализуем диагностические правила, выявляющие потенциальные уязвимости. Всё это нам нужно для того, чтобы удовлетворять потребности клиентов, у которых высокие требования к безопасности разрабатываемого ПО.
OS Command Injection — это уязвимость, позволяющая злоумышленнику выполнять вредоносные команды уровня операционной системы на какой-либо машине. Возможно это тогда, когда команда уровня ОС частично или полностью формируется из данных, пришедших извне.
Уязвимость серьёзная: в классификации OWASP Top Ten она находится в категории A03 – Injections. Уметь превентивно находить такое — важно. О том, как мы сделали диагностики, помогающие в этом, расскажу далее.
Adventure time
Создание диагностики всегда похоже на небольшое приключение. Изучается предметная область (в данном случае, выполнение ОС команд через Java) и различные нюансы, которые могут усложнить создание диагностики. Всё это сопоставляется с возможностями анализатора на момент создания правила. И если в анализаторе не хватает какого-либо механизма, чтобы создать диагностику — нужно озаботиться этим.
И при создании диагностики, ищущей OS Command Injections, мы прошли абсолютно через всё, что я перечислил выше. Но давайте по порядку
Выполнение ОС команд в Java
В стандартной Java-библиотеке для выполнения ОС команд есть 2 класса. Рассмотрим оба.
Runtime
Первый — это Runtime
. Он позволяет Java-приложению взаимодействовать с окружением, в котором оно работает.
Для выполнения команды ОС нужно воспользоваться методом exec
, передав в него нужную команду_._ Давайте сразу взглянем на пример:
public class Main { public static void main(String[] args) throws IOException { Process process = Runtime.getRuntime().exec("ping 127.0.0.1"); try { System.out.println(getProcessOutput(process)); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } } private static String getProcessOutput(Process process) throws IOException, InterruptedException { try (InputStream stream = process.getInputStream(); BufferedInputStream inputStream = new BufferedInputStream(stream) ) { return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException e) { throw new RuntimeException(e); } } }
Здесь происходит запуск команды ping 127.0.0.1
и вывод output’а процесса в консоль. Предлагаю взглянуть на фрагмент вывода:
Pinging 127.0.0.1 with 32 bytes of data: Reply from 127.0.0.1: bytes=32 time<1ms TTL=128 Reply from 127.0.0.1: bytes=32 time<1ms TTL=128 ....
Всё работает — это отлично.
Если чуть более подробно: методу exec
передаётся строка, представляющая команду операционной системы и её аргументы. Этот метод запускает процесс ОС и возвращает объект типа Process
, который содержит информацию о нём.
А в getProcessOutput
я просто получаю вывод выполняемого процесса и преобразую его в строку.
Очень важный момент: метод exec
под капотом использует класс ProcessBuilder
. О нём мы поговорим дальше.
ProcessBuilder
ProcessBuilder
является вторым классом, который позволяет выполнять команды уровня ОС. Команду для исполнения можно задать через конструктор класса или с помощью метода command
. Чтобы запустить процесс, достаточно вызвать метод start
, который инициирует создание процесса и возвращает объект Process
, связанный с ним.
Пример использования:
public class Main { public static void main(String[] args) throws IOException { ProcessBuilder processBuilder = new ProcessBuilder("ping", "127.0.0.1"); Process process = processBuilder.start(); try { System.out.println(getProcessOutput(process)); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } } // .... }
Как вы можете заметить, в сравнении с предыдущим примером изменились только первые строчки в методе main
. В них создаётся объект, который запускает нужный нам процесс. Однако там и заключён один интересный момент. Конструктору ProcessBuilder
мы передаём команду с параметрами «по частям». Первым параметром идёт сама команда, следующими — её аргументы.
И именно на основе этой команды метод start
инициирует запускаемый процесс.
Ранее я упомянул, что у класса Runtime
метод exec
под капотом использует ProcessBuilder
. Какие из этого следуют нюансы, я расскажу далее.
А это безопасно?
После того, как я узнал об этих методах, я задался вопросом: «А можно ли добавить вредоносную команду к основной, если команда формируется на основе данных, пришедших извне?».
Пошёл пробовать:
public class Main { public static void main(String[] args) throws IOException { String notTaintedCommand = "ping "; String taintedData = args[0]; Process process = Runtime.getRuntime().exec(notTaintedCommand + taintedData); try { System.out.println(getProcessOutput(process)); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } } // .... }
Данными, пришедшими извне, здесь выступают элементы массива args
. В рамках гипотетического примера представим, что нам оттуда пришла строка 127.0.0.1 & notepad
.
Ожидается, что нам поступит ip-адрес, который мы попробуем пропинговать. Но злосчастные «данные извне» пытаются испортить нам всю картину. Подразумевается, что после выполнения этой команды сначала пропингуется какой-то ip адрес, а потом произойдёт «инъекция блокнотом». По крайней мере, выполни это в оболочке cmd
, и всё бы было именно так.
Но, как говорится, выкуси. Вывод будет следующим:
Bad parameter &.
Это сообщение выброшено самой утилитой ping
.
Всё дело в том, что «под капотом» метод exec
разбивает строку на массив строк по символам-разделителям. Этот массив передаётся в конструктор создаваемого объекта ProcessBuilder
. А сам ProcessBuilder
, как я уже говорил выше, создаёт процесс на основе команды, которая представляет из себя первый элемент массива.
То есть вызова оболочки (cmd
в Windows или bash
в Unix-системах) не происходит. Из-за этого метасимволы оболочки, такие как &
или |
, интерпретируются не так, как этого можно было бы ожидать. Они действительно передаются вызываемой программе как отдельные параметры. Из-за этого мы и увидели такое сообщение.
Складывается ощущение, что если команда формируется лишь частично на основе внешних данных, то и угроза не такая серьёзная, какой могла бы быть (ну, если конечно команда не rm
). Всегда ли это так? Чуть позже я отвечу на этот вопрос, оставайтесь на связи.
Время диагностики
Мы рассмотрели, как реализуется вызов ОС команд в Java. Теперь можно переходить к созданию диагностики.
Для нас изначально это казалось достаточно простой задачей. По сути, это должно было быть простой taint-диагностикой.
Если резюмировать вкратце, то:
-
есть источники, т.е. места, откуда внешние данные приходят;
-
есть сами непроверенные данные;
-
есть стоки, т.е. места, в которых непроверенные данные могут спровоцировать эксплуатацию уязвимости;
-
есть санитизация, т.е. процесс проверки / очистки внешних данных. Происходить она должна перед тем, как данные попадают в сток.
И у нашего анализатора есть механизм, который позволяет обнаружить ситуацию, когда непроверенные данные попали в стоки несанитизированными. Про его реализацию мы написали две технические статьи (тык и тык) и одну более общего характера (тык).
Сначала планировалось просто разметить источники, стоки и санитизирующие методы. Но преградой, не позволившей нам сразу это сделать, стали две разные возможные ситуации — Command Injection и Argument Injection.
Выше я говорил, что команда может формироваться на основе внешних данных полностью либо частично. Так вот, эти ситуации классифицируются как две разные потенциальные уязвимости:
-
Command Injection (CWE-78) — команда и аргументы полностью приходят извне.
-
Argument Injection (CWE-88) — команда задана изначально, извне приходят только её параметры.
Почему их разделили? Как мне кажется, уровень опасности у них довольно-таки разный. В случае с Command Injection мы, как гипотетические хакеры, можем управлять машиной. В случае же с Argument Injection всё зависит от обстоятельств.
И нам, как анализатору, нужно уметь различать эти ситуации для того, чтобы выдавать разные сообщения в зависимости от того, что конкретно произошло.
Если в случае с ProcessBuilder
всё достаточно просто — нам нужно посмотреть, в каком по счёту параметре находятся непроверенные данные. Если в первом, то это Command Injection, а если в любом другом, то это Argument Injection.
С методом exec
дела обстоят чуть сложнее — мы ведь в него передаём одну строку, в которой сразу и команда, и аргументы. Как мы поступили?
В целом, решение по своей задумке достаточно тривиальное. Мы смотрим, как формируется строка, передаваемая в exec
. Если она полностью пришла извне, то это Command Injection. Но если команда определена изначально, а строка, пришедшая извне, с ней конкатенируется, то мы воспринимаем это как Argument Injection.
Осилив эту сложность, диагностики мы сделали. И теперь можем ловить, к примеру, такое:
@RestController("/osInjection") public class OSCommandController { @GetMapping("/execute") public String execute(@RequestParam("command") String command) throws IOException { String taintedData = command; Process process = Runtime.getRuntime().exec(taintedData); try { return getProcessOutput(process); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } } // .... }
V5310 Possible command injection. Potentially tainted data in ‘taintedData’ variable is used to create OS command. OSCommandController.java 20
Или такое:
@RestController("/os_injection") public class OSCommandController { @GetMapping("/execute") public String execute(@RequestParam("argument") String argument) throws IOException { String notTaintedData = "ping"; String taintedData = argument; Process process = Runtime.getRuntime().exec(notTaintedData + " " + taintedData); try { return getProcessOutput(process); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } } // .... }
V5311 Possible argument injection. Potentially tainted data in ‘taintedData’ variable is used to create OS command. OSCommandController.java 21
Ура-ура!
Инъекция блокнотом
Теперь хочется обговорить тот момент, который я даже в название статьи вынес. Помните, я рассказывал выше, что процессы формируются напрямую, а не через оболочку? И, соответственно, конкатенировать к ней дополнительно вредоносную команду не то чтобы и возможно.
Да, изначально ситуация действительно обстоит так. Но не зря я вам выше говорил, что в случае Argument Injection серьёзность зависит от обстоятельств.
Что, если мы хотим, чтобы для вызова команд всё же использовалась оболочка ОС? Так сделать действительно можно. В случае с Windows, к примеру, достаточно, чтобы вызываемой командой была cmd
, а первым параметром — /c
. Тогда нужная нам команда выполнится через оболочку cmd
.
То есть, раз вызов происходит через оболочку, значит и конкатенирующими метасимволами оболочки можно будет воспользоваться? Давайте посмотрим:
public class Main { public static void main(String[] args) throws IOException { String notTaintedCommand = "cmd /c ping "; String taintedData = args[0]; Process process = Runtime.getRuntime().exec(notTaintedCommand + taintedData); try { System.out.println(getProcessOutput(process)); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } } // .... }
Представим, что в args[0]
лежит 127.0.0.1 & notepad
. Я запустил и угадайте, что произошло. Пропинговался нужный ip-адрес и… действительно открылся блокнот.
Этот случай демонстрирует, что Argument Injection не настолько безобидная уязвимость, как может показаться. Вызов оболочки и передача ей команды как аргумента — пример тех самых «обстоятельств», о которых я говорил выше.
И, казалось бы, эта ситуация нивелирует разницу между уровнями серьёзности Command и Argument уязвимостей. Тогда зачем нам две отдельные диагностики? Мы руководствовались следующим: вызов оболочки — лишь частный случай. Соответственно, степень серьёзности Argument Injection зависит от ситуации. А вот с Command Injection ситуация понятна всегда: её степень серьёзности высокая вне зависимости от обстоятельств. Отсюда и разделение на две отдельные диагностики.
А что, если не всё так просто?
Вы можете сказать, что приведённые выше примеры достаточно наивны. И вы будете правы. Ситуации действительно могут быть не настолько очевидными. Как минимум на уровне источников и стоков. Мы это понимаем и поэтому в ближайшее время в рамках Java анализатора будут поддержаны пользовательские аннотации. Что это такое?
По сути, это механизм, позволяющий «познакомить» анализатор с вашим кодом. Например, в контексте taint-диагностик вы можете сообщить анализатору, что будет являться источником, стоком или санитизирующим методом. Реализован он будет отдельным файлом, в котором пользователь сможет всё это разметить.
Безусловно, классы из стандартных или достаточно популярных библиотек мы размечаем сами. Но в проекте может использоваться собственная библиотека для, к примеру, получения данных извне. И возможность его соответствующим образом проаннотировать помогает анализатору более «гибко» подстроиться под проект клиента.
Заключение
С данными, пришедшими в программу извне, всегда нужно быть осторожным.
К слову, раз я упомянул осторожность, то не могу не затронуть понравившуюся мне рекомендацию OWASP. В своей заметке, касающейся этой уязвимости, их первый совет заключается в следующем: вызова ОС команд стоит избегать в принципе. Ведь как в Java, так и во множестве других языков, с большой вероятностью по умолчанию уже есть необходимые, более высокоуровневые инструменты для выполняемой задачи. Звучит довольно резонно. Я тоже склоняюсь к переиспользованию существующих решений, а не к изобретению новых.
Также не могу не сказать, что теперь в Java-анализаторе PVS-Studio с релиза 7.35 есть диагностики, которые помогут вам обнаружить вышеописанные потенциальные уязвимости. Если есть желание попробовать, то сделать это можно по ссылке.
А на этом буду с вами прощаться! До скорых встреч!
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Vladislav Bogdanov. Notepad injection or the story of writing new diagnostic rules.
ссылка на оригинал статьи https://habr.com/ru/articles/894872/
Добавить комментарий