PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.4
Patchstack – WordPress & Plugins Security v2.1.4
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.4, at includes/listener.php

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