Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Язык программирования Python приобрел большую популярность среди разработчиков благодаря различным решениям, заложенным в его архитектуру. Такими решениями являются сильная динамическая типизация, то есть язык не допускает неявных приведений типов в неоднозначных ситуациях (например, сложение букв и цифр), при этом тип переменной определяется во время присваивания ей значения и может изменяться по ходу программы. Также полезной функцией языка Python автоматическое управление памятью и поддержка множества парадигм программирования, таких как структурное и функциональное программирование. И наконец, Python является полностью объектно-ориентированным языком с поддержкой интроспекции, то есть способности определять тип объекта во время выполнения.
Но, как всякий другой язык программирования, Python имеет ряд уязвимостей, о которых мы подробно поговорим в этой статье. Так как статья ориентирована прежде всего на разработчиков Python, то мы будем говорить об ошибках в коде, которые по тем или иным причинам могут допустить программисты в процессе разработки. И начнем мы с рассмотрения инъекций команд.
Такие разные инъекции
Прежде всего, начнем с определения. Инъекция команд — это вид атаки, целью которой является выполнение произвольных команд ОС сервера или произвольных SQL запросов в случае с СУБД.
Инъекция команд позволяет выполнить практически любую команду ОС с правами текущего пользователя. Так, командная строка shell, запущенная с помощью инъекции /bin/sh в приложение, работающее с правами root, будет работать также с правами привилегированного пользователя. Причина такого “странного” поведения кода кроется в некорректной обработке данных при динамической оценке интерпретатором выполняемого кода.
Еще более интересными вариантами инъекции команд является возможность предоставить удаленный доступ с правами пользователя, запустившего приложения. Таким образом, очевидно, что выполнение произвольных команд посредством инъекций в приложения, написанные на Python, являются достаточно опасной уязвимостью. Далее мы рассмотрим несколько примеров использования небезопасных команд языка Python.
Объект Popen
Команда popen
, а также команды popen3
, popen4
выполняют переданную строку как команду, что создает возможность для инъекции произвольных команд. Рассмотрим небольшой пример:
import os
user_input = "/etc && cat /etc/passwd
os.popen("ls -l " + user_input)
Здесь мы в переменной user_input
в качестве пользовательского ввода получаем путь к тому каталогу, содержимое которого нужно вывести на экран. Но если пользователь передает в качестве параметра не только каталог, но и другую команду, то popen выполняет обе команды. То есть в данном случае выводит содержимое файла /etc/passwd.
Еще одна проблемная команда — это subprocess
. Команда subprocess
позволяет создавать новые процессы, подключаться к их каналам ввода/вывода/ошибок и получать их коды возврата. Данная команда позволяет создавать новые процессы, а метод call
выполняет переданную текстовую строку:
import subprocess
import sys
user_input = "/bin && cat /etc/passwd"
subprocess.call("grep -R {} .".format(user_input), shell=True)
И еще одна аналогичная команда, выполняющая переданные пользователем данные без какой-либо проверки это os.system:
import os
user_input = "/etc && cat /etc/passwd"
os.system("grep -R {} .".format(user_input))
SQL инъекции
Инъекции команд SQL, наверное, можно назвать наиболее распространенным видом инъекций команд. SQL инъекции — это метод внедрения кода, который позволяет злоумышленнику вставить или изменить SQL-запрос в приложении. Для понимания проблемы приведем пример фрагмента кода:
def list_users():
rank = request.args.get('rank', '')
if rank == 'admin':
return "Can't list admins!"
c = CONNECTION.cursor()
c.execute("SELECT username, rank FROM users WHERE rank = '{0}'".format(rank))
data = c.fetchall()
Здесь, если в качестве запроса к веб-приложению передать следующую строку:
http://localhost:5000/users?rank=user’ UNION ALL SELECT * FROM SSN—
Тогда приложение вернет все записи из таблицы SSN. SQL-инъекции являются проблемой для большинства языков программирования, не только для Python.
Общие рекомендации по защите
Мы рассмотрели несколько примеров использования небезопасных команд и способов эксплуатации инъекций. Теперь самое время поговорить о том, как можно защититься от таких уязвимостей. Сразу хочу заметить, что инъекции команд — это не единственный вид уязвимостей, которые есть в Python, и в следующих статьях мы поговорим о других проблемах данного языка.
Итак, список рекомендаций для разработчиков можно начать с «прописной истины», а именно: Никогда не доверяйте данным, передаваемым пользователями! Да, концепция нулевого доверия существует уже много лет и все вроде бы хорошо знают, что нельзя доверять данным передаваемым пользователями, но уязвимости инъекции команд по прежнему можно встретить, особенно в веб-приложениях.
Лучше всего вообще запретить выполнение любых команд, вводимых пользователем. То есть, сделайте список или какой-то другой интерфейс, в котором пользователь может выбрать то действие, которое ему нужно. При этом, лучше всего максимально ограничить ввод произвольных параметров, которые пользователь может передать команде.
Приведем небольшой пример. Пусть у нас в приложении используется команда find
. Мы можем «захардкодить» выполнение этой команды, но оставить ввод произвольных параметров. Вроде бы пользователь не может осуществить инъекцию в чистом виде, но у find есть ключик exec, позволяющий выполнить любую команду. И если наше исходное приложение будет запущено под sudo или с SUID bit, то после инъекции ключа exec с нужным значением злоумышленник получит права root.
sudo find . -exec /bin/sh ; -quit
Таким образом, необходимо контролировать не только сами команды, вводимые пользователем но и ключи к командам, которые они собираются использовать. В случае с командой find
разработчик приложения должен жестко ограничить, какие именно ключи и с какими значениями могут использоваться. Пользователю должно быть доступно для ввода только значение маски искомого файла, все остальные параметры должны устанавливаться с помощью переключателей в интерфейсе приложения.
Кроме того, не стоит пренебрегать рекомендациями сообщества в части использования безопасных команд и параметров.
Безопасные команды
Рассмотрим безопасные варианты из приведенных выше примеров кода:
user_input = "cat /etc/passwd”
subprocess.popen (['ls', '-l', user_input ]) # команда отдельно
Здесь пользовательский ввод будет обрабатываться отдельно от команды и содержимое user_input
не будет выполнено как команда.
Команда subprocess.check_output
позволит проверить пользовательский ввод на наличие команд:
subprocess.check_output('ls -l dir/’)# проверка ввода
Значение shell=False
не позволит выполнить команды:
subprocess.call('echo $HOME', shell=False)# запрет выполнения
И наконец, проверка переданных приложению аргументов на соответствие имени какого-либо существующего файла.
os.path.isfile(sys.argv [ 1])# проверка передаваемых аргументов
В случае если избежать пользовательского ввода по тем или иным причинам нельзя, можно воспользоваться специальными библиотеками для контроля вводимых команд. Посмотрим несколько примеров использования таких библиотек.
Модуль Re
Принцип работы модуля — это использование регулярных выражений для проверки вводимых пользователем строк на наличие потенциально опасных команд.
import re
Python_code = input()
Pattern = re.compile(‘re_command_pattern’)
If pattern.fullmatch(python_code):
# выполнение python_code
Модуль Yaml
Еще одно средство контроля пользовательского ввода — это модуль Yaml. С помощью этого модуля мы можем проверить является ли переданный пользователем файл потенциально опасным или это обычный безобидный файл.
В примере ниже мы сначала проверяем обычный текстовый файл, на который программа никак не реагирует, а затем передаем в качестве входной строки путь к файлу /bin/ls и получаем кучу различных сообщений об ошибках.
import yaml
user_input = input()
with open(user_input) as secure_file:
contents = yaml.safe_load(secure_file)
Заключение
В этой статье мы поговорили об уязвимостях языка Python, связанных с инъекциями команд, а также о том, с помощью каких средств можно бороться с данными уязвимостями. В следующей статье мы продолжим рассмотрение проблем небезопасной разработки в Python.
А сейчас рекомендую всем желающим посетить открытый урок «Основы ООП в Python», на котором мы познакомимся с наследованием и научимся работать с классами. В результате этого занятия вы:
- научитесь создавать свои классы;
- разберётесь с наследованием;
- научитесь переопределять методы и обращаться к методам суперклассов.
Записаться на открытый урок можно на странице специализации "Python Developer".