PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.5
Patchstack – WordPress & Plugins Security v2.1.5
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.5, at includes/listener.php

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