PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.0
Patchstack – WordPress & Plugins Security v2.1.0
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.0, at includes/listener.php

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