Отловить бэкдуры или трояны на большом проекте бывает головная боль для программиста/системного администратора если он туда попал, даже если проект использовал Git или другую систему контроля версий это не всегда панацея что бы найти backdoor.
Большинство бэкдуров для PHP обсуфицированы что бы их было труднее найти и после распаковки своего кода используют eval, вот здесь мы и будем их ловить.
В данной статье будет описан метод перехвата функции eval логирования ее и блокирования подозрительных вызовов, данный способ актуален для хостинга на VDS/VPS или выделенном сервере (Dedicated Server) так как на потребуется собрать свое расширение для PHP.
Сборка модуля и примеры актуальные для:
Ставим
Если у вас PHP 7 версии
клонируем репозитарий
для PHP 8 версии
Затем собираем модуль
прописываем его что бы загружался
перезапускаем Apache
Создаем файл /home/bitrix/www/evalhook.php
В /home/bitrix/www/.htaccess прописываем строчку
она указывает PHP что перед выполнением любого php файла нужно выполнить этот скрипт
https://www.php.net/manual/ru/ini.core.php#ini.auto-prepend-file
проверяем работу скриптом evaltest.php
открываем его в браузере, на экране должны увидеть
injection failed
в папке /home/bitrix/www/logs
будет создан файл eval-block-hook-дата.log
так же evalhook.php проверяет POST запросы на инъекции кода и блокирует их, записывая в лог postdata-block-hook-дата.log
весь eval код который скрипт пропустил он запишет в лог evalhook-дата.log у Битрикса много мест где используется eval и их вызов не имеет "криминального" характера
пример штатного случая
Большинство бэкдуров для PHP обсуфицированы что бы их было труднее найти и после распаковки своего кода используют eval, вот здесь мы и будем их ловить.
В данной статье будет описан метод перехвата функции eval логирования ее и блокирования подозрительных вызовов, данный способ актуален для хостинга на VDS/VPS или выделенном сервере (Dedicated Server) так как на потребуется собрать свое расширение для PHP.
Сборка модуля и примеры актуальные для:
- CentOS 7
- 1С-Битрикс: Веб-окружение 7.5.5
- PHP 8.1.29
- Сайт установлен в папку /home/bitrix/www
Ставим
yum install php-devel
Если у вас PHP 7 версии
клонируем репозитарий
git clone https://github.com/extremecoders-re/php-eval-hook
cd php-eval-hook
для PHP 8 версии
git clone https://github.com/dev-bx/php8-eval-hook
cd php8-eval-hook
Затем собираем модуль
phpize
./configure --enable-evalhook
make
make install
прописываем его что бы загружался
echo "extension=evalhook.so" > /etc/php.d/10-evalhook.ini
перезапускаем Apache
systemctl restart httpd
Создаем файл /home/bitrix/www/evalhook.php
<?php
/*
* Copyright (c) 2024. DEV-BX.RU
* ruslan@dev-bx.ru
*/
class DevBxLogger {
private $filename = false;
/**
* @var bool
*/
private $logRequestUri = true;
/**
* @var bool
*/
private $logTrace = true;
/**
* @var bool
*/
private $logFooter = true;
public static function checkDirPath($path)
{
if (function_exists('CheckDirPath'))
{
return CheckDirPath($path);
}
//remove file name
if(mb_substr($path, -1) != "/")
{
$p = mb_strrpos($path, "/");
$path = mb_substr($path, 0, $p);
}
$path = rtrim($path, "/");
if($path == "")
{
//current folder always exists
return true;
}
if(!file_exists($path))
{
return mkdir($path, 0755, true);
}
return is_dir($path);
}
public static function getBackTrace($limit = 0, $options = null, $skip = 1)
{
if(!defined("DEBUG_BACKTRACE_PROVIDE_OBJECT"))
{
define("DEBUG_BACKTRACE_PROVIDE_OBJECT", 1);
}
if ($options === null)
{
$options = ~DEBUG_BACKTRACE_PROVIDE_OBJECT;
}
$trace = debug_backtrace($options, ($limit > 0? $limit + $skip : 0));
if ($limit > 0)
{
return array_slice($trace, $skip, $limit);
}
return array_slice($trace, $skip);
}
protected static function formatTrace(array $trace = null)
{
if ($trace)
{
$traceLines = array();
foreach ($trace as $traceNum => $traceInfo)
{
$traceLine = '';
if (array_key_exists('class', $traceInfo))
$traceLine .= $traceInfo['class'].$traceInfo['type'];
if (array_key_exists('function', $traceInfo))
$traceLine .= $traceInfo['function'].'()';
if (array_key_exists('file', $traceInfo))
{
$traceLine .= ' '.$traceInfo['file'];
if (array_key_exists('line', $traceInfo))
$traceLine .= ':'.$traceInfo['line'];
}
if ($traceLine)
$traceLines[] = ' from '.$traceLine;
}
return implode("\n", $traceLines);
}
else
{
return "";
}
}
public static function getFileNameForLog($trace)
{
$arRegTpl = array(
'component.$1-%Y-%m-%d.log' => '#.*\/components\/.+\/(.+)\/.*$#U',
'module.$1-%Y-%m-%d.log' => '#.*\/modules\/(.+)\/.*$#U',
'admin.$1-%Y-%m-%d.log' => '#\/bitrix\/admin\/(.+)\..*$#',
'$1-%Y-%m-%d.log' => '#.*\/(.+\.php)$#',
);
$fileName = 'debug-%Y-%m-%d.log';
foreach ($trace as $traceNum => $traceInfo)
{
if (array_key_exists('file', $traceInfo))
{
foreach ($arRegTpl as $replace=>$pattern)
{
if (preg_match($pattern, $traceInfo['file']))
{
$fileName = preg_replace($pattern,$replace, $traceInfo['file']);
break 2;
}
}
}
}
return static::formatFileName($fileName);
}
private static function formatFileName($fileName)
{
$d = new \DateTime();
$arMacro = array(
'%Y' => $d->format('Y'),
'%y' => $d->format('y'),
'%m' => $d->format('m'),
'%d' => $d->format('d'),
'%H' => $d->format('H'),
'%h' => $d->format('h'),
'%i' => $d->format('i'),
'%s' => $d->format('s'),
);
return str_replace(array_keys($arMacro),array_values($arMacro), $fileName);
}
private static $registryMap = [];
private function __construct($filename)
{
$this->filename = $filename;
}
public static function getInstance($filename = false)
{
if (isset(self::$registryMap[$filename]))
return self::$registryMap[$filename];
self::$registryMap[$filename] = new static($filename);
return self::$registryMap[$filename];
}
public function setLogRequestUri($value): DevBxLogger
{
$this->logRequestUri = $value === true;
return $this;
}
public function setLogTrace($value): DevBxLogger
{
$this->logTrace = $value === true;
return $this;
}
public function setLogFooter($value): DevBxLogger
{
$this->logFooter = $value === true;
return $this;
}
public function logVar($var, $varName = '', $traceSkip = 1): DevBxLogger
{
$arTrace = self::getBackTrace(30, null, $traceSkip);
if ($this->filename === false)
{
$logFilename = $_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.'logs'.DIRECTORY_SEPARATOR.static::getFileNameForLog($arTrace);
} else
{
$logFilename = $_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.'logs'.DIRECTORY_SEPARATOR.static::formatFileName($this->filename);
}
$header = '';
if (isset($_SERVER["REQUEST_URI"]) && $_SERVER["REQUEST_URI"] && $this->logRequestUri)
$header .= "REQUEST URI: ".$_SERVER["REQUEST_URI"]."\n";
$trace = static::formatTrace($arTrace);
$body = '';
if ($varName)
$body = $varName.":\n";
if (is_object($var) || is_array($var))
$body .= var_export($var, true);
else
$body .= $var;
$footer = str_repeat("-", 30);
$logSessid = '';
$logTime = '';
if (function_exists('bitrix_sessid_val'))
{
$logSessid = "\nSESSID: ".bitrix_sessid_val();
}
if (isset($_SERVER['REQUEST_TIME_FLOAT']))
{
$logTime = "\nTIME: ".(microtime(true)-$_SERVER['REQUEST_TIME_FLOAT']);
}
if (!isset($_SERVER['DEVBX_DEBUG_ID']))
{
$_SERVER['DEVBX_DEBUG_ID'] = md5(uniqid());
}
$logDebugId = "\nDEBUG ID: ".$_SERVER['DEVBX_DEBUG_ID'];
$message =
($header ? "\n" . $header : '').
"\nDate: ".date("Y-m-d H:i:s") .
$logSessid.
$logTime.
$logDebugId.
"\n" . $body .
($this->logTrace ? "\n\n" . $trace : '').
($this->logFooter ? "\n" . $footer : '').
"\n";
self::checkDirPath($logFilename);
file_put_contents($logFilename, $message, FILE_APPEND);
return $this;
}
public static function log($var, $varName = '', $fileName = false)
{
self::getInstance($fileName)->logVar($var, $varName, 2);
}
}
function __eval($code, $file)
{
$arTrustRules = [
'/^\$eval_result=/',
'/^return CSite::/',
'/^namespace/',
'/public static function getMap()/m',
'/^return \(\(isset\(/mi',
'/extends \\\\Bitrix\\\\Main\\\\ORM\\\\/mi',
'/\$FORM->ShowInput/mi',
];
$arBlockRules = [
'/require_once/mi',
'/kutep.ua/mi',
'/wget_file_upload/mi',
'/system\(/mi',
'/exec\(/mi',
'/copy\(/mi',
'/copy_file_upload/mi',
'/file_raw_upload/mi',
'/file_put_contents/mi',
'/inject_include_file/mi',
'/chmod/mi',
'/base64_decode/mi',
'/ini_get/mi',
'/touch/mi',
'/opendir/mi',
'/mkdir/mi',
'/shell_exec/mi',
'/fwrite/mi',
'/readfile/mi',
'/evaltest/mi',
];
foreach ($arBlockRules as $rule) {
if (preg_match($rule, $code)) {
DevBxLogger::log($code, $file, 'eval-block-hook-%Y-%m-%d.log');
return '';
}
}
foreach ($arTrustRules as $rule) {
if (preg_match($rule, $code))
return $code;
}
DevBxLogger::log($code, $file, 'evalhook-%Y-%m-%d.log');
return $code;
}
function devbxCheckPostData()
{
$postData = file_get_contents('php://input');
$arBlockRules = [
'/require_once/mi',
'/kutep.ua/mi',
'/wget_file_upload/mi',
'/exec\(/mi',
'/copy\(/mi',
'/copy_file_upload/mi',
'/file_raw_upload/mi',
'/file_put_contents/mi',
'/inject_include_file/mi',
'/chmod/mi',
'/base64_decode/mi',
'/ini_get/mi',
//'/touch/mi',
'/opendir/mi',
'/mkdir/mi',
'/shell_exec/mi',
'/fwrite/mi',
'/readfile/mi',
'/evaltest/mi',
'/\$\_SERVER/mi',
'/wp_43/mi',
'/ext_www/mi',
];
foreach ($arBlockRules as $rule) {
if (preg_match($rule, $postData)) {
DevBxLogger::log(array(
'rule' => $rule,
'rawData' => $postData,
'$_POST' => $_POST,
'$_REQUEST' => $_REQUEST,
'$_FILES' => $_FILES,
), 'postData', 'postdata-block-hook-%Y-%m-%d.log');
die();
}
}
if (!empty($_FILES)) {
foreach ($_FILES as $ar) {
if ($ar['size'] > 0 && $ar['error'] == 0) {
$fileData = file_get_contents($ar['tmp_name']);
foreach ($arBlockRules as $rule) {
if (preg_match($rule, $fileData)) {
DevBxLogger::log(array(
'rule' => $rule,
'fileData' => $fileData,
'$_POST' => $_POST,
'$_REQUEST' => $_REQUEST,
'$_FILES' => $_FILES,
), 'postData', 'postdata-file-block-hook-%Y-%m-%d.log');
die();
}
}
}
}
}
}
devbxCheckPostData();
В /home/bitrix/www/.htaccess прописываем строчку
php_value auto_prepend_file /home/bitrix/www/evalhook.php
она указывает PHP что перед выполнением любого php файла нужно выполнить этот скрипт
https://www.php.net/manual/ru/ini.core.php#ini.auto-prepend-file
проверяем работу скриптом evaltest.php
<?php
eval("shell_exec('ls'); echo 'injection success';die();");
echo 'injection failed';
открываем его в браузере, на экране должны увидеть
injection failed
в папке /home/bitrix/www/logs
будет создан файл eval-block-hook-дата.log
REQUEST URI: /evaltest.php
Date: 2024-08-16 14:32:52
TIME: 0.0065360069274902
DEBUG ID: 3f9caf496039270d16f7dab563060167
/home/bitrix/www/evaltest.php(2) : eval()'d code:
shell_exec('ls'); echo 'injection success';die();
from DevBxLogger::log() /home/bitrix/www/evalhook.php:306
from __eval() /home/bitrix/www/evaltest.php:2
from eval() /home/bitrix/www/evaltest.php:2
------------------------------
так же evalhook.php проверяет POST запросы на инъекции кода и блокирует их, записывая в лог postdata-block-hook-дата.log
весь eval код который скрипт пропустил он запишет в лог evalhook-дата.log у Битрикса много мест где используется eval и их вызов не имеет "криминального" характера
пример штатного случая
REQUEST URI: /
Date: 2024-08-16 14:35:39
SESSID: 10660d3d043af7d4a3468573867381ef
TIME: 0.038811922073364
DEBUG ID: 2d8e9318f6e99b02d76be926df0d6a67
/home/bitrix/www/bitrix/modules/main/classes/general/site.php(930) : eval()'d code:
return preg_match("#^/desktop_app/router.php\?alias=([\.\-0-9a-zA-Z]+)&videoconf#", $GLOBALS['APPLICATION']->GetCurPage(0));
from DevBxLogger::log() /home/bitrix/www/evalhook.php:316
from __eval() /home/bitrix/www/bitrix/modules/main/classes/general/site.php:930
from eval() /home/bitrix/www/bitrix/modules/main/classes/general/site.php:930
from CAllSite::GetCurTemplate() /home/bitrix/www/bitrix/modules/main/include.php:1
from require_once() /home/bitrix/www/bitrix/modules/main/include/prolog_before.php:19
from require_once() /home/bitrix/www/bitrix/modules/main/include/prolog.php:10
from require_once() /home/bitrix/www/bitrix/header.php:1
from require() /home/bitrix/www/index.php:2
------------------------------