PluginProbe
WebTotem Security / 3.0.1
WebTotem Security v3.0.1
3.0.1 3.0.0 trunk 1.0 1.1 1.2 1.3 1.3.1 1.3.2 1.3.3 2.0 2.1 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.1 2.2.2 2.2.3 2.2.4 All 109 releases
wt-security / lib / modules / logs / Scan.php

Scan.php in WebTotem Security 3.0.1, at lib/modules/logs/Scan.php

457 lines 14.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('WEBTOTEM_INIT') || WEBTOTEM_INIT !== true) {
4 if (!headers_sent()) {
5 header('HTTP/1.1 403 Forbidden');
6 }
7 die("Protected By WebTotem!");
8 }
9
10 require_once 'FileInfo.php';
11
12 /**
13 * WebTotem scan class for WordPress.
14 */
15 class WebTotemScan
16 {
17 /**
18 *
19 */
20 public static function initialize()
21 {
22 if (WebTotemOption::getOption('scan_init')) {
23 $time_start = microtime(true);
24
25 $max_execution_time = ini_get('max_execution_time');
26 if ($max_execution_time < 300) {
27 if (function_exists('set_time_limit')) @set_time_limit(300);
28 @ini_set('max_execution_time', '300');
29 }
30 $max_execution_time = ini_get('max_execution_time');
31
32 $scan_temp = json_decode(WebTotemOption::getOption('scan_temp'), true) ?: [];
33
34 if (empty($scan_temp)) {
35 $scan_temp = [
36 'current_scan' => 'scanDB',
37 'need_to_scan' => [],
38 'links' => [],
39 ];
40 }
41
42 $scan_running = json_decode(WebTotemOption::getOption('scan_running'), true) ?: ['status' => 'stop'];
43 $seconds_from_previous_start = $time_start - ($scan_running['time_start'] ?? $time_start);
44 if ($scan_running['status'] == 'stop' || $seconds_from_previous_start > $max_execution_time) {
45
46 WebTotemOption::setOptions(['scan_running' => ['status' => 'run', 'time_start' => $time_start]]);
47
48 if ($scan_temp['current_scan'] == 'scanDB') {
49 self::scanDB($scan_temp, $max_execution_time, $time_start);
50 WebTotemOption::setOptions(['scan_running' => ['status' => 'stop']]);
51 return;
52 }
53
54 if ($scan_temp['current_scan'] == 'scanFiles') {
55 self::scanFiles($scan_temp, $max_execution_time, $time_start);
56 WebTotemOption::setOptions(['scan_running' => ['status' => 'stop']]);
57 return;
58 }
59
60 if ($scan_temp['current_scan'] == 'checkConfidentialFiles') {
61 self::checkConfidentialFiles($scan_temp, $max_execution_time, $time_start);
62 WebTotemOption::setOptions(['scan_running' => ['status' => 'stop']]);
63 return;
64 }
65
66 if ($scan_temp['current_scan'] == 'crawler') {
67 WebTotemCrawler::init($scan_temp);
68 WebTotemOption::setOptions(['scan_running' => ['status' => 'stop']]);
69 return;
70 }
71
72 }
73
74 }
75
76 }
77
78 /**
79 * Database scanning, search for links, scripts and iframe tags,
80 * formation of an array of data on them
81 */
82 public static function scanDB($scan_temp, $max_execution_time, $time_start)
83 {
84 $tables = $scan_temp['need_to_scan'] ?: self::getTables();
85 $links = $scan_temp['links'] ?: [];
86
87 $needles = ['%href%', '%<iframe%', '%.js%'];
88
89 foreach ($tables['posts'] as $key => $table) {
90 $rows = self::getRows($table, ['post_content' => $needles], 'guid');
91
92 foreach ($rows as $row) {
93 $links[] = ['link' => $row->guid, 'page' => __('DB scan', 'wtotem'), 'is_internal' => true];;
94 }
95
96 unset($tables['posts'][$key]);
97
98 $time_end = microtime(true);
99 if (($time_end - $time_start) > $max_execution_time - 5) {
100 WebTotemOption::setOptions([
101 'scan_temp' => [
102 'current_scan' => 'scanDB',
103 'need_to_scan' => $tables,
104 'links' => $links,
105 ]
106 ]);
107 return;
108 }
109
110 }
111
112 foreach ($tables['comments'] as $relation => $table) {
113 $rows = self::getRows($table, ['comment_content' => $needles], 'guid');
114
115 $posts_ids = array_column($rows, 'comment_post_ID');
116 $posts_rows = self::getRows($relation, ['ID' => $posts_ids]);
117 $posts_rows = WebTotem::arrayMapIndex(WebTotem::convertObjectToArray($posts_rows), 'ID');
118
119 foreach ($rows as $row) {
120 $links[] = ['link' => $posts_rows[$row->comment_post_ID]['guid'], 'page' => __('DB scan', 'wtotem'), 'is_internal' => true];
121 }
122
123 unset($tables['comments'][$relation]);
124
125 $time_end = microtime(true);
126 if (($time_end - $time_start) > $max_execution_time - 5) {
127 WebTotemOption::setOptions([
128 'scan_temp' => [
129 'current_scan' => 'scanDB',
130 'need_to_scan' => $tables,
131 'links' => $links,
132 ]
133 ]);
134 return;
135 }
136 }
137
138 WebTotemOption::setOptions([
139 'scan_temp' => [
140 'current_scan' => 'scanFiles',
141 'need_to_scan' => [],
142 'links' => $links,
143 ]
144 ]);
145
146 }
147
148 /**
149 * Getting values from the table.
150 *
151 * @param array $options
152 * Array options.
153 * @param string $table
154 * Table name.
155 * @param string $fields
156 * Required fields.
157 *
158 * @return array
159 */
160 private static function getRows($table, $options = false, $fields = false)
161 {
162 global $wpdb;
163 $table_name = self::add_prefix($table);
164
165 if ($options) {
166 foreach ($options as $key => $value) {
167 if (is_array($value)) {
168 foreach ($value as $val) {
169 $where[] = $key . " LIKE '" . $val . "'";
170 }
171 } else {
172 $where[] = $key . " LIKE '" . $value . "'";
173 }
174 }
175 }
176 $where = isset($where) ? 'WHERE (' . implode(' OR ', $where) . ')' : '';
177 if (strpos($table, 'posts') !== false) {
178 $where .= $where ? " AND " : "WHERE ";
179 $where .= "post_status = 'publish'";
180 }
181
182 $fields = $fields ?: '*';
183 $rows = $wpdb->get_results("SELECT $fields FROM $table_name $where");
184
185 return (array)$rows ?: [];
186 }
187
188 /**
189 * Get an array of tables
190 */
191 private static function getTables()
192 {
193 $tables = [
194 'posts' => [],
195 'comments' => []
196 ];
197
198 if (WebTotem::isMultiSite()) {
199 $blogs = self::getRows(self::add_prefix('blogs'));
200 foreach ($blogs as $blog) {
201 $tables['posts'][] = $blog['blog_id'] . '_posts';
202 $tables['comments'][$blog['blog_id'] . '_posts'] = $blog['blog_id'] . '_comments';
203 }
204 }
205 return $tables;
206 }
207
208 /**
209 * Returns the table with the site prefix added.
210 *
211 * @param string $table
212 * Table name.
213 * @return string
214 */
215 public static function add_prefix($table)
216 {
217 global $wpdb;
218 return $wpdb->prefix . $table;
219 }
220
221 /**
222 * Files scanning, search for links, scripts and iframe tags,
223 * formation of an array of data on them
224 */
225 public static function scanFiles($scan_temp, $max_execution_time, $time_start)
226 {
227
228 $tree = $scan_temp['need_to_scan'] ?? [];
229 $links = $scan_temp['links'] ?? [];
230
231 $site_url = get_site_url();
232 $fileInfo = new WebTotemFileInfo();
233 $abspath = ABSPATH;
234
235 if (empty($tree)) {
236 // Adding files of active plugins
237 if (WebTotem::isMultiSite()) {
238 $all_plugs = array_keys(get_site_option('active_sitewide_plugins'));
239 } else {
240 $all_plugs = get_option('active_plugins');
241 }
242 foreach ($all_plugs as $value) {
243 $plugin = explode('/', $value);
244 $tree = array_merge($tree, $fileInfo->getDirectoryTree(WP_PLUGIN_DIR . '/' . $plugin[0]));
245 }
246
247 // Adding files of active theme
248 $tree = array_merge($tree, $fileInfo->getDirectoryTree(get_template_directory()));
249 }
250
251 foreach ($tree as $key => $file_path) {
252 $content = $fileInfo::fileContent($file_path);
253 if (self::hasMatches($content)) {
254 $link = $site_url . str_replace($abspath, '/', $file_path);
255 $links[] = ['link' => $link, 'page' => __('File scan', 'wtotem'), 'is_internal' => true];
256 }
257 unset($tree[$key]);
258
259 $time_end = microtime(true);
260 if (($time_end - $time_start) > $max_execution_time - 5) {
261 WebTotemOption::setOptions([
262 'scan_temp' => [
263 'current_scan' => 'scanFiles',
264 'need_to_scan' => $tree,
265 'links' => $links,
266 ]
267 ]);
268 return;
269 }
270
271 }
272
273 WebTotemOption::setOptions([
274 'scan_temp' => [
275 'current_scan' => 'checkConfidentialFiles',
276 'need_to_scan' => [],
277 'ready_to_save' => false,
278 'links' => $links,
279 ]
280 ]);
281 }
282
283
284 /**
285 * Get matches.
286 *
287 * @param string $content
288 *
289 * @return bool
290 */
291 private static function hasMatches($content)
292 {
293 $pattern = '/(<a.*?href=["\'](([\da-z\.-\/]+)([\/\w\.-\?\%\&]*)*\/?)["\'].*?>|<script.*?src=["\'](.*?)["\'].*?>|<iframe.*?src=["\'](.*?)["\'].*?>|onclick="[^"]*location[^"][^\'"]+\'([^\']+)\')/i';
294 if (preg_match($pattern, $content)) {
295 return true;
296 }
297 return false;
298 }
299
300 /**
301 * Files scanning, search for confidential files.
302 */
303 public static function checkConfidentialFiles($scan_temp, $max_execution_time, $time_start)
304 {
305
306 $files = $scan_temp['need_to_scan'] ?? [];
307 $files_data = $scan_temp['confidential_files'] ?? [];
308 $root_path = ABSPATH;
309
310 if (empty($files) and !$scan_temp['ready_to_save']) {
311 $patterns = [
312 '.user.ini',
313 'wp-config.php.bak',
314 'wp-config.php.bak.a2',
315 'wp-config.php.swo',
316 'wp-config.php.save',
317 'wp-config.php~',
318 'wp-config.old',
319 '.wp-config.php.swp',
320 'wp-config.bak',
321 'wp-config.save',
322 'wp-config.php_bak',
323 'wp-config.php.swp',
324 'wp-config.php.old',
325 'wp-config.php.original',
326 'wp-config.php.orig',
327 'wp-config.txt',
328 'wp-config.original',
329 'wp-config.orig',
330 '*.bak',
331 '*.back',
332 '*.backup',
333 '*.old',
334 ];
335
336 $mask = implode(',', $patterns);
337 $files = self::glob_tree_search($root_path, '{' . $mask . '}', false);
338 $files = array_merge(self::glob_tree_search($root_path . '/wp-content/', '{' . $mask . '}'), $files);
339 }
340
341 foreach ($files as $file_path) {
342 $url = site_url(str_replace($root_path, '', $file_path));
343
344 if (WebTotem::isPubliclyAccessible($url, $file_path)) {
345 $array = explode(DIRECTORY_SEPARATOR, $file_path);
346 $name = array_pop($array);
347 $files_data[] = [
348 'path' => $file_path,
349 'name' => $name,
350 'size' => filesize($file_path),
351 'modified_at' => date("Y-m-d H:i:s", filectime($file_path)),
352 'url' => $url,
353 ];
354 }
355
356 $time_end = microtime(true);
357 if (($time_end - $time_start) > $max_execution_time - 5) {
358 WebTotemOption::setOptions([
359 'scan_temp' => [
360 'current_scan' => 'checkConfidentialFiles',
361 'need_to_scan' => $files,
362 'links' => $scan_temp['links'],
363 'confidential_files' => $files_data,
364 ]
365 ]);
366 return;
367 }
368
369 }
370
371 if ($scan_temp['ready_to_save']) {
372 if ($files_data) {
373 self::saveData($files_data);
374 }
375 } else {
376 WebTotemOption::setOptions([
377 'scan_temp' => [
378 'current_scan' => 'checkConfidentialFiles',
379 'need_to_scan' => [],
380 'links' => $scan_temp['links'],
381 'ready_to_save' => true,
382 'confidential_files' => $files_data,
383 ]
384 ]);
385 return;
386 }
387
388 WebTotemOption::setOptions([
389 'scan_temp' => [
390 'current_scan' => 'crawler',
391 'need_to_scan' => [],
392 'ready_to_save' => false,
393 'links' => $scan_temp['links'],
394 'confidential_files' => [],
395 ]
396 ]);
397
398 }
399
400 /**
401 * Save data.
402 *
403 * @param array $data
404 * Array matches data.
405 */
406 private static function saveData($data)
407 {
408
409 WebTotemDB::deleteData([], 'confidential_files');
410 $values = '';
411 foreach ($data as $file) {
412 $values .= sprintf("('%s','%s','%s','%s','%s','%s'),",
413 date("Y-m-d H:i:s"),
414 urlencode($file['path']),
415 urlencode($file['name']),
416 $file['size'],
417 $file['modified_at'],
418 $file['url']
419 );
420 }
421
422 $values = substr_replace($values, ";", -1);
423
424 $columns = '(created_at, path, name, size, modified_at, url)';
425
426 WebTotemDB::setRows('confidential_files', $columns, $values);
427 }
428
429 /**
430 * Search through all subdirectories using recursion.
431 *
432 * @param string $path
433 * The initial directory of the search.
434 * @param string $mask
435 * Search mask.
436 *
437 * @return array
438 * Array of file paths found by mask.
439 */
440 public static function glob_tree_search($path, $mask, $recursively = true)
441 {
442 $out = [];
443 foreach (glob($path . $mask, GLOB_BRACE) as $file_path) {
444 $out[] = $file_path;
445 }
446
447 if ($recursively) {
448 foreach (glob($path . '/*', GLOB_ONLYDIR) as $dir) {
449 $out = array_merge($out, self::glob_tree_search($dir, $mask));
450 }
451 }
452
453 return $out;
454 }
455
456 }
457