The "clean" baseline unintentionally shipped ~34 planted webshells/backdoors
plus sbtrack (a third-party SEO-cloaking proxy) and inert scanner probes,
carried in from the vetted-live Helix3 template. They survived because the IOC
safety net keyed on extension ('*.php.json') while the attacker disguised one
webshell under ~20 fake extensions (.inc.json/.phar.json/.php[3-5].json/
.php.json.json) plus plain-.json defacements.
- History rewritten (git filter-repo) to remove all 48 files from every commit.
- build-baseline.sh step 6 is now CONTENT-based: any templates/**/*.json with a
PHP open tag is dropped (legit Helix3 presets are pure JSON).
- malware-iocs.paths extended with plain-.json defacements, probe artifacts and
non-.json droppers a content grep cannot key on.
Found by the imrcli HIDS YARA union layer during baseline integration (the
baseline-diff alone would have marked them baseline-match).
Assisted-by: claude-code@claude-opus-4-8
106 lines
5.0 KiB
Bash
Executable File
106 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# build-baseline.sh — reproducibly assemble the www.archline.hu clean CODE baseline.
|
|
#
|
|
# Output (out/) = the deployed docroot (code only), which the operator's HIDS
|
|
# baseline-diff verifies the live server against.
|
|
#
|
|
# The repo stores the baseline CONSTITUENTS, not the deployed tree; this script
|
|
# performs the deterministic "deployment" (Joomla manifest-mapping for packaged
|
|
# extensions, direct placement for no-source ones), per RUNBOOK.md.
|
|
#
|
|
# Usage: ./build-baseline.sh [OUT_DIR] (default: ./out)
|
|
set -euo pipefail
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
OUT="${1:-$ROOT/out}"
|
|
log(){ printf '[build] %s\n' "$*"; }
|
|
|
|
rm -rf "$OUT"; mkdir -p "$OUT"
|
|
|
|
# Ensure the developer-controlled submodules are populated (cadcommonlib, maintenance).
|
|
git -C "$ROOT" submodule update --init \
|
|
cadline/backend/common_cadline_libraries cadline/backend/maintenance >/dev/null 2>&1 || \
|
|
log "WARN: submodule update failed — common_cadline_libraries/maintenance may be empty"
|
|
|
|
# 1) Joomla core — official 3.8.11 package (deployed form), minus demo assets and
|
|
# runtime data dirs (cache/tmp/logs/images are data, not verified code).
|
|
log "core: Joomla 3.8.11 (official package)"
|
|
rsync -a \
|
|
--exclude='/cache/' --exclude='/tmp/' --exclude='/logs/' \
|
|
--exclude='/images/sampledata/' --exclude='/images/banners/' --exclude='/images/headers/' \
|
|
--exclude='/images/joomla_black.png' --exclude='/images/powered_by.png' \
|
|
"$ROOT/core/" "$OUT/"
|
|
|
|
# 2) Upstream-package extensions — deployed-layout, byte-reconciled to live.
|
|
# In git these are authored as two commits per modified module: (a) the
|
|
# pristine upstream package manifest-mapped by lib/jmap.py, (b) the reconcile
|
|
# to the live bytes (line-endings + install files + Cadline delta). `git diff`
|
|
# between those two commits documents exactly how live differs from upstream.
|
|
# At build time the reconciled (live-byte) deployed files are placed directly.
|
|
for d in "$ROOT"/packages/*/; do
|
|
[ -d "$d" ] || continue
|
|
log "package: $(basename "$d")"
|
|
rsync -a --exclude='.provenance' "$d" "$OUT/"
|
|
done
|
|
|
|
# 3) No-source extensions — vetted deployed files (no obtainable package),
|
|
# placed directly (already in deployed layout).
|
|
for d in "$ROOT"/deployed/*/; do
|
|
[ -d "$d" ] || continue
|
|
log "deployed: $(basename "$d")"
|
|
rsync -a --exclude='.provenance' "$d" "$OUT/"
|
|
done
|
|
|
|
# 4) Cadline own-code (components/modules/plugins/templates/docroot apps).
|
|
for d in "$ROOT"/cadline/*/; do
|
|
[ -d "$d" ] || continue
|
|
log "cadline: $(basename "$d")"
|
|
rsync -a --exclude='.provenance' --exclude='.git' "$d" "$OUT/"
|
|
done
|
|
|
|
# 5) Cadline core modifications — live versions overlaid on the package core
|
|
# (the ~24 legit customizations); the 3 trojanized core files stay clean.
|
|
if [ -d "$ROOT/overrides" ]; then
|
|
log "overrides: Cadline core modifications"
|
|
rsync -a --exclude='.gitkeep' "$ROOT/overrides/" "$OUT/"
|
|
fi
|
|
|
|
# NOTE: common_cadline_libraries + maintenance come from the authoritative
|
|
# developer submodules (cadcommonlib, maintenance) as-is. Their content currently
|
|
# diverges from the live docroot (mixed line-endings + a content drift on
|
|
# crm_hardlock.class.php / POfile.php) because live was not last deployed from the
|
|
# submodule HEADs. The baseline reflects the authoritative repos; the HIDS flags
|
|
# the live divergence until live is re-deployed or the drift is reconciled.
|
|
# Line-ending normalization is intentionally left to the verifier, not applied here.
|
|
|
|
# 6) Safety net — CONTENT-based malware exclusion. The constituent sources above
|
|
# are vetted clean, but this catches a planted file regardless of its extension
|
|
# disguise. The 2026 incident planted one webshell under ~20 fake extensions
|
|
# (.inc.json / .phar.json / .php[3-5].json / .php.json.json) plus plain-.json
|
|
# defacements — an extension allowlist (the old '*.php.json' filter) missed ALL
|
|
# of them. The robust signal is CONTENT, not extension.
|
|
log "exclude: content-based malware safety net"
|
|
# (a) Any .json under templates/ carrying a PHP open tag is NOT a legit Helix3
|
|
# layout preset (those are pure JSON) — it is a disguised dropper. This closes
|
|
# the .json-disguise gap deterministically.
|
|
if [ -d "$OUT/templates" ]; then
|
|
find "$OUT/templates" -type f -name '*.json' 2>/dev/null | while IFS= read -r j; do
|
|
if grep -Ilq -m1 -E '<\?php|<\?=' "$j" 2>/dev/null; then
|
|
log " drop (php-in-json): ${j#"$OUT"/}"; rm -f "$j"
|
|
fi
|
|
done
|
|
fi
|
|
# (b) Legacy extension patterns (belt-and-suspenders; harmless if already gone).
|
|
find "$OUT" \( -name '*.php.json' -o -name '*.phtml.json' -o -name 'h3x_*' -o -name '_h3x_*' \) -delete 2>/dev/null || true
|
|
# (c) Explicit IOC path list (docroot-relative) — the plain-.json defacements,
|
|
# probe artifacts and non-.json droppers that a content grep cannot key on.
|
|
if [ -f "$ROOT/malware-iocs.paths" ]; then
|
|
while IFS= read -r ioc; do
|
|
[ -z "$ioc" ] && continue
|
|
case "$ioc" in \#*) continue;; esac
|
|
rm -rf "${OUT:?}/$ioc"
|
|
done < "$ROOT/malware-iocs.paths"
|
|
fi
|
|
|
|
log "baseline built at: $OUT ($(find "$OUT" -type f | wc -l) files)"
|