PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.3
Patchstack – WordPress & Plugins Security v2.1.3
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.3, at includes/listener.php

635 lines 19.2 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 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( ( $this->get_option( 'patchstack_autoblock_minutes', 30 ) + $this->get_option( 'patchstack_autoblock_blocktime', 60 ) ) ) ),
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 // Check if X failed login attempts were made.
579 global $wpdb;
580 $results = $wpdb->get_results(
581 $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( ( $this->get_option( 'patchstack_anti_bruteforce_blocktime', 60 ) + $this->get_option( 'patchstack_anti_bruteforce_minutes', 5 ) ), $this->get_option( 'patchstack_anti_bruteforce_attempts', 10 ) ) ),
582 OBJECT
583 );
584
585 // Return the banned IP addresses.
586 wp_send_json( array( 'banned' => $results ) );
587 }
588
589 /**
590 * Unban a banned login IP address.
591 *
592 * @return void
593 */
594 private function unbanLogin() {
595 if ( ! isset( $_POST['id'], $_POST['type'] ) || !ctype_digit( $_POST['id'] ) ) {
596 exit;
597 }
598
599 global $wpdb;
600
601 // Unblock the IP; delete the logs of the IP.
602 if ( $_POST['type'] == 'unblock' ) {
603 // First get the IP address to unblock.
604 $result = $wpdb->get_results(
605 $wpdb->prepare( 'SELECT ip FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id = %d', array( (int) $_POST['id'] ) )
606 );
607
608 // Unblock the IP address.
609 if ( isset( $result[0], $result[0]->ip ) && filter_var( $result[0]->ip, FILTER_VALIDATE_IP ) ) {
610 $wpdb->query(
611 $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE ip = %s', array( $result[0]->ip ) )
612 );
613 }
614 }
615
616 // Unblock and whitelist the IP.
617 if ( $_POST['type'] == 'unblock_whitelist' ) {
618 // First get the IP address to whitelist.
619 $result = $wpdb->get_results(
620 $wpdb->prepare( 'SELECT ip FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id = %d', array( (int) $_POST['id'] ) )
621 );
622
623 // Whitelist and unblock the IP address.
624 if ( isset( $result[0], $result[0]->ip ) && filter_var( $result[0]->ip, FILTER_VALIDATE_IP ) ) {
625 update_option( 'patchstack_login_whitelist', $this->get_option( 'patchstack_login_whitelist', '' ) . "\n" . $result[0]->ip );
626 $wpdb->query(
627 $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE ip = %s', array( $result[0]->ip ) )
628 );
629 }
630 }
631
632 $this->returnResults( null, 'The unban has been processed.' );
633 }
634 }
635