PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.13
Patchstack – WordPress & Plugins Security v2.1.13
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.13, at includes/listener.php

658 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 );
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 $options = wp_load_alloptions();
451 $settings = array();
452 $found = array();
453 foreach ( $options as $slug => $value ) {
454 if ( strpos( $slug, 'patchstack_' ) !== false ) {
455 array_push( $found, $slug );
456 $settings[] = array(
457 'option_name' => $slug,
458 'option_value' => $value
459 );
460 }
461 }
462
463 // Check for potential missing options and add them to the output.
464 foreach( array( 'patchstack_firewall_custom_rules' ) as $slug ) {
465 if ( ! isset ( $found[$slug] ) ) {
466 $settings[] = array(
467 'option_name' => $slug,
468 'option_value' => $this->get_option( $slug, '' )
469 );
470 }
471 }
472
473 // Add custom values which aren't directly available from the options table.
474 // User roles available for whitelisting.
475 $roles = wp_roles();
476 $roles = $roles->get_names();
477 $roles_available = array();
478 foreach ( $roles as $key => $role ) {
479 $roles_available[ $key ] = $role;
480 }
481 $settings[] = array(
482 'option_name' => 'patchstack_basic_firewall_roles_available',
483 'option_value' => serialize( $roles_available ),
484 );
485
486 // Whether or not auto-updates are disabled in the code.
487 $settings[] = array(
488 'option_name' => 'patchstack_auto_updates_disabled',
489 'option_value' => defined( 'AUTOMATIC_UPDATER_DISABLED' ) && AUTOMATIC_UPDATER_DISABLED,
490 );
491
492 wp_send_json( $settings );
493 }
494
495 /**
496 * Pull firewall rules from the API.
497 *
498 * @return void
499 */
500 private function refreshRules() {
501 do_action( 'patchstack_post_dynamic_firewall_rules' );
502 $this->returnResults( null, 'Firewall rules have been refreshed.' );
503 }
504
505 /**
506 * Get a list of IP addresses that are currently banned by the firewall.
507 *
508 * @return void
509 */
510 private function getFirewallBans() {
511 // Calculate block time.
512 $minutes = (int) $this->get_option( 'patchstack_autoblock_minutes', 30 );
513 $timeout = (int) $this->get_option( 'patchstack_autoblock_blocktime', 60 );
514 if ( empty( $minutes ) || empty( $timeout ) ) {
515 $time = 30 + 60;
516 } else {
517 $time = $minutes + $timeout;
518 }
519
520 global $wpdb;
521 $results = $wpdb->get_results(
522 $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 ) ),
523 OBJECT
524 );
525
526 $out = array();
527 foreach ( $results as $result ) {
528 if ( isset( $result->ip ) ) {
529 array_push( $out, $result->ip );
530 }
531 }
532
533 wp_send_json( $out );
534 }
535
536 /**
537 * Unban a specific IP address from the firewall.
538 *
539 * @return void
540 */
541 private function unbanFirewallIp() {
542 if ( ! isset( $_POST['webarx_ip'] ) || !filter_var( $_POST['webarx_ip'], FILTER_VALIDATE_IP ) ) {
543 return;
544 }
545
546 global $wpdb;
547 $wpdb->query( $wpdb->prepare( 'UPDATE ' . $wpdb->prefix . 'patchstack_firewall_log SET apply_ban = 0 WHERE ip = %s', array( $_POST['webarx_ip'] ) ) );
548 $this->returnResults( null, 'The IP has been unbanned.' );
549 }
550
551 /**
552 * Send all current software on the WordPress site to the API.
553 *
554 * @return void
555 */
556 private function uploadSoftware() {
557 do_action( 'patchstack_send_software_data' );
558 $this->returnResults( null, 'The software data has been sent to the API.' );
559 }
560
561 /**
562 * Upload the firewall and activity logs.
563 *
564 * @return void
565 */
566 private function uploadLogs() {
567 do_action( 'patchstack_send_hacker_logs' );
568 do_action( 'patchstack_send_event_logs' );
569 $this->returnResults( null, 'The logs have been sent to the API.' );
570 }
571
572 /**
573 * Get the currently banned IP addresses from the login page.
574 *
575 * @return void
576 */
577 private function getLoginBans() {
578 // Calculate block time.
579 $minutes = (int) $this->get_option( 'patchstack_anti_bruteforce_minutes', 30 );
580 $timeout = (int) $this->get_option( 'patchstack_anti_bruteforce_blocktime', 60 );
581 if ( empty( $minutes ) || empty( $timeout ) ) {
582 $time = 30 + 60;
583 } else {
584 $time = $minutes + $timeout;
585 }
586
587 // Check if X failed login attempts were made.
588 global $wpdb;
589 $results = $wpdb->get_results(
590 $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 ) ) ),
591 OBJECT
592 );
593
594 // Return the banned IP addresses.
595 wp_send_json( array( 'banned' => $results ) );
596 }
597
598 /**
599 * Unban a banned login IP address.
600 *
601 * @return void
602 */
603 private function unbanLogin() {
604 if ( ! isset( $_POST['id'], $_POST['type'] ) || !ctype_digit( $_POST['id'] ) ) {
605 exit;
606 }
607
608 global $wpdb;
609
610 // Unblock the IP; delete the logs of the IP.
611 if ( $_POST['type'] == 'unblock' ) {
612 // First get the IP address to unblock.
613 $result = $wpdb->get_results(
614 $wpdb->prepare( 'SELECT ip FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id = %d', array( (int) $_POST['id'] ) )
615 );
616
617 // Unblock the IP address.
618 if ( isset( $result[0], $result[0]->ip ) && filter_var( $result[0]->ip, FILTER_VALIDATE_IP ) ) {
619 $wpdb->query(
620 $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE ip = %s', array( $result[0]->ip ) )
621 );
622 }
623 }
624
625 // Unblock and whitelist the IP.
626 if ( $_POST['type'] == 'unblock_whitelist' ) {
627 // First get the IP address to whitelist.
628 $result = $wpdb->get_results(
629 $wpdb->prepare( 'SELECT ip FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id = %d', array( (int) $_POST['id'] ) )
630 );
631
632 // Whitelist and unblock the IP address.
633 if ( isset( $result[0], $result[0]->ip ) && filter_var( $result[0]->ip, FILTER_VALIDATE_IP ) ) {
634 update_option( 'patchstack_login_whitelist', $this->get_option( 'patchstack_login_whitelist', '' ) . "\n" . $result[0]->ip );
635 $wpdb->query(
636 $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE ip = %s', array( $result[0]->ip ) )
637 );
638 }
639 }
640
641 $this->returnResults( null, 'The unban has been processed.' );
642 }
643
644 /**
645 * Get information for debugging purposes.
646 *
647 * @return void
648 */
649 private function debugInfo() {
650 $debug = array(
651 'server' => $_SERVER,
652 'php' => phpversion()
653 );
654
655 wp_send_json( $debug );
656 }
657 }
658