Скрипт защиты от хакеров на PHP и .htaccess
В интернете хакеры постоянно сканируют сайты на уязвимости. При этом обычно идет перебор по словарю уязвимостей с помощью атаки brute force.
Если Ваш сайт написан на известном движке, типа WordPress, то для него ежедневно находят все новые и новые дыры. Данный скрипт проверяет известные ему атаки и полностью блокирует доступ с данного IP адреса к сайту с помощью записи в Deny from xxx.xxx.xxx.xxx в .htaccess, чтобы даже если появилась новая уязвимость, то хакер не сможет ее проверить и получить несанкционированный доступ.
Данный скрипт будет работать на любом хостинге, включая виртуальный и доменный, без root-прав. Вам необходимо поместить данный скрипт в файл, например banit.php и вставить вызов данного скрипта в начало всех ваших скриптов. Чаще всего достаточно вставить вызов в начало вашего корневого index.php с помощью команды:
include_once $_SERVER['DOCUMENT_ROOT'].'/banit.php';
Для исключения разрастания файла .htaccess, рекомендуется периодически вызывать скрипт с параметром ?cron для очистки старых блокировок. Оптимально вызывать по сron раз в неделю.
.\d.\d) [^\n]+\nDeny from \d.\d.\d.\d/", $buf,$ar,PREG_SET_ORDER); if(!empty($ar)) < $f_save = 0; echo "Всего заблокировано IP адресов: ".count($ar); //echo strtotime('-1 day')."\n"; foreach ($ar as $s) < $s[1]=substr_replace($s[1], '20', 6, 0); //echo $s[0]." ~ ".$s[1]." ~ ".strtotime($s[1])." ~ ".date('d.m.Y',strtotime($s[1]))."\n"; if (strtotime($s[1]) < strtotime('-30 day')) < $buf = str_replace($s[0], '', $buf); $f_save++; >> if ($f_save>0) < if(!is_file($v . '~')||filemtime($v . '~')file_put_contents($v, $buf); echo "Разблокировал ".$f_save; > > die; > if(substr($_SERVER['REQUEST_URI'],0,2)=='//')$_SERVER['REQUEST_URI']=substr($_SERVER['REQUEST_URI'],1); foreach(['/xmlrpc.php','/wp-login.php','/plus/recommend.php','/wp-admin','/wp-content'.'/wp-includes','/phpmyadmin','/.well-known/'] as $s) if(mb_strlen($_SERVER['REQUEST_URI'])>=mb_strlen($s) && mb_substr($_SERVER['REQUEST_URI'],0,mb_strlen($s))==$s) __BanIt('Hacker WP');; if ( strpos($_SERVER['REQUEST_URI'], "/wp-includes/wlwmanifest.xml") !== false) __BanIt('Hacker WP');; if ( strpos($_SERVER['REQUEST_URI'], ".php/component/users/") !== false) __BanIt('component'); // /index.php/component/users/?view=registration if ( strpos($_SERVER['REQUEST_URI'], "DOCUMENT_ROOT") !== false) __BanIt('DOCUMENT_ROOT'); // ?_SERVER[DOCUMENT_ROOT]=http://www.aerothaiunion.com/sik.txt?. if ( strpos($_SERVER['REQUEST_URI'], "'A=0") !== false) __BanIt('A=0'); // /price/?brand=641'A=0, > if(isset($_SERVER['HTTP_REFERER'])) < if(strpos($_SERVER['HTTP_REFERER'], '\"')!==false || strpos($_SERVER['HTTP_REFERER'], '\'')!==false)__BanIt('апостроф в HTTP_REFERER'); >if(!empty($_SERVER['QUERY_STRING'])) < $s=urldecode($_SERVER['QUERY_STRING']); foreach (["mosConfig_","_REQUEST","GLOBALS","if ($_COOKIE) < if (!empty($_COOKIE['PHPSESSID']))< if (strpos($_COOKIE['PHPSESSID'], 'disclaimer_accepted') !== false || $_COOKIE['PHPSESSID'] == '-1\'' || strpos($_COOKIE['PHPSESSID'], 'username=') !== false )< __BanIt('Hacker Cookie'); >> foreach ($_COOKIE as $key => $val) < if (strpos($key, 'wordpress') === 0 || strpos($key, 'phpbb') === 0 || $key == 'PHPSESSID' && $val == 'deleted' || substr($key,0,10)=='BITRIX_SM_' || in_array($key, ['is_first_access','CMSRabbitAdminLangRUN','RabbitCookie']))< __BanIt('Hacker Cookie'); >> > if (isset($_REQUEST['clientaction']) || isset($_REQUEST['"']) || isset($_REQUEST['1\'']) || isset($_REQUEST['\''])) < __BanIt('REQUEST_KEY'); >function __BanIt($msg) // если передан ip0, сообщение не выдается < $ips=[]; if (!empty($_SERVER['REMOTE_ADDR'])) $ips[]=$_SERVER['REMOTE_ADDR']; $ip2 = getenv('REMOTE_ADDR'); if (!empty($ip2) && $ip2 != "0.0.0.0" && !in_array($ip2, $ips)) $ips[]=$ip2; $ip2 = getenv('HTTP_X_FORWARDED_FOR'); // используется не анонимными прокси-серверами для передачи реального IP клиента X-Forwarded-For: client_ip, proxy1_ip, . proxyN_ip if (!empty($ip2) && $ip2 != "0.0.0.0" && !in_array($ip2, $ips)) $ips[]=$ip2; $ip2 = getenv('HTTP_FORWARDED_FOR'); if (!empty($ip2) && $ip2 != "0.0.0.0" && !in_array($ip2, $ips)) $ips[]=$ip2; $ip2 = getenv('HTTP_FORWARDED'); // Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43 if (!empty($ip2) && $ip2 != "0.0.0.0" && !in_array($ip2, $ips)) $ips[]=$ip2; $ip2 = getenv('HTTP_X_COMING_FROM'); if (!empty($ip2) && $ip2 != "0.0.0.0" && !in_array($ip2, $ips)) $ips[]=$ip2; $ip2 = getenv('HTTP_VIA'); // если не пустая, значит используется proxy. Значение - адрес (или несколько адресов) proxy сервера. if (!empty($ip2) && $ip2 != "0.0.0.0" && !in_array($ip2, $ips)) $ips[]=$ip2; $ip2 = getenv('HTTP_XROXY_CONNECTION'); if (!empty($ip2) && $ip2 != "0.0.0.0" && !in_array($ip2, $ips)) $ips[]=$ip2; $ip2 = getenv('HTTP_XROXY_CONNECTION'); if (!empty($ip2) && $ip2 != "0.0.0.0" && !in_array($ip2, $ips)) $ips[]=$ip2; $ip2 = getenv('HTTP_CLIENT_IP'); if (!empty($ip2) && $ip2 != "0.0.0.0" && !in_array($ip2, $ips)) $ips[]=$ip2; if (!empty($_SERVER['HTTP_X_REAL_IP']))< $ip2 = $_SERVER['HTTP_X_REAL_IP']; if ($ip2 != "0.0.0.0" && !in_array($ip2, $ips)) $ips[]=$ip2; >if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) < //https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers- $ip2 = $_SERVER['HTTP_CF_CONNECTING_IP']; if ($ip2 != "0.0.0.0" && !in_array($ip2, $ips)) $ips[]=$ip2; >foreach ($ips as $ip) file_put_contents($_SERVER['DOCUMENT_ROOT'].'/.htaccess', "\n#".date('d.m.y').' '.$msg."\nDeny from ".$ip, FILE_APPEND|LOCK_EX); header("HTTP/1.0 403 Ban you"); die('Ban you'); >
PHP-скрипт для защиты от парсинга и ботов
Нижепредложенный скрипт был наконец написан (лет 6 назад), когда устал каждый день смотреть логи сервера, где чётко видно было «мусорные» запросы, раздувающие лог и дающие лишнюю нагрузку на хостинге.
Исключительно в познавательных целях! Нормальную защиту реализуют средствами сервера, а этот php-скрипт лишь превентивная мера.
Огромная часть бот-трафика это запросы вида:
- 51.120.240.89 — — [01/Apr/2022:15:28:26 +0300] «GET /wp-content/plugins/ubh/up.php/.well-known/ HTTP/1.1» 403 «Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36»
- 51.120.240.89 — — [01/Apr/2022:15:28:44 +0300] «GET /wp-content/plugins/ubh/up.php/.well-known/ HTTP/1.1» 403 «Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36»
- 51.120.240.89 — — [01/Apr/2022:15:29:16 +0300] «GET /wp-content/uploads/ HTTP/1.1» 403 «Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36»
- 51.120.240.89 — — [01/Apr/2022:15:29:42 +0300] «GET /wp-includes/ HTTP/1.1» 403 «Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0
И все 50-200 строк в таком духе, за короткий промежуток времени.
То есть видим ip-адрес атакующего — [дата:время] «вид запроса и собственно сам запрос (/wp-content/wp-includes)» код ответа сервера 403 (т.к. ip-адрес не российский, город Осло, но об этом в следующих постах) строка UserAgent (может быть любой).
Сам скрипт настраиваемый по частоте запросов в единицу времени. Например, 4 запроса за 1 секунду приведут к блокировке атакующего ip-адреса на 60 секунд.
Чтобы не было вопросов «А как же поисковые боты, типа Яндекса и пр.» встроил проверку на поискового бота. Если это например робот Яндекса, то скрипт пропускает его и не проверяет больше ничего, не следит за активностью. Если это не из списка разрешённых ботов, то идёт отслеживание активности и если это откровенно «долбёжка», парсинг или как в вышеприведённом кусочке лога — попытка узнать/взломать вашу CMS — однозначно блокировка на указанное в настройках время (у меня 60 сек).
/*** Класс проверки и блокировки ip-адреса. */ class BotBlockIp < /*** Время блокировки в секундах. */ const blockSeconds = 60; /** * Интервал времени запросов страниц. */ const intervalSeconds = 1; /** * Количество запросов страницы в интервал времени. */ const intervalTimes = 4; /** * Флаг подключения всегда активных пользователей. */ const isAlwaysActive = true; /** * Флаг подключения всегда заблокированных пользователей. */ const isAlwaysBlock = true; /** * Путь к директории кэширования активных пользователей. */ const pathActive = 'active'; /** * Путь к директории кэширования заблокированных пользователей. */ const pathBlock = 'block'; /** * Флаг абсолютных путей к директориям. */ const pathIsAbsolute = false; /** * Список всегда активных пользователей. */ public static $alwaysActive = array( ); /** * Список всегда заблокированных пользователей. */ public static $alwaysBlock = array( ); /** * Метод проверки ip-адреса на активность и блокировку. */ public static function checkIp() < // Если это поисковый бот, то выходим ничего не делая if(self::is_bot())< return; >// Получение ip-адреса $ip_address = self::_getIp(); // Пропускаем всегда активных пользователей if (in_array($ip_address, self::$alwaysActive) && self::isAlwaysActive) < return; >// Блокируем всегда заблокированных пользователей if (in_array($ip_address, self::$alwaysBlock) && self::isAlwaysBlock) < header('HTTP/1.0 403 Forbidden'); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo 'Вы заблокированы администрацией ресурса.
'; exit; > // Установка путей к директориям $path_active = self::pathActive; $path_block = self::pathBlock; // Приведение путей к директориям к абсолютному виду if (!self::pathIsAbsolute) < $path_active = str_replace('\\' , '/', dirname(__FILE__) . '/' . $path_active . '/'); $path_block = str_replace('\\' , '/', dirname(__FILE__) . '/' . $path_block . '/'); >// Проверка возможности записи в директории if (!is_writable($path_active)) < die('Директория кэширования активных пользователей не создана или закрыта для записи.'); >if (!is_writable($path_block)) < die('Директория кэширования заблокированных пользователей не создана или закрыта для записи.'); >// Проверка активных ip-адресов $is_active = false; if ($dir = opendir($path_active)) < while (false !== ($filename = readdir($dir))) < // Выбирается ip + время активации этого ip if (preg_match('#^(\d.\d.\d.\d)_(\d+)$#', $filename, $matches)) < if ($matches[2] >= time() - self::intervalSeconds) < if ($matches[1] == $ip_address) < $times = intval(trim(file_get_contents($path_active . $filename))); if ($times >= self::intervalTimes - 1) < touch($path_block . $filename); unlink($path_active . $filename); >else < file_put_contents($path_active . $filename, $times + 1); >$is_active = true; > > else < unlink($path_active . $filename); >> > closedir($dir); > // Проверка заблокированных ip-адресов $is_block = false; if ($dir = opendir($path_block)) < while (false !== ($filename = readdir($dir))) < // Выбирается ip + время блокировки этого ip if (preg_match('#^(\d.\d.\d.\d)_(\d+)$#', $filename, $matches)) < if ($matches[2] >= time() - self::blockSeconds) < if ($matches[1] == $ip_address) < $is_block = true; $time_block = $matches[2] - (time() - self::blockSeconds) + 1; >> else < unlink($path_block . $filename); >> > closedir($dir); > // ip-адрес заблокирован if ($is_block) < header('HTTP/1.0 502 Bad Gateway'); echo ''; echo ''; echo '
'; echo ''; echo ''; echo ''; echo ''; echo '502 Bad Gateway'; echo '
'; echo 'К сожалению, Вы временно заблокированы, из-за частого запроса страниц сайта.
'; echo 'Вам придется подождать. Через ' . $time_block . ' секунд(ы) Вы будете автоматически разблокированы.'; echo '
'; echo ''; echo ''; exit; > // Создание идентификатора активного ip-адреса if (!$is_active) < touch($path_active . $ip_address . '_' . time()); >> /** * Метод получения текущего ip-адреса из переменных сервера. */ private static function _getIp() < // ip-адрес по умолчанию $ip_address = '127.0.0.1'; // Массив возможных ip-адресов $addrs = array(); // Сбор данных возможных ip-адресов if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) < // Проверяется массив ip-клиента установленных прозрачными прокси-серверами foreach (array_reverse(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])) as $value) < $value = trim($value); // Собирается ip-клиента if (preg_match('#^\d.\d.\d.\d$#', $value)) < $addrs[] = $value; >> > // Собирается ip-клиента if (isset($_SERVER['HTTP_CLIENT_IP'])) < $addrs[] = $_SERVER['HTTP_CLIENT_IP']; >// Собирается ip-клиента if (isset($_SERVER['HTTP_X_CLUSTER_CLIENT_IP'])) < $addrs[] = $_SERVER['HTTP_X_CLUSTER_CLIENT_IP']; >// Собирается ip-клиента if (isset($_SERVER['HTTP_PROXY_USER'])) < $addrs[] = $_SERVER['HTTP_PROXY_USER']; >// Собирается ip-клиента if (isset($_SERVER['REMOTE_ADDR'])) < $addrs[] = $_SERVER['REMOTE_ADDR']; >// Фильтрация возможных ip-адресов, для выявление нужного foreach ($addrs as $value) < // Выбирается ip-клиента if (preg_match('#^(\d).(\d).(\d).(\d)$#', $value, $matches)) < $value = $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4]; if ('. ' != $value) < $ip_address = $value; break; >> > // Возврат полученного ip-адреса return $ip_address; > /** * Метод проверки на поискового бота. */ private static function is_bot() < if (!empty($_SERVER['HTTP_USER_AGENT'])) < $options = array( 'YandexBot', 'YandexAccessibilityBot', 'YandexMobileBot','YandexDirectDyn', 'YandexScreenshotBot', 'YandexImages', 'YandexVideo', 'YandexVideoParser', 'YandexMedia', 'YandexBlogs', 'YandexFavicons', 'YandexWebmaster', 'YandexPagechecker', 'YandexImageResizer','YandexAdNet', 'YandexDirect', 'YaDirectFetcher', 'YandexCalendar', 'YandexSitelinks', 'YandexMetrika', 'YandexNews', 'YandexNewslinks', 'YandexCatalog', 'YandexAntivirus', 'YandexMarket', 'YandexVertis', 'YandexForDomain', 'YandexSpravBot', 'YandexSearchShop', 'YandexMedianaBot', 'YandexOntoDB', 'YandexOntoDBAPI', 'Googlebot', 'Googlebot-Image', 'Mediapartners-Google', 'AdsBot-Google', 'Mail.RU_Bot', 'bingbot', 'Accoona', 'ia_archiver', 'Ask Jeeves', 'OmniExplorer_Bot', 'W3C_Validator', 'WebAlta', 'YahooFeedSeeker', 'Yahoo!', 'Ezooms', '', 'Tourlentabot', 'MJ12bot', 'AhrefsBot', 'SearchBot', 'SiteStatus', 'Nigma.ru', 'Baiduspider', 'Statsbot', 'SISTRIX', 'AcoonBot', 'findlinks', 'proximic', 'OpenindexSpider','statdom.ru', 'Exabot', 'Spider', 'SeznamBot', 'oBot', 'C-T bot', 'Updownerbot', 'Snoopy', 'heritrix', 'Yeti', 'DomainVader', 'DCPbot', 'PaperLiBot' ); foreach($options as $row) < if (stripos($_SERVER['HTTP_USER_AGENT'], $row) !== false) < return true; >> > return false; > > // Проверка текущего ip-адреса BotBlockIp::checkIp();
- создаём папку, например block;
- в ней создаём папки active и block;
- создаём php-файл с вышеприведённым скриптом, например bot_block_ip.php;
- на любом сайте, в индексном файле, в самом начале подключаем наш скрипт: Например, в Битрикс я разместил скрипт в папке tools:
Скрипт прекрасно работает на php 7.4. Анализируя логи сервера стал замечать, что атакующие боты стали делать паузы между запросами (раньше доходило до 10-20 запросов в секунду, сейчас некоторые боты стали делать 1-2 запроса в секунду-две) и было решено — отсечь трафик не из России. Конечно пользоваться vpn и proxy никто не запрещает, но доля «мусорного» трафика сошла почти на нет. Если этот пост заинтересует достаточное количество людей, то напишу в следующих постах о доработанной версии этого скрипта, который блокирует все запросы не из России например (можно любую страну выбрать).
Спасибо, что прочитали. Сильно не критикуйте, скрипт работает, что от него и требовалось 🙂
Защита PHP скрипта путям привязки к доменному имени
Думаю, многие сталкивались с тем, что иногда необходимо защитить ваш скрипт от копирования, и вы использовали разные Ioncube, PHPLockit, но многим неудобно каждый раз кодировать. Понимаю, что в Ioncube есть такая штука, благодаря которой можно генерировать ключи безопасности и т.д., но многим невыгодно покупать или же пользоваться чужими услугами, а то мало ли, все бывает.
Некоторым хочется кодировать не весь код, а лишь его часть, и для этого многие используют такой «массив» для защиты:
$_SERVER['HTTP_HOST']='разрешенный домен';
- Скрипт будет закодирован путем обфускатора, который посылает API запрос к сайту, где API ищет доменное имя и ключ в базе данных; если он есть, то скрипт будет работать, если нет, то он будет переадресовывать основному сайту (пример будет таким: domain.com/api.php?domain=mysite1.ru&key=4024B-C0876-4FF0C-9A298-80EFA);
- Также у нас будет скрипт api.php, который будет отвечать за работу проверки лицензии и т.д.;
- Также хочу выделить лицензионный ключ, который мы будем получать. Лицензионный ключ — это md5 хэш домена, который будет проверятся через api, а в базе данных будет записан лишь сам домен и его статус.
Решение
1. Выдача лицензий и проверка действительности скрипта через api:
'localhost', 'user' => 'root', 'pass' => '', 'base' => 'lic' ); $db = new mysqli($config['host'], $config['user'], $config['pass'], $config['base']); if($db->connect_errno) < exit('Ошибка: Не удалось подключиться к базе данных!'); >$db->set_charset("utf8"); class Main < public function keygen($domain)< $key[0] = strtoupper(md5($domain)); $key[1] = substr($key[0], 0, 5); $key[2] = substr($key[0], 5, 5); $key[3] = substr($key[0], 10, 5); $key[4] = substr($key[0], 15, 5); $key[5] = substr($key[0], 20, 5); return $key[1].'-'.$key[2].'-'.$key[3].'-'.$key[4].'-'.$key[5]; >public function getLicInfo($domain)< global $db, $config; $sql = "SELECT * FROM `licenses` WHERE `domain` = '' LIMIT 1"; $result = $db->query($sql); if($result->num_rows == 1)< return $result->fetch_assoc(); > return false; > > $main = new Main; $domain = "".$_GET['domain'].""; $key = strtoupper($_GET['key']); if($lic = $main->getLicInfo($domain))< if($lic['license_status'])< $twokey = $main->keygen($lic['license_domain']); if($twokey == $key) < $a = 'OK_'.$lic['license_domain']; >else < $a = 'ERROR_wrongkey'; >> else < $a = 'ERROR_nolicense'; >> else < $a = 'ERROR_nolicense'; >echo $a; exit(); ?>
Вот сам код api.php. Тут я хотел бы обратить ваше внимание на следующий код:
Данный класс создает ключ домена путем использования md5 хэш.
2.Проверка доменного имена на наличие в базе данных
В 1 пункте мы с вами обозревали код api.php, который отвечает за работу скрипта. Хочу выделить код, который я уже выделял:
public function getLicInfo($domain)< global $db, $config; $sql = "SELECT * FROM `licenses` WHERE `domain` = '' LIMIT 1"; $result = $db->query($sql); if($result->num_rows == 1)< return $result->fetch_assoc(); > return false; > >
Данный класс проверяет доменное имя на наличие в базе данных, и если все удачно, то скрипт работает; если нет, то он переадресует на основной сайт.
Вот мы с вами закончили обозревать код api.php, который отвечает за основную работу проверки лицензии, но теперь стоит вопрос: «А как его реализовать в самом скрипте?»
Делается оно благодаря следующему коду:
public function __destruct() < $request = file_get_contents("http://domain.com/api.php?domain=". $_SERVER['HTTP_HOST'] ."&key _", $request); if($status[0] != "OK" && "".$_SERVER['HTTP_HOST']."" != $status[1])< header("Location: http://domain.com/"); >>
Этот код отправляет запрос в API и если имеется в базе данных, и если доменное имя есть в базе данных, то скрипт работает, если нет, то не работает. Такая же ситуация, если код неправильный, для этого в api.php существует следующий «отрезок» кода:
if($lic = $main->getLicInfo($domain))< if($lic['license_status'])< $twokey = $main->keygen($lic['license_domain']); if($twokey == $key) < $a = 'OK_'.$lic['license_domain']; >else < $a = 'ERROR_wrongkey'; >> else < $a = 'ERROR_nolicense'; >> else < $a = 'ERROR_nolicense'; >echo $a; exit();
Думаю, тут понятно: если все правильно, то «выходит» сообщение ok_myssite.com и это удовлетворяет, то скрипт продолжает работу, а если введен неверный ключ или доменное имя, то «выходит» следующие сообщения (смотря где есть ошибка):
Думаю, все. Также жду конструктивных комментариев, и благодарю pixxxel за его статью «Защита PHP скриптов от копирования — это возможно?»