{"id":14279,"url":"\/distributions\/14279\/click?bit=1&hash=4408d97a995353c62a7353088166cda4ded361bf29df096e086ea0bbb9c1b2fc","title":"\u0427\u0442\u043e \u0432\u044b\u0431\u0435\u0440\u0435\u0442\u0435: \u0432\u044b\u0435\u0445\u0430\u0442\u044c \u043f\u043e\u0437\u0436\u0435 \u0438\u043b\u0438 \u0437\u0430\u0435\u0445\u0430\u0442\u044c \u0440\u0430\u043d\u044c\u0448\u0435?","buttonText":"","imageUuid":""}

Безопасный код. Детектим и блокируем CSRF

Материал является продолжением серий статей «Безопасный код», ранее опубликованные темы можете прочитать на VC.ru (Безопасный код, FileUpload).

Современные веб-приложения предоставляют большой спектр функционала: загружать и изменять аватарки на своем профиле аккаунта, добавлять информацию на стену, изменять пароль, почту и т.д. Уязвимость CSRF (Cross-site request forgery – межсайтовая подделка запроса) дает возможность злоумышленнику производить различные манипуляции с аккаунтом пользователя от лица самого пользователя, при этом, жертва даже не будет знать об этом. Со стороны сайта это выглядит так, будто пользователь отправляет запрос на сервер, который не проверяет, действительно ли владелец аккаунта сделал этот запрос. Например, злоумышленник может сделать такую страницу, которая при открытии создаст запрос на смену пароля. Запрос отправится на сервер, выполнится и пароль поменяется, при этом пользователь ничего не заметит.

Как обычно, цель написания материала - желание поделиться с разработчиками своими мыслями и опытом по безопасности, которые стараюсь соблюдать при создании веб-приложений.

В рамках материала будут представлены:

· Метод проверки на наличие уязвимости CSRF в форме на примере DWVA (обучающая, уязвимая платформа, для нахождения уязвимостей, Damn Vulnerable Web Application)

· Векторы атаки

· Способы защиты от CSRF

Проверяем форму на наличие CSRF

Как всегда, для примера использую свой любимый DVWA. Переходим на вкладку CSRF. Нас встречает форма смены пароля Рис.1.

Рис.1 Вкладка CSRF

Для начала сменю пароль для проверки функционала. Новый пароль будет 222222222, отправляю запрос, и он выполняется (рис.2).

Рис.2 Смена пароля

Это пример доступного функционала пользователя.

Как проверить форму на наличие CSRF?

1. Поиск потенциально уязвимой формы

2. Разработка эксплойта для проверки на уязвимость

3. Запуск разработанного эксплойта

4. Проверка успешности выполнения эксплойта

Форму мы нашли, теперь создадим эксплойт для эксплуатации уязвимости. Кликаю правой кнопкой мыши по форме, далее пункт исследовать элемент и ищу тег form (рис.3).

Рис.3 Содержимое тега form

Теперь мы можем написать страницу для смены пароля. Будущая страница и будет эксплойтом. Заходим в любой редактор текста, вставляем содержимое тега.

<form action=”#” method=”GET”> New password: <br> <input autocomplete="off" name="password_new" type="password"><br> Confirm new password: <br> <input autocomplete="off" name="password_conf" type="password"> <br> <input value="Change" name="Change" type="submit"> </form>

Сохраняю страницу с названием csrf.html. Если запустить страницу, то получаем следующее (рис.4):

Рис.4 Страница csrf.html

В данном случае открылась обычная страница с формой смены пароля. Для того чтобы отправить запрос веб-приложению с этой формы, нужно отредактировать ее. Вместо символа #, нужно вставить адрес формы, в моем случае, это будет http://10.20.14.214/dvwa/vulnerabilities/csrf/. Это нужно для того, чтобы моя страница поняла, куда отправлять запрос. Изменяю пароль на 333333 в своей форме, выполняю запрос и получаю сообщение об успешном изменении пароля (рис.5).

Рис.5 Сообщение об успешном изменении пароля

Что же произошло? Я отправил запрос со своей локальной страницы на смену пароля, сервер получил запрос, обработал его, проверил, с какого аккаунта был сделан запрос, но не проверил, действительно ли владелец этого аккаунта отправил запрос. Данный пример является базой для выявления CSRF.

