Okay, so I'm a whatever comes before Junior in the dev world, a wannabe, maybe? But I still have a few servers running live sites for some clients and myself. One of the servers had some questionable activity, so I started a log tracking anytime a page with critical data/script access is visited, or a database query is executed.
The site is one of my first projects, built using PHP 7.4. Hosting is by Bluehost.
The log records the current username (the login cookie's value), query/page summary, IP address, and a timestamp. After registering for about a week, I've found some disturbing results.
About a quarter of the traffic onto the server is from unrecognized IP addresses, most of these are trying to access a page, where they fail the validation (checking if the login cookie is set and that it has a certain length) and are redirected to a login page. However, multiple IP addresses (recorded into the log with an empty string for the login cookie value) are somehow bypassing this and executing sensitive scripts on the page that alter the database (only supposed to be available to an admin).
Have the attackers figured out a way to block the authentication script from running? When logging in, I use prepared statements, and also, I validate the username and passwords in separate queries, hoping to avoid SQL injection. Even if the attackers were able to create a cookie with the proper name (whether through great guess or XSS grab) it is being stored with the value of an empty string, and should still fail the length requirement validation check.(right?)
Moreover, there was a glaring vulnerability from using an outdated Google API library (accessing the clients google calendar) where GuzzleHTTP was causing all sorts of vulnerabilities (The following list is from github's dependabot):
- Change in port should be considered a change in origin
- CURLOPT_HTTPAUTH option not cleared on change of origin
- Failure to strip the Cookie header on change in host or HTTP downgrade
- Fix failure to strip Authorization header on HTTP downgrade
- Cross-domain cookie leakage in Guzzle
- HTTP Proxy header vulnerability
- Improper Input Validation in guzzlehttp/psr7
I updated the library, blocked relevant IP ranges, and changed the login cookie name. But the issue has happened with two more IP addresses over the past week. I'm at a complete loss for how this is happening, and I would very much appreciate any guidance on how to continue troubleshooting.
UPDATE: The question was too vague, so I will be posting some more detailed info here Every page except the login page calls a header file
Example-page.php:
<?PHP
include '../secure.db.conn.php'; //db connection
$note = 'accessed client list'
$log = $conn->query("INSERT INTO admin_log (user, note, ip) VALUES ('" . $_COOKIE['login'] . "', '$note', '$ipAddress')");
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client List</title>
<?php include "header.php"; ?>
<link rel="stylesheet" href="./css/style.css">
</head>
header.php
<!-- Bootstrap core CSS & JS dependencies-->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev nYRRuWlolflfl" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin="anonymous"></script>
<!-- Favicons -->
<?php include "nav.php"; ?>
nav.php
<?php
include dirname(__DIR__) . "/config.php"; //
include dirname(__DIR__) . "/master/siteSearch.php";
//user validation
if (!isset($_COOKIE["login"]) || strlen($_COOKIE["login"]) <= 3) {
$URL = $rootURL . "/login/";
if (headers_sent()) {
echo ("<script>location.href='$URL'</script>");
} else {
header("Location: $URL");
}
exit;
}
?>
<!-- Nav menu -->
<script> //There's a site search in navbar
$(document).ready(function() {
$('#site_search').on('input', function() {
var query = $(this).val()
$.ajax({
url: "<?php echo $rootURL ?>/siteSearch.php",
method: "post",
data: {
query: query
},
success: function(data) {
$('#site_search_results').html(data);
}
})
})
/*When google Oauth token expires, the server deletes it.
This check helps ensure the user gets a new token in a timely manner*/
$(document).ready(function() {
var isToken = <?php echo (file_exists(dirname(__DIR__) . '/token.json') ? 1 : 0); ?>;
if (isToken == 0) {
$.ajax({
url: "<?php echo $rootURL ?>/oauth/oauth.php",
data: {
tokes: isToken
},
method: "post",
success: function(data) {
$('#primaryNav-Modaldetail').html(data)
$('#primaryNav-Modal').modal("show")
}
})
}
})
The login page code may be useful too (I did not abstract anything fearing that I may omit a seemingly inaccuous- though actually vulnerable- line of code login.php
// Check if the user is already logged in, if yes then redirect him to welcome page
if(isset($_COOKIE['masterLogin']) && strlen($_COOKIE['masterLogin']) >= 3){
header("location: welcome.php");
exit;
}
// Include config file
require_once "secure.db.conn.php";
// Define variables and initialize with empty values
$username = $password = "";
$username_err = $password_err = $login_err = "";
// Processing form data when form is submitted
if($_SERVER["REQUEST_METHOD"] == "POST"){
// Check if username is empty
if(empty(trim($_POST["username"]))){
$username_err = "Please enter username.";
} else{
$username = trim($_POST["username"]);
}
// Check if password is empty
if(empty(trim($_POST["password"]))){
$password_err = "Please enter your password.";
} else{
$password = trim($_POST["password"]);
}
// Validate credentials
if(empty($username_err) && empty($password_err)){
// Prepare a select statement
$sql = "SELECT id, username, password FROM master WHERE username = :username";
if($stmt = $conn->prepare($sql)){
// Bind variables to the prepared statement as parameters
$stmt->bindParam(":username", $param_username, PDO::PARAM_STR);
// Set parameters
$param_username = trim($_POST["username"]);
// Attempt to execute the prepared statement
if($stmt->execute()){
// Check if username exists, if yes then verify password
if($stmt->rowCount() == 1){
if($row = $stmt->fetch()){
$id = $row["id"];
$username = $row["username"];
$hashed_password = $row["password"];
if(password_verify($password, $hashed_password)){
// Password is correct, so start a new session
session_start();
if(!empty($_POST["remember"])) {
setcookie ("login",$username,time() (5*24*60*60), '/');
} else {
setcookie ("login",$username,time() (4*60*60), '/');
}
// Redirect user to welcome page
header("location: welcome.php");
} else{
// Password is not valid, display a generic error message
$login_err = "Invalid username or password.";
}
}
} else{
// Username doesn't exist, display a generic error message
$login_err = "Invalid username or password.";
}
} else{
echo "Oops! Something went wrong. Please try again later.";
}
// Close statement
unset($stmt);
}
}
// Close connection
unset($conn);
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Hugo 0.88.1">
<title>Sign in</title>
<!-- Bootstrap core CSS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev nYRRuWlolflfl" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin="anonymous"></script>
<!-- Favicons (ommitted for Stack Overflow) -->
</head>
<body >
<?php
if(!empty($login_err)){
echo '<div >' . $login_err . '</div>';
}
?>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
<div >
<label>Username</label>
<input type="text" name="username" value="<?php echo $username; ?>">
<span ><?php echo $username_err; ?></span>
</div>
<div >
<label>Password</label>
<input type="password" name="password" >
<span ><?php echo $password_err; ?></span>
</div>
<input type="checkbox" value="remember-me" name="remember" id="remember" <?php if(isset($_COOKIE["remember_login"])) { ?> checked <?php } ?>/> Remember me
</label>
<div >
<input type="submit" value="Login">
</form>
</div>
</body>
</html>
CodePudding user response:
Your complete user validation seems to consist of this single statement:
if (!isset($_COOKIE["login"]) || strlen($_COOKIE["login"]) <= 3) {
<user is not valid, redirect to login>
}
<user is valid>
All this code does is check whether there is a cookie named "login" and whether the content is longer than 3 characters.
That is NOT a proper validation of a logged in user. Anybody can create such a cookie and can access the protected part of your site.
A cookie should contain something that proves that the user is really the one that just logged in. Suppose an user logged in with an username and password, and we put a "token" in the cookie, to somehow prove it is the same user.
Such a token can be a random generated string, for instance with random_bytes(), which is stored in the database alongside the users credentials, at the moment someone logs in. Now you have a cookie, and a row in your database, both containing an unique random string.
To validate whether an user has indeed already logged in, all you have to do is compare the two. Yes, this require a lookup in the data, but if you also remember the row id of the user in the cookie this can be very quick.
Does any of this make any sense?
Using tokens like this is not perfect. Cookies can still be stolen, but since any cookie, made like this, is only valid as long as the login session lasts, it will be very hard to spoof it. It will certainly be harder than simply making a cookie with the right name and more than 3 characters.