mirror of
https://ghproxy.net/https://github.com/CaptainCore/captaincore.git
synced 2026-08-04 14:10:32 +08:00
145 lines
5.7 KiB
Bash
Executable file
145 lines
5.7 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
#
|
|
# Detect WordPress users with malformed or legacy password hashes
|
|
#
|
|
# Stock WordPress (6.9+) creates passwords as $wp$2y$10$... (bcrypt+phpass).
|
|
# Older installs may have $P$ phpass hashes (legitimate but stale — rehashed
|
|
# on next successful login). Anything else in user_pass is anomalous and
|
|
# indicates either a direct DB write (attacker plant) or a corrupted record:
|
|
#
|
|
# - Raw MD5 (32 hex chars) → almost never legitimate on modern WP
|
|
# - Empty user_pass → cannot log in via wp-login (suspicious for admin accounts)
|
|
# - Anything not starting with '$' → not a recognized PHP password hash format
|
|
#
|
|
# Output format: SEVERITY|ID|login|role|hash_type|user_registered
|
|
#
|
|
# Usage:
|
|
# captaincore ssh <site> --script=detect-malformed-passwords
|
|
# captaincore ssh @all --script=detect-malformed-passwords --quiet --label
|
|
#
|
|
# Flags:
|
|
# --quiet Only output if findings are present (for bulk scanning)
|
|
#
|
|
|
|
set -uo pipefail
|
|
|
|
# ── Argument parsing ─────────────────────────────────────────────
|
|
quiet=""
|
|
for _arg in "$@"; do
|
|
case "$_arg" in
|
|
--quiet) quiet=true ;;
|
|
--*) ;;
|
|
*) WP_ROOT="$_arg" ;;
|
|
esac
|
|
done
|
|
|
|
WP_ROOT="${WP_ROOT:-.}"
|
|
cd "$WP_ROOT" 2>/dev/null || { echo "ERROR: Cannot access $WP_ROOT"; exit 1; }
|
|
|
|
if [ ! -f "wp-config.php" ] && [ -f "public/wp-config.php" ]; then
|
|
cd public
|
|
elif [ ! -f "wp-config.php" ] && [ -f "public_html/wp-config.php" ]; then
|
|
cd public_html
|
|
fi
|
|
|
|
if [ ! -f "wp-config.php" ]; then
|
|
[ -z "$quiet" ] && echo "WordPress not found."
|
|
exit 0
|
|
fi
|
|
|
|
if ! command -v wp &>/dev/null; then
|
|
echo "ERROR: WP-CLI not available"
|
|
exit 1
|
|
fi
|
|
|
|
WP_FLAGS="--skip-themes --skip-plugins --skip-packages"
|
|
|
|
PREFIX=$(wp db prefix $WP_FLAGS 2>/dev/null)
|
|
if [ -z "$PREFIX" ]; then
|
|
[ -z "$quiet" ] && echo "ERROR: could not read table prefix (DB unreachable?)"
|
|
exit 1
|
|
fi
|
|
|
|
# ── Detection query ──────────────────────────────────────────────
|
|
# Flags users whose user_pass falls into one of three anomalous categories:
|
|
# 1. Empty (cannot log in via standard wp-login flow)
|
|
# 2. Raw MD5 (32 hex chars; legacy WordPress format pre-2008)
|
|
# 3. Unknown format (does not match any recognized hash prefix)
|
|
# Excludes legitimate formats:
|
|
# - $wp$2y$... (WP 6.9+ bcrypt+phpass hybrid)
|
|
# - $P$ ... $H$ (phpass, legit but stale until next login)
|
|
# - $2a$ $2b$ $2y$ (bcrypt direct — used by some auth plugins)
|
|
# - $argon2* (passwords-evolved plugin)
|
|
read -r -d '' QUERY <<SQL || true
|
|
SELECT
|
|
u.ID,
|
|
u.user_login,
|
|
u.user_email,
|
|
u.user_registered,
|
|
CASE
|
|
WHEN u.user_pass = '' THEN 'EMPTY'
|
|
WHEN u.user_pass REGEXP '^[a-f0-9]{32}\$' THEN 'RAW_MD5'
|
|
WHEN u.user_pass REGEXP '^[a-f0-9]{40}\$' THEN 'RAW_SHA1'
|
|
WHEN u.user_pass REGEXP '^[a-f0-9]{64}\$' THEN 'RAW_SHA256'
|
|
ELSE 'UNKNOWN'
|
|
END AS hash_type,
|
|
CASE
|
|
WHEN um.meta_value LIKE '%administrator%' THEN 'administrator'
|
|
WHEN um.meta_value LIKE '%editor%' THEN 'editor'
|
|
WHEN um.meta_value LIKE '%author%' THEN 'author'
|
|
WHEN um.meta_value LIKE '%contributor%' THEN 'contributor'
|
|
WHEN um.meta_value LIKE '%subscriber%' THEN 'subscriber'
|
|
WHEN um.meta_value LIKE '%customer%' THEN 'customer'
|
|
WHEN um.meta_value IS NULL THEN 'none'
|
|
ELSE 'other'
|
|
END AS role
|
|
FROM \`${PREFIX}users\` u
|
|
LEFT JOIN \`${PREFIX}usermeta\` um ON um.user_id = u.ID
|
|
AND um.meta_key = '${PREFIX}capabilities'
|
|
WHERE u.user_pass = ''
|
|
OR u.user_pass REGEXP '^[a-f0-9]{32}\$'
|
|
OR u.user_pass REGEXP '^[a-f0-9]{40}\$'
|
|
OR u.user_pass REGEXP '^[a-f0-9]{64}\$'
|
|
OR (
|
|
u.user_pass NOT REGEXP '^\\\\\$wp\\\\\$'
|
|
AND u.user_pass NOT REGEXP '^\\\\\$P\\\\\$'
|
|
AND u.user_pass NOT REGEXP '^\\\\\$H\\\\\$'
|
|
AND u.user_pass NOT REGEXP '^\\\\\$2[aby]\\\\\$'
|
|
AND u.user_pass NOT REGEXP '^\\\\\$argon2'
|
|
AND u.user_pass != ''
|
|
AND u.user_pass NOT REGEXP '^[a-f0-9]{32,}\$'
|
|
)
|
|
ORDER BY u.ID
|
|
SQL
|
|
|
|
results=$(wp db query "$QUERY" --skip-column-names $WP_FLAGS 2>/dev/null)
|
|
|
|
if [ -z "$results" ]; then
|
|
[ -z "$quiet" ] && echo "No malformed password hashes detected."
|
|
exit 0
|
|
fi
|
|
|
|
# ── Output findings ──────────────────────────────────────────────
|
|
# Severity matrix:
|
|
# RAW_MD5 + administrator = CRITICAL (joinadrian.com pattern)
|
|
# RAW_MD5 + non-admin = HIGH
|
|
# EMPTY + administrator = HIGH (cannot log in normally — suspicious for admin)
|
|
# EMPTY + non-admin = MEDIUM (could be SSO/external-auth user)
|
|
# RAW_SHA1 / RAW_SHA256 + admin = CRITICAL
|
|
# RAW_SHA1 / RAW_SHA256 + non-admin = HIGH
|
|
# UNKNOWN + admin = CRITICAL (anomalous format on highest-priv account)
|
|
# UNKNOWN + non-admin = HIGH
|
|
echo "$results" | awk 'BEGIN { FS="\t"; OFS="|" } {
|
|
id=$1; login=$2; registered=$4; hash_type=$5; role=$6
|
|
|
|
if (hash_type == "EMPTY") {
|
|
severity = (role == "administrator") ? "HIGH" : "MEDIUM"
|
|
} else if (hash_type == "RAW_MD5" || hash_type == "RAW_SHA1" || hash_type == "RAW_SHA256" || hash_type == "UNKNOWN") {
|
|
severity = (role == "administrator") ? "CRITICAL" : "HIGH"
|
|
} else {
|
|
severity = "MEDIUM"
|
|
}
|
|
|
|
print severity, id, login, role, hash_type, registered
|
|
}'
|