Допустим, ваше приложение имеет много форм и проверить каждую затруднительно. Тогда вы можете воспользоваться автоматизированным методом выявления уязвимости. ПО для автоматизированного метода существует большая масса, поэтому я упомяну лишь пару приложений, которые, на мой взгляд, наиболее применимы и универсальны.

1. BurpSuite – универсальное решение для тестирования веб-приложений. Позволяет в real-live режиме перехватывать пакеты и изменять их. Из коробки доступны некоторые автоматизированные алгоритмы для поиска уязвимостей, например:

- sql-injection (внедрение sql кода)

- xss (Cross-Site Scripting — «межсайтовый скриптинг»)

2. IronWASP – бесплатная программа для сканирования безопасности веб-приложения с открытым исходным кодом. Поддерживает сканирование большинства распространенных уязвимостей (более подробно смотрите в документации инструментов Kali, т.к. инструмент дорабатывается). Немаловажная особенность приложения - поддержка выявлений ложных срабатываний, что поможет вам на этапе анализа безопасности.

Почему появляется уязвимость CSRF?

Немаловажную роль в CSRF играют куки. Атака не пройдет, если я не буду авторизован на сайте в момент отправки запроса с локальной страницы. Атака CSRF в принципе невозможна, если пользователь не залогинен на сайте. Куки позволяют проверить веб-приложению, что пришел именно «тот» пользователь, при этом, они не дают информации о данных, которые пользователь отправляет. Иначе говоря, использование куки, в случае с CSRF, только усугубляет ситуацию, но это не значит, что для защиты нужно отказываться от использования куки. О методе защиты поговорим после рассмотрения векторов атаки.

Анализ кода

