mirror of
https://github.com/Kicksecure/security-misc.git
synced 2025-05-02 22:14:50 -04:00

Similar to: chmod og-ugx /path/to/filename Removing execution permission is useful to make binaries such as 'su' fail closed rather than fail open if suid was removed from these. Do not remove read access since no security benefit and easier to manually undo for users. chmod 744
416 lines
15 KiB
Bash
Executable file
416 lines
15 KiB
Bash
Executable file
#!/bin/bash
|
|
|
|
## Copyright (C) 2012 - 2019 ENCRYPTED SUPPORT LP <adrelanos@riseup.net>
|
|
## See the file COPYING for copying conditions.
|
|
|
|
## https://forums.whonix.org/t/disable-suid-binaries/7706
|
|
## https://forums.whonix.org/t/re-mount-home-and-other-with-noexec-and-nosuid-among-other-useful-mount-options-for-better-security/7707
|
|
|
|
## To view previous modes and how these were changed:
|
|
## meld /var/lib/permission-hardening/existing_mode/statoverride /var/lib/permission-hardening/new_mode/statoverride
|
|
|
|
## To undo:
|
|
## sudo /usr/lib/security-misc/permission-hardening-undo
|
|
|
|
#set -x
|
|
set -e
|
|
set -o pipefail
|
|
|
|
exit_code=0
|
|
|
|
mkdir -p /var/lib/permission-hardening/existing_mode
|
|
mkdir -p /var/lib/permission-hardening/new_mode
|
|
dpkg_admindir_parameter_existing_mode="--admindir /var/lib/permission-hardening/existing_mode"
|
|
dpkg_admindir_parameter_new_mode="--admindir /var/lib/permission-hardening/new_mode"
|
|
|
|
echo_wrapper_ignore() {
|
|
echo "run: $@"
|
|
"$@" 2>/dev/null || true
|
|
}
|
|
|
|
echo_wrapper_audit() {
|
|
echo "run: $@"
|
|
return_code=0
|
|
"$@" || \
|
|
{ \
|
|
return_code="$?" ; \
|
|
exit_code=203 ; \
|
|
echo "ERROR: above command failed with exit code '$return_code'! calling function name: '${FUNCNAME[1]}'" >&2 ; \
|
|
};
|
|
}
|
|
|
|
echo_wrapper_silent_audit() {
|
|
## TODO: remove echo
|
|
echo "run (debugging): $@"
|
|
return_code=0
|
|
"$@" || \
|
|
{ \
|
|
return_code="$?" ; \
|
|
exit_code=204 ; \
|
|
echo "ERROR: above command '$@' failed with exit code '$return_code'! calling function name: '${FUNCNAME[1]}'" >&2 ; \
|
|
};
|
|
}
|
|
|
|
add_nosuid_statoverride_entry() {
|
|
local fso_to_process
|
|
fso_to_process="$fso"
|
|
local should_be_counter
|
|
should_be_counter="$(find "$fso_to_process" -perm /u=s,g=s | wc -l)" || true
|
|
local counter_actual
|
|
counter_actual=0
|
|
|
|
local line
|
|
while read -r line; do
|
|
true "line: $line"
|
|
counter_actual="$(( counter_actual + 1 ))"
|
|
|
|
local arr file_name existing_mode existing_owner existing_group
|
|
arr=($line)
|
|
file_name="${arr[0]}"
|
|
existing_mode="${arr[1]}"
|
|
existing_owner="${arr[2]}"
|
|
existing_group="${arr[3]}"
|
|
|
|
if [ "$arr" = "" ]; then
|
|
echo "ERROR: arr is empty. line: '$line'" >&2
|
|
continue
|
|
fi
|
|
if [ "$file_name" = "" ]; then
|
|
echo "ERROR: file_name is empty. line: '$line'" >&2
|
|
continue
|
|
fi
|
|
if [ "$existing_mode" = "" ]; then
|
|
echo "ERROR: existing_mode is empty. line: '$line'" >&2
|
|
continue
|
|
fi
|
|
if [ "$existing_owner" = "" ]; then
|
|
echo "ERROR: existing_owner is empty. line: '$line'" >&2
|
|
continue
|
|
fi
|
|
if [ "$existing_group" = "" ]; then
|
|
echo "ERROR: existing_group is empty. line: '$line'" >&2
|
|
continue
|
|
fi
|
|
|
|
## -h file True if file is a symbolic Link.
|
|
## -u file True if file has its set-user-id bit set.
|
|
## -g file True if file has its set-group-id bit set.
|
|
|
|
if test -h "$file_name" ; then
|
|
## https://forums.whonix.org/t/kernel-hardening/7296/323
|
|
true "skip symlink: $file_name"
|
|
continue
|
|
fi
|
|
|
|
if test -d "$file_name" ; then
|
|
true "skip directory: $file_name"
|
|
continue
|
|
fi
|
|
|
|
local setuid setuid_output setsgid setsgid_output
|
|
setuid=""
|
|
setuid_output=""
|
|
if test -u "$file_name" ; then
|
|
setuid=true
|
|
setuid_output="set-user-id"
|
|
fi
|
|
setsgid=""
|
|
setsgid_output=""
|
|
if test -g "$file_name" ; then
|
|
setsgid=true
|
|
setsgid_output="set-group-id"
|
|
fi
|
|
|
|
local setuid_or_setsgid
|
|
setuid_or_setsgid=""
|
|
if [ "$setuid" = "true" ] || [ "$setsgid" = "true" ]; then
|
|
setuid_or_setsgid=true
|
|
fi
|
|
if [ "$setuid_or_setsgid" = "" ]; then
|
|
continue
|
|
fi
|
|
|
|
## Remove suid / gid and execute permission for 'group' and 'others'.
|
|
## Similar to: chmod og-ugx /path/to/filename
|
|
## Removing execution permission is useful to make binaries such as 'su' fail closed rather
|
|
## than fail open if suid was removed from these.
|
|
## Do not remove read access since no security benefit and easier to manually undo for users.
|
|
## Are there suid or sgid binaries which are still useful if suid / sgid has been removed from these?
|
|
new_mode="744"
|
|
|
|
local is_whitelisted
|
|
is_whitelisted=""
|
|
for white_list_entry in $whitelist ; do
|
|
if [ "$file_name" = "$white_list_entry" ]; then
|
|
is_whitelisted="true"
|
|
## Stop looping through the whitelist.
|
|
break
|
|
fi
|
|
done
|
|
|
|
local is_match_whitelisted
|
|
is_match_whitelisted=""
|
|
for matchwhite_list_entry in $matchwhitelist ; do
|
|
if echo "$file_name" | grep -q "$matchwhite_list_entry" ; then
|
|
is_match_whitelisted="true"
|
|
## Stop looping through the matchwhitelist.
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ "$is_whitelisted" = "true" ]; then
|
|
echo "INFO: SKIP whitelisted - $setuid_output $setsgid_output found - file_name: '$file_name' | existing_mode: '$existing_mode'"
|
|
continue
|
|
fi
|
|
|
|
if [ "$is_match_whitelisted" = "true" ]; then
|
|
echo "INFO: SKIP matchwhitelisted - $setuid_output $setsgid_output found - file_name: '$file_name' | existing_mode: '$existing_mode' | matchwhite_list_entry: '$matchwhite_list_entry'"
|
|
continue
|
|
fi
|
|
|
|
echo "INFO: $setuid_output $setsgid_output found - file_name: '$file_name' | existing_mode: '$existing_mode' | new_mode: '$new_mode'"
|
|
|
|
if dpkg-statoverride $dpkg_admindir_parameter_existing_mode --list "$file_name" >/dev/null ; then
|
|
true "OK Existing mode already saved previously. No need to save again."
|
|
else
|
|
## Save existing_mode in separate database.
|
|
## Not using --update as not intending to enforce existing_mode.
|
|
echo_wrapper_silent_audit dpkg-statoverride $dpkg_admindir_parameter_existing_mode --add "$existing_owner" "$existing_group" "$existing_mode" "$file_name"
|
|
fi
|
|
|
|
## No need to check "dpkg-statoverride --list" for existing entries.
|
|
## If existing_mode was correct already, we would not have reached this point.
|
|
## Since existing_mode is incorrect, remove from dpkg-statoverride and re-add.
|
|
|
|
## Remove from real database.
|
|
echo_wrapper_ignore dpkg-statoverride --remove "$file_name"
|
|
|
|
## Remove from separate database.
|
|
echo_wrapper_ignore dpkg-statoverride $dpkg_admindir_parameter_new_mode --remove "$file_name"
|
|
|
|
## Add to real database and use --update to make changes on disk.
|
|
echo_wrapper_audit dpkg-statoverride --add --update "$existing_owner" "$existing_group" "$new_mode" "$file_name"
|
|
|
|
## Not using --update as this is only for recording.
|
|
echo_wrapper_silent_audit dpkg-statoverride $dpkg_admindir_parameter_new_mode --add "$existing_owner" "$existing_group" "$new_mode" "$file_name"
|
|
|
|
## /lib will hit ARG_MAX if using bash 'shopt -s globstar' and '/lib/**'.
|
|
## Using 'find' with '-perm /u=s,g=s' is faster and avoids ARG_MAX.
|
|
## https://forums.whonix.org/t/disable-suid-binaries/7706/17
|
|
done < <( find "$fso_to_process" -perm /u=s,g=s -print0 | xargs -I{} -0 stat -c "%n %a %U %G" {} )
|
|
|
|
## Sanity test.
|
|
if [ ! "$should_be_counter" = "$counter_actual" ]; then
|
|
echo "INFO: fso_to_process: '$fso_to_process' | counter_actual : '$counter_actual'"
|
|
echo "INFO: fso_to_process: '$fso_to_process' | should_be_counter: '$should_be_counter'"
|
|
exit_code=202
|
|
echo "ERROR: counter does not check out." >&2
|
|
fi
|
|
}
|
|
|
|
set_file_perms() {
|
|
echo "INFO: START parsing config_file: '$config_file'"
|
|
local line
|
|
while read -r line; do
|
|
if [ "$line" = "" ]; then
|
|
continue
|
|
fi
|
|
|
|
if [[ "$line" =~ ^# ]]; then
|
|
continue
|
|
fi
|
|
|
|
if [[ "$line" =~ [0-9a-zA-Z/] ]]; then
|
|
true "OK line contains only white listed characters."
|
|
else
|
|
exit_code=200
|
|
echo "ERROR: cannot parse line with invalid character: $line" >&2
|
|
continue
|
|
fi
|
|
|
|
#global fso
|
|
local mode_from_config owner_from_config group_from_config capability_from_config
|
|
if ! read -r fso mode_from_config owner_from_config group_from_config capability_from_config <<< "$line" ; then
|
|
exit_code=201
|
|
echo "ERROR: cannot parse line: $line" >&2
|
|
continue
|
|
fi
|
|
|
|
local fso_without_trailing_slash
|
|
fso_without_trailing_slash="${fso%/}"
|
|
|
|
if [ "$mode_from_config" = "whitelist" ]; then
|
|
## TODO: test/add white spaces inside file name support
|
|
whitelist+="$fso_without_trailing_slash "
|
|
continue
|
|
fi
|
|
|
|
if [ "$mode_from_config" = "matchwhitelist" ]; then
|
|
## TODO: test/add white spaces inside file name support
|
|
matchwhitelist+="$fso "
|
|
continue
|
|
fi
|
|
|
|
if ! [ -e "$fso" ]; then
|
|
echo "INFO: fso: '$fso' - does not exist. This is likely normal."
|
|
continue
|
|
fi
|
|
|
|
## Use dpkg-statoverride so permissions are not reset during upgrades.
|
|
|
|
if [ "$mode_from_config" = "nosuid" ]; then
|
|
## If mode_from_config is "nosuid" the config does not set owner and
|
|
## group. Therefore do not enforce owner/group check.
|
|
|
|
add_nosuid_statoverride_entry
|
|
else
|
|
local string_length_of_mode_from_config
|
|
string_length_of_mode_from_config="${#mode_from_config}"
|
|
if [ "$string_length_of_mode_from_config" -gt "4" ]; then
|
|
echo "ERROR: Mode '$mode_from_config' is invalid!" >&2
|
|
continue
|
|
fi
|
|
if [ "$string_length_of_mode_from_config" -lt "3" ]; then
|
|
echo "ERROR: Mode '$mode_from_config' is invalid!" >&2
|
|
continue
|
|
fi
|
|
|
|
if ! getent passwd | grep -q "^${owner_from_config}:" ; then
|
|
echo "ERROR: owner_from_config '$owner_from_config' does not exist!" >&2
|
|
continue
|
|
fi
|
|
|
|
if ! getent group | grep -q "^${group_from_config}:" ; then
|
|
echo "ERROR: group_from_config '$group_from_config' does not exist!" >&2
|
|
continue
|
|
fi
|
|
|
|
local mode_for_grep
|
|
mode_for_grep="$mode_from_config"
|
|
first_character_of_mode_from_config="${mode_from_config::1}"
|
|
if [ "$first_character_of_mode_from_config" = "0" ]; then
|
|
## Remove leading '0'.
|
|
mode_for_grep="${mode_from_config:1}"
|
|
fi
|
|
|
|
local stat_output
|
|
stat_output=""
|
|
if ! stat_output="$(stat -c "%n %a %U %G" "$fso_without_trailing_slash")" ; then
|
|
echo "ERROR: failed to run 'stat' for fso_without_trailing_slash: '$fso_without_trailing_slash'!" >&2
|
|
continue
|
|
fi
|
|
|
|
local arr file_name existing_mode existing_owner existing_group
|
|
arr=($stat_output)
|
|
file_name="${arr[0]}"
|
|
existing_mode="${arr[1]}"
|
|
existing_owner="${arr[2]}"
|
|
existing_group="${arr[3]}"
|
|
|
|
if [ "$arr" = "" ]; then
|
|
echo "ERROR: arr is empty. stat_output: '$stat_output' | line: '$line'" >&2
|
|
continue
|
|
fi
|
|
if [ "$file_name" = "" ]; then
|
|
echo "ERROR: file_name is empty. stat_output: '$stat_output' | line: '$line'" >&2
|
|
continue
|
|
fi
|
|
if [ "$existing_mode" = "" ]; then
|
|
echo "ERROR: existing_mode is empty. stat_output: '$stat_output' | line: '$line'" >&2
|
|
continue
|
|
fi
|
|
if [ "$existing_owner" = "" ]; then
|
|
echo "ERROR: existing_owner is empty. stat_output: '$stat_output' | line: '$line'" >&2
|
|
continue
|
|
fi
|
|
if [ "$existing_group" = "" ]; then
|
|
echo "ERROR: $existing_group is empty. stat_output: '$stat_output' | line: '$line'" >&2
|
|
continue
|
|
fi
|
|
|
|
## Check there is an entry for the fso.
|
|
##
|
|
## example: dpkg-statoverride --list | grep /home
|
|
## output:
|
|
## root root 755 /home
|
|
##
|
|
## dpkg-statoverride does not show leading '0'.
|
|
if dpkg-statoverride --list "$fso_without_trailing_slash" >/dev/null ; then
|
|
true "There is an fso entry. Check if owner/group/mode match."
|
|
if dpkg-statoverride --list | grep -q "$owner_from_config $group_from_config $mode_for_grep $fso_without_trailing_slash" ; then
|
|
true "OK The owner/group/mode matches. No further action required."
|
|
else
|
|
true "The owner/group/mode do not match, therefore remove and re-add the entry to update it."
|
|
## fso_without_trailing_slash instead of fso to prevent
|
|
## "dpkg-statoverride: warning: stripping trailing /"
|
|
|
|
if dpkg-statoverride $dpkg_admindir_parameter_existing_mode --list "$fso_without_trailing_slash" >/dev/null ; then
|
|
true "OK Existing mode already saved previously. No need to save again."
|
|
else
|
|
## Save existing_mode in separate database.
|
|
## Not using --update as not intending to enforce existing_mode.
|
|
echo_wrapper_silent_audit dpkg-statoverride $dpkg_admindir_parameter_existing_mode --add "$existing_owner" "$existing_group" "$existing_mode" "$fso_without_trailing_slash"
|
|
fi
|
|
|
|
echo_wrapper_audit dpkg-statoverride $dpkg_admindir_parameter_new_mode --remove "$fso_without_trailing_slash"
|
|
|
|
## Remove from and add to real database.
|
|
echo_wrapper_audit dpkg-statoverride --remove "$fso_without_trailing_slash"
|
|
echo_wrapper_audit dpkg-statoverride --add --update "$owner_from_config" "$group_from_config" "$mode_from_config" "$fso_without_trailing_slash"
|
|
|
|
## Save in separate database.
|
|
## Not using --update as this is only for saving.
|
|
echo_wrapper_silent_audit dpkg-statoverride $dpkg_admindir_parameter_new_mode --add "$owner_from_config" "$group_from_config" "$mode_from_config" "$fso_without_trailing_slash"
|
|
fi
|
|
else
|
|
true "There is no fso entry. Therefore add one."
|
|
|
|
if dpkg-statoverride $dpkg_admindir_parameter_existing_mode --list "$fso_without_trailing_slash" >/dev/null ; then
|
|
true "OK Existing mode already saved previously. No need to save again."
|
|
else
|
|
## Save existing_mode in separate database.
|
|
## Not using --update as not intending to enforce existing_mode.
|
|
echo_wrapper_silent_audit dpkg-statoverride $dpkg_admindir_parameter_existing_mode --add "$existing_owner" "$existing_group" "$existing_mode" "$fso_without_trailing_slash"
|
|
fi
|
|
|
|
## Add to real database.
|
|
echo_wrapper_audit dpkg-statoverride --add --update "$owner_from_config" "$group_from_config" "$mode_from_config" "$fso_without_trailing_slash"
|
|
|
|
## Save in separate database.
|
|
## Not using --update as this is only for saving.
|
|
echo_wrapper_silent_audit dpkg-statoverride $dpkg_admindir_parameter_new_mode --add "$owner_from_config" "$group_from_config" "$mode_from_config" "$fso_without_trailing_slash"
|
|
fi
|
|
fi
|
|
|
|
if [ "$capability_from_config" = "" ]; then
|
|
continue
|
|
fi
|
|
|
|
if [ "$capability_from_config" = "none" ]; then
|
|
echo_wrapper_audit setcap -r "$fso"
|
|
else
|
|
if ! capsh --print | grep "Bounding set" | grep -q "$capability_from_config" ; then
|
|
echo "ERROR: capability_from_config '$capability_from_config' does not exist!" >&2
|
|
continue
|
|
fi
|
|
|
|
echo_wrapper_audit setcap "${capability_from_config}+ep" "$fso"
|
|
fi
|
|
done < "$config_file"
|
|
echo "INFO: END parsing config_file: '$config_file'"
|
|
}
|
|
|
|
parse_config_folder() {
|
|
shopt -s nullglob
|
|
for config_file in /etc/permission-hardening.d/*.conf /usr/local/etc/permission-hardening.d/*.conf; do
|
|
set_file_perms
|
|
done
|
|
}
|
|
|
|
parse_config_folder
|
|
|
|
if [ ! "$exit_code" = "0" ]; then
|
|
echo "ERROR: Will exit with non-zero exit code: '$exit_code'" >&2
|
|
fi
|
|
|
|
exit "$exit_code"
|