PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.10
Patchstack – WordPress & Plugins Security v2.1.10
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.10, at includes/listener.php

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