<?php if( isset( $_GET[ 'Change' ] ) ) { // Получаем вывод $pass_new = $_GET[ 'password_new' ]; $pass_conf = $_GET[ 'password_conf' ]; // Проверка на совпадение пароля if( $pass_new == $pass_conf ) { $pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : "")); $pass_new = md5( $pass_new ); // Обновляем пароли в базе $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';"; $result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' ); // Сообщение об изменении пароля $html .= "<pre>Password Changed.</pre>"; } else { // Введенные пароли не совпадают $html .= "<pre>Passwords did not match.</pre>"; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>

Выше представленный код, это часть бэкенда нашей страницы, из-за которого появляется уязвимость CSRF, т. е. так писать код не надо. Если кратко, то код забирает введенные данные из первого поля ('password_new') и из второго ('password_conf'), далее проверяет их на сходство (введенные пароли должны совпадать), если все ок, то старый пароль обновляется до нового. В данном случае, токены не используются, секретных ключей тоже нету, поэтому уязвимость CSRF и существует в данной форме.

Теперь рассмотрим случай, когда CSRF невозможен

<?php if( isset( $_GET[ 'Change' ] ) ) { // Проверяем токен Anti-CSRF checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); // Получаем данные из введенной формы $pass_curr = $_GET[ 'password_current' ]; $pass_new = $_GET[ 'password_new' ]; $pass_conf = $_GET[ 'password_conf' ]; // Удаляем лишние символы, переводим пароль в MD5 $pass_curr = stripslashes( $pass_curr ); $pass_curr = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_curr ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : "")); $pass_curr = md5( $pass_curr ); // Проверяем правильность текущего пароля $data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' ); $data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR ); $data->bindParam( ':password', $pass_curr, PDO::PARAM_STR ); $data->execute(); // Совпадают ли оба новых пароля и совпадает ли текущий пароль с сессией пользователя? if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) { $pass_new = stripslashes( $pass_new ); $pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : "")); $pass_new = md5( $pass_new ); // Обновить базу данных с новым паролем $data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' ); $data->bindParam( ':password', $pass_new, PDO::PARAM_STR ); $data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR ); $data->execute(); // Обратная связь для пользователя $html .= "<pre>Password Changed.</pre>"; } else { // Проблема с совпадением паролей $html .= "<pre>Passwords did not match or current password incorrect.</pre>"; } } // Генерируем анти-CSRF токен generateSessionToken(); ?>

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

Для тестирования этого решения перейдите в директорию с DVWA, далее vulnerabilities -> csrf -> source. В этой директории редактируйте любой из файлов (имя файла означает сложность эксплуатации), т. е. замените код на представленный и запустите эту измененную сложность.

Векторы атаки

1. Запуск html файла пользователем

Предупрежден, значит вооружен. Здесь я опишу базовые сценарии мошенников, для реализации уязвимости, дабы показать, насколько простая она может быть, а чем атака проще, тем она опаснее. Из названия, вы могли понять, что для реализации этого сценария, пользователю предлагают запустить файл html. При запуске файл отработает форму и отправит на сервер запрос, в нашем случае, для примера, это будет запрос на изменение пароля. Как это выглядит со стороны мошенника? Для начала, форма приводится к нужному виду, пишется скрипт для автоматической отправки запроса, далее пользователь открывает файл и запрос выполняется (рис.6). Для тестирования этого сценария, вы можете скопировать код к себе в html страницу и запустить его.

<form id=form1 action=”http://10.20.14.214/dvwa/vulnerabilities/csrf/” method=”GET”> <input type="hidden" autocomplete="off" name="password_new" value="666666"><br> <input type="hidden" autocomplete="off" name="password_conf" value="666666"> <input value="Change" name="Change" type="hidden"> </form> <script> document.getElementById('form1').submit();</script>

Рис.6 Результат выполнения файла

2. Переход по ссылке

Что же будет делать мошенник, если у него не получится реализовать первый сценарий? Допустим, пользователь не открывает подозрительные файлы, в таком случае злоумышленник может попробовать заставить жертву перейти по ссылке. Данный сценарий вытекает из первого с небольшими модификациями: мошенник просто заливает на свой веб-сайт ранее созданную страницу, которая при открытии отправит запрос на изменение пароля на сервер (рис.8). Для тестирования сценария можете поднять свой веб-сервер Apache (рис.7).

Рис.7 Загрузка файла на локальный веб-сайт и поднятие Apache
Рис.8 Результат перехода по ссылке

Защита

Переходим к защите своего веб-приложения. На своем веб-сайте для смены пароля, необходимо сделать функцию проверки пароля, т. е. пароль не будет меняться на новый, пока пользователь не введет старый. Это нужно, чтобы удостовериться, действительно ли пользователь сам намерен это сделать. Тогда уязвимость будет поэксплуатирована только в том случае, если злоумышленник заведомо знает пароль. Однако, как я писал ранее, данная уязвимость дает возможность не только менять пароль, но и к примеру, выкладывать информацию в свой блог, а представленный метод защищает только от смены пароля. Чтобы не писать много проверок на верификацию пользователя, рассмотрим некоторые техники для защиты.

1. Синхронизированные токены

Мы, как разработчики, не можем перед каждым действием пользователя спрашивать его пароль, поэтому лучшим способом защитить свой веб-сайт будет использование динамически синхронизирующихся токенов. Как они работают? Мы генерируем токен на стороне клиента, перед тем как посылать запрос, далее посылаем его вместе с запросом, а затем подтверждаем его на стороне сервера, когда он пытается обработать его. Токен должен быть уникальным для каждого пользователя и для каждого запроса, чтобы его нельзя было перехватить или угадать. Для этого необходимо включить информацию о пользователе и запросе в токен, также, хорошей практикой является добавление в токен ключа. Помните, ранее я говорил про использование куки? Так вот, вместе с куками, передавать этот уникальный токен нельзя. После генерации токена, его необходимо зашифровать, используя одностороннее шифрование, чтобы в случае перехвата, хакер не смог расшифровать его, используя обратное шифрование. Для лучшего понимания, как это должно работать, приведу пример. Например, пользователь имеет id «22», тогда значение токена «22». Пользователь хочет сменить пароль на 12345, при этом токен=2212345. Добавим в токен ключ «Oleg» (это просто рандомный ключ, который я взял из головы), тогда получим токен «2212345Oleg», также неплохо будет иметь динамический ключ, и тогда он не будет статичным, что сделает расшифровку токена более сложным. Далее шифруем токен односторонним шифрованием и получим 84a3207e842f3ef29bfb7d26c1b7f1b7 (здесь используется md5). В таком случае, запрос, который пользователь отправляет на сервер будет иметь вид – http://website.com/password.php?uid=22&newPass=12345&token=84a3207e842f3ef29bfb7d26c1b7f1b7. Сервер получит запрос: он знает id пользователя, знает новый пароль, знает секретный ключ, знает формулу создания токена. Теперь сервер проходит те же шаги шифрования токена и получает такой токен, сравнивает его с полученным, если они равны, то запрос будет выполнен, в противном случае будет ошибка.

2. Двойная отправка файла куки

Ранее я говорил про использование куки на сайте. Благодаря этой технологии возможна CSRF, однако, если использовать меры защиты, представленные в этом материале, то использование куки становится полностью безопасным. Если, в каких-либо случаях использование токена невозможно, тогда на помощь придет использование двойной отправки файла куки. Мы отправляем случайное значение в файле куки и в запросе. В это время, сервер проверяет, совпадают ли эти значения. Веб-приложение должно генерировать зашифрованное значение в файл куки отдельно от идентификатора сеанса. Далее, в качестве скрытого значения, в формы, вставляется это значение, и если оно равно значению на сервере, то запрос будет выполнен. Рассмотрим реализацию двойной отправки куки.

<?php session_start(); require_once 'token.php'; const username = 'admin'; const password = 'admin123'; if (isset($_POST['username']) && isset($_POST['password'])) //Если отправляется форма { if ($_POST['username'] === username && $_POST['password'] === password) { $_SESSION['username'] = $_POST['username']; //записываем имя пользователя в базу setcookie("session", session_id()); Token::generate_token(session_id()); header('Location: ./counter.php'); //перенаправляем на главную страницу } else { echo "<script>alert('Check username and password');</script>"; echo "<noscript>Check username and password</noscript>"; } } ?>

Эта страница проверяет, существует ли такой пользователь в базе, если да, то сервер записывает куки для этого аккаунта и перенаправляет на главную страницу. Более детально по теории Здесь.

3. HMAC

Альтернатива двойной отправки файла куки является использование токена HMAC (hash-based message authentication code) с секретным ключом. HMAC используется для аутентификации сообщения по хэш-функциям. Токен HMAC помещается в файл куки, известный только серверу. Данный метод похож на зашифрованный файл куки, однако данный способ защиты менее ресурсозатратный, т. к. не надо зашифровывать и расшифровывать значения из файла.

// определим коды ошибок, которые мы будем возвращать. define("E_UNSUPPORTED_HASH_ALGO",-1); class HMAC_Generator{ private $key, $algo; private $sign_param_name = "hmac"; function __construct($key, $algo = "sha256"){ $this->key = $key; $this->algo = $algo; } function make_data_hmac($data, $key = NULL){ // если не задан ключ в параметре - используем из свойств if(empty($key)) $key = $this->key; // если параметр с подписью есть в массиве - уберем. if(isset($data[$this->sign_param_name])) unset($data[$this->sign_param_name]); // отсортируем по ключам в алфавитном порядке - // на случай, если последовательность полей изменилась // например, если данные передавались GET- или POST-запросом. HMAC_Generator::ksort_recursive($data); // сформируем JSON (или другую сериализацию - можно переопределить метрд encode_string) $data_enc = $this->serialize_array($data); // формируем и возвращаем подпись return $this->make_signature($data_enc, $key); } function check_data_hmac($data, $key = NULL, $sign_param_name = NULL){ // если не задан ключ в аргументах - используем из свойств if(empty($key)) $key = $this->key; // если не задано имя параметра с подписью аргументах в параметре - используем из свойств if(empty($sign_param_name)) $sign_param_name = $this->sign_param_name; // если в данных нет подписи - сразу вернем false if(empty($data[$sign_param_name])) return false; // исходный HMAC нам приходит в том же массиве, что и данные, // заберем его значение для сверки и выкинем из массива $hmac = $data[$sign_param_name]; unset($data[$sign_param_name]); // сформируем контрольный HMAC $orig_hmap = $this->make_data_hmac($data, $key); // проверку осуществляем регистронезависимо if(strtolower($orig_hmap) != strtolower($hmac)) return false; else return true; } // Установка алгоритма хеширования function set_hash_algo($algo){ // приведем к нижнему регистру $algo = strtolower($algo); // проверим, поддерживается ли системой выбранный алгоритм if(in_array($algo, hash_algos())) $this->algo = $algo; else return E_UNSUPPORTED_HASH_ALGO; } // // сериализацию и хеширование - выносим в отдельные методы, просто перепишите или переопределите их // private function serialize_array($data){ // кодируем все в json, в случае если мы будем собирать подпись не только в PHP, // такой тип сериализации - оптимальный $data_enc = json_encode($data, JSON_UNESCAPED_UNICODE); return $data_enc; } // переопределите, если будет другой алго формирования подписи, не HASH HMAC private function make_signature($data_enc, $key){ // сформируем подпись HMAC при помощи выбранного аглоритма $hmac = hash_hmac($this->algo, $data_enc, $key); return $hmac; } // статический метод для рекурсивной сортировки массива по именам ключей public static function ksort_recursive(&$array, $sort_flags = SORT_REGULAR) { // если это не массив - сразу вернем false if (!is_array($array)) return false; ksort($array, $sort_flags); foreach ($array as &$arr) { HMAC_Generator::ksort_recursive($arr, $sort_flags); } return true; } }

Это реализация класса, с помощью которого можно подписывать данные и проверять их. Данный код был взят из статьи «Подписываем данные: HMAC на практике в API и Web-формах». Более подробно можно ознакомиться ЗДЕСЬ.

4. Same-Site Cookie

SameSite Cookie – это относительно новая технология, предназначенная для защиты от подделки межсайтовых запросов. Она позволяет гибко настраивать куки в зависимости от предпочтений. Настройка зависит от трех аргументов:

- None – ограничения на куки не будут установлены

- Lax – к кукам добавлено исключение, когда они передаются при навигации высокого уровня. Исключает CSRF в POST-запросах, но не исключает в GET. По умолчанию, используется именно этот параметр

- Strict – этот параметр означает, что с вашего приложения не будут отправлены куки, т.е полный запрет на передачу куки на запрос с другого ресурса. Предпочтительнее для безопасности, но не совсем удобный в рамках передачи контента для пользователя

Где можно настроить этот параметр?

  1. Для PHP >= v7.3
setcookie($name, $value, [ 'expires' => time() + 86400, 'path' => '/', 'domain' => 'domain.example', 'secure' => true, 'httponly' => true, 'samesite' => 'None', ]);

2. Apache

Header always edit Set-Cookie (.*) "$1; SameSite=Lax"

3. Nginx

location / { # your usual config ... # hack, set all cookies to secure, httponly and samesite (strict or lax) proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; }

5. Проверка на XSS

XSS — тип атаки на веб-системы, заключающийся во внедрении в выдаваемую веб-системой страницу вредоносного кода и взаимодействии этого кода с веб-сервером злоумышленника. Является разновидностью атаки «Внедрение кода». Почему наличие XSS может привести к CSRF? Зловредный скрипт дает возможность злоумышленнику отправлять запрос от имени пользователя на сервер и получать ответы без каких-либо ограничений. При этом, если пункты, описанные ранее, были выполнены, то наличие XSS сводит к нулю эти методы защиты. Через XSS можно получить как и зашифрованный токен, так и секретное значение из файла куки. Как проверить форму на XSS? Это тема для отдельной статьи, которую в дальнейшем я напишу. Если кратко, то в форму введите <script>alert(“XSS”)</script>, если форма выполнится и вы увидите сообщение XSS, то форма уязвима.

Краткий итог для пользователей

Что же делать пользователю для защиты своих данных? Специалисты кибербезопасности не просто так постоянно говорят про периодическую смену пароля от своих аккаунтов, про нежелательные переходы по ссылкам и запуск файлов от недоверенных источников. Из первого сценария вытекает вывод, что не нужно открывать подозрительные сайты. Из второго сценария можно сделать вывод, что не нужно переходить по ссылкам, которые ведут на незнакомые сайты (зловредные сайты могут быть даже с HTTPS). Если веб-приложение позволяет сделать двухфакторную авторизацию, то такой вариант поможет защитить ваши данные, но только в том случае, если при критических изменениях на вашем аккаунте, приложение запросит данные от двухфакторной авторизации, в ином случае данный метод не подойдет. Хорошей практикой является выход из сессии на вашем аккаунте, дабы минимизировать риск взлома, т. к. автоматическая отправка формы не отработает, если вы не залогинены на сайте.

Итоги

Я поделился с вами простыми и актуальными сценариями атак, которые пользуются мошенники, актуальными техниками защиты вашего веб-приложения. Они простые в реализации и действенны в защите. Пишите безопасные приложения, делитесь своим опытом с коллегами, говорите о проблемах, с которыми вы или ваши знакомые сталкивались, вместе мы сделаем наше будущее безопасным, спасибо!

0
Комментарии
-3 комментариев
Раскрывать всегда