PHP Classes

File: vault/functions.php

Recommend this page to a friend!
  Classes of Caleb   PHP Mussel   vault/functions.php   Download  
File: vault/functions.php
Role: Example script
Content type: text/plain
Description: Example script
Class: PHP Mussel
PHP file virus scanner to detect malware
Author: By
Last change:
Date: 5 years ago
Size: 234,838 bytes
 

Contents

Class file image Download
<?php /** * This file is a part of the phpMussel package. * Homepage: https://phpmussel.github.io/ * * PHPMUSSEL COPYRIGHT 2013 AND BEYOND BY THE PHPMUSSEL TEAM. * * Authors: * @see PEOPLE.md * * License: GNU/GPLv2 * @see LICENSE.txt * * This file: Functions file (last modified: 2019.04.07). */ /** * Extends compatibility with phpMussel to PHP 5.4.x by introducing some simple * polyfills for functions introduced with newer versions of PHP. */ if (substr(PHP_VERSION, 0, 4) === '5.4.') { require $phpMussel['Vault'] . 'php5.4.x.php'; } /** Autoloader for phpMussel classes. */ spl_autoload_register(function ($Class) { $Vendor = (($Pos = strpos($Class, "\\", 1)) === false) ? '' : substr($Class, 0, $Pos); $File = __DIR__ . '/classes/' . ((!$Vendor || $Vendor === 'phpMussel') ? '' : $Vendor . '/') . ( (($Pos = strrpos($Class, "\\")) === false) ? $Class : substr($Class, $Pos + 1) ) . '.php'; if (is_readable($File)) { require $File; } }); /** * Registers plugin closures/functions to their intended hooks. * * @param string $What The name of the closure/function to execute. * @param string $Where Where to execute it (the designated "plugin hook"). * @return bool Execution failed(false)/succeeded(true). */ $phpMussel['Register_Hook'] = function ($What, $Where) use (&$phpMussel) { if (!isset($phpMussel['MusselPlugins']['hooks'], $phpMussel['MusselPlugins']['closures']) || !$What || !$Where) { return false; } if (!isset($phpMussel['MusselPlugins']['hooks'][$Where])) { $phpMussel['MusselPlugins']['hooks'][$Where] = []; } $phpMussel['MusselPlugins']['hooks'][$Where][] = $What; if (!function_exists($What) && isset($GLOBALS[$What]) && is_object($GLOBALS[$What])) { $phpMussel['MusselPlugins']['closures'][] = $What; } return true; }; /** * Executes plugin closures/functions. * * @param string $HookID Where to execute it (the designated "plugin hook"). * @return bool Execution failed(false)/succeeded(true). */ $phpMussel['Execute_Hook'] = function ($HookID) use (&$phpMussel) { if (!isset($phpMussel['MusselPlugins']['hooks'][$HookID])) { return false; } foreach ($phpMussel['MusselPlugins']['hooks'][$HookID] as $Registered) { if (isset($GLOBALS[$Registered]) && is_object($GLOBALS[$Registered])) { $GLOBALS[$Registered](); } elseif (function_exists($Registered)) { call_user_func($Registered); } } return true; }; /** * Replaces encapsulated substrings within an input string with the value of * elements within an input array, whose keys correspond to the substrings. * Accepts two input parameters: An input array (1), and an input string (2). * * @param array $Needle The input array (the needle[/s]). * @param string $Haystack The input string (the haystack). * @return string The resultant string. */ $phpMussel['ParseVars'] = function ($Needle, $Haystack) { if (!is_array($Needle) || empty($Haystack)) { return ''; } array_walk($Needle, function ($Value, $Key) use (&$Haystack) { if (!is_array($Value)) { $Haystack = str_replace('{' . $Key . '}', $Value, $Haystack); } }); return $Haystack; }; /** * Implodes multidimensional arrays. * * @param array $ar The array to be imploded. * @param string|array $j An optional "needle" or "joiner" to use for imploding * the array. If a numeric array is used, an element of the array * corresponding to the recursion depth will be used as the needle or * joiner. * @param int $i Used by the function when calling itself recursively, for the * purpose of tracking recursion depth (shouldn't be used outside the * function). * @param bool $e Optional; When set to false, empty elements will be ignored. * @return string The imploded array. */ $phpMussel['implode_md'] = function ($ar, $j = '', $i = 0, $e = true) use (&$phpMussel) { if (!is_array($ar)) { return $ar; } $c = count($ar); if (!$c || is_array($i)) { return false; } if (is_array($j)) { if (!$x = $j[$i]) { return false; } } else { $x = $j; } $out = ''; while ($c > 0) { $key = key($ar); if (is_array($ar[$key])) { $i++; $ar[$key] = $phpMussel['implode_md']($ar[$key], $j, $i); $i--; } if (!$out) { $out = $ar[$key]; } elseif (!(!$e && empty($ar[$key]))) { $out .= $x . $ar[$key]; } next($ar); $c--; } return $out; }; /** * Does some simple decoding work on strings. * * @param string $str The string to be decoded. * @return string The decoded string. */ $phpMussel['prescan_decode'] = function ($str) use (&$phpMussel) { $nstr = html_entity_decode(urldecode(str_ireplace('&amp;#', '&#', str_ireplace('&amp;amp;', '&amp;', $str)))); if ($nstr !== $str) { $nstr = $phpMussel['prescan_decode']($nstr); } return $nstr; }; /** * Some simple obfuscation for potentially blocked functions; We need this to * avoid triggering false positives for some potentially overzealous * server-based security solutions that would usually flag this file as * malicious when they detect it containing the names of suspect functions. * * @param string $n An alias for the function that we want to call. * @param string $str Some data to parse to the function being called. * @return string The parsed data and/or decoded string (if $str is empty, the * the resolved alias will be returned instead). */ $phpMussel['Function'] = function ($n, $str = '') { static $x = 'abcdefghilnorstxz12346_'; $fList = [ 'GZ' => $x[6] . $x[16] . $x[8] . $x[10] . $x[5] . $x[9] . $x[0] . $x[14] . $x[4], 'R13' => $x[13] . $x[14] . $x[12] . $x[22] . $x[12] . $x[11] . $x[14] . $x[17] . $x[19], 'B64' => $x[1] . $x[0] . $x[13] . $x[4] . $x[21] . $x[20] . $x[22] . $x[3] . $x[4] . $x[2] . $x[11] . $x[3] . $x[4], 'HEX' => $x[7] . $x[4] . $x[15] . $x[18] . $x[1] . $x[8] . $x[10] ]; if (!isset($fList[$n])) { return ''; } if (!$str || !function_exists($fList[$n])) { return $fList[$n]; } try { $Return = $fList[$n]($str); } catch (\Exception $e) { $Return = ''; } return $Return; }; /** * Does some more complex decoding and normalisation work on strings. * * @param string $str The string to be decoded/normalised. * @param bool $html If true, "style" and "script" tags will be stripped from * the input string (optional; defaults to false). * @param bool $decode If false, the input string will be normalised, but not * decoded; If true, the input string will be normalised *and* decoded. * Optional; Defaults to false. * @return string The decoded/normalised string. */ $phpMussel['prescan_normalise'] = function ($str, $html = false, $decode = false) use (&$phpMussel) { $ostr = ''; if ($decode) { $ostr .= $str; while (true) { if (function_exists($phpMussel['Function']('GZ'))) { if ($c = preg_match_all( '/(' . $phpMussel['Function']('GZ') . '\s*\(\s*["\'])(.{1,4096})(,\d)?(["\']\s*\))/i', $str, $matches)) { for ($i = 0; $c > $i; $i++) { $str = str_ireplace( $matches[0][$i], '"' . $phpMussel['Function']('GZ', $phpMussel['substrbl']($phpMussel['substraf']($matches[0][$i], $matches[1][$i]), $matches[4][$i])) . '"', $str ); } continue; } } if ($c = preg_match_all( '/(' . $phpMussel['Function']('B64') . '|decode_base64|base64\.b64decode|atob|Base64\.decode64)(\s*' . '\(\s*["\'\`])([\da-z+\/]{4})*([\da-z+\/]{4}|[\da-z+\/]{3}=|[\da-z+\/]{2}==)(["\'\`]' . '\s*\))/i', $str, $matches)) { for ($i = 0; $c > $i; $i++) { $str = str_ireplace( $matches[0][$i], '"' . $phpMussel['Function']('B64', $phpMussel['substrbl']($phpMussel['substraf']($matches[0][$i], $matches[1][$i] . $matches[2][$i]), $matches[5][$i])) . '"', $str ); } continue; } if ($c = preg_match_all( '/(' . $phpMussel['Function']('R13') . '\s*\(\s*["\'])([^\'"\(\)]{1,4096})(["\']\s*\))/i', $str, $matches)) { for ($i = 0; $c > $i; $i++) { $str = str_ireplace( $matches[0][$i], '"' . $phpMussel['Function']('R13', $phpMussel['substrbl']($phpMussel['substraf']($matches[0][$i], $matches[1][$i]), $matches[3][$i])) . '"', $str ); } continue; } if ($c = preg_match_all( '/(' . $phpMussel['Function']('HEX') . '\s*\(\s*["\'])([\da-f]{1,4096})(["\']\s*\))/i', $str, $matches )) { for ($i = 0; $c > $i; $i++) { $str = str_ireplace( $matches[0][$i], '"' . $phpMussel['HexSafe']($phpMussel['substrbl']($phpMussel['substraf']($matches[0][$i], $matches[1][$i]), $matches[3][$i])) . '"', $str ); } continue; } if ($c = preg_match_all( '/([Uu][Nn][Pp][Aa][Cc][Kk]\s*\(\s*["\']\s*H\*\s*["\']\s*,\s*["\'])([\da-fA-F]{1,4096})(["\']\s*\))/', $str, $matches)) { for ($i = 0; $c > $i; $i++) { $str = str_replace($matches[0][$i], '"' . $phpMussel['HexSafe']($phpMussel['substrbl']($phpMussel['substraf']($matches[0][$i], $matches[1][$i]), $matches[3][$i])) . '"', $str); } continue; } break; } } $str = preg_replace('/[^\x21-\x7e]/', '', strtolower($phpMussel['prescan_decode']($str . $ostr))); unset($ostr); if ($html) { $str = preg_replace([ '@<script[^>]*?>.*?</script>@si', '@<[\/\!]*?[^<>]*?>@si', '@<style[^>]*?>.*?</style>@siU', '@<![\s\S]*?--[ \t\n\r]*>@' ], '', $str); } return trim($str); }; /** * Gets substring from haystack prior to the first occurrence of needle. * * @param string $h The haystack. * @param string $n The needle. * @return string The substring. */ $phpMussel['substrbf'] = function ($h, $n) { return !$n ? '' : substr($h, 0, strpos($h, $n)); }; /** * Gets substring from haystack after the first occurrence of needle. * * @param string $h The haystack. * @param string $n The needle. * @return string The substring. */ $phpMussel['substraf'] = function ($h, $n) { return !$n ? '' : substr($h, strpos($h, $n) + strlen($n)); }; /** * Gets substring from haystack prior to the last occurrence of needle. * * @param string $h The haystack. * @param string $n The needle. * @return string The substring. */ $phpMussel['substrbl'] = function ($h, $n) { return !$n ? '' : substr($h, 0, strrpos($h, $n)); }; /** * Gets substring from haystack after the last occurrence of needle. * * @param string $h The haystack. * @param string $n The needle. * @return string The substring. */ $phpMussel['substral'] = function ($h, $n) { return !$n ? '' : substr($h, strrpos($h, $n) + strlen($n)); }; /** * This function reads files and returns the contents of those files. * * @param string $File Path and filename of the file to read. * @param int $s Number of blocks to read from the file (optional; can be * manually specified, but it's best to just ignore it and let the * function work it out for itself). * @param bool $PreChecked When false, checks that the file exists and is * writable. Defaults to false. * @param int $Blocks The total size of a single block in kilobytes (optional; * defaults to 128, i.e., 128KB or 131072 bytes). This can be modified by * developers as per their individual needs. Generally, a smaller value * will increase stability but decrease performance, whereas a larger * value will increase performance but decrease stability. * @return string|bool Content of the file returned by the function (or false * on failure). */ $phpMussel['ReadFile'] = function ($File, $Size = 0, $PreChecked = false, $Blocks = 128) { if (!$PreChecked && (!is_file($File) || !is_readable($File))) { return false; } /** Blocksize to bytes. */ $Blocksize = $Blocks * 1024; $Filesize = filesize($File); if (!$Size) { $Size = ($Filesize && $Blocksize) ? ceil($Filesize / $Blocksize) : 0; } $Data = ''; if ($Size > 0) { $Handle = fopen($File, 'rb'); $r = 0; while ($r < $Size) { $Data .= fread($Handle, $Blocksize); $r++; } fclose($Handle); } return $Data ?: false; }; /** * A very simple wrapper for file() that checks for the existence of files * before attempting to read them, in order to avoid warnings about * non-existent files. * * @param string $Filename Refer to the description for file(). * @param int $Flags Refer to the description for file(). * @param array $Context Refer to the description for file(). * @return array|bool Same as with file(), but won't trigger warnings. */ $phpMussel['ReadFileAsArray'] = function ($Filename, $Flags = 0, $Context = false) { if (!is_readable($Filename)) { return false; } if (!$Context) { return !$Flags ? file($Filename) : file($Filename, $Flags); } return file($Filename, $Flags, $Context); }; /** * Deletes expired cache entries and regenerates cache files. * * @param string $Delete Forcibly delete a specific cache entry (optional). * @return bool Operation succeeded (true) or failed (false). */ $phpMussel['CleanCache'] = function ($Delete = '') use (&$phpMussel) { if (!empty($phpMussel['InstanceCache']['CacheCleaned'])) { return true; } $phpMussel['InstanceCache']['CacheCleaned'] = true; $CacheFiles = []; $FileIndex = $phpMussel['cachePath'] . 'index.dat'; if (!is_readable($FileIndex)) { return false; } $FileDataOld = $FileData = $phpMussel['ReadFile']($FileIndex); if (strpos($FileData, ';') !== false) { $FileData = explode(';', $FileData); foreach ($FileData as &$ThisData) { if (strpos($ThisData, ':') === false) { $ThisData = ''; continue; } $ThisData = explode(':', $ThisData, 3); if (($Delete && $Delete === $ThisData[0]) || ($ThisData[1] > 0 && $phpMussel['Time'] > $ThisData[1])) { $FileKey = bin2hex(substr($ThisData[0], 0, 1)); if (!isset($CacheFiles[$FileKey])) { $CacheFiles[$FileKey] = !is_readable( $phpMussel['cachePath'] . $FileKey . '.tmp' ) ? '' : $phpMussel['ReadFile']($phpMussel['cachePath'] . $FileKey . '.tmp', 0, true); } while (strpos($CacheFiles[$FileKey], $ThisData[0] . ':') !== false) { $CacheFiles[$FileKey] = str_ireplace($ThisData[0] . ':' . $phpMussel['substrbf']( $phpMussel['substraf']($CacheFiles[$FileKey], $ThisData[0] . ':'), ';' ) . ';', '', $CacheFiles[$FileKey]); } $ThisData = ''; continue; } $ThisData = $ThisData[0] . ':' . $ThisData[1]; } $FileData = str_replace(';;', ';', implode(';', array_filter($FileData)) . ';'); if ($FileDataOld !== $FileData) { $Handle = fopen($FileIndex, 'w'); fwrite($Handle, $FileData); fclose($Handle); } } foreach ($CacheFiles as $CacheEntryKey => $CacheEntryValue) { if (strlen($CacheEntryValue) < 2) { if (file_exists($phpMussel['cachePath'] . $CacheEntryKey . '.tmp')) { unlink($phpMussel['cachePath'] . $CacheEntryKey . '.tmp'); } continue; } $Handle = fopen($phpMussel['cachePath'] . $CacheEntryKey . '.tmp', 'w'); fwrite($Handle, $CacheEntryValue); fclose($Handle); } return true; }; /** * Retrieves cache entries. * * @param string|array $Entry The name of the cache entry/entries to retrieve; * Can be a string to specify a single entry, or an array of strings to * specify multiple entries. * @return string|array Contents of the cache entry/entries. */ $phpMussel['FetchCache'] = function ($Entry = '') use (&$phpMussel) { $phpMussel['CleanCache'](); $phpMussel['InitialiseCache'](); /** Override if using a different preferred caching mechanism. */ if ($phpMussel['Cache']->Using) { if (is_array($Entry)) { $Out = []; foreach ($Entry as $ThisKey => $ThisEntry) { $Out[$ThisKey] = $phpMussel['Cache']->getEntry($ThisEntry); } return $Out; } return $phpMussel['Cache']->getEntry($Entry); } /** Default process. */ if (!$Entry) { return ''; } if (is_array($Entry)) { $Out = []; array_walk($Entry, function ($Value, $Key) use (&$phpMussel, &$Out) { $Out[$Key] = $phpMussel['FetchCache']($Value); }); return $Out; } $File = $phpMussel['cachePath'] . bin2hex(substr($Entry, 0, 1)) . '.tmp'; if (!is_readable($File) || !$FileData = $phpMussel['ReadFile']($File, 0, true)) { return ''; } if (!$Item = strpos($FileData, $Entry . ':') !== false ? $Entry . ':' . $phpMussel['substrbf']( $phpMussel['substraf']($FileData, $Entry . ':'), ';' ) . ';' : '') { return ''; } $Expiry = $phpMussel['substrbf']($phpMussel['substraf']($Item, $Entry . ':'), ':'); if ($Expiry > 0 && $phpMussel['Time'] > $Expiry) { while (strpos($FileData, $Entry . ':') !== false) { $FileData = str_ireplace($Item, '', $FileData); } $Handle = fopen($File, 'w'); fwrite($Handle, $FileData); fclose($Handle); return ''; } if (!$ItemData = $phpMussel['substrbf']($phpMussel['substraf']($Item, $Entry . ':' . $Expiry . ':'), ';')) { return ''; } return $phpMussel['Function']('GZ', $phpMussel['HexSafe']($ItemData)) ?: ''; }; /** * Creates cache entry and saves it to the cache. * * @param string $Entry Name of the cache entry to create. * @param int $Expiry Unix time until the cache entry expires. * @param string $ItemData Contents of the cache entry. * @return bool This should always return true, unless something goes wrong. */ $phpMussel['SaveCache'] = function ($Entry = '', $Expiry = 0, $ItemData = '') use (&$phpMussel) { $phpMussel['CleanCache'](); $phpMussel['InitialiseCache'](); /** Override if using a different preferred caching mechanism. */ if ($phpMussel['Cache']->Using) { if ($Expiry <= 0) { $Expiry = 0; } elseif ($Expiry > $phpMussel['Time']) { $Expiry = $Expiry - $phpMussel['Time']; } return $phpMussel['Cache']->setEntry($Entry, $ItemData, $Expiry); } /** Default process. */ if (!$Entry || !$ItemData) { return false; } if (!$Expiry) { $Expiry = $phpMussel['Time']; } $File = $phpMussel['cachePath'] . bin2hex($Entry[0]) . '.tmp'; $Data = $phpMussel['ReadFile']($File) ?: ''; while (strpos($Data, $Entry . ':') !== false) { $Data = str_ireplace($Entry . ':' . $phpMussel['substrbf']($phpMussel['substraf']($Data, $Entry . ':'), ';') . ';', '', $Data); } $Data .= $Entry . ':' . $Expiry . ':' . bin2hex(gzdeflate($ItemData,9)) . ';'; $Handle = fopen($File, 'w'); fwrite($Handle, $Data); fclose($Handle); $IndexFile = $phpMussel['cachePath'] . 'index.dat'; $IndexNewData = $IndexData = $phpMussel['ReadFile']($IndexFile) ?: ''; while (strpos($IndexNewData, $Entry . ':') !== false) { $IndexNewData = str_ireplace($Entry . ':' . $phpMussel['substrbf']($phpMussel['substraf']($IndexNewData, $Entry . ':'), ';') . ';', '', $IndexNewData); } $IndexNewData .= $Entry . ':' . $Expiry . ';'; if ($IndexNewData !== $IndexData) { $IndexHandle = fopen($IndexFile, 'w'); fwrite($IndexHandle, $IndexNewData); fclose($IndexHandle); } return true; }; /** Reads and prepares cached hash data. */ $phpMussel['PrepareHashCache'] = function () use (&$phpMussel) { if (!isset($phpMussel['HashCache'])) { $phpMussel['HashCache'] = []; } if ($phpMussel['HashCache']['Data'] = ( $phpMussel['Config']['general']['scan_cache_expiry'] > 0 ) ? $phpMussel['FetchCache']('HashCache') : '') { $phpMussel['HashCache']['Data'] = explode(';', $phpMussel['HashCache']['Data']); $Build = []; foreach ($phpMussel['HashCache']['Data'] as $CacheItem) { if (strpos($CacheItem, ':') !== false) { $CacheItem = explode(':', $CacheItem, 4); if ($CacheItem[1] > $phpMussel['Time']) { $Build[$CacheItem[0]] = $CacheItem; } } } $phpMussel['HashCache']['Data'] = $Build; } }; /** * Quarantines file uploads by using a key generated from your quarantine key * to bitshift the input string (the file uploads), appending a header with an * explanation of what the bitshifted data is, along with an MD5 hash checksum * of its non-quarantined counterpart, and then saves it all to a QFU file, * storing these QFU files in your quarantine directory. * * This isn't hardcore encryption, but it should be sufficient to prevent * accidental execution of quarantined files and to allow safe handling of * those files, which is the whole point of quarantining them in the first * place. Improvements might be made in the future. * * @param string $In The input string (the file upload / source data). * @param string $Key Your quarantine key. * @param string $IP Data origin (usually, the IP address of the uploader). * @param string $ID The QFU filename to use (calculated beforehand). * @return bool This should always return true, unless something goes wrong. */ $phpMussel['Quarantine'] = function ($In, $Key, $IP, $ID) use (&$phpMussel) { if (!$In || !$Key || !$IP || !$ID || !function_exists('gzdeflate') || ( strlen($Key) < 128 && !$Key = $phpMussel['HexSafe'](hash('sha512', $Key) . hash('whirlpool', $Key)) )) { return false; } if ($phpMussel['Config']['legal']['pseudonymise_ip_addresses']) { $IP = $phpMussel['Pseudonymise-IP']($IP); } $k = strlen($Key); $FileSize = strlen($In); $Head = "\xa1phpMussel\x21" . $phpMussel['HexSafe'](md5($In)) . pack('l*', $FileSize) . "\x01"; $In = gzdeflate($In, 9); $Out = ''; $i = 0; while ($i < $FileSize) { for ($j = 0; $j < $k; $j++, $i++) { if (strlen($Out) >= $FileSize) { break 2; } $L = substr($In, $i, 1); $R = substr($Key, $j, 1); $Out .= ($L === false ? "\x00" : $L) ^ ($R === false ? "\x00" : $R); } } $Out = "\x2f\x3d\x3d\x20phpMussel\x20Quarantined\x20File\x20Upload\x20\x3d" . "\x3d\x5c\n\x7c\x20Time\x2fDate\x20Uploaded\x3a\x20" . str_pad($phpMussel['Time'], 18, ' ') . "\x7c\n\x7c\x20Uploaded\x20From\x3a\x20" . str_pad($IP, 22, ' ') . "\x20\x7c\n\x5c" . str_repeat("\x3d", 39) . "\x2f\n\n\n" . $Head . $Out; $UsedMemory = $phpMussel['MemoryUse']($phpMussel['qfuPath']); $UsedMemory['Size'] += strlen($Out); $UsedMemory['Count']++; if ($DeductBytes = $phpMussel['ReadBytes']($phpMussel['Config']['general']['quarantine_max_usage'])) { $DeductBytes = $UsedMemory['Size'] - $DeductBytes; $DeductBytes = ($DeductBytes > 0) ? $DeductBytes : 0; } if ($DeductFiles = $phpMussel['Config']['general']['quarantine_max_files']) { $DeductFiles = $UsedMemory['Count'] - $DeductFiles; $DeductFiles = ($DeductFiles > 0) ? $DeductFiles : 0; } if ($DeductBytes > 0 || $DeductFiles > 0) { $UsedMemory = $phpMussel['MemoryUse']($phpMussel['qfuPath'], $DeductBytes, $DeductFiles); } $Handle = fopen($phpMussel['qfuPath'] . $ID . '.qfu', 'a'); fwrite($Handle, $Out); fclose($Handle); if (!$phpMussel['EOF']) { $phpMussel['Stats-Increment']('Web-Quarantined', 1); } return true; }; /** * Calculates the total memory used by a directory, and optionally enforces * memory usage and number of files limits on that directory. Should be * regarded as part of the phpMussel quarantine functionality. * * @param string $Path The path of the directory to be checked. * @param int $Delete How many bytes to delete from the target directory; Omit * or set to 0 to avoid deleting files on the basis of total bytes. * @param int $DeleteFiles How many files to delete from the target directory; Omit or set to 0 to avoid deleting files. * @return array Contains two integer elements: `Size`: The actual, total * memory used by the target directory. `Count`: The total number of files * found in the target directory by the time of closure exit. */ $phpMussel['MemoryUse'] = function ($Path, $Delete = 0, $DeleteFiles = 0) { $Offset = strlen($Path); $Files = []; $List = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($Path), RecursiveIteratorIterator::SELF_FIRST); foreach ($List as $Item => $List) { $File = str_replace("\\", '/', substr($Item, $Offset)); if ($File && preg_match('~\.qfu$~i', $Item) && is_file($Item) && !is_link($Item) && is_readable($Item)) { $Files[$File] = filemtime($Item); } } unset($Item, $List, $Offset); $Arr = ['Size' => 0, 'Count' => 0]; asort($Files, SORT_NUMERIC); foreach ($Files as $File => $Modified) { $File = $Path . $File; $Size = filesize($File); if (($Delete > 0 || $DeleteFiles > 0) && unlink($File)) { $DeleteFiles--; $Delete -= $Size; continue; } $Arr['Size'] += $Size; $Arr['Count']++; } return $Arr; }; /** * Checks if $Needle (string) matches (is equal or identical to) $Haystack * (string), or a specific substring of $Haystack, to within a specific * threshold of the levenshtein distance between the $Needle and the $Haystack * or the $Haystack substring specified. * * This function is useful for expressing the differences between two strings * as an integer value and for then determining whether a specific value as per * those differences is met. * * @param string $Needle The needle (will be matched against the $Haystack, or, * if substring positions are specified, against the $Haystack substring * specified). * @param string $Haystack The haystack (will be matched against the $Needle). * Note that for the purposes of calculating the levenshtein distance, it * doesn't matter which string is a $Needle and which is a $Haystack (the * value should be the same if the two were reversed). However, when * specifying substring positions, those substring positions are applied * to the $Haystack, and not the $Needle. Note, too, that if the $Needle * length is greater than the $Haystack length (after having applied the * substring positions to the $Haystack), $Needle and $Haystack will be * switched. * @param int $pos_A The initial position of the $Haystack to use for the * substring, if using a substring (optional; defaults to `0`; `0` is the * beginning of the $Haystack). * @param int $pos_Z The final position of the $Haystack to use for the * substring, if using a substring (optional; defaults to `0`; `0` will * instruct the function to continue to the end of the $Haystack, and * thus, if both $pos_A and $pos_Z are `0`, the entire $Haystack will be * used). * @param int $min The threshold minimum (the minimum levenshtein distance * required in order for the two strings to be considered a match). * Optional; Defaults to `0`. If `0` or less is specified, there is no * minimum, and so, any and all strings should always match, as long as * the levenshtein distance doesn't surpass the threshold maximum. * @param int $max The threshold maximum (the maximum levenshtein distance * allowed for the two strings to be considered a match). Optional; * Defaults to `-1`. If exactly `-1` is specified, there is no maximum, * and so, any and all strings should always match, as long as the * threshold minimum is met. * @param bool $bool Specifies to the function whether to return the * levenshtein distance of the two strings (as an integer) or to return * the results of the match (as a boolean; true for match success, false * for match failure). Optional; Defaults to true. If true is specified, * the function will return a boolean value (the results of the match), * and if false is specified, the levenshtein distance will be returned. * @param bool $case Specifies to the function whether to treat the two strings * as case-sensitive (when true is specified) or case-insensitive (when * false is specified) when calculating the levenshtein distance. * Optional; Defaults to false. * @param int $cost_ins The cost to apply for character/byte insertions for * when calculating the levenshtein distance. Optional; Defaults to 1. * @param int $cost_rep The cost to apply for character/byte replacements for * when calculating the levenshtein distance. Optional; Defaults to 1. * @param int $cost_del The cost to apply for character/byte deletions for when * calculating the levenshtein distance. Optional; Defaults to 1. * @return bool|int The function will return either a boolean or an integer, * depending on the state of $bool (but will also return false whenever an * error occurs). */ $phpMussel['lv_match'] = function ($Needle, $Haystack, $pos_A = 0, $pos_Z = 0, $min = 0, $max = -1, $bool = true, $case = false, $cost_ins = 1, $cost_rep = 1, $cost_del = 1) { if (!function_exists('levenshtein') || is_array($Needle) || is_array($Haystack)) { return false; } $nlen = strlen($Needle); $pos_A = (int)$pos_A; $pos_Z = (int)$pos_Z; $min = (int)$min; $max = (int)$max; if ($pos_A !== 0 || $pos_Z !== 0) { $Haystack = ( $pos_Z === 0 ) ? substr($Haystack, $pos_A) : substr($Haystack, $pos_A, $pos_Z); } $hlen = strlen($Haystack); if ($nlen < 1 || $hlen < 1) { return $bool ? false : 0; } if ($nlen > $hlen) { $x = [$Needle, $nlen, $Haystack, $hlen]; $Haystack = $x[0]; $hlen = $x[1]; $Needle = $x[2]; $nlen = $x[3]; } if ($cost_ins === 1 && $cost_rep === 1 && $cost_del === 1) { $lv = $case ? levenshtein( $Haystack, $Needle ) : levenshtein( strtolower($Haystack), strtolower($Needle) ); } else { $lv = $case ? levenshtein( $Haystack, $Needle, $cost_ins, $cost_rep, $cost_del ) : levenshtein( strtolower($Haystack), strtolower($Needle), $cost_ins, $cost_rep, $cost_del ); } return $bool ? (($min === 0 || $lv >= $min) && ($max === -1 || $lv <= $max)) : $lv; }; /** * Returns the high and low nibbles corresponding to the first byte of the * input string. * * @param string $Input The input string. * @return array Contains two elements, both standard decimal integers; The * first is the high nibble of the input string, and the second is the low * nibble of the input string. */ $phpMussel['split_nibble'] = function ($Input) { $Input = bin2hex($Input); return [hexdec(substr($Input, 0, 1)), hexdec(substr($Input, 1, 1))]; }; /** * Returns a string representing the binary bits of its input, whereby each * byte of the output is either one or zero. * Output can be reversed with `$phpMussel['implode_bits']()`. * * @param string $Input The input string (see closure description above). * @return string The output string (see closure description above). */ $phpMussel['explode_bits'] = function ($Input) { $Out = ''; $Len = strlen($Input); for ($Byte = 0; $Byte < $Len; $Byte++) { $Out .= str_pad(decbin(ord($Input[$Byte])), 8, '0', STR_PAD_LEFT); } return $Out; }; /** * The reverse of `$phpMussel['explode_bits']()`. * * @param string $Input The input string (see closure description above). * @return string The output string (see closure description above). */ $phpMussel['implode_bits'] = function ($Input) { $Chunks = str_split($Input, 8); $Count = count($Chunks); for ($Out = '', $Chunk = 0; $Chunk < $Count; $Chunk++) { $Out .= chr(bindec($Chunks[$Chunk])); } return $Out; }; /** * Expands phpMussel detection shorthand to complete identifiers, makes some * determinations based on those identifiers against the package * configuration (e.g., whether specific signatures should be weighted or * ignored based on those identifiers), and returns a complete signature name * containing all relevant identifiers. * * Originally, this function was created to allow phpMussel to partially * compress its signatures without jeopardising speed, performance or * efficiency, because by allowing phpMussel to partially compress its * signatures, the total signature file footprint could be reduced, thus * allowing the inclusion of a greater number of signatures without causing * excessive footprint bloat. Its purpose has expanded since then though. * * @param string $VN The signature name WITH identifiers compressed (i.e., * the shorthand version of the signature name). * @return string The signature name WITHOUT identifiers compressed (i.e., the * identifiers have been decompressed/expanded), or the input verbatim. */ $phpMussel['vn_shorthand'] = function ($VN) use (&$phpMussel) { /** Determine whether the signature is weighted. */ $phpMussel['InstanceCache']['weighted'] = false; /** Determine whether the signature should be ignored due to package configuration. */ $phpMussel['InstanceCache']['ignoreme'] = false; /** Byte 0 confirms whether the signature name uses shorthand. */ if ($VN[0] !== "\x1a") { return $VN; } /** Check whether shorthand data has been fetched. If it hasn't, fetch it. */ if (!isset($phpMussel['shorthand.yaml'])) { if (!file_exists($phpMussel['Vault'] . 'shorthand.yaml') || !is_readable($phpMussel['Vault'] . 'shorthand.yaml')) { return $VN; } $phpMussel['shorthand.yaml'] = (new \Maikuolan\Common\YAML($phpMussel['ReadFile']($phpMussel['Vault'] . 'shorthand.yaml')))->Data; } /** Will be populated by the signature name. */ $Out = ''; /** Byte 1 contains vendor name and signature metadata information. */ $Nibbles = $phpMussel['split_nibble']($VN[1]); /** Populate vendor name. */ if ( !empty($phpMussel['shorthand.yaml']['Vendor Shorthand'][$Nibbles[0]]) && is_array($phpMussel['shorthand.yaml']['Vendor Shorthand'][$Nibbles[0]]) && !empty($phpMussel['shorthand.yaml']['Vendor Shorthand'][$Nibbles[0]][$Nibbles[1]]) && is_string($phpMussel['shorthand.yaml']['Vendor Shorthand'][$Nibbles[0]][$Nibbles[1]]) ) { $SkipMeta = true; $Out .= $phpMussel['shorthand.yaml']['Vendor Shorthand'][$Nibbles[0]][$Nibbles[1]] . '-'; } elseif ( !empty($phpMussel['shorthand.yaml']['Vendor Shorthand'][$Nibbles[0]]) && is_string($phpMussel['shorthand.yaml']['Vendor Shorthand'][$Nibbles[0]]) ) { $Out .= $phpMussel['shorthand.yaml']['Vendor Shorthand'][$Nibbles[0]] . '-'; } /** Populate weight options. */ if (( !empty($phpMussel['shorthand.yaml']['Vendor Weight Options'][$Nibbles[0]][$Nibbles[1]]) && $phpMussel['shorthand.yaml']['Vendor Weight Options'][$Nibbles[0]][$Nibbles[1]] === 'Weighted' ) || ( !empty($phpMussel['shorthand.yaml']['Vendor Weight Options'][$Nibbles[0]]) && $phpMussel['shorthand.yaml']['Vendor Weight Options'][$Nibbles[0]] === 'Weighted' )) { $phpMussel['InstanceCache']['weighted'] = true; } /** Populate signature metadata information. */ if (empty($SkipMeta) && !empty($phpMussel['shorthand.yaml']['Metadata Shorthand'][$Nibbles[1]])) { $Out .= $phpMussel['shorthand.yaml']['Metadata Shorthand'][$Nibbles[1]] . '.'; } /** Byte 2 contains vector information. */ $Nibbles = $phpMussel['split_nibble']($VN[2]); /** Populate vector information. */ if (!empty($phpMussel['shorthand.yaml']['Vector Shorthand'][$Nibbles[0]][$Nibbles[1]])) { $Out .= $phpMussel['shorthand.yaml']['Vector Shorthand'][$Nibbles[0]][$Nibbles[1]] . '.'; } /** Byte 3 contains malware type information. */ $Nibbles = $phpMussel['split_nibble']($VN[3]); /** Populate malware type information. */ if (!empty($phpMussel['shorthand.yaml']['Malware Type Shorthand'][$Nibbles[0]][$Nibbles[1]])) { $Out .= $phpMussel['shorthand.yaml']['Malware Type Shorthand'][$Nibbles[0]][$Nibbles[1]] . '.'; } /** Populate ignore options. */ if (!empty($phpMussel['shorthand.yaml']['Malware Type Ignore Options'][$Nibbles[0]][$Nibbles[1]])) { $IgnoreOption = $phpMussel['shorthand.yaml']['Malware Type Ignore Options'][$Nibbles[0]][$Nibbles[1]]; if (isset($phpMussel['Config']['signatures'][$IgnoreOption]) && !$phpMussel['Config']['signatures'][$IgnoreOption]) { $phpMussel['InstanceCache']['ignoreme'] = true; } } /** Return the signature name and exit the closure. */ return $Out . substr($VN, 4); }; /** * Used for performing lookups to the Google Safe Browsing API (v4). * @link https://developers.google.com/safe-browsing/v4/lookup-api * * @param array $urls An array of the URLs to lookup. * @param array $URLsNoLookup An optional array of URLs to NOT lookup. * @param array $DomainsNoLookup An optional array of domains to NOT lookup. * @return int The results of the lookup. 200 if AT LEAST ONE of the queried * URLs are listed on any of Google Safe Browsing lists; 204 if NONE of * the queried URLs are listed on any of Google Safe Browsing lists; 400 * if the request is malformed; 401 if the API key is missing or isn't * authorised; 503 if the service is unavailable (e.g., if it's been * throttled); 999 if something unexpected occurs (such as, for example, * if a programmatic error is encountered). */ $phpMussel['SafeBrowseLookup'] = function ($urls, $URLsNoLookup = [], $DomainsNoLookup = []) use (&$phpMussel) { if (empty($phpMussel['Config']['urlscanner']['google_api_key'])) { return 401; } /** Count and prepare the URLs. */ if (!$c = count($urls)) { return 400; } for ($i = 0; $i < $c; $i++) { $Domain = (strpos($urls[$i], '/') !== false) ? $phpMussel['substrbf']($urls[$i], '/') : $urls[$i]; if (!empty($URLsNoLookup[$urls[$i]]) || !empty($DomainsNoLookup[$Domain])) { unset($urls[$i]); continue; } $urls[$i] = ['url' => $urls[$i]]; } sort($urls); /** After we've prepared the URLs, we prepare our JSON array. */ $arr = json_encode([ 'client' => [ 'clientId' => 'phpMussel', 'clientVersion' => $phpMussel['ScriptVersion'] ], 'threatInfo' => [ 'threatTypes' => [ 'THREAT_TYPE_UNSPECIFIED', 'MALWARE', 'SOCIAL_ENGINEERING', 'UNWANTED_SOFTWARE', 'POTENTIALLY_HARMFUL_APPLICATION' ], 'platformTypes' => ['ANY_PLATFORM'], 'threatEntryTypes' => ['URL'], 'threatEntries' => $urls ] ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); /** Fetch the cache entry for Google Safe Browsing, if it doesn't already exist. */ if (!isset($phpMussel['InstanceCache']['urlscanner_google'])) { $phpMussel['InstanceCache']['urlscanner_google'] = $phpMussel['FetchCache']('urlscanner_google'); } /** Generate new cache expiry time. */ $newExpiry = $phpMussel['Time'] + $phpMussel['Config']['urlscanner']['cache_time']; /** Generate a reference for the cache entry for this lookup. */ $cacheRef = md5($arr) . ':' . $c . ':' . strlen($arr) . ':'; /** This will contain the lookup response. */ $Response = ''; /** Check if this lookup has already been performed. */ while (strpos($phpMussel['InstanceCache']['urlscanner_google'], $cacheRef) !== false) { $Response = $phpMussel['substrbf']($phpMussel['substral']($phpMussel['InstanceCache']['urlscanner_google'], $cacheRef), ';'); /** Safety mechanism. */ if (!$Response || strpos($phpMussel['InstanceCache']['urlscanner_google'], $cacheRef . $Response . ';') === false) { $Response = ''; break; } $expiry = $phpMussel['substrbf']($Response, ':'); if ($expiry > $phpMussel['Time']) { $Response = $phpMussel['substraf']($Response, ':'); break; } $phpMussel['InstanceCache']['urlscanner_google'] = str_ireplace($cacheRef . $Response . ';', '', $phpMussel['InstanceCache']['urlscanner_google']); $Response = ''; } /** If this lookup has already been performed, return the results without repeating it. */ if ($Response) { /** Update the cache entry for Google Safe Browsing. */ $newExpiry = $phpMussel['SaveCache']('urlscanner_google', $newExpiry, $phpMussel['InstanceCache']['urlscanner_google']); if ($Response === '200') { /** Potentially harmful URL detected. */ return 200; } elseif ($Response === '204') { /** Potentially harmful URL *NOT* detected. */ return 204; } elseif ($Response === '400') { /** Bad/malformed request. */ return 400; } elseif ($Response === '401') { /** Unauthorised (possibly a bad API key). */ return 401; } elseif ($Response === '503') { /** Service unavailable. */ return 503; } /** Something bad/unexpected happened. */ return 999; } /** Prepare the URL to use with cURL. */ $uri = 'https://safebrowsing.googleapis.com/v4/threatMatches:find?key=' . $phpMussel['Config']['urlscanner']['google_api_key']; /** cURL stuff here. */ $Request = curl_init($uri); curl_setopt($Request, CURLOPT_FRESH_CONNECT, true); curl_setopt($Request, CURLOPT_HEADER, false); curl_setopt($Request, CURLOPT_POST, true); /** Ensure it knows we're sending JSON data. */ curl_setopt($Request, CURLOPT_HTTPHEADER, ['Content-type: application/json']); /** The Google Safe Browsing API requires HTTPS+SSL (there's no way around this). */ curl_setopt($Request, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); curl_setopt($Request, CURLOPT_RETURNTRANSFER, true); /* * Setting "CURLOPT_SSL_VERIFYPEER" to false can be somewhat risky due to man-in-the-middle attacks, but lookups * seemed to always fail when it was set to true during testing, so, for the sake of this actually working at all, * I'm setting it as false, but we should try to fix this in the future at some point. */ curl_setopt($Request, CURLOPT_SSL_VERIFYPEER, false); /* We don't want to leave the client waiting for *too* long. */ curl_setopt($Request, CURLOPT_TIMEOUT, $phpMussel['Timeout']); curl_setopt($Request, CURLOPT_USERAGENT, $phpMussel['ScriptUA']); curl_setopt($Request, CURLOPT_POSTFIELDS, $arr); /** Execute and get the response. */ $Response = curl_exec($Request); $phpMussel['LookupCount']++; /** Check for errors and print to the screen if there were any. */ if (!$Response) { throw new \Exception(curl_error($Request)); } /** Close the cURL session. */ curl_close($Request); if (strpos($Response, '"matches":') !== false) { /** Potentially harmful URL detected. */ $returnVal = 200; } else { /** Potentially harmful URL *NOT* detected. */ $returnVal = 204; } /** Update the cache entry for Google Safe Browsing. */ $phpMussel['InstanceCache']['urlscanner_google'] .= $cacheRef . ':' . $newExpiry . ':' . $returnVal . ';'; $newExpiry = $phpMussel['SaveCache']('urlscanner_google', $newExpiry, $phpMussel['InstanceCache']['urlscanner_google']); return $returnVal; }; /** * Checks whether signature length is confined within an acceptable limit. * * @param int $Length * @return bool */ $phpMussel['ConfineLength'] = function ($Length) { return ($Length < 4 || $Length > 1024); }; /** * Detection trigger closure (appends detection information). Generally called * from within the data handler. When as a method, should treat as private. */ $phpMussel['Detected'] = function (&$heur, &$lnap, &$VN, &$ofn, &$ofnSafe, &$out, &$flagged, &$MD5, &$str_len) use (&$phpMussel) { if (!$flagged) { $phpMussel['killdata'] .= $MD5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $heur['detections']++; $phpMussel['InstanceCache']['detections_count']++; if ($phpMussel['InstanceCache']['weighted']) { $heur['weight']++; $heur['cli'] .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), sprintf($phpMussel['L10N']->getString('detected'), $VN) ) . "\n"; $heur['web'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), sprintf($phpMussel['L10N']->getString('detected'), $VN) . ' (' . $ofnSafe . ')' ); return; } $out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), sprintf($phpMussel['L10N']->getString('detected'), $VN) ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), sprintf($phpMussel['L10N']->getString('detected'), $VN) . ' (' . $ofnSafe . ')' ); }; /** * All needles must assert as the assert state being instances of haystacks. * * @param array $Haystacks The haystacks. * @param array $Needles The needles. * @param string $Padding An optional string to pad haystacks and needles. * @param bool $AssertState MUST (true) or must NOT (false) be an instance of. * @param bool $Mode ALL (false) or ANY (true) must assert. * @return bool True if requirement conforms; False otherwise. */ $phpMussel['ContainsMustAssert'] = function ($Haystacks, $Needles, $Padding = ',', $AssertState = false, $Mode = false) { foreach ($Haystacks as $Haystack) { $Haystack = $Padding . $Haystack . $Padding; foreach ($Needles as $Needle) { $Needle = $Padding . $Needle . $Padding; if (!$Mode) { if (!is_bool(strpos($Haystack, $Needle)) !== $AssertState) { return false; } continue; } if (!is_bool(strpos($Haystack, $Needle)) === $AssertState) { return true; } } } return !$Mode; }; /** * Confines a string boundary as per rules specified by parameters. * * @param string $Data The string. * @param string|int $Initial The start of the boundary or string initial offset value. * @param string|int $Terminal The end of the boundary or string terminal offset value. * @param array $SectionOffsets Section offset values. */ $phpMussel['DataConfineByOffsets'] = function (&$Data, &$Initial, &$Terminal, &$SectionOffsets) { if ($Initial === '*' && $Terminal === '*') { return; } if (substr($Initial, 0, 2) === 'SE') { $SectionNum = (int)substr($Initial, 2); $Initial = '*'; $Terminal = '*'; if (isset($SectionOffsets[$SectionNum][0])) { $Data = substr($Data, $SectionOffsets[$SectionNum][0] * 2); } if (isset($SectionOffsets[$SectionNum][1])) { $Data = substr($Data, 0, $SectionOffsets[$SectionNum][1] * 2); } } elseif (substr($Initial, 0, 2) === 'SL') { $Remainder = strlen($Initial) > 3 && substr($Initial, 2, 1) === '+' ? (substr($Initial, 3) ?: 0) : 0; $Initial = '*'; $Final = count($SectionOffsets); if ($Final > 0 && isset($SectionOffsets[$Final - 1][0])) { $Data = substr($Data, ($SectionOffsets[$Final - 1][0] + $Remainder) * 2); } if ($Terminal !== '*' && $Terminal !== 'Z') { $Data = substr($Data, 0, $Terminal * 2); $Terminal = '*'; } } elseif (substr($Initial, 0, 1) === 'S') { if (($PlusPos = strpos($Initial, '+')) !== false) { $SectionNum = substr($Initial, 1, $PlusPos - 1) ?: 0; $Remainder = substr($Initial, $PlusPos + 1) ?: 0; } else { $SectionNum = substr($Initial, 1) ?: 0; $Remainder = 0; } $Initial = '*'; if (isset($SectionOffsets[$SectionNum][0])) { $Data = substr($Data, ($SectionOffsets[$SectionNum][0] + $Remainder) * 2); } if ($Terminal !== '*' && $Terminal !== 'Z') { $Data = substr($Data, 0, $Terminal * 2); $Terminal = '*'; } } else { if ($Initial !== '*' && $Initial !== 'A') { $Data = substr($Data, $Initial * 2); $Initial = '*'; } if ($Terminal !== '*' && $Terminal !== 'Z') { $Data = substr($Data, 0, $Terminal * 2); $Terminal = '*'; } } }; /** * Responsible for handling any data fed to it from the recursor. It shouldn't * be called manually nor from any other contexts. It takes the data given to * it from the recursor and checks that data against the various signatures of * phpMussel, before returning the results of those checks back to the * recursor. * * @param string $str Raw binary data to be checked, supplied by the parent * closure (generally, the contents of the files to be scanned). * @param int $dpt Represents the current depth of recursion from which the * closure has been called, used for determining how far to indent any * entries generated for logging and for the display of scan results in * CLI. * @param string $ofn Represents the "original filename" of the file being * scanned (in this context, referring to the name supplied by the upload * client or CLI operator, as opposed to the temporary filename assigned * by the server or anything else). * @return array|bool Returns an array containing the results of the scan as * both an integer (the first element) and as human-readable text (the * second element), or returns false if any problems occur preventing the * data handler from completing its normal process. */ $phpMussel['DataHandler'] = function ($str = '', $dpt = 0, $ofn = '') use (&$phpMussel) { /** If the memory cache isn't set at this point, something has gone very wrong. */ if (!isset($phpMussel['InstanceCache'])) { throw new \Exception($phpMussel['L10N']->getString( 'required_variables_not_defined' ) ?: '[phpMussel] Required variables aren\'t defined: Can\'t continue.'); } /** Plugin hook: "DataHandler_start". */ $phpMussel['Execute_Hook']('DataHandler_start'); /** Identifies whether the scan target has been flagged for any reason yet. */ $flagged = false; /** Increment scan depth. */ $dpt++; /** Controls indenting relating to scan depth for normal logging and for CLI-mode scanning. */ $lnap = str_pad('> ', ($dpt + 1), '-', STR_PAD_LEFT); /** Output variable (for when the output is a string). */ $Out = ''; /** There's no point bothering to scan zero-byte files. */ if (!$str_len = strlen($str)) { return [1, '']; } $md5 = md5($str); $sha = sha1($str); $sha256 = hash('sha256', $str); $crc = hash('crc32b', $str); /** $fourcc: First four bytes of the scan target in hexadecimal notation. */ $fourcc = strtolower(bin2hex(substr($str, 0, 4))); /** $twocc: First two bytes of the scan target in hexadecimal notation. */ $twocc = substr($fourcc, 0, 4); /** * $CoExMeta: Contains metadata pertaining to the scan target, intended to * be used by the "complex extended" signatures. */ $CoExMeta = '$ofn:' . $ofn . ';md5($ofn):' . md5($ofn) . ';$dpt:' . $dpt . ';$str_len:' . $str_len . ';$md5:' . $md5 . ';$sha:' . $sha . ';$crc:' . $crc . ';$fourcc:' . $fourcc . ';$twocc:' . $twocc . ';'; /** Indicates whether a signature is considered a "weighted" signature. */ $phpMussel['InstanceCache']['weighted'] = false; /** Variables used for weighted signatures and for heuristic analysis. */ $heur = ['detections' => 0, 'weight' => 0, 'cli' => '', 'web' => '']; /** Scan target has no name? That's a little suspicious. */ if (!$ofn) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ":\n"; $phpMussel['InstanceCache']['detections_count']++; $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), $phpMussel['L10N']->getString('scan_missing_filename') ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('scan_missing_filename') ); return [2, $Out]; } /** URL-encoded version of the scan target name. */ $ofnSafe = urlencode($ofn); /** Generate cache ID. */ $phpMussel['HashCacheData'] = $md5 . md5($ofn); /** Register object scanned. */ if (isset($phpMussel['cli_args'][1]) && $phpMussel['cli_args'][1] == 'cli_scan') { $phpMussel['Stats-Increment']('CLI-Scanned', 1); } else { $phpMussel['Stats-Increment']($phpMussel['EOF'] ? 'API-Scanned' : 'Web-Scanned', 1); } /** * Check for the existence of a cache entry corresponding to the file * being scanned, and if it exists, use it instead of scanning the file. */ if (isset($phpMussel['HashCache']['Data'][$phpMussel['HashCacheData']])) { if (!$phpMussel['EOF']) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; } if (!empty($phpMussel['HashCache']['Data'][$phpMussel['HashCacheData']][2])) { $phpMussel['InstanceCache']['detections_count']++; $Out .= $phpMussel['HexSafe']($phpMussel['HashCache']['Data'][$phpMussel['HashCacheData']][2]); if (!empty($phpMussel['HashCache']['Data'][$phpMussel['HashCacheData']][3])) { $phpMussel['whyflagged'] .= $phpMussel['HexSafe']($phpMussel['HashCache']['Data'][$phpMussel['HashCacheData']][3]); } } /** Set debug values, if this has been enabled. */ if (isset($phpMussel['DebugArr'])) { $phpMussel['DebugArrKey'] = count($phpMussel['DebugArr']); $phpMussel['DebugArr'][$phpMussel['DebugArrKey']] = [ 'Filename' => $ofn, 'FromCache' => true, 'Depth' => $dpt, 'Size' => $str_len, 'MD5' => $md5, 'SHA1' => $sha, 'SHA256' => $sha256, 'CRC32B' => $crc, '2CC' => $twocc, '4CC' => $fourcc, 'ScanPhase' => $phpMussel['InstanceCache']['phase'], 'Container' => $phpMussel['InstanceCache']['container'], 'Results' => !$Out ? 1 : 2, 'Output' => $Out ]; } /** Object not flagged. */ if (!$Out) { return [1, '']; } /** Register object flagged. */ if (isset($phpMussel['cli_args'][1]) && $phpMussel['cli_args'][1] == 'cli_scan') { $phpMussel['Stats-Increment']('CLI-Flagged', 1); } else { $phpMussel['Stats-Increment']($phpMussel['EOF'] ? 'API-Flagged' : 'Web-Blocked', 1); } /** Object flagged. */ return [2, $Out]; } /** Indicates whether we're in CLI-mode. */ $climode = ($phpMussel['Mussel_sapi'] && $phpMussel['Mussel_PHP']) ? 1 : 0; if ( $phpMussel['Config']['attack_specific']['scannable_threshold'] > 0 && $str_len > $phpMussel['ReadBytes']($phpMussel['Config']['attack_specific']['scannable_threshold']) ) { $str_len = $phpMussel['ReadBytes']($phpMussel['Config']['attack_specific']['scannable_threshold']); $str = substr($str, 0, $str_len); $str_cut = 1; } else { $str_cut = 0; } /** Indicates whether we need to decode the contents of the scan target. */ $decode_or_not = (( $phpMussel['Config']['attack_specific']['decode_threshold'] > 0 && $str_len > $phpMussel['ReadBytes']($phpMussel['Config']['attack_specific']['decode_threshold']) ) || $str_len < 16) ? 0 : 1; /** These are sometimes used by the "CoEx" ("complex extended") signatures. */ $len_kb = ($str_len > 1024) ? 1 : 0; $len_hmb = ($str_len > 524288) ? 1 : 0; $len_mb = ($str_len > 1048576) ? 1 : 0; $len_hgb = ($str_len > 536870912) ? 1 : 0; $phase = $phpMussel['InstanceCache']['phase']; $container = $phpMussel['InstanceCache']['container']; $pdf_magic = ($fourcc == '25504446'); /** CoEx flags for configuration directives related to signatures. */ foreach ([ 'detect_adware', 'detect_joke_hoax', 'detect_pua_pup', 'detect_packer_packed', 'detect_shell', 'detect_deface', 'detect_encryption' ] as $Flag) { $$Flag = $phpMussel['Config']['signatures'][$Flag] ? 1 : 0; } /** Cleanup. */ unset($Flag); /** Available if the file is a Chrome extension. */ $CrxPubKey = empty($phpMussel['CrxPubKey']) ? '' : $phpMussel['CrxPubKey']; $CrxSig = empty($phpMussel['CrxSig']) ? '' : $phpMussel['CrxSig']; /** Get file extensions. */ list($xt, $xts, $gzxt, $gzxts) = $phpMussel['FetchExt']($ofn); $CoExMeta .= '$xt:' . $xt . ';$xts:' . $xts . ';'; /** Input ($str) as hexadecimal data. */ $str_hex = bin2hex($str); $str_hex_len = $str_len * 2; /** Input ($str) normalised. */ $str_norm = $phpMussel['prescan_normalise']($str, false, $decode_or_not); $str_norm_len = strlen($str_norm); /** Normalised input ($str_norm) as hexadecimal data. */ $str_hex_norm = bin2hex($str_norm); $str_hex_norm_len = $str_norm_len * 2; /** Input ($str) normalised for HTML. */ $str_html = $phpMussel['prescan_normalise']($str, true, $decode_or_not); $str_html_len = strlen($str_html); /** HTML normalised input ($str_html) as hexadecimal data. */ $str_hex_html = bin2hex($str_html); $str_hex_html_len = $str_html_len * 2; /** Look for potential Linux/ELF indicators. */ $is_elf = ($fourcc === '7f454c46' || $xt === 'elf'); /** Look for potential graphics/image indicators. */ $is_graphics = empty($str) ? false : $phpMussel['Indicator-Image']($xt, substr($str_hex, 0, 32)); /** Look for potential HTML indicators. */ $is_html = (strpos( ',asp*,dht*,eml*,hta*,htm*,jsp*,php*,sht*,', ',' . $xts . ',' ) !== false || preg_match( '/3c(?:21646f6374797065|6(?:120|26f6479|8656164|8746d6c|96672616d65|96d67|f626a656374)|7(?:36372697074|461626c65|469746c65))/i', $str_hex_norm ) || preg_match( '/(?:6(?:26f6479|8656164|8746d6c)|7(?:36372697074|461626c65|469746c65))3e/i', $str_hex_norm )); /** Look for potential email indicators. */ $is_email = (strpos( ',htm*,ema*,eml*,', ',' . $xts . ',' ) !== false || preg_match( '/0a(?:4(?:36f6e74656e742d54797065|4617465|6726f6d|d6573736167652d4944|d4' . '94d452d56657273696f6e)|5(?:265706c792d546f|2657475726e2d50617468|3656e64' . '6572|375626a656374|46f|82d4d61696c6572))3a20/i', $str_hex) || preg_match('/0a2d2d.{32}(?:2d2d)?(?:0d)?0a/i', $str_hex)); /** Look for potential Mach-O indicators. */ $is_macho = preg_match('/^(?:cafe(?:babe|d00d)|c[ef]faedfe|feedfac[ef])$/', $fourcc); /** Look for potential PDF indicators. */ $is_pdf = ($pdf_magic || $xt === 'pdf'); /** Look for potential Shockwave/SWF indicators. */ $is_swf = ( strpos(',435753,465753,5a5753,', ',' . substr($str_hex, 0, 6) . ',') !== false || strpos(',swf,swt,', ',' . $xt . ',') !== false ); /** "Infectable"? Used by ClamAV General and ClamAV ASCII signatures. */ $infectable = true; /** "Asciiable"? Used by all ASCII signatures. */ $asciiable = (bool)$str_hex_norm_len; /** Used to identify whether to check against OLE signatures. */ $is_ole = !empty($phpMussel['InstanceCache']['file_is_ole']) && ( !empty($phpMussel['InstanceCache']['file_is_macro']) || strpos(',bin,ole,xml,rels,', ',' . $xt . ',') !== false ); /** Worked by the switch file. */ $fileswitch = 'unassigned'; if (!isset($phpMussel['InstanceCache']['switch.dat'])) { $phpMussel['InstanceCache']['switch.dat'] = $phpMussel['ReadFileAsArray']($phpMussel['sigPath'] . 'switch.dat', FILE_IGNORE_NEW_LINES); } if (!$phpMussel['InstanceCache']['switch.dat']) { $phpMussel['InstanceCache']['scan_errors']++; if (!$phpMussel['Config']['signatures']['fail_silently']) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ":\n"; } $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('scan_signature_file_missing') . ' (switch.dat)' ); return [-3, $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), $phpMussel['L10N']->getString('scan_signature_file_missing') . ' (switch.dat)' ) . "\n"]; } } foreach ($phpMussel['InstanceCache']['switch.dat'] as $ThisRule) { $Switch = (strpos($ThisRule, ';') === false) ? $ThisRule : $phpMussel['substral']($ThisRule, ';'); if (strpos($Switch, '=') === false) { continue; } $Switch = explode('=', preg_replace('/[^\x20-\xff]/', '', $Switch)); if (empty($Switch[0])) { continue; } if (empty($Switch[1])) { $Switch[1] = false; } $theSwitch = $Switch[0]; $ThisRule = (strpos($ThisRule, ';') === false) ? [$ThisRule] : explode(';', $phpMussel['substrbl']($ThisRule, ';')); foreach ($ThisRule as $Fragment) { $Fragment = (strpos($Fragment, ':') === false) ? false : $phpMussel['SplitSigParts']($Fragment, 7); if (empty($Fragment[0])) { continue 2; } if ($Fragment[0] === 'LV') { if (!isset($Fragment[1]) || substr($Fragment[1], 0, 1) !== '$') { continue 2; } $lv_haystack = substr($Fragment[1],1); if (!isset($$lv_haystack) || is_array($$lv_haystack)) { continue 2; } $lv_haystack = $$lv_haystack; if ($climode) { $lv_haystack = $phpMussel['substral']($phpMussel['substral']($lv_haystack, '/'), "\\"); } $lv_needle = isset($Fragment[2]) ? $Fragment[2] : ''; $pos_A = isset($Fragment[3]) ? $Fragment[3] : 0; $pos_Z = isset($Fragment[4]) ? $Fragment[4] : 0; $lv_min = isset($Fragment[5]) ? $Fragment[5] : 0; $lv_max = isset($Fragment[6]) ? $Fragment[6] : -1; if (!$phpMussel['lv_match']($lv_needle, $lv_haystack, $pos_A, $pos_Z, $lv_min, $lv_max)) { continue 2; } } elseif (isset($Fragment[2])) { if (isset($Fragment[3])) { if ($Fragment[2] === 'A') { if ( strpos(',FD,FD-RX,FD-NORM,FD-NORM-RX,', ',' . $Fragment[0] . ',') === false || ( $Fragment[0] === 'FD' && strpos("\x01" . substr($str_hex, 0, $Fragment[3] * 2), "\x01" . $Fragment[1]) === false ) || ( $Fragment[0] === 'FD-RX' && !preg_match('/\A(?:' . $Fragment[1] . ')/i', substr($str_hex, 0, $Fragment[3] * 2)) ) || ( $Fragment[0] === 'FD-NORM' && strpos("\x01" . substr($str_hex_norm, 0, $Fragment[3] * 2), "\x01" . $Fragment[1]) === false ) || ( $Fragment[0] === 'FD-NORM-RX' && !preg_match('/\A(?:' . $Fragment[1] . ')/i', substr($str_hex_norm, 0, $Fragment[3] * 2)) ) ) { continue 2; } } elseif ( strpos(',FD,FD-RX,FD-NORM,FD-NORM-RX,', ',' . $Fragment[0] . ',') === false || ( $Fragment[0] === 'FD' && strpos(substr($str_hex, $Fragment[2] * 2, $Fragment[3] * 2), $Fragment[1]) === false ) || ( $Fragment[0] === 'FD-RX' && !preg_match('/(?:' . $Fragment[1] . ')/i', substr($str_hex, $Fragment[2] * 2, $Fragment[3]*2)) ) || ( $Fragment[0] === 'FD-NORM' && strpos(substr($str_hex_norm, $Fragment[2] * 2, $Fragment[3] * 2), $Fragment[1]) === false ) || ( $Fragment[0] === 'FD-NORM-RX' && !preg_match('/(?:' . $Fragment[1] . ')/i', substr($str_hex_norm, $Fragment[2] * 2, $Fragment[3]*2)) ) ) { continue 2; } } else { if ($Fragment[2] === 'A') { if ( strpos(',FN,FD,FD-RX,FD-NORM,FD-NORM-RX,', ',' . $Fragment[0] . ',') === false || ( $Fragment[0] === 'FN' && !preg_match('/\A(?:' . $Fragment[1] . ')/i', $ofn) ) || ( $Fragment[0] === 'FD' && strpos("\x01" . $str_hex, "\x01" . $Fragment[1]) === false ) || ( $Fragment[0] === 'FD-RX' && !preg_match('/\A(?:' . $Fragment[1] . ')/i', $str_hex) ) || ( $Fragment[0] === 'FD-NORM' && strpos("\x01" . $str_hex_norm, "\x01" . $Fragment[1]) === false ) || ( $Fragment[0] === 'FD-NORM-RX' && !preg_match('/\A(?:' . $Fragment[1] . ')/i', $str_hex_norm) ) ) { continue 2; } } elseif ( strpos(',FD,FD-RX,FD-NORM,FD-NORM-RX,', ',' . $Fragment[0] . ',') === false || ( $Fragment[0] === 'FD' && strpos(substr($str_hex, $Fragment[2] * 2), $Fragment[1]) === false ) || ( $Fragment[0] === 'FD-RX' && !preg_match('/(?:' . $Fragment[1] . ')/i', substr($str_hex, $Fragment[2] * 2)) ) || ( $Fragment[0] === 'FD-NORM' && strpos(substr($str_hex_norm, $Fragment[2] * 2), $Fragment[1]) === false ) || ( $Fragment[0] === 'FD-NORM-RX' && !preg_match('/(?:' . $Fragment[1] . ')/i', substr($str_hex_norm, $Fragment[2] * 2)) ) ) { continue 2; } } } elseif ( ($Fragment[0] === 'FN' && !preg_match('/(?:' . $Fragment[1] . ')/i', $ofn)) || ($Fragment[0] === 'FS-MIN' && $str_len < $Fragment[1]) || ($Fragment[0] === 'FS-MAX' && $str_len > $Fragment[1]) || ($Fragment[0] === 'FD' && strpos($str_hex, $Fragment[1]) === false) || ($Fragment[0] === 'FD-RX' && !preg_match('/(?:' . $Fragment[1] . ')/i', $str_hex)) || ($Fragment[0] === 'FD-NORM' && strpos($str_hex_norm, $Fragment[1]) === false) || ($Fragment[0] === 'FD-NORM-RX' && !preg_match('/(?:' . $Fragment[1] . ')/i', $str_hex_norm)) ) { continue 2; } elseif (substr($Fragment[0], 0, 1) === '$') { $vf = substr($Fragment[0], 1); if (!isset($$vf) || is_array($$vf) || $$vf != $Fragment[1]) { continue 2; } } elseif (substr($Fragment[0], 0, 2) === '!$') { $vf = substr($Fragment[0], 2); if (!isset($$vf) || is_array($$vf) || $$vf == $Fragment[1]) { continue 2; } } elseif (strpos(',FN,FS-MIN,FS-MAX,FD,FD-RX,FD-NORM,FD-NORM-RX,', ',' . $Fragment[0] . ',') === false) { continue 2; } } if (count($Switch) > 1) { if ($Switch[1] === 'true') { $$theSwitch = true; continue; } if ($Switch[1] === 'false') { $$theSwitch = false; continue; } $$theSwitch = $Switch[1]; } else { if (!isset($$theSwitch)) { $$theSwitch = true; continue; } $$theSwitch = (!$$theSwitch); } } unset($theSwitch, $Switch, $ThisRule); /** Section offsets. */ $SectionOffsets = []; /** Confirmation of whether or not the file is a valid PE file. */ $is_pe = false; /** Number of PE sections in the file. */ $NumOfSections = 0; $PEFileDescription = $PEFileVersion = $PEProductName = $PEProductVersion = $PECopyright = $PEOriginalFilename = $PECompanyName = ''; if ( !empty($phpMussel['InstanceCache']['PE_Sectional']) || !empty($phpMussel['InstanceCache']['PE_Extended']) || $phpMussel['Config']['attack_specific']['corrupted_exe'] ) { $PEArr = []; $PEArr['SectionArr'] = []; if ($twocc === '4d5a') { $PEArr['Offset'] = $phpMussel['UnpackSafe']('S', substr($str, 60, 4)); $PEArr['Offset'] = $PEArr['Offset'][1]; while (true) { $PEArr['DoScan'] = true; if ($PEArr['Offset'] < 1 || $PEArr['Offset'] > 16384 || $PEArr['Offset'] > $str_len) { $PEArr['DoScan'] = false; break; } $PEArr['Magic'] = substr($str, $PEArr['Offset'], 2); if ($PEArr['Magic']!=='PE') { $PEArr['DoScan'] = false; break; } $PEArr['Proc'] = $phpMussel['UnpackSafe']('S', substr($str, $PEArr['Offset'] + 4, 2)); $PEArr['Proc'] = $PEArr['Proc'][1]; if ($PEArr['Proc'] != 0x14c && $PEArr['Proc'] != 0x8664) { $PEArr['DoScan'] = false; break; } $PEArr['NumOfSections'] = $phpMussel['UnpackSafe']('S', substr($str, $PEArr['Offset'] + 6, 2)); $NumOfSections = $PEArr['NumOfSections'] = $PEArr['NumOfSections'][1]; $CoExMeta .= 'PE_Offset:' . $PEArr['Offset'] . ';PE_Proc:' . $PEArr['Proc'] . ';NumOfSections:' . $NumOfSections . ';'; if ($NumOfSections < 1 || $NumOfSections > 40) { $PEArr['DoScan'] = false; } break; } if (!$PEArr['DoScan']) { if ($phpMussel['Config']['attack_specific']['corrupted_exe']) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $heur['detections']++; $phpMussel['InstanceCache']['detections_count']++; $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), $phpMussel['L10N']->getString('corrupted') ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('corrupted') . ' (' . $ofnSafe . ')' ); } } else { $is_pe = true; $asciiable = false; $PEArr['OptHdrSize'] = $phpMussel['UnpackSafe']('S', substr($str, $PEArr['Offset'] + 20, 2)); $PEArr['OptHdrSize'] = $PEArr['OptHdrSize'][1]; for ($PEArr['k'] = 0; $PEArr['k'] < $NumOfSections; $PEArr['k']++) { $PEArr['SectionArr'][$PEArr['k']] = []; $PEArr['SectionArr'][$PEArr['k']]['SectionHead'] = substr($str, $PEArr['Offset'] + 24 + $PEArr['OptHdrSize'] + ($PEArr['k'] * 40), $NumOfSections * 40); $PEArr['SectionArr'][$PEArr['k']]['SectionName'] = str_ireplace("\x00", '', substr($PEArr['SectionArr'][$PEArr['k']]['SectionHead'], 0, 8)); $PEArr['SectionArr'][$PEArr['k']]['VirtualSize'] = $phpMussel['UnpackSafe']('S', substr($PEArr['SectionArr'][$PEArr['k']]['SectionHead'], 8, 4)); $PEArr['SectionArr'][$PEArr['k']]['VirtualSize'] = $PEArr['SectionArr'][$PEArr['k']]['VirtualSize'][1]; $PEArr['SectionArr'][$PEArr['k']]['VirtualAddress'] = $phpMussel['UnpackSafe']('S', substr($PEArr['SectionArr'][$PEArr['k']]['SectionHead'], 12, 4)); $PEArr['SectionArr'][$PEArr['k']]['VirtualAddress'] = $PEArr['SectionArr'][$PEArr['k']]['VirtualAddress'][1]; $PEArr['SectionArr'][$PEArr['k']]['SizeOfRawData'] = $phpMussel['UnpackSafe']('S', substr($PEArr['SectionArr'][$PEArr['k']]['SectionHead'], 16, 4)); $PEArr['SectionArr'][$PEArr['k']]['SizeOfRawData'] = $PEArr['SectionArr'][$PEArr['k']]['SizeOfRawData'][1]; $PEArr['SectionArr'][$PEArr['k']]['PointerToRawData'] = $phpMussel['UnpackSafe']('S', substr($PEArr['SectionArr'][$PEArr['k']]['SectionHead'], 20, 4)); $PEArr['SectionArr'][$PEArr['k']]['PointerToRawData'] = $PEArr['SectionArr'][$PEArr['k']]['PointerToRawData'][1]; $PEArr['SectionArr'][$PEArr['k']]['SectionData'] = substr( $str, $PEArr['SectionArr'][$PEArr['k']]['PointerToRawData'], $PEArr['SectionArr'][$PEArr['k']]['SizeOfRawData'] ); $SectionOffsets[$PEArr['k']] = [ $PEArr['SectionArr'][$PEArr['k']]['PointerToRawData'], $PEArr['SectionArr'][$PEArr['k']]['SizeOfRawData'] ]; $PEArr['SectionArr'][$PEArr['k']]['MD5'] = md5( $PEArr['SectionArr'][$PEArr['k']]['SectionData'] ); $phpMussel['PEData'] .= $PEArr['SectionArr'][$PEArr['k']]['SizeOfRawData'] . ':' . $PEArr['SectionArr'][$PEArr['k']]['MD5'] . ':' . $ofn . '-' . $PEArr['SectionArr'][$PEArr['k']]['SectionName'] . "\n"; $CoExMeta .= 'SectionName:' . $PEArr['SectionArr'][$PEArr['k']]['SectionName'] . ';VirtualSize:' . $PEArr['SectionArr'][$PEArr['k']]['VirtualSize'] . ';VirtualAddress:' . $PEArr['SectionArr'][$PEArr['k']]['VirtualAddress'] . ';SizeOfRawData:' . $PEArr['SectionArr'][$PEArr['k']]['SizeOfRawData'] . ';MD5:' . $PEArr['SectionArr'][$PEArr['k']]['MD5'] . ';'; $PEArr['SectionArr'][$PEArr['k']] = $PEArr['SectionArr'][$PEArr['k']]['SizeOfRawData'] . ':' . $PEArr['SectionArr'][$PEArr['k']]['MD5'] . ':'; } if (strpos($str, "V\x00a\x00r\x00F\x00i\x00l\x00e\x00I\x00n\x00f\x00o\x00\x00\x00\x00\x00\x24") !== false) { $PEArr['Parts'] = $phpMussel['substral']($str, "V\x00a\x00r\x00F\x00i\x00l\x00e\x00I\x00n\x00f\x00o\x00\x00\x00\x00\x00\x24"); $PEArr['FINFO'] = []; foreach ([ ["F\x00i\x00l\x00e\x00D\x00e\x00s\x00c\x00r\x00i\x00p\x00t\x00i\x00o\x00n\x00\x00\x00", 'PEFileDescription'], ["F\x00i\x00l\x00e\x00V\x00e\x00r\x00s\x00i\x00o\x00n\x00\x00\x00", 'PEFileVersion'], ["P\x00r\x00o\x00d\x00u\x00c\x00t\x00N\x00a\x00m\x00e\x00\x00\x00", 'PEProductName'], ["P\x00r\x00o\x00d\x00u\x00c\x00t\x00V\x00e\x00r\x00s\x00i\x00o\x00n\x00\x00\x00", 'PEProductVersion'], ["L\x00e\x00g\x00a\x00l\x00C\x00o\x00p\x00y\x00r\x00i\x00g\x00h\x00t\x00\x00\x00", 'PECopyright'], ["O\x00r\x00i\x00g\x00i\x00n\x00a\x00l\x00F\x00i\x00l\x00e\x00n\x00a\x00m\x00e\x00\x00\x00", 'PEOriginalFilename'], ["C\x00o\x00m\x00p\x00a\x00n\x00y\x00N\x00a\x00m\x00e\x00\x00\x00", 'PECompanyName'], ] as $PEVars) { if (strpos($PEArr['Parts'], $PEVars[0]) !== false && ( ${$PEVars[1]} = trim(str_ireplace("\x00", '', $phpMussel['substrbf']( $phpMussel['substral']($PEArr['Parts'], $PEVars[0]), "\x00\x00\x00" ))) )) { $PEArr['FINFO'][] = '$' . $PEVars[1] . ':' . md5(${$PEVars[1]}) . ':' . strlen(${$PEVars[1]}) . ':'; } } unset($PEVars, $PEArr['Parts']); } } } } /** Look for potential indicators of not being HTML. */ $is_not_html = (!$is_html && ($is_macho || $is_elf || $is_pe)); /** Look for potential indicators of not being PHP. */ $is_not_php = (( strpos(',phar,', ',' . $xt . ',') === false && strpos(',php*,', ',' . $xts . ',') === false && strpos(',phar,', ',' . $gzxt . ',') === false && strpos(',php*,', ',' . $gzxts . ',') === false && strpos($str_hex_norm, '3c3f706870') === false ) || $is_pe); /** Set debug values, if this has been enabled. */ if (isset($phpMussel['DebugArr'])) { $phpMussel['DebugArrKey'] = count($phpMussel['DebugArr']); $phpMussel['DebugArr'][$phpMussel['DebugArrKey']] = [ 'Filename' => $ofn, 'FromCache' => false, 'Depth' => $dpt, 'Size' => $str_len, 'MD5' => $md5, 'SHA1' => $sha, 'SHA256' => $sha256, 'CRC32B' => $crc, '2CC' => $twocc, '4CC' => $fourcc, 'ScanPhase' => $phase, 'Container' => $container, 'FileSwitch' => $fileswitch, 'Is_ELF' => $is_elf, 'Is_Graphics' => $is_graphics, 'Is_HTML' => $is_html, 'Is_Email' => $is_email, 'Is_MachO' => $is_macho, 'Is_PDF' => $is_pdf, 'Is_SWF' => $is_swf, 'Is_PE' => $is_pe, 'Is_Not_HTML' => $is_not_html, 'Is_Not_PHP' => $is_not_php ]; if ($is_pe) { $phpMussel['DebugArr'][$phpMussel['DebugArrKey']] += [ 'NumOfSections' => $NumOfSections, 'PEFileDescription' => $PEFileDescription, 'PEFileVersion' => $PEFileVersion, 'PEProductName' => $PEProductName, 'PEProductVersion' => $PEProductVersion, 'PECopyright' => $PECopyright, 'PEOriginalFilename' => $PEOriginalFilename, 'PECompanyName' => $PECompanyName ]; } } /** Plugin hook: "during_scan". */ $phpMussel['Execute_Hook']('during_scan'); /** Begin URL scanner. */ if ( isset($phpMussel['InstanceCache']['URL_Scanner']) || !empty($phpMussel['Config']['urlscanner']['lookup_hphosts']) || !empty($phpMussel['Config']['urlscanner']['google_api_key']) ) { $phpMussel['LookupCount'] = 0; $URLScanner = [ 'FixedSource' => preg_replace('~(data|f(ile|tps?)|https?|sftp):~i', "\x01\\1:", str_replace("\\", '/', $str_norm)) . "\x01", 'DomainsNoLookup' => [], 'DomainsCount' => 0, 'Domains' => [], 'DomainPartsNoLookup' => [], 'DomainParts' => [], 'Queries' => [], 'URLsNoLookup' => [], 'URLsCount' => 0, 'URLs' => [], 'URLPartsNoLookup' => [], 'URLParts' => [], 'TLDs' => [], 'Iterable' => 0, 'Matches' => [] ]; if (preg_match_all( '~(?:data|f(?:ile|tps?)|https?|sftp)://(?:www\d{0,3}\.)?([\da-z.-]{1,512})[^\da-z.-]~i', $URLScanner['FixedSource'], $URLScanner['Matches'] )) { foreach ($URLScanner['Matches'][1] as $ThisURL) { $URLScanner['DomainParts'][$URLScanner['Iterable']] = $ThisURL; if (strpos($URLScanner['DomainParts'][$URLScanner['Iterable']], '.') !== false) { $URLScanner['TLDs'][$URLScanner['Iterable']] = 'TLD:' . $phpMussel['substral']( $URLScanner['DomainParts'][$URLScanner['Iterable']], '.' ) . ':'; } $ThisURL = md5($ThisURL) . ':' . strlen($ThisURL) . ':'; $URLScanner['Domains'][$URLScanner['Iterable']] = 'DOMAIN:' . $ThisURL; $URLScanner['DomainsNoLookup'][$URLScanner['Iterable']] = 'DOMAIN-NOLOOKUP:' . $ThisURL; $URLScanner['Iterable']++; } } $URLScanner['DomainsNoLookup'] = array_unique($URLScanner['DomainsNoLookup']); $URLScanner['Domains'] = array_unique($URLScanner['Domains']); $URLScanner['DomainParts'] = array_unique($URLScanner['DomainParts']); $URLScanner['TLDs'] = array_unique($URLScanner['TLDs']); sort($URLScanner['DomainsNoLookup']); sort($URLScanner['Domains']); sort($URLScanner['DomainParts']); sort($URLScanner['TLDs']); $URLScanner['Iterable'] = 0; $URLScanner['Matches'] = ''; if (preg_match_all( '~(?:data|f(?:ile|tps?)|https?|sftp)://(?:www\d{0,3}\.)?([!#$&-;=?@-\[\]_a-z\~]+)[^!#$&-;=?@-\[\]_a-z\~]~i', $URLScanner['FixedSource'], $URLScanner['Matches'] )) { foreach ($URLScanner['Matches'][1] as $ThisURL) { if (strlen($ThisURL) > 4096) { $ThisURL = substr($ThisURL, 0, 4096); } $URLScanner['This'] = md5($ThisURL) . ':' . strlen($ThisURL) . ':'; $URLScanner['URLsNoLookup'][$URLScanner['Iterable']] = 'URL-NOLOOKUP:' . $URLScanner['This']; $URLScanner['URLParts'][$URLScanner['Iterable']] = $ThisURL; $URLScanner['URLs'][$URLScanner['Iterable']] = 'URL:' . $URLScanner['This']; $URLScanner['Iterable']++; if (preg_match('/[^\da-z.-]$/i', $ThisURL)) { $URLScanner['x'] = preg_replace('/[^\da-z.-]+$/i', '', $ThisURL); $URLScanner['This'] = md5($URLScanner['x']) . ':' . strlen($URLScanner['x']) . ':'; $URLScanner['URLsNoLookup'][$URLScanner['Iterable']] = 'URL-NOLOOKUP:' . $URLScanner['This']; $URLScanner['URLParts'][$URLScanner['Iterable']] = $URLScanner['x']; $URLScanner['URLs'][$URLScanner['Iterable']] = 'URL:' . $URLScanner['This']; $URLScanner['Iterable']++; } if (strpos($ThisURL, '?') !== false) { $URLScanner['x'] = $phpMussel['substrbf']($ThisURL, '?'); $URLScanner['This'] = md5($URLScanner['x']) . ':' . strlen($URLScanner['x']) . ':'; $URLScanner['URLsNoLookup'][$URLScanner['Iterable']] = 'URL-NOLOOKUP:' . $URLScanner['This']; $URLScanner['URLParts'][$URLScanner['Iterable']] = $URLScanner['x']; $URLScanner['URLs'][$URLScanner['Iterable']] = 'URL:' . $URLScanner['This']; $URLScanner['x'] = $phpMussel['substraf']($ThisURL, '?'); $URLScanner['Queries'][$URLScanner['Iterable']] = 'QUERY:' . md5($URLScanner['x']) . ':' . strlen($URLScanner['x']) . ':'; $URLScanner['Iterable']++; } } unset($URLScanner['x'], $URLScanner['This']); } unset($ThisURL, $URLScanner['Matches']); $URLScanner['URLsNoLookup'] = array_unique($URLScanner['URLsNoLookup']); $URLScanner['URLs'] = array_unique($URLScanner['URLs']); $URLScanner['URLParts'] = array_unique($URLScanner['URLParts']); $URLScanner['Queries'] = array_unique($URLScanner['Queries']); sort($URLScanner['URLsNoLookup']); sort($URLScanner['URLs']); sort($URLScanner['URLParts']); sort($URLScanner['Queries']); } /** Process non-mappable signatures. */ foreach ([ ['General_Command_Detections', 0], ['Hash', 1], ['PE_Sectional', 2], ['PE_Extended', 3], ['URL_Scanner', 4], ['Complex_Extended', 5] ] as $ThisConf) { /** Plugin hook: "new_sigfile_type". */ $phpMussel['Execute_Hook']('new_sigfile_type'); $SigFiles = isset($phpMussel['InstanceCache'][$ThisConf[0]]) ? explode(',', $phpMussel['InstanceCache'][$ThisConf[0]]) : []; foreach ($SigFiles as $SigFile) { if (!$SigFile) { continue; } if (!isset($phpMussel['InstanceCache'][$SigFile])) { $phpMussel['InstanceCache'][$SigFile] = $phpMussel['ReadFile']($phpMussel['sigPath'] . $SigFile); } /** Plugin hook: "new_sigfile". */ $phpMussel['Execute_Hook']('new_sigfile'); if (!$phpMussel['InstanceCache'][$SigFile]) { $phpMussel['InstanceCache']['scan_errors']++; if (!$phpMussel['Config']['signatures']['fail_silently']) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ":\n"; } $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('scan_signature_file_missing') . ' (' . $SigFile . ')' ); return [-3, $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), $phpMussel['L10N']->getString('scan_signature_file_missing') . ' (' . $SigFile . ')' ) . "\n"]; } } elseif ($ThisConf[1] === 0) { if (substr($phpMussel['InstanceCache'][$SigFile], 0, 9) === 'phpMussel') { $phpMussel['InstanceCache'][$SigFile] = substr($phpMussel['InstanceCache'][$SigFile], 11, -1); } $ArrayCSV = explode(',', $phpMussel['InstanceCache'][$SigFile]); foreach ($ArrayCSV as $ItemCSV) { if (strpos($str_hex_norm, $ItemCSV) !== false) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $heur['detections']++; $phpMussel['InstanceCache']['detections_count']++; $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('scan_command_injection') ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('scan_command_injection') . ', \'' . $phpMussel['HexSafe']($ItemCSV) . '\' (' . $ofnSafe . ')' ); } } unset($ItemCSV, $ArrayCSV); } elseif ($ThisConf[1] === 1) { foreach ([$md5, $sha, $sha256] as $CheckThisHash) { if (strpos($phpMussel['InstanceCache'][$SigFile], "\n" . $CheckThisHash . ':' . $str_len . ':') !== false) { $xSig = $phpMussel['substraf']($phpMussel['InstanceCache'][$SigFile], "\n" . $CheckThisHash . ':' . $str_len . ':'); if (strpos($xSig, "\n") !== false) { $xSig = $phpMussel['substrbf']($xSig, "\n"); } $xSig = $phpMussel['vn_shorthand']($xSig); if ( strpos($phpMussel['InstanceCache']['greylist'], ',' . $xSig . ',') === false && empty($phpMussel['InstanceCache']['ignoreme']) ) { $phpMussel['Detected']($heur, $lnap, $xSig, $ofn, $ofnSafe, $Out, $flagged, $md5, $str_len); } } } } elseif ($ThisConf[1] === 2) { for ($PEArr['k'] = 0; $PEArr['k'] < $NumOfSections; $PEArr['k']++) { if (strpos($phpMussel['InstanceCache'][$SigFile], $PEArr['SectionArr'][$PEArr['k']]) !== false) { $xSig = $phpMussel['substraf']($phpMussel['InstanceCache'][$SigFile], $PEArr['SectionArr'][$PEArr['k']]); if (strpos($xSig, "\n") !== false) { $xSig = $phpMussel['substrbf']($xSig, "\n"); } $xSig = $phpMussel['vn_shorthand']($xSig); if ( strpos($phpMussel['InstanceCache']['greylist'], ',' . $xSig . ',') === false && empty($phpMussel['InstanceCache']['ignoreme']) ) { $phpMussel['Detected']($heur, $lnap, $xSig, $ofn, $ofnSafe, $Out, $flagged, $md5, $str_len); } } } } elseif ($ThisConf[1] === 3) { if (!empty($PEArr['FINFO'])) { foreach ($PEArr['FINFO'] as $PEArr['ThisPart']) { if (substr_count($phpMussel['InstanceCache'][$SigFile], $PEArr['ThisPart'])) { $xSig = $phpMussel['substraf']($phpMussel['InstanceCache'][$SigFile], $PEArr['ThisPart']); if (strpos($xSig, "\n") !== false) { $xSig = $phpMussel['substrbf']($xSig, "\n"); } $xSig = $phpMussel['vn_shorthand']($xSig); if ( !substr_count($phpMussel['InstanceCache']['greylist'], ',' . $xSig . ',') && empty($phpMussel['InstanceCache']['ignoreme']) ) { $phpMussel['Detected']($heur, $lnap, $xSig, $ofn, $ofnSafe, $Out, $flagged, $md5, $str_len); } } } } } elseif ($ThisConf[1] === 4) { foreach ([$URLScanner['DomainsNoLookup'], $URLScanner['URLsNoLookup']] as $URLScanner['ThisArr']) { foreach ($URLScanner['ThisArr'] as $URLScanner['This']) { if (strpos($phpMussel['InstanceCache'][$SigFile], $URLScanner['This']) !== false) { $xSig = $phpMussel['substraf']($phpMussel['InstanceCache'][$SigFile], $URLScanner['This']); if (strpos($xSig, "\n") !== false) { $xSig = $phpMussel['substrbf']($xSig, "\n"); } if (substr($URLScanner['This'], 0, 15) === 'DOMAIN-NOLOOKUP') { $URLScanner['DomainPartsNoLookup'][$xSig] = true; continue; } $URLScanner['URLPartsNoLookup'][$xSig] = true; } } } foreach ([ $URLScanner['TLDs'], $URLScanner['Domains'], $URLScanner['URLs'], $URLScanner['Queries'] ] as $URLScanner['ThisArr']) { foreach ($URLScanner['ThisArr'] as $URLScanner['This']) { if (substr_count($phpMussel['InstanceCache'][$SigFile], $URLScanner['This'])) { $xSig = $phpMussel['substraf']($phpMussel['InstanceCache'][$SigFile], $URLScanner['This']); if (strpos($xSig, "\n") !== false) { $xSig = $phpMussel['substrbf']($xSig, "\n"); } if ( ($xSig = $phpMussel['vn_shorthand']($xSig)) && !substr_count($phpMussel['InstanceCache']['greylist'], ',' . $xSig . ',') && empty($phpMussel['InstanceCache']['ignoreme']) ) { $phpMussel['Detected']($heur, $lnap, $xSig, $ofn, $ofnSafe, $Out, $flagged, $md5, $str_len); } } } } } elseif ($ThisConf[1] === 5) { $SigName = ''; foreach ([ 'NumOfSections', 'PECompanyName', 'PECopyright', 'PEFileDescription', 'PEFileVersion', 'PEOriginalFilename', 'PEProductName', 'PEProductVersion', 'container', 'crc', 'fileswitch', 'fourcc', 'is_elf', 'is_email', 'is_graphics', 'is_html', 'is_macho', 'is_not_html', 'is_not_php', 'is_ole', 'is_pdf', 'is_pe', 'is_swf', 'md5', 'phase', 'sha', 'sha256', 'str_len', 'twocc', 'xt', 'xts' ] as $ThisCheckFor) { if (!isset($$ThisCheckFor)) { continue; } $ThisCheckValue = "\n$" . $ThisCheckFor . ':' . ( substr($ThisCheckFor, 0, 3) !== 'is_' ? $$ThisCheckFor : ($$ThisCheckFor ? '1' : '0') ) . ';'; if (strpos($phpMussel['InstanceCache'][$SigFile], $ThisCheckValue) === false) { continue; } $xSig = explode($ThisCheckValue, $phpMussel['InstanceCache'][$SigFile]); $xSigCount = count($xSig); if (isset($xSig[0])) { $xSig[0] = ''; } if ($xSigCount > 0) { for ($xIter = 1; $xIter < $xSigCount; $xIter++) { if (strpos($xSig[$xIter], "\n") !== false) { $xSig[$xIter] = $phpMussel['substrbf']($xSig[$xIter], "\n"); } if (strpos($xSig[$xIter], ';') !== false) { if (strpos($xSig[$xIter], ':') === false) { continue; } $SigName = $phpMussel['vn_shorthand']($phpMussel['substral']($xSig[$xIter], ';')); $xSig[$xIter] = explode(';', $phpMussel['substrbl']($xSig[$xIter], ';')); } else { $SigName = $phpMussel['vn_shorthand']($xSig[$xIter]); $xSig[$xIter] = []; } foreach ($xSig[$xIter] as $ThisSigPart) { if (empty($ThisSigPart)) { continue 2; } $ThisSigPart = $phpMussel['SplitSigParts']($ThisSigPart, 7); if ($ThisSigPart[0] === 'LV') { if (!isset($ThisSigPart[1]) || substr($ThisSigPart[1], 0, 1) !== '$') { continue 2; } $lv_haystack = substr($ThisSigPart[1], 1); if (!isset($$lv_haystack) || is_array($$lv_haystack)) { continue 2; } $lv_haystack = $$lv_haystack; if ($climode) { $lv_haystack = $phpMussel['substral']($phpMussel['substral']($lv_haystack, '/'), "\\"); } $lv_needle = (isset($ThisSigPart[2])) ? $ThisSigPart[2] : ''; $pos_A = (isset($ThisSigPart[3])) ? $ThisSigPart[3] : 0; $pos_Z = (isset($ThisSigPart[4])) ? $ThisSigPart[4] : 0; $lv_min = (isset($ThisSigPart[5])) ? $ThisSigPart[5] : 0; $lv_max = (isset($ThisSigPart[6])) ? $ThisSigPart[6] : -1; if (!$phpMussel['lv_match']($lv_needle, $lv_haystack, $pos_A, $pos_Z, $lv_min, $lv_max)) { continue 2; } continue; } if (isset($ThisSigPart[2])) { if (isset($ThisSigPart[3])) { if ($ThisSigPart[2] == 'A') { if (strpos(',FD,FD-RX,FD-NORM,FD-NORM-RX,META,', ',' . $ThisSigPart[0] . ',') === false || ( $ThisSigPart[0] == 'FD' && strpos("\x01" . substr($str_hex, 0, $ThisSigPart[3] * 2), "\x01" . $ThisSigPart[1]) === false ) || ( $ThisSigPart[0] == 'FD-RX' && !preg_match('/\A(?:' . $ThisSigPart[1] . ')/i', substr($str_hex, 0, $ThisSigPart[3] * 2)) ) || ( $ThisSigPart[0] == 'FD-NORM' && strpos("\x01" . substr($str_hex_norm, 0, $ThisSigPart[3] * 2), "\x01" . $ThisSigPart[1]) === false ) || ( $ThisSigPart[0] == 'FD-NORM-RX' && !preg_match('/\A(?:' . $ThisSigPart[1] . ')/i', substr($str_hex_norm, 0, $ThisSigPart[3] * 2)) ) || ( $ThisSigPart[0] == 'META' && !preg_match('/\A(?:' . $ThisSigPart[1] . ')/i', substr($CoExMeta, 0, $ThisSigPart[3] * 2)) )) { continue 2; } continue; } if (strpos(',FD,FD-RX,FD-NORM,FD-NORM-RX,META,', ',' . $ThisSigPart[0] . ',') === false || ( $ThisSigPart[0] == 'FD' && strpos(substr($str_hex, $ThisSigPart[2] * 2, $ThisSigPart[3] * 2), $ThisSigPart[1]) === false ) || ( $ThisSigPart[0] == 'FD-RX' && !preg_match('/(?:' . $ThisSigPart[1] . ')/i', substr($str_hex, $ThisSigPart[2] * 2, $ThisSigPart[3] * 2)) ) || ( $ThisSigPart[0] == 'FD-NORM' && strpos(substr($str_hex_norm, $ThisSigPart[2] * 2, $ThisSigPart[3] * 2), $ThisSigPart[1]) === false ) || ( $ThisSigPart[0] == 'FD-NORM-RX' && !preg_match('/(?:' . $ThisSigPart[1] . ')/i', substr($str_hex_norm, $ThisSigPart[2] * 2, $ThisSigPart[3] * 2)) ) || ( $ThisSigPart[0] == 'META' && !preg_match('/(?:' . $ThisSigPart[1] . ')/i', substr($CoExMeta, $ThisSigPart[2] * 2, $ThisSigPart[3] * 2)) )) { continue 2; } continue; } if ($ThisSigPart[2] == 'A') { if (strpos(',FN,FD,FD-RX,FD-NORM,FD-NORM-RX,META,', ',' . $ThisSigPart[0] . ',') === false || ( $ThisSigPart[0] == 'FN' && !preg_match('/\A(?:' . $ThisSigPart[1] . ')/i', $ofn) ) || ( $ThisSigPart[0] == 'FD' && strpos("\x01" . $str_hex, "\x01" . $ThisSigPart[1]) === false ) || ( $ThisSigPart[0] == 'FD-RX' && !preg_match('/\A(?:' . $ThisSigPart[1] . ')/i', $str_hex) ) || ( $ThisSigPart[0] == 'FD-NORM' && strpos("\x01" . $str_hex_norm, "\x01" . $ThisSigPart[1]) === false ) || ( $ThisSigPart[0] == 'FD-NORM-RX' && !preg_match('/\A(?:' . $ThisSigPart[1] . ')/i', $str_hex_norm) ) || ( $ThisSigPart[0] == 'META' && !preg_match('/\A(?:' . $ThisSigPart[1] . ')/i', $CoExMeta) )) { continue 2; } continue; } if (strpos(',FD,FD-RX,FD-NORM,FD-NORM-RX,META,', ',' . $ThisSigPart[0] . ',') === false || ( $ThisSigPart[0] == 'FD' && strpos(substr($str_hex, $ThisSigPart[2] * 2), $ThisSigPart[1]) === false ) || ( $ThisSigPart[0] == 'FD-RX' && !preg_match('/(?:' . $ThisSigPart[1] . ')/i', substr($str_hex, $ThisSigPart[2] * 2)) ) || ( $ThisSigPart[0] == 'FD-NORM' && strpos(substr($str_hex_norm, $ThisSigPart[2] * 2), $ThisSigPart[1]) === false ) || ( $ThisSigPart[0] == 'FD-NORM-RX' && !preg_match('/(?:' . $ThisSigPart[1] . ')/i', substr($str_hex_norm, $ThisSigPart[2] * 2)) ) || ( $ThisSigPart[0] == 'META' && !preg_match('/(?:' . $ThisSigPart[1] . ')/i', substr($CoExMeta, $ThisSigPart[2] * 2)) )) { continue 2; } continue; } if (( $ThisSigPart[0] == 'FN' && !preg_match('/(?:' . $ThisSigPart[1] . ')/i', $ofn) ) || ( $ThisSigPart[0] == 'FS-MIN' && $str_len < $ThisSigPart[1] ) || ( $ThisSigPart[0] == 'FS-MAX' && $str_len > $ThisSigPart[1] ) || ( $ThisSigPart[0] == 'FD' && strpos($str_hex, $ThisSigPart[1]) === false ) || ( $ThisSigPart[0] == 'FD-RX' && !preg_match('/(?:' . $ThisSigPart[1] . ')/i', $str_hex) ) || ( $ThisSigPart[0] == 'FD-NORM' && strpos($str_hex_norm, $ThisSigPart[1]) === false ) || ( $ThisSigPart[0] == 'FD-NORM-RX' && !preg_match('/(?:' . $ThisSigPart[1] . ')/i', $str_hex_norm) ) || ( $ThisSigPart[0] == 'META' && !preg_match('/(?:' . $ThisSigPart[1] . ')/i', $CoExMeta) )) { continue 2; } if (substr($ThisSigPart[0], 0, 1) === '$') { $vf = substr($ThisSigPart[0], 1); if (!isset($$vf) || is_array($$vf) || $$vf != $ThisSigPart[1]) { continue 2; } continue; } if (substr($ThisSigPart[0], 0, 2) === '!$') { $vf = substr($ThisSigPart[0], 2); if (!isset($$vf) || is_array($$vf) || $$vf == $ThisSigPart[1]) { continue 2; } continue; } if (strpos(',FN,FS-MIN,FS-MAX,FD,FD-RX,FD-NORM,FD-NORM-RX,META,', ',' . $ThisSigPart[0] . ',') === false) { continue 2; } } if ( $SigName && strpos($phpMussel['InstanceCache']['greylist'], ',' . $SigName . ',') === false && empty($phpMussel['InstanceCache']['ignoreme']) ) { $phpMussel['Detected']($heur, $lnap, $SigName, $ofn, $ofnSafe, $Out, $flagged, $md5, $str_len); } } } } /** Cleanup. */ unset($SigName, $xIter, $xSigCount, $xSig, $ThisSigPart, $ThisCheckValue, $ThisCheckFor); } } } /** Process mappable signatures. */ foreach ([ ['Filename', 'str_hex', 'str_hex_len', 2], ['Standard', 'str_hex', 'str_hex_len', 0], ['Normalised', 'str_hex_norm', 'str_hex_norm_len', 0], ['HTML', 'str_hex_html', 'str_hex_html_len', 0], ['Standard_RegEx', 'str_hex', 'str_hex_len', 1], ['Normalised_RegEx', 'str_hex_norm', 'str_hex_norm_len', 1], ['HTML_RegEx', 'str_hex_html', 'str_hex_html_len', 1] ] as $ThisConf) { $DataSource = $ThisConf[1]; $DataSourceLen = $ThisConf[2]; /** Plugin hook: "new_sigfile_type". */ $phpMussel['Execute_Hook']('new_sigfile_type'); $SigFiles = isset($phpMussel['InstanceCache'][$ThisConf[0]]) ? explode(',', $phpMussel['InstanceCache'][$ThisConf[0]]) : []; foreach ($SigFiles as $SigFile) { if (!$SigFile) { continue; } if (!isset($phpMussel['InstanceCache'][$SigFile])) { $phpMussel['InstanceCache'][$SigFile] = $phpMussel['ReadFileAsArray']($phpMussel['sigPath'] . $SigFile, FILE_IGNORE_NEW_LINES); } /** Plugin hook: "new_sigfile". */ $phpMussel['Execute_Hook']('new_sigfile'); if (!$phpMussel['InstanceCache'][$SigFile]) { $phpMussel['InstanceCache']['scan_errors']++; if (!$phpMussel['Config']['signatures']['fail_silently']) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ":\n"; } $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('scan_signature_file_missing') . ' (' . $SigFile . ')' ); return [-3, $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), $phpMussel['L10N']->getString('scan_signature_file_missing') . ' (' . $SigFile . ')' ) . "\n"]; } continue; } $NumSigs = count($phpMussel['InstanceCache'][$SigFile]); for ($SigNum = 0; $SigNum < $NumSigs; $SigNum++) { if (!$ThisSig = $phpMussel['InstanceCache'][$SigFile][$SigNum]) { continue; } if (substr($ThisSig, 0, 1) == '>') { $ThisSig = explode('>', $ThisSig, 4); if (!isset($ThisSig[1], $ThisSig[2], $ThisSig[3])) { break; } $ThisSig[3] = (int)$ThisSig[3]; if ($ThisSig[1] == 'FN') { if (!preg_match('/(?:' . $ThisSig[2] . ')/i', $ofn)) { if ($ThisSig[3] <= $SigNum) { break; } $SigNum = $ThisSig[3] - 1; } } elseif ($ThisSig[1] == 'FS-MIN') { if ($str_len < $ThisSig[2]) { if ($ThisSig[3] <= $SigNum) { break; } $SigNum = $ThisSig[3] - 1; } } elseif ($ThisSig[1] == 'FS-MAX') { if ($str_len > $ThisSig[2]) { if ($ThisSig[3] <= $SigNum) { break; } $SigNum = $ThisSig[3] - 1; } } elseif ($ThisSig[1] == 'FD') { if (strpos($$DataSource, $ThisSig[2]) === false) { if ($ThisSig[3] <= $SigNum) { break; } $SigNum = $ThisSig[3] - 1; } } elseif ($ThisSig[1] == 'FD-RX') { if (!preg_match('/(?:' . $ThisSig[2] . ')/i', $$DataSource)) { if ($ThisSig[3] <= $SigNum) { break; } $SigNum = $ThisSig[3] - 1; } } elseif (substr($ThisSig[1], 0, 1) == '$') { $vf = substr($ThisSig[1], 1); if (isset($$vf) && !is_array($$vf)) { if ($$vf != $ThisSig[2]) { if ($ThisSig[3] <= $SigNum) { break; } $SigNum = $ThisSig[3] - 1; } continue; } if ($ThisSig[3] <= $SigNum) { break; } $SigNum = $ThisSig[3] - 1; } elseif (substr($ThisSig[1], 0, 2) == '!$') { $vf = substr($ThisSig[1], 2); if (isset($$vf) && !is_array($$vf)) { if ($$vf == $ThisSig[2]) { if ($ThisSig[3] <= $SigNum) { break; } $SigNum = $ThisSig[3] - 1; } continue; } if ($ThisSig[3] <= $SigNum) { break; } $SigNum = $ThisSig[3] - 1; } else { break; } continue; } if (strpos($ThisSig, ':') !== false) { $VN = $phpMussel['SplitSigParts']($ThisSig); if (!isset($VN[1]) || !strlen($VN[1])) { continue; } if ($ThisConf[3] === 2) { $ThisSig = preg_split('/[\x00-\x1f]+/', $VN[1], -1, PREG_SPLIT_NO_EMPTY); $ThisSig = ($ThisSig === false) ? '' : implode('', $ThisSig); $VN = $phpMussel['vn_shorthand']($VN[0]); if ( $ThisSig && strpos($phpMussel['InstanceCache']['greylist'], ',' . $VN . ',') === false && empty($phpMussel['InstanceCache']['ignoreme']) ) { if (preg_match('/(?:' . $ThisSig . ')/i', $ofn)) { $phpMussel['Detected']($heur, $lnap, $VN, $ofn, $ofnSafe, $Out, $flagged, $md5, $str_len); } } } elseif ($ThisConf[3] === 0 || $ThisConf[3] === 1) { $ThisSig = preg_split(( $ThisConf[3] === 0 ? '/[^\da-f>]+/i' : '/[\x00-\x1f]+/' ), $VN[1], -1, PREG_SPLIT_NO_EMPTY); $ThisSig = ($ThisSig === false ? '' : implode('', $ThisSig)); $ThisSigLen = strlen($ThisSig); if ($phpMussel['ConfineLength']($ThisSigLen)) { continue; } $xstrf = isset($VN[2]) ? $VN[2] : '*'; $xstrt = isset($VN[3]) ? $VN[3] : '*'; $VN = $phpMussel['vn_shorthand']($VN[0]); $VNLC = strtolower($VN); if (($is_not_php && ( strpos($VNLC, '-php') !== false || strpos($VNLC, '.php') !== false )) || ($is_not_html && ( strpos($VNLC, '-htm') !== false || strpos($VNLC, '.htm') !== false )) || $$DataSourceLen < $ThisSigLen) { continue; } if ( strpos($phpMussel['InstanceCache']['greylist'], ',' . $VN . ',') === false && empty($phpMussel['InstanceCache']['ignoreme']) ) { if ($ThisConf[3] === 0) { $ThisSig = strpos($ThisSig, '>') !== false ? explode('>', $ThisSig) : [$ThisSig]; $ThisSigCount = count($ThisSig); $ThisString = $$DataSource; $phpMussel['DataConfineByOffsets']($ThisString, $xstrf, $xstrt, $SectionOffsets); if ($xstrf === 'A') { $ThisString = "\x01" . $ThisString; $ThisSig[0] = "\x01" . $ThisSig[0]; } if ($xstrt === 'Z') { $ThisString .= "\x01"; $ThisSig[$ThisSigCount - 1] .= "\x01"; } for ($ThisSigi = 0; $ThisSigi < $ThisSigCount; $ThisSigi++) { if (strpos($ThisString, $ThisSig[$ThisSigi]) === false) { continue 2; } if ($ThisSigCount > 1 && strpos($ThisString, $ThisSig[$ThisSigi]) !== false) { $ThisString = $phpMussel['substraf']($ThisString, $ThisSig[$ThisSigi]); } } } else { $ThisString = $$DataSource; $phpMussel['DataConfineByOffsets']($ThisString, $xstrf, $xstrt, $SectionOffsets); if ($xstrf === 'A') { if ($xstrt === 'Z') { if (!preg_match('/\A(?:' . $ThisSig . ')$/i', $ThisString)) { continue; } } elseif (!preg_match('/\A(?:' . $ThisSig . ')/i', $ThisString)) { continue; } } else { if ($xstrt === 'Z') { if (!preg_match('/(?:' . $ThisSig . ')$/i', $ThisString)) { continue; } } elseif (!preg_match('/(?:' . $ThisSig . ')/i', $ThisString)) { continue; } } } $phpMussel['Detected']($heur, $lnap, $VN, $ofn, $ofnSafe, $Out, $flagged, $md5, $str_len); } } } } } } /** Plugin hook: "before_domains_api_lookup". */ $phpMussel['Execute_Hook']('before_domains_api_lookup'); /** Perform API lookups for domains. */ if (isset($URLScanner) && !$Out) { $URLScanner['DomainsCount'] = count($URLScanner['DomainParts']); /** Codeblock for performing hpHosts API lookups. */ if ($phpMussel['Config']['urlscanner']['lookup_hphosts'] && $URLScanner['DomainsCount']) { /** Fetch the cache entry for hpHosts, if it doesn't already exist. */ if (!isset($phpMussel['InstanceCache']['urlscanner_domains'])) { $phpMussel['InstanceCache']['urlscanner_domains'] = $phpMussel['FetchCache']('urlscanner_domains'); } $URLScanner['y'] = $phpMussel['Time'] + $phpMussel['Config']['urlscanner']['cache_time']; $URLScanner['ScriptIdentEncoded'] = urlencode($phpMussel['ScriptIdent']); $URLScanner['classes'] = [ 'EMD' => "\x1a\x82\x10\x1bXXX", 'EXP' => "\x1a\x82\x10\x16XXX", 'GRM' => "\x1a\x82\x10\x32XXX", 'HFS' => "\x1a\x82\x10\x32XXX", 'PHA' => "\x1a\x82\x10\x32XXX", 'PSH' => "\x1a\x82\x10\x31XXX" ]; for ($i = 0; $i < $URLScanner['DomainsCount']; $i++) { if (!empty($URLScanner['DomainPartsNoLookup'][$URLScanner['DomainParts'][$i]])) { continue; } if ( $phpMussel['Config']['urlscanner']['maximum_api_lookups'] > 0 && $phpMussel['LookupCount'] > $phpMussel['Config']['urlscanner']['maximum_api_lookups'] ) { if ($phpMussel['Config']['urlscanner']['maximum_api_lookups_response']) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), $phpMussel['L10N']->getString('too_many_urls') ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('too_many_urls') . ' (' . $ofnSafe . ')' ); } break; } $URLScanner['This'] = md5($URLScanner['DomainParts'][$i]) . ':' . strlen($URLScanner['DomainParts'][$i]) . ':'; while (substr_count($phpMussel['InstanceCache']['urlscanner_domains'], $URLScanner['This'])) { $URLScanner['Class'] = $phpMussel['substrbf']($phpMussel['substral']($phpMussel['InstanceCache']['urlscanner_domains'], $URLScanner['This']), ';'); if (!substr_count($phpMussel['InstanceCache']['urlscanner_domains'], $URLScanner['This'] . ':' . $URLScanner['Class'] . ';')) { break; } $URLScanner['Expiry'] = (int)$phpMussel['substrbf']($URLScanner['Class'], ':'); if ($URLScanner['Expiry'] > $phpMussel['Time']) { $URLScanner['Class'] = $phpMussel['substraf']($URLScanner['Class'], ':'); if (!$URLScanner['Class']) { continue 2; } $URLScanner['Class'] = $phpMussel['vn_shorthand']($URLScanner['Class']); $phpMussel['Detected']($heur, $lnap, $URLScanner['Class'], $ofn, $ofnSafe, $Out, $flagged, $md5, $str_len); } $phpMussel['InstanceCache']['urlscanner_domains'] = str_ireplace($URLScanner['This'] . $URLScanner['Class'] . ';', '', $phpMussel['InstanceCache']['urlscanner_domains']); } $URLScanner['req'] = 'v=' . $URLScanner['ScriptIdentEncoded'] . '&s=' . $URLScanner['DomainParts'][$i] . '&class=true'; $URLScanner['req_result'] = $phpMussel['Request']( 'http://verify.hosts-file.net/?' . $URLScanner['req'], ['v' => $URLScanner['ScriptIdentEncoded'], 's' => $URLScanner['DomainParts'][$i], 'Class' => true], 12 ); $phpMussel['LookupCount']++; if (substr($URLScanner['req_result'], 0, 6) == "Listed") { $URLScanner['Class'] = substr($URLScanner['req_result'], 7, 3); $URLScanner['Class'] = isset($URLScanner['classes'][$URLScanner['Class']]) ? $URLScanner['classes'][$URLScanner['Class']] : "\x1a\x82\x10\x3fXXX"; $phpMussel['InstanceCache']['urlscanner_domains'] .= $URLScanner['This'] . $URLScanner['y'] . ':' . $URLScanner['Class'] . ';'; $URLScanner['Class'] = $phpMussel['vn_shorthand']($URLScanner['Class']); $phpMussel['Detected']($heur, $lnap, $URLScanner['Class'], $ofn, $ofnSafe, $Out, $flagged, $md5, $str_len); } $phpMussel['InstanceCache']['urlscanner_domains'] .= $URLScanner['Domains'][$i] . $URLScanner['y'] . ':;'; } $phpMussel['SaveCache']('urlscanner_domains', $URLScanner['y'], $phpMussel['InstanceCache']['urlscanner_domains']); } $URLScanner['URLsCount'] = count($URLScanner['URLParts']); /** Codeblock for performing Google Safe Browsing API lookups. */ if ($phpMussel['Config']['urlscanner']['google_api_key'] && $URLScanner['URLsCount']) { $URLScanner['URLsChunked'] = ( $URLScanner['URLsCount'] > 500 ) ? array_chunk($URLScanner['URLParts'], 500) : [$URLScanner['URLParts']]; $URLScanner['URLChunks'] = count($URLScanner['URLsChunked']); for ($i = 0; $i < $URLScanner['URLChunks']; $i++) { if ( $phpMussel['Config']['urlscanner']['maximum_api_lookups'] > 0 && $phpMussel['LookupCount'] > $phpMussel['Config']['urlscanner']['maximum_api_lookups'] ) { if ($phpMussel['Config']['urlscanner']['maximum_api_lookups_response']) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), $phpMussel['L10N']->getString('too_many_urls') ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('too_many_urls') . ' (' . $ofnSafe . ')' ); } break; } try { $URLScanner['SafeBrowseLookup'] = $phpMussel['SafeBrowseLookup']( $URLScanner['URLsChunked'][$i], $URLScanner['URLPartsNoLookup'], $URLScanner['DomainPartsNoLookup'] ); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } if ($URLScanner['SafeBrowseLookup'] !== 204) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $URLScanner['L10N'] = $phpMussel['L10N']->getString( 'SafeBrowseLookup_' . $URLScanner['SafeBrowseLookup'] ) ?: $phpMussel['L10N']->getString('SafeBrowseLookup_999'); $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), $URLScanner['L10N'] ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $URLScanner['L10N'] . ' (' . $ofnSafe . ')' ); } } } } /** URL scanner data cleanup. */ unset($URLScanner); /** Plugin hook: "before_chameleon_detections". */ $phpMussel['Execute_Hook']('before_chameleon_detections'); /** PHP chameleon attack detection. */ if ($phpMussel['Config']['attack_specific']['chameleon_from_php']) { if ($phpMussel['ContainsMustAssert']([ $phpMussel['Config']['attack_specific']['can_contain_php_file_extensions'], $phpMussel['Config']['attack_specific']['archive_file_extensions'] ], [$xts, $gzxts, $xt, $gzxt]) && strpos($str_hex_norm, '3c3f706870') !== false) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $heur['detections']++; $phpMussel['InstanceCache']['detections_count']++; $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), sprintf($phpMussel['L10N']->getString('scan_chameleon'), 'PHP') ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), sprintf($phpMussel['L10N']->getString('scan_chameleon'), 'PHP') . ' (' . $ofnSafe . ')' ); } } /** Executable chameleon attack detection. */ if ($phpMussel['Config']['attack_specific']['chameleon_from_exe']) { $Chameleon = ''; if (strpos(',acm,ax,com,cpl,dll,drv,exe,ocx,rs,scr,sys,', ',' . $xt . ',') !== false) { if ($twocc !== '4d5a') { $Chameleon = 'EXE'; } } elseif ($twocc === '4d5a') { $Chameleon = 'EXE'; } if ($xt === 'elf') { if ($fourcc !== '7f454c46') { $Chameleon = 'ELF'; } } elseif ($fourcc === '7f454c46') { $Chameleon = 'ELF'; } if ($xt === 'lnk') { if (substr($str_hex, 0, 16) !== '4c00000001140200') { $Chameleon = 'LNK'; } } elseif (substr($str_hex, 0, 16) === '4c00000001140200') { $Chameleon = 'LNK'; } if ($xt === 'msi' && substr($str_hex, 0, 16) !== 'd0cf11e0a1b11ae1') { $Chameleon = 'MSI'; } if ($Chameleon) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $heur['detections']++; $phpMussel['InstanceCache']['detections_count']++; $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), sprintf($phpMussel['L10N']->getString('scan_chameleon'), $Chameleon) ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), sprintf($phpMussel['L10N']->getString('scan_chameleon'), $Chameleon) . ' (' . $ofnSafe . ')' ); } } /** Archive chameleon attack detection. */ if ($phpMussel['Config']['attack_specific']['chameleon_to_archive']) { $Chameleon = ''; if ($xts === 'zip*' && $twocc !== '504b') { $Chameleon = 'Zip'; } elseif ($xt === 'rar' && ($fourcc !== '52617221' && $fourcc !== '52457e5e')) { $Chameleon = 'Rar'; } elseif ($xt === 'gz' && $twocc !== '1f8b') { $Chameleon = 'Gzip'; } elseif ($xt === 'bz2' && substr($str_hex, 0, 6) !== '425a68') { $Chameleon = 'Bzip2'; } if ($Chameleon) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $heur['detections']++; $phpMussel['InstanceCache']['detections_count']++; $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), sprintf($phpMussel['L10N']->getString('scan_chameleon'), $Chameleon) ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), sprintf($phpMussel['L10N']->getString('scan_chameleon'), $Chameleon) . ' (' . $ofnSafe . ')' ); } } /** Office document chameleon attack detection. */ if ($phpMussel['Config']['attack_specific']['chameleon_to_doc']) { if (strpos(',doc,dot,pps,ppt,xla,xls,wiz,', ',' . $xt . ',') !== false) { if ($fourcc !== 'd0cf11e0') { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $heur['detections']++; $phpMussel['InstanceCache']['detections_count']++; $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), sprintf($phpMussel['L10N']->getString('scan_chameleon'), 'Office') ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), sprintf($phpMussel['L10N']->getString('scan_chameleon'), 'Office') . ' (' . $ofnSafe . ')' ); } } } /** Image chameleon attack detection. */ if ($phpMussel['Config']['attack_specific']['chameleon_to_img']) { $Chameleon = ''; if ( (($xt === 'bmp' || $xt === 'dib') && $twocc !== '424d') || ($xt === 'gif' && (substr($str_hex, 0, 12) !== '474946383761' && substr($str_hex, 0, 12) !== '474946383961')) || (preg_match('~j(?:fif?|if|peg?|pg)~', $xt) && substr($str_hex, 0, 6) !== 'ffd8ff') || ($xt === 'jp2' && substr($str_hex, 0, 16) !== '0000000c6a502020') || (($xt === 'pdd' || $xt === 'psd') && $fourcc !== '38425053') || ($xt === 'png' && $fourcc !== '89504e47') || ($xt === 'webp' && ($fourcc !== '52494646' || substr($str, 8, 4) !== 'WEBP')) || ($xt === 'xcf' && substr($str, 0, 8) !== 'gimp xcf') ) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $heur['detections']++; $phpMussel['InstanceCache']['detections_count']++; $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), sprintf($phpMussel['L10N']->getString('scan_chameleon'), $phpMussel['L10N']->getString('image')) ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), sprintf($phpMussel['L10N']->getString('scan_chameleon'), $phpMussel['L10N']->getString('image')) . ' (' . $ofnSafe . ')' ); } } /** PDF chameleon attack detection. */ if ($phpMussel['Config']['attack_specific']['chameleon_to_pdf']) { if ($xt === 'pdf' && !$pdf_magic) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $heur['detections']++; $phpMussel['InstanceCache']['detections_count']++; $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), sprintf($phpMussel['L10N']->getString('scan_chameleon'), 'PDF') ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), sprintf($phpMussel['L10N']->getString('scan_chameleon'), 'PDF') . ' (' . $ofnSafe . ')' ); } } /** Control character detection. */ if ($phpMussel['Config']['attack_specific']['block_control_characters']) { if (preg_match('/[\x00-\x08\x0b\x0c\x0e\x1f\x7f]/i', $str)) { $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('detected_control_characters') ) . "\n"; $heur['detections']++; $phpMussel['InstanceCache']['detections_count']++; if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('detected_control_characters') . ' (' . $ofnSafe . ')' ); } } /** * If the heuristic weight of the current scan iteration exceeds the * heuristic threshold defined by the configuration, or if outs has already * been filled, dump all heuristic detections and non-heuristic detections * together into outs and regard the iteration as flagged. */ if ( $heur['weight'] >= $phpMussel['Config']['heuristic']['threshold'] || $Out ) { $Out .= $heur['cli']; $phpMussel['whyflagged'] .= $heur['web']; } /** Plugin hook: "before_vt". */ $phpMussel['Execute_Hook']('before_vt'); /** Virus Total API integration. */ if ( !$Out && !empty($phpMussel['Config']['virustotal']['vt_public_api_key']) ) { $DoScan = false; $phpMussel['Config']['virustotal']['vt_suspicion_level'] = (int)$phpMussel['Config']['virustotal']['vt_suspicion_level']; if ($phpMussel['Config']['virustotal']['vt_suspicion_level'] === 0) { $DoScan = ($heur['weight'] > 0); } elseif ($phpMussel['Config']['virustotal']['vt_suspicion_level'] === 1) { $DoScan = ( $heur['weight'] > 0 || $is_pe || $fileswitch === 'chrome' || $fileswitch === 'java' || $fileswitch === 'docfile' || $fileswitch === 'vt_interest' ); } elseif ($phpMussel['Config']['virustotal']['vt_suspicion_level'] === 2) { $DoScan = true; } if ($DoScan) { $VTWeight = ['weight' => 0, 'cli' => '', 'web' => '']; if (!isset($phpMussel['InstanceCache']['vt_quota'])) { $phpMussel['InstanceCache']['vt_quota'] = $phpMussel['FetchCache']('vt_quota'); } $x = 0; if (!empty($phpMussel['InstanceCache']['vt_quota'])) { $phpMussel['InstanceCache']['vt_quota'] = explode(';', $phpMussel['InstanceCache']['vt_quota']); foreach ($phpMussel['InstanceCache']['vt_quota'] as &$phpMussel['ThisQuota']) { if ($phpMussel['ThisQuota'] > $phpMussel['Time']) { $x++; } else { $phpMussel['ThisQuota'] = ''; } } unset($phpMussel['ThisQuota']); $phpMussel['InstanceCache']['vt_quota'] = implode(';', $phpMussel['InstanceCache']['vt_quota']); } if ($x < $phpMussel['Config']['virustotal']['vt_quota_rate']) { $VTParams = [ 'apikey' => $phpMussel['Config']['virustotal']['vt_public_api_key'], 'resource' => $md5 ]; $VTRequest = $phpMussel['Request']( 'http://www.virustotal.com/vtapi/v2/file/report?apikey=' . urlencode($phpMussel['Config']['virustotal']['vt_public_api_key']) . '&resource=' . $md5, $VTParams, 12); $VTJSON = json_decode($VTRequest, true); $y = $phpMussel['Time'] + ($phpMussel['Config']['virustotal']['vt_quota_time'] * 60); $phpMussel['InstanceCache']['vt_quota'] .= $y . ';'; while (substr_count($phpMussel['InstanceCache']['vt_quota'], ';;')) { $phpMussel['InstanceCache']['vt_quota'] = str_ireplace(';;', ';', $phpMussel['InstanceCache']['vt_quota']); } $phpMussel['SaveCache']('vt_quota', $y + 60, $phpMussel['InstanceCache']['vt_quota']); if (isset($VTJSON['response_code'])) { $VTJSON['response_code'] = (int)$VTJSON['response_code']; if ( isset($VTJSON['scans']) && $VTJSON['response_code'] === 1 && is_array($VTJSON['scans']) ) { foreach ($VTJSON['scans'] as $VTKey => $VTValue) { if ($VTValue['detected'] && $VTValue['result']) { $VN = $VTKey . '(VirusTotal)-' . $VTValue['result']; if ( strpos($phpMussel['InstanceCache']['greylist'], ',' . $VN . ',') === false && empty($phpMussel['InstanceCache']['ignoreme']) ) { if (!$flagged) { $phpMussel['killdata'] .= $md5 . ':' . $str_len . ':' . $ofn . "\n"; $flagged = true; } $heur['detections']++; $phpMussel['InstanceCache']['detections_count']++; if ($phpMussel['Config']['virustotal']['vt_weighting'] > 0) { $VTWeight['weight']++; $VTWeight['web'] .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation'), sprintf($phpMussel['L10N']->getString('detected'), $VN) ) . "\n"; $VTWeight['cli'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), sprintf($phpMussel['L10N']->getString('detected'), $VN) . ' (' . $ofnSafe . ')' ); } else { $Out .= $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), sprintf($phpMussel['L10N']->getString('detected'), $VN) ) . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), sprintf($phpMussel['L10N']->getString('detected'), $VN) . ' (' . $ofnSafe . ')' ); } } } } } } if ( $VTWeight['weight'] > 0 && $VTWeight['weight'] >= $phpMussel['Config']['virustotal']['vt_weighting'] ) { $Out .= $VTWeight['web']; $phpMussel['whyflagged'] .= $VTWeight['cli']; } } } } /** Plugin hook: "after_vt". */ $phpMussel['Execute_Hook']('after_vt'); if ( isset($phpMussel['HashCacheData']) && !isset($phpMussel['HashCache']['Data'][$phpMussel['HashCacheData']]) && $phpMussel['Config']['general']['scan_cache_expiry'] > 0 ) { if (empty($phpMussel['HashCache']['Data']) || !is_array($phpMussel['HashCache']['Data'])) { $phpMussel['HashCache']['Data'] = []; } $phpMussel['HashCache']['Data'][$phpMussel['HashCacheData']] = [ $phpMussel['HashCacheData'], $phpMussel['Time'] + $phpMussel['Config']['general']['scan_cache_expiry'], (empty($Out) ? '' : bin2hex($Out)), (empty($phpMussel['whyflagged']) ? '' : bin2hex($phpMussel['whyflagged'])) ]; } /** Set final debug values, if this has been enabled. */ if (isset($phpMussel['DebugArr'], $phpMussel['DebugArrKey'])) { $phpMussel['DebugArr'][$phpMussel['DebugArrKey']]['Results'] = !$Out ? 1 : 2; $phpMussel['DebugArr'][$phpMussel['DebugArrKey']]['Output'] = $Out; } if ($Out) { /** Register object flagged. */ if (isset($phpMussel['cli_args'][1]) && $phpMussel['cli_args'][1] == 'cli_scan') { $phpMussel['Stats-Increment']('CLI-Flagged', 1); } else { $phpMussel['Stats-Increment']($phpMussel['EOF'] ? 'API-Flagged' : 'Web-Blocked', 1); } } /** Exit data handler. */ return !$Out ? [1, ''] : [2, $Out]; }; /** * Splits a signature into its constituent parts (name, pattern, etc). * * @param string $Sig The signature. * @param int $Max The maximum number of parts to return (optional). * @return array The parts. */ $phpMussel['SplitSigParts'] = function ($Sig, $Max = -1) { return preg_split('~(?<!\?|\<)\:~', $Sig, $Max, PREG_SPLIT_NO_EMPTY); }; /** * Handles scanning for files contained within archives. * * @param string $x Scan results inherited from parent in the form of a string. * @param int $r Scan results inherited from parent in the form of an integer. * @param string $Indent Line padding for the scan results. * @param string $ItemRef A reference to the path and original filename of the * item being scanned in relation to its container and/or its hierarchy * within the scan process. * @param string $Filename The original filename of the item being scanned. * @param string $Data The data to be scanned. * @param int $Depth The depth of the item being scanned in relation to its * container and/or its hierarchy within the scan process. * @param string $MD5 A hash for the content, inherited from the parent. */ $phpMussel['MetaDataScan'] = function (&$x, &$r, $Indent, $ItemRef, $Filename, &$Data, $Depth, $MD5) use (&$phpMussel) { /** Plugin hook: "MetaDataScan_start". */ $phpMussel['Execute_Hook']('MetaDataScan_start'); /** Data is empty. Nothing to scan. Exit early. */ if (!$Filesize = strlen($Data)) { return; } /** Filesize thresholds. */ if ( $phpMussel['Config']['files']['filesize_archives'] && $phpMussel['Config']['files']['filesize_limit'] > 0 && $Filesize > $phpMussel['ReadBytes']($phpMussel['Config']['files']['filesize_limit']) ) { if (!$phpMussel['Config']['files']['filesize_response']) { $x .= $Indent . $phpMussel['L10N']->getString('ok') . ' (' . $phpMussel['L10N']->getString('filesize_limit_exceeded') . ").\n"; return; } $r = 2; $phpMussel['killdata'] .= $MD5 . ':' . $Filesize . ':' . $ItemRef . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('filesize_limit_exceeded') . ' (' . $ItemRef . ')' ); $x .= $Indent . $phpMussel['L10N']->getString('filesize_limit_exceeded') . $phpMussel['L10N']->getString('_fullstop_final') . "\n"; return; } /** Process filetype blacklisting, whitelisting, and greylisting. */ if ($phpMussel['Config']['files']['filetype_archives']) { list($xt, $xts, $gzxt, $gzxts) = $phpMussel['FetchExt']($Filename); if ($phpMussel['ContainsMustAssert']([ $phpMussel['Config']['files']['filetype_whitelist'] ], [$xt, $xts], ',', true, true)) { $x .= $Indent . $phpMussel['L10N']->getString('scan_no_problems_found') . "\n"; return; } if ($phpMussel['ContainsMustAssert']([ $phpMussel['Config']['files']['filetype_blacklist'] ], [$xt, $xts], ',', true, true)) { $r = 2; $phpMussel['killdata'] .= $MD5 . ':' . $Filesize . ':' . $ItemRef . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('filetype_blacklisted') . ' (' . $ItemRef . ')' ); $x .= $Indent . $phpMussel['L10N']->getString('filetype_blacklisted') . $phpMussel['L10N']->getString('_fullstop_final') . "\n"; return; } if (!empty($phpMussel['Config']['files']['filetype_greylist']) && $phpMussel['ContainsMustAssert']([ $phpMussel['Config']['files']['filetype_greylist'] ], [$xt, $xts])) { $r = 2; $phpMussel['killdata'] .= $MD5 . ':' . $Filesize . ':' . $ItemRef . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('filetype_blacklisted') . ' (' . $ItemRef . ')' ); $x .= $Indent . $phpMussel['L10N']->getString('filetype_blacklisted') . $phpMussel['L10N']->getString('_fullstop_final') . "\n"; return; } } /** Determine whether the file being scanned is a macro. */ $phpMussel['InstanceCache']['file_is_macro'] = ( preg_match('~vbaProject\.bin$~i', $Filename) || preg_match('~^\xd0\xcf|\x00Attribut|\x01CompObj|\x05Document~', $Data) ); /** Handle macro detection and blocking. */ if ($phpMussel['Config']['attack_specific']['block_macros'] && $phpMussel['InstanceCache']['file_is_macro']) { $r = 2; $phpMussel['killdata'] .= $MD5 . ':' . $Filesize . ':' . $ItemRef . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('macros_not_permitted') . ' (' . $ItemRef . ')' ); $x .= $Indent . $phpMussel['L10N']->getString('macros_not_permitted') . $phpMussel['L10N']->getString('_fullstop_final') . "\n"; return; } /** Increment objects scanned count. */ $phpMussel['InstanceCache']['objects_scanned']++; /** Send the scan target to the data handler. */ try { $Scan = $phpMussel['DataHandler']($Data, $Depth, $Filename); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } /** * Check whether the file is compressed. If it's compressed, attempt to * decompress it, and then scan the decompressed version of the file. We'll * only bother doing this if the file hasn't already been flagged though. */ if ($Scan[0] === 1) { /** Create a new compression object. */ $CompressionObject = new \phpMussel\CompressionHandler\CompressionHandler($Data); /** Now we'll try to decompress the file. */ if (!$CompressionResults = $CompressionObject->TryEverything()) { /** Success! Now we'll send it to the data handler. */ try { $Scan = $phpMussel['DataHandler']($CompressionObject->Data, $Depth, $phpMussel['DropTrailingCompressionExtension']($Filename)); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } /** * Replace originally scanned data with decompressed data in case * needed by the archive handler. */ $Data = $CompressionObject->Data; } /** Cleanup. */ unset($CompressionResults, $CompressionObject); } /** Destroy item-specific metadata set by the archive handler instance. */ unset($phpMussel['CrxPubKey'], $phpMussel['CrxSig']); /** Update the results if anything bad was found and then exit. */ if ($Scan[0] !== 1) { $r = $Scan[0]; $x .= '-' . $Scan[1]; return; } /** Or, if nothing bad was found for this entry, make a note of it. */ $x .= $Indent . $phpMussel['L10N']->getString('scan_no_problems_found') . "\n"; }; /** * Looks for indicators of image files (i.e., attempts to determine whether a * file is an image file). * * @param string $Ext The file extension. * @param string $Head The file header. * @return bool True: Indicators found. False: Indicators not found. */ $phpMussel['Indicator-Image'] = function ($Ext, $Head) { return ( preg_match( '/^(?:bm[2p]|c(d5|gm)|d(ib|w[fg]|xf)|ecw|fits|gif|img|j(f?if?|p[2s]|pe?g?2?|xr)|p(bm|cx|dd|gm|ic|n[gms]|' . 'pm|s[dp])|s(id|v[ag])|tga|w(bmp?|ebp|mp)|x(cf|bmp))$/' , $Ext) || preg_match( '/^(?:0000000c6a502020|25504446|38425053|424d|474946383[79]61|57454250|67696d7020786366|89504e47|ffd8ff)/' , $Head) ); }; /** * Fetches extensions data from filenames. * * @param string $ofn The original filename. * @return array The extensions data. */ $phpMussel['FetchExt'] = function ($ofn) { $decPos = strrpos($ofn, '.'); $ofnLen = strlen($ofn); if ($decPos === false || $decPos === ($ofnLen - 1)) { return ['-', '-', '-', '-']; } $xt = strtolower(substr($ofn, ($decPos + 1))); $xts = substr($xt, 0, 3) . '*'; if (strtolower(substr($ofn, -3)) === '.gz') { $ofnNoGZ = substr($ofn, 0, ($ofnLen - 3)); $decPosNoGZ = strrpos($ofnNoGZ, '.'); if ($decPosNoGZ !== false && $decPosNoGZ !== (strlen($ofnNoGZ) - 1)) { $gzxt = strtolower(substr($ofnNoGZ, ($decPosNoGZ + 1))); $gzxts = substr($gzxt, 0, 3) . '*'; } } else { $gzxts = $gzxt = '-'; } return [$xt, $xts, $gzxt, $gzxts]; }; /** * Remove occurrence of $A from leading substring of $B. */ $phpMussel['RemoveLeadMatch'] = function ($A, $B) { $LenA = strlen($A); $LenB = strlen($B); for ($Iter = 0; $Iter < $LenA && $Iter < $LenB; $Iter++) { $CharA = substr($A, $Iter, 1); $CharB = substr($B, $Iter, 1); if ($CharA !== $CharB) { break; } } return ($Iter === $LenB) ? '' : substr($B, $Iter); }; /** * Get substring of string after final slash. */ $phpMussel['SubstrAfterFinalSlash'] = function ($String) { return strpos($String, '/') !== false ? substr($String, strrpos($String, '/') + 1) : ( strpos($String, "\\") !== false ? substr($String, strrpos($String, "\\") + 1) : $String ); }; /** * Responsible for recursing through any files given to it to be scanned, which * may be necessary for the case of archives and directories. It performs the * preparations necessary for scanning files using the "data handler" and the * "meta data scan" closures. Additionally, it performs some necessary * whitelist, blacklist and greylist checks, filesize and file extension * checks, and handles the processing and extraction of files from archives, * fetching the files contained in archives being scanned in order to process * those contained files as so that they, too, may be scanned. * * When phpMussel is instructed to scan a directory or an array of multiple * files, the recursor is the closure function responsible for iterating * through that directory and/or array queued for scanning, and if necessary, * will recurse itself (such as for when scanning a directory containing * sub-directories or when scanning a multidimensional array of multiple files * and/or directories). * * @param string|array $f In the context of the initial file upload scanning * that phpMussel performs when operating via a server, this parameter (a * string) represents the "temporary filename" of the file being scanned * (the temporary filename, in this context, referring to the name * temporarily assigned to the file by the server upon the file being * uploaded to the temporary uploads location assigned to the server). * When operating in the context of CLI mode, both $f and $ofn represent * the scan target, as per specified by the CLI operator; The only * difference between the two is when the scan target is a directory, * rather than a single file; $f will represent the full path to the file * (so, directory plus filename), whereas $ofn will represent only the * filename. This parameter can also accept an array of filenames. * @param bool $n This optional parameter is a boolean (defaults to false, but * set to true during the initial scan of file uploads), indicating the * format for returning the scan results. False instructs the function to * return results as an integer; True instructs the function to return * results as human readable text (refer to Section 3A of the README * documentation, "HOW TO USE (FOR WEB SERVERS)", for more information). * @param bool $zz This optional parameter is a boolean (defaults to false, but * set to true during the initial scan of file uploads), indicating to the * function whether or not arrayed results should be imploded prior to * being returned to the calling function. False instructs the function to * return the arrayed results as verbatim; True instructs the function to * return the arrayed results as an imploded string. * @param int $dpt Represents the current depth of recursion from which the * function has been called. This information is used for determining how * far to indent any entries generated for logging and for the display of * scan results in CLI (you should never manually set this parameter * yourself). * @param string $ofn For the file upload scanning that phpMussel normally * performs by default, this parameter represents the "original filename" * of the file being scanned (the original filename, in this context, * referring to the name supplied by the upload client, as opposed to the * temporary filename assigned by the server or anything else). * When operating in the context of CLI mode, both $f and $ofn represent * the scan target, as per specified by the CLI operator; The only * difference between the two is when the scan target is a directory, * rather than a single file; $f will represent the full path to the file * (so, directory plus filename), whereas $ofn will represent only the * filename. * @return int|string|array The scan results, returned as an array when the $f * parameter is an array and when $n and/or $zz is/are false, and * otherwise returned as per described by the README documentation. The * function may also die the script and return nothing, if something goes * wrong, such as if the function is triggered in the absence of the * required $phpMussel['InstanceCache'] variable being set. */ $phpMussel['Recursor'] = function ($f = '', $n = false, $zz = false, $dpt = 0, $ofn = '') use (&$phpMussel) { if (!isset($phpMussel['InstanceCache'])) { throw new \Exception($phpMussel['L10N']->getString( 'required_variables_not_defined' ) ?: '[phpMussel] Required variables aren\'t defined: Can\'t continue.'); } /** Plugin hook: "Recursor_start". */ $phpMussel['Execute_Hook']('Recursor_start'); /** Prepare signature files for the scan process. */ if (empty($phpMussel['InstanceCache']['OrganisedSigFiles'])) { $phpMussel['OrganiseSigFiles'](); $phpMussel['InstanceCache']['OrganisedSigFiles'] = true; } if ($phpMussel['EOF']) { $phpMussel['whyflagged'] = $phpMussel['killdata'] = $phpMussel['PEData'] = ''; if ($dpt === 0 || !isset( $phpMussel['InstanceCache']['objects_scanned'], $phpMussel['InstanceCache']['detections_count'], $phpMussel['InstanceCache']['scan_errors'] )) { $phpMussel['InstanceCache']['objects_scanned'] = 0; $phpMussel['InstanceCache']['detections_count'] = 0; $phpMussel['InstanceCache']['scan_errors'] = 0; } } else { if (!isset($phpMussel['killdata'])) { $phpMussel['killdata'] = ''; } if (!isset($phpMussel['whyflagged'])) { $phpMussel['whyflagged'] = ''; } if (!isset($phpMussel['PEData'])) { $phpMussel['PEData'] = ''; } if (!isset( $phpMussel['InstanceCache']['objects_scanned'], $phpMussel['InstanceCache']['detections_count'], $phpMussel['InstanceCache']['scan_errors'] )) { $phpMussel['InstanceCache']['objects_scanned'] = 0; $phpMussel['InstanceCache']['detections_count'] = 0; $phpMussel['InstanceCache']['scan_errors'] = 0; } } /** Increment scan depth. */ $dpt++; /** Controls indenting relating to scan depth for normal logging and for CLI-mode scanning. */ $lnap = str_pad('> ', ($dpt + 1), '-', STR_PAD_LEFT); /** * If the scan target is an array, iterate through the array and recurse * the recursor with each array element. */ if (is_array($f)) { foreach ($f as &$Current) { try { $Current = $phpMussel['Recursor']($Current, $n, false, $dpt, $Current); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } } return ($n && $zz) ? $phpMussel['implode_md']($f) : $f; } $ofn = $phpMussel['prescan_decode']($ofn); $ofnSafe = urlencode($ofn); /** * If the scan target is a directory, iterate through the directory * contents and recurse the recursor with these contents. */ if (is_dir($f)) { if (!is_readable($f)) { $phpMussel['InstanceCache']['scan_errors']++; return !$n ? 0 : $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), sprintf($phpMussel['L10N']->getString('failed_to_access'), $ofn) ) . "\n"; } $Dir = $phpMussel['DirectoryRecursiveList']($f); foreach ($Dir as &$Sub) { try { $Sub = $phpMussel['Recursor']($f . '/' . $Sub, $n, false, $dpt, $Sub); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } } return ($n && $zz) ? $phpMussel['implode_md']($Dir) : $Dir; } /** Define file phase. */ $phpMussel['InstanceCache']['phase'] = 'file'; /** Indicates whether the scan target is a part of a container. */ $phpMussel['InstanceCache']['container'] = 'none'; /** Indicates whether the scan target is an OLE object. */ $phpMussel['InstanceCache']['file_is_ole'] = false; /** Fetch the greylist if it hasn't already been fetched. */ if (!isset($phpMussel['InstanceCache']['greylist'])) { if (!file_exists($phpMussel['Vault'] . 'greylist.csv')) { $phpMussel['InstanceCache']['greylist'] = ','; $Handle = fopen($phpMussel['Vault'] . 'greylist.csv', 'a'); fwrite($Handle, ','); fclose($Handle); } else { $phpMussel['InstanceCache']['greylist'] = $phpMussel['ReadFile']($phpMussel['Vault'] . 'greylist.csv'); } } /** Plugin hook: "before_scan". */ $phpMussel['Execute_Hook']('before_scan'); $fnCRC = hash('crc32b', $ofn); /** Kill it here if the scan target isn't a valid file. */ if (!$f || !$d = is_file($f)) { return (!$n) ? 0 : $lnap . $phpMussel['L10N']->getString('scan_checking') . ' \'' . $ofn . '\' (FN: ' . $fnCRC . "):\n-" . $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), $phpMussel['L10N']->getString('invalid_file') ) . "\n"; } $fS = filesize($f); if ($phpMussel['Config']['files']['filesize_limit'] > 0) { if ($fS > $phpMussel['ReadBytes']($phpMussel['Config']['files']['filesize_limit'])) { if (!$phpMussel['Config']['files']['filesize_response']) { return (!$n) ? 1 : $lnap . $phpMussel['L10N']->getString('scan_checking') . ' \'' . $ofn . '\' (FN: ' . $fnCRC . "):\n-" . $lnap . $phpMussel['L10N']->getString('ok') . ' (' . $phpMussel['L10N']->getString('filesize_limit_exceeded') . ").\n"; } $phpMussel['killdata'] .= '--FILESIZE-LIMIT--------NO-HASH-:' . $fS . ':' . $ofn . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('filesize_limit_exceeded') . ' (' . $ofnSafe . ')' ); if ($phpMussel['Config']['general']['delete_on_sight'] && is_readable($f)) { unlink($f); } return (!$n) ? 2 : $lnap . $phpMussel['L10N']->getString('scan_checking') . ' \'' . $ofn . '\' (FN: ' . $fnCRC . "):\n-" . $lnap . $phpMussel['L10N']->getString('filesize_limit_exceeded') . $phpMussel['L10N']->getString('_fullstop_final') . "\n"; } } if (!$phpMussel['Config']['attack_specific']['allow_leading_trailing_dots'] && ( substr($ofn, 0, 1) === '.' || substr($ofn, -1) === '.' )) { $phpMussel['killdata'] .= '--FILENAME-MANIPULATION-NO-HASH-:' . $fS . ':' . $ofn . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('scan_filename_manipulation_detected') . ' (' . $ofnSafe . ')' ); if ($phpMussel['Config']['general']['delete_on_sight'] && is_readable($f)) { unlink($f); } return (!$n) ? 2 : $lnap . $phpMussel['L10N']->getString('scan_checking') . ' \'' . $ofn . '\' (FN: ' . $fnCRC . "):\n-" . $lnap . sprintf( $phpMussel['L10N']->getString('_exclamation_final'), $phpMussel['L10N']->getString('scan_filename_manipulation_detected') ) . "\n"; } /** Get file extensions. */ list($xt, $xts, $gzxt, $gzxts) = $phpMussel['FetchExt']($ofn); /** Process filetype whitelisting. */ if ($phpMussel['ContainsMustAssert']([ $phpMussel['Config']['files']['filetype_whitelist'] ], [$xt, $xts, $gzxt, $gzxts], ',', true, true)) { return (!$n) ? 1 : $lnap . $phpMussel['L10N']->getString('scan_checking') . ' \'' . $ofn . '\' (FN: ' . $fnCRC . "):\n-" . $lnap . $phpMussel['L10N']->getString('scan_no_problems_found') . "\n"; } /** Process filetype blacklisting. */ if ($phpMussel['ContainsMustAssert']([ $phpMussel['Config']['files']['filetype_blacklist'] ], [$xt, $xts, $gzxt, $gzxts], ',', true, true)) { $phpMussel['killdata'] .= '--FILETYPE-BLACKLISTED--NO-HASH-:' . $fS . ':' . $ofn . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('filetype_blacklisted') . ' (' . $ofnSafe . ')' ); if ($phpMussel['Config']['general']['delete_on_sight'] && is_readable($f)) { unlink($f); } return (!$n) ? 2 : $lnap . $phpMussel['L10N']->getString('scan_checking') . ' \'' . $ofn . '\' (FN: ' . $fnCRC . "):\n-" . $lnap . $phpMussel['L10N']->getString('filetype_blacklisted') . $phpMussel['L10N']->getString('_fullstop_final') . "\n"; } /** Process filetype greylisting (when relevant). */ if (!empty($phpMussel['Config']['files']['filetype_greylist']) && $phpMussel['ContainsMustAssert']([ $phpMussel['Config']['files']['filetype_greylist'] ], [$xt, $xts, $gzxt, $gzxts])) { $phpMussel['killdata'] .= '----FILETYPE--NOT-GREYLISTED----:' . $fS . ':' . $ofn . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('filetype_blacklisted') . ' (' . $ofnSafe . ')' ); if ($phpMussel['Config']['general']['delete_on_sight'] && is_readable($f)) { unlink($f); } return (!$n) ? 2 : $lnap . $phpMussel['L10N']->getString('scan_checking') . ' \'' . $ofn . '\' (FN: ' . $fnCRC . "):\n-" . $lnap . $phpMussel['L10N']->getString('filetype_blacklisted') . $phpMussel['L10N']->getString('_fullstop_final') . "\n"; } /** Read in the file to be scanned. */ $in = $phpMussel['ReadFile']($f, ( $phpMussel['Config']['attack_specific']['scannable_threshold'] > 0 && $fS > $phpMussel['ReadBytes']($phpMussel['Config']['attack_specific']['scannable_threshold']) ) ? $phpMussel['ReadBytes']($phpMussel['Config']['attack_specific']['scannable_threshold']) : $fS, true); /** Generate CRC for the file to be scanned. */ $fdCRC = hash('crc32b', $in); /** Check for non-image items. */ if (!empty($in) && $phpMussel['Config']['compatibility']['only_allow_images'] && !$phpMussel['Indicator-Image']($xt, bin2hex(substr($in, 0, 16)))) { $phpMussel['killdata'] .= md5($in) . ':' . $fS . ':' . $ofn . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('only_allow_images') . ' (' . $ofnSafe . ')' ); if ($phpMussel['Config']['general']['delete_on_sight'] && is_readable($f)) { unlink($f); } return (!$n) ? 2 : $lnap . $phpMussel['L10N']->getString('scan_checking') . ' \'' . $ofn . '\' (FN: ' . $fnCRC . '; FD: ' . $fdCRC . "):\n-" . $lnap . $phpMussel['L10N']->getString('only_allow_images') . $phpMussel['L10N']->getString('_fullstop_final') . "\n"; } /** Increment objects scanned count. */ $phpMussel['InstanceCache']['objects_scanned']++; /** Send the scan target to the data handler. */ try { $z = $phpMussel['DataHandler']($in, $dpt, $ofn); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } /** * Check whether the file is compressed. If it's compressed, attempt to * decompress it, and then scan the decompressed version of the file. We'll * only bother doing this if the file hasn't already been flagged though. */ if ($z[0] === 1) { /** Create a new compression object. */ $CompressionObject = new \phpMussel\CompressionHandler\CompressionHandler($in); /** Now we'll try to decompress the file. */ if (!$CompressionResults = $CompressionObject->TryEverything()) { /** Success! Now we'll send it to the data handler. */ try { $z = $phpMussel['DataHandler']($CompressionObject->Data, $dpt, $phpMussel['DropTrailingCompressionExtension']($ofn)); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } /** * Replace originally scanned data with decompressed data in case * needed by the archive handler. */ $in = $CompressionObject->Data; } /** Cleanup. */ unset($CompressionResults, $CompressionObject); } /** Executed if there were any problems or if anything was detected. */ if ($z[0] !== 1) { /** Quarantine if necessary. */ if ($z[0] === 2) { if ( $phpMussel['Config']['general']['quarantine_key'] && !$phpMussel['Config']['general']['honeypot_mode'] && strlen($in) < $phpMussel['ReadBytes']($phpMussel['Config']['general']['quarantine_max_filesize']) ) { $qfu = $phpMussel['Time'] . '-' . md5($phpMussel['Config']['general']['quarantine_key'] . $fdCRC . $phpMussel['Time']); $phpMussel['Quarantine']( $in, $phpMussel['Config']['general']['quarantine_key'], $_SERVER[$phpMussel['IPAddr']], $qfu ); $phpMussel['killdata'] .= sprintf($phpMussel['L10N']->getString('quarantined_as'), $qfu) . "\n"; } } /** Delete if necessary. */ if ($phpMussel['Config']['general']['delete_on_sight'] && is_readable($f)) { unlink($f); } /** Exit. */ return !$n ? $z[0] : sprintf( '%s%s \'%s\' (FN: %s; FD: %s):%s%s', $lnap, $phpMussel['L10N']->getString('scan_checking'), $ofn, $fnCRC, $fdCRC, "\n", $z[1] ); } $x = sprintf( '%1$s%2$s \'%3$s\' (FN: %4$s; FD: %5$s):%6$s-%1$s%7$s%6$s', $lnap, $phpMussel['L10N']->getString('scan_checking'), $ofn, $fnCRC, $fdCRC, "\n", $phpMussel['L10N']->getString('scan_no_problems_found') ); /** Results. */ $r = 1; /** * Begin archive phase. * Note: Archive phase will only occur when "check_archives" is enabled and * when no problems were detected with the scan target by this point. */ if ( $phpMussel['Config']['files']['check_archives'] && !empty($in) && $phpMussel['Config']['files']['max_recursion'] > 1 ) { /** Define archive phase. */ $phpMussel['InstanceCache']['phase'] = 'archive'; /** In case there's any temporary files we need to delete afterwards. */ $phpMussel['InstanceCache']['tempfilesToDelete'] = []; /** Begin processing archives. */ $phpMussel['ArchiveRecursor']($x, $r, $in, (isset($CompressionResults) && !$CompressionResults) ? '' : $f, 0, urlencode($ofn)); /** Begin deleting any temporary files that snuck through. */ foreach ($phpMussel['InstanceCache']['tempfilesToDelete'] as $DeleteThis) { if (file_exists($DeleteThis)) { unlink($DeleteThis); } } } /** Quarantine if necessary. */ if ($r === 2) { if ( $phpMussel['Config']['general']['quarantine_key'] && !$phpMussel['Config']['general']['honeypot_mode'] && strlen($in) < $phpMussel['ReadBytes']($phpMussel['Config']['general']['quarantine_max_filesize']) ) { $qfu = $phpMussel['Time'] . '-' . md5( $phpMussel['Config']['general']['quarantine_key'] . $fdCRC . $phpMussel['Time'] ); $phpMussel['Quarantine']( $in, $phpMussel['Config']['general']['quarantine_key'], $_SERVER[$phpMussel['IPAddr']], $qfu ); $phpMussel['killdata'] .= sprintf($phpMussel['L10N']->getString('quarantined_as'), $qfu); } } /** Delete if necessary. */ if ($r !== 1 && $phpMussel['Config']['general']['delete_on_sight'] && is_readable($f)) { unlink($f); } /** Exit. */ return !$n ? $r : $x; }; /** * Quine detection for the archive handler. * * @param int $ScanDepth The current scan depth. * @param string $ParentHash Parent data hash. * @param int $ParentLen Parent data length. * @param string $ChildHash Child data hash. * @param int $ChildLen Child data length. * @return bool True when a quine is detected; False otherwise. */ $phpMussel['QuineDetector'] = function ($ScanDepth, $ParentHash, $ParentLen, $ChildHash, $ChildLen) use (&$phpMussel) { $phpMussel['Quine'][$ScanDepth - 1] = [$ParentHash, $ParentLen]; for ($Iterate = 0; $Iterate < $ScanDepth; $Iterate++) { if ($phpMussel['Quine'][$Iterate][0] === $ChildHash && $phpMussel['Quine'][$Iterate][1] === $ChildLen) { return true; } } return false; }; /** * Convert Chrome Extension data to standard Zip data. * * @param string $Data Referenced via the archive recursor. * @return bool True when conversion succeeds; False otherwise (e.g., not Crx). */ $phpMussel['ConvertCRX'] = function (&$Data) use (&$phpMussel) { if (substr($Data, 0, 4) !== 'Cr24' || strlen($Data) <= 16) { return false; } $CRX = ['Version' => unpack('i*', substr($Data, 4, 4))]; if ($CRX['Version'][1] === 2) { $CRX['PubKeyLen'] = unpack('i*', substr($Data, 8, 4)); $CRX['SigLen'] = unpack('i*', substr($Data, 12, 4)); $ZipBegin = 16 + $CRX['PubKeyLen'][1] + $CRX['SigLen'][1]; if (substr($Data, $ZipBegin, 2) === 'PK') { $phpMussel['CrxPubKey'] = bin2hex(substr($Data, 16, $CRX['PubKeyLen'][1])); $phpMussel['CrxSig'] = bin2hex(substr($Data, 16 + $CRX['PubKeyLen'][1], $CRX['SigLen'][1])); $Data = substr($Data, $ZipBegin); return true; } } return false; }; /** * Archive recursor. * * This is where we recurse through archives during the scan. * * @param string $x Scan results inherited from parent in the form of a string. * @param int $r Scan results inherited from parent in the form of an integer. * @param string $Data The data to be scanned (preferably an archive). * @param string $File A path to the file, to be able to access it directly if * needed (because the zip and rar classes require a file pointer). * @param int $ScanDepth The current scan depth (supplied during recursion). * @param string $ItemRef A reference to the parent container (for logging). */ $phpMussel['ArchiveRecursor'] = function (&$x, &$r, $Data, $File = '', $ScanDepth = 0, $ItemRef = '') use (&$phpMussel) { /** Plugin hook: "ArchiveRecursor_start". */ $phpMussel['Execute_Hook']('ArchiveRecursor_start'); /** Create quine detection array. */ if (!$ScanDepth || !isset($phpMussel['Quine'])) { $phpMussel['Quine'] = []; } /** Count recursion depth. */ $ScanDepth++; /** Used for CLI and logging. */ $Indent = str_pad('> ', $ScanDepth + 1, '-', STR_PAD_LEFT); /** Reset container definition. */ $phpMussel['InstanceCache']['container'] = 'none'; /** The class to use to handle the data to be scanned. */ $Handler = ''; /** The type of container to be scanned (mostly just for logging). */ $ConType = ''; /** Check whether Crx, and convert if necessary. */ if ($phpMussel['ConvertCRX']($Data)) { /** Reset the file pointer (because the content has been modified anyway). */ $File = ''; } /** Get file extensions. */ if ($File) { list($xt, $xts, $gzxt, $gzxts) = $phpMussel['FetchExt']($File); } elseif ($Exts = $phpMussel['substral']($ItemRef, '.')) { list($xt, $xts, $gzxt, $gzxts) = $phpMussel['FetchExt']($Exts); } else { $xt = $xts = $gzxt = $gzxts = ''; } /** Set appropriate container definitions and specify handler class. */ if (substr($Data, 0, 2) === 'PK') { $Handler = 'ZipHandler'; if ($xt === 'ole') { $ConType = 'OLE'; } elseif ($xt === 'crx') { $ConType = 'Crx'; } elseif ($xt === 'smpk') { $ConType = 'SMPTE'; } elseif ($xt === 'xpi') { $ConType = 'XPInstall'; } elseif ($xts === 'app*') { $ConType = 'App'; } elseif (strpos( ',docm,docx,dotm,dotx,potm,potx,ppam,ppsm,ppsx,pptm,pptx,xlam,xlsb,xlsm,xlsx,xltm,xltx,', ',' . $xt . ',' ) !== false) { $ConType = 'OpenXML'; } elseif (strpos( ',odc,odf,odg,odm,odp,ods,odt,otg,oth,otp,ots,ott,', ',' . $xt . ',' ) !== false || $xts === 'fod*') { $ConType = 'OpenDocument'; } elseif (strpos(',opf,epub,', ',' . $xt . ',') !== false) { $ConType = 'EPUB'; } else { $ConType = 'ZIP'; $phpMussel['InstanceCache']['container'] = 'zipfile'; } if ($ConType !== 'ZIP') { $phpMussel['InstanceCache']['file_is_ole'] = true; $phpMussel['InstanceCache']['container'] = 'pkfile'; } } elseif ( substr($Data, 257, 6) === "ustar\x00" || strpos(',tar,tgz,tbz,tlz,tz,', ',' . $xt . ',') !== false ) { $Handler = 'TarHandler'; $ConType = 'TarFile'; $phpMussel['InstanceCache']['container'] = 'tarfile'; } elseif (substr($Data, 0, 4) === 'Rar!' || substr($Data, 0, 4) === "\x52\x45\x7e\x5e") { $Handler = 'RarHandler'; $ConType = 'RarFile'; $phpMussel['InstanceCache']['container'] = 'rarfile'; } /** Not an archive. Exit early. */ if (!$Handler) { return; } /** Call the archive handler. */ if (!class_exists('\phpMussel\ArchiveHandler\ArchiveHandler')) { require $phpMussel['Vault'] . 'classes/ArchiveHandler.php'; } /** Hash the current input data. */ $DataHash = md5($Data); /** Fetch length of current input data. */ $DataLen = strlen($Data); /** Handle zip files. */ if ($Handler === 'ZipHandler') { /** * Encryption guard. * See: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT */ if ($phpMussel['Config']['files']['block_encrypted_archives']) { $Bits = $phpMussel['explode_bits'](substr($Data, 6, 2)); if ($Bits && $Bits[7]) { $r = -4; $phpMussel['killdata'] .= $DataHash . ':' . $DataLen . ':' . $ItemRef . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('encrypted_archive') . ' (' . $ItemRef . ')' ); $x .= sprintf( '-%1$s%2$s \'%3$s\' (FN: %4$s; FD: %5$s):%6$s--%1$s%7$s%8$s%6$s', $Indent, $phpMussel['L10N']->getString('scan_checking'), $ItemRef, hash('crc32b', $File), hash('crc32b', $Data), "\n", $phpMussel['L10N']->getString('encrypted_archive'), $phpMussel['L10N']->getString('_fullstop_final') ); return; } } /** Guard. */ if (!class_exists('ZipArchive')) { if (!$phpMussel['Config']['signatures']['fail_extensions_silently']) { $r = -1; $phpMussel['killdata'] .= $DataHash . ':' . $DataLen . ':' . $ItemRef . "\n"; $phpMussel['whyflagged'] .= $phpMussel['L10N']->getString('scan_extensions_missing') . ' (Zip)'; $x .= sprintf( '-%1$s%2$s \'%3$s\' (FN: %4$s; FD: %5$s):%6$s--%1$s%7$s%6$s', $Indent, $phpMussel['L10N']->getString('scan_checking'), $ItemRef, hash('crc32b', $File), hash('crc32b', $Data), "\n", $phpMussel['L10N']->getString('scan_extensions_missing') . ' (Zip)' ); return; } } /** ZipHandler needs a file pointer. */ if (!$File || !is_readable($File)) { /** * File pointer not available. Probably already inside an * archive. Let's create a temporary file for this. */ $PointerObject = new \phpMussel\TemporaryFileHandler\TemporaryFileHandler($Data, $phpMussel['cachePath']); $Pointer = &$PointerObject->Filename; $phpMussel['InstanceCache']['tempfilesToDelete'][] = $Pointer; } else { /** File pointer available. Let's reference it. */ $Pointer = &$File; } /** We have a valid a pointer. Let's instantiate the object. */ if ($Pointer) { $ArchiveObject = new \phpMussel\ArchiveHandler\ZipHandler($Pointer); } } /** Handle tar files. */ if ($Handler === 'TarHandler') { /** TarHandler can work with data directly. */ $ArchiveObject = new \phpMussel\ArchiveHandler\TarHandler($Data); } /** Handle rar files. */ if ($Handler === 'RarHandler') { /** Guard. */ if (!class_exists('RarArchive') || !class_exists('RarEntry')) { if (!$phpMussel['Config']['signatures']['fail_extensions_silently']) { $r = -1; $phpMussel['killdata'] .= $DataHash . ':' . $DataLen . ':' . $ItemRef . "\n"; $phpMussel['whyflagged'] .= $phpMussel['L10N']->getString('scan_extensions_missing') . ' (Rar)'; $x .= sprintf( '-%1$s%2$s \'%3$s\' (FN: %4$s; FD: %5$s):%6$s--%1$s%7$s%6$s', $Indent, $phpMussel['L10N']->getString('scan_checking'), $ItemRef, hash('crc32b', $File), hash('crc32b', $Data), "\n", $phpMussel['L10N']->getString('scan_extensions_missing') . ' (Rar)' ); return; } } /** RarHandler needs a file pointer. */ if (!$File || !is_readable($File)) { /** * File pointer not available. Probably already inside an * archive. Let's create a temporary file for this. */ $PointerObject = new \phpMussel\TemporaryFileHandler\TemporaryFileHandler($Data, $phpMussel['cachePath']); $Pointer = &$PointerObject->Filename; $phpMussel['InstanceCache']['tempfilesToDelete'][] = $Pointer; } else { /** File pointer available. Let's reference it. */ $Pointer = &$File; } /** We have a valid a pointer. Let's instantiate the object. */ if ($Pointer) { $ArchiveObject = new \phpMussel\ArchiveHandler\RarHandler($Pointer); } } /** Archive object has been instantiated. Let's proceed. */ if (isset($ArchiveObject) && is_object($ArchiveObject)) { /** No errors reported. Let's try checking its contents. */ if ($ArchiveObject->ErrorState === 0) { /** Used to count the number of entries processed. */ $Processed = 0; /** Iterate through the archive's contents. */ while ($ArchiveObject->EntryNext()) { /** Flag the archive if it exceeds the "max_files_in_archives" limit and return. */ if ( $phpMussel['Config']['files']['max_files_in_archives'] > 0 && $Processed > $phpMussel['Config']['files']['max_files_in_archives'] ) { $r = 2; $phpMussel['killdata'] .= $DataHash . ':' . $DataLen . ':' . $ItemRef . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('too_many_files_in_archive') . ' (' . $ItemRef . ')' ); $x .= sprintf( '-%1$s%2$s \'%3$s\' (FN: %4$s; FD: %5$s):%6$s--%1$s%7$s%8$s%6$s', $Indent, $phpMussel['L10N']->getString('scan_checking'), $ItemRef, hash('crc32b', $File), hash('crc32b', $Data), "\n", $phpMussel['L10N']->getString('too_many_files_in_archive'), $phpMussel['L10N']->getString('_fullstop_final') ); unset($ArchiveObject, $Pointer, $PointerObject); return; } $Processed++; /** Encryption guard. */ if ($phpMussel['Config']['files']['block_encrypted_archives'] && $ArchiveObject->EntryIsEncrypted()) { $r = -4; $phpMussel['killdata'] .= $DataHash . ':' . $DataLen . ':' . $ItemRef . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('encrypted_archive') . ' (' . $ItemRef . ')' ); $x .= sprintf( '-%1$s%2$s \'%3$s\' (FN: %4$s; FD: %5$s):%6$s--%1$s%7$s%8$s%6$s', $Indent, $phpMussel['L10N']->getString('scan_checking'), $ItemRef, hash('crc32b', $File), hash('crc32b', $Data), "\n", $phpMussel['L10N']->getString('encrypted_archive'), $phpMussel['L10N']->getString('_fullstop_final') ); unset($ArchiveObject, $Pointer, $PointerObject); return; } /** Fetch and prepare filename. */ if ($Filename = $ArchiveObject->EntryName()) { if (strpos($Filename, "\\") !== false) { $Filename = $phpMussel['substral']($Filename, "\\"); } if (strpos($Filename, '/') !== false) { $Filename = $phpMussel['substral']($Filename, '/'); } } /** Fetch filesize. */ $Filesize = $ArchiveObject->EntryActualSize(); /** Fetch content and build hashes. */ $Content = $ArchiveObject->EntryRead($Filesize); $MD5 = md5($Content); $NameCRC32 = hash('crc32b', $Filename); $DataCRC32 = hash('crc32b', $Content); $InternalCRC = $ArchiveObject->EntryCRC(); $ThisItemRef = $ItemRef . '>' . urlencode($Filename); /** Verify filesize, integrity, etc. Exit early in case of problems. */ if ($Filesize !== strlen($Content) || ($InternalCRC && preg_replace('~^0+~', '', $DataCRC32) !== preg_replace('~^0+~', '', $InternalCRC) )) { $r = 2; $phpMussel['killdata'] .= $MD5 . ':' . $Filesize . ':' . $ThisItemRef . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('scan_tampering') . ' (' . $ThisItemRef . ')' ); $x .= sprintf( '-%1$s%2$s \'%3$s\' (FN: %4$s; FD: %5$s):%6$s--%1$s%7$s%8$s%6$s', $Indent, $phpMussel['L10N']->getString('scan_checking'), $ThisItemRef, $NameCRC32, $DataCRC32, "\n", $phpMussel['L10N']->getString('recursive'), $phpMussel['L10N']->getString('_fullstop_final') ); unset($ArchiveObject, $Pointer, $PointerObject); return; } /** Executed if the recursion depth limit has been exceeded. */ if ($ScanDepth > $phpMussel['Config']['files']['max_recursion']) { $r = 2; $phpMussel['killdata'] .= $MD5 . ':' . $Filesize . ':' . $ThisItemRef . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('recursive') . ' (' . $ThisItemRef . ')' ); $x .= sprintf( '-%1$s%2$s \'%3$s\' (FN: %4$s; FD: %5$s):%6$s--%1$s%7$s%8$s%6$s', $Indent, $phpMussel['L10N']->getString('scan_checking'), $ThisItemRef, $NameCRC32, $DataCRC32, "\n", $phpMussel['L10N']->getString('recursive'), $phpMussel['L10N']->getString('_fullstop_final') ); unset($ArchiveObject, $Pointer, $PointerObject); return; } /** Quine detection. */ if ($phpMussel['QuineDetector']($ScanDepth, $DataHash, $DataLen, $MD5, $Filesize)) { $r = 2; $phpMussel['killdata'] .= $MD5 . ':' . $Filesize . ':' . $ThisItemRef . "\n"; $phpMussel['whyflagged'] .= sprintf( $phpMussel['L10N']->getString('_exclamation'), sprintf($phpMussel['L10N']->getString('detected'), 'Quine') . ' (' . $ThisItemRef . ')' ); $x .= sprintf( '-%1$s%2$s \'%3$s\' (FN: %4$s; FD: %5$s):%6$s--%1$s%7$s%8$s%6$s', $Indent, $phpMussel['L10N']->getString('scan_checking'), $ThisItemRef, $NameCRC32, $DataCRC32, "\n", sprintf($phpMussel['L10N']->getString('detected'), 'Quine'), $phpMussel['L10N']->getString('_fullstop_final') ); unset($ArchiveObject, $Pointer, $PointerObject); return; } /** Ready to check the entry. */ $x .= sprintf( '-%1$s%2$s \'%3$s\' (FN: %4$s; FD: %5$s):%6$s', $Indent, $phpMussel['L10N']->getString('scan_checking'), $ThisItemRef, $NameCRC32, $DataCRC32, "\n" ); /** Scan the entry. */ try { $phpMussel['MetaDataScan']( $x, $r, '--' . $Indent, $ThisItemRef, $Filename, $Content, $ScanDepth, $MD5 ); } catch (\Exception $e) { unset($ArchiveObject, $Pointer, $PointerObject); throw new \Exception($e->getMessage()); } /** If we've already found something bad, we can exit early to save time. */ if ($r !== 1) { unset($ArchiveObject, $Pointer, $PointerObject); return; } /** Finally, check whether the archive entry is an archive. */ $phpMussel['ArchiveRecursor']($x, $r, $Content, '', $ScanDepth, $ThisItemRef); } } } /** Unset order is important for temporary files to be able to be deleted properly. */ unset($ArchiveObject, $Pointer, $PointerObject); }; /** * Drops trailing extensions from filenames if the extension matches that of a * compression format supported by the compression handler. * * @param string $Filename The filename. * @return string The filename sans compression extension. */ $phpMussel['DropTrailingCompressionExtension'] = function ($Filename) { return preg_replace(['~\.t[gbl]?z[\da-z]?$~i', '~\.(?:bz2?|gz|lha|lz[fhowx])$~i'], ['.tar', ''], $Filename); }; /** * Forks the PHP process when scanning in CLI mode. This ensures that if PHP * crashes during scanning, phpMussel can continue to scan any remaining items * queued for scanning (because if the parent process handles the scan queue * and the child process handles the actual scanning of each item queued for * scanning, if the child process crashes, the parent process can simply create * a new child process to continue iterating through the queue). * * There are some additional benefits to be had by scanning in this way, such * as the ability to kill a child process, responsible for the actual scanning, * and yet still have the results of this scanning logged (which, if killed in * this way prior to completing the scan, would likely involve some type of * error). * * @param string $f The name of the item to be scanned, with path included. * @param string $ofn The name of the item to be scanned, without any path * included (so, just the name by itself). * @return string The scan results, piped back to the parent from the child * process and returned to the calling function as a string. */ $phpMussel['Fork'] = function ($f = '', $ofn = '') use (&$phpMussel) { $pf = popen( $phpMussel['Mussel_PHP'] . ' "' . $phpMussel['Vault'] . '../loader.php" "cli_scan" "' . $f . '" "' . $ofn . '"', 'r' ); $s = ''; while ($x = fgets($pf)) { $s .= $x; } pclose($pf); return $s; }; /** Assigns an array to use for dumping scan debug information (optional). */ $phpMussel['Set-Scan-Debug-Array'] = function (&$Var) use (&$phpMussel) { if (isset($phpMussel['DebugArr'])) { unset($phpMussel['DebugArr']); } if (!is_array($Var)) { $Var = []; } $phpMussel['DebugArr'] = &$Var; }; /** Destroys the scan debug array (optional). */ $phpMussel['Destroy-Scan-Debug-Array'] = function (&$Var) use (&$phpMussel) { unset($phpMussel['DebugArrKey'], $phpMussel['DebugArr']); $Var = null; }; /** * The main scan closure, responsible for initialising scans in most * circumstances. Should generally be called whenever phpMussel is * required by external scripts, apps, CMS, etc. * * Please refer to Section 3A of the README documentation, "HOW TO USE (FOR WEB * SERVERS)", for more information. * * @param string|array $f Indicates which file, files, directory, or * directories to scan (can be a string, an array, or a multidimensional * array). * @param bool $n A boolean, indicating the format for the scan results to be * returned as. False instructs the function to return the results as an * integer; True instructs the function to return the results as human * readable text. Optional; Defaults to false. * @param bool $zz A boolean, indicating to the function whether or not arrayed * results should be imploded prior to being returned to the calling * function. False instructs the function to return the arrayed results as * verbatim; True instructs the function to return the arrayed results as * an imploded string. Optional; Defaults to false. * @param int $dpt Represents the current depth of recursion from which the * function has been called. This information is used for determining how * far to indent any entries generated for logging (you should never * manually set this parameter yourself). * @param string $ofn For the file upload scanning that phpMussel normally * performs by default, this parameter represents the "original filename" * of the file being scanned (the original filename, in this context, * referring to the name supplied by the upload client, as opposed to the * temporary filename assigned by the server or anything else). * @return bool|int|string|array The scan results, returned as an array when * the $f parameter is an array and when $n and/or $zz is/are false, and * otherwise returned as per described by the README documentation. The * function may also die the script and return nothing, if something goes * wrong, such as if the function is triggered in the absence of the * required $phpMussel['InstanceCache'] variable being set, and may also return * false, in the absence of the required $phpMussel['HashCache']['Data'] * variable being set. */ $phpMussel['Scan'] = function ($f = '', $n = false, $zz = false, $dpt = 0, $ofn = '') use (&$phpMussel) { if (!isset($phpMussel['InstanceCache'])) { throw new \Exception($phpMussel['L10N']->getString( 'required_variables_not_defined' ) ?: '[phpMussel] Required variables aren\'t defined: Can\'t continue.'); } /** Prepare signature files for the scan process. */ if (empty($phpMussel['InstanceCache']['OrganisedSigFiles'])) { $phpMussel['OrganiseSigFiles'](); $phpMussel['InstanceCache']['OrganisedSigFiles'] = true; } /** Initialise statistics if they've been enabled. */ $phpMussel['Stats-Initialise'](); if ($phpMussel['EOF']) { $phpMussel['PrepareHashCache'](); } if (!isset($phpMussel['HashCache']['Data'])) { return false; } if (!$ofn) { $ofn = $f; } $xst = time() + ($phpMussel['Config']['general']['timeOffset'] * 60); $xst2822 = $phpMussel['TimeFormat']($xst, $phpMussel['Config']['general']['timeFormat']); try { $r = $phpMussel['Recursor']($f, $n, $zz, $dpt, $ofn); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } $xet = time() + ($phpMussel['Config']['general']['timeOffset'] * 60); $xet2822 = $phpMussel['TimeFormat']($xet, $phpMussel['Config']['general']['timeFormat']); /** Plugin hook: "after_scan". */ $phpMussel['Execute_Hook']('after_scan'); if ($n && !is_array($r)) { $r = $xst2822 . ' ' . $phpMussel['L10N']->getString('started') . $phpMussel['L10N']->getString('_fullstop_final') . "\n" . $r . $xet2822 . ' ' . $phpMussel['L10N']->getString('finished') . $phpMussel['L10N']->getString('_fullstop_final') . "\n"; $phpMussel['WriteScanLog']($r); } if (!isset($phpMussel['SkipSerial'])) { $phpMussel['WriteSerial']($xst, $xet); } if ($phpMussel['EOF']) { if ($phpMussel['Config']['general']['scan_cache_expiry'] > 0) { foreach ($phpMussel['HashCache']['Data'] as &$phpMussel['ThisItem']) { if (is_array($phpMussel['ThisItem'])) { $phpMussel['ThisItem'] = implode(':', $phpMussel['ThisItem']) . ';'; } } /** Update hash cache. */ $phpMussel['SaveCache']( 'HashCache', $phpMussel['Time'] + $phpMussel['Config']['general']['scan_cache_expiry'], implode('', $phpMussel['HashCache']['Data']) ); unset($phpMussel['ThisItem'], $phpMussel['HashCache']['Data']); } } /** Register scan event. */ $phpMussel['Stats-Increment']($phpMussel['EOF'] ? 'API-Events' : 'Web-Events', 1); /** Update statistics. */ if (!empty($phpMussel['CacheModified'])) { $phpMussel['Statistics'] = $phpMussel['SaveCache']('Statistics', -1, serialize($phpMussel['Statistics'])); } /** Exit scan process. */ return $r; }; /** * Writes to the serialized logfile upon scan completion. * * @param string $StartTime When the scan started. * @param string $FinishTime When the scan finished. * @return bool True on success; False on failure. */ $phpMussel['WriteSerial'] = function ($StartTime = '', $FinishTime = '') use (&$phpMussel) { if ($phpMussel['Mussel_sapi']) { $Origin = 'CLI'; } else { $Origin = $phpMussel['Config']['legal']['pseudonymise_ip_addresses'] ? $phpMussel['Pseudonymise-IP']( $_SERVER[$phpMussel['IPAddr']] ) : $_SERVER[$phpMussel['IPAddr']]; } $ScanData = empty($phpMussel['whyflagged']) ? $phpMussel['L10N']->getString('data_not_available') : trim($phpMussel['whyflagged']); if ($phpMussel['Config']['general']['scan_log_serialized']) { if (!isset($phpMussel['InstanceCache']['objects_scanned'])) { $phpMussel['InstanceCache']['objects_scanned'] = 0; } if (!isset($phpMussel['InstanceCache']['detections_count'])) { $phpMussel['InstanceCache']['detections_count'] = 0; } if (!isset($phpMussel['InstanceCache']['scan_errors'])) { $phpMussel['InstanceCache']['scan_errors'] = 1; } $Handle = [ 'Data' => serialize([ 'start_time' => $StartTime ?: (isset($phpMussel['InstanceCache']['start_time']) ? $phpMussel['InstanceCache']['start_time'] : '-'), 'end_time' => $FinishTime ?: (isset($phpMussel['InstanceCache']['end_time']) ? $phpMussel['InstanceCache']['end_time'] : '-'), 'origin' => $Origin, 'objects_scanned' => $phpMussel['InstanceCache']['objects_scanned'], 'detections_count' => $phpMussel['InstanceCache']['detections_count'], 'scan_errors' => $phpMussel['InstanceCache']['scan_errors'], 'detections' => $ScanData ]) . "\n", 'File' => $phpMussel['TimeFormat']($phpMussel['Time'], $phpMussel['Config']['general']['scan_log_serialized']) ]; $WriteMode = (!file_exists($phpMussel['Vault'] . $Handle['File']) || ( $phpMussel['Config']['general']['truncate'] > 0 && filesize($phpMussel['Vault'] . $Handle['File']) >= $phpMussel['ReadBytes']($phpMussel['Config']['general']['truncate']) )) ? 'w' : 'a'; if ($phpMussel['BuildLogPath']($Handle['File'])) { $Stream = fopen($phpMussel['Vault'] . $Handle['File'], $WriteMode); fwrite($Stream, $Handle['Data']); fclose($Stream); if ($WriteMode === 'w') { $phpMussel['LogRotation']($phpMussel['Config']['general']['scan_log_serialized']); } return true; } } return false; }; /** * Writes to the standard scan log upon scan completion. * * @param string $Data What to write. * @return bool True on success; False on failure. */ $phpMussel['WriteScanLog'] = function ($Data) use (&$phpMussel) { if ($phpMussel['Config']['general']['scan_log']) { $File = $phpMussel['TimeFormat']($phpMussel['Time'], $phpMussel['Config']['general']['scan_log']); if (!file_exists($phpMussel['Vault'] . $File)) { $Data = $phpMussel['safety'] . "\n" . $Data; } $WriteMode = (!file_exists($phpMussel['Vault'] . $File) || ( $phpMussel['Config']['general']['truncate'] > 0 && filesize($phpMussel['Vault'] . $File) >= $phpMussel['ReadBytes']($phpMussel['Config']['general']['truncate']) )) ? 'w' : 'a'; if ($phpMussel['BuildLogPath']($File)) { $Handle = fopen($phpMussel['Vault'] . $File, 'a'); fwrite($Handle, $Data); fclose($Handle); if ($WriteMode === 'w') { $phpMussel['LogRotation']($phpMussel['Config']['general']['scan_log']); } return true; } } return false; }; /** * A simple closure for replacing date/time placeholders with corresponding * date/time information. Used by the logfiles and some timestamps. * * @param int $Time A unix timestamp. * @param string|array $In An input or an array of inputs to manipulate. * @return string|array The adjusted input(/s). */ $phpMussel['TimeFormat'] = function ($Time, $In) use (&$phpMussel) { $Time = date('dmYHisDMP', $Time); $values = [ 'dd' => substr($Time, 0, 2), 'mm' => substr($Time, 2, 2), 'yyyy' => substr($Time, 4, 4), 'yy' => substr($Time, 6, 2), 'hh' => substr($Time, 8, 2), 'ii' => substr($Time, 10, 2), 'ss' => substr($Time, 12, 2), 'Day' => substr($Time, 14, 3), 'Mon' => substr($Time, 17, 3), 'tz' => substr($Time, 20, 3) . substr($Time, 24, 2), 't:z' => substr($Time, 20, 6) ]; $values['d'] = (int)$values['dd']; $values['m'] = (int)$values['mm']; return is_array($In) ? array_map(function ($Item) use (&$values, &$phpMussel) { return $phpMussel['ParseVars']($values, $Item); }, $In) : $phpMussel['ParseVars']($values, $In); }; /** * Fix incorrect typecasting for some for some variables that sometimes default * to strings instead of booleans or integers. */ $phpMussel['AutoType'] = function (&$Var, $Type = '') use (&$phpMussel) { if ($Type === 'string' || $Type === 'timezone') { $Var = (string)$Var; } elseif ($Type === 'int' || $Type === 'integer') { $Var = (int)$Var; } elseif ($Type === 'real' || $Type === 'double' || $Type === 'float') { $Var = (float)$Var; } elseif ($Type === 'bool' || $Type === 'boolean') { $Var = (strtolower($Var) !== 'false' && $Var); } elseif ($Type === 'kb') { $Var = $phpMussel['ReadBytes']($Var, 1); } else { $LVar = strtolower($Var); if ($LVar === 'true') { $Var = true; } elseif ($LVar === 'false') { $Var = false; } elseif ($Var !== true && $Var !== false) { $Var = (int)$Var; } } }; /** * Used to send cURL requests. * * @param string $URI The resource to request. * @param array $Params (Optional) An associative array of key-value pairs to * to send along with the request. * @return string The results of the request. */ $phpMussel['Request'] = function ($URI, $Params = '', $Timeout = '') use (&$phpMussel) { if (!$Timeout) { $Timeout = $phpMussel['Timeout']; } /** Initialise the cURL session. */ $Request = curl_init($URI); $LCURI = strtolower($URI); $SSL = (substr($LCURI, 0, 6) === 'https:'); curl_setopt($Request, CURLOPT_FRESH_CONNECT, true); curl_setopt($Request, CURLOPT_HEADER, false); if (empty($Params)) { curl_setopt($Request, CURLOPT_POST, false); } else { curl_setopt($Request, CURLOPT_POST, true); curl_setopt($Request, CURLOPT_POSTFIELDS, $Params); } if ($SSL) { curl_setopt($Request, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); curl_setopt($Request, CURLOPT_SSL_VERIFYPEER, false); } curl_setopt($Request, CURLOPT_FOLLOWLOCATION, true); curl_setopt($Request, CURLOPT_MAXREDIRS, 1); curl_setopt($Request, CURLOPT_RETURNTRANSFER, true); curl_setopt($Request, CURLOPT_TIMEOUT, $Timeout); curl_setopt($Request, CURLOPT_USERAGENT, $phpMussel['ScriptUA']); /** Execute and get the response. */ $Response = curl_exec($Request); /** Close the cURL session. */ curl_close($Request); /** Return the results of the request. */ return $Response; }; /** * Used to generate new salts when necessary, which may be occasionally used by * some specific optional peripheral features (note: should not be considered * cryptographically secure; especially so for versions of PHP < 7). * * @return string Salt. */ $phpMussel['GenerateSalt'] = function () { static $MinLen = 32; static $MaxLen = 72; static $MinChr = 1; static $MaxChr = 255; $Salt = ''; if (function_exists('random_int')) { try { $Length = random_int($MinLen, $MaxLen); } catch (\Exception $e) { $Length = rand($MinLen, $MaxLen); } } else { $Length = rand($MinLen, $MaxLen); } if (function_exists('random_bytes')) { try { $Salt = random_bytes($Length); } catch (\Exception $e) { $Salt = ''; } } if (empty($Salt)) { if (function_exists('random_int')) { try { for ($Index = 0; $Index < $Length; $Index++) { $Salt .= chr(random_int($MinChr, $MaxChr)); } } catch (\Exception $e) { $Salt = ''; for ($Index = 0; $Index < $Length; $Index++) { $Salt .= chr(rand($MinChr, $MaxChr)); } } } else { for ($Index = 0; $Index < $Length; $Index++) { $Salt .= chr(rand($MinChr, $MaxChr)); } } } return $Salt; }; /** * Clears expired entries from a list. * * @param string $List The list to clear from. * @param bool $Check A flag indicating when changes have occurred. */ $phpMussel['ClearExpired'] = function (&$List, &$Check) use (&$phpMussel) { if ($List) { $End = 0; while (true) { $Begin = $End; if (!$End = strpos($List, "\n", $Begin + 1)) { break; } $Line = substr($List, $Begin, $End - $Begin); if ($Split = strrpos($Line, ',')) { $Expiry = (int)substr($Line, $Split + 1); if ($Expiry < $phpMussel['Time']) { $List = str_replace($Line, '', $List); $End = 0; $Check = true; } } } } }; /** Fetch information about signature files and prepare for use with the scan process. */ $phpMussel['OrganiseSigFiles'] = function () use (&$phpMussel) { $LastActive = $phpMussel['FetchCache']('Active') ?: ''; if (empty($phpMussel['Config']['signatures']['Active'])) { if ($LastActive) { $phpMussel['ClearHashCache'](); } return false; } if ($phpMussel['Config']['signatures']['Active'] !== $LastActive) { $phpMussel['ClearHashCache'](); if (isset($phpMussel['Cache']) && $phpMussel['Cache']->Using) { $phpMussel['Cache']->deleteEntry('Active'); } else { $phpMussel['SaveCache']('Active', -1, $phpMussel['Config']['signatures']['Active']); } } $Classes = [ 'General_Command_Detections', 'Filename', 'Hash', 'Standard', 'Standard_RegEx', 'Normalised', 'Normalised_RegEx', 'HTML', 'HTML_RegEx', 'PE_Extended', 'PE_Sectional', 'Complex_Extended', 'URL_Scanner' ]; $List = explode(',', $phpMussel['Config']['signatures']['Active']); foreach ($List as $File) { $File = (strpos($File, ':') === false) ? $File : substr($File, strpos($File, ':') + 1); $Handle = fopen($phpMussel['sigPath'] . $File, 'rb'); if (fread($Handle, 9) !== 'phpMussel') { fclose($Handle); continue; } $Class = fread($Handle, 1); fclose($Handle); $Nibbles = $phpMussel['split_nibble']($Class); if (!empty($Classes[$Nibbles[0]])) { if (!isset($phpMussel['InstanceCache'][$Classes[$Nibbles[0]]])) { $phpMussel['InstanceCache'][$Classes[$Nibbles[0]]] = ','; } $phpMussel['InstanceCache'][$Classes[$Nibbles[0]]] .= $File . ','; } } }; /** A simple safety wrapper for unpack. */ $phpMussel['UnpackSafe'] = function ($Format, $Data) { return (strlen($Data) > 1) ? unpack($Format, $Data) : ''; }; /** A simple safety wrapper for hex2bin. */ $phpMussel['HexSafe'] = function ($Data) use (&$phpMussel) { return ($Data && !preg_match('/[^\da-f]/i', $Data) && !(strlen($Data) % 2)) ? $phpMussel['Function']('HEX', $Data) : ''; }; /** If input isn't an array, make it so. Remove empty elements. */ $phpMussel['Arrayify'] = function (&$Input) { if (!is_array($Input)) { $Input = [$Input]; } $Input = array_filter($Input); }; /** * Read byte value configuration directives as byte values. * * @param string $In Input. * @param int $Mode Operating mode. 0 for true byte values, 1 for validating. * Default is 0. * @return string|int Output (depends on operating mode). */ $phpMussel['ReadBytes'] = function ($In, $Mode = 0) { if (preg_match('/[KMGT][oB]$/i', $In)) { $Unit = substr($In, -2, 1); } elseif (preg_match('/[KMGToB]$/i', $In)) { $Unit = substr($In, -1); } $Unit = isset($Unit) ? strtoupper($Unit) : 'K'; $In = (float)$In; if ($Mode === 1) { return $Unit === 'B' || $Unit === 'o' ? $In . 'B' : $In . $Unit . 'B'; } $Multiply = ['K' => 1024, 'M' => 1048576, 'G' => 1073741824, 'T' => 1099511627776]; return (int)floor($In * (isset($Multiply[$Unit]) ? $Multiply[$Unit] : 1)); }; /** * Improved recursive directory iterator for phpMussel. * * @param string $Base Directory root. * @return array Directory tree. */ $phpMussel['DirectoryRecursiveList'] = function ($Base) { $Arr = []; $Offset = strlen($Base); $List = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($Base), RecursiveIteratorIterator::SELF_FIRST); foreach ($List as $Item => $List) { if (preg_match('~^(?:/\.\.|./\.|\.{3})$~', str_replace("\\", '/', substr($Item, -3))) || !is_readable($Item)) { continue; } $Arr[] = substr($Item, $Offset); } return $Arr; }; /** * Duplication avoidance (forking the process via recursive CLI mode commands). * * @param string $Command Command with parameters for the action to be taken. * @param callable $Callable Executed normally when not forking the process. * @return string Returnable data to be echoed to the CLI output. */ $phpMussel['CLI-RecursiveCommand'] = function ($Command, $Callable) use (&$phpMussel) { $Params = substr($Command, strlen($phpMussel['cmd']) + 1); if (is_dir($Params)) { if (!is_readable($Params)) { return sprintf($phpMussel['L10N']->getString('failed_to_access'), $Params) . "\n"; } $Decal = [':-) - (-:', ':-) \\ (-:', ':-) | (-:', ':-) / (-:']; $Frame = 0; $Terminal = $Params[strlen($Params) - 1]; if ($Terminal !== "\\" && $Terminal !== '/') { $Params .= '/'; } $List = $phpMussel['DirectoryRecursiveList']($Params); $Returnable = ''; foreach ($List as $Item) { echo "\r" . $Decal[$Frame]; $Returnable .= $phpMussel['Fork']($phpMussel['cmd'] . ' ' . $Params . $Item, $Item) . "\n"; $Frame = $Frame < 3 ? $Frame + 1 : 0; } echo "\r "; return $Returnable; } return is_file($Params) ? $Callable($Params) : sprintf($phpMussel['L10N']->getString('cli_is_not_a'), $Params) . "\n"; }; /** Handles errors (will expand this later). */ $phpMussel['ErrorHandler_1'] = function ($errno) use (&$phpMussel) { return; }; /** Duplication avoidance (some file handling for honeypot functionality). */ $phpMussel['ReadFile-For-Honeypot'] = function (&$Array, $File) use (&$phpMussel) { if (!isset($Array['qdata'])) { $Array['qdata'] = ''; } $Array['odata'] = $phpMussel['ReadFile']($File); $Array['len'] = strlen($Array['odata']); $Array['crc'] = hash('crc32b', $Array['odata']); $Array['qfile'] = $phpMussel['Time'] . '-' . md5($phpMussel['Config']['general']['quarantine_key'] . $Array['crc'] . $phpMussel['Time']); if ($Array['len'] > 0 && $Array['len'] < $phpMussel['ReadBytes']($phpMussel['Config']['general']['quarantine_max_filesize'])) { $phpMussel['Quarantine']( $Array['odata'], $phpMussel['Config']['general']['quarantine_key'], $_SERVER[$phpMussel['IPAddr']], $Array['qfile'] ); } if ($phpMussel['Config']['general']['delete_on_sight'] && is_readable($File)) { unlink($File); } $Array['qdata'] .= sprintf( 'TEMP FILENAME: %1$s%6$sFILENAME: %2$s%6$sFILESIZE: %3$s%6$sMD5: %4$s%6$s%5$s', $File, urlencode($File), $Array['len'], md5($Array['odata']), sprintf($phpMussel['L10N']->getString('quarantined_as'), $Array['qfile']), "\n" ); }; /** Duplication avoidance (assigning kill details and unlinking files). */ $phpMussel['KillAndUnlink'] = function () use (&$phpMussel) { $phpMussel['killdata'] .= '-UPLOAD-LIMIT-EXCEEDED--NO-HASH-:' . $phpMussel['upload']['FilesData']['FileSet']['size'][$phpMussel['ThisIter']] . ':' . $phpMussel['upload']['FilesData']['FileSet']['name'][$phpMussel['ThisIter']] . "\n"; $phpMussel['whyflagged'] .= sprintf($phpMussel['L10N']->getString('_exclamation'), $phpMussel['L10N']->getString('upload_limit_exceeded') . ' (' . $phpMussel['upload']['FilesData']['FileSet']['name'][$phpMussel['ThisIter']] . ')' ); if ( $phpMussel['Config']['general']['delete_on_sight'] && is_uploaded_file($phpMussel['upload']['FilesData']['FileSet']['tmp_name'][$phpMussel['ThisIter']]) && is_readable($phpMussel['upload']['FilesData']['FileSet']['tmp_name'][$phpMussel['ThisIter']]) && !$phpMussel['Config']['general']['honeypot_mode'] ) { unlink($phpMussel['upload']['FilesData']['FileSet']['tmp_name'][$phpMussel['ThisIter']]); } }; /** Increments statistics if they've been enabled. */ $phpMussel['Stats-Increment'] = function ($Statistic, $Amount) use (&$phpMussel) { if ($phpMussel['Config']['general']['statistics'] && isset($phpMussel['Statistics'][$Statistic])) { $phpMussel['Statistics'][$Statistic] += $Amount; $phpMussel['CacheModified'] = true; } }; /** Initialise statistics if they've been enabled. */ $phpMussel['Stats-Initialise'] = function () use (&$phpMussel) { if ($phpMussel['Config']['general']['statistics']) { $phpMussel['CacheModified'] = false; if ($phpMussel['Statistics'] = ($phpMussel['FetchCache']('Statistics') ?: [])) { if (is_string($phpMussel['Statistics'])) { unserialize($phpMussel['Statistics']) ?: []; } } if (empty($phpMussel['Statistics']['Other-Since'])) { $phpMussel['Statistics'] = [ 'Other-Since' => $phpMussel['Time'], 'Web-Events' => 0, 'Web-Scanned' => 0, 'Web-Blocked' => 0, 'Web-Quarantined' => 0, 'CLI-Events' => 0, 'CLI-Scanned' => 0, 'CLI-Flagged' => 0, 'API-Events' => 0, 'API-Scanned' => 0, 'API-Flagged' => 0 ]; $phpMussel['CacheModified'] = true; } } }; /** Clears out the hash cache. */ $phpMussel['ClearHashCache'] = function () use (&$phpMussel) { $phpMussel['InitialiseCache'](); /** Override if using a different preferred caching mechanism. */ if ($phpMussel['Cache']->Using) { return $phpMussel['Cache']->deleteEntry('HashCache'); } /** Default process. */ $File = $phpMussel['cachePath'] . '48.tmp'; $Data = $phpMussel['ReadFile']($File) ?: ''; while (strpos($Data, 'HashCache:') !== false) { $Data = str_ireplace('HashCache:' . $phpMussel['substrbf']($phpMussel['substraf']($Data, 'HashCache:'), ';') . ';', '', $Data); } if (strlen($Data) < 2) { unset($File); } else { $Handle = fopen($File, 'w'); fwrite($Handle, $Data); fclose($Handle); } $IndexFile = $phpMussel['cachePath'] . 'index.dat'; $IndexNewData = $IndexData = $phpMussel['ReadFile']($IndexFile) ?: ''; while (strpos($IndexNewData, 'HashCache:') !== false) { $IndexNewData = str_ireplace('HashCache:' . $phpMussel['substrbf']($phpMussel['substraf']($IndexNewData, 'HashCache:'), ';') . ';', '', $IndexNewData); } if ($IndexNewData !== $IndexData) { $IndexHandle = fopen($IndexFile, 'w'); fwrite($IndexHandle, $IndexNewData); fclose($IndexHandle); } return true; }; /** * Build directory path for logfiles. * * @param string $File The file we're building for. * @return bool True on success; False on failure. */ $phpMussel['BuildLogPath'] = function ($File) use (&$phpMussel) { $ThisPath = $phpMussel['Vault']; $File = str_replace("\\", '/', $File); while (strpos($File, '/') !== false) { $Dir = substr($File, 0, strpos($File, '/')); $ThisPath .= $Dir . '/'; $File = substr($File, strlen($Dir) + 1); if (!file_exists($ThisPath) || !is_dir($ThisPath)) { if (!mkdir($ThisPath)) { return false; } } } return true; }; /** * Checks whether the specified directory is empty. * * @param string $Directory The directory to check. * @return bool True if empty; False if not empty. */ $phpMussel['IsDirEmpty'] = function ($Directory) { return !((new \FilesystemIterator($Directory))->valid()); }; /** * Deletes empty directories (used by some front-end functions and log rotation). * * @param string $Dir The directory to delete. */ $phpMussel['DeleteDirectory'] = function ($Dir) use (&$phpMussel) { while (strrpos($Dir, '/') !== false || strrpos($Dir, "\\") !== false) { $Separator = (strrpos($Dir, '/') !== false) ? '/' : "\\"; $Dir = substr($Dir, 0, strrpos($Dir, $Separator)); if (!is_dir($phpMussel['Vault'] . $Dir) || !$phpMussel['IsDirEmpty']($phpMussel['Vault'] . $Dir)) { break; } rmdir($phpMussel['Vault'] . $Dir); } }; /** Convert configuration directives for logfiles to regexable patterns. */ $phpMussel['BuildLogPattern'] = function ($Str, $GZ = false) { return '~^' . preg_replace( ['~\\\{(?:dd|mm|yy|hh|ii|ss)\\\}~i', '~\\\{yyyy\\\}~i', '~\\\{(?:Day|Mon)\\\}~i', '~\\\{tz\\\}~i', '~\\\{t\\\:z\\\}~i'], ['\d{2}', '\d{4}', '\w{3}', '.{1,2}\d{4}', '.{1,2}\d{2}\:\d{2}'], preg_quote(str_replace("\\", '/', $Str)) ) . ($GZ ? '(?:\.gz)?' : '') . '$~i'; }; /** * GZ-compress a file (used by log rotation). * * @param string $File The file to GZ-compress. * @return bool True if the file exists and is readable; False otherwise. */ $phpMussel['GZCompressFile'] = function ($File) { if (!is_file($File) || !is_readable($File)) { return false; } static $Blocksize = 131072; $Filesize = filesize($File); $Size = ($Filesize && $Blocksize) ? ceil($Filesize / $Blocksize) : 0; if ($Size > 0) { $Handle = fopen($File, 'rb'); $HandleGZ = gzopen($File . '.gz', 'wb'); $Block = 0; while ($Block < $Size) { $Data = fread($Handle, $Blocksize); gzwrite($HandleGZ, $Data); $Block++; } gzclose($HandleGZ); fclose($Handle); } return true; }; /** * Log rotation. * * @param string $Pattern What to identify logfiles by (should be supplied via the relevant logging directive). * @return bool False when log rotation is disabled or errors occur; True otherwise. */ $phpMussel['LogRotation'] = function ($Pattern) use (&$phpMussel) { $Action = empty($phpMussel['Config']['general']['log_rotation_action']) ? '' : $phpMussel['Config']['general']['log_rotation_action']; $Limit = empty($phpMussel['Config']['general']['log_rotation_limit']) ? 0 : $phpMussel['Config']['general']['log_rotation_limit']; if (!$Limit || ($Action !== 'Delete' && $Action !== 'Archive')) { return false; } $Pattern = $phpMussel['BuildLogPattern']($Pattern); $Arr = []; $Offset = strlen($phpMussel['Vault']); $List = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($phpMussel['Vault']), RecursiveIteratorIterator::SELF_FIRST); foreach ($List as $Item => $List) { $ItemFixed = str_replace("\\", '/', substr($Item, $Offset)); if ($ItemFixed && preg_match($Pattern, $ItemFixed) && is_readable($Item)) { $Arr[$ItemFixed] = filemtime($Item); } } unset($ItemFixed, $List, $Offset); $Count = count($Arr); $Err = 0; if ($Count > $Limit) { asort($Arr, SORT_NUMERIC); foreach ($Arr as $Item => $Modified) { if ($Action === 'Archive') { $Err += !$phpMussel['GZCompressFile']($phpMussel['Vault'] . $Item); } $Err += !unlink($phpMussel['Vault'] . $Item); if (strpos($Item, '/') !== false) { $phpMussel['DeleteDirectory']($Item); } $Count--; if (!($Count > $Limit)) { break; } } } return $Err ? false : true; }; /** * Pseudonymise an IP address (reduce IPv4s to /24s and IPv6s to /32s). * * @param string $IP An IP address. * @return string A pseudonymised IP address. */ $phpMussel['Pseudonymise-IP'] = function ($IP) { if (($CPos = strpos($IP, ':')) !== false) { $Parts = [(substr($IP, 0, $CPos) ?: ''), (substr($IP, $CPos +1) ?: '')]; if (($CPos = strpos($Parts[1], ':')) !== false) { $Parts[1] = substr($Parts[1], 0, $CPos) ?: ''; } $Parts = $Parts[0] . ':' . $Parts[1] . '::x'; return str_replace(':::', '::', $Parts); } return preg_replace( '/^([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])$/i', '\1.\2.\3.x', $IP ); }; /** Initialise cache. */ $phpMussel['InitialiseCache'] = function () use (&$phpMussel) { /** Exit early if already initialised. */ if (isset($phpMussel['Cache'])) { return; } /** Create new cache object. */ $phpMussel['Cache'] = new \Maikuolan\Common\Cache(); $phpMussel['Cache']->EnableAPCu = $phpMussel['Config']['supplementary_cache_options']['enable_apcu']; $phpMussel['Cache']->EnableMemcached = $phpMussel['Config']['supplementary_cache_options']['enable_memcached']; $phpMussel['Cache']->EnableRedis = $phpMussel['Config']['supplementary_cache_options']['enable_redis']; $phpMussel['Cache']->EnablePDO = $phpMussel['Config']['supplementary_cache_options']['enable_pdo']; $phpMussel['Cache']->MemcachedHost = $phpMussel['Config']['supplementary_cache_options']['memcached_host']; $phpMussel['Cache']->MemcachedPort = $phpMussel['Config']['supplementary_cache_options']['memcached_port']; $phpMussel['Cache']->RedisHost = $phpMussel['Config']['supplementary_cache_options']['redis_host']; $phpMussel['Cache']->RedisPort = $phpMussel['Config']['supplementary_cache_options']['redis_port']; $phpMussel['Cache']->RedisTimeout = $phpMussel['Config']['supplementary_cache_options']['redis_timeout']; $phpMussel['Cache']->PDOdsn = $phpMussel['Config']['supplementary_cache_options']['pdo_dsn']; $phpMussel['Cache']->PDOusername = $phpMussel['Config']['supplementary_cache_options']['pdo_username']; $phpMussel['Cache']->PDOpassword = $phpMussel['Config']['supplementary_cache_options']['pdo_password']; $phpMussel['Cache']->connect(); };