---
name: sands-backdoor-and-doc-leak
description: Sands staging carries an active malware backdoor + plaintext owner-credential CSVs; all 3 sites serve owner documents with no real file-level auth
metadata:
  type: project
---

Scoping audit of the three Hartling owner sites (2026-08-24) found security
issues that gate the rebuild and one active incident.

**FORENSICS (access logs, 2026-08-25).** Logs retain ~30 days (earliest
2026-07-27), so the 2026-06-25 implantation is NOT in the window. Within it:
- **The backdoor WAS used — 12 times by 6 distinct residential US IPs**
  (`24.46.45.140`, `35.144.84.139`, `74.73.70.94`, `71.226.250.189`,
  `71.239.61.132`, `99.42.146.7`) on Jul 27 (x3), Aug 7 (x2), Aug 19 (x1). Each
  hit `/?garment-prototype-blocks=<key>` -> 302 -> loaded `/wp-admin/` (1.1-1.8 MB
  responses = real dashboard). Post-access they browsed `plugins.php` (17),
  `admin.php` (16), `options-general.php` (6), `plugin-install.php` (1),
  `update.php` (1) — human admin-UI browsing, not automation.
- **The unauthenticated RCE path was NEVER invoked — 0 hits** for
  `action=garment`/`ev_data`. So no payload was delivered that way.
- **No cryptominer.** No mining process on the box; top CPU is normal php-fpm /
  redis / mariadb / imunify360. No attacker-installed plugin: everything in
  `wp-content/plugins` modified since Jul 20 is a legitimate auto-update
  (wordfence, kadence, ACF, pdf-embedder, cloudflare-cache).
- **The real cause of the frequent outages is NOT the backdoor.** It is PHP-FPM
  worker-pool exhaustion: nginx logged `no live upstreams` (10) and
  `connect() failed / Connection refused` (16), clustered 2026-07-21 15:06-15:19
  plus 07/22 and 07/28. The slow-request log is dominated by `index.php` (738) and
  **`wp-login.php` (533)**, and access logs show **954 login POSTs** with spikes
  (274 on Jul 29, 254 on Aug 18) from a botnet — top source `137.23.50.92` (244),
  clusters in `209.87.169.x` and `185.198.240.x`. Same failure family as the
  WP OOM/worker-exhaustion runbook in /srv/CLAUDE.md: bound the pool / block the
  login flood, or it keeps falling over. **Note the site has NO login rate-limit
  plugin active.**

**REMEDIATION 2026-08-25 (all live, rian directed).** Roles: downgraded `kwhitt`,
`ovidio` (both "hartling group staff", which carries user-management caps) and
`mbeeson` (editor) to plain `owner`; stripped all `bbp_moderator` roles (forum
inactive). **Exactly one administrator remains: `rian80` / rian@rian.ca.**
**Salts rotated** — `wp-salt.php` regenerated on-server (never printed), old file
saved to `/home/master/quarantine-.../wp-salt.php.pre-rotation` (0600, outside the
web root). This matters because the backdoor called `wp_set_auth_cookie()`, so
attacker cookies stayed valid after plugin removal; rotation kills them. Verified
after: wp-login 200 and renders, homepage 302 (normal gate), backdoor URL issues
**no** auth cookie. STILL OWED: rotate DB/API credentials, and a login rate-limit.

