#!/bin/bash # # Shell script for tracking unauthorized system users and their logins. # The script checks if unauthorized system users with enabled shell are presented on the system. # By comparing with authorized users. # The authorized users can have or don't have SSH access allowed. # SSH access controls by flags: ssh_allowed and ssh_denied in authorized users list file. # The script includes several main checks: # 1) Check for any user other than root with UID=0. # 2) Check for unauthorized system users with enabled shell. # 3) Check for recent logins of unauthorized system users with enabled shell. # In case something is found script creates lock file, logs a message and exits with code 2. # The lock file needed for avoiding self-resolved cases. # Presence of the lock file doesn't skip the main checks. # Bash strict mode. set -uo pipefail # Declare and assign global variables. # Authorized users associative array. declare -A AUTHORIZED_USERS # Get the directory where the script is located. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" # Path to authorized users list file in the script's directory. AUTHORIZED_USERS_FILE="${SCRIPT_DIR}/authorized_users.list" # Auth log. AUTH_LOG="/var/log/secure" # cPanel users list. CPANEL_USERS_LIST="" # Shell patterns to exclude during the check. EXCLUDED_SHELL_PATTERNS="(/[s]?bin/(false|nologin|halt|shutdown|sync)|/usr/local/cpanel/bin/noshell)" # Determine a lock file. LOCK_FILE="/var/lock/check_unauthorized_user.lock" # Determine a log file. LOG_FILE="/var/log/check_unauthorized_user.log" # Set the PATH PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin # Systems users array. SYSTEM_USERS=() # System users with enabled shell array. SYSTEM_USERS_WITH_ENABLED_SHELL=() ####################################### # Logs a message with a timestamp. # Globals: # LOG_FILE - The file where the logs will be appended. # Arguments: # message - The message to be logged. # no_log - Optional, if provided, the message will be printed without timestamp and not logged to log file. # Outputs: # Writes message to stdout or stdout and file. ####################################### logger() { # Retrieve the message to be logged from the first argument. local message="${1}" # Retrieve the optional 'no_log' argument, if provided. local no_log="${2:-}" # Check if 'no_log' is set (not empty). if [[ -n "${no_log}" ]]; then # Print the message to stdout without a timestamp. echo "${message}" else # Check if the log file exists and is writable. if [[ ! -w "${LOG_FILE}" ]]; then echo "ERROR! Log file '${LOG_FILE}' does not exist or is not writable." echo "HINT: Log file '${LOG_FILE}' should have the following permissions: 600, owner nrpe:nrpe (CloudLinux/AlmaLinux)." exit 3 fi # Generate a timestamp in the format: Month Day HH:MM:SS ±hhmm. local timestamp timestamp=$(date +"%b %d %H:%M:%S %z") # Append the message with the timestamp to the log file. echo "[${timestamp}] ${message}" >> "${LOG_FILE}" fi } ####################################### # Check for the existence of a lock file and create it if it does not exist. # Globals: # LOCK_FILE # LOG_FILE # Arguments: # None # Returns: # Exits with code 2 if the lock file exists. ####################################### set_lock() { # Check if the lock file exists. if [[ -f "$LOCK_FILE" ]]; then logger "CRITICAL! ${LOCK_FILE} exists. Investigation needed." "no_log" logger "HINT: Check ${LOG_FILE} for details." "no_log" exit 2 else # If the lock file does not exist, create it. touch "$LOCK_FILE" logger "Lock file was created: ${LOCK_FILE}" fi } ####################################### # Get the timestamp from a certain number of minutes ago # Globals: # None # Arguments: # Minutes ago (default: 30 minutes ago) # Returns: # Timestamp string representing a specific time in the past. ####################################### get_minutes_ago_timestamp() { # Retrieve the number of minutes ago from the first argument; default to 30 if not provided. local minutes_ago=${1:-30} # Use the `date` command to get the timestamp for the specified number of minutes ago. # Format the output as: Month Day HH:MM:SS. date --date="${minutes_ago} minutes ago" "+%b %e %H:%M:%S" } ####################################### # Get the user's shell based on the username. # Globals: # None # Arguments: # User to check. # Returns: # User shell, 3 on error. ####################################### get_user_shell() { local username="$1" # Check if the username is provided (not empty). if [[ -z "$username" ]]; then # Log an error message and exit with code 3 if username is not provided. logger "ERROR! Username is not provided" "no_log" exit 3 fi # Get the login shell for the user by querying the passwd database. # `getent passwd "$username"` retrieves the user entry. # `cut -d: -f7` extracts the 7th field (the shell) from the entry. local user_shell user_shell=$(getent passwd "$username" | cut -d: -f7) echo "$user_shell" } ####################################### # Get the user's uid based on the username. # Globals: # None # Arguments: # User to check. # Returns: # User uid, 3 on error. ####################################### get_user_uid() { local username="$1" # Check if the username is provided (not empty). if [[ -z "$username" ]]; then # Log an error message and exit with code 3 if username is not provided. logger "ERROR! Username is not provided." "no_log" exit 3 fi # Get the uid for the user by querying the passwd database. # `getent passwd "$username"` retrieves the user entry. # `cut -d: -f3` extracts the 7th field (the user uid) from the entry. local user_uid user_uid=$(getent passwd "${username}" | cut -d: -f3) echo "$user_uid" } ####################################### # Get any user other than root with UID=0. # Globals: # None # Arguments: # None # Returns: # Total amount of non-root users with UID=0, exit with code 2. ####################################### # New function to check get_users_with_uid0() { local -i uid0_users_count=0 local uid0_users_details="" # Loop through each line of the passwd file, splitting by colon. while IFS=: read -r username _ uid _; do # Check if the UID is 0 and the username is not root. if [[ "${uid}" -eq 0 && "${username}" != "root" ]]; then # Log a critical message if a non-root user with UID=0 is found. logger "CRITICAL! Non-root user ${username} has UID=0" # Increment the counter for users with UID=0. (( uid0_users_count += 1 )) # Append the user details. uid0_users_details+="${username}, " fi done < <(getent passwd) # Use getent to read the passwd database. # If any non-root users with UID=0 were found. if [[ "${uid0_users_count}" -gt 0 ]]; then # Invoke function to set lock file. set_lock # Remove trailing comma and space. uid0_users_details="${uid0_users_details%, }" # Log a critical message with the list of non-root users with UID=0. logger "CRITICAL! Non-root users with UID=0 found (${uid0_users_count}): ${uid0_users_details}" "no_log" # Exit with code 2 to indicate that any non-root users with UID=0 were found. exit 2 fi } ####################################### # Get list of cPanel users from WHM API # Globals: # CPANEL_USERS_LIST # Arguments: # None # Returns: # cPanel users list. ####################################### get_cpanel_users() { # Get cPanel users using WHM API call and format the output. CPANEL_USERS_LIST=$(whmapi1 --output=jsonpretty listaccts | jq '.data.acct[].user' | tr -d "\"" | sort) } ####################################### # Get list of system users excluding cPanel users # Globals: # CPANEL_USERS_LIST # SYSTEM_USERS # Arguments: # None # Returns: # System users list. ####################################### get_system_users() { # Read each username from the list of system users. while read -r username; do # Check if the username is not present in the cPanel users list. # `grep -q "^${username}$"` searches for an exact match of the username. # `<<< "${CPANEL_USERS_LIST}"` provides the cPanel users list as input. if ! grep -q "^${username}$" <<< "${CPANEL_USERS_LIST}"; then # Add the username to the SYSTEM_USERS array if not in the cPanel users list. SYSTEM_USERS+=("${username}") fi done < <(getent passwd | cut -d: -f1 | sort) # Get the sorted list of system usernames. } ####################################### # Get list of system users with enabled shell. # Globals: # EXCLUDED_SHELL_PATTERNS # SYSTEM_USERS_WITH_ENABLED_SHELL # SYSTEM_USERS # Arguments: # None # Returns: # System users with enabled shell list. ####################################### get_system_users_with_enabled_shell() { # Iterate over each user in the SYSTEM_USERS array. for user in "${SYSTEM_USERS[@]}"; do # Get the login shell for the user by calling the get_user_shell function. local user_shell user_shell=$(get_user_shell "${user}") # Check if the user's shell does not match any of the excluded shell patterns. # `grep -q -E "${EXCLUDED_SHELL_PATTERNS}"` searches for any match of the excluded patterns. # `<<< "${user_shell}"` provides the user’s shell as input. if ! grep -q -E "${EXCLUDED_SHELL_PATTERNS}" <<< "${user_shell}"; then # Add the user to the SYSTEM_USERS_WITH_ENABLED_SHELL array if their shell is not excluded. SYSTEM_USERS_WITH_ENABLED_SHELL+=("${user}") fi done } ####################################### # Get unauthorized system users with enabled shell # Globals: # AUTHORIZED_USERS # SYSTEM_USERS_WITH_ENABLED_SHELL # Arguments: # None # Returns: # Total amount of unauthorized system users with exit code 2 ####################################### get_unauthorized_system_users() { local -i unauthorized_system_users_count=0 local unauthorized_system_users_details="" # Iterate over each user in the SYSTEM_USERS_WITH_ENABLED_SHELL array. for user in "${SYSTEM_USERS_WITH_ENABLED_SHELL[@]}"; do local authorized_user=false # Check if the user is in the authorized users list. if [[ -n "${AUTHORIZED_USERS[$user]+x}" ]]; then authorized_user=true fi # If the user is not in the authorized users list. if [[ "${authorized_user}" == false ]]; then # Get the user's UID by calling the get_user_uid function. local user_uid user_uid=$(get_user_uid "${user}") # Get the user's shell by calling the get_user_shell function. local user_shell user_shell=$(get_user_shell "${user}") # Log a message to log file if the user is not in the authorized users list. logger "CRITICAL! Found unauthorized system user ${user} with uid ${user_uid} and enabled ${user_shell} shell." # Increment the counter for unauthorized system users. (( unauthorized_system_users_count += 1 )) # Append the unauthorized user details to the string. unauthorized_system_users_details+="${user} (uid ${user_uid}), " fi done # If any unauthorized system users were found. if [[ "${unauthorized_system_users_count}" -gt 0 ]]; then # Invoke function to set lock file. set_lock # Remove trailing comma and space from details string. unauthorized_system_users_details="${unauthorized_system_users_details%, }" # Log a critical message with the count and details of unauthorized users. logger "CRITICAL! Unauthorized system users found (${unauthorized_system_users_count}): ${unauthorized_system_users_details}" "no_log" # Exit with code 2 to indicate that unauthorized users were found. exit 2 fi } ####################################### # Get unauthorized system users recent logins using authentication logs # Globals: # AUTHORIZED_USERS # AUTH_LOG # SYSTEM_USERS_WITH_ENABLED_SHELL # Arguments: # None # Returns: # Total amount of unauthorized system users to login exit code 2 ####################################### get_unauthorized_system_users_logins() { local threshold_timestamp # Get the timestamp from 20 minutes ago threshold_timestamp=$(get_minutes_ago_timestamp 20) local -i unauthorized_system_users_logins_count=0 local unauthorized_system_users_logins_details="" # Iterate over each user in the SYSTEM_USERS_WITH_ENABLED_SHELL array. for user in "${SYSTEM_USERS_WITH_ENABLED_SHELL[@]}"; do local matches # Search for log entries related to "sshd" where a session was opened for the specified user. # Filter the results to include only those entries that are equal to or greater than the threshold timestamp. matches=$(grep -E "sshd.*: session opened for user ${user}" "${AUTH_LOG}" | \ awk -v threshold="${threshold_timestamp}" '$0 >= threshold') # If there are any log entries matching the criteria. if [[ -n "${matches}" ]]; then local ssh_login_allowed=false # If the user is in the authorized users list and has ssh login allowed if [[ -n "${AUTHORIZED_USERS[$user]+x}" ]]; then if [[ "${AUTHORIZED_USERS[$user]}" == "ssh_allowed" ]]; then ssh_login_allowed=true fi fi # If the user is not in the authorized users list or has ssh_denied if [[ "${ssh_login_allowed}" == false ]]; then # Get the user's UID by calling the get_user_uid function. local user_uid user_uid=$(get_user_uid "${user}") # Get the user's shell by calling the get_user_shell function. local user_shell user_shell=$(get_user_shell "${user}") # Log messages to log file if unauthorized system user login('s) found. logger "CRITICAL! Recent login found for unauthorized system user ${user} with uid ${user_uid} and enabled ${user_shell} shell." logger "According to the following record('s) from ${AUTH_LOG} log: ${matches}" # Increment the counter for unauthorized system users logins. (( unauthorized_system_users_logins_count += 1 )) # Append the unauthorized user logins details to the string. unauthorized_system_users_logins_details+="${user} (uid ${user_uid}), " fi fi done # If any unauthorized system users logins were found. if [[ "${unauthorized_system_users_logins_count}" -gt 0 ]]; then # Invoke function to set lock file. set_lock # Remove trailing comma and space. unauthorized_system_users_logins_details="${unauthorized_system_users_logins_details%, }" logger "CRITICAL! Unauthorized system users SSH logins found (${unauthorized_system_users_logins_count}): ${unauthorized_system_users_logins_details}" "no_log" # Exit with code 2 to indicate that unauthorized users logins were found. exit 2 fi } ####################################### # Get unauthorized system users and their logins. # Globals: # AUTHORIZED_USERS_FILE # CPANEL_USERS_LIST # Arguments: # None ####################################### main() { # Check if the file exists and is not empty if [[ -f "${AUTHORIZED_USERS_FILE}" && -s "${AUTHORIZED_USERS_FILE}" ]]; then # Read the file line by line while IFS=, read -r user access; do # Skip lines that start with a hash (#) or if either user or access is empty. if [[ "$user" =~ ^# || -z "$user" || -z "$access" ]]; then continue fi # Trim any extra whitespace around user and access user=$(echo "$user" | xargs) access=$(echo "$access" | xargs) # Ensure both user and access are non-empty before updating the array if [[ -n "$user" && -n "$access" ]]; then # Update the array with valid user and access AUTHORIZED_USERS["$user"]="$access" else # Log an error message and exit with code 3 if user or access is invalid logger "ERROR! Invalid username or access in ${AUTHORIZED_USERS_FILE}. User: '${user}', Access: '${access}'." "no_log" exit 3 fi done < "${AUTHORIZED_USERS_FILE}" else # Log an error message and exit with code 3 if the file is empty or not found logger "ERROR! File ${AUTHORIZED_USERS_FILE} is either empty or not found." "no_log" exit 3 fi # Invoke function to get any user other than root with UID=0. get_users_with_uid0 # Check if cPanel binary exists before calling the function. if [ ! -f /usr/local/cpanel/cpanel ]; then # Define cPanel users list as empty in case if there is no cPanel. CPANEL_USERS_LIST="" else # Invoke function to get cPanel users list if cPanel is installed. get_cpanel_users fi # Invoke function to get list of system users excluding cPanel users. get_system_users # Invoke function to get list of system users with enabled shell. get_system_users_with_enabled_shell # Invoke function to get recent logins of unauthorized system users. get_unauthorized_system_users_logins # Invoke function to get unauthorized system users with enabled shell. get_unauthorized_system_users # Handle the case when no unauthorized users were found but the lock file exists. if [[ -f "$LOCK_FILE" ]]; then # Invoke function to control lock file. set_lock else # Print a message if everything is ok and exit with code 0. logger "OK. Unauthorized system users and logins are NOT detected." "no_log" exit 0 fi } # Run a default mode. if [[ -z $* ]]; then # Invoke main function main fi