PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.6
Patchstack – WordPress & Plugins Security v2.3.6
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 / upload.php

upload.php in Patchstack – WordPress & Plugins Security 2.3.6, at includes/upload.php

455 lines 14.6 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 upload the local logs to our API so it can
10 * be shown on the app.
11 */
12 class P_Upload extends P_Core {
13
14 /**
15 * Add the actions required to upload logs to our API.
16 *
17 * @param Patchstack $core
18 * @return void
19 */
20 public function __construct( $core ) {
21 parent::__construct( $core );
22
23 // In case the software has never been synchronized, force it.
24 if ( ! get_option( 'patchstack_software_data_hash', false ) && ! get_option( 'patchstack_software_upload_attempted', false ) ) {
25 $this->upload_software();
26 }
27
28 // Register the actions.
29 add_action( 'patchstack_send_software_data', [ $this, 'upload_software' ] );
30 add_action( 'patchstack_send_hacker_logs', [ $this, 'upload_firewall_logs' ] );
31 add_action( 'patchstack_send_event_logs', [ $this, 'upload_activity_logs' ] );
32 add_action( 'patchstack_import_ap_logs', [ $this, 'import_ap_logs' ] );
33
34 // In case a plugin or upgrade has been performed, re-synchronize with the app.
35 add_action( 'activated_plugin', [ $this, 'upload_software' ] );
36 add_action( 'deactivated_plugin', [ $this, 'upload_software' ] );
37 add_action( 'deleted_plugin', [ $this, 'upload_software' ] );
38 add_action( 'upgrader_process_complete', [ $this, 'upload_software' ] );
39 add_action( '_core_updated_successfully', [ &$this, 'upload_software' ] );
40 }
41
42 /**
43 * Synchronize the software data with our API.
44 * This includes plugins, themes, WordPress and PHP version.
45 *
46 * @return void|array
47 */
48 public function upload_software() {
49 // Get the software data and hash.
50 $data = $this->get_software_data();
51 $hash = sha1( json_encode( $data ) );
52
53 // Do not sync for no reason.
54 if ( ! defined( 'DOING_CRON' ) && ! isset( $_POST['patchstack_secret'] ) && get_option( 'patchstack_software_data_hash', false ) === $hash && ! is_admin() && ! defined( 'WP_CLI' ) ) {
55 return;
56 }
57
58 // Make sure to not keep calling this function.
59 update_option( 'patchstack_software_upload_attempted', true );
60
61 // Synchronize the software list with the API.
62 $results = $this->plugin->api->upload_software( [ 'software' => json_encode( $data ) ] );
63 if ( isset( $results['success'] ) ) {
64 update_option( 'patchstack_software_data_hash', $hash );
65
66 // The result will also contain a list of all vulnerable plugins on the site that is returned by the API.
67 // If the auto update setting is enabled for vulnerable plugins, perform the update once the 15 minute
68 // scheduled task "patchstack_update_plugins" is executed.
69 $update = get_site_option( 'patchstack_auto_update', [] );
70 if ( isset( $results['vulnerable'] ) && is_array( $update ) && in_array( 'vulnerable', $update ) ) {
71 update_site_option( 'patchstack_vulnerable_plugins', $results['vulnerable'] );
72 }
73
74 // If we have vulnerable plugins, determine if we had them before and if not, pull latest firewall rules.
75 if ( isset( $results['vulnerable'] ) && count( $results['vulnerable'] ) > 0 ) {
76 $prev = get_site_option( 'patchstack_latest_vulnerable', [] );
77 foreach ( $results['vulnerable'] as $vuln ) {
78 if ( is_array( $prev ) && ! in_array ( $vuln, $prev ) ) {
79 do_action( 'patchstack_post_dynamic_firewall_rules' );
80 break;
81 }
82 }
83
84 update_site_option( 'patchstack_latest_vulnerable', $results['vulnerable'] );
85 } else {
86 update_site_option( 'patchstack_latest_vulnerable', [] );
87 }
88
89 // If we received the number of vulnerable count.
90 if ( isset ( $results['vulnerability_count'], $results['vulnerability_fix_count'] ) ) {
91 update_option( 'patchstack_vulns_present', $results['vulnerability_count'] );
92 update_option( 'patchstack_fixes_present', $results['vulnerability_fix_count'] );
93 }
94
95 return $results;
96 }
97
98 return;
99 }
100
101 /**
102 * Synchronize the firewall logs with our API.
103 *
104 * @return void
105 */
106 public function upload_firewall_logs() {
107 global $wpdb;
108
109 // Do not execute upload action on free sites.
110 if ( ! $this->license_is_active() || $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
111 return;
112 }
113
114 // Do not process if we are already processing a previous batch.
115 if ( get_option( 'patchstack_firewall_log_processing', false ) ) {
116 return;
117 }
118
119 update_option( 'patchstack_firewall_log_processing', true );
120
121 // Attempt to fetch data, if any.
122 $lastId = get_option( 'patchstack_firewall_log_lastid', 0 );
123 $successId = $lastId;
124
125 // Do a maximum of 100 log entries per cronjob.
126 for ($i = 0; $i <= 1; $i++) {
127 // Pull the data from the database, in batches of 100.
128 $items = $wpdb->get_results(
129 $wpdb->prepare(
130 'SELECT id, ip, log_date, request_uri, user_agent, fid, method, post_data FROM ' . $wpdb->prefix . 'patchstack_firewall_log WHERE id > %d ORDER BY id LIMIT 0,100',
131 $lastId
132 )
133 );
134
135 // No need to continue if we have no data.
136 if ( $wpdb->num_rows == 0 ) {
137 update_option( 'patchstack_firewall_log_lastid', 0 );
138 break;
139 }
140
141 // Construct the array to be uploaded to our API.
142 $logs = [];
143 foreach ( $items as $item ) {
144
145 // Entries that we don't want to store on the API side.
146 if ( stripos( $item->request_uri, 'wp-comments-post' ) !== false ) {
147 continue;
148 }
149
150 // Push to entries to be uploaded.
151 $logs[] = [
152 'ip' => $item->ip,
153 'fid' => $item->fid,
154 'request_uri' => $item->request_uri,
155 'user_agent' => $item->user_agent,
156 'method' => $item->method,
157 'log_date' => $item->log_date,
158 'post_data' => $item->post_data,
159 ];
160
161 $lastId = $item->id;
162 }
163
164 // JSON encode the logs and upload.
165 $logs = json_encode( $logs );
166 $results = $this->plugin->api->upload_firewall_logs(
167 [
168 'logs' => $logs,
169 'type' => 'firewall',
170 ]
171 );
172
173 if ( isset( $results['errors'] ) ) {
174 update_option( 'patchstack_firewall_log_lastid', $successId );
175 break;
176 }
177
178 $successId = $lastId;
179 update_option( 'patchstack_firewall_log_lastid', $successId );
180 }
181
182 // Delete the logs.
183 $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_firewall_log WHERE id <= ' . (int) $successId );
184
185 // No longer processing.
186 update_option( 'patchstack_firewall_log_processing', false );
187 }
188
189 /**
190 * Synchronize the activity logs with our API.
191 *
192 * @return void
193 */
194 public function upload_activity_logs() {
195 global $wpdb;
196
197 // Do not execute upload action on free sites.
198 if ( ! $this->license_is_active() || $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
199 return;
200 }
201
202 // Do not process if we are already processing a previous batch.
203 if ( get_option( 'patchstack_eventlog_processing', false ) ) {
204 return;
205 }
206
207 update_option( 'patchstack_eventlog_processing', true );
208
209 // Determine if we should upload failed logins to the app.
210 $where = " AND action != 'failed login' ";
211 if ( $this->get_option( 'patchstack_activity_log_failed_logins_db', 0 ) == 1 ) {
212 $where = ' ';
213 }
214
215 // Attempt to fetch data, if any.
216 $lastId = get_option( 'patchstack_eventlog_lastid', 0 );
217 $successId = $lastId;
218
219 // Do a maximum of several hundred log entries per cronjob.
220 for ($i = 0; $i <= 1; $i++) {
221 // Pull the data from the database, in batches of 100.
222 $items = $wpdb->get_results(
223 $wpdb->prepare(
224 'SELECT id, author, ip, object, object_id, object_name, action, date FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id > %d' . $where . 'ORDER BY id LIMIT 0,100',
225 $lastId
226 ),
227 ARRAY_A
228 );
229
230 // No need to continue if we have no data.
231 if ( $wpdb->num_rows == 0 ) {
232 update_option( 'patchstack_eventlog_lastid', 0 );
233 break;
234 }
235
236 // Get the last ID in the result set.
237 $lastId = $items[count($items) - 1]['id'];
238
239 // Send to the API.
240 $logs = json_encode( $items );
241 $results = $this->plugin->api->upload_activity_logs( [ 'logs' => $logs ] );
242 if ( isset( $results['errors'] ) ) {
243 update_option( 'patchstack_eventlog_lastid', $successId );
244 break;
245 }
246
247 $successId = $lastId;
248 update_option( 'patchstack_eventlog_lastid', $successId );
249 }
250
251 // Delete the logs.
252 $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id <= ' . (int) $successId );
253
254 // No longer processing.
255 update_option( 'patchstack_eventlog_processing', false );
256 }
257
258 /**
259 * Obtain information about the software that the user has installed.
260 * This includes plugins, themes, WordPress and PHP version.
261 *
262 * @return array
263 */
264 public function get_software_data() {
265 if ( ! function_exists( 'get_plugins' ) ) {
266 require_once ABSPATH . 'wp-admin/includes/plugin.php';
267 }
268 if ( ! function_exists( 'get_plugin_updates' ) ) {
269 require_once ABSPATH . 'wp-admin/includes/update.php';
270 }
271
272 // Refetch updates data if we are performing a plugin listener related action.
273 if ( isset( $_POST['patchstack_secret'] ) ) {
274 @require_once ABSPATH . 'wp-includes/update.php';
275 @wp_update_themes();
276 @wp_update_plugins();
277 }
278
279 // Fetch list of plugins.
280 $all_plugin = get_plugins();
281 $installed_plugins = array_keys( $all_plugin );
282 $updatable_plugins = get_plugin_updates();
283 $software_list = [];
284
285 foreach ( $installed_plugins as $plugin ) {
286 if ( ! file_exists( WP_PLUGIN_DIR . '/' . $plugin ) ) {
287 continue;
288 }
289
290 $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
291 $new_version = empty( $updatable_plugins[ $plugin ]->update->new_version ) ? '' : $updatable_plugins[ $plugin ]->update->new_version;
292 $plugin_name = empty( $plugin_data['Name'] ) ? '' : $plugin_data['Name'];
293 $plugin_version = empty( $plugin_data['Version'] ) ? '' : $plugin_data['Version'];
294
295 if ( ! empty( $plugin_name ) && ! empty( $plugin_version ) ) {
296
297 // Determine the active state.
298 if ( isset( $_GET['action'], $_GET['plugin'] ) && $_GET['action'] == 'deactivate' && $_GET['plugin'] == $plugin) {
299 $active = 0;
300 } else {
301 $active = (int) is_plugin_active( $plugin );
302 }
303
304 $software_list[] = [
305 'sw_type' => 'plugin',
306 'sw_name' => $plugin_name,
307 'sw_cur_ver' => $plugin_version,
308 'sw_new_ver' => $new_version,
309 'sw_key' => $plugin,
310 'sw_active' => $active
311 ];
312 }
313 }
314
315 // Fetch list of themes.
316 $themes = wp_get_themes();
317 $themes_keys = array_keys( $themes );
318 $updatable_themes = get_theme_updates();
319
320 foreach ( $themes_keys as $theme_key ) {
321 $themes_data = $themes[ $theme_key ];
322 $theme_temporary = empty( $updatable_themes[ $theme_key ] ) ? '' : $updatable_themes[ $theme_key ];
323 $theme_new_version = empty( $updatable_themes[ $theme_key ] ) || ! isset( $theme_temporary->update, $theme_temporary->update['new_version'] ) ? '' : $theme_temporary->update['new_version'];
324 $theme_name = $themes_data->get( 'Name' );
325 $theme_version = $themes_data->get( 'Version' );
326
327 if ( ! empty( $theme_name ) && ! empty( $theme_version ) ) {
328 $software_list[] = [
329 'sw_type' => 'theme',
330 'sw_name' => $theme_name,
331 'sw_cur_ver' => $theme_version,
332 'sw_new_ver' => $theme_new_version,
333 'sw_key' => $theme_key,
334 ];
335 }
336 }
337
338 // Fetch WordPress version.
339 global $wp_version;
340 $core_updates = get_core_updates();
341 $new_wp_version = ( ! empty( $core_updates ) && $core_updates[0]->response == 'upgrade' ) ? $core_updates[0]->version : '';
342 $software_list[] = [
343 'sw_type' => 'wordpress',
344 'sw_name' => 'WordPress',
345 'sw_cur_ver' => $wp_version,
346 'sw_new_ver' => $new_wp_version,
347 ];
348
349 // Fetch PHP version.
350 $software_list[] = [
351 'sw_type' => 'php',
352 'sw_name' => 'PHP',
353 'sw_cur_ver' => phpversion(),
354 'sw_new_ver' => '',
355 ];
356
357 // Fetch database server version.
358 global $wpdb;
359 if ( ! is_null( $wpdb ) ) {
360 $software_list[] = [
361 'sw_type' => 'database',
362 'sw_name' => 'Database',
363 'sw_cur_ver' => $wpdb->get_var( 'SELECT VERSION()' ),
364 'sw_new_ver' => ''
365 ];
366 }
367
368 $software_list = apply_filters( 'patchstack_get_software_data', $software_list );
369 return $software_list;
370 }
371
372 /**
373 * Import the logs generated by the auto prepend firewall rules.
374 *
375 * @return void
376 */
377 public function import_ap_logs()
378 {
379 if ( ! get_option( 'patchstack_firewall_ap_enabled', false ) ) {
380 return;
381 }
382
383 // Do not process if we are already processing a previous batch.
384 if ( get_option( 'patchstack_firewall_log_ap_processing', false ) ) {
385 return;
386 }
387
388 update_option( 'patchstack_firewall_log_ap_processing', true );
389
390 // Attempt to load config file.
391 $logs = __DIR__ . '/../../../pslogs/logs.php';
392 if ( ! file_exists( $logs ) ) {
393 return;
394 }
395
396 // Load the extension.
397 if ( ! file_exists( __DIR__ . '/../lib/patchstack/vendor/autoload.php' ) ) {
398 return;
399 }
400
401 global $wpdb;
402
403 require_once __DIR__ . '/../lib/patchstack/vendor/autoload.php';
404 $extension = new Patchstack\Extensions\WordPress\Extension( [], $this );
405
406 // Read the logs file.
407 $file = new SplFileObject( $logs );
408
409 // Iterate through each line.
410 while ( ! $file->eof() ) {
411 $line = $file->fgets();
412
413 // Skip first line.
414 if ( trim($line) == '' || strpos( $line, '<?php' ) !== false ) {
415 continue;
416 }
417
418 // Decode the line to import.
419 $data = json_decode( base64_decode( $line ), true );
420 if ( ! $data || ! is_array( $data ) ) {
421 continue;
422 }
423
424 // Insert into the logs.
425 $wpdb->insert(
426 $wpdb->get_blog_prefix( $data['site_id'] ) . 'patchstack_firewall_log',
427 [
428 'ip' => $data['ip'],
429 'request_uri' => $data['request_uri'],
430 'user_agent' => $data['user_agent'],
431 'method' => $data['method'],
432 'fid' => $data['fid'],
433 'flag' => '',
434 'post_data' => $data['post_data'],
435 'block_type' => 'BLOCK'
436 ]
437 );
438
439 // Update counters.
440 $hits = (int) $this->get_blog_option( $data['site_id'], 'patchstack_hits_all_time', 0 );
441 $this->update_blog_option( $data['site_id'], 'patchstack_hits_all_time', $hits + 1 );
442
443 $counters = $this->get_blog_option( $data['site_id'], 'patchstack_hits_last_30', [] );
444 $counters = $extension->merge_counters( [ date('Y-m-d') => 1 ], $counters );
445 $this->update_blog_option( $data['site_id'], 'patchstack_hits_last_30', $counters );
446 }
447
448 $file = null;
449 file_put_contents( $logs, '<?php exit; ?>' . PHP_EOL );
450
451 // Update processing state.
452 update_option( 'patchstack_firewall_log_ap_processing', false );
453 }
454 }
455