Sync all scripts from website downloads — 352 scripts total

Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
This commit is contained in:
2026-05-25 03:31:08 +02:00
parent dbd6bf0324
commit a1a17e81a1
332 changed files with 174509 additions and 1106 deletions
+706
View File
@@ -0,0 +1,706 @@
#!/usr/bin/env bash
#########################################################################################
#### permissions-fixer.sh — Find and fix common file permission problems ####
#### Scans web roots, shared dirs, home dirs, /tmp for broken permissions, ACLs, ####
#### orphaned ownership, and missing setgid bits ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version 1.0 ####
#### ####
#### Usage: ####
#### ./permissions-fixer.sh # audit mode ####
#### ./permissions-fixer.sh --fix # apply fixes ####
#### ./permissions-fixer.sh --scan /var/www /srv/shared # specific paths ####
#### ####
#### See --help for all options. ####
#########################################################################################
set -euo pipefail
# ── Defaults ──────────────────────────────────────────────────────────
SCAN_PATHS=()
FIX_MODE="false"
DRY_RUN="false"
WEB_USER=""
SKIP_WEB="false"
SKIP_HOME="false"
SKIP_MOUNT="false"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
YES="false"
# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
COUNT_WARN=0
COUNT_FAIL=0
COUNT_FIX=0
COUNT_SKIP=0
# ── Colors ────────────────────────────────────────────────────────────
setup_colors() {
if [[ "$COLOR" == "never" ]]; then
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
return
fi
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
else
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
fi
}
# ── Logging ───────────────────────────────────────────────────────────
log() { echo -e "${CYAN}[INFO]${RESET} $*"; }
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }
die() { err "$*"; exit 1; }
tag_fail() { echo -e " ${RED}[FAIL]${RESET} $*"; ((COUNT_FAIL++)) || true; }
tag_warn() { echo -e " ${YELLOW}[WARN]${RESET} $*"; ((COUNT_WARN++)) || true; }
tag_fix() { echo -e " ${GREEN}[FIX]${RESET} $*"; ((COUNT_FIX++)) || true; }
tag_skip() { echo -e " ${DIM}[SKIP]${RESET} $*"; ((COUNT_SKIP++)) || true; }
section_header() {
echo ""
echo -e " ${BOLD}${CYAN}── $1 ──${RESET}"
echo ""
}
# ── Helpers ───────────────────────────────────────────────────────────
apply_fix() {
local description="$1"
shift
if [[ "$FIX_MODE" == "true" ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
tag_skip "(dry-run) $description"
else
if "$@" 2>/dev/null; then
tag_fix "$description"
else
err "Failed to apply: $description"
fi
fi
fi
}
detect_web_user() {
if [[ -n "$WEB_USER" ]]; then
verbose "Web user override: $WEB_USER"
return
fi
for proc in nginx apache2 httpd; do
if pgrep -x "$proc" &>/dev/null; then
case "$proc" in
nginx) WEB_USER="$(ps -eo user,comm --no-headers | awk '$2=="nginx" && $1!="root" {print $1; exit}')" ;;
apache2) WEB_USER="$(ps -eo user,comm --no-headers | awk '$2=="apache2" && $1!="root" {print $1; exit}')" ;;
httpd) WEB_USER="$(ps -eo user,comm --no-headers | awk '$2=="httpd" && $1!="root" {print $1; exit}')" ;;
esac
if [[ -n "$WEB_USER" ]]; then
verbose "Detected web user from running $proc: $WEB_USER"
return
fi
fi
done
for candidate in www-data nginx apache; do
if id "$candidate" &>/dev/null; then
WEB_USER="$candidate"
verbose "Detected web user from passwd: $WEB_USER"
return
fi
done
WEB_USER=""
verbose "Could not detect web server user"
}
# ══════════════════════════════════════════════════════════════════════
# CHECK FUNCTIONS
# ══════════════════════════════════════════════════════════════════════
check_web_root() {
if [[ "$SKIP_WEB" == "true" ]]; then
verbose "Skipping web root checks (--skip-web)"
return
fi
section_header "Web Root Permissions"
detect_web_user
if [[ -z "$WEB_USER" ]]; then
tag_warn "No web server user detected — skipping web root checks"
log "Use --web-user USER to specify manually"
return
fi
log "Web server user: ${WEB_USER}"
local web_root="/var/www"
for p in "${SCAN_PATHS[@]}"; do
if [[ "$p" == /var/www* ]] || [[ "$p" == /srv/www* ]]; then
web_root="$p"
break
fi
done
if [[ ! -d "$web_root" ]]; then
verbose "Web root ${web_root} does not exist — skipping"
return
fi
log "Scanning web root: ${web_root}"
# Check file ownership
while IFS= read -r file; do
[[ -z "$file" ]] && continue
local owner
owner=$(stat -c '%U' "$file" 2>/dev/null || echo "UNKNOWN")
tag_fail "Not owned by ${WEB_USER}: ${file} (owner: ${owner})"
apply_fix "chown ${WEB_USER}: ${file}" chown "${WEB_USER}:" "$file"
done < <(find "$web_root" -xdev -not -user "$WEB_USER" -not -path "*/\.git/*" 2>/dev/null || true)
# Check directory permissions (should be 755)
while IFS= read -r dir; do
[[ -z "$dir" ]] && continue
local perms
perms=$(stat -c '%a' "$dir" 2>/dev/null || echo "0")
if [[ "$perms" != "755" ]]; then
tag_warn "Directory not 755: ${dir} (${perms})"
apply_fix "chmod 755 ${dir}" chmod 755 "$dir"
else
verbose "OK: ${dir} (${perms})"
fi
done < <(find "$web_root" -xdev -type d -not -path "*/\.git/*" 2>/dev/null || true)
# Check file permissions (should be 644)
while IFS= read -r file; do
[[ -z "$file" ]] && continue
local perms
perms=$(stat -c '%a' "$file" 2>/dev/null || echo "0")
if [[ "$perms" != "644" ]]; then
tag_warn "File not 644: ${file} (${perms})"
apply_fix "chmod 644 ${file}" chmod 644 "$file"
else
verbose "OK: ${file} (${perms})"
fi
done < <(find "$web_root" -xdev -type f -not -path "*/\.git/*" 2>/dev/null || true)
}
check_world_writable() {
section_header "World-Writable Files"
local exclude_args=(-not -path "/tmp/*" -not -path "/var/tmp/*" -not -path "/dev/shm/*"
-not -path "/proc/*" -not -path "/sys/*")
local found=0
for scan_dir in "${SCAN_PATHS[@]}"; do
[[ -d "$scan_dir" ]] || continue
[[ "$scan_dir" == "/tmp" || "$scan_dir" == "/var/tmp" || "$scan_dir" == "/dev/shm" ]] && continue
while IFS= read -r file; do
[[ -z "$file" ]] && continue
tag_fail "World-writable: ${file} ($(stat -c '%a %U:%G' "$file" 2>/dev/null))"
apply_fix "chmod o-w ${file}" chmod o-w "$file"
((found++)) || true
done < <(find "$scan_dir" -xdev -perm -0002 -not -type l "${exclude_args[@]}" 2>/dev/null || true)
done
if [[ "$found" -eq 0 ]]; then
log "No world-writable files found outside /tmp"
fi
}
check_orphaned_files() {
section_header "Orphaned Files (No Valid Owner/Group)"
local found=0
for scan_dir in "${SCAN_PATHS[@]}"; do
[[ -d "$scan_dir" ]] || continue
while IFS= read -r file; do
[[ -z "$file" ]] && continue
local uid gid
uid=$(stat -c '%u' "$file" 2>/dev/null || echo "?")
gid=$(stat -c '%g' "$file" 2>/dev/null || echo "?")
tag_fail "Orphaned file: ${file} (UID=${uid} GID=${gid})"
((found++)) || true
done < <(find "$scan_dir" -xdev \( -nouser -o -nogroup \) -not -path "/proc/*" -not -path "/sys/*" 2>/dev/null || true)
done
if [[ "$found" -eq 0 ]]; then
log "No orphaned files found"
else
log "Orphaned files require manual owner assignment — not auto-fixed"
fi
}
check_setgid_shared() {
section_header "Shared Directory Setgid"
local found=0
local dirs_to_check=()
for p in "${SCAN_PATHS[@]}"; do
if [[ "$p" == /srv* ]]; then
dirs_to_check+=("$p")
fi
done
[[ -d "/srv" ]] && dirs_to_check+=("/srv")
# Deduplicate
local -A seen=()
local unique_dirs=()
for d in "${dirs_to_check[@]}"; do
if [[ -z "${seen[$d]+x}" ]]; then
seen[$d]=1
unique_dirs+=("$d")
fi
done
for scan_dir in "${unique_dirs[@]}"; do
[[ -d "$scan_dir" ]] || continue
while IFS= read -r dir; do
[[ -z "$dir" ]] && continue
local perms
perms=$(stat -c '%a' "$dir" 2>/dev/null || echo "0")
# Check if group-writable (bit 1 of group octal) but not setgid
local group_octal="${perms:1:1}"
if (( group_octal % 2 == 0 )); then
verbose "Not group-writable, skipping: $dir"
continue
fi
# Check setgid
local mode
mode=$(stat -c '%04a' "$dir" 2>/dev/null || echo "0000")
if [[ "${mode:0:1}" != "2" && "${mode:0:1}" != "3" && "${mode:0:1}" != "6" && "${mode:0:1}" != "7" ]]; then
tag_warn "Group-writable but no setgid: ${dir} (${mode})"
apply_fix "chmod g+s ${dir}" chmod g+s "$dir"
((found++)) || true
else
verbose "OK setgid: ${dir} (${mode})"
fi
done < <(find "$scan_dir" -xdev -type d -not -path "/proc/*" -not -path "/sys/*" 2>/dev/null || true)
done
if [[ "$found" -eq 0 ]]; then
log "No missing setgid bits found on shared directories"
fi
}
check_home_dirs() {
if [[ "$SKIP_HOME" == "true" ]]; then
verbose "Skipping home directory checks (--skip-home)"
return
fi
section_header "Home Directory Permissions"
[[ -d "/home" ]] || { verbose "/home does not exist — skipping"; return; }
local found=0
for home_dir in /home/*/; do
[[ -d "$home_dir" ]] || continue
home_dir="${home_dir%/}"
local perms
perms=$(stat -c '%a' "$home_dir" 2>/dev/null || echo "0")
# Check home dir is 700 or 750
if [[ "$perms" != "700" && "$perms" != "750" ]]; then
tag_warn "Home dir too open: ${home_dir} (${perms})"
apply_fix "chmod 750 ${home_dir}" chmod 750 "$home_dir"
((found++)) || true
else
verbose "OK: ${home_dir} (${perms})"
fi
# Check .ssh directory
local ssh_dir="${home_dir}/.ssh"
if [[ -d "$ssh_dir" ]]; then
local ssh_perms
ssh_perms=$(stat -c '%a' "$ssh_dir" 2>/dev/null || echo "0")
if [[ "$ssh_perms" != "700" ]]; then
tag_fail "SSH dir not 700: ${ssh_dir} (${ssh_perms})"
apply_fix "chmod 700 ${ssh_dir}" chmod 700 "$ssh_dir"
((found++)) || true
else
verbose "OK: ${ssh_dir} (${ssh_perms})"
fi
# Check authorized_keys
if [[ -f "${ssh_dir}/authorized_keys" ]]; then
local ak_perms
ak_perms=$(stat -c '%a' "${ssh_dir}/authorized_keys" 2>/dev/null || echo "0")
if [[ "$ak_perms" != "600" ]]; then
tag_fail "authorized_keys not 600: ${ssh_dir}/authorized_keys (${ak_perms})"
apply_fix "chmod 600 ${ssh_dir}/authorized_keys" chmod 600 "${ssh_dir}/authorized_keys"
((found++)) || true
fi
fi
# Check private keys
while IFS= read -r key_file; do
[[ -z "$key_file" ]] && continue
local key_perms
key_perms=$(stat -c '%a' "$key_file" 2>/dev/null || echo "0")
if [[ "$key_perms" != "600" ]]; then
tag_fail "Private key not 600: ${key_file} (${key_perms})"
apply_fix "chmod 600 ${key_file}" chmod 600 "$key_file"
((found++)) || true
fi
done < <(find "$ssh_dir" -maxdepth 1 -type f \( -name "id_*" -not -name "*.pub" \) 2>/dev/null || true)
fi
done
if [[ "$found" -eq 0 ]]; then
log "All home directory permissions look correct"
fi
}
check_broken_acls() {
section_header "Broken ACL Masks"
if ! command -v getfacl &>/dev/null; then
log "getfacl not installed — skipping ACL checks"
return
fi
local found=0
for scan_dir in "${SCAN_PATHS[@]}"; do
[[ -d "$scan_dir" ]] || continue
while IFS= read -r file; do
[[ -z "$file" ]] && continue
# Only check files that actually have ACLs (+ in ls output or via getfacl)
local acl_output
acl_output=$(getfacl -p "$file" 2>/dev/null || true)
[[ -z "$acl_output" ]] && continue
local has_named_entries="false"
local mask_line=""
while IFS= read -r line; do
case "$line" in
user:?*:*) has_named_entries="true" ;;
group:?*:*) has_named_entries="true" ;;
mask::*) mask_line="$line" ;;
esac
done <<< "$acl_output"
if [[ "$has_named_entries" == "false" || -z "$mask_line" ]]; then
continue
fi
local mask_perms="${mask_line##*::}"
# Check each named entry against the mask
while IFS= read -r line; do
local entry_perms=""
case "$line" in
user:?*:*) entry_perms="${line##*:}" ;;
group:?*:*) entry_perms="${line##*:}" ;;
*) continue ;;
esac
# Compare: if entry has bits the mask doesn't, effective is reduced
local reduced="false"
if [[ "$entry_perms" == *r* && "$mask_perms" != *r* ]]; then reduced="true"; fi
if [[ "$entry_perms" == *w* && "$mask_perms" != *w* ]]; then reduced="true"; fi
if [[ "$entry_perms" == *x* && "$mask_perms" != *x* ]]; then reduced="true"; fi
if [[ "$reduced" == "true" ]]; then
tag_warn "ACL mask restricts effective perms: ${file} (entry=${entry_perms}, mask=${mask_perms})"
((found++)) || true
break
fi
done <<< "$acl_output"
done < <(find "$scan_dir" -xdev -maxdepth 3 -not -path "/proc/*" -not -path "/sys/*" 2>/dev/null | head -5000 || true)
done
if [[ "$found" -eq 0 ]]; then
log "No broken ACL masks found"
else
log "ACL mask issues require manual review — not auto-fixed"
fi
}
check_immutable_files() {
section_header "Immutable Files (chattr +i)"
if ! command -v lsattr &>/dev/null; then
log "lsattr not installed — skipping immutable file checks"
return
fi
local found=0
for scan_dir in "${SCAN_PATHS[@]}"; do
[[ -d "$scan_dir" ]] || continue
while IFS= read -r line; do
[[ -z "$line" ]] && continue
# lsattr format: "----i---------e------- /path/to/file"
# skip directory headers like "/tmp/subdir:" (no attribute field)
[[ "$line" =~ ^[[:space:]]*[-a-zA-Z]+[[:space:]]+ ]] || continue
local attrs file
attrs="${line%% *}"
file="${line#* }"
# check for immutable flag (4th char in attrs is 'i')
if [[ "$attrs" == ????i* ]]; then
tag_warn "Immutable file: ${file}"
((found++)) || true
fi
done < <(lsattr -R "$scan_dir" 2>/dev/null | grep -v "^lsattr:" || true)
done
if [[ "$found" -eq 0 ]]; then
log "No immutable files found"
else
log "Immutable files reported for awareness — not auto-fixed"
fi
}
check_mount_options() {
if [[ "$SKIP_MOUNT" == "true" ]]; then
verbose "Skipping mount option checks (--skip-mount)"
return
fi
section_header "Mount Option Audit"
local found=0
local -A mount_checks=(
["/tmp"]="noexec nosuid nodev"
["/var/tmp"]="noexec nosuid nodev"
["/dev/shm"]="noexec nosuid nodev"
)
for mount_point in "${!mount_checks[@]}"; do
local expected_opts="${mount_checks[$mount_point]}"
local current_opts
current_opts=$(findmnt -n -o OPTIONS "$mount_point" 2>/dev/null || echo "")
if [[ -z "$current_opts" ]]; then
verbose "${mount_point} is not a separate mount point — skipping"
continue
fi
log "Checking mount: ${mount_point} (${current_opts})"
for opt in $expected_opts; do
if echo "$current_opts" | grep -qw "$opt"; then
verbose "OK: ${mount_point} has ${opt}"
else
tag_fail "Missing mount option: ${mount_point} needs ${opt}"
((found++)) || true
fi
done
done
if [[ "$found" -eq 0 ]]; then
log "All checked mount points have recommended security options"
else
log "Mount option changes require fstab edit + remount — not auto-fixed"
fi
}
# ══════════════════════════════════════════════════════════════════════
# CONFIRMATION
# ══════════════════════════════════════════════════════════════════════
confirm_fix() {
if [[ "$FIX_MODE" != "true" || "$DRY_RUN" == "true" || "$YES" == "true" ]]; then
return 0
fi
echo ""
echo -e " ${BOLD}${YELLOW}⚠ WARNING: --fix mode will modify file permissions${RESET}"
echo -e " ${YELLOW}Back up critical data before proceeding.${RESET}"
echo ""
read -rp " Continue with fixes? [y/N] " answer
case "$answer" in
[yY][eE][sS]|[yY]) return 0 ;;
*) log "Aborted by user."; exit 0 ;;
esac
}
require_root_for_fix() {
if [[ "$FIX_MODE" == "true" && "$DRY_RUN" != "true" && $EUID -ne 0 ]]; then
die "--fix mode requires root privileges. Run with sudo."
fi
}
# ══════════════════════════════════════════════════════════════════════
# SUMMARY
# ══════════════════════════════════════════════════════════════════════
print_summary() {
echo ""
echo -e " ${BOLD}══════════════════════════════════════════${RESET}"
echo -e " ${BOLD}Permissions Fixer Summary${RESET}"
echo -e " ${BOLD}══════════════════════════════════════════${RESET}"
echo ""
printf " %-30s %b\n" "Warnings [WARN]:" "${YELLOW}${COUNT_WARN}${RESET}"
printf " %-30s %b\n" "Security risks [FAIL]:" "${RED}${COUNT_FAIL}${RESET}"
printf " %-30s %b\n" "Fixes applied [FIX]:" "${GREEN}${COUNT_FIX}${RESET}"
printf " %-30s %b\n" "Skipped (dry-run) [SKIP]:" "${DIM}${COUNT_SKIP}${RESET}"
local total=$((COUNT_WARN + COUNT_FAIL + COUNT_FIX + COUNT_SKIP))
echo ""
printf " %-30s %d\n" "Total findings:" "$total"
if [[ "$FIX_MODE" == "true" && "$DRY_RUN" == "true" ]]; then
echo ""
log "Dry-run mode — no changes were made. Re-run without --dry-run to apply."
fi
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# USAGE
# ══════════════════════════════════════════════════════════════════════
usage() {
cat <<EOF
${SCRIPT_NAME} — Find and fix common file permission problems
USAGE:
${SCRIPT_NAME} [OPTIONS]
OPTIONS:
--scan PATH [PATH...] Scan specific paths (default: /var/www /home /srv /tmp)
--fix Apply fixes (default: audit/report only). Requires root.
--dry-run Show what --fix would do without doing it
--web-user USER Override web server user detection
--skip-web Skip web root checks
--skip-home Skip home directory checks
--skip-mount Skip mount option checks
--yes Skip confirmation prompt for --fix mode
--verbose Show all files checked, not just issues
--no-color Disable color output
-h, --help Show this help
EXAMPLES:
# Audit mode (default) — report issues without changing anything
./permissions-fixer.sh
# Scan specific paths
./permissions-fixer.sh --scan /var/www /srv/shared
# Preview fixes without applying
./permissions-fixer.sh --fix --dry-run
# Apply fixes (requires root)
sudo ./permissions-fixer.sh --fix
# Apply fixes non-interactively
sudo ./permissions-fixer.sh --fix --yes
# Override web server user
./permissions-fixer.sh --web-user nginx
# Skip home and mount checks
./permissions-fixer.sh --skip-home --skip-mount
EXIT CODES:
0 No issues found
1 Warnings found
2 Security risks found
EOF
}
# ══════════════════════════════════════════════════════════════════════
# ARGUMENT PARSING
# ══════════════════════════════════════════════════════════════════════
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--scan)
shift
while [[ $# -gt 0 && "$1" != --* ]]; do
SCAN_PATHS+=("$1")
shift
done
;;
--fix)
FIX_MODE="true"; shift ;;
--dry-run)
DRY_RUN="true"; shift ;;
--web-user)
WEB_USER="$2"; shift 2 ;;
--skip-web)
SKIP_WEB="true"; shift ;;
--skip-home)
SKIP_HOME="true"; shift ;;
--skip-mount)
SKIP_MOUNT="true"; shift ;;
--yes)
YES="true"; shift ;;
--verbose)
VERBOSE="true"; shift ;;
--no-color)
COLOR="never"; shift ;;
--help|-h)
usage
exit 0 ;;
*)
err "Unknown option: $1"
echo "Run ${SCRIPT_NAME} --help for usage" >&2
exit 1 ;;
esac
done
# Default scan paths if none specified
if [[ ${#SCAN_PATHS[@]} -eq 0 ]]; then
SCAN_PATHS=(/var/www /home /srv /tmp)
fi
}
# ══════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════
main() {
setup_colors
parse_args "$@"
setup_colors # re-init in case --no-color was passed
local mode_label="Audit"
if [[ "$FIX_MODE" == "true" && "$DRY_RUN" == "true" ]]; then
mode_label="Dry-run"
elif [[ "$FIX_MODE" == "true" ]]; then
mode_label="Fix"
fi
echo ""
echo -e "${BOLD}Permissions Fixer — $(hostname -f 2>/dev/null || hostname)${RESET}"
echo -e "${DIM}$(date '+%Y-%m-%d %H:%M:%S %Z')${RESET}"
echo -e "${DIM}Mode: ${mode_label}${RESET}"
echo -e "${DIM}Scanning: ${SCAN_PATHS[*]}${RESET}"
require_root_for_fix
confirm_fix
check_web_root
check_world_writable
check_orphaned_files
check_setgid_shared
check_home_dirs
check_broken_acls
check_immutable_files
check_mount_options
print_summary
# Exit code based on severity
if [[ "$COUNT_FAIL" -gt 0 ]]; then
exit 2
elif [[ "$COUNT_WARN" -gt 0 ]]; then
exit 1
fi
exit 0
}
main "$@"