Дэвид Лебланк - 19 смертных грехов, угрожающих безопасности программ
□ Не используйте конкатенацию строк для построения SQL–предложения даже при вызове хранимых процедур. Хранимые процедуры, конечно, полезны, но решить проблему полностью они не могут.
□ Не используйте конкатенацию строк для построения SQL–предложения внутри хранимых процедур.
□ Не передавайте хранимым процедурам непроверенные параметры.
□ Не ограничивайтесь простым дублированием символов одинарной и двойной кавычки.
□ Не соединяйтесь с базой данных от имени привилегированного пользователя, например sa или root.
□ Не включайте в текст программы имя и пароль пользователя, а также строку соединения.
□ Не сохраняйте конфигурационный файл с параметрами соединения в корне Web–сервера.
Стоит подумать
□ О том, чтобы запретить доступ ко всем пользовательским таблицам в базе данных, разрешив лишь исполнение хранимых процедур. После этого во всех запросах должны использоваться только хранимые процедуры и параметризованные предложения.
Грех 5. Внедрение команд
В чем состоит грех
В 1994 году автор этой главы сидел перед экраном компьютера SGI с операционной системой IRIX, на котором отображалась картинка с приглашением ввести имя и пароль. Там была возможность распечатать кое–какую документацию и указать соответствующий принтер. Автор задумался, как это могло бы быть реализовано, ввел строку, никоим образом не связанную с именем принтера, и неожиданно получил интерфейс администратора на машине, к которой у него вовсе не должно было быть доступа. Более того, он даже и не пытался войти.
Это и есть типичная атака с внедрением команды, когда введенные пользователем данные по какой–то причине интерпретируются как команда. Часто такая команда может наделять человека контролем над данными и вообще куда более широкими полномочиями, чем предполагалось.
Подверженные греху языки
Внедрение команд становится проблемой всякий раз, когда данные и команды хранятся вместе. Да, язык способен исключить наиболее примитивные способы внедрения команд за счет подходящего API (интерфейса прикладного программирования), осуществляющего тщательную проверку входных данных. Но всегда остается шанс, что новые API породят доселе неведомые варианты атак с внедрением команд.
Как происходит грехопадение
Внедрение команды происходит, когда непроверенные данные передаются тому или иному компилятору или интерпретатору, который может счесть, что это вовсе не данные.
Канонический пример – это передача аргументов системному командному интерпретатору без какого–либо контроля. Например, в старой версии IRIX вышеупомянутое окно регистрации содержало примерно такой код:
...char buf[1024];
snprintf(buf, "system lpr -P %s", user_input, sizeof(buf) – 1);
system(buf);
В данном случае пользователь не имел никаких привилегий, это вообще мог быть любой человек, случайно оказавшийся рядом с рабочей станцией. И достаточно было ему ввести строку FRED; xterm&, как тут же появилось бы окно консоли, поскольку символ ; завершает предыдущую команду системного интерпретатора (shell), а команда xterm создает новое окно консоли, готовое для ввода команд. При этом символ & сообщает системе, что новый процесс нужно запустить, не блокируя текущий. (В Windows символ & имеет тот же смысл, что точка с запятой в UNIX.) А поскольку процесс, контролирующий вход в систему, имел привилегии администратора, то и созданная консоль обладала такими же привилегиями.
Как будет показано ниже, в разных языках есть немало функций, уязвимых для таких атак. Но для успеха атаки с внедрением команды обращение к системному интерпретатору необязательно. Противник мог бы вызвать также интерпретатор самого языка. Это довольно распространенный способ в таких языках высокого уровня, как Perl или Python. Рассмотрим, например, такой код на языке Python:
...def call_func(user_input, system_data):
exec 'special_function_%s("%s")' % (system_data, user_input)
Здесь встроенный в Python оператор % работает примерно так же, как спецификаторы формата в функциях семейства printf из стандартной библиотеки С. Он сопоставляет значения в скобках с шаблонами %s в форматной строке. Идея заключалась в том, чтобы вызвать выбранную системой функцию, передав ей введенный пользователем аргумент. Например, если бы строка system_data была равна sample, a user_input – f red, то программа выполнила бы такой код:
...special_function_sample(«fred»)
Причем этот код работал бы в том же контексте, что и запустившая его функция exec.
Теперь противник, контролирующий переменную user_input, может выполнить произвольный код на Python. Для этого ему достаточно поставить кавычку, за ней правую скобку и точку с запятой, например так:
...fred"); print("foo
Тогда программа выполнит следующий код:
...special_function_sample(«fred»); print(«foo»)
В результате будет выполнено не только то, что хотел автор программы, но и напечатана строка foo. Противник может сделать буквально все, что ему заблагорассудится: удалить файлы, к которым имеет доступ программа, или, скажем, создать новое сетевое соединение. Если такая гибкость позволяет противнику расширить свои привилегии, то это уже угроза безопасности.
Многие проблемы такого рода возникают, когда данные и управляющие структуры перемешаны, и с помощью того или иного специального символа противник может переключить контекст с данных на команды. Если речь идет о командных интерпретаторах, то таких волшебных символов тьма–тьмущая. Например, в большинстве клонов UNIX противнику достаточно ввести точку с запятой (конец предложения), обратную кавычку (данные между двумя обратными кавычками исполняются как команда) или вертикальную черту (все следующее за ней относится уже к другому процессу, связанному с первым), и после этого он сможет исполнять произвольные команды. Есть и другие специальные символы, меняющие контекст; мы перечислили лишь самые очевидные.
Для устранения проблем, связанных с запуском команд, часто применяют вызов API, позволяющий запустить программу непосредственно, в обход командного интерпретатора. Например, в UNIX есть семейство функций execv(), которым передается просто имя запускаемой программы и ее параметры.
Это неплохо, но полностью проблему не решает, потому что запущенная программа может сама поместить данные рядом с важными управляющими конструкциями. Предположим, к примеру, что execv() используется для запуска Python–программы, которая затем передает список полученных аргументов функции exec. И мы снова оказываемся в исходной ситуации. Нам встречались программы, в которых через execv() исполнялся файл /bin/sh (это и есть командный интерпретатор), при этом весь смысл execv() теряется.
Родственные грехи
Есть несколько грехов, которые можно считать частными случаями внедрения команд. Ясно, что внедрение SQL – это особый вид атаки с внедрением команд. К той же категории можно отнести атаки на форматную строку, поскольку противник вставляет в переменную, которую программист рассматривал как данные, команды чтения и записи (например, спецификатор %п это команда записи). Но эти частные случаи столь широко распространены, что мы посвятили им отдельные главы.
Та же ошибка лежит в основе кросс–сайтовых сценариев (cross–site scripting), когда в отсутствие должного контроля противник может вставить в данные некоторые управляющие элементы разметки.
Где искать ошибку
Вот основные характерные признаки:
□ команды (или управляющая информация) и данные расположены друг за другом;
□ существует возможность, что данные будут интерпретироваться как команда, зачастую из–за наличия специальных символов, например кавычки и точки с запятой;
□ контроль над исполняемой командой может наделить противника более широкими привилегиями, чем он имел до этого.
Выявление ошибки на этапе анализа кода
Этой ошибке подвержены многочисленные вызовы API и конструкции, встречающиеся в самых разных языках программирования. В ходе анализа кода следует первым делом обращать внимание на те конструкции, которые потенциально можно использовать для вызова командного процессора (входящего в оболочку ОС, базы данных либо интерпретатора самого языка). Нужно выяснить, встречаются ли такие конструкции в программе. Если да, проверьте, предприняты ли адекватные защитные меры. Хотя защита зависит от природы греха (см., например, обсуждение внедрения SQL–команд в грехе 4), но в общем случае рекомендуется скептически относиться к подходу «все кроме», отдавая предпочтение подходу «только такие» (см. ниже раздел «Искупление греха»).
Вот перечень наиболее распространенных конструкций, которые могут стать причиной ошибки.
Тестирование
В общем случае нужно рассмотреть все входные данные, понять, какому интерпретатору команд они могут быть переданы, а затем попробовать включить в тестовые данные различные используемые в этом интерпретаторе метасимволы и посмотреть, что получится. Разумеется, тестовые данные надо подбирать так, чтобы в случае успешного срабатывания также получался какой–то наблюдаемый результат.