**REMOVED FROM LIVE PRODUCTION 2026-08-25** (rian directed; done over SSH).
Sands = Cloudways app **`yxqnbzygqr`** on `165.227.177.0`
(`master_vmxzsgfbpe`). Sequence: `wp plugin deactivate` -> copy the plugin dir to
`/home/master/quarantine-thesandsowners-2026-08-25/` (outside every web root, kept
as evidence) -> `rm -rf` from `wp-content/plugins/`. Verified after: plugin dir
gone, signature count 0 across plugins/themes, 0 `active_plugins` entries, the
forgery URL now issues **no** auth cookie (plain wp-login redirect), and
`admin-ajax.php?action=garment-prototype-blocks` returns `0` (no handler). Site up.
**Survey found no second implant:** `wp core verify-checksums` PASSES (the modified
wp-includes files were just the WP 7.0.4 update); the only extra core-level files
are `wp-salt.php` (Cloudways) and `wp-alive.php` (rian's own BW uptime check), both
present on all sibling apps; no PHP in uploads; no mu-plugins dir; only ONE admin
(`rian80`, rian's own, 2022); cron clean; Wordfence active again. **Fleet scan of
all 10 apps on that server: 0 hits** for the signature and for the self-hiding
pattern. STILL OWED: rotate salts + DB/app credentials and force password resets
(2 months of RCE = assume everything readable was read); the site's auth salts live
in `wp-salt.php`.

**CONFIRMED STILL LIVE ON PRODUCTION 2026-08-25 (before the removal above).** rian re-synced Sands+Palms
staging from production; the backdoor **came back on Sands**, same file, same
mtime (Jun 25 09:56), and was re-registered in `active_plugins`. Re-quarantined.
This is proof the LIVE thesandsowners.com has never been cleaned — the malware is
still on the Cloudways host. Any future prod->staging sync will reintroduce it
until production is disinfected. Shore Club and Palms were clean on re-sync.

**STAGING REMEDIATED 2026-08-25** (rian approved): backdoor file renamed to
`garment-prototype-blocks.php.MALWARE-QUARANTINED-2026-08-25` (inert, kept as
evidence), removed from `active_plugins` (21->20), 8 credential CSVs deleted.
`srv-gw security-audit` after = 0 critical. Scan confirmed the signature exists
on NO other site and there's no stray PHP in any uploads dir / no mu-plugins.
**STILL OPEN: live production on Cloudways** — rian to check thesandsowners.com
(and siblings) for the same plugin and rotate any secrets it could read.

**INCIDENT — treat live thesandsowners.com as compromised.**
`hartling-thesandsowners/wp-content/plugins/garment-prototype-blocks/garment-prototype-blocks.php`
(fake author "WordPress", v4.16.12) is malware: self-hides from the plugin list;
unauthenticated RCE via `wp_ajax_nopriv_garment-prototype-blocks` (AES-decrypts
`$_POST[ev_data]`, key from string `vgpvaIa4579f9r4`, runs `call_user_func_array`);
admin-login bypass via `GET ?garment-prototype-blocks=vgpvaIa4579f9r4`. Wordfence
was DEACTIVATED on that site. File mtime 2026-06-25 matches the last prod->staging
UpdraftPlus refresh => production almost certainly carried it. Do NOT probe the
live `?garment-prototype-blocks=...` URL — it would forge an admin session.
Also: 8 copies of `Owners-Credentials-The-Sands-...csv` (columns include plaintext
`password`) sit in `uploads/2021/03/`, publicly fetchable, not WP attachments.

**THE PALMS' 250 PROTECTED DOCUMENTS HAVE NO BACKUP (verified 2026-08-25).**
`wp_bw_protected_files` lists **250 files / 150.9 MB**; on this server only the 2
stub files exist. Checked the newest UpdraftPlus "others" archive
(`backup_2026-06-25-2057_...-others.zip`): it contains `bw-protected/` with ONLY
`index.php` + `.htaccess` — **the real documents are in no backup at all**, because
bw-lock-files moved them out of `uploads/` into `wp-content/bw-protected/` and
UpdraftPlus isn't capturing them. They exist in exactly ONE place: the Cloudways
production filesystem. Board minutes and financials, single copy, no redundancy.
Fixing this is urgent independent of the rebuild. No SSH access to Cloudways is
configured on mosiah (no ssh config, no known_hosts entry, host doesn't resolve),
so pulling them needs rian to add mosiah's public key in the Cloudways dashboard.

**Systemic (all 3 sites): owner documents have NO file-level auth.** PDFs are
static files at guessable `/wp-content/uploads/YYYY/MM/*.pdf`; only the *listing
pages* are WP-private. The Palms added a custom `bw-lock-files` plugin (moves
files to `wp-content/bw-protected/`, serves via `/bw-file/<id>` behind
`is_user_logged_in()`), but it only protects files migrated on 2026-01-08 — 13
newer PDFs are public, and its 250 protected files live ONLY on the Cloudways
production box (outside the uploads backup path). WP also generated public
first-page JPG previews of confidential PDFs. This leak class is exactly what the
rebuild must eliminate: serve every doc through an auth+policy streaming route,
`X-Robots-Tag: noindex`, files outside webroot. See [[rebuild-plan]].

**Account reality:** Sands 134 users / ~13 active in 12mo; Shore Club 48 / 6;
Palms 136 / 8. ~90% of hashes are legacy phpass `$P$B`. No unit/phone custom
data exists to migrate — only name, email, role. The Palms had OPEN registration
(`users_can_register=1`, `default_role=owner`) — anyone could self-grant access.
