phpffcms /
ffcms
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
| 1 | <?php |
||
| 2 | |||
| 3 | namespace Apps\Model\Basic; |
||
| 4 | |||
| 5 | use Ffcms\Core\Exception\NativeException; |
||
| 6 | use Ffcms\Core\Exception\SyntaxException; |
||
| 7 | use Ffcms\Core\Helper\FileSystem\File; |
||
| 8 | |||
| 9 | class Antivirus |
||
| 10 | { |
||
| 11 | const EXECUTE_LIMIT = 5; // in seconds |
||
| 12 | const DEPRECATED_TIME = 3600; // actuality of scan list |
||
| 13 | // file extensions to be scanned |
||
| 14 | public $affectedExt = [ |
||
| 15 | '.php', '.php3', '.phtml', |
||
| 16 | '.htm', '.html', |
||
| 17 | '.txt', '.js', '.pl', '.cgi', '.py', '.bash', '.sh', '.ssi', '.inc', '.pm', '.tpl' |
||
| 18 | ]; |
||
| 19 | |||
| 20 | private $signatures; |
||
| 21 | private $beginTime; |
||
| 22 | private $scanFiles; |
||
| 23 | |||
| 24 | private $infected; |
||
| 25 | |||
| 26 | public function __construct() |
||
| 27 | { |
||
| 28 | if (!File::exist('/Private/Antivirus/Signatures.xml')) { |
||
| 29 | throw new NativeException('Antivirus signatures is not founded in: {root}/Private/Antivirus/Signatures.xml'); |
||
| 30 | } |
||
| 31 | |||
| 32 | $this->beginTime = time(); |
||
| 33 | |||
| 34 | $this->signatures = new \DOMDocument(); |
||
| 35 | $this->signatures->load(root . '/Private/Antivirus/Signatures.xml'); |
||
| 36 | |||
| 37 | // list of files is not prepared, 1st iteration |
||
| 38 | if (!File::exist('/Private/Antivirus/ScanFiles.json')) { |
||
| 39 | $this->prepareScanlist(); |
||
| 40 | } |
||
| 41 | |||
| 42 | if (!File::exist('/Private/Antivirus/ScanFiles.json')) { |
||
| 43 | throw new SyntaxException('Directory /Private/Antivirus/ can not be writed!'); |
||
| 44 | } |
||
| 45 | |||
| 46 | $this->scanFiles = json_decode(File::read('/Private/Antivirus/ScanFiles.json')); |
||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 47 | if (File::exist('/Private/Antivirus/Infected.json')) { |
||
| 48 | $this->infected = (array)json_decode(File::read('/Private/Antivirus/Infected.json')); |
||
| 49 | } |
||
| 50 | } |
||
| 51 | |||
| 52 | /** |
||
| 53 | * Make scan of files with preparation and time limit |
||
| 54 | * @return array |
||
| 55 | */ |
||
| 56 | public function make() |
||
| 57 | { |
||
| 58 | foreach ($this->scanFiles as $idx => $file) { |
||
| 59 | $now = time(); |
||
| 60 | // calculate time limit for scan per file |
||
| 61 | if ($now - $this->beginTime >= static::EXECUTE_LIMIT) { |
||
| 62 | break; |
||
| 63 | } |
||
| 64 | |||
| 65 | $this->scanContent($file); |
||
| 66 | unset($this->scanFiles->$idx); |
||
| 67 | } |
||
| 68 | |||
| 69 | // write infected info |
||
| 70 | File::write('/Private/Antivirus/Infected.json', json_encode($this->infected)); |
||
| 71 | // refresh file list to scan ;) |
||
| 72 | File::write('/Private/Antivirus/ScanFiles.json', json_encode($this->scanFiles)); |
||
| 73 | |||
| 74 | return [ |
||
| 75 | 'left' => count(get_object_vars($this->scanFiles)), |
||
| 76 | 'detect' => count($this->infected) |
||
| 77 | ]; |
||
| 78 | } |
||
| 79 | |||
| 80 | /** |
||
| 81 | * Scan signle file via defined $path |
||
| 82 | * @param string $path |
||
| 83 | * @return bool |
||
| 84 | */ |
||
| 85 | private function scanContent($path) |
||
| 86 | { |
||
| 87 | // get file content plain |
||
| 88 | $content = File::read($path); |
||
| 89 | |||
| 90 | // nothing to check |
||
| 91 | if ($content === null || $content === false) { |
||
| 92 | return false; |
||
| 93 | } |
||
| 94 | |||
| 95 | $normalized = $this->normalizeContent($content); |
||
| 96 | |||
| 97 | // list malware signatures |
||
| 98 | $db = $this->signatures->getElementsByTagName('signature'); |
||
| 99 | $detected = false; |
||
| 100 | foreach ($db as $sig) { |
||
| 101 | $sigContent = $sig->nodeValue; |
||
| 102 | $attr = $sig->attributes; |
||
| 103 | $attrId = $attr->getNamedItem('id')->nodeValue; |
||
| 104 | $attrFormat = $attr->getNamedItem('format')->nodeValue; |
||
| 105 | $attrTitle = $attr->getNamedItem('title')->nodeValue; |
||
| 106 | $attrSever = $attr->getNamedItem('sever')->nodeValue; |
||
| 107 | |||
| 108 | switch ($attrFormat) { |
||
| 109 | case 're': |
||
| 110 | if ((preg_match('#(' . $sigContent . ')#smi', $content, $found, PREG_OFFSET_CAPTURE)) || |
||
| 111 | (preg_match('#(' . $sigContent . ')#smi', $normalized, $found, PREG_OFFSET_CAPTURE)) |
||
| 112 | ) { |
||
| 113 | $detected = true; |
||
| 114 | $pos = $found[0][1]; |
||
| 115 | $this->infected[$path][] = [ |
||
| 116 | 'pos' => (int)$pos, |
||
| 117 | 'sigId' => $attrId, |
||
| 118 | 'sigRule' => $sigContent, |
||
| 119 | 'sever' => $attrSever, |
||
| 120 | 'title' => $attrTitle |
||
| 121 | ]; |
||
| 122 | } |
||
| 123 | |||
| 124 | break; |
||
| 125 | case 'const': |
||
| 126 | if ((($pos = strpos($content, $sigContent)) !== false) || |
||
| 127 | (($pos = strpos($normalized, $sigContent)) !== false) |
||
| 128 | ) { |
||
| 129 | $this->infected[$path][] = [ |
||
| 130 | 'pos' => (int)$pos, |
||
| 131 | 'sigId' => $attrId, |
||
| 132 | 'sigRule' => $sigContent, |
||
| 133 | 'sever' => $attrSever, |
||
| 134 | 'title' => $attrTitle |
||
| 135 | ]; |
||
| 136 | $detected = true; |
||
| 137 | } |
||
| 138 | |||
| 139 | break; |
||
| 140 | } |
||
| 141 | } |
||
| 142 | return $detected; |
||
| 143 | } |
||
| 144 | |||
| 145 | /** |
||
| 146 | * Prepare scan list on first run. Scan directory's and save as JSON |
||
| 147 | */ |
||
| 148 | private function prepareScanlist() |
||
| 149 | { |
||
| 150 | $files = (object)File::listFiles(root, $this->affectedExt); |
||
| 151 | File::write('/Private/Antivirus/ScanFiles.json', json_encode($files)); |
||
| 152 | } |
||
| 153 | |||
| 154 | /** |
||
| 155 | * Prepare content, replacing any encoding of it, based on Hex, OctDec or other offset's |
||
| 156 | * @param string $content |
||
| 157 | * @return mixed |
||
| 158 | */ |
||
| 159 | private function normalizeContent($content) |
||
| 160 | { |
||
| 161 | $content = @preg_replace_callback('/\\\\x([a-fA-F0-9]{1,2})/i', 'escapedHexToHex', $content); // strip hex ascii notation |
||
| 162 | $content = @preg_replace_callback('/\\\\([0-9]{1,3})/i', 'escapedOctDec', $content); // strip dec ascii notation |
||
| 163 | $content = preg_replace('/[\'"]\s*?\.\s*?[\'"]/smi', '', $content); // concat fragmented strings |
||
| 164 | $content = preg_replace('|/\*.*?\*/|smi', '', $content); // remove comments to detect fragmented pieces of malware |
||
| 165 | |||
| 166 | return $content; |
||
| 167 | } |
||
| 168 | } |
||
| 169 |