﻿# Инъекция блокнотом или история о том, как мы новые диагностики делали

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

![1240_Notepade_Injection_ru/image1.png](https://import.viva64.com/docx/blog/1240_Notepade_Injection_ru/image1.png)

## Погружение

Приветствую читающих\! В этой статье мы рассмотрим, какие в Java есть средства для вызова ОС команд и какими уязвимостями может быть чревато их неаккуратное использование\. Вам будет представлена своеобразная выжимка информации, собранная в процессе создания диагностик для обнаружения вышеописанных потенциальных уязвимостей\.

Эти диагностики были выбраны не просто так\. Java\-анализатор PVS\-Studio взял курс на становление полноценным [SAST](https://pvs-studio.ru/ru/blog/terms/6631/)\-решением\. Для этого мы реализуем диагностические правила, выявляющие потенциальные уязвимости\. Всё это нам нужно для того, чтобы удовлетворять потребности клиентов, у которых высокие требования к безопасности разрабатываемого ПО\. 

OS Command Injection — это уязвимость, позволяющая злоумышленнику выполнять вредоносные команды уровня операционной системы на какой\-либо машине\. Возможно это тогда, когда команда уровня ОС частично или полностью формируется из данных, пришедших извне\.

Уязвимость серьёзная: в классификации OWASP Top Ten она находится в категории [A03 – Injections](https://owasp.org/Top10/A03_2021-Injection/)\. Уметь превентивно находить такое — важно\. О том, как мы сделали диагностики, помогающие в этом, расскажу далее\.

## Adventure time

Создание диагностики всегда похоже на небольшое приключение\. Изучается предметная область \(в данном случае, выполнение ОС команд через Java\) и различные нюансы, которые могут усложнить создание диагностики\. Всё это сопоставляется с возможностями анализатора на момент создания правила\. И если в анализаторе не хватает какого\-либо механизма, чтобы создать диагностику — нужно озаботиться этим\.

И при создании диагностики, ищущей OS Command Injections, мы прошли абсолютно через всё, что я перечислил выше\. Но давайте по порядку :\)

### Выполнение ОС команд в Java

В стандартной Java\-библиотеке для выполнения ОС команд есть 2 класса\. Рассмотрим оба\.

#### Runtime

Первый — это `Runtime`\. Он позволяет Java\-приложению взаимодействовать с окружением, в котором оно работает\. 

Для выполнения команды ОС нужно воспользоваться методом `exec`, передав в него нужную команду_\._ Давайте сразу взглянем на пример:

```cpp
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'а процесса в консоль\. Предлагаю взглянуть на фрагмент вывода:

```cpp
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`, связанный с ним\.

Пример использования:

```cpp
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`\. Какие из этого следуют нюансы, я расскажу далее\. 

### А это безопасно?

После того, как я узнал об этих методах, я задался вопросом: "А можно ли добавить вредоносную команду к основной, если команда формируется на основе данных, пришедших извне?"\.

Пошёл пробовать:

```cpp
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\-диагностикой\.

Если резюмировать вкратце, то:

* есть **источники**, т\.е\. места, откуда внешние данные приходят;
* есть сами **непроверенные** данные;
* есть **стоки**, т\.е\. места, в которых непроверенные данные могут спровоцировать эксплуатацию уязвимости;
* есть **санитизация**, т\.е\. процесс проверки / очистки внешних данных\. Происходить она должна перед тем, как данные попадают в сток\.

И у нашего анализатора есть механизм, который позволяет обнаружить ситуацию, когда непроверенные данные попали в стоки несанитизированными\. Про его реализацию мы написали две технические статьи \([тык](https://pvs-studio.ru/ru/blog/posts/java/1198/) и [тык](https://pvs-studio.ru/ru/blog/posts/java/1228/)\) и одну более общего характера \([тык](https://pvs-studio.ru/ru/blog/posts/java/1214/)\)\.

Сначала планировалось просто разметить источники, стоки и санитизирующие методы\. Но преградой, не позволившей нам сразу это сделать, стали две разные возможные ситуации — Command Injection и Argument Injection\.

Выше я говорил, что команда может формироваться на основе внешних данных полностью либо частично\. Так вот, эти ситуации классифицируются как две разные потенциальные уязвимости: 

* Command Injection \([CWE\-78](https://cwe.mitre.org/data/definitions/78.html)\) — команда и аргументы полностью приходят извне\.
* Argument Injection \([CWE\-88](https://cwe.mitre.org/data/definitions/88.html)\) — команда задана изначально, извне приходят только её параметры\.

Почему их разделили? Как мне кажется, уровень опасности у них довольно\-таки разный\. В случае с Command Injection мы, как гипотетические хакеры, можем управлять машиной\. В случае же с Argument Injection всё зависит от обстоятельств\.

И нам, как анализатору, нужно уметь различать эти ситуации для того, чтобы выдавать разные сообщения в зависимости от того, что конкретно произошло\.

Если в случае с `ProcessBuilder` всё достаточно просто — нам нужно посмотреть, в каком по счёту параметре находятся непроверенные данные\. Если в первом, то это Command Injection, а если в любом другом, то это Argument Injection\. 

С методом `exec` дела обстоят чуть сложнее — мы ведь в него передаём одну строку, в которой сразу и команда, и аргументы\. Как мы поступили?

В целом, решение по своей задумке достаточно тривиальное\. Мы смотрим, как формируется строка, передаваемая в `exec`\. Если она полностью пришла извне, то это Command Injection\. Но если команда определена изначально, а строка, пришедшая извне, с ней конкатенируется, то мы воспринимаем это как Argument Injection\. 

Осилив эту сложность, диагностики мы сделали\. И теперь можем ловить, к примеру, такое:

```cpp
@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](https://pvs-studio.ru/ru/docs/warnings/v5310/) Possible command injection\. Potentially tainted data in 'taintedData' variable is used to create OS command\. OSCommandController\.java 20

Или такое:

```cpp
@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](https://pvs-studio.ru/ru/docs/warnings/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`\.

То есть, раз вызов происходит через оболочку, значит и конкатенирующими метасимволами оболочки можно будет воспользоваться? Давайте посмотрим:

```cpp
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\-диагностик вы можете сообщить анализатору, что будет являться источником, стоком или санитизирующим методом\. Реализован он будет отдельным файлом, в котором пользователь сможет всё это разметить\.

Безусловно, классы из стандартных или достаточно популярных библиотек мы размечаем сами\. Но в проекте может использоваться собственная библиотека для, к примеру, получения данных извне\. И возможность его соответствующим образом проаннотировать помогает анализатору более "гибко" подстроиться под проект клиента\.

## Заключение 

С данными, пришедшими в программу извне, всегда нужно быть осторожным\.

К слову, раз я упомянул осторожность, то не могу не затронуть понравившуюся мне [рекомендацию](https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html) OWASP\. В своей заметке, касающейся этой уязвимости, их первый совет заключается в следующем: вызова ОС команд стоит избегать в принципе\. Ведь как в Java, так и во множестве других языков, с большой вероятностью по умолчанию уже есть необходимые, более высокоуровневые инструменты для выполняемой задачи\. Звучит довольно резонно\. Я тоже склоняюсь к переиспользованию существующих решений, а не к изобретению новых\.

Также не могу не сказать, что теперь в Java\-анализаторе PVS\-Studio с [релиза 7\.35](https://pvs-studio.ru/ru/blog/posts/1225/) есть диагностики, которые помогут вам обнаружить вышеописанные потенциальные уязвимости\. Если есть желание попробовать, то сделать это можно по [ссылке](https://pvs-studio.ru/ru/pvs-studio/try-free/)\.

А на этом буду с вами прощаться\! До скорых встреч\!