Skip to content

Instantly share code, notes, and snippets.

@BlueSkyXN
Last active May 14, 2026 06:46
Show Gist options
  • Select an option

  • Save BlueSkyXN/4777513302311bfac05a65a710f4e25f to your computer and use it in GitHub Desktop.

Select an option

Save BlueSkyXN/4777513302311bfac05a65a710f4e25f to your computer and use it in GitHub Desktop.
nginx-cve42945-audit.sh
#!/bin/sh
# nginx-cve42945-audit-v7.sh
# Read-only NGINX / BaoTa / SafeLine audit collector for CVE-2026-42945-style rewrite risk patterns.
# It does NOT modify config, reload/restart services, or send HTTP requests.
#
# Optional environment variables:
# OUT_BASE=/root # output root directory; fallback to /tmp if not writable
# COLLECT_FULL_CONFIG=1 # include sanitized full scanned config copies
# COLLECT_RAW_CONFIG=1 # include raw scanned config copies; not recommended
# LOG_SCAN=0 # disable nginx log suspicious excerpt scanning
# COLLECT_FULL_LOGS=1 # collect full nginx error logs only; access logs are never fully copied by default
# MAX_CONF_SIZE_KB=5120 # skip config-like files larger than this
# LOG_MAX_LINES=300 # max suspicious log lines per log file
# SINGLE_LOG=1 # write one combined .log artifact; default enabled
# PACKAGE_TGZ=1 # also build .tgz package; default disabled
# KEEP_WORK_DIR=1 # keep intermediate output directory; default disabled when using single log
# INCLUDE_NGINX_T_IN_LOG=1 # include sanitized nginx -T dumps in the single log; default disabled
# SAFELINE_NGINX_ROOT=/data/safeline/resources/nginx
set -u
SCRIPT_VERSION="v7-single-log-nginx-baota-safeline"
TS="$(date +%Y%m%d-%H%M%S 2>/dev/null || echo unknown-time)"
HOST="$(hostname 2>/dev/null || uname -n 2>/dev/null || echo unknown-host)"
HOST_SAFE="$(printf '%s' "$HOST" | sed 's/[^A-Za-z0-9_.-]/_/g')"
OUT_BASE="${OUT_BASE:-/root}"
if [ ! -d "$OUT_BASE" ] || [ ! -w "$OUT_BASE" ]; then
OUT_BASE="/tmp"
fi
OUT_DIR="$OUT_BASE/nginx-cve42945-audit-$HOST_SAFE-$TS"
mkdir -p "$OUT_DIR" 2>/dev/null || {
OUT_DIR="/tmp/nginx-cve42945-audit-$HOST_SAFE-$TS"
mkdir -p "$OUT_DIR" || exit 1
}
REPORT="$OUT_DIR/console-report.txt"
SUMMARY="$OUT_DIR/final-summary.txt"
SINGLE_LOG="${SINGLE_LOG:-1}"
PACKAGE_TGZ="${PACKAGE_TGZ:-0}"
KEEP_WORK_DIR="${KEEP_WORK_DIR:-0}"
INCLUDE_NGINX_T_IN_LOG="${INCLUDE_NGINX_T_IN_LOG:-0}"
SINGLE_LOG_FILE="${SINGLE_LOG_FILE:-$OUT_BASE/nginx-cve42945-audit-$HOST_SAFE-$TS.log}"
SAFELINE_NGINX_ROOT="${SAFELINE_NGINX_ROOT:-/data/safeline/resources/nginx}"
mkdir -p \
"$OUT_DIR/system" \
"$OUT_DIR/process" \
"$OUT_DIR/nginx" \
"$OUT_DIR/nginx/nginxT" \
"$OUT_DIR/nginx/config-context" \
"$OUT_DIR/nginx/config-full-sanitized" \
"$OUT_DIR/nginx/config-full-raw" \
"$OUT_DIR/baota" \
"$OUT_DIR/safeline" \
"$OUT_DIR/logs" \
"$OUT_DIR/tmp"
say() {
printf '%s\n' "$*" | tee -a "$REPORT"
}
run_capture() {
# usage: run_capture output_file command args...
out="$1"
shift
{
printf '### command:'
for a in "$@"; do printf ' %s' "$a"; done
printf '\n'
"$@"
rc=$?
printf '\n### exit_code: %s\n' "$rc"
} > "$out" 2>&1
}
append_unique() {
file="$1"
val="$2"
[ -n "$val" ] || return 0
grep -Fx -- "$val" "$file" >/dev/null 2>&1 || printf '%s\n' "$val" >> "$file"
}
sanitize_stream() {
# Conservative text sanitizer. Keeps rewrite/$1/?/location structure intact.
sed -E \
-e 's/(Authorization:[[:space:]]*)[^[:space:]]+/\1<REDACTED>/Ig' \
-e 's/(Bearer[[:space:]]+)[A-Za-z0-9._~+\/-=]+/\1<REDACTED>/Ig' \
-e 's/([Pp]ass(word)?|[Ss]ecret|[Tt]oken|[Aa]pi[_-]?[Kk]ey|[Aa]ccess[_-]?[Kk]ey|[Ss]ignature|[Cc]redential)([[:space:]]*[:=][[:space:]]*)[^;&[:space:]"'"'"']+/\1\3<REDACTED>/g' \
-e 's/(auth_basic_user_file[[:space:]]+)[^;]+;/\1<REDACTED>;/Ig' \
-e 's#(/[^[:space:];]*\.(key|pem|p12|pfx))#<REDACTED_PRIVATE_KEY_PATH>#Ig'
}
size_kb() {
f="$1"
if command -v stat >/dev/null 2>&1; then
# GNU stat on most Linux systems
bytes="$(stat -c '%s' "$f" 2>/dev/null || wc -c < "$f" 2>/dev/null || echo 0)"
else
bytes="$(wc -c < "$f" 2>/dev/null || echo 0)"
fi
echo $(( (bytes + 1023) / 1024 ))
}
is_text_file() {
f="$1"
[ -r "$f" ] || return 1
# GNU grep -Iq handles binary. If unavailable, this usually still works on Linux.
grep -Iq . "$f" 2>/dev/null
}
# -------------------------------
# 0. Header
# -------------------------------
say "============================================================"
say "NGINX CVE-2026-42945 rewrite-risk audit collector $SCRIPT_VERSION"
say "host: $HOST"
say "time: $(date 2>/dev/null || echo unknown)"
say "output_dir: $OUT_DIR"
say "mode: read-only; no config change; no reload/restart; no HTTP requests"
say "artifact_mode: single_log=$SINGLE_LOG package_tgz=$PACKAGE_TGZ keep_work_dir=$KEEP_WORK_DIR"
if [ "$SINGLE_LOG" = "1" ]; then
say "single_log_file: $SINGLE_LOG_FILE"
fi
say "============================================================"
# -------------------------------
# 1. System and software inventory
# -------------------------------
say ""
say "[1/8] Collecting system and software information"
{
echo "===== hostname ====="
hostname 2>/dev/null || true
echo
echo "===== date ====="
date 2>/dev/null || true
echo
echo "===== uname ====="
uname -a 2>/dev/null || true
echo
echo "===== architecture ====="
uname -m 2>/dev/null || true
getconf LONG_BIT 2>/dev/null || true
echo
echo "===== os-release ====="
cat /etc/os-release 2>/dev/null || true
echo
echo "===== redhat-release/debian-version/alpine-release ====="
cat /etc/redhat-release 2>/dev/null || true
cat /etc/debian_version 2>/dev/null || true
cat /etc/alpine-release 2>/dev/null || true
echo
echo "===== kernel ASLR ====="
printf '%s' "/proc/sys/kernel/randomize_va_space="
cat /proc/sys/kernel/randomize_va_space 2>/dev/null || true
echo
echo "===== core pattern ====="
cat /proc/sys/kernel/core_pattern 2>/dev/null || true
echo
echo "===== ulimit -c ====="
(ulimit -c) 2>/dev/null || true
echo
echo "===== package inventory: nginx/openresty/tengine ====="
if command -v dpkg >/dev/null 2>&1; then
dpkg -l 2>/dev/null | grep -Ei 'nginx|openresty|tengine|libnginx' || true
fi
if command -v rpm >/dev/null 2>&1; then
rpm -qa 2>/dev/null | grep -Ei 'nginx|openresty|tengine' || true
fi
if command -v apk >/dev/null 2>&1; then
apk info -v 2>/dev/null | grep -Ei 'nginx|openresty|tengine' || true
fi
} > "$OUT_DIR/system/system-info.txt" 2>&1
# -------------------------------
# 2. Process and network exposure
# -------------------------------
say "[2/8] Collecting NGINX process and listening-port information"
{
echo "===== nginx/openresty/tengine processes ====="
ps -eo pid,ppid,user,etime,args 2>/dev/null | grep -Ei 'nginx: master|nginx: worker|openresty|tengine' | grep -v grep || true
echo
echo "===== systemctl status snippets ====="
if command -v systemctl >/dev/null 2>&1; then
systemctl status nginx --no-pager 2>/dev/null | sed -n '1,80p' || true
systemctl status openresty --no-pager 2>/dev/null | sed -n '1,80p' || true
systemctl status tengine --no-pager 2>/dev/null | sed -n '1,80p' || true
fi
echo
echo "===== init scripts ====="
ls -l /etc/init.d/nginx /etc/init.d/openresty /etc/init.d/tengine 2>/dev/null || true
echo
echo "===== listening ports ====="
if command -v ss >/dev/null 2>&1; then
ss -lntp 2>/dev/null || ss -lnt 2>/dev/null || true
elif command -v netstat >/dev/null 2>&1; then
netstat -lntp 2>/dev/null || netstat -lnt 2>/dev/null || true
fi
echo
echo "===== ip addresses ====="
ip addr 2>/dev/null | sed -n '1,160p' || ifconfig 2>/dev/null || true
} > "$OUT_DIR/process/process-network.txt" 2>&1
# -------------------------------
# 3. Locate NGINX-like binaries and active config hints
# -------------------------------
say "[3/8] Locating nginx/openresty/tengine binaries and active config hints"
BIN_LIST="$OUT_DIR/nginx/bin-candidates.txt"
HINT_LIST="$OUT_DIR/nginx/active-config-hints.txt"
: > "$BIN_LIST"
: > "$HINT_LIST"
# Running master process hints.
ps -eo pid,args 2>/dev/null | grep -E 'nginx: master|openresty: master|tengine' | grep -v grep > "$OUT_DIR/process/master-processes.txt" 2>/dev/null || true
while IFS= read -r line; do
pid="$(printf '%s\n' "$line" | awk '{print $1}')"
[ -n "$pid" ] || continue
exe=""
[ -e "/proc/$pid/exe" ] && exe="$(readlink -f "/proc/$pid/exe" 2>/dev/null || true)"
[ -n "$exe" ] && append_unique "$BIN_LIST" "$exe"
cmd=""
[ -r "/proc/$pid/cmdline" ] && cmd="$(tr '\000' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)"
[ -n "$cmd" ] || cmd="$line"
prefix=""
conf=""
prev=""
# shell word splitting is acceptable for typical nginx cmdline flags.
for arg in $cmd; do
if [ "$prev" = "-c" ]; then conf="$arg"; prev=""; continue; fi
if [ "$prev" = "-p" ]; then prefix="$arg"; prev=""; continue; fi
case "$arg" in
-c) prev="-c" ;;
-p) prev="-p" ;;
-c*) conf="$(printf '%s' "$arg" | sed 's/^-c//')" ;;
-p*) prefix="$(printf '%s' "$arg" | sed 's/^-p//')" ;;
esac
done
printf 'pid=%s|bin=%s|prefix=%s|conf=%s|cmd=%s\n' "$pid" "$exe" "$prefix" "$conf" "$cmd" >> "$HINT_LIST"
done < "$OUT_DIR/process/master-processes.txt"
# Common locations and PATH.
for b in \
/www/server/nginx/sbin/nginx \
/usr/sbin/nginx \
/sbin/nginx \
/usr/local/nginx/sbin/nginx \
/usr/local/openresty/nginx/sbin/nginx \
/openresty/nginx/sbin/nginx \
/usr/local/tengine/sbin/nginx \
/opt/nginx/sbin/nginx \
/opt/openresty/nginx/sbin/nginx
do
[ -x "$b" ] && append_unique "$BIN_LIST" "$(readlink -f "$b" 2>/dev/null || echo "$b")"
done
for c in nginx openresty tengine; do
if command -v "$c" >/dev/null 2>&1; then
b="$(command -v "$c" 2>/dev/null || true)"
[ -n "$b" ] && append_unique "$BIN_LIST" "$(readlink -f "$b" 2>/dev/null || echo "$b")"
fi
done
{
echo "===== binary candidates ====="
cat "$BIN_LIST" 2>/dev/null || true
echo
echo "===== active config hints ====="
cat "$HINT_LIST" 2>/dev/null || true
} > "$OUT_DIR/nginx/detection-summary.txt"
# -------------------------------
# 4. NGINX version/build and nginx -T dumps
# -------------------------------
say "[4/8] Collecting nginx -v/-V and nginx -T outputs"
: > "$OUT_DIR/nginx/version-all.txt"
: > "$OUT_DIR/nginx/nginxT-files.txt"
T_INDEX=0
while IFS= read -r bin; do
[ -x "$bin" ] || continue
safe_bin="$(printf '%s' "$bin" | sed 's#[/ ]#_#g; s#[^A-Za-z0-9_.-]#_#g')"
{
echo "===== binary: $bin ====="
echo "--- -v ---"
"$bin" -v 2>&1 || true
echo
echo "--- -V ---"
"$bin" -V 2>&1 || true
echo
} >> "$OUT_DIR/nginx/version-all.txt" 2>&1
run_one_T() {
_tag="$1"
shift
T_INDEX=$((T_INDEX + 1))
out="$OUT_DIR/nginx/nginxT/nginxT-${T_INDEX}-${safe_bin}-${_tag}.txt"
{
echo "===== binary: $bin ====="
echo "===== args: $* ====="
"$bin" "$@" -T
rc=$?
echo "===== exit_code: $rc ====="
} > "$out" 2>&1
printf '%s\n' "$out" >> "$OUT_DIR/nginx/nginxT-files.txt"
}
# default config
run_one_T "default" >/dev/null 2>&1 || true
# active hints for this binary or generic hints
while IFS= read -r hint; do
hbin="$(printf '%s' "$hint" | sed -n 's/^.*|bin=\([^|]*\)|.*$/\1/p')"
hpfx="$(printf '%s' "$hint" | sed -n 's/^.*|prefix=\([^|]*\)|conf=.*$/\1/p')"
hconf="$(printf '%s' "$hint" | sed -n 's/^.*|conf=\([^|]*\)|cmd=.*$/\1/p')"
[ -z "$hbin" ] || [ "$hbin" = "$bin" ] || continue
if [ -n "$hpfx" ] && [ -n "$hconf" ]; then
run_one_T "active-p-c" -p "$hpfx" -c "$hconf" >/dev/null 2>&1 || true
elif [ -n "$hconf" ]; then
run_one_T "active-c" -c "$hconf" >/dev/null 2>&1 || true
elif [ -n "$hpfx" ]; then
run_one_T "active-p" -p "$hpfx" >/dev/null 2>&1 || true
fi
done < "$HINT_LIST"
done < "$BIN_LIST"
# Extract config files from nginx -T output.
LOADED_CONF_LIST="$OUT_DIR/nginx/loaded-config-files-from-nginxT.txt"
: > "$LOADED_CONF_LIST"
while IFS= read -r tf; do
[ -r "$tf" ] || continue
sed -n 's/^# configuration file \(.*\):$/\1/p' "$tf" 2>/dev/null | while IFS= read -r p; do
[ -n "$p" ] && append_unique "$LOADED_CONF_LIST" "$p"
done
done < "$OUT_DIR/nginx/nginxT-files.txt"
# -------------------------------
# 5. BaoTa/SafeLine and config file discovery
# -------------------------------
say "[5/8] Discovering BaoTa/SafeLine/multi-site and NGINX-related config files"
CONF_ROOTS="$OUT_DIR/nginx/config-roots.txt"
CONF_ALL="$OUT_DIR/nginx/conf-files-all.txt"
CONF_SCAN="$OUT_DIR/nginx/conf-files-scanned.txt"
CONF_SKIP="$OUT_DIR/nginx/conf-files-skipped.txt"
: > "$CONF_ROOTS"
: > "$CONF_ALL"
: > "$CONF_SCAN"
: > "$CONF_SKIP"
for d in \
/www/server/nginx/conf \
/www/server/panel/vhost \
/etc/nginx \
/usr/local/nginx/conf \
/usr/local/openresty/nginx/conf \
/openresty/nginx/conf \
/usr/local/tengine/conf \
/opt/nginx/conf \
/opt/openresty/nginx/conf \
"$SAFELINE_NGINX_ROOT"
do
[ -d "$d" ] && append_unique "$CONF_ROOTS" "$d"
done
# Also include directories of files reported by nginx -T.
while IFS= read -r f; do
[ -n "$f" ] || continue
if [ -f "$f" ]; then
append_unique "$CONF_ROOTS" "$(dirname "$f")"
append_unique "$CONF_ALL" "$f"
fi
done < "$LOADED_CONF_LIST"
# Find config-like files under roots. Intentionally broad for BaoTa and SafeLine multi-site layouts.
while IFS= read -r d; do
[ -d "$d" ] || continue
find "$d" -maxdepth 8 -type f \
! -name '*.log' ! -name '*.pid' ! -name '*.sock' \
! -name '*.key' ! -name '*.pem' ! -name '*.p12' ! -name '*.pfx' ! -name '*.jks' ! -name '*.crt' ! -name '*.csr' \
2>/dev/null | while IFS= read -r f; do
# Keep nginx-related files. In BaoTa/SafeLine dirs, extension can vary, so scan text files too.
case "$f" in
*.conf|*.inc|*.include|*.rules|*.rule|*.vhost|*/vhost/*|"$SAFELINE_NGINX_ROOT"/*|*/nginx/conf/*|*/openresty/nginx/conf/*|*/tengine/conf/*)
append_unique "$CONF_ALL" "$f"
;;
esac
done
done < "$CONF_ROOTS"
MAX_CONF_SIZE_KB="${MAX_CONF_SIZE_KB:-5120}"
while IFS= read -r f; do
[ -f "$f" ] || { printf '%s | missing\n' "$f" >> "$CONF_SKIP"; continue; }
kb="$(size_kb "$f")"
if [ "$kb" -gt "$MAX_CONF_SIZE_KB" ]; then
printf '%s | skipped_large_%sKB\n' "$f" "$kb" >> "$CONF_SKIP"
continue
fi
if ! is_text_file "$f"; then
printf '%s | skipped_non_text_or_unreadable\n' "$f" >> "$CONF_SKIP"
continue
fi
append_unique "$CONF_SCAN" "$f"
done < "$CONF_ALL"
# BaoTa site inventory.
{
echo "===== baota path check ====="
ls -ld /www/server/panel /www/server/panel/vhost /www/server/nginx 2>/dev/null || true
echo
echo "===== baota nginx version files if present ====="
cat /www/server/nginx/version.pl 2>/dev/null || true
cat /www/server/nginx/src/version 2>/dev/null || true
echo
echo "===== baota vhost tree summary ====="
find /www/server/panel/vhost -maxdepth 4 -type f 2>/dev/null | sed -n '1,1000p' || true
echo
echo "===== baota site configs ====="
for f in /www/server/panel/vhost/nginx/*.conf; do
[ -f "$f" ] || continue
site="$(basename "$f" .conf)"
loaded="no"
grep -Fx -- "$f" "$LOADED_CONF_LIST" >/dev/null 2>&1 && loaded="yes"
listen="$(grep -nE '^[[:space:]]*listen[[:space:]]+' "$f" 2>/dev/null | head -5 | sanitize_stream | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')"
server_name="$(grep -nE '^[[:space:]]*server_name[[:space:]]+' "$f" 2>/dev/null | head -5 | sanitize_stream | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')"
rewrite_file="/www/server/panel/vhost/rewrite/${site}.conf"
[ -f "$rewrite_file" ] || rewrite_file=""
printf 'site=%s|loaded_by_nginx_T=%s|conf=%s|rewrite=%s|listen=%s|server_name=%s\n' "$site" "$loaded" "$f" "$rewrite_file" "$listen" "$server_name"
done
} > "$OUT_DIR/baota/baota-inventory.txt" 2>&1
# SafeLine mapped NGINX config inventory.
{
echo "===== safeline path check ====="
echo "safeline_nginx_root=$SAFELINE_NGINX_ROOT"
ls -ld "$SAFELINE_NGINX_ROOT" 2>/dev/null || true
echo
echo "===== safeline nginx tree summary ====="
find "$SAFELINE_NGINX_ROOT" -maxdepth 6 -type f 2>/dev/null | sed -n '1,1000p' || true
echo
echo "===== safeline files selected for scan ====="
grep -F -- "$SAFELINE_NGINX_ROOT" "$CONF_SCAN" 2>/dev/null | sed -n '1,1000p' || true
echo
echo "===== safeline core nginx directives ====="
while IFS= read -r f; do
[ -r "$f" ] || continue
case "$f" in
"$SAFELINE_NGINX_ROOT"/*)
grep -nE '^[[:space:]]*(server[[:space:]]*\{|listen[[:space:]]+|server_name[[:space:]]+|location[[:space:]]|rewrite[[:space:]]+|set[[:space:]]+|if[[:space:]]*\(|try_files[[:space:]]+|proxy_pass[[:space:]]+|include[[:space:]]+)' "$f" 2>/dev/null | sanitize_stream | sed "s#^#$f:#"
;;
esac
done < "$CONF_SCAN"
} > "$OUT_DIR/safeline/safeline-inventory.txt" 2>&1
# Generic website/server block inventory from scanned config files.
{
echo "===== scanned config files ====="
cat "$CONF_SCAN" 2>/dev/null || true
echo
echo "===== core nginx directives: server/listen/server_name/location/rewrite/set/if/try_files/proxy_pass/include ====="
while IFS= read -r f; do
[ -r "$f" ] || continue
grep -nE '^[[:space:]]*(server[[:space:]]*\{|listen[[:space:]]+|server_name[[:space:]]+|location[[:space:]]|rewrite[[:space:]]+|set[[:space:]]+|if[[:space:]]*\(|try_files[[:space:]]+|proxy_pass[[:space:]]+|include[[:space:]]+)' "$f" 2>/dev/null | sanitize_stream | sed "s#^#$f:#"
done < "$CONF_SCAN"
} > "$OUT_DIR/nginx/core-config-lines.txt" 2>&1
# -------------------------------
# 6. Risk scan: grep fallback + Python block heuristic if available
# -------------------------------
say "[6/8] Scanning config files for rewrite + ? + numbered-capture risk patterns"
RISK_DIRECT="$OUT_DIR/nginx/risk-direct-high.txt"
RISK_REWRITE_Q="$OUT_DIR/nginx/rewrite-with-question.txt"
RISK_NUMBERED="$OUT_DIR/nginx/numbered-captures.txt"
RISK_BLOCK="$OUT_DIR/nginx/risk-block-check.txt"
RISK_CONTEXT_DIR="$OUT_DIR/nginx/config-context"
: > "$RISK_DIRECT"
: > "$RISK_REWRITE_Q"
: > "$RISK_NUMBERED"
: > "$RISK_BLOCK"
while IFS= read -r f; do
[ -r "$f" ] || continue
grep -nE '^[[:space:]]*rewrite[[:space:]].*\?.*\$[1-9][0-9]*|^[[:space:]]*rewrite[[:space:]].*\$[1-9][0-9]*.*\?' "$f" 2>/dev/null | sanitize_stream | sed "s#^#$f:#" >> "$RISK_DIRECT" || true
grep -nE '^[[:space:]]*rewrite[[:space:]].*\?' "$f" 2>/dev/null | sanitize_stream | sed "s#^#$f:#" >> "$RISK_REWRITE_Q" || true
grep -nE '\$[1-9][0-9]*' "$f" 2>/dev/null | sanitize_stream | sed "s#^#$f:#" >> "$RISK_NUMBERED" || true
done < "$CONF_SCAN"
PY_BIN=""
if command -v python3 >/dev/null 2>&1; then
PY_BIN="$(command -v python3)"
elif command -v python >/dev/null 2>&1; then
PY_BIN="$(command -v python)"
fi
if [ -n "$PY_BIN" ]; then
"$PY_BIN" - "$CONF_SCAN" "$RISK_BLOCK" "$RISK_CONTEXT_DIR" "$SAFELINE_NGINX_ROOT" <<'PY' 2> "$OUT_DIR/nginx/python-scan-stderr.txt"
from __future__ import print_function
import io, os, re, sys, hashlib
conf_list = sys.argv[1]
out_path = sys.argv[2]
ctx_dir = sys.argv[3]
safeline_root = sys.argv[4].rstrip('/') if len(sys.argv) > 4 else '/data/safeline/resources/nginx'
rewrite_q = re.compile(r'^\s*rewrite\b.*\?', re.I)
rewrite_direct = re.compile(r'^\s*rewrite\b', re.I)
numbered = re.compile(r'\$[1-9][0-9]*')
follow = re.compile(r'^\s*(rewrite|set|if)\b', re.I)
sensitive = re.compile(r'(?i)(password|passwd|secret|token|api[_-]?key|access[_-]?key|authorization|credential|signature)(\s*[:=]\s*)[^;&\s"\']+')
private_path = re.compile(r'(?i)/[^\s;]*\.(key|pem|p12|pfx)')
def sanitize(s):
s = sensitive.sub(lambda m: m.group(1) + m.group(2) + '<REDACTED>', s)
s = private_path.sub('<REDACTED_PRIVATE_KEY_PATH>', s)
s = re.sub(r'(?i)(Authorization:\s*)\S+', r'\1<REDACTED>', s)
s = re.sub(r'(?i)(Bearer\s+)[A-Za-z0-9._~+/=-]+', r'\1<REDACTED>', s)
return s
def strip_comment(s):
out = []
in_s = False
in_d = False
esc = False
for ch in s:
if esc:
out.append(ch); esc = False; continue
if ch == '\\':
out.append(ch); esc = True; continue
if ch == "'" and not in_d:
in_s = not in_s; out.append(ch); continue
if ch == '"' and not in_s:
in_d = not in_d; out.append(ch); continue
if ch == '#' and not in_s and not in_d:
break
out.append(ch)
return ''.join(out)
def site_kind(path):
p = path
site = '-'
kind = 'generic'
if safeline_root and p.startswith(safeline_root + '/'):
kind = 'safeline-nginx'
rel = p[len(safeline_root)+1:]
site = rel.split('/')[0] if rel else '-'
elif '/www/server/panel/vhost/rewrite/' in p:
kind = 'bt-rewrite'; site = os.path.basename(p).replace('.conf', '')
elif '/www/server/panel/vhost/nginx/redirect/' in p or '/www/server/panel/vhost/redirect/' in p:
kind = 'bt-redirect'
parts = p.split('/')
if 'redirect' in parts:
idx = parts.index('redirect')
if idx + 1 < len(parts): site = parts[idx+1]
elif '/www/server/panel/vhost/nginx/proxy/' in p or '/www/server/panel/vhost/proxy/' in p:
kind = 'bt-proxy'
parts = p.split('/')
if 'proxy' in parts:
idx = parts.index('proxy')
if idx + 1 < len(parts): site = parts[idx+1]
elif '/www/server/panel/vhost/nginx/' in p:
kind = 'bt-site-conf'; site = os.path.basename(p).replace('.conf', '')
return site, kind
def safe_name(path, no):
h = hashlib.sha1((path + ':' + str(no)).encode('utf-8', 'ignore')).hexdigest()[:12]
base = re.sub(r'[^A-Za-z0-9_.-]+', '_', os.path.basename(path))[:80]
return '%s-line%s-%s.txt' % (base, no, h)
def write_context(path, lines, no, reason):
try:
if not os.path.isdir(ctx_dir):
os.makedirs(ctx_dir)
start = max(1, no - 8)
end = min(len(lines), no + 8)
name = safe_name(path, no)
cp = os.path.join(ctx_dir, name)
with io.open(cp, 'w', encoding='utf-8', errors='ignore') as w:
w.write(u'file: %s\nline: %s\nreason: %s\n--- context ---\n' % (path, no, reason))
for i in range(start, end + 1):
mark = '>>' if i == no else ' '
w.write(u'%s %6d: %s\n' % (mark, i, sanitize(lines[i-1].rstrip('\n'))))
return cp
except Exception:
return ''
paths = []
with io.open(conf_list, 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
p = line.strip()
if p and os.path.isfile(p):
paths.append(p)
records = []
for path in paths:
try:
with io.open(path, 'r', encoding='utf-8', errors='ignore') as f:
raw_lines = f.readlines()
except Exception as e:
continue
depth = 0
last_rewrite_q = {}
for idx, raw in enumerate(raw_lines, 1):
line = strip_comment(raw).strip()
if not line:
# still track braces conservatively from raw? no
continue
site, kind = site_kind(path)
if rewrite_direct.search(line) and '?' in line and numbered.search(line):
ctx = write_context(path, raw_lines, idx, 'HIGH same-line rewrite + ? + $N')
records.append(('HIGH', path, idx, site, kind, 'same-line rewrite contains ? and $1/$2 numbered capture', sanitize(line), ctx))
if rewrite_q.search(line):
last_rewrite_q[depth] = (idx, line)
if follow.search(line) and numbered.search(line):
for d in range(depth, -1, -1):
if d in last_rewrite_q:
prev_no, prev_line = last_rewrite_q[d]
if prev_no != idx:
ctx = write_context(path, raw_lines, idx, 'CHECK block has earlier rewrite + ? and later $N')
reason = 'same block has earlier rewrite+? at line %s; current rewrite/set/if uses $1/$2' % prev_no
records.append(('CHECK', path, idx, site, kind, reason, sanitize(line), ctx))
break
# Update depth after checking current line. It is an approximation, but useful for nginx blocks.
depth += line.count('{') - line.count('}')
for d in list(last_rewrite_q.keys()):
if d > depth:
del last_rewrite_q[d]
with io.open(out_path, 'w', encoding='utf-8') as out:
for rec in records:
sev, path, no, site, kind, reason, line, ctx = rec
out.write(u'[%s] file=%s line=%s site=%s kind=%s reason=%s text=%s context=%s\n' % (sev, path, no, site, kind, reason, line, ctx))
PY
else
echo "python3/python not found; block-level heuristic scan skipped. Grep direct scan still available." > "$RISK_BLOCK"
fi
# Optional full config copies.
if [ "${COLLECT_FULL_CONFIG:-0}" = "1" ]; then
while IFS= read -r f; do
[ -r "$f" ] || continue
rel="$(printf '%s' "$f" | sed 's#^/##; s#[/ ]#_#g; s#[^A-Za-z0-9_.-]#_#g')"
sanitize_stream < "$f" > "$OUT_DIR/nginx/config-full-sanitized/$rel" 2>/dev/null || true
done < "$CONF_SCAN"
fi
if [ "${COLLECT_RAW_CONFIG:-0}" = "1" ]; then
while IFS= read -r f; do
[ -r "$f" ] || continue
rel="$(printf '%s' "$f" | sed 's#^/##; s#[/ ]#_#g; s#[^A-Za-z0-9_.-]#_#g')"
cp "$f" "$OUT_DIR/nginx/config-full-raw/$rel" 2>/dev/null || true
done < "$CONF_SCAN"
fi
# -------------------------------
# 7. Log suspicious excerpts
# -------------------------------
say "[7/8] Collecting limited NGINX log/crash clues"
LOG_SCAN="${LOG_SCAN:-1}"
LOG_MAX_LINES="${LOG_MAX_LINES:-300}"
LOG_FILES="$OUT_DIR/logs/nginx-log-files.txt"
: > "$LOG_FILES"
if [ "$LOG_SCAN" = "1" ]; then
for d in /var/log/nginx /www/wwwlogs /www/server/nginx/logs /usr/local/nginx/logs /usr/local/openresty/nginx/logs /usr/local/tengine/logs; do
[ -d "$d" ] || continue
find "$d" -maxdepth 2 -type f \( -name '*.log' -o -name '*access*' -o -name '*error*' \) 2>/dev/null | while IFS= read -r lf; do
append_unique "$LOG_FILES" "$lf"
done
done
{
echo "===== system nginx crash clues since 2026-05-13 where available ====="
if command -v journalctl >/dev/null 2>&1; then
journalctl -u nginx --since '2026-05-13' --no-pager 2>/dev/null | grep -Ei 'segfault|core|worker process|exited|signal|trap|abort' | tail -n "$LOG_MAX_LINES" || true
journalctl --since '2026-05-13' --no-pager 2>/dev/null | grep -Ei 'nginx|openresty|tengine' | grep -Ei 'segfault|core|worker process|exited|signal|trap|abort' | tail -n "$LOG_MAX_LINES" || true
fi
echo
echo "===== dmesg crash clues ====="
dmesg -T 2>/dev/null | grep -Ei 'nginx|openresty|tengine|segfault|trap|core' | tail -n "$LOG_MAX_LINES" || true
} | sanitize_stream > "$OUT_DIR/logs/system-crash-clues.txt" 2>&1
while IFS= read -r lf; do
[ -r "$lf" ] || continue
rel="$(printf '%s' "$lf" | sed 's#^/##; s#[/ ]#_#g; s#[^A-Za-z0-9_.-]#_#g')"
{
echo "file: $lf"
echo "size_kb: $(size_kb "$lf")"
echo "--- suspicious lines: crash/error/url-encoding/long request ---"
if printf '%s' "$lf" | grep -Eiq 'error'; then
grep -Ei 'segfault|core dumped|worker process|exited on signal|signal|malloc|free\(|corrupt|overflow|abort|rewrite' "$lf" 2>/dev/null | tail -n "$LOG_MAX_LINES" || true
else
awk 'length($0) > 3000 || /%2[bB]|%25|%26|%3[fF]|\+/' "$lf" 2>/dev/null | tail -n "$LOG_MAX_LINES" || true
fi
} | sanitize_stream > "$OUT_DIR/logs/suspicious-$rel.txt" 2>&1
if [ "${COLLECT_FULL_LOGS:-0}" = "1" ] && printf '%s' "$lf" | grep -Eiq 'error'; then
sanitize_stream < "$lf" > "$OUT_DIR/logs/full-error-$rel.txt" 2>/dev/null || true
fi
done < "$LOG_FILES"
else
echo "LOG_SCAN disabled by LOG_SCAN=0" > "$OUT_DIR/logs/log-scan-disabled.txt"
fi
# -------------------------------
# 8. Final summary and package
# -------------------------------
say "[8/8] Building final summary and collection package"
count_lines() {
_f="$1"
if [ -f "$_f" ]; then
wc -l < "$_f" 2>/dev/null | tr -d ' ' | tr -d '\n'
else
printf '0'
fi
}
count_matching_lines() {
_pat="$1"
_f="$2"
if [ -f "$_f" ]; then
grep "$_pat" "$_f" 2>/dev/null | wc -l | tr -d ' ' | tr -d '\n'
else
printf '0'
fi
}
append_file_section() {
_title="$1"
_file="$2"
echo
echo "============================================================"
echo "$_title"
echo "============================================================"
if [ -s "$_file" ]; then
cat "$_file" 2>/dev/null || true
else
echo "EMPTY_OR_MISSING: $_file"
fi
}
build_single_log() {
_out="$1"
_parent="$(dirname "$_out" 2>/dev/null || echo ".")"
mkdir -p "$_parent" 2>/dev/null || true
{
echo "============================================================"
echo "NGINX CVE-2026-42945 AUDIT SINGLE LOG"
echo "============================================================"
echo "host: $HOST"
echo "time: $(date 2>/dev/null || echo unknown)"
echo "script: $SCRIPT_VERSION"
echo "mode: read-only; no config change; no reload/restart; no HTTP requests"
echo "work_dir_used_during_scan: $OUT_DIR"
echo
echo "NOTE:"
echo "- This single log includes the key audit evidence needed for review."
echo "- Raw nginx -T dumps are not included by default. Set INCLUDE_NGINX_T_IN_LOG=1 to include sanitized nginx -T dumps."
append_file_section "FINAL SUMMARY" "$SUMMARY"
append_file_section "SYSTEM INFO" "$OUT_DIR/system/system-info.txt"
append_file_section "PROCESS AND NETWORK" "$OUT_DIR/process/process-network.txt"
append_file_section "NGINX DETECTION SUMMARY" "$OUT_DIR/nginx/detection-summary.txt"
append_file_section "NGINX VERSIONS AND BUILDS" "$OUT_DIR/nginx/version-all.txt"
append_file_section "NGINX LOADED CONFIG FILES FROM nginx -T" "$OUT_DIR/nginx/loaded-config-files-from-nginxT.txt"
append_file_section "CONFIG ROOTS" "$OUT_DIR/nginx/config-roots.txt"
append_file_section "CONFIG FILES SCANNED" "$OUT_DIR/nginx/conf-files-scanned.txt"
append_file_section "CONFIG FILES SKIPPED" "$OUT_DIR/nginx/conf-files-skipped.txt"
append_file_section "BAOTA INVENTORY" "$OUT_DIR/baota/baota-inventory.txt"
append_file_section "SAFELINE INVENTORY" "$OUT_DIR/safeline/safeline-inventory.txt"
append_file_section "CORE CONFIG LINES" "$OUT_DIR/nginx/core-config-lines.txt"
append_file_section "RISK DIRECT HIGH" "$RISK_DIRECT"
append_file_section "RISK BLOCK CHECK" "$RISK_BLOCK"
append_file_section "REWRITE WITH QUESTION" "$RISK_REWRITE_Q"
append_file_section "NUMBERED CAPTURES" "$RISK_NUMBERED"
echo
echo "============================================================"
echo "RISK CONTEXTS"
echo "============================================================"
_ctx_found=0
for _ctx in "$RISK_CONTEXT_DIR"/*.txt; do
[ -f "$_ctx" ] || continue
_ctx_found=1
append_file_section "CONTEXT: $_ctx" "$_ctx"
done
[ "$_ctx_found" = "1" ] || echo "NONE"
append_file_section "SYSTEM CRASH CLUES" "$OUT_DIR/logs/system-crash-clues.txt"
echo
echo "============================================================"
echo "SUSPICIOUS LOG EXCERPTS"
echo "============================================================"
_log_found=0
for _lf in "$OUT_DIR/logs"/suspicious-*.txt; do
[ -f "$_lf" ] || continue
_log_found=1
append_file_section "LOG EXCERPT: $_lf" "$_lf"
done
[ "$_log_found" = "1" ] || echo "NONE"
if [ "$INCLUDE_NGINX_T_IN_LOG" = "1" ]; then
echo
echo "============================================================"
echo "SANITIZED NGINX -T DUMPS"
echo "============================================================"
_t_found=0
while IFS= read -r _tf; do
[ -f "$_tf" ] || continue
_t_found=1
echo
echo "------------------------------------------------------------"
echo "nginx -T file: $_tf"
echo "------------------------------------------------------------"
sanitize_stream < "$_tf" 2>/dev/null || true
done < "$OUT_DIR/nginx/nginxT-files.txt"
[ "$_t_found" = "1" ] || echo "NONE"
fi
} > "$_out" 2>&1
}
HIGH_COUNT="$(count_lines "$RISK_DIRECT")"
PY_HIGH_COUNT="$(count_matching_lines '^\[HIGH\]' "$RISK_BLOCK")"
CHECK_COUNT="$(count_matching_lines '^\[CHECK\]' "$RISK_BLOCK")"
REWRITE_Q_COUNT="$(count_lines "$RISK_REWRITE_Q")"
NUMBERED_COUNT="$(count_lines "$RISK_NUMBERED")"
CONF_SCAN_COUNT="$(count_lines "$CONF_SCAN")"
BIN_COUNT="$(count_lines "$BIN_LIST")"
BT_SITE_COUNT="$(count_matching_lines '^site=' "$OUT_DIR/baota/baota-inventory.txt")"
SAFELINE_CONF_COUNT="$(grep -F -- "$SAFELINE_NGINX_ROOT" "$CONF_SCAN" 2>/dev/null | wc -l | tr -d ' ' | tr -d '\n')"
VERSION_WARN="unknown"
if grep -Eiq 'nginx version: nginx/(0\.|1\.([0-9]|1[0-9]|2[0-9])\.|1\.30\.0)' "$OUT_DIR/nginx/version-all.txt" 2>/dev/null; then
VERSION_WARN="yes-nginx-version-appears-within-0.x-or-1.0-through-1.30.0"
elif grep -Eiq 'nginx version: nginx/(1\.30\.[1-9]|1\.3[1-9]\.|1\.[4-9][0-9]\.|[2-9]\.)' "$OUT_DIR/nginx/version-all.txt" 2>/dev/null; then
VERSION_WARN="appears-patched-version-line-or-newer"
fi
REWRITE_MODULE="unknown"
if grep -q -- '--without-http_rewrite_module' "$OUT_DIR/nginx/version-all.txt" 2>/dev/null; then
REWRITE_MODULE="some-build-shows-without-http_rewrite_module"
else
REWRITE_MODULE="rewrite-module-likely-present-or-not-explicitly-disabled"
fi
RESULT="LOW/NO OBVIOUS CONFIG TRIGGER"
if [ "${HIGH_COUNT:-0}" -gt 0 ] || [ "${PY_HIGH_COUNT:-0}" -gt 0 ]; then
RESULT="HIGH RISK CANDIDATE"
elif [ "${CHECK_COUNT:-0}" -gt 0 ]; then
RESULT="CANDIDATE RISK - NEED CONTEXT REVIEW"
elif [ "${REWRITE_Q_COUNT:-0}" -gt 0 ] && [ "${NUMBERED_COUNT:-0}" -gt 0 ]; then
RESULT="POSSIBLE RISK - rewrite-with-question and numbered captures both exist"
elif [ "$VERSION_WARN" = "yes-nginx-version-appears-within-0.x-or-1.0-through-1.30.0" ]; then
RESULT="VERSION/ENVIRONMENT WARNING - old nginx but no obvious trigger found"
fi
{
echo "============================================================"
echo "FINAL SUMMARY"
echo "============================================================"
echo "host: $HOST"
echo "time: $(date 2>/dev/null || echo unknown)"
echo "script: $SCRIPT_VERSION"
echo "result: $RESULT"
echo "version_assessment: $VERSION_WARN"
echo "rewrite_module_assessment: $REWRITE_MODULE"
echo "binaries_found: $BIN_COUNT"
echo "configs_scanned: $CONF_SCAN_COUNT"
echo "baota_sites_found: $BT_SITE_COUNT"
echo "safeline_configs_scanned: $SAFELINE_CONF_COUNT"
echo "same_line_high_grep_count: $HIGH_COUNT"
echo "python_high_count: $PY_HIGH_COUNT"
echo "python_block_check_count: $CHECK_COUNT"
echo "rewrite_with_question_count: $REWRITE_Q_COUNT"
echo "numbered_capture_count: $NUMBERED_COUNT"
echo
echo "===== binaries ====="
cat "$BIN_LIST" 2>/dev/null || true
echo
echo "===== nginx versions/builds ====="
sed -n '1,180p' "$OUT_DIR/nginx/version-all.txt" 2>/dev/null || true
echo
echo "===== BaoTa sites summary ====="
grep '^site=' "$OUT_DIR/baota/baota-inventory.txt" 2>/dev/null | sed -n '1,120p' || true
echo
echo "===== SafeLine config summary ====="
sed -n '1,160p' "$OUT_DIR/safeline/safeline-inventory.txt" 2>/dev/null || true
echo
echo "===== HIGH direct same-line candidates ====="
if [ -s "$RISK_DIRECT" ]; then sed -n '1,200p' "$RISK_DIRECT"; else echo "NONE"; fi
echo
echo "===== Block-level HIGH/CHECK candidates ====="
if [ -s "$RISK_BLOCK" ]; then sed -n '1,240p' "$RISK_BLOCK"; else echo "NONE"; fi
echo
echo "===== rewrite with ? sample ====="
if [ -s "$RISK_REWRITE_Q" ]; then sed -n '1,120p' "$RISK_REWRITE_Q"; else echo "NONE"; fi
echo
echo "===== numbered capture sample ====="
if [ -s "$RISK_NUMBERED" ]; then sed -n '1,120p' "$RISK_NUMBERED"; else echo "NONE"; fi
echo
echo "===== listening ports sample ====="
sed -n '/===== listening ports =====/,$p' "$OUT_DIR/process/process-network.txt" 2>/dev/null | sed -n '1,120p' || true
echo
echo "===== crash/log clues sample ====="
sed -n '1,160p' "$OUT_DIR/logs/system-crash-clues.txt" 2>/dev/null || true
echo
echo "===== artifact summary ====="
if [ "$SINGLE_LOG" = "1" ]; then
echo "single combined log: $SINGLE_LOG_FILE"
fi
if [ "$PACKAGE_TGZ" = "1" ]; then
echo "tgz package: $OUT_DIR.tgz"
else
echo "tgz package: disabled by PACKAGE_TGZ=0"
fi
if [ "$KEEP_WORK_DIR" = "1" ] || [ "$SINGLE_LOG" != "1" ]; then
echo "work directory: $OUT_DIR"
echo "console report: console-report.txt"
echo "system info: system/system-info.txt"
echo "process/network: process/process-network.txt"
echo "nginx versions: nginx/version-all.txt"
echo "nginx -T outputs: nginx/nginxT/"
echo "loaded config file list: nginx/loaded-config-files-from-nginxT.txt"
echo "scanned config file list: nginx/conf-files-scanned.txt"
echo "core config lines: nginx/core-config-lines.txt"
echo "risk direct: nginx/risk-direct-high.txt"
echo "risk block: nginx/risk-block-check.txt"
echo "risk contexts: nginx/config-context/"
echo "baota inventory: baota/baota-inventory.txt"
echo "safeline inventory: safeline/safeline-inventory.txt"
echo "log clues: logs/"
else
echo "work directory: temporary; removed after single log is built unless KEEP_WORK_DIR=1"
fi
} > "$SUMMARY" 2>&1
cat "$SUMMARY" | tee -a "$REPORT"
if [ "$SINGLE_LOG" = "1" ]; then
build_single_log "$SINGLE_LOG_FILE"
fi
PKG=""
if [ "$PACKAGE_TGZ" = "1" ]; then
PKG="$OUT_DIR.tgz"
# Exclude raw configs unless explicitly collected. Directories may be empty.
tar czf "$PKG" -C "$(dirname "$OUT_DIR")" "$(basename "$OUT_DIR")" 2>/dev/null || true
fi
say ""
say "============================================================"
say "COLLECTION ARTIFACTS"
say "============================================================"
if [ "$SINGLE_LOG" = "1" ]; then
say "single_log: $SINGLE_LOG_FILE"
fi
if [ "$PACKAGE_TGZ" = "1" ]; then
say "package: $PKG"
else
say "package: disabled"
fi
if [ "$KEEP_WORK_DIR" = "1" ] || [ "$SINGLE_LOG" != "1" ]; then
say "output_dir: $OUT_DIR"
say "report: $REPORT"
else
say "output_dir: $OUT_DIR (temporary; removing after successful single log build)"
fi
say ""
if [ "$SINGLE_LOG" = "1" ]; then
say "Send me the single .log file above."
else
say "Send me either the console output above or the output directory/package."
fi
say "To also create a .tgz package: PACKAGE_TGZ=1 sh $0"
say "To keep intermediate files: KEEP_WORK_DIR=1 sh $0"
say "To include sanitized nginx -T dumps in the single log: INCLUDE_NGINX_T_IN_LOG=1 sh $0"
if [ "$KEEP_WORK_DIR" != "1" ] && [ "$SINGLE_LOG" = "1" ] && [ -s "$SINGLE_LOG_FILE" ]; then
rm -rf "$OUT_DIR" 2>/dev/null || true
fi
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment