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

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

454 lines 14.5 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 return $software_list;
369 }
370
371 /**
372 * Import the logs generated by the auto prepend firewall rules.
373 *
374 * @return void
375 */
376 public function import_ap_logs()
377 {
378 if ( ! get_option( 'patchstack_firewall_ap_enabled', false ) ) {
379 return;
380 }
381
382 // Do not process if we are already processing a previous batch.
383 if ( get_option( 'patchstack_firewall_log_ap_processing', false ) ) {
384 return;
385 }
386
387 update_option( 'patchstack_firewall_log_ap_processing', true );
388
389 // Attempt to load config file.
390 $logs = __DIR__ . '/../../../pslogs/logs.php';
391 if ( ! file_exists( $logs ) ) {
392 return;
393 }
394
395 // Load the extension.
396 if ( ! file_exists( __DIR__ . '/../lib/patchstack/vendor/autoload.php' ) ) {
397 return;
398 }
399
400 global $wpdb;
401
402 require_once __DIR__ . '/../lib/patchstack/vendor/autoload.php';
403 $extension = new Patchstack\Extensions\WordPress\Extension( [], $this );
404
405 // Read the logs file.
406 $file = new SplFileObject( $logs );
407
408 // Iterate through each line.
409 while ( ! $file->eof() ) {
410 $line = $file->fgets();
411
412 // Skip first line.
413 if ( trim($line) == '' || strpos( $line, '<?php' ) !== false ) {
414 continue;
415 }
416
417 // Decode the line to import.
418 $data = json_decode( base64_decode( $line ), true );
419 if ( ! $data || ! is_array( $data ) ) {
420 continue;
421 }
422
423 // Insert into the logs.
424 $wpdb->insert(
425 $wpdb->get_blog_prefix( $data['site_id'] ) . 'patchstack_firewall_log',
426 [
427 'ip' => $data['ip'],
428 'request_uri' => $data['request_uri'],
429 'user_agent' => $data['user_agent'],
430 'method' => $data['method'],
431 'fid' => $data['fid'],
432 'flag' => '',
433 'post_data' => $data['post_data'],
434 'block_type' => 'BLOCK'
435 ]
436 );
437
438 // Update counters.
439 $hits = (int) $this->get_blog_option( $data['site_id'], 'patchstack_hits_all_time', 0 );
440 $this->update_blog_option( $data['site_id'], 'patchstack_hits_all_time', $hits + 1 );
441
442 $counters = $this->get_blog_option( $data['site_id'], 'patchstack_hits_last_30', [] );
443 $counters = $extension->merge_counters( [ date('Y-m-d') => 1 ], $counters );
444 $this->update_blog_option( $data['site_id'], 'patchstack_hits_last_30', $counters );
445 }
446
447 $file = null;
448 file_put_contents( $logs, '<?php exit; ?>' . PHP_EOL );
449
450 // Update processing state.
451 update_option( 'patchstack_firewall_log_ap_processing', false );
452 }
453 }
454