File: /var/www/shoetique/wp-content/plugins/sucuri-scanner/src/lastlogins-failed.php
<?php
/**
* Code related to the lastlogins-failed.php interface.
*
* PHP version 5
*
* @category Library
* @package Sucuri
* @subpackage SucuriScanner
* @author Daniel Cid <dcid@sucuri.net>
* @copyright 2010-2018 Sucuri Inc.
* @license https://www.gnu.org/licenses/gpl-2.0.txt GPL2
* @link https://wordpress.org/plugins/sucuri-scanner
*/
if (!defined('SUCURISCAN_INIT') || SUCURISCAN_INIT !== true) {
if (!headers_sent()) {
/* Report invalid access if possible. */
header('HTTP/1.1 403 Forbidden');
}
exit(1);
}
/**
* Print a list with the failed logins occurred during the last hour.
*
* @return string A list with the failed logins occurred during the last hour.
*/
function sucuriscan_failed_logins_panel()
{
$template_variables = array(
'FailedLogins.List' => '',
'FailedLogins.Total' => '',
'FailedLogins.MaxFailedLogins' => 0,
'FailedLogins.NoItemsVisibility' => 'visible',
'FailedLogins.WarningVisibility' => 'visible',
'FailedLogins.PaginationLinks' => '',
'FailedLogins.PaginationVisibility' => 'hidden',
);
// Define variables for the pagination.
$page_number = SucuriScanTemplate::pageNumber();
$max_per_page = SUCURISCAN_MAX_PAGINATION_BUTTONS;
$page_offset = ($page_number - 1) * $max_per_page;
$page_limit = ($page_offset + $max_per_page);
// Clear failed login logins when delete button is pressed.
if (SucuriScanInterface::checkNonce() && SucuriScanRequest::post(':delete_failedlogins')) {
SucuriScanEvent::clearLastLogs('sucuri-failedlogins.php');
}
$max_failed_logins = SucuriScanOption::getOption(':maximum_failed_logins');
$notify_bruteforce_attack = SucuriScanOption::getOption(':notify_bruteforce_attack');
$failed_logins = sucuriscan_get_all_failed_logins($page_offset, $max_per_page);
if ($failed_logins) {
$counter = 0;
for ($key = $page_offset; $key < $page_limit; $key++) {
if (array_key_exists($key, $failed_logins['entries'])) {
$login_data = $failed_logins['entries'][ $key ];
if (!is_array($login_data)) {
continue;
}
$template_variables['FailedLogins.List'] .= SucuriScanTemplate::getSnippet(
'lastlogins-failedlogins',
array(
'FailedLogins.Num' => $login_data['attempt_count'],
'FailedLogins.Username' => $login_data['user_login'],
'FailedLogins.RemoteAddr' => $login_data['remote_addr'],
'FailedLogins.UserAgent' => $login_data['user_agent'],
'FailedLogins.Datetime' => SucuriScan::datetime($login_data['attempt_time']),
)
);
$counter++;
}
}
if ($counter > 0) {
$template_variables['FailedLogins.NoItemsVisibility'] = 'hidden';
}
$template_variables['FailedLogins.PaginationLinks'] = SucuriScanTemplate::pagination(
'%%SUCURI.URL.Lastlogins%%#failed',
$failed_logins['count'],
$max_per_page
);
if ($failed_logins['count'] > $max_per_page) {
$template_variables['FailedLogins.PaginationVisibility'] = 'visible';
}
}
$template_variables['FailedLogins.MaxFailedLogins'] = $max_failed_logins;
if ($notify_bruteforce_attack == 'enabled') {
$template_variables['FailedLogins.WarningVisibility'] = 'hidden';
}
return SucuriScanTemplate::getSection('lastlogins-failedlogins', $template_variables);
}
/**
* Find the full path of the file where the information of the failed logins
* will be stored, it will be created automatically if does not exists (and if
* the destination folder has permissions to write). This method can also be
* used to reset the content of the datastore file.
*
* @see sucuriscan_reset_failed_logins()
*
* @param bool $get_old_logs Whether the old logs will be retrieved or not.
* @param bool $reset Whether the file will be resetted or not.
* @return string|false Absolute path to the file.
*/
function sucuriscan_failed_logins_datastore_path($get_old_logs = false, $reset = false)
{
$file_name = $get_old_logs ? 'sucuri-oldfailedlogins.php' : 'sucuri-failedlogins.php';
$datastore_path = SucuriScan::dataStorePath($file_name);
$default_content = sucuriscan_failed_logins_default_content();
// Create the file if it does not exists.
if (!file_exists($datastore_path) || $reset) {
@file_put_contents($datastore_path, $default_content, LOCK_EX);
}
// Return the datastore path if the file exists (or was created).
if (is_readable($datastore_path)) {
return $datastore_path;
}
return false;
}
/**
* Default content of the datastore file where the failed logins are being kept.
*
* @return string Default content of the file.
*/
function sucuriscan_failed_logins_default_content()
{
return "<?php exit(0); ?>\n";
}
/**
* Returns failed logins data including old entries.
*
* @param int $offset Initial index to start the array.
* @param int $limit Number of items in the returned array.
* @return array|false Failed logins data.
*/
function sucuriscan_get_all_failed_logins($offset = 0, $limit = -1)
{
$all = array();
$new = sucuriscan_get_failed_logins();
$old = sucuriscan_get_failed_logins(true, $offset, $limit);
if ($new && $old) {
// Merge the new and old failed logins.
$all = array();
$all['first_attempt'] = $old['first_attempt'];
$all['last_attempt'] = $new['last_attempt'];
$all['count'] = $new['count'] + $old['count'];
$all['diff_time'] = abs($all['last_attempt'] - $all['first_attempt']);
$all['entries'] = array_merge($new['entries'], $old['entries']);
return $all;
} elseif ($new && !$old) {
return $new;
} elseif (!$new && $old) {
return $old;
}
return false;
}
/**
* Read and parse the content of the datastore file where the failed logins are
* being kept. This method will also calculate the difference in time between
* the first and last login attempt registered in the file to later decide if
* there is a brute-force attack in progress (and send an email notification
* with the report) or reset the file after considering it a normal behavior of
* the site.
*
* @param bool $get_old_logs Whether the old logs will be retrieved or not.
* @param int $offset Array index from where to start collecting the data.
* @param int $limit Number of items to insert into the returned array.
* @return array|false Information and entries gathered from the failed logins datastore file.
*/
function sucuriscan_get_failed_logins($get_old_logs = false, $offset = 0, $limit = -1)
{
$datastore_path = sucuriscan_failed_logins_datastore_path($get_old_logs);
if (!$datastore_path) {
return false;
}
$lines = SucuriScanFileInfo::fileLines($datastore_path);
if (!$lines) {
return false;
}
$failed_logins = array(
'count' => 0,
'first_attempt' => 0,
'last_attempt' => 0,
'diff_time' => 0,
'entries' => array(),
);
// Read and parse all the entries found in the datastore file.
$initial = count($lines) - 1;
$processed = 0;
// Start from the newest entry in the file.
for ($key = $initial; $key >= 0; $key--) {
$line = trim($lines[ $key ]);
// Skip lines that are clearly not JSON-encoded.
if (substr($line, 0, 1) !== '{') {
continue;
}
// Reduce the memory allocation by skipping unnecessary lines (LEFT).
if ($limit > 0 && $failed_logins['count'] < $offset) {
$failed_logins['entries'][] = $line;
$failed_logins['count'] += 1;
continue;
}
// Reduce the memory allocation by skipping unnecessary lines (RIGHT).
if ($limit > 0 && $processed > $limit) {
$failed_logins['entries'][] = $line;
$failed_logins['count'] += 1;
continue;
}
// Decode data only if necessary.
$login_data = @json_decode($line, true);
$processed++; /* count decoded data */
if (is_array($login_data)) {
$login_data['attempt_count'] = ( $key + 1 );
if (!$login_data['user_agent']) {
$login_data['user_agent'] = __('Unknown', 'sucuri-scanner');
}
if (!isset($login_data['user_password'])) {
$login_data['user_password'] = '';
}
$failed_logins['entries'][] = $login_data;
$failed_logins['count'] += 1;
}
}
// Stop if the there is no data.
if ($failed_logins['count'] <= 0) {
return false;
}
// Calculate the different time between the first and last attempt.
$idx = abs($failed_logins['count'] - 1);
$last = $failed_logins['entries'][$idx];
$first = $failed_logins['entries'][0];
if (!is_array($last)) {
/* In case the JSON is not decoded yet */
$last = @json_decode($last, true);
}
if (!is_array($first)) {
/* In case the JSON is not decoded yet */
$first = @json_decode($first, true);
}
$failed_logins['last_attempt'] = $last['attempt_time'];
$failed_logins['first_attempt'] = $first['attempt_time'];
$failed_logins['diff_time'] = abs($last['attempt_time'] - $first['attempt_time']);
return $failed_logins;
}
/**
* Add a new entry in the datastore file where the failed logins are being kept,
* this entry will contain the username, timestamp of the login attempt, remote
* address of the computer sending the request, and the user-agent.
*
* @param string $user_login Information from the current failed login event.
* @return bool True if the information was saved, false otherwise.
*/
function sucuriscan_log_failed_login($user_login = '')
{
$storage = sucuriscan_failed_logins_datastore_path();
if (!$storage) {
return false;
}
$login_data = json_encode(
array(
'user_login' => $user_login,
'attempt_time' => time(),
'remote_addr' => SucuriScan::getRemoteAddr(),
'user_agent' => SucuriScan::getUserAgent(),
)
);
return (bool) @file_put_contents(
$storage,
$login_data . "\n",
FILE_APPEND
);
}
/**
* Read and parse all the entries in the datastore file where the failed logins
* are being kept, this will loop through all these items and generate a table
* in HTML code to send as a report via email according to the plugin settings
* for the email alerts.
*
* @param array $failed_logins Information gathered from the failed logins.
* @return bool Whether the report was sent via email or not.
*/
function sucuriscan_report_failed_logins($failed_logins = array())
{
if (!$failed_logins
|| !isset($failed_logins['count'])
|| $failed_logins['count'] < 1
) {
return false;
}
$mail_content = '';
$prettify_mails = SucuriScanMail::prettifyMails();
if ($prettify_mails) {
$table_html = '<table border="1" cellspacing="0" cellpadding="0">';
// Add the table headers.
$table_html .= '<thead>';
$table_html .= '<tr>';
$table_html .= '<th>' . __('Username', 'sucuri-scanner') . '</th>';
$table_html .= '<th>' . __('Password', 'sucuri-scanner') . '</th>';
$table_html .= '<th>' . __('IP Address', 'sucuri-scanner') . '</th>';
$table_html .= '<th>' . __('Attempt Timestamp', 'sucuri-scanner') . '</th>';
$table_html .= '<th>' . __('Attempt Date/Time', 'sucuri-scanner') . '</th>';
$table_html .= '</tr>';
$table_html .= '</thead>';
$table_html .= '<tbody>';
}
foreach ($failed_logins['entries'] as $login_data) {
$login_data['attempt_date'] = SucuriScan::datetime($login_data['attempt_time']);
if ($prettify_mails) {
$table_html .= '<tr>';
$table_html .= '<td>' . esc_attr($login_data['user_login']) . '</td>';
$table_html .= '<td>' . esc_attr($login_data['user_password']) . '</td>';
$table_html .= '<td>' . esc_attr($login_data['remote_addr']) . '</td>';
$table_html .= '<td>' . esc_attr($login_data['attempt_time']) . '</td>';
$table_html .= '<td>' . esc_attr($login_data['attempt_date']) . '</td>';
$table_html .= '</tr>';
} else {
$mail_content .= "\n";
$mail_content .= __('Username', 'sucuri-scanner') . ":\x20" . $login_data['user_login'] . "\n";
$mail_content .= __('Password', 'sucuri-scanner') . ":\x20" . $login_data['user_password'] . "\n";
$mail_content .= __('IP Address', 'sucuri-scanner') . ":\x20" . $login_data['remote_addr'] . "\n";
$mail_content .= __('Attempt Timestamp', 'sucuri-scanner') . ":\x20" . $login_data['attempt_time'] . "\n";
$mail_content .= __('Attempt Date/Time', 'sucuri-scanner') . ":\x20" . $login_data['attempt_date'] . "\n";
}
}
if ($prettify_mails) {
$table_html .= '</tbody>';
$table_html .= '</table>';
$mail_content = $table_html;
}
if (SucuriScanEvent::notifyEvent('bruteforce_attack', $mail_content)) {
sucuriscan_reset_failed_logins();
return true;
}
return false;
}
/**
* Remove all the entries in the datastore file where the failed logins are
* being kept. The execution of this method will not delete the file (which is
* likely the best move) but rather will clean its content and append the
* default code defined by another method above.
*
* @return bool Whether the datastore file was resetted or not.
*/
function sucuriscan_reset_failed_logins()
{
$datastore_path = SucuriScan::dataStorePath('sucuri-failedlogins.php');
$datastore_backup_path = sucuriscan_failed_logins_datastore_path(true, false);
$default_content = sucuriscan_failed_logins_default_content();
$current_content = @file_get_contents($datastore_path);
$current_content = str_replace($default_content, '', $current_content);
@file_put_contents(
$datastore_backup_path,
$current_content,
FILE_APPEND
);
return (bool) sucuriscan_failed_logins_datastore_path(false, true);
}