PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.21
Patchstack – WordPress & Plugins Security v2.1.21
2.3.7 trunk 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.17 2.1.18 2.1.19 2.1.2 2.1.20 2.1.21 2.1.22 2.1.23 2.1.24 2.1.25 2.1.3 2.1.4 2.1.5 2.1.6 All 49 releases
patchstack / includes / listener.php

listener.php in Patchstack – WordPress & Plugins Security 2.1.21, at includes/listener.php

654 lines 19.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // Do not allow the file to be called directly.
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * This class is used to communicate from the API to the plugin.
10 */
11 class P_Listener extends P_Core {
12
13 /**
14 * Add the actions required to hide the login page.
15 *
16 * @param Patchstack $core
17 * @return void
18 */
19 public function __construct( $core ) {
20 parent::__construct( $core );
21
22 // Only hook into the action if the authentication is set and valid.
23 if ( isset( $_POST['webarx_secret'] ) && $this->verifyToken( $_POST['webarx_secret'] ) ) {
24 add_action( 'init', array( $this, 'handleRequest' ) );
25 }
26 }
27
28 /**
29 * Handle the incoming request.
30 *
31 * @return void
32 */
33 public function handleRequest() {
34 // Loop through all possible actions.
35 foreach ( array(
36 'webarx_remote_users' => 'listUsers',
37 'webarx_firewall_switch' => 'switchFirewallStatus',
38 'webarx_wordpress_upgrade' => 'wordpressCoreUpgrade',
39 'webarx_theme_upgrade' => 'themeUpgrade',
40 'webarx_plugins_upgrade' => 'pluginsUpgrade',
41 'webarx_plugins_toggle' => 'pluginsToggle',
42 'webarx_plugins_delete' => 'pluginsDelete',
43 'webarx_get_options' => 'getAvailableOptions',
44 'webarx_set_options' => 'saveOptions',
45 'webarx_refresh_rules' => 'refreshRules',
46 'webarx_get_firewall_bans' => 'getFirewallBans',
47 'webarx_firewall_unban_ip' => 'unbanFirewallIp',
48 'webarx_upload_software' => 'uploadSoftware',
49 'webarx_upload_logs' => 'uploadLogs',
50 'webarx_send_ping' => 'sendPing',
51 'webarx_login_bans' => 'getLoginBans',
52 'webarx_unban_login' => 'unbanLogin',
53 'webarx_debug_info' => 'debugInfo'
54 ) as $key => $action ) {
55 // Special case for Patchstack plugin upgrade.
56 if ( isset( $_POST[ $key ] ) ) {
57 $this->$action();
58 }
59 }
60 }
61
62 /**
63 * Determine if the provided secret hash equals the sha1 of the private id and key.
64 *
65 * @param string $secret Hash that is sent from our API.
66 * @return boolean
67 */
68 public function verifyToken( $secret ) {
69 $id = get_option( 'patchstack_clientid' );
70 $key = get_option( 'patchstack_secretkey' );
71
72 if ( empty( $id ) || empty ( $key ) || strlen( $secret ) != 40) {
73 return false;
74 }
75
76 return hash_equals( sha1( $id . $key ), $secret );
77 }
78
79 /**
80 * Determine if given action succeded or not, then return the appropriate message.
81 *
82 * @param mixed $thing
83 * @param string $success
84 * @param string $fail
85 * @return void
86 */
87 private function returnResults( $thing, $success = '', $fail = '' ) {
88 if ( ! is_wp_error( $thing ) && $thing !== false ) {
89 wp_send_json( array( 'success' => $success ) );
90 }
91
92 wp_send_json( array( 'error' => $fail ) );
93 }
94
95 /**
96 * Send a ping back to the API.
97 *
98 * @return void
99 */
100 private function sendPing() {
101 do_action( 'patchstack_send_ping' );
102 wp_send_json(
103 array(
104 'firewall' => $this->get_option( 'patchstack_basic_firewall' ) == 1,
105 )
106 );
107 }
108
109 /**
110 * Get list of all users on WordPress
111 *
112 * @return void
113 */
114 private function listUsers() {
115 // Only fetch data we actually need.
116 $users = get_users( array( 'role__in' => array( 'administrator', 'editor', 'author', 'contributor' ) ) );
117 $roles = wp_roles();
118 $roles = $roles->get_names();
119 $data = array();
120
121 // Loop through all users.
122 foreach ( $users as $user ) {
123
124 // Get text friendly version of the role.
125 $text = '';
126 foreach ( $user->roles as $role ) {
127 if ( isset( $roles[ $role ] ) ) {
128 $text .= $roles[ $role ] . ', ';
129 } else {
130 $text .= $role . ', ';
131 }
132 }
133
134 // Push to array that we will eventually output.
135 array_push(
136 $data,
137 array(
138 'id' => $user->data->ID,
139 'username' => $user->data->user_login,
140 'email' => $user->data->user_email,
141 'roles' => substr( $text, 0, -2 ),
142 )
143 );
144 }
145
146 wp_send_json( array( 'users' => $data ) );
147 }
148
149 /**
150 * Switch the firewall status from on to off or off to on.
151 *
152 * @return string
153 */
154 private function switchFirewallStatus() {
155 $state = $this->get_option( 'patchstack_basic_firewall' ) == 1;
156 update_option( 'patchstack_basic_firewall', $state == 1 ? 0 : 1 );
157 $this->returnResults( null, 'Firewall ' . ( $state == 1 ? 'disabled' : 'enabled' ) . '.', null );
158 }
159
160 /**
161 * Upgrade the core of WordPress.
162 *
163 * @return string|void
164 */
165 private function wordpressCoreUpgrade() {
166 @set_time_limit( 180 );
167
168 // Get the core update info.
169 wp_version_check();
170 $core = get_site_transient( 'update_core' );
171
172 // Any updates available?
173 if ( ! isset( $core->updates ) ) {
174 $this->returnResults( false, null, 'No update available at this time.' );
175 }
176
177 // Are we on the latest version already?
178 if ( $core->updates[0]->response == 'latest' ) {
179 $this->returnResults( false, null, 'Site is already running the latest version available.' );
180 }
181
182 // Require some libraries and attempt the upgrade.
183 @include_once ABSPATH . '/wp-admin/includes/admin.php';
184 @include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
185 $skin = new Automatic_Upgrader_Skin();
186 $upgrader = new Core_Upgrader( $skin );
187 $result = $upgrader->upgrade(
188 $core->updates[0],
189 array(
190 'attempt_rollback' => true,
191 'do_rollback' => true,
192 'allow_relaxed_file_ownership' => true,
193 )
194 );
195 if ( ! $result ) {
196 $this->returnResults( false, null, 'The WordPress core could not be upgraded, most likely because of invalid filesystem connection information.' );
197 }
198
199 // Synchronize again with the API.
200 do_action( 'patchstack_send_software_data' );
201 $this->returnResults( $results, 'WordPress core has been upgraded.' );
202 }
203
204 /**
205 * Upgrade a WordPress theme.
206 *
207 * @return string|void
208 */
209 private function themeUpgrade() {
210 if ( !isset( $_POST['webarx_theme_upgrade'] ) ) {
211 return;
212 }
213
214 @set_time_limit( 180 );
215
216 // Require some files we need to execute the upgrade.
217 $theme = wp_filter_nohtml_kses( $_POST['webarx_theme_upgrade'] );
218 @include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
219 if ( file_exists( ABSPATH . 'wp-admin/includes/class-theme-upgrader.php' ) ) {
220 @include_once ABSPATH . 'wp-admin/includes/class-theme-upgrader.php';
221 }
222 @include_once ABSPATH . 'wp-admin/includes/misc.php';
223 @include_once ABSPATH . 'wp-admin/includes/file.php';
224
225 // Upgrade the theme.
226 $skin = new Automatic_Upgrader_Skin();
227 $upgrader = new Theme_Upgrader( $skin );
228 $result = $upgrader->upgrade( $theme, array( 'allow_relaxed_file_ownership' => true ) );
229 if ( ! $result ) {
230 $this->returnResults( false, null, 'The theme could not be upgraded, most likely because of invalid filesystem connection information.' );
231 }
232
233 // Synchronize again with the API.
234 do_action( 'patchstack_send_software_data' );
235 $this->returnResults( null, 'The theme has been updated successfully.' );
236 }
237
238 /**
239 * Upgrade a batch of plugins at once.
240 *
241 * @return string|void
242 */
243 private function pluginsUpgrade() {
244 if (!isset( $_POST['webarx_plugins_upgrade'] ) ) {
245 return;
246 }
247
248 @set_time_limit( 180 );
249
250 // Must have a valid number of plugins received to upgrade.
251 $plugins = wp_filter_nohtml_kses( $_POST['webarx_plugins_upgrade'] );
252 $plugins = explode( '|', $plugins );
253 if ( count( $plugins ) == 0 ) {
254 $this->returnResults( false, null, 'No valid plugin names have been given.' );
255 }
256
257 // Require some files we need to execute the upgrade.
258 @include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
259 if ( file_exists( ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php' ) ) {
260 @include_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';
261 }
262 @include_once ABSPATH . 'wp-admin/class-automatic-upgrader-skin.php';
263
264 @include_once ABSPATH . 'wp-admin/includes/plugin.php';
265 @include_once ABSPATH . 'wp-admin/includes/misc.php';
266 @include_once ABSPATH . 'wp-admin/includes/file.php';
267 @include_once ABSPATH . 'wp-admin/includes/template.php';
268 @wp_update_plugins();
269 $all_plugins = get_plugins();
270
271 // New array with all available plugins and the ones we want to upgrade.
272 $upgrade = array();
273 foreach ( $all_plugins as $path => $data ) {
274 $t = explode( '/', $path );
275 if ( in_array( $t[0], $plugins ) ) {
276 array_push( $upgrade, $path );
277 }
278 }
279
280 // Don't continue if we have no valid plugins to upgrade.
281 if ( count( $upgrade ) == 0 ) {
282 $this->returnResults( false, null, 'No valid plugin names have been given.' );
283 }
284
285 // Upgrade the plugins.
286 $skin = new Automatic_Upgrader_Skin();
287 $upgrader = new Plugin_Upgrader( $skin );
288 $result = $upgrader->bulk_upgrade( $upgrade, array( 'allow_relaxed_file_ownership' => true ) );
289 if ( ! $result ) {
290 $this->returnResults( false, null, 'The plugins could not be upgraded, most likely because of invalid filesystem connection information.' );
291 }
292
293 // Synchronize again with the API.
294 do_action( 'patchstack_send_software_data' );
295 $this->returnResults( null, 'The plugins have been updated successfully.' );
296 }
297
298 /**
299 * Toggle the state of a batch of plugin to activated or de-activated.
300 *
301 * @return string|void
302 */
303 private function pluginsToggle() {
304 if (!isset( $_POST['webarx_plugins'], $_POST['webarx_plugins_toggle'] ) ) {
305 return;
306 }
307
308 @set_time_limit( 180 );
309
310 // Must have a valid number of plugins received to toggle.
311 $plugins = wp_filter_nohtml_kses( $_POST['webarx_plugins'] );
312 $plugins = explode( '|', $plugins );
313 $state = $_POST['webarx_plugins_toggle'] == 'on' ? 'on' : 'off';
314 if ( count( $plugins ) == 0 ) {
315 $this->returnResults( false, null, 'No valid plugin names have been given.' );
316 }
317
318 @include_once ABSPATH . 'wp-admin/includes/plugin.php';
319 $all_plugins = get_plugins();
320
321 // New array with all available plugins and the ones we want to toggle.
322 $toggle = array();
323 foreach ( $all_plugins as $path => $data ) {
324 $t = explode( '/', $path );
325
326 // Don't continue if the plugin does not exist locally.
327 if ( ! in_array( $t[0], $plugins ) ) {
328 continue;
329 }
330
331 // If plugin should be turned on, check if it's already turned on first.
332 if ( $state == 'on' && ! is_plugin_active( $path ) ) {
333 array_push( $toggle, $path );
334 }
335
336 // If plugin should be turned off, check if it's already turned off first.
337 if ( $state == 'off' && is_plugin_active( $path ) ) {
338 array_push( $toggle, $path );
339 }
340 }
341
342 // Don't continue if we have no valid plugins to toggle..
343 if ( count( $toggle ) == 0 ) {
344 $this->returnResults( false, null, 'The plugins are already turned ' . $state . '.' );
345 }
346
347 // Turn the plugins on or off?
348 if ( $state == 'on' ) {
349 activate_plugins( $toggle );
350 }
351
352 if ( $state == 'off' ) {
353 deactivate_plugins( $toggle );
354 }
355
356 // Synchronize again with the API.
357 do_action( 'patchstack_send_software_data' );
358 $this->returnResults( null, 'The ' . ( count( $toggle ) == 1 ? 'plugin has' : 'plugins have' ) . ' been successfully turned ' . $state . '.' );
359 }
360
361 /**
362 * Delete a batch of plugins.
363 *
364 * @return string|void
365 */
366 private function pluginsDelete() {
367 if (!isset( $_POST['webarx_plugins'] ) ) {
368 return;
369 }
370
371 @set_time_limit( 180 );
372
373 // Must have a valid number of plugins received to toggle.
374 $plugins = wp_filter_nohtml_kses( $_POST['webarx_plugins'] );
375 $plugins = explode( '|', $plugins );
376 if ( count( $plugins ) == 0 ) {
377 $this->returnResults( false, null, 'No valid plugin names have been given.' );
378 }
379
380 @include_once ABSPATH . 'wp-admin/includes/file.php';
381 @include_once ABSPATH . 'wp-admin/includes/plugin.php';
382 $all_plugins = get_plugins();
383
384 // New array with all available plugins and the ones we want to toggle.
385 $delete = array();
386 foreach ( $all_plugins as $path => $data ) {
387 $t = explode( '/', $path );
388
389 // Don't continue if the plugin does not exist locally.
390 if ( ! in_array( $t[0], $plugins ) ) {
391 continue;
392 }
393
394 array_push( $delete, $path );
395 }
396
397 // Don't continue if we have no valid plugins to toggle..
398 if ( count( $delete ) == 0 ) {
399 $this->returnResults( false, null, 'No valid plugins to delete.' );
400 }
401
402 @deactivate_plugins( $delete );
403 @delete_plugins( $delete );
404
405 // Synchronize again with the API.
406 do_action( 'patchstack_send_software_data' );
407 $this->returnResults( null, 'The plugins have been successfully deleted.' );
408 }
409
410 /**
411 * Save received options.
412 *
413 * @return void
414 */
415 private function saveOptions() {
416 if ( ! isset( $_POST['webarx_set_options'], $_POST['webarx_secret'] ) ) {
417 exit;
418 }
419
420 // Get the received options.
421 $options = json_decode( base64_decode( $_POST['webarx_set_options'] ), true );
422 if ( ! $options || count( $options ) == 0 ) {
423 exit;
424 }
425
426 // Loop through the options and update their value.
427 $exclude_filter = array('patchstack_firewall_custom_rules');
428 foreach ( $options as $key => $value ) {
429 if ( array_key_exists( $key, $this->plugin->admin_options->options ) ) {
430
431 // Some options should not be filtered and could cause unexpected behavior if they are filtered.
432 if ( ! in_array( $key, $exclude_filter ) ) {
433 $value = map_deep($value, 'wp_filter_nohtml_kses');
434 }
435
436 update_option( $key, $value, true );
437 }
438 }
439
440 $this->returnResults( null, 'Plugin options has been updated.' );
441 }
442
443 /**
444 * Return list of keys and values of Patchstack options.
445 *
446 * @return array
447 */
448 private function getAvailableOptions() {
449 // Get all options and filter by the Patchstack prefix.
450 global $wpdb;
451 $options = $wpdb->get_results( "SELECT option_name, option_value FROM " . $wpdb->options . " WHERE option_name LIKE 'patchstack_%'" );
452 $settings = array();
453 $found = array();
454 foreach ( $options as $option ) {
455 array_push( $found, $option->option_name );
456 $settings[] = (array) $option;
457 }
458
459 // Check for potential missing options and add them to the output.
460 foreach( array( 'patchstack_firewall_custom_rules' ) as $slug ) {
461 if ( ! isset ( $found[$slug] ) ) {
462 $settings[] = array(
463 'option_name' => $slug,
464 'option_value' => $this->get_option( $slug, '' )
465 );
466 }
467 }
468
469 // Add custom values which aren't directly available from the options table.
470 // User roles available for whitelisting.
471 $roles = wp_roles();
472 $roles = $roles->get_names();
473 $roles_available = array();
474 foreach ( $roles as $key => $role ) {
475 $roles_available[ $key ] = $role;
476 }
477 $settings[] = array(
478 'option_name' => 'patchstack_basic_firewall_roles_available',
479 'option_value' => serialize( $roles_available ),
480 );
481
482 // Whether or not auto-updates are disabled in the code.
483 $settings[] = array(
484 'option_name' => 'patchstack_auto_updates_disabled',
485 'option_value' => defined( 'AUTOMATIC_UPDATER_DISABLED' ) && AUTOMATIC_UPDATER_DISABLED,
486 );
487
488 wp_send_json( $settings );
489 }
490
491 /**
492 * Pull firewall rules from the API.
493 *
494 * @return void
495 */
496 private function refreshRules() {
497 do_action( 'patchstack_post_dynamic_firewall_rules' );
498 $this->returnResults( null, 'Firewall rules have been refreshed.' );
499 }
500
501 /**
502 * Get a list of IP addresses that are currently banned by the firewall.
503 *
504 * @return void
505 */
506 private function getFirewallBans() {
507 // Calculate block time.
508 $minutes = (int) $this->get_option( 'patchstack_autoblock_minutes', 30 );
509 $timeout = (int) $this->get_option( 'patchstack_autoblock_blocktime', 60 );
510 if ( empty( $minutes ) || empty( $timeout ) ) {
511 $time = 30 + 60;
512 } else {
513 $time = $minutes + $timeout;
514 }
515
516 global $wpdb;
517 $results = $wpdb->get_results(
518 $wpdb->prepare( 'SELECT ip FROM ' . $wpdb->prefix . "patchstack_firewall_log WHERE apply_ban = 1 AND log_date >= ('" . current_time( 'mysql' ) . "' - INTERVAL %d MINUTE) GROUP BY ip", array( $time ) ),
519 OBJECT
520 );
521
522 $out = array();
523 foreach ( $results as $result ) {
524 if ( isset( $result->ip ) ) {
525 array_push( $out, $result->ip );
526 }
527 }
528
529 wp_send_json( $out );
530 }
531
532 /**
533 * Unban a specific IP address from the firewall.
534 *
535 * @return void
536 */
537 private function unbanFirewallIp() {
538 if ( ! isset( $_POST['webarx_ip'] ) || !filter_var( $_POST['webarx_ip'], FILTER_VALIDATE_IP ) ) {
539 return;
540 }
541
542 global $wpdb;
543 $wpdb->query( $wpdb->prepare( 'UPDATE ' . $wpdb->prefix . 'patchstack_firewall_log SET apply_ban = 0 WHERE ip = %s', array( $_POST['webarx_ip'] ) ) );
544 $this->returnResults( null, 'The IP has been unbanned.' );
545 }
546
547 /**
548 * Send all current software on the WordPress site to the API.
549 *
550 * @return void
551 */
552 private function uploadSoftware() {
553 do_action( 'patchstack_send_software_data' );
554 $this->returnResults( null, 'The software data has been sent to the API.' );
555 }
556
557 /**
558 * Upload the firewall and activity logs.
559 *
560 * @return void
561 */
562 private function uploadLogs() {
563 do_action( 'patchstack_send_hacker_logs' );
564 do_action( 'patchstack_send_event_logs' );
565 $this->returnResults( null, 'The logs have been sent to the API.' );
566 }
567
568 /**
569 * Get the currently banned IP addresses from the login page.
570 *
571 * @return void
572 */
573 private function getLoginBans() {
574 // Calculate block time.
575 $minutes = (int) $this->get_option( 'patchstack_anti_bruteforce_minutes', 30 );
576 $timeout = (int) $this->get_option( 'patchstack_anti_bruteforce_blocktime', 60 );
577 if ( empty( $minutes ) || empty( $timeout ) ) {
578 $time = 30 + 60;
579 } else {
580 $time = $minutes + $timeout;
581 }
582
583 // Check if X failed login attempts were made.
584 global $wpdb;
585 $results = $wpdb->get_results(
586 $wpdb->prepare( 'SELECT id, ip, date FROM ' . $wpdb->prefix . "patchstack_event_log WHERE action = 'failed login' AND date >= ('" . current_time( 'mysql' ) . "' - INTERVAL %d MINUTE) GROUP BY ip HAVING COUNT(ip) >= %d ORDER BY date DESC", array( $time, $this->get_option( 'patchstack_anti_bruteforce_attempts', 10 ) ) ),
587 OBJECT
588 );
589
590 // Return the banned IP addresses.
591 wp_send_json( array( 'banned' => $results ) );
592 }
593
594 /**
595 * Unban a banned login IP address.
596 *
597 * @return void
598 */
599 private function unbanLogin() {
600 if ( ! isset( $_POST['id'], $_POST['type'] ) || !ctype_digit( $_POST['id'] ) ) {
601 exit;
602 }
603
604 global $wpdb;
605
606 // Unblock the IP; delete the logs of the IP.
607 if ( $_POST['type'] == 'unblock' ) {
608 // First get the IP address to unblock.
609 $result = $wpdb->get_results(
610 $wpdb->prepare( 'SELECT ip FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id = %d', array( (int) $_POST['id'] ) )
611 );
612
613 // Unblock the IP address.
614 if ( isset( $result[0], $result[0]->ip ) && filter_var( $result[0]->ip, FILTER_VALIDATE_IP ) ) {
615 $wpdb->query(
616 $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE ip = %s', array( $result[0]->ip ) )
617 );
618 }
619 }
620
621 // Unblock and whitelist the IP.
622 if ( $_POST['type'] == 'unblock_whitelist' ) {
623 // First get the IP address to whitelist.
624 $result = $wpdb->get_results(
625 $wpdb->prepare( 'SELECT ip FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id = %d', array( (int) $_POST['id'] ) )
626 );
627
628 // Whitelist and unblock the IP address.
629 if ( isset( $result[0], $result[0]->ip ) && filter_var( $result[0]->ip, FILTER_VALIDATE_IP ) ) {
630 update_option( 'patchstack_login_whitelist', $this->get_option( 'patchstack_login_whitelist', '' ) . "\n" . $result[0]->ip );
631 $wpdb->query(
632 $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE ip = %s', array( $result[0]->ip ) )
633 );
634 }
635 }
636
637 $this->returnResults( null, 'The unban has been processed.' );
638 }
639
640 /**
641 * Get information for debugging purposes.
642 *
643 * @return void
644 */
645 private function debugInfo() {
646 $debug = array(
647 'server' => $_SERVER,
648 'php' => phpversion()
649 );
650
651 wp_send_json( $debug );
652 }
653 }
654