Current Path : /var/www/www-root/data/www/monolith-realty.ru/bitrix/modules/security/classes/general/ |
Current File : /var/www/www-root/data/www/monolith-realty.ru/bitrix/modules/security/classes/general/xscan.php |
<?php use PhpParser\Node; use PhpParser\NodeFinder; use Bitrix\Security\XScanResultTable; use Bitrix\Security\XScanResult; IncludeModuleLangFile(__FILE__); class CBitrixXscan { static $var = '\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'; static $spaces = "[ \r\t\n]*"; static $request = '(?:_REQUEST|_GET|_POST|_COOKIE|_SERVER(?!\[[\'"]DOCUMENT_ROOT[\'"]\])|_FILES)'; static $functions = '(?:parse_str|hex2bin|str_rot13|base64_decode|url_decode|str_replace|str_ireplace|preg_replace|move_uploaded_file)'; static $evals = ['eval', 'assert', 'create_function', 'exec', 'passthru', 'pcntl_exec', 'popen', 'proc_open', 'set_include_path', 'shell_exec', 'system']; static $evals_reg = '(?:assert|call_user_func|call_user_func_array|create_function|eval|exec|ob_start|passthru|pcntl_exec|popen|proc_open|set_include_path|shell_exec|system)'; static $black_reg = '(https?://[0-9a-z\-]+\.pw/|wp-config|wp-admin|wp-login|deprecated-media-js|customize-menus-rtl|adminer_errors|/etc/passwd|/etc/hosts|mysql_pdo|__halt_compiler|/bin/sh|registerPHPFunctions|[e3]xp[l1][o0][i1][7td])'; static $mehtods = [ 'Bitrix\Im\Call\Auth::authorizeById', 'Bitrix\ImOpenLines\Controller\Widget\Filter\Authorization::authorizeById', 'Bitrix\Imopenlines\Widget\Auth::authorizeById', 'Bitrix\Sale\Delivery\Services\Automatic::createConfig', 'Bitrix\Sender\Internals\DataExport::toCsv', 'Bitrix\Sender\Internals\QueryController\Base::call', 'CAllSaleBasket::ExecuteCallbackFunction', 'CAllSaleOrder::PrepareSql', 'CBPHelper::UsersStringToArray', 'CControllerClient::RunCommand', 'CMailFilter::CheckPHP', 'CMailFilter::DoPHPAction', 'CRestUtil::makeAuth', 'CSaleHelper::getOptionOrImportValues', 'CWebDavTools::sendJsonResponse', ]; public $false_positives = ['9223e925409363b7db262cfea1b6a7e2', '4d2cb64743ff3647bad4dea540d5b08e', 'd40c4da27ce1860c111fc0e68a4b39b5', 'ef9287187dc22a6ce47476fd80720878', '13484affcdf9f45d29b61d732f8a5855', '4a171d5dc7381cce26227c5d83b5ba0c', 'b41d3b390f0f5ac060f9819e40bda7eb', '40142320d26a29586dc8528cfb183aac', 'f454f39a15ec9240d93df67536372c1b', '29bba835e33ab80598f88e438857f342', '77cdd8164d4940cb6bfaac906383a766', '5b3425a6ff518fa2337b373e1c799959', '7c60ccaee2b919c9e6b16b307eb80dab', 'bde611db5c3545005a7270edcffd8dc2', '4d6b616171dbf06ff57d1dab8ea6bbce', 'a85abce54b4deb8cb157438dddca5a7c', 'de4f7ee97d421cf14d3951c0b4e5c2dd', '379918e8f6486ce9a7bb2ed5a69dbee6', '7ac4a2afcee04e683b092eb9402ee7ed', '1d5eb769111fc9c7be2021300ee5740e', 'f2357a1fe8e984052b6ee69933d467dc', 'a9158139e1a619ca8cc320cf4469c250']; static $default_config = ['request' => true, 'from_request' => true, 'crypted' => true, 'files' => true, 'assigned' => false, 'params' => false, 'concat' => true, 'hardcoded' => false, 'value' => true, 'recursive' => false]; public static $database = false; public $db_log = null; public $db_file = null; public $doc_root = null; public $start_time = null; public $time_limit = null; public $base_dir = null; public $break_point = null; public $skip_path = null; public $found = false; public $mem_enought = false; public $progress = 0; public $total = 0; public $collect_exceptions = true; private $errors = []; static $cryptors = ['rot13', 'str_rot13', 'base32_decode', 'base64_decode', 'gzinflate', 'unserialize', 'url_decode', 'pack', 'unpack', 'hex2bin', 'bzdecompress', 'gzuncompress', 'lzf_decompress', 'strrev']; static $string_change = ['preg_replace', 'str_ireplace', 'str_replace', 'substr', 'strrev']; static $scoring = [ '[337] strings from black list' => [0.9], '[630] long line' => [0.4], '[321] base64_encoded code' => [0.8], '[610] strange vars' => [0.5], '[302] preg_replace_eval' => [0.9], '[663] binary data' => [0.75], '[640] strange exif' => [0.6], '[500] php wrapper' => [0.7], '[665] chars by code' => [0.8], '[665] encoded code' => [0.8], '[303] create_function' => [0.8, 0.8, 0.1, 1, 0.8, 0.3, 0.7, 0.8, 0.8], '[300] eval' => [1, 0.4, 0.1, 1, 0.8, 0.3, 0.7, 0.8, 0.9], '[302] unsafe callable argument' => [0.8, 0.8, 0.1, 1, 0.8, 0.3, 0.7, 0.8, 0.8], '[307] danger method' => [1, 0.4, 0.1, 1, 0.8, 0.3, 0.7, 0.8, 0.9], '[662] function return as a function' => [0.9, 0.8, 0.1, 1, 1, 0.3, 0.7, 0.7, 0.8], '[663] strange function' => [1, 1, 0.1, 1, 1, 0.8, 0.7, 0.8, 0.9], '[302] eregi' => [0.8, 0.8, 0.1, 1, 0.8, 0.3, 0.7, 0.8, 0.8], '[887] backticks' => [1, 0.8, 0.1, 1, 0.8, 0.3, 0.7, 0.8, 0.8], '[600] strange include' => [0.8, 0.8, 0.1, 1, 0.8, 0.3, 0.7, 0.8, 0.8], '[660] array member as a function' => [0.9, 0.8, 0.1, 1, 0.8, 0.3, 0.5, 0.8, 0.8], '[298] mysql function' => [0.6, 0.8, 0.1, 1, 0.8, 0.9, 0.7, 0.8, 0.8], '[300] command injection' => [1, 0.7, 0.1, 1, 0.8, 0.6, 0.7, 0.8, 0.9], '[299] mail function' => [0.6, 0.8, 0.1, 1, 0.8, 0.3, 0.7, 0.8, 0.8], '[650] variable as a function' => [0.9, 0.8, 0.1, 1, 0.8, 0.3, 0.5, 0.8, 0.8], '[304] filter_callback' => [0.6, 0.8, 0.1, 1, 0.8, 0.3, 0.7, 0.8, 0.8], '[305] strange function and eval' => [0.8, 0.8, 0.1, 1, 0.8, 0.3, 0.7, 0.8, 0.8], '[301] file operations' => [0.5, 0.4, 0.1, 1, 0.8, 0.3, 0.7, 0.8, 0.8], '[302] file operations' => [0.8, 0.4, 0.1, 1, 0.8, 0.3, 0.7, 0.8, 0.8], '[400] bitrix auth' => [0.9, 0.8, 1, 1, 0.8, 0.3, 0.7, 0.8, 0.8], '[308] no prolog file operations' => [0.5, 0.4, 0.1, 1, 0.8, 0.3, 0.7, 0.8, 0.8], ]; private $results = []; private $tags = []; private $result_collection = null; private $score = 1; function __construct($progress = 0, $total = 0) { $this->doc_root = rtrim($_SERVER['DOCUMENT_ROOT'], '/'); $this->result_collection = new \Bitrix\Security\XScanResults(); $this->db_file = $this->doc_root . '/bitrix/modules/security/data/database.json'; $this->start_time = time(); $mem = (int)ini_get('memory_limit'); $this->time_limit = ini_get('max_execution_time') ?: 30; $this->time_limit = min($this->time_limit, 30); $this->time_limit = $this->time_limit * 0.7; $this->mem_enought = $mem == -1 || $mem >= 128; $this->progress = $progress; $this->total = $total; $this->parser = (new PhpParser\ParserFactory)->create(PhpParser\ParserFactory::PREFER_PHP7); $this->nodeFinder = new NodeFinder; $this->errorHandler = new PhpParser\ErrorHandler\Collecting; $this->pprinter = new PhpParser\PrettyPrinter\Standard; $errs = XScanResultTable::getList(['select' => ['SRC'], 'filter' => ['TYPE' => 'file', 'MESSAGE' => 'error']]); while ($row = $errs->fetch()) { $this->errors[] = $row['SRC']; } } function clean() { global $DB; $DB->Query("TRUNCATE TABLE b_sec_xscan_results", true); $this->errors = []; } function CheckEvents() { global $DB; $r = $DB->Query('SELECT * from b_module_to_module'); while ($row = $r->Fetch()) { if ($row['TO_CLASS'] && $row['TO_METHOD']) { $class_method = trim($row['TO_CLASS'] . '::' . $row['TO_METHOD'], '\\'); $found = false; foreach (self::$mehtods as $mtd) { if (stripos($class_method, $mtd) !== false) { $found = true; break; } } if ($found) { $result = (new XScanResult)->setType('event')->setSrc($row['ID'])->setScore(1)->setMessage('[050] dangerous method at event, check arguments'); $this->result_collection[] = $result; } } } } function CheckAgents() { global $DB; $r = $DB->Query('SELECT * from b_agent'); while ($row = $r->Fetch()) { if (!$row['NAME']) { continue; } $src = "<?php\n" . $row['NAME'] . "\n?>"; $this->CheckCode($src); if ($this->results) { $message = []; foreach ($this->results as $res) { $message[] = $res['subj']; } if (is_array($message)) { $message = implode(' <br> ', array_unique($message)); } $result = (new XScanResult)->setType('agent')->setSrc($row['ID'])->setScore(1)->setMessage($message); $this->result_collection[] = $result; } } } static function crc($a) { return crc32(implode('|', $a)); } static function CountBlocks($src, &$result) { $code = strtolower($src); $code = preg_replace('~<\?(php|=)?~', '', $code); $code = preg_replace('~<[^>$()]*?>~', '', $code); $code = str_replace('?>', '', $code); $code = preg_split('~[\n;{}(),\s]+~', $code); $arr = []; foreach ($code as $chunk) { $chunk = trim($chunk); if ($chunk !== '') { $arr[] = $chunk; } } $crcs = []; if (!empty($arr)) { while (count($arr) < 3) { $arr[] = $arr[0]; } $block = [$arr[0], $arr[1], $arr[2]]; $crcs[] = self::crc($block); $end = count($arr) - 1; for ($i = 3; $i <= $end; $i++) { $block = [$block[1], $block[2], $arr[$i]]; $crcs[] = self::crc($block); } } $result = array_unique($crcs); unset($code); unset($arr); unset($crcs); } function SearchInDataBase($src) { $result = []; $found = []; self::CountBlocks($src, $result); foreach ($result as $token) { if (isset(self::$database['tokens'][$token])) { foreach (self::$database['tokens'][$token] as $shell) { if (!isset($found[$shell])) { $found[$shell] = 0; } $found[$shell] += 1; } } } $bFound = false; foreach ($found as $key => $value) { if ($value / self::$database['shells'][$key] > 0.8) { $bFound = true; break; } } unset($result); unset($found); return $bFound; } function addResult($subj, $code, $score, $checksum = '') { $this->results[] = ['subj' => $subj, 'code' => $code, 'score' => $score, 'checksum' => $checksum]; } static function detectDocRoot($file_path) { static $doc_root; if (!$doc_root || strpos($file_path, $doc_root . '/') !== 0) { $path = explode('/', ltrim($file_path, '/')); $doc_root = ''; $found = false; foreach ($path as $comp) { if (is_file($doc_root . '/bitrix/.settings.php')) { $found = true; break; } $doc_root .= '/' . $comp; } if (!$found) { $doc_root = ''; } } return $doc_root; } static function getVersion($moduleName, $doc_root) { $moduleName = preg_replace("/[^a-zA-Z0-9_.]+/i", "", trim($moduleName)); if ($moduleName == '') return false; if ($moduleName == 'main') { $content = file_get_contents("$doc_root/bitrix/modules/main/classes/general/version.php"); } else { $content = file_get_contents("$doc_root/bitrix/modules/$moduleName/install/version.php"); } preg_match('/\d+\.\d+\.\d+/', $content, $m); $version = count($m)? $m[0]: false; return $version; } static function getHashes($module, $version) { global $DB; static $static_cache; static $map = [ 'socserv' => 'socialservices', 'system' => 'main', 'rating' => 'main', 'spotlight' => 'main', 'desktop' => 'main', 'menu' => 'main', 'pdf' => 'fileman', 'player' => 'fileman', 'map' => 'fileman', 'news' => 'iblock', 'photo' => 'iblock', 'support' => 'iblock', 'rss' => 'iblock', 'voting' => 'vote', 'payroll' => 'intranet', 'planner' => 'intranet', 'eshop' => 'bitrix.eshop', 'furniture' => 'bitrix.sitecorporate', 'app' => 'rest' ]; $module = isset($map[$module])? $map[$module]: $module; if (!is_array($static_cache)){ $static_cache = []; } $key = $module . '_' . $version; if (isset($static_cache[$key])) { return $static_cache[$key]; } $cache = \Bitrix\Main\Data\Cache::createInstance(); if ($cache && $cache->initCache(12 * 3600, 'xscan_' . $key, 'xscan')) { $result = $cache->getVars(); $static_cache[$key] = $result; return $static_cache[$key]; } else { $sHost = COption::GetOptionString("main", "update_site", "www.bitrixsoft.com"); $proxyAddr = COption::GetOptionString("main", "update_site_proxy_addr", ""); $proxyPort = COption::GetOptionString("main", "update_site_proxy_port", ""); $proxyUserName = COption::GetOptionString("main", "update_site_proxy_user", ""); $proxyPassword = COption::GetOptionString("main", "update_site_proxy_pass", ""); $dbtype = mb_strtolower($DB->type); $http = new \Bitrix\Main\Web\HttpClient(); $http->setProxy($proxyAddr, $proxyPort, $proxyUserName, $proxyPassword); $data = $http->get("https://{$sHost}/bitrix/updates/checksum.php?check_sum=Y&module_id={$module}&ver={$version}&dbtype={$dbtype}&mode=2"); $result = @unserialize(gzinflate($data), ['allowed_classes' => false]); $static_cache[$key] = []; if (is_array($result)) { $result = array_filter($result, function ($value) { return substr($value, -4) === '.php'; }, ARRAY_FILTER_USE_KEY); $cache->startDataCache(); $cache->endDataCache($result); $static_cache[$key] = $result; } return $static_cache[$key]; } return false; } static function checkByHash($file_path) { $module = ''; $file = ''; if (preg_match("~bitrix/modules/([^.]+?)/(.+)~", $file_path, $matches)) { $file = $matches[2]; $module = $matches[1]; } elseif(preg_match("~/bitrix(/components/bitrix/socialnetwork_(?:group|user)/.+)~", $file_path, $matches)) { $file = 'install' . $matches[1]; $module = 'socialnetwork'; } elseif(preg_match("~/bitrix(/components/bitrix/photogallery_user/.+)~", $file_path, $matches)) { $file = 'install' . $matches[1]; $module = 'photogallery'; } elseif(preg_match("~/bitrix(/(?:components|wizards)/bitrix/([a-z24]+)[./].+)~", $file_path, $matches)) { $file = 'install' . $matches[1]; $module = $matches[2]; } elseif(preg_match("~/bitrix(/templates/bitrix24/.+)~", $file_path, $matches)) { $file = 'install' . $matches[1]; $module = 'intranet'; } elseif(preg_match("~/bitrix(/blocks/bitrix/.+)~", $file_path, $matches)) { $file = 'install' . $matches[1]; $module = 'landing'; } if($file && $module) { $doc_root = static::detectDocRoot($file_path); if(!$doc_root) { return false; } $version = static::getVersion($module, $doc_root); if (!$version) { return; } $hashes = static::getHashes($module, $version); if ($hashes && isset($hashes[$file]) && $hashes[$file] === md5_file($file_path)) { return true; } } return false; } function CheckFile($file_path) { $this->results = []; $this->tags = []; static $me; if (!$me) { $me = realpath(__FILE__); } if (realpath($file_path) == $me) { return false; } if (in_array($file_path, $this->errors)) { return false; } if ($this->SystemFile($file_path)) { return false; } # CODE 100 if (basename($file_path) == '.htaccess') { $src = file_get_contents($file_path); $res = preg_match('#<(\?|script)#i', $src, $regs); if ($res) { $this->addResult('[100] htaccess', $regs[0], 1); return true; } $res = preg_match('#\bwp-[a-z]+\.php#i', $src, $regs); if ($res) { $this->addResult('[100] htaccess', $regs[0], 1); return true; } if (preg_match_all('#x-httpd-php[578]?\s+(.+)#i', $src, $regs)) { foreach ($regs[1] as $i => $val) { $val = preg_split('/\s+/', $val); foreach ($val as $ext) { $ext = trim(strtolower($ext), '"\''); if (!in_array($ext, ['.php', '.php5', '.php7', '.html', ''])) { $this->addResult('[100] htaccess', $regs[0][$i], 1); return true; } } } } return false; } # CODE 110 if (preg_match('#^/upload/.*\.php$#i', str_replace($this->doc_root, '', $file_path))) { $this->addResult('[110] php file in upload dir', '', 1); return true; } if (!preg_match('#\.php[578]?$#i', $file_path, $regs)) { return false; } if (static::checkByHash($file_path)) { return false; } # CODE 200 if (($src = @file_get_contents($file_path)) === false) { $this->addResult('[200] read error', '', 1); return true; } $this->CheckCodeInternal($src, $file_path); $tot = 1; foreach ($this->results as $value) { $tot = $tot * (1 - $value['score']); } $tot = round(1 - $tot, 2); $this->score = $tot; return !empty($this->results); } function CalcChecksum($file_path, $code, $subj) { $doc_root = static::detectDocRoot($file_path); if ($doc_root) { $file_path = substr($file_path, strlen($doc_root)); } if (strpos($file_path, '/') !== 0) { $file_path = '/' . $file_path; } $file_path = preg_replace('#^/bitrix/modules/[a-z0-9._]+/install/components/bitrix#', '/bitrix/components/bitrix', $file_path); $checksum = md5($file_path . '|' . trim($code) . '|' . $subj); return $checksum; } function IsFalsePositive($checksum) { return in_array($checksum, $this->false_positives, true); } function getResult() { return $this->results; } function getErrors() { return $this->errors; } function setErrors($val) { $this->errors = $val; } function getTags() { return $this->tags; } function getScore() { return $this->score; } function CheckCode(&$src, $file_path = false) { $this->results = []; $this->tags = []; return $this->CheckCodeInternal($src, $file_path); } private function CheckCodeInternal(&$src, $file_path = false) { $file_path = $file_path ? $file_path : ''; if (!self::$database && is_file($this->db_file) && $this->mem_enought) { $tmp = file_get_contents($this->db_file); self::$database = json_decode($tmp, true); unset($tmp); } $code = preg_replace("/<\?=/", "<?php echo ", $src); $code = preg_replace("/<\?(?!php)/", "<?php ", $code); $code = preg_replace("/else if\s*\(/", "elseif (", $code); // crutch # OBFUSCATORS $cmt = ''; if ( ($cmt = '$$') && substr_count($code, '${${') > 0 || ($cmt = 'vars') && preg_match_all('/(?:\$|function\s+)(?:[o0]{4,}|[il]{4,})/i', $code) > 3 || ($cmt = 'goto') && preg_match_all('/goto\s+[0-9A-Z]+\s*;/i', $code) > 2 || ($cmt = 'globals') && preg_match_all('/\$GLOBALS\s*\[["\'][0-9_]+["\']\]/', $code) > 3 || ($cmt = 'base64_short') && preg_match_all("/base64_decode\s*\(\s*[^$].{3,14}\)/i", $code) > 3 || ($cmt = 'functions') && preg_match_all("/function\s+_\w{1,3}\b/i", $code) > 3 || ($cmt = 'concat') && preg_match_all("~(?:(['\"])[0-9a-z=+\/_]{1,20}\\1(?:\s*\.\s*)?){2,}~i", $code) > 20 || // ($cmt = 'len') && strlen($code) / max(substr_count($code, "\n"), 1) > 500 || // ($cmt = 'base_strings') && preg_match_all('~[0-9A-Z+/]{80,100}~i', $code) > 5 || ($cmt = 'urlenc') && preg_match_all('/(%[0-9A-Z]{2}){80,100}/i', $code) > 2 || ($cmt = 'base64_keys') && substr_count($code, "[base64") > 1 /* || ($cmt = 'long_space') && preg_match('/\t{30,}+(?:[()$]|\S.*?[()$])/', $code) || ($cmt = 'long_space') && preg_match('/[\t ]{80,}+(?:[()$]|\S.*?[()$])/', $code) */ ) { $this->tags[] = defined('XSCAN_DEBUG') ? "obfuscator [$cmt]" : "obfuscator"; if (in_array($cmt, ['$$', 'vars', 'goto', 'globals', 'base64_short', 'functions', 'concat'])) { $this->addResult('[001] obfuscator', '', 0.6); } } else { $comments = []; preg_match_all('~/\*(.+?)\*/~', $code, $comments); $cnt = 0; $comments = $comments ? $comments[1] : []; $comments = array_unique($comments); foreach ($comments as $comment) { $comment = trim($comment); if (strlen($comment) <= 15 && preg_match('~([A-Za-z\s_]++|[0-9]++|[!@#$%^&():;`<>?,.{}|\~[\]+-=?]){4,}~', $comment)) { $cnt += 1; if ($cnt > 20) { $this->tags[] = defined('XSCAN_DEBUG') ? "obfuscator [comments]" : "obfuscator"; $this->addResult('[001] obfuscator', '', 0.6); break; } } } unset($comments); if ($cnt < 20) { $funcsVars = []; preg_match_all('/(?:\$|function\s+)([0-9a-z_]++)/i', $code, $funcsVars); $cnt = 0; $funcsVars = $funcsVars ? $funcsVars[1] : []; $funcsVars = array_unique($funcsVars); foreach ($funcsVars as $value) { $value = str_replace('24', '', $value); // crutch if (preg_match('/\d/', $value) && preg_match('/_\d|(?:[a-z_]++|[A-Z_]++|[0-9]++){4,}/', $value)) { $cnt++; } } if (count($funcsVars) && $cnt / count($funcsVars) > 0.5) { $this->tags[] = defined('XSCAN_DEBUG') ? "obfuscator [rand_names]" : "obfuscator"; } unset($funcsVars); } } if (strpos($file_path, '/bitrix/modules/main/') !== false) { $this->tags[] = 'core'; } if (preg_match('~/bitrix/(?:modules|components)/[0-9a-z_]+\.[0-9a-z_]+/~i', $file_path) || preg_match('~/bitrix/components/(?!bitrix/)~i', $file_path)) { $this->tags[] = 'marketplace'; } if (preg_match('/(?:[a-z_]++|[0-9]++){4,}/i', $file_path)) { $this->tags[] = 'random_name'; } if (preg_match('~/lang/~i', $file_path)) { $this->tags[] = 'lang'; } if (preg_match('~/\.~i', $file_path)) { $this->tags[] = 'hidden'; } if (strpos($file_path, '/bitrix/modules/') === false && strpos($file_path, '/upload/') === false && strpos($file_path, '/bitrix/php_interface/') === false && strpos($src, 'B_PROLOG_INCLUDED') === false && strpos($src, '/bitrix/header.php') === false && strpos($src, '/bitrix/modules/main/start.php') === false && strpos($src, '/bitrix/modules/main/include/prolog') === false && strpos($src, '/bitrix/modules/main/include/mainpage.php') === false && strpos($src, '/bitrix/main/include/routing_index.php') === false ) { $this->tags[] = 'no_prolog'; if (preg_match('/copy\s*\(|file_put_contents|move_uploaded_file|fwrite|fputs/i', $src, $m)) { $subj = '[308] no prolog file operations'; $checksum = $this->CalcChecksum($file_path, $m[0], $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $m[0], self::CalcCrit($subj), $checksum); } } } $parser = $this->parser; $errorHandler = $this->errorHandler; $pprinter = $this->pprinter; $errorHandler->clearErrors(); try { $stmts = $parser->parse($code, $errorHandler); $params = []; if (!$stmts && $errorHandler->getErrors()) { throw new Exception('syntax error in file'); } $this->CheckStmts($stmts, $params, $file_path); } catch (Exception $e) { // echo 'Parse Error: ' . $file_path . " " . $e->getMessage() . "\n"; if ($this->collect_exceptions) { $this->addResult('[000] syntax error in file', '', 1); } } # REGEXP BASED CODES $src = preg_replace('#/\*.*?\*/#s', '', $src); $src = preg_replace('#[\r\n][ \t]*//.*#m', '', $src); $src = preg_replace('/[\r\n][ \t]*#.*/m', '', $src); # CODE 007 if (self::$database && $this->SearchInDataBase($src)) { $this->addResult('[007] looks like a well-known shell', '', 1); return true; // is not false-positive } # CODE 302 if (preg_match_all('#preg_replace' . self::$spaces . '(\(((?>[^()]+)|(?-2))*\))#i', $src, $regs)) { foreach ($regs[1] as $i => $val) { $code = $regs[0][$i]; $spiltter = $val[2]; $spl = $spiltter === '#' ? '~' : '#'; if (preg_match($spl . preg_quote($spiltter) . '[imsxADSUXju]*e[imsxADSUXju]*[\'"]' . $spl, $val)) { $subj = '[302] preg_replace_eval'; $checksum = $this->CalcChecksum($file_path, $code, $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $code, self::CalcCrit($subj), $checksum); } } } } $content = preg_replace('/[\'"]\s*?\.\s*?[\'"]/smi', '', $src); # CODE 321 if (preg_match_all('#[A-Za-z0-9+/]{20,}=*#i', $content, $regs)) { foreach ($regs[0] as $val) { $code = $val; $val = base64_decode($val); if (preg_match('#(' . self::$request . '|' . self::$functions . '|' . self::$evals_reg . '|' . self::$black_reg . ')#i', $val)) { $subj = '[321] base64_encoded code'; $checksum = $this->CalcChecksum($file_path, $code, $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $code, self::CalcCrit($subj), $checksum); } } } } // unset($content); # CODE 337 if (preg_match_all('#' . self::$black_reg . '#i', $content, $regs)) { $code = implode(' | ', $regs[0]); $subj = '[337] strings from black list'; $checksum = $this->CalcChecksum($file_path, $code, $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $code, self::CalcCrit($subj), $checksum); } } # CODE 400 /* if (preg_match_all('#\$(USER|GLOBALS..USER..)->Authorize' . self::$spaces . '(\(((?>[^()]+)|(?-2))*\))#i', $src, $regs)) {*/ // // foreach ($regs[3] as $i => $val) { // $code = $regs[0][$i]; // // $val = explode(',', $val)[0]; // // if (preg_match('#' . self::$request . '|([\'"]?0?[xbe]?[0-9]+[\'"]?)#', $val)) { // $subj = '[400] bitrix auth'; // if ($checksum = $this->CalcChecksum($file_path, $code, $subj) && $this->IsFalsePositive($checksum)) { // $this->results[] = [$subj, $code]; // } // // } // } // } # CODE 500 if (preg_match_all('#[\'"](php://filter|phar://)#i', $content, $regs)) { foreach ($regs[0] as $i => $value) { $code = $value; $subj = '[500] php wrapper'; $checksum = $this->CalcChecksum($file_path, $code, $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $code, self::CalcCrit($subj), $checksum); } } } # CODE 630 if (preg_match('#[a-z0-9+=/\n\r]{255,}#im', $src, $regs)) { $code = $regs[0]; if (!preg_match('#data:image/[^;]+;base64,[a-z0-9+=/]{255,}#i', $src, $regs)) { $subj = '[630] long line'; $checksum = $this->CalcChecksum($file_path, $code, $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $code, self::CalcCrit($subj), $checksum); } } } # CODE 640 if (preg_match_all('#exif_read_data\(#i', $src, $regs)) { foreach ($regs[0] as $i => $value) { $code = $value; $subj = '[640] strange exif'; $checksum = $this->CalcChecksum($file_path, $code, $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $code, self::CalcCrit($subj), $checksum); } } } # CODE 663 if (preg_match("#^.*([\x01-\x08\x0b\x0c\x0f-\x1f])#m", $src, $regs)) { $code = $regs[1]; if (!preg_match('#^\$ser_content = #', $regs[0])) { $subj = '[663] binary data'; $checksum = $this->CalcChecksum($file_path, $code, $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $code, self::CalcCrit($subj), $checksum); } } } # CODE 665 if ($file_path && preg_match_all('#(?:\\\\x[a-f0-9]{2}|\\\\[0-9]{2,3})+#i', $content, $regs)) { $regs = $regs[0]; $all = implode("", $regs); if (count($regs) > 1) { $regs[] = $all; } $found = false; foreach ($regs as $code) { $val = stripcslashes($code); if (preg_match('#(' . self::$request . '|' . self::$functions . '|' . self::$evals_reg . '|' . self::$black_reg . ')#i', $val)) { $subj = '[665] encoded code'; $checksum = $this->CalcChecksum($file_path, $code, $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $code, self::CalcCrit($subj), $checksum); $found = true; } } elseif (preg_match_all('#[A-Za-z0-9+/]{20,}=*#i', $val, $regs)) { foreach ($regs[0] as $val) { $val = base64_decode($val); if (preg_match('#(' . self::$request . '|' . self::$functions . '|' . self::$evals_reg . '|' . self::$black_reg . ')#i', $val)) { $subj = '[665] encoded code'; $checksum = $this->CalcChecksum($file_path, $code, $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $code, self::CalcCrit($subj), $checksum); $found = true; } } } } } if (!$found && strlen($all) / filesize($file_path) > 0.1) { $subj = '[665] chars by code'; $checksum = $this->CalcChecksum($file_path, $code, $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $code, self::CalcCrit($subj), $checksum); } } } unset($src); unset($content); return !empty($this->results); } function CheckStmts($stmts, &$params, $file_path, $in_closure = false) { $nodeFinder = $this->nodeFinder; $pprinter = $this->pprinter; $nodeFinder->find($stmts, function (Node $node) use (&$file_path) { if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) { $this->CheckStmts($node->stmts, $node->params, $file_path); $node->stmts = []; } elseif ($node instanceof Node\Expr\Closure) { $this->CheckStmts($node->stmts, $node->params, $file_path, true); $node->stmts = []; } elseif ($node instanceof Node\Expr\ArrowFunction) { $this->CheckStmts($node->expr, $node->params, $file_path, true); $node->stmts = []; } }); $nodes = ['assigns' => [], 'variables' => [], 'params' => [], 'foreaches' => [], 'calls' => [], 'evals' => [], 'backticks' => [], 'includes' => [], 'auth' => [], 'mtds' => [], 'strings' => []]; $extract = false; $nodeFinder->find($stmts, function (Node $node) use (&$nodes, &$pprinter, &$extract) { if ($node->getComments()) { $node->setAttribute('comments', []); } // if ($node instanceof Node\Stmt\Function_) { // $name = $node->name instanceof Node\Identifier ? $node->name->toString() : false // if (is_string($name) && self::isVarStrange('$' . trim($name, '_'))) { // $this->addResult('[110] strange function name', $name, 0.3); // } // } if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) { $nodes['assigns'][] = $node; } if ($node instanceof Node\Expr\Variable) { $nodes['variables'][] = $node; } if ($node instanceof Node\Stmt\Foreach_) { $nodes['foreaches'][] = $node; } if ($node instanceof Node\Expr\FuncCall) { $nodes['calls'][] = $node; if ($node->name instanceof Node\Name && $node->name->toLowerString() === 'extract') { if (count($node->args) < 2 || $pprinter->prettyPrintExpr($node->args[1]->value) !== 'EXTR_SKIP') { $extract = true; } } } if ($node instanceof Node\Expr\Eval_) { $nodes['evals'][] = $node; } if ($node instanceof Node\Expr\ShellExec) { $nodes['backticks'][] = $node; } if ($node instanceof Node\Expr\Include_) { $nodes['includes'][] = $node; } if ($node instanceof Node\Expr\MethodCall && $node->name instanceof Node\Identifier && $node->name->toLowerString() == 'authorize' && preg_match('/user|globals/i', $pprinter->prettyPrintExpr($node->var))) { $nodes['auth'][] = $node; } if ($node instanceof Node\Expr\MethodCall && $node->name instanceof Node\Expr\Variable && $node->var instanceof Node\Expr\ArrayDimFetch && $node->var->var instanceof Node\Expr\Variable && $node->var->var->name == 'GLOBALS' && (!($node->var->dim instanceof Node\Scalar\String_) || $node->var->dim->value == 'USER') ) { $nodes['auth'][] = $node; } if ($node instanceof Node\Expr\StaticCall) { $nodes['mtds'][] = $node; } if ($node instanceof Node\Scalar\String_ && $node->value) { $nodes['strings'][] = $node; } # this is dirty hack if ($node instanceof Node\Expr\ArrayDimFetch && $node->var instanceof Node\Expr\Variable && $node->var->name == '_SERVER') { $dim = $node->dim instanceof Node\Scalar\String_ ? $node->dim->value : "qwerty"; if (!preg_match('/^(?:DOCUMENT_ROOT|SERVER_ADDR|REMOTE_ADDR|SERVER_NAME|HTTPS|SERVER_PORT|REMOTE_PORT)$/', $dim)) { $node->var->name = '_REQUEST'; } } // if ($node instanceof Node\Expr\ArrayDimFetch && $node->var instanceof Node\Expr\Variable && $node->var->name == 'GLOBALS') { // $dim = $node->dim instanceof Node\Scalar\String_ ? $node->dim->value : "qwerty"; // if (preg_match('/^(?:_GET|_POST|_REQUEST|_COOKIE|_FILES|_SERVER)$/', $dim)) { // $node->var->name = '_REQUEST'; // } // } }); $vars_names = []; $vars = [ 'request' => ['_GET' => true, '_POST' => true, '_REQUEST' => true, '_COOKIE' => true, '_FILES' => true], 'params' => [], 'from_request' => [], 'crypted' => [], 'assigned' => ['_GET' => true, '_POST' => true, '_REQUEST' => true, '_COOKIE' => true, '_SESSION' => true, '_SERVER' => true, '_FILES' => true, 'this' => true, 'USER' => true, 'DB' => true, 'APPLICATION' => true], 'values' => [], 'closures' => [], ]; foreach ($nodes['variables'] as $var) { if (is_string($var->name)) { $var = '$' . $var->name; } else { $var = $this->pprinter->prettyPrintExpr($var->name); } $vars_names[] = $var; } $vars_names = array_unique($vars_names); foreach ($params as $param) { $n = substr($this->pprinter->prettyPrintExpr($param->var), 1); $vars['params'][$n] = true; $vars['assigned'][$n] = true; if ($param->type instanceof Node\Name\FullyQualified && implode('', $param->type->parts) == 'Closure') { $vars['closures'][] = $n; } } foreach ($nodes['assigns'] as $fnd) { $n = substr($this->pprinter->prettyPrintExpr($fnd->var), 1); $vars['assigned'][$n] = true; if ($fnd->expr instanceof Node\Expr\Closure) { $vars['closures'][] = $n; } } foreach ($nodes['foreaches'] as $fnd) { if ($fnd->keyVar) { $n = substr($this->pprinter->prettyPrintExpr($fnd->keyVar), 1); $vars['assigned'][$n] = true; } if ($fnd->valueVar) { $n = substr($this->pprinter->prettyPrintExpr($fnd->valueVar), 1); $vars['assigned'][$n] = true; } } for ($_ = 0; $_ < 2; $_++) { $res = []; foreach ($nodes['assigns'] as $node) { $flag = $nodeFinder->findFirst($node->expr, function (Node $node) use (&$vars) { return $node instanceof Node\Expr\Variable && is_string($node->name) && $node->name && (isset($vars['request'][$node->name]) || isset($vars['from_request'][$node->name])); } ); if ($flag) { $res[] = $node; } } foreach ($res as $fnd) { $n = substr($this->pprinter->prettyPrintExpr($fnd->var), 1); $vars['from_request'][$n] = true; } } for ($_ = 0; $_ < 1; $_++) { $res = []; foreach ($nodes['assigns'] as $node) { $flag = $nodeFinder->findFirst($node->expr, function (Node $node) { return $node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && in_array($node->name->toLowerString(), self::$cryptors, true); } ); if ($flag) { $res[] = $node; } } foreach ($res as $fnd) { $n = substr($this->pprinter->prettyPrintExpr($fnd->var), 1); $vars['crypted'][$n] = true; } } foreach ($nodes['assigns'] as $fnd) { $n = substr($this->pprinter->prettyPrintExpr($fnd->var), 1); $tmp = $this->parseValue($fnd->expr, $vars); if (!isset($vars['values'][$n])) { $vars['values'][$n] = $tmp; } else { $vars['values'][$n] .= '|' . $tmp; } } // print_r($vars['values']); $crypto_vars = array_keys($vars['crypted']); # CODE 300 $res = []; $config = self::genConfig(['params' => true, 'assigned' => $extract]); foreach ($nodes['evals'] as $node) { [$flag, $comment] = $this->CheckArg($node->expr, $vars, $config); if ($flag) { $node->setAttribute('comment', $comment); $res[] = $node; } } $this->CheckResults($res, '[300] eval', $file_path); # CODE 298 $sql_map = [ 'mysqli_connect' => [0, 1, 2, 3], 'mysqli_query' => [1], 'mysqli_real_query' => [1], # i know its removed at php7 'mysql_connect' => [0, 1, 2], 'mysql_query' => [1], 'mysql_db_query' => [0, 1], ]; $config = self::genConfig(['params' => true]); $res = $this->checkFuncCalls($nodes['calls'], $sql_map, $nodeFinder, $vars, $config); $this->CheckResults($res, '[298] mysql function', $file_path); # CODE 299 $mail_map = [ 'mail' => [0], 'bxmail' => [0], ]; $config = self::genConfig(); $res = $this->checkFuncCalls($nodes['calls'], $mail_map, $nodeFinder, $vars, $config); $this->CheckResults($res, '[299] mail function', $file_path); # CODE 300 $evals_map = [ 'assert' => [0], 'create_function' => [0], 'exec' => [0], 'passthru' => [0], 'pcntl_exec' => [0], 'popen' => [0], 'proc_open' => [0], 'set_include_path' => [0], 'shell_exec' => [0], 'system' => [0] ]; $config = self::genConfig(['params' => true, 'assigned' => $extract]); $res = $this->checkFuncCalls($nodes['calls'], $evals_map, $nodeFinder, $vars, $config); $this->CheckResults($res, '[300] command injection', $file_path); # CODE 301 $files_map = [ 'copy' => [1], // 0,1 'file_get_contents' => [0], 'file_put_contents' => [0], 'move_uploaded_file' => [1], // 0,1 'opendir' => [0], 'fopen' => [0] ]; $config = self::genConfig(['concat' => false, 'files' => false, 'value' => false]); $res = $this->checkFuncCalls($nodes['calls'], $files_map, $nodeFinder, $vars, $config); $this->CheckResults($res, '[301] file operations', $file_path); /* $files_map = [ 'file_put_contents' => [1], 'fwrite' => [1], 'fputs' => [1], ]; $config = self::genConfig(['value'=> true, 'recursive' => true, 'concat'=> false]); $res = $this->checkFuncCalls($nodes['calls'], $files_map, $nodeFinder, $vars, $config); $this->CheckResults($res, '[302] file operations', $file_path); */ # CODE 302 $f_w_clb_map = ['call_user_func' => [0], 'call_user_func_array' => [0], 'forward_static_call' => [0], 'forward_static_call_array' => [0], 'register_shutdown_function' => [0], 'register_tick_function' => [0], 'ob_start' => [0], 'usort' => [1], 'uasort' => [1], 'uksort' => [1], 'array_walk' => [1], 'array_walk_recursive' => [1], 'array_reduce' => [1], 'array_intersect_ukey' => [2], 'array_uintersect' => [2], 'array_uintersect_assoc' => [2], 'array_intersect_uassoc' => [2], 'array_uintersect_uassoc' => [2, 3], 'array_diff_ukey' => [2], 'array_udiff' => [2], 'array_udiff_assoc' => [2], 'array_diff_uassoc' => [2], 'array_udiff_uassoc' => [2, 3], 'array_filter' => [1], 'array_map' => [0], 'mb_ereg_replace_callback' => [1] ]; $config = self::genConfig(['assigned' => $extract]); $res = $this->checkFuncCalls($nodes['calls'], $f_w_clb_map, $nodeFinder, $vars, $config); $this->CheckResults($res, '[302] unsafe callable argument', $file_path); # CODE 303 $some_calls = array_filter($nodes['calls'], function (Node $node) use (&$danger) { return $node->name instanceof Node\Name && $node->name->toLowerString() == 'create_function'; } ); $res = []; foreach ($some_calls as $node) { $flag = $nodeFinder->findFirst($node->args, function (Node $node) { return ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && (!function_exists($node->name->toLowerString()) || in_array($node->name->toLowerString(), self::$cryptors, true) || in_array($node->name->toLowerString(), self::$string_change, true)) ) || ($node instanceof Node\Scalar\String_ && preg_match('/(?:assert|' . implode('|', self::$cryptors) . ')/i', $node->value)); } ); if ($flag) { $res[] = $node; } } $this->CheckResults($res, '[303] create_function', $file_path); # CODE 304 $clb = ['filter_input', 'filter_input_array', 'filter_var', 'filter_var_array']; $some_calls = array_filter($nodes['calls'], function (Node $node) use (&$clb) { return $node->name instanceof Node\Name && in_array($node->name->toLowerString(), $clb, true); } ); $res = []; foreach ($some_calls as $node) { if (preg_match_all('#(?:_POST|_GET|_COOKIE|_REQUEST|FILTER_CALLBACK|1024|filter_input|filter_var)|' . self::$evals_reg . '|' . self::$functions . '#i', $pprinter->prettyPrint($node->args)) > 1) { $res[] = $node; } } $this->CheckResults($res, '[304] filter_callback', $file_path); # CODE 305 $res = []; foreach ($nodes['evals'] as $node) { $flag = $nodeFinder->findFirst($node->expr, function (Node $node) { return ($node instanceof Node\Expr\FuncCall && ( ($node->name instanceof Node\Name && !function_exists($node->name->toLowerString())) || ($node->name instanceof Node\Expr\Variable) || ($node->name instanceof Node\Expr\ArrayDimFetch) ) ); } ); if ($flag) { $node->setAttribute('comment', 'strange code'); $res[] = $node; } } $this->CheckResults($res, '[305] strange function and eval', $file_path); # CODE 306 $eregi_map = [ 'mb_eregi_replace' => [1], 'mb_ereg_replace' => [1], ]; $config = self::genConfig(); $res = $this->checkFuncCalls($nodes['calls'], $eregi_map, $nodeFinder, $vars, $config); $this->CheckResults($res, '[302] eregi', $file_path); # CODE 307 $res = []; foreach ($nodes['mtds'] as $node) { $class = $node->class instanceof Node\Name ? $node->class->toString() : ''; $mtd = $node->name instanceof Node\Identifier ? $node->name->toString() : ''; if (!$class || !$mtd) { continue; } $class_method = "$class::$mtd"; foreach (self::$mehtods as $mtd) { if (stripos($class_method, $mtd) !== false) { $arg = isset($node->args[0]) ? $node->args[0] : false; [$flag, $comment] = $this->CheckArg($arg->value, $vars, $config); if ($flag) { $res[] = $node; $node->setAttribute('comment', $comment); } break; } } } $this->CheckResults($res, '[307] danger method', $file_path); # CODE 400 $config = self::genConfig(['hardcoded' => true]); $res = []; foreach ($nodes['auth'] as $node) { $arg = isset($node->args[0]) ? $node->args[0] : false; $flag = false; $comment = ''; [$flag, $comment] = $this->CheckArg($arg->value, $vars, $config); if ($flag) { $node->setAttribute('comment', $comment); $res[] = $node; } } $this->CheckResults($res, '[400] bitrix auth', $file_path); # CODE 600 $res = []; $config = self::genConfig(['concat' => false, 'value' => false]); foreach ($nodes['includes'] as $node) { $flag = false; $comment = ''; $inc = $pprinter->prettyPrintExpr($node->expr); if (preg_match('/\.(gif|png|jpg|jpeg|var|pdf|exe)/i', $inc)) { $flag = true; $comment = 'gif|png|jpg|jpeg|var|pdf|exe'; } elseif (preg_match('#(https?|ftps?|compress\.zlib|php|glob|data|phar)://#i', $inc)) { $flag = true; $comment = 'wrapper'; } else { [$flag, $comment] = $this->CheckArg($node->expr, $vars, $config); // $flag = $flag || $nodeFinder->findFirst($node->expr, // function (Node $node) use (&$vars) { // return $node instanceof Node\Expr\Variable && is_string($node->name) && $node->name && (isset($vars['request'][$node->name]) || isset($vars['crypted'][$node->name])); // } // ); } if ($flag) { $node->setAttribute('comment', $comment); $res[] = $node; } } $this->CheckResults($res, '[600] strange include', $file_path); # CODE 610 615 620 $checked = []; foreach ($nodes['variables'] as $var) { $v = $this->pprinter->prettyPrintExpr($var); if (in_array($v, $checked, true)) { continue; } $checked[] = $v; if (preg_match('#\$_{3,}#i', $v) || preg_match('#\$\{.*?(?:->|::|\()#i', $v)) { $subj = '[610] strange vars'; $checksum = $this->CalcChecksum($file_path, $v, $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $v, self::CalcCrit($subj), $checksum); } } if (preg_match('#\${["\']\\\\x[0-9]{2}[a-z0-9\\\\]+["\']}#i', $v)) { $subj = '[615] hidden vars'; $checksum = $this->CalcChecksum($file_path, $v, $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $v, self::CalcCrit($subj), $checksum); } } if (preg_match("#\$(?:[\x80-\xff][_\x80-\xff]*|_(?:[\x80-\xff][_\x80-\xff]*|_[_\x80-\xff]+))" . self::$spaces . '=#i', $v)) { $subj = '[620] binary vars'; $checksum = $this->CalcChecksum($file_path, $v, $subj); if (!$this->IsFalsePositive($checksum)) { $this->addResult($subj, $v, self::CalcCrit($subj), $checksum); } } } # CODE 650 $res = []; $config = self::genConfig(); foreach ($nodes['calls'] as $node) { $flag = false; $comment = ''; if ($node->name instanceof Node\Expr\Variable) { $var = is_string($node->name) ? '$' . $node->name : $this->pprinter->prettyPrintExpr($node->name); $name = substr($var, 1); [$flag, $comment] = $this->CheckArg($node->name, $vars, $config); if (!$flag) { $flag = $nodeFinder->findFirst($node->args, function (Node $node) { return $node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Expr\Variable; }); if ($flag) { $comment = 'functions inside'; } } if (!$flag) { foreach ($node->args as $arg) { if ($arg instanceof Node\Arg) { [$flag, $comment] = $this->CheckArg($arg->value, $vars, $config); if ($flag) { $comment = $comment; break; } } } } if (!$flag && !in_array($name, $vars['closures'], true) && !$in_closure) { $comment = 'other'; if (self::isVarStrange($var)) { $comment = 'strange var'; } $flag = true; } if ($flag) { $node->setAttribute('comment', $comment); $res[] = $node; } } } $this->CheckResults($res, '[650] variable as a function', $file_path); # CODE 660 $config = self::genConfig(); $res = []; foreach ($nodes['calls'] as $node) { if ($node->name instanceof Node\Expr\ArrayDimFetch) { $res[] = $node; [$flag, $comment] = $this->CheckArg($node->name, $vars, $config); if (!$flag) { foreach ($node->args as $arg) { [$flag, $comment] = $this->CheckArg($arg->value, $vars, $config); if ($flag) { break; } } } if ($flag) { $node->setAttribute('comment', $comment); } } } $this->CheckResults($res, '[660] array member as a function', $file_path); # CODE 662 $config = self::genConfig(); $res = []; foreach ($nodes['calls'] as $node) { if ($node->name instanceof Node\Expr\FuncCall) { $res[] = $node; [$flag, $comment] = $this->CheckArg($node->name, $vars, $config); if ($flag) { $node->setAttribute('comment', $comment); } } } $this->CheckResults($res, '[662] function return as a function', $file_path); $res = []; foreach ($nodes['calls'] as $node) { if ($node->name instanceof Node\Scalar\String_ || $node->name instanceof Node\Expr\BinaryOp) { $res[] = $node; [$flag, $comment] = $this->CheckArg($node->name, $vars, $config); if ($flag) { $node->setAttribute('comment', $comment); } } } $this->CheckResults($res, '[663] strange function', $file_path); # CODE 665 $res = []; foreach ($nodes['strings'] as $node) { $str = $node->value; $str2 = base64_decode($str); if ($str2 && preg_match('#(' . self::$request . '|' . self::$functions . '|' . self::$evals_reg . '|' . self::$black_reg . ')#i', $str2)) { $res[] = $node; continue; } if (preg_match('/^[a-z\s0-9:._-]+$/i', $str)) { continue; } if (strpos($str, '<?') !== false && preg_match('#(' . self::$request . '|' . self::$functions . '|' . self::$evals_reg . '|' . self::$black_reg . ')#i', $str)) { $subscan = new CBitrixXscan(); $subscan->collect_exceptions = false; // $str2 = "<?php\n" . $str; if ($subscan->CheckCode($str)) { $res[] = $node; } unset($str, $str2, $subscan); } } $this->CheckResults($res, '[665] encoded code', $file_path); # CODE 887 $this->CheckResults($nodes['backticks'], '[887] backticks', $file_path); unset($stmts, $nodes, $some_calls, $res, $req, $code); } public static function genConfig($options = false) { $config = self::$default_config; if (is_array($options)) { foreach ($options as $key => $val) { $config[$key] = $val; } } return $config; } public function checkFuncCalls(&$all_calls, &$funcs_map, &$nodeFinder, &$vars, &$config) { $funcs = array_keys($funcs_map); $some_calls = array_filter($all_calls, function (Node $node) use (&$funcs) { return $node->name instanceof Node\Name && in_array($node->name->toLowerString(), $funcs, true); }); $result = []; foreach ($some_calls as $node) { $ret = false; $func = $node->name->toLowerString(); $comment = ''; foreach ($funcs_map[$func] as $i) { if (!isset($node->args[$i]) || $ret) { continue; } if ($node->args[$i] instanceof Node\Arg && $node->args[$i]->value instanceof Node\Expr\Closure) { continue; } $arg = $node->args[$i]->value; [$ret, $comment] = $this->CheckArg($arg, $vars, $config); } if ($ret && $comment) { $node->setAttribute('comment', $comment); $result[] = $node; } } return $result; } public function parseValue($node, &$vars) { $ret = ''; $temp_name = false; while ($node instanceof Node\Expr\ArrayDimFetch || $node instanceof Node\Expr\PropertyFetch) { if ($node instanceof Node\Expr\ArrayDimFetch && $node->dim instanceof Node\Scalar\String_ and $node->dim->value == 'tmp_name') { $temp_name = true; } if ($node instanceof Node\Expr\ArrayDimFetch && $node->var instanceof Node\Expr\Variable && $node->var->name == 'GLOBALS' && $node->dim instanceof Node\Scalar\String_) { $node = new Node\Expr\Variable($node->dim->value); } else { $node = $node->var; } } if ($node instanceof Node\Expr\Variable) { $name = $node->name; if (is_string($name) && $name) { if (isset($vars['values'][$name])) { $ret = $vars['values'][$name]; } elseif (isset($vars['request'][$name]) && !$temp_name) { $ret = '$_REQUEST'; } elseif (isset($vars['from_request'][$name])) { $ret = '$_FROM_REQUEST'; } elseif (isset($vars['crypted'][$name])) { $ret = 'CRYPTED'; } elseif (isset($vars['params'][$name])) { $ret = 'PARAMS'; } } elseif (!is_string($name)) { $ret = $name = $this->parseValue($name, $vars); } } elseif ($node instanceof Node\Expr\BinaryOp) { $left = $this->parseValue($node->left, $vars); $right = $this->parseValue($node->right, $vars); if ($node instanceof Node\Expr\BinaryOp\Div && (int)$right != 0) { $ret = (string)((int)$left / (int)$right); } elseif ($node instanceof Node\Expr\BinaryOp\Mul) { $ret = (string)((int)$left * (int)$right); } elseif ($node instanceof Node\Expr\BinaryOp\Minus) { $ret = (string)((int)$left - (int)$right); } elseif ($node instanceof Node\Expr\BinaryOp\Plus) { $ret = (string)((int)$left + (int)$right); } elseif ($node instanceof Node\Expr\BinaryOp\BitwiseXor) { $ret = (string)($left ^ $right); } else { $ret = $left . $right; } } elseif ($node instanceof Node\Scalar\Encapsed) { foreach ($node->parts as $part) { $part = $this->parseValue($part, $vars); $ret .= $part; } } elseif ($node instanceof Node\Scalar\LNumber || $node instanceof Node\Scalar\DNumber || $node instanceof Node\Scalar\String_ || $node instanceof Node\Scalar\EncapsedStringPart ) { $ret = (string)$node->value; } elseif ($node instanceof Node\Expr\FuncCall) { $name = $node->name instanceof Node\Name ? $node->name->toLowerString() : "\$v"; if ($name === 'chr') { $v = $this->parseValue($node->args[0]->value, $vars); $ret = chr((int)$v); } else { $args = []; foreach ($node->args as $arg) { if ($arg instanceof Node\Arg) { $args[] = $this->parseValue($arg->value, $vars); } } $ret = "$name(" . implode(",", $args) . ")"; [$a, $b] = self::checkString($ret); $ret = $a ? $b : ''; unset($args); } } elseif ($node instanceof Node\Expr\Ternary) { $if = $this->parseValue($node->if, $vars); $ret = $if ?: $this->parseValue($node->else, $vars); } elseif ($node && property_exists($node, 'expr')) { return $this->parseValue($node->expr, $vars); } return $ret; } public function CheckArg($arg, &$vars, &$config) { $ret = false; $comment = ''; $temp_name = false; while ($arg instanceof Node\Expr\ArrayDimFetch || $arg instanceof Node\Expr\PropertyFetch) { if ($arg instanceof Node\Expr\ArrayDimFetch && $arg->dim instanceof Node\Scalar\String_ and $arg->dim->value == 'tmp_name') { $temp_name = true; } if ($arg instanceof Node\Expr\ArrayDimFetch && $arg->var instanceof Node\Expr\Variable && $arg->var->name == 'GLOBALS' && $arg->dim instanceof Node\Scalar\String_) { $arg = new Node\Expr\Variable($arg->dim->value); } else { $arg = $arg->var; } } $temp_name = $temp_name && ($arg instanceof Node\Expr\Variable and is_string($arg->name) && $arg->name == '_FILES'); while ($arg instanceof Node\Expr\ConstFetch) { $arg = $arg->name; } if ($config['hardcoded'] && ( $arg instanceof Node\Scalar\LNumber || $arg instanceof Node\Scalar\DNumber || $arg instanceof Node\Scalar\String_ || $arg instanceof Node\Scalar\Encapsed) ) { $comment = 'hardcoded value'; $ret = true; } elseif ($arg instanceof Node\Expr\Variable) { $name = $arg->name; if (!is_string($name)) { $ret = true; $comment = 'crypted var'; } else { $ret = is_string($name) && $name && ($config['files'] || (!$config['files'] && !$temp_name)) && ( ($config['request'] && isset($vars['request'][$name]) && $comment = 'request') || ($config['from_request'] && isset($vars['from_request'][$name]) && $comment = 'var from request') || ($config['crypted'] && isset($vars['crypted'][$name]) && $comment = 'crypted var') || ($config['assigned'] && !isset($vars['assigned'][$name]) && $comment = 'var was not assigned') || ($config['params'] && isset($vars['params'][$name]) && $comment = 'var from params') || ($name == 'GLOBALS' && $comment = 'strange globals') ); } } elseif ($arg instanceof Node\Expr\FuncCall && $arg->name instanceof Node\Name) { $name = $arg->name->toLowerString(); $ret = in_array($name, self::$evals, true) || in_array($name, ['getenv', 'debug_backtrace'], true) || ($config['crypted'] && in_array($name, self::$cryptors, true)); if (!$ret) { foreach ($arg->args as $argv) { [$ret, $comment] = $this->CheckArg($argv->value, $vars, $config); if ($ret) { break; } } } else { $comment = 'danger function'; } } elseif ($arg instanceof Node\Scalar\String_) { $comment = 'danger function'; $ret = preg_match('/^(' . implode('|', self::$evals) . '|call_user_func|getenv)$/i', $arg->value); } elseif ($arg instanceof Node\Scalar\EncapsedStringPart) { $comment = 'danger function'; $ret = preg_match('/(' . implode('|', self::$evals) . '|call_user_func|getenv)/i', $arg->value); } elseif ($arg instanceof Node\Name) { $comment = 'danger function'; $func = $arg->toLowerString(); $ret = preg_match('/(' . implode('|', self::$evals) . '|call_user_func|getenv)/i', $func); } elseif ($arg instanceof Node\Expr\BinaryOp\Concat || $arg instanceof Node\Expr\BinaryOp\Coalesce) { [$a, $b] = $this->CheckArg($arg->left, $vars, $config); if ($a) { [$ret, $comment] = [$a, $b]; } else { [$a, $b] = $this->CheckArg($arg->right, $vars, $config); if ($a) { [$ret, $comment] = [$a, $b]; } elseif ($config['concat']) { $comment = 'strange concatination'; $ret = true; } } } elseif ($arg instanceof Node\Scalar\Encapsed) { foreach ($arg->parts as $part) { [$a, $b] = $this->CheckArg($part, $vars, $config); if ($a) { [$ret, $comment] = [$a, $b]; } } } elseif ($arg instanceof Node\Expr\Ternary) { [$a, $b] = $this->CheckArg($arg->if, $vars, $config); if ($a) { [$ret, $comment] = [$a, $b]; } else { [$a, $b] = $this->CheckArg($arg->else, $vars, $config); if ($a) { [$ret, $comment] = [$a, $b]; } } } // print_r($arg); if (!$ret && $config['value']) { $val = $this->parseValue($arg, $vars); if ($config['recursive']) { $subscan = new CBitrixXscan(); $subscan->collect_exceptions = false; $res = $subscan->CheckCode($val); if ($res) { [$ret, $comment] = [true, 'recursive']; } unset($subscan); } else { [$ret, $comment] = self::checkString($val); } } return [$ret, $comment]; } public static function checkString($val) { $ret = ''; $comment = ''; if (preg_match('/BXS_(?:EVAL|CRYPTED|BLACKLIST|REQUEST)/', $val, $m)) { $ret = true; $comment = $m[0]; } elseif (preg_match('/\b(' . implode('|', self::$evals) . '|getenv)\b/i', $val)) { $ret = true; $comment = 'BXS_EVAL'; } elseif (preg_match('/\b(' . implode('|', self::$cryptors) . '|CRYPTED)\b/i', $val)) { $ret = true; $comment = 'BXS_CRYPTED'; } elseif (preg_match('#\b' . self::$black_reg . '\b#i', $val)) { $ret = true; $comment = 'BXS_BLACKLIST'; } elseif (preg_match('/(\$_REQUEST|\$_FROM_REQUEST)/i', $val)) { $ret = true; $comment = 'BXS_REQUEST'; } return [$ret, $comment]; } public static function isVarStrange($var) { $ret = 0; $ret = preg_match('/^\$_?([0o]+|[1li]+)$/i', $var); // obfusacator $ret = $ret || preg_match('/^\$__/i', $var) || $var == '$_'; $ret = $ret || preg_match('/__/', $var); $ret = $ret || preg_match('/^\$_*[a-z0-9]{1,2}$/i', $var); // very short $ret = $ret || preg_match('/\d{2,}$/i', $var); // 2+ digits in the end $ret = $ret || preg_match_all('/[A-Z][a-z][A-Z]/', $var) > 1; // CaSe dAnCe $ret = $ret || preg_match('/[^$a-z0-9_]/i', $var); $ret = $ret || preg_match('/[a-z]+[0-9]+[a-z]+/i', $var); // digits in centre $ret = $ret || (preg_match_all('#[qwrtpsdfghjklzxcvbnm]{4,}#i', $var, $regs) && (strlen(implode('', $regs[0])) / strlen($var) > 0.4)); return $ret > 0; } public static function CalcCrit($subj, $com = '') { if (!isset(self::$scoring[$subj])) { die("error: " . $subj); } static $nums = [ 'self' => 0, 'strange concatination' => 1, 'hardcoded value' => 2, 'request' => 3, 'danger function' => 4, 'var from params' => 5, 'var was not assigned' => 6, 'crypted var' => 7, 'var from request' => 8, ]; $self = self::$scoring[$subj][0]; if ($com == 'other') { $arg = 0.3; } else { $num = isset($nums[$com]) ? $nums[$com] : 0; $arg = isset(self::$scoring[$subj][$num]) ? self::$scoring[$subj][$num] : 1; } return round($self * $arg, 2); } public function CheckResults(&$res, $subj, $file_path) { foreach ($res as $r) { $code = $this->pprinter->prettyPrintExpr($r); $com = $r->getAttribute('comment', ''); $crit = self::CalcCrit($subj, $com); $checksum = $this->CalcChecksum($file_path, $code, $subj); $str = defined('XSCAN_DEBUG') ? "$subj [$com] | $crit | $checksum" : $subj; if (!$this->IsFalsePositive($checksum)) { $this->addResult($str, $code, $crit, $checksum); } } } public static function ParseNode(&$node) { if (isset($arr[0])) { foreach ($node as $v) { self::ParseNode($v); } return; } } static function CountVars($str) { $regular = '#' . self::$var . '#'; if (!preg_match_all($regular, $str, $regs)) { return 0; } $ar0 = $regs[0]; $ar0 = array_unique($ar0); $ar0 = array_filter($ar0, function ($v) { return !in_array($v, ['$_GET', '$_POST', '$_REQUEST', '$_GET', '$_SERVER', '$_FILES', '$APPLICATION', '$DB', '$USER']); }); return count($ar0); } static function StatVulnCheck($str, $bAll = false) { $regular = $bAll ? '#\$?[a-z_]+#i' : '#' . self::$var . '#'; if (!preg_match_all($regular, $str, $regs)) { return false; } $ar0 = $regs[0]; $ar1 = array_unique($ar0); $uniq = count($ar1) / count($ar0); $ar2 = []; foreach ($ar1 as $var) { if ($bAll && function_exists($var)) { $p = 0; } elseif ($bAll && preg_match('#^[a-z]{1,2}$#i', $var)) { $p = 1; } elseif (preg_match('#^\$?(function|php|csv|sql|__DIR__|__FILE__|__LINE__|DBDebug|DBType|DBName|DBPassword|DBHost|APPLICATION)$#i', $var)) { $p = 0; } elseif (preg_match('#__#', $var)) { $p = 1; } elseif (preg_match('#^\$(ar|str)[A-Z]#', $var, $regs)) { $p = 0; } elseif (preg_match_all('#([qwrtpsdfghjklzxcvbnm]{3,}|[a-z]+[0-9]+[a-z]+)#i', $var, $regs)) { $p = strlen(implode('', $regs[0])) / strlen($var) > 0.3; } else { $p = 0; } $ar2[] = $p; } $prob = array_sum($ar2) / count($ar2); if ($prob < 0.3) { return false; } if (!$bAll) { return self::StatVulnCheck($str, true); } return true; } function Search($path, $mode = 'search') { $path = str_replace('\\', '/', $path); do { $path = str_replace('//', '/', $path, $flag); } while ($flag); if (php_sapi_name() != "cli") { header('xscan-bp: ' . $path, true); } if ($this->start_time && time() - $this->start_time > $this->time_limit) { if ($mode == 'search' && !$this->break_point) { $this->break_point = $path; } if ($mode == 'count') { $this->total = 0; } return; } if ($mode == 'search' && $this->skip_path && !$this->found) { if (strpos($this->skip_path, dirname($path)) !== 0) { return; } if ($this->skip_path == $path) { $this->found = true; } } if (is_dir($path)) // dir { $p = realpath($path); if (is_link($path)) { $d = dirname($path); if (strpos($p, $d) !== false || strpos($d, $p) !== false) { return true; } } $dir = opendir($path); $isbitrix = basename($path) == 'bitrix' && is_file($path . '/.settings.php'); while ($item = readdir($dir)) { if ($item == '.' || $item == '..') { continue; } if ($isbitrix && in_array($item, ['cache', 'managed_cache', 'stack_cache', 'updates'])) { continue; } $this->Search($path . '/' . $item, $mode); } closedir($dir); } elseif (preg_match('/(?:\.htaccess|\.php[578]?)$/i', $path)) // file { if ($mode == 'count') { $this->total += 1; return; } if (!$this->skip_path || $this->found) { $this->progress += 1; $res = $this->CheckFile($path); if ($res) { $this->pushResult($path); } } } } function SystemFile($f) { static $system = [ '/bitrix/modules/controller/install/activities/bitrix/controllerremoteiblockactivity/controllerremoteiblockactivity.php', '/bitrix/activities/bitrix/controllerremoteiblockactivity/controllerremoteiblockactivity.php', '/bitrix/modules/main/classes/general/update_class.php', '/bitrix/modules/main/classes/general/file.php', '/bitrix/modules/imconnectorserver/lib/connectors/telegrambot/emojiruleset.php', '/bitrix/modules/imconnectorserver/lib/connectors/facebook/emojiruleset.php', '/bitrix/modules/main/include.php', '/bitrix/modules/main/classes/general/update_client.php', '/bitrix/modules/main/install/wizard/wizard.php', '/bitrix/modules/main/start.php', '/bitrix/modules/landing/lib/mutator.php', '/bitrix/modules/main/tools.php', '/bitrix/modules/main/lib/engine/response/redirect.php', '/bitrix/modules/main/lib/config/option.php', '/bitrix/modules/main/classes/general/main.php', '/bitrix/modules/main/lib/UpdateSystem/PortalInfo.php', '/bitrix/modules/main/lib/UpdateSystem/HashCodeParser.php', '/bitrix/modules/main/lib/UpdateSystem/ActivationSystem.php', '/bitrix/modules/main/lib/license.php', '/bitrix/modules/crm/classes/general/sql_helper.php' ]; foreach ($system as $path) { if (preg_match('#' . $path . '$#', $f)) { return true; } } return false; } function pushResult($f) { $message = []; foreach ($this->results as $res) { $message[] = $res['subj']; } if (is_array($message)) { $message = implode(' <br> ', array_unique($message)); } $stat = @stat($f); $result = (new XScanResult)->setType('file')->setSrc($f)->setScore($this->score)->setMessage($message); if (is_array($stat)) { $result->setCtime(ConvertTimeStamp($stat['ctime'], "FULL")); $result->setMtime(ConvertTimeStamp($stat['mtime'], "FULL")); } $result->setTags(implode(' ', $this->tags)); $this->result_collection[] = $result; } function SavetoDB() { if (isset($this->result_collection) && $this->result_collection) { $this->result_collection->save(true); } unset($this->result_collection); } static function ShowMsg($str, $color = 'green') { $class = $color == 'green' ? 'ui-alert-primary ui-alert-icon-info' : 'ui-alert-danger ui-alert-icon-danger'; return '<br><div class="ui-alert ' . $class . '"><span class="ui-alert-message">' . $str . '</span></div><br>'; } static function HumanSize($s) { $i = 0; $ar = ['b', 'kb', 'M', 'G']; while ($s > 1024) { $s /= 1024; $i++; } return round($s, 1) . ' ' . $ar[$i]; } static function getIsolateButton($file_path) { $file_path = htmlspecialcharsbx(CUtil::JSEscape($file_path)); return '<a class="ui-btn ui-btn-danger ui-btn-sm" style="text-decoration: none; color: #ffffff;" onclick="xscan_prison(\'' . $file_path . '\')">' . GetMessage("BITRIX_XSCAN_ISOLATE") . '</a>'; } static function getUnIsolateButton($file_path) { $file_path = htmlspecialcharsbx(CUtil::JSEscape($file_path)); return '<a class="ui-btn ui-btn-primary ui-btn-sm" style="text-decoration: none; color: #ffffff;" onclick="xscan_release(\'' . $file_path . '\')">' . GetMessage("BITRIX_XSCAN_UNISOLATE") . '</a>'; } static function getHideButton($file_path) { $file_path = htmlspecialcharsbx(CUtil::JSEscape($file_path)); return '<a class="ui-btn ui-btn-success ui-btn-sm" style="text-decoration: none; color: #ffffff;" onclick="xscan_hide(\'' . $file_path . '\')">' . GetMessage("BITRIX_XSCAN_HIDE_BTN") . '</a>'; } static function getFileWatchLink($file_path) { return sprintf( '<a href="?action=showfile&file=%s">%s</a>', urlencode($file_path), htmlspecialcharsbx($file_path) ); } static function getFileWatchButton($file_path) { return sprintf( '<a class="ui-btn ui-btn-sm" style="text-decoration: none; color: #ffffff;" target="_blank" href="?action=showfile&file=%s">' . GetMessage("BITRIX_XSCAN_WATCH_EVENT") . '</a>', urlencode($file_path) ); } static function getEventWatchLink($event, $table, $id) { return sprintf( '<a target="_blank" href="/bitrix/admin/perfmon_row_edit.php?table_name=%s&pk[ID]=%d">%s</a>', $table, $id, htmlspecialcharsbx($event) ); } static function getEventWatchButton($table, $id) { return sprintf( '<a class="ui-btn ui-btn-sm" target="_blank" style="text-decoration: none; color: #ffffff;" href="/bitrix/admin/perfmon_row_edit.php?table_name=%s&pk[ID]=%d">' . GetMessage("BITRIX_XSCAN_WATCH_EVENT") . '</a>', $table, $id ); } static function getTotal($filter) { return XScanResultTable::getCount($filter); } static function getList($filter, $nav, $sort) { $output = []; $results = XScanResultTable::getList([ 'filter' => $filter, 'offset' => $nav->getOffset(), 'limit' => $nav->getlimit(), 'order' => $sort['sort'] ]); foreach ($results as $result) { if ($result['TYPE'] === 'file') { $type = $result['MESSAGE']; $f = $result['SRC']; $code = preg_match('#\[([0-9]+)\]#', $type, $regs) ? $regs[1] : 0; $fu = urlencode(trim($f)); $bInPrison = strpos('[100]', $type) === false; if (!file_exists($f) && file_exists($new_f = preg_replace('#\.php[578]?$#i', '.ph_', $f))) { $bInPrison = false; $f = $new_f; $fu = urlencode(trim($new_f)); } $action = ''; if (preg_match('/\.ph[_p][578]?$/i', $f)) { $action = strtolower(substr($f, -4)) !== '.ph_' ? self::getIsolateButton($f) : self::getUnIsolateButton($f); } $output[] = [ 'data' => [ 'ID' => $result['ID'], 'FILE_NAME' => self::getFileWatchLink($f), 'FILE_TYPE' => $type, 'FILE_SCORE' => $result['SCORE'], 'FILE_SIZE' => self::HumanSize(@filesize($f)), 'FILE_MODIFY' => $result['MTIME'], 'FILE_CREATE' => $result['CTIME'], 'TAGS' => $result['TAGS'], 'ACTIONS' => $action, 'HIDE' => self::getHideButton($f), ] ]; } else { $table = $result['TYPE'] === 'agent' ? 'b_agent' : 'b_module_to_module'; $output[] = [ 'data' => [ 'ID' => $result['ID'], 'FILE_NAME' => self::getEventWatchLink($result['TYPE'] . " " . $result['SRC'], $table, $result['SRC']), 'FILE_TYPE' => $result['MESSAGE'], 'FILE_SCORE' => $result['SCORE'], 'ACTIONS' => self::getEventWatchButton($table, $result['SRC']) ] ]; } } return $output; } }