# Glaz-Tech Industries — Website Security Assessment

**Classification:** CONFIDENTIAL — Security
**Date:** 2026-06-03 (updated 2026-06-04 — Appendix A; deep host + SQL-infrastructure recon folded into **C0-Extended**, the findings table, and the roadmap)
**Assessor:** Arizona Computer Guru (Mike Swanson)
**Target:** Glaztech customer/e-commerce web application — server `WWW` (192.168.8.72, public 65.113.52.88), site `glaztech_new` at `D:\web\glaztech_4`, SQL backend `192.168.8.62,3436` (`GTI-INV-SQL`)
**Method:** Authorized read-only assessment via GuruRMM (config/registry inspection, source-code review of the on-server VB.NET source, read-only/aggregate DB inspection, and — 2026-06-04 — read-only host telemetry and SQL **metadata** inspection: IIS/app-pool config, services, scheduled tasks, file ACLs, listening ports, firewall state, certificate metadata, plus `sys.configurations` / `sys.servers` / `msdb` job + backup metadata and `sys.databases` encryption state). **No cardholder data was retrieved** and **no secret values were intentionally retrieved**; sensitive columns were classified by aggregate only. *(One domain-administrator password was returned incidentally inside a SQL Agent job-step preview; it was redacted from all assessment artifacts and is not reproduced here — see Assumptions and C0-Extended.)*

> Companion report: `2026-06-03-pci-cardholder-data-finding.md` (cardholder-data storage detail).

---

## Executive Summary (for Steve Eastman / Tom)

This is written plainly for the business owners. The detail and evidence are in the sections that follow.

**What is at stake.** Your **internal billing system** (the staff-operated GTIware/PSA program that auto-charges open invoices) is still writing customers' full credit-card numbers — and the security code on the back of the card (CVV) — into your databases in plain, readable text, and it is adding more of them every day. The **public website itself does not store cards**, but it is the dangerous **access path** to those same databases: its shared administrative database login plus a query-injection flaw reach the exact tables where the stored cards live. The website is **also in PCI scope** in its own right because its pay pages **receive and forward** customers' card numbers to the processor. Storing CVV and unencrypted card numbers like this is a direct violation of the credit-card industry's security rules (PCI DSS), so you are **out of PCI compliance right now**, not at some future date.

The bigger problem is how the website is wired to the database. The website logs into the shared database server with an account that has **full administrative control of that entire server** — not just the website's own data. That same server holds **every office's stored credit cards, roughly 9,000 customer and employee passwords (also in plain text), years of business archives, and payroll data.** Because of a flaw in how the website builds its database queries (SQL injection), an attacker who simply **guesses one customer login** — and the logins are easy to guess and have no lockout — is **one step away from reaching that administrative account.** From there they could:

- **Steal everything:** all offices' card numbers and security codes, all ~9,000 passwords, all customer/financial records, and the multi-year archives.
- **Destroy everything:** delete or encrypt the databases for every office and demand a ransom. The backups sit on the same reachable server, so recovery could be impossible.
- **Commit fraud:** alter invoices, balances, and payment records, or run charges on stored cards.

A breach of this kind would very likely trigger **legally required breach notifications to your customers, card-brand fines, and loss of your ability to accept cards** — on top of the direct theft and any ransom.

**New, and more urgent (verified 2026-06-04).** A deeper inspection of the server confirmed the danger goes beyond "someone could reach the database." The same easy-to-guess website login leads, through flaws already on the box, **straight toward control of your whole Windows network** — because (a) the database server is configured so a successful break-in can run operating-system commands immediately, and (b) your **main Windows administrator password is sitting in plain text inside the database's own scheduled jobs**, where that same break-in can simply read it. In short, one guessed customer password could lead not just to stolen cards but toward **takeover of the entire company network.** Full technical detail is in **C0-Extended**; these findings move several items into a "do first, this week" containment list below.

**The key point about "we switched to Payrilla/Paya."** (This same point is reinforced in "Current State Verified" and finding **C0** below — the repetition is deliberate, because it is the single most common misunderstanding about this risk.) Changing credit-card processors does **not** fix any of this on its own. We verified on 2026-06-03 that the website is **still** running on the old processor (CyberSource/PNC) and that the internal billing system is **still** writing plaintext cards to your databases every day. Switching processors only protects this website **after** it is actually re-pointed to the new processor's secure **tokenized/hosted payment system** — *a setup where the card number goes straight to the processor and is never stored on Glaztech's servers* — **and** the stored plaintext cards are purged. Until both of those happen, the exposure described here is fully live and growing.

**Do first — emergency containment (this week, before the moves below):**
- **Rotate the Windows administrator password** and remove the plain-text copy of it stored inside the database's scheduled jobs (updating every place that uses it in the same change window).
- **Turn off the database's ability to run operating-system commands** (`xp_cmdshell`), and **fix the file permissions** so the website's config file — which holds the database and payment-processor passwords — is no longer readable by everyone on the box.
- **Turn the server's firewall back on** (it is currently off for two of three network profiles) and **replace the end-of-life RealVNC** (a ~2009 version, Steve's tool) with a current, supported remote-access option.

**Then this week (top moves):**
1. **Stop the website from logging into the database as an administrator.** Give it a new, restricted login that can only see its own data — and that explicitly **cannot** reach the stored cards, the passwords, or the other offices' databases. (This must be sequenced carefully — see the roadmap — so we don't break the live site.)
2. **Stop writing the CVV security code today** — a small change to the internal billing system's card-save path so no new CVVs are recorded — and **purge the CVVs already stored** (CVV must never be retained at all). The CVV purge is safe to do immediately once you confirm the auto-charge engine does not re-submit CVV when it runs a saved card; if it does, fix that path first. (Full card-*number* purge comes later — after the tokenized payment path is live — so auto-billing isn't broken.)
3. **Turn on failed-login logging and basic lockout immediately** so guessing attacks can be seen and slowed, and **rotate the administrator password that is sitting in plain text in the website's config file.** Important: before rotating, identify **everything** that uses that login (the internal billing engine, jobs, linked servers) and update them in the same change window — rotating it blindly will break live internal systems. Best done once the new restricted website login above is proven working.
4. **Start the real migration** to the new processor's tokenized/hosted payment system so card numbers never land on Glaztech servers — then, once that is live, **purge the plaintext card numbers** already stored. **Get development tools off the live payment server, replace the end-of-life RealVNC,** and lock down who and what can reach the database server over the network.

The rest of this report is the technical detail, evidence, and a careful step-by-step remediation order so the fixes can be made **without taking the live website down.**

---

## Overall Risk: CRITICAL

The site stores cardholder data (PAN + CVV) and **all user passwords in plaintext**, contains SQL injection and reflected XSS, and runs on a server that doubles as a developer workstation and carries end-of-life software (e.g. RealVNC 4.x). Multiple findings are independently sufficient to cause a reportable breach.

**PCI scope note.** The website does not just sit near the cardholder data — the quick-pay flows **process and transmit** it: the pages **receive the PAN and CVV via POST and forward them through the CyberSource SDK** to the processor. Receiving/forwarding card data places the internet-facing server squarely **inside the PCI cardholder-data environment (CDE) today**, independent of whether the website itself writes cards to disk (it does not — see "Where the Cards Are Stored"). Read any "no card-handling" phrasing in this report as "no card **storage** code on the website," **not** "out of PCI scope."

| # | Finding | Severity |
|---|---|---|
| C0 | **Internet-facing website connects to SQL Server as `sysadmin`** — a SQLi gives full control of the entire shared instance | Critical (top) |
| C1 | Plaintext PAN + stored CVV in DB | Critical |
| C2 | All user passwords stored in plaintext; passwords emailed in cleartext | Critical |
| C3 | SQL injection via fake `quo()` escaper (incl. payment pages) | Critical |
| C4 | Reflected XSS in `gt_errorpage.aspx` | High |
| C5 | **`xp_cmdshell` already enabled + domain-admin password stored cleartext in `msdb` jobs** — a `tom` SQLi yields immediate OS command execution on the DB server *and* domain-admin takeover (see C0-Extended) | Critical |
| H1 | Production payment server is also a dev workstation (VS, SDKs, build tools) | High |
| H2 | End-of-life RealVNC 4.2.8 (≈2009) listening on the CDE host (owner-operated) | High |
| H3 | `debug="true"` + `customErrors=Off` + exceptions echoed to users | High |
| H4 | Server accepts TLS 1.0/1.1 on the listener | High |
| H5 | No cookie Secure/HttpOnly hardening, no MFA, no lockout, session-fixation risk | High |
| H6 | Single shared SQL login with full card-column read; creds in `Web.config` | High |
| H7 | No anti-forgery (CSRF) tokens on payment / ACH / quick-pay flows | High |
| H8 | Missing security response headers (no CSP / HSTS / X-Frame-Options / etc.) | High (low-risk server-config quick win) |
| H9 | No at-rest encryption (TDE) on any of 47 DBs; CHD `.bak` backups copied to an SMB share | High |
| H10 | Host firewall off (Private/Public profiles); RDP/SMB/VNC/WinRM/MSMQ listening; **7 linked servers** form a cross-subnet lateral mesh reachable from `tom` | High |
| M1 | Outdated/unused third-party components; SHA1 machineKey; source on prod | Medium |

---

## Current State Verified — 2026-06-03 (exposure is ACTIVE and ONGOING)

Re-checked the live system after a report that card processing had moved to a different provider ("Payrilla"). *(This processor-switch point is also summarized in the Executive Summary and revisited in C0 — repeated deliberately because it is the most common misunderstanding about this risk.)*
- **The website is still on CyberSource/PNC.** There is **no Payrilla integration** anywhere in the site code or config; the live payment pages remain `online-payment-pnc.aspx` / `quick-pay-pnc.aspx` → CyberSource (`api.cybersource.com`).
- **Card processing is live via PNC today** — `web_payment_header` shows `CC-WebPayment-PNC` approvals/errors on 2026-06-03 (e.g. 14:47 Approved, 15:07 Error) plus E-Check.
- **Plaintext cards are still being written to the local databases every day** — `cc_file` last write: Tucson **2026-06-03 14:15** (8 writes/60d), Phoenix **2026-06-03 10:19** (14 writes/60d). This is ongoing accumulation, not historical data.
- *(Anomaly: `cc_file` returns "Invalid object name" in the `corp` DB though it existed there earlier the same day — needs a second look; the per-office DBs are unaffected and still writing.)*

- **Where is "Payrilla/Paya," then?** Not in the website (CyberSource) and not in Sage 100 — Sage's native CC module is **disabled** (`CreditCardEnable = N`, 0 stored cards). So the new processor is **not visible in either system we can reach**; if it is in use it is a **separate channel** (e.g. a standalone Paya/Payrilla virtual terminal or a different portal) that does **not** cover the website. This is the likely source of the "we moved to Payrilla for everything" belief — it may be true for some manual/standalone processing, but it is **not** true for the customer website, which is the system storing plaintext cards.

**Implication:** any move to Payrilla has **not** been applied to this website. The plaintext PAN + CVV exposure in this report is fully active and **growing daily**; switching processors provides no protection until the website is actually re-pointed to a tokenized store and the stored data is purged. The correct remediation path is to **complete a real migration to the new processor's secure token vault** (store a token, never the PAN), stop writing `cc_file`/`cof_payments_header`, purge the existing plaintext cards + CVV, and then decommission CyberSource. Treat this as a live, present risk.

---

## C0 (TOP CRITICAL) — The internet-facing website connects to SQL Server as `sysadmin`

The single most dangerous fact in this engagement (verified 2026-06-03). The public website logs into the database with login **`tom`**, which is a **SQL Server `sysadmin`** (also `securityadmin`, `dbcreator`, `db_owner`) — i.e. **complete control of the entire instance** at `192.168.8.62,3436` (SQL host `GTI-INV-SQL`). `tom` is a **named SQL login** (SQL authentication, created 2018) that is a *member of the `sysadmin` role* — **not** the built-in `sa` and **not** a constrained service account — and its **password is stored in the website's `Web.config`** on the internet-facing server, so anyone who can read that file (via the SQLi, the dev tooling on the box, or a file-disclosure bug) obtains a sysadmin credential outright. That instance is **shared with the internal GTIware PSA** and hosts **46 databases**: every office's production DB (`glaz_prod*`), all `*_archive` databases (years of history), the PDF stores, TimeForce (`qqest`), `gti_samsara`, and the system DBs (`master`, `msdb`).

Because a SQL injection runs **as the connecting login**, the website's SQLi flaw executes with `sysadmin` rights over that whole instance. Cross-database reach is confirmed live: from a single injectable page on one office's connection we read other offices' card tables (`glaz_prod_phx.dbo.cc_file` = 141, `glaz_prod_den` = 190, `glaz_prod_elp` = 179).

### Attack chain — low effort, catastrophic result
1. **Guess one website login** (username = the customer account number, enumerable; passwords plaintext, as short as 3 chars; **no lockout/rate-limiting**).
2. **Hit an injectable page** (payment pages build SQL via the non-escaping `quo()` helper).
3. **Inject as `sysadmin`** → total control of the SQL server.

### What an attacker can do as `sysadmin` (spelled out)
**Steal everything:** dump **all offices' stored cards (full PAN + CVV)** from `cc_file` + the entire `cof_payments_header` history (tens of thousands of card numbers); dump **~9,000+ plaintext passwords** (`web_security`); dump every customer/invoice/order/balance and the multi-year **archive** DBs; read TimeForce/payroll (`qqest`) and anything else on the instance.
**Destroy everything:** `DROP DATABASE`/`DROP TABLE`/`DELETE`/`TRUNCATE` across all 46 databases — wipe live production for **every office** and the archives; or encrypt/drop and ransom (backups are also reachable, so recovery may be impossible).
**Manipulate / commit fraud:** alter invoices, balances, prices, payment records; change account access levels (grant themselves admin); insert fraudulent transactions; falsify or delete audit history.
**Take over the server + network:** use the **already-enabled `xp_cmdshell`** (verified `value_in_use=1` — no enable step needed) to run **OS commands on the SQL server (192.168.8.62)** — install malware/ransomware, create OS accounts, exfiltrate files, **disable backups and AV**; recover the **`glaztech\administrator` domain-admin password, which is stored in cleartext in `msdb` SQL Agent job steps** (readable by `tom` directly, even without `xp_cmdshell`); and **pivot across the 7 linked servers** into accounting (`mas_gti`), payroll (`qqest`), and backup hosts on two subnets — i.e. **domain-admin takeover.** Plant persistent SQL backdoors and erase SQL audit trails. **This escalation chain is verified and detailed in C0-Extended.**

**Bottom line: one guessed customer password is one SQL-injection away from full theft, destruction, or ransom of the entire GTIware database server — and a foothold into the rest of the network.**

### Required remediation (do this FIRST)
1. **Stop the website connecting as `sysadmin`.** Give the website a dedicated **least-privilege** SQL login scoped to only the specific tables/views/procs it needs, with **no access to the GTIware databases** (`cc_file`, `web_security`, other offices, archives). Then revoke `sysadmin`/`securityadmin`/`dbcreator`. **Do this in the safe order** in the "Sequencing the least-privilege DB migration" callout under the Remediation Roadmap — `tom` may be used by GTIware/other processes, so demoting it in place can break the live site.
2. **Separate the website's data from GTIware** — the internet-facing app must not share a SQL instance with the internal PSA's cardholder data. Either move the website to its own database/instance, **or** strictly partition permissions so the public web login **cannot see or reach** the GTIware databases.
3. **Stop storing cardholder data on-prem at all** (best fix): either **call the processor's API directly** (processor-hosted fields / vault — no card data lands on Glaztech systems) or, at minimum, **tokenize the entire process** (store a processor token, never the PAN). **CVV must never be stored — period (PCI Req 3.2).** Any cardholder data that must be at rest has to be **encrypted**. *(Note: simply switching processors — the "we moved to Payrilla/Paya" point raised in the Executive Summary and Current State sections — does not accomplish this on its own; the stored-card exposure persists until the tokenized path is live and the stored PANs are purged. The repetition of this point across those sections is deliberate.)*
4. Then: fix the SQL injection (parameterize, remove `quo()`), add login lockout/rate-limiting, hash passwords.

---

## C0-Extended — Verified blast radius and privilege-escalation chain (deep recon, 2026-06-04)

A follow-up read-only recon of the `WWW` host and the SQL instance (host telemetry + SQL **metadata** only — no CHD, no secret values) confirmed C0's "foothold into the rest of the network" warning is not hypothetical. The public website is the front door to a **multi-server, sysadmin-and-domain-admin blast radius**, and the path from a web compromise to full domain takeover needs **no additional vulnerability** beyond the SQLi already documented in C3.

### The card engine runs in-process on the public server, on the `sysadmin` login
- The IIS worker for the public site (`w3wp.exe`, PID 13752) runs as `IIS APPPOOL\glaztech_new` — a low-privilege *local* identity (good locally). **But** the GTIware card-on-file engine DLLs **`gt_auto_process_2020.dll` and `glaztech_utilities_2020.dll` are present in the live `D:\web\glaztech_4\Bin\`** (plus duplicate copies in `Bin\Old_bin\` and `Old_code\Bin\`).
- Their `.dll.config` files carry **no connection strings of their own**, so the in-process engine uses the website's `Web.config` `tom` (sysadmin) connections. **A web-tier compromise therefore lands directly inside the card-on-file write/charge engine, with `sysadmin` DB rights** — the "website vs. GTIware" distinction collapses for any in-process or host-level attacker. The CDE is effectively the public host itself.

### From SQLi to OS command execution — already armed
- `sys.configurations`: **`xp_cmdshell = 1` (enabled, `value_in_use = 1`).** There is no "enable it first" step — a `tom` SQLi can `EXEC xp_cmdshell` for **immediate OS command execution on the SQL server** (`192.168.8.62` / `GTI-INV-SQL`). (`clr enabled`, `Ole Automation Procedures`, `Ad Hoc Distributed Queries`, and `remote admin connections` are all `0` — `xp_cmdshell` is the live one.)
- The **SQL Server Agent service runs as `Administrator@glaztech.com`** (a domain-admin-class account). Any Agent job step — which sysadmin `tom` can create — executes as that account.

### Domain-admin password sitting in cleartext in `msdb`
- Multiple backup-copy SQL Agent job steps run `EXEC xp_cmdshell 'net use \\192.168.8.52\sql_backup\... /user:glaztech\administrator <CLEARTEXT-PASSWORD> /persistent:yes'`. **The `glaztech\administrator` domain-admin password is stored in cleartext in `msdb.dbo.sysjobsteps`,** readable by any sysadmin/`tom` connection — i.e. **harvestable straight from the web-app SQLi, without even invoking `xp_cmdshell`.** (The password value was redacted from all assessment artifacts and is not reproduced; it must be rotated — see the Emergency-containment bucket in the roadmap.)

### Lateral reach — a cross-subnet linked-server mesh
- The instance defines **7 linked servers, all with remote-login enabled and most with data access enabled**, spanning two subnets: `192.168.0.54,55181`; `192.168.0.55,55181` (the `mas_gti` Sage/accounting system); `192.168.8.212,3436`; `192.168.8.52,3436`; `192.168.8.62,3430`; `GLAZ\TIMEFORCE` (payroll `qqest`); and `GTI-INV-SQL\GTISQL`. From the single internet-reachable `tom` credential an attacker pivots across the whole SQL estate — **including accounting and payroll** — on two subnets.
- The real automated card-charging runs **server-side** via CmdExec Agent jobs invoking `d:\sql_jobs\bin\gt_console_apps.exe` (`cp` = courtesy/card payments, `is` = invoice statements, `oa` = order acks, `lo` = load PDFs) as the domain-admin Agent account — a second GTIware execution host beyond the in-process DLLs.

### Over-privileged logins; no encryption at rest
- **Sysadmin logins:** `tom` (the website's app login), the built-in **`sa` is enabled**, `GTI-INV-SQL\Administrator`, plus SQL service virtual accounts. Most databases run at **compatibility level 90 (SQL Server 2005-era).**
- **No encryption at rest:** **all 47 databases report `is_encrypted = False`** (no TDE), including every CHD-bearing `glaz_prod*`. Production `.bak` backups are written to `d:\sql_backup\prod\` **and copied to the `\\192.168.8.52\sql_backup\` SMB share** — unencrypted cardholder data on a reachable file share (this confirms and locates the "backups contain plaintext PAN/CVV" liability noted under C1 / "Where the Cards Are Stored").

### Secrets on the web host, readable by everyone
- `Web.config` `appSettings` hold **two payment processors' credentials in cleartext** — CyberSource (`cybs.Password`, `cybs.merchantID.password`, plus on-disk signing keys at `cybs.keysDirectory` / `cybs.keyFilename`) **and PNC** (`pnc_key`, `pnc_pin`, `pnc_token`, transaction/surcharge URLs).
- The web-root ACL grants **`Everyone:(R)`** on `D:\web\glaztech_4`, its `bin`, **and `Web.config`** — so any local principal (or any of the remote-access agents below) can read the `tom` SQL password and both processors' secrets. (Not world-*writable*, so no direct web-shell drop via ACL.)

### Remote access — clarified
- The remote-management agents on this host — **both ScreenConnect clients (`support.azcomputer.guru` and the `kgc7jt` ScreenConnect Cloud instance), Datto RMM (CentraStage), Datto EDR (Infocyte), Syncro, Splashtop, and GuruRMM** — are **ACG's own sanctioned management stack**, not a third-party intrusion. *(An initial pass flagged the `kgc7jt` ScreenConnect as "foreign/unrecognized"; on confirmation it is an ACG cloud instance — withdrawn, no finding.)*
- The one genuine remote-access finding is **end-of-life RealVNC 4.x (`WinVNC4`), listening on 5900/5800** — **the owner's (Steve's) tool**, a ~2009 build with known auth-bypass-class issues, running on the CDE host. Update/replace it with a current supported version (coordinate with Steve) — see **H2**.

### Host network posture (H10)
- **Host firewall: Domain profile On, but Private and Public profiles Off.** Listening services include RDP (3389), SMB (445), NetBIOS (139), VNC (5800/5900), ScreenConnect (6783), WinRM (5985/47001), MSMQ (1801/210x), and Dell OMSA (1311) — on a flat network these are lateral-movement surface.
- **TLS 1.0 is explicitly enabled** in SChannel (`Enabled=1`, Server and Client), not merely left at OS default (sharpens H4).
- **New internet-facing endpoints** under the same site: **`/webhooks`** (a Samsara webhook receiver, custom `samsara_webhook_receiver.dll`, physical path `...\incoming\samsara`) and **`/webhooks1`** (a `webhooks_test` app) — unreviewed input surfaces not in the original scope.
- **Additional hosts surfaced:** a second `\\192.168.0.147\web\glaztech_4` (referenced by a cleanup job), backup servers `192.168.8.52` / `192.168.8.212`, the MAS system `192.168.0.55`, and linked `192.168.0.54`.

### The chain, end to end
Public site → SQLi as sysadmin `tom` (C3/C0) → **(a)** read all CHD + ~9,000 plaintext passwords (C1/C2); **(b)** `xp_cmdshell` → OS RCE on the DB server; **(c)** read the cleartext `glaztech\administrator` password from `msdb`; **(d)** pivot across 7 linked servers (accounting, payroll, backups) on two subnets; **(e)** reach **domain admin.** In parallel, the in-process GTIware engine and the `Everyone:(R)` secrets give a host-level attacker the same outcome without SQLi. **This is why the containment items now lead the remediation roadmap.**

---

## Critical Findings

### C1 — Plaintext PAN and stored CVV (see companion PCI report)
`cc_file` (~780 saved cards) and `cof_payments_header` (tens of thousands of rows; e.g. Phoenix 14,496 / 11,794 plaintext) store full card numbers unencrypted, and `cc_file.cc_code` retains CVV (PCI Req 3.2 — prohibited). Detail and remediation in the PCI report.

### C2 — All passwords stored in plaintext; cleartext password email
- **Customer portal:** auth stored proc `get_web_accesslevel` compares `web_security.web_password = @passwd` **with no hashing**. `web_security` holds **~9,000+ plaintext passwords** (corp 6,017 + tuc 3,012 + other offices), 0 hash-like values, lengths 3–19.
- **Employees:** `emp/employee-login.aspx` "forgot password" verifies last name + email, then **emails the user their existing plaintext password** (`"The password to your employee profile is: " + pword`) — only possible with reversible/plaintext storage.
- **Impact:** any DB read (or the existing SQLi) exposes every customer/employee credential in the clear; password reuse means broad downstream compromise. Weak "lastname + email" knowledge check gates the password email.
- **Fix:** store only salted password hashes (PBKDF2/bcrypt/Argon2); never email passwords — implement a time-limited reset-token flow. For the ~9,000-account cutover, prefer a lower-disruption rollout (hash on the fly via "force change on next successful login" + reset tokens) over a single forced global reset — see the roadmap.

### C3 — SQL injection via non-escaping `quo()` helper
```vb
Function quo(stext) As String
    Return "'" + stext + "'"     ' wraps in quotes but does NOT escape embedded quotes
End Function
```
- Used to build **concatenated dynamic SQL** in multiple pages including payment flows (`ach.aspx.vb`, `quick-pay-ach.aspx.vb`, `quick-pay-pnc.aspx.vb`, `quick-pay.aspx.vb`, `order-detail*`). Any input containing `'` breaks out of the string → injection.
- Codebase posture is *mixed*: 948 properly parameterized calls vs. **59 concatenated SQL statements** (~10 joining user input). The login path itself is parameterized (sproc) and **not** injectable; the risk is the concatenated set.
- **Fix:** replace all concatenation with parameterized commands / stored procedures; delete `quo()`. Prioritize payment pages.

### C4 — Reflected XSS in `gt_errorpage.aspx`
- `smessage = Request.QueryString("errmsg")` (line 20) → `lblerr.Text = smessage` (line 48). `Label.Text` is **not** HTML-encoded, and the app redirects many exceptions to `gt_errorpage.aspx?errmsg=<msg>` (often containing raw `ex.Message`). An attacker-supplied `errmsg=<script>…</script>` executes in the victim's browser.
- **Fix:** HTML-encode (`Server.HtmlEncode`) before output; stop placing exception text in URLs; show generic errors to users and log details server-side.

---

## Attack Path — A Single Guessed Login → the Entire Card Database

Chaining the findings into the realistic worst case, with difficulty ratings.

**Step 1 — Obtain a customer login (LOW).** Username = the customer **account number** (enumerable, not secret). Passwords are plaintext, **as short as 3 characters**, no complexity rules, and there is **no account lockout or rate-limiting** — unlimited guessing / credential-stuffing.

**Step 2 — Normal UI (masked display).** Payment pages *display* cards masked to last-4 (`xxxx-xxxx-xxxx-1234`), so a point-and-click attacker sees last-4 + expiry + cardholder/billing data and can transact on saved cards. Note: the read proc `get_cc_data` is `SELECT * FROM cc_file WHERE acct_no=@acctno` — it returns the **full PAN and CVV to the application server**; only the *display* is masked, and the `@acctno` parameter makes it an IDOR-shaped full-card read. Any endpoint returning that proc's output unmasked (or the SQLi below) yields full numbers.

> **TO VERIFY (high priority) — IDOR on `get_cc_data(@acctno)`.** We have **not** confirmed how the `@acctno` passed to `get_cc_data` is bound to the authenticated session. **Recommend confirming** whether the application strictly derives `@acctno` from the logged-in user's own account (so a customer can only retrieve **their own** cards), versus accepting an `acct_no` supplied by the client — in which case **any logged-in user could request an arbitrary account number** and pull that account's full PAN/CVV to the app server (a classic Insecure Direct Object Reference). This is an open verification task; if the binding is not server-side and session-enforced, it is a second full-PAN read path independent of the SQLi and should be treated as Critical.

**Step 3 — SQL injection (FULL exposure).** The post-login payment pages (`quick-pay`, `ach`, `quick-pay-pnc`) build SQL with the non-escaping `quo()` helper and require only a valid session. A logged-in attacker can UNION-inject `SELECT cc_number, cc_code FROM cc_file` and exfiltrate **every stored full card number AND CVV** for the office — directly, because the data is **plaintext** (no encryption/key to defeat). UI masking is irrelevant at this layer.

| Goal | Difficulty |
|---|---|
| Obtain a valid login | **Low** (no lockout, guessable username, 3-char plaintext passwords) |
| See last-4 / transact via UI | **Low** |
| Exfiltrate ALL full PAN + CVV | **Low–Moderate** (one login + standard SQLi; plaintext data) |

**There is no defense-in-depth** — every compensating control (lockout, password hashing, PAN encryption, parameterized queries) is absent, so the first failure is the last failure. Highest-leverage breakers: login lockout/rate-limiting, parameterize the payment-page SQL (remove `quo()`), purge CVV + tokenize/encrypt PAN.

---

## Why the Cards Are Stored, and Where They Flow

**Business purpose — card-on-file invoice auto-pay.** Cards are stored (with an `activate` flag on `cc_file`) so the business can automatically charge customers' open invoices. The proc `i_get_cc_on_file_invoices` joins `invoice` × `cc_file` for active cards with an outstanding, delivered balance; `gt_auto_process_2020.dll` / `glaztech_utilities_2020.dll` are the engine that reads the stored card and bills it (currently via CyberSource). **This is GTIware (the internal PSA), staff-operated — NOT the website's storage code.** *Important nuance verified 2026-06-04 (see C0-Extended): those engine DLLs are physically deployed in the website's own `D:\web\glaztech_4\Bin\` and load **in-process into the public IIS worker**, and they carry no DB credentials of their own — so they run on the website's `tom` (sysadmin) connection. A second copy of the billing logic, `d:\sql_jobs\bin\gt_console_apps.exe`, runs server-side from SQL Agent jobs. So while the **storage feature** is GTIware's, the **execution surface co-resides with the internet-facing site** — which is why a web compromise reaches the card engine directly.* The website has **no card-storage code** (no `cc_file`/`save_cc_data`/`gt_auto_process` references in its `.aspx`/`.vb`), and its quick-pay pages even disclaim saving cards. **It does, however, still receive and forward card data:** the quick-pay flows accept the PAN/CVV via POST and pass them through the CyberSource SDK to the processor, which is what keeps the website inside the PCI CDE (see the PCI scope note under Overall Risk). "No card-handling" here means **no storage**, not out-of-scope. Saved-card rows are stamped with **staff** usernames (Victoria, Bryce, Diana) and notes like "run card when requested." The large `cof_payments_header` history (e.g. 14,496 rows in Phoenix) is years of these GTIware charges.

**Where the full PAN is used.** Only five DB objects reference the full `cc_number`: `save_cc_data`/`save_cc_data1`/`save_cc_data2` (writes) and the `is_cc_active`/`is_cc_on_file` functions. However, **`get_cc_data` is `SELECT *`**, so it also returns the full PAN + CVV whenever a saved card is read for charging — the full number crosses to the app server on every card-on-file charge; the UI only masks the *display*.

**Containment — does NOT spread to other systems.** The Sage 100 ERP DB (`mas_gti`) has **0** procedures referencing `cc_file` or `web_security` — the plaintext cards do **not** propagate into Sage. (Sage's native CC module is in fact **disabled** — `SY_Company.CreditCardEnable = N`, `AR_CustomerCreditCard` = 0 rows — so Sage stores no cards at all and is not a cardholder-data location.) Exposure is **contained to the GTIware databases** (15 office DBs) on `GTI-INV-SQL` (`192.168.8.62`) — the **same SQL instance the public website connects to**, which is exactly how the website's SQLi reaches the card data (see **C0**). Secondary exposure surfaces: **database backups** (every backup of those DBs contains plaintext PAN + CVV) and **stale on-disk code/data copies** (`Old_bin`, `Old_code`).

**Fix preserves the feature.** Have GTIware **call the processor's API directly / use its hosted vault** (no card data stored on-prem), or at minimum **tokenize** (store a processor token; let the processor hold the PAN) — `gt_auto_process` keeps auto-billing by token while removing every stored PAN/CVV (and the backup liability). **CVV must never be stored (PCI Req 3.2)**, and any at-rest cardholder data must be **encrypted**.

---

## High Findings

### H1 — Production payment server is also a developer workstation
Installed on the live server: **Visual Studio Community 2015 and 2022, .NET 8 SDKs, MSBuild/Build Tools, TFS office integration, IIS 10 Express, Notepad++, WinRAR 7.22, OpenSSL 3.5.0**. Full application **source code is on the box** (128 `.vb` + 125 `.aspx.vb`, not precompiled). This massively expands attack surface and blast radius on the host that processes cardholder data. **Fix:** move development off the production host; deploy precompiled; remove SDKs/IDEs/dev tools.

### H2 — End-of-life RealVNC on the cardholder-data host (owner-operated)
**RealVNC 4.x (`WinVNC4`) is installed and listening on 5900/5800** — a ≈2009 build, critically outdated with known auth-bypass-class issues, running on the server that processes cardholder data. This is **Steve's own remote-access tool**, not ACG's. *(For clarity: the other remote-management agents on the box — both ScreenConnect clients including the `kgc7jt` cloud instance, Datto RMM/EDR, Syncro, Splashtop, GuruRMM — are **ACG's sanctioned management stack** and are not findings; an earlier draft mis-flagged the `kgc7jt` ScreenConnect as third-party and that has been withdrawn.)* **Fix:** update or replace RealVNC 4.x with a current supported version (or retire it), coordinated with Steve; ensure whatever remains is patched and access-controlled.

### H3 — Debug/error information disclosure
`Web.config`: **`<compilation debug="true" …>`** in production and **`<customErrors mode="Off"/>`** present; login/employee code echo `ex.Message` to the page or via `errmsg`. Leaks stack traces, SQL errors, internal paths. **Fix:** `debug="false"`, `customErrors="On"` with a generic page, stop surfacing exception text to users.

### H4 — Listener accepts TLS 1.0/1.1
SChannel: **TLS 1.0 Server `Enabled=1`** (and TLS 1.1 at OS default = enabled); TLS 1.2 enabled. The public HTTPS endpoint therefore still negotiates deprecated TLS — a PCI listener finding. **Fix:** disable TLS 1.0/1.1 (SChannel server) after confirming no legacy client dependency; keep TLS 1.2.

### H5 — Session/credential handling
- Custom **Session-variable auth** (no ASP.NET Forms auth); **no session-ID regeneration on login** → session-fixation risk.
- **No `requireSSL`** and **no `httpOnlyCookies`** configured → cookies not marked Secure (site was HTTP-reachable until the 2026-06-03 HTTP→HTTPS redirect was added).
- **No MFA, no account lockout / rate limiting**; username = customer **account number** (guessable) → brute-force exposure.
- **Detection blind spot (see Appendix A):** the employee login returns **HTTP 200 on both success and failure** (no redirect-on-success), and the app logs **no failed login attempts** anywhere. App-level auth never touches the Windows Security log. The net effect is that a slow credential-guessing attack against staff or customer accounts would be **effectively invisible** — there is no lockout to stop it and no log to detect it after the fact.
- **Fix:** Secure+HttpOnly cookies, regenerate session on login, add lockout/throttling, **add failed-login logging (timestamp, username, source IP)**, consider MFA for employee/admin access.

### H6 — Database access model
Web app connects with a **single shared SQL login (`tom`)** that has full read on card and password columns (no column-level control); **connection strings with credentials are in `Web.config`** on the web server (15+ per-office DBs). **Fix:** least-privilege per-function accounts, remove blanket card/password read, protect/secret-manage connection strings, enable TDE at rest.

### H7 — No anti-forgery (CSRF) protection on payment / ACH / quick-pay flows
Authentication is custom **Session-variable** based (see H5), and we observed **no ASP.NET anti-forgery / CSRF tokens** on the state-changing flows — the payment, ACH, and quick-pay POST handlers act on the session alone, with no per-request token tying the request to the user's own page. The consequence: a logged-in customer (or a staff member with a session) who is lured to a malicious page could have a **payment, ACH submission, or account change submitted on their behalf cross-site**, without their intent. This is a distinct **financial / business-logic** risk from the SQLi (C3) and XSS (C4) — those are injection flaws; this is forged-but-valid requests. **Fix:** add per-request anti-forgery tokens (e.g. ASP.NET `AntiForgeryToken` or an equivalent synchronizer-token pattern) to all state-changing POSTs, validate them server-side, and reject requests without a matching token. *(Note: the absence of anti-forgery tokens is the verified observation; a working cross-site exploit was not constructed — see Assumptions.)*

### H8 — Missing security response headers
*Severity: **High** — but a low-risk, server-config-only quick win (no application code change), which is why it is sequenced early in the roadmap ahead of higher-effort code fixes.*

The application does not emit the standard hardening response headers:
- **No Content-Security-Policy (CSP)** — a CSP would materially reduce the impact of the reflected XSS in **C4** (it constrains what injected script can load/run) and is the single highest-value header to add here.
- **No HSTS (`Strict-Transport-Security`)** beyond the recently-added HTTP→HTTPS redirect (see H5) — without HSTS the browser is not told to refuse plain HTTP on future visits.
- **No `X-Frame-Options` / frame-ancestors** — leaves the site open to clickjacking/framing.
- **No `X-Content-Type-Options: nosniff`** — allows MIME-sniffing of responses.
- **No `Referrer-Policy`** — leaks full referrer URLs cross-site.

**Fix:** add these headers at the IIS/site level (CSP, HSTS, X-Frame-Options/`frame-ancestors`, X-Content-Type-Options, Referrer-Policy). CSP first, as a partial mitigation for C4 while the XSS itself is fixed.

---

## Medium / Component Hygiene

- **Outdated third-party libraries in `bin`:** `AjaxControlToolkit 3.0.30930` (2008 — present but **not referenced**, remove it), `Microsoft.IdentityModel.Tokens` / `System.IdentityModel.Tokens.Jwt 5.1.2` (2017), `CyberSource SDK 1.4.10` (legacy), assorted GrapeCity ActiveReports versions. Inventory and update/remove.
- **`machineKey validation="SHA1"`** — move to SHA-256 (AES/HMACSHA256) with managed keys.
- **Source code resident on production** — remove; deploy build artifacts only.
- **OpenSSL 3.5.0 / WinRAR 7.22 / Chrome** on a server — patch or remove; reduce footprint.

---

## What is Acceptable (balanced view)
- **OS patching is current-ish:** Windows Server 2019, build 17763.8755, patched through **May 2026** (supported to 2029) — the OS itself is *not* the weak point.
- **Most data access is parameterized** (948 parameterized calls) — the SQLi exposure is a bounded set of concatenated queries, not pervasive.
- **The Sage 100 ERP DB** (`mas_gti`) stores **no** cardholder data — its native CC module is **disabled** (`CreditCardEnable = N`, `AR_CustomerCreditCard` = 0 rows). The plaintext exposure is **entirely GTIware's card-on-file feature** — **not** Sage, and **not** the website's payment pages (which store nothing, though they remain in PCI scope as a card-data conduit, see Overall Risk). The website's role is the *access path* (C0) and a card-data *conduit*, not the storer. *(Detail in "Why the Cards Are Stored" below.)*
- TLS 1.2 to CyberSource now works (payment outage fixed 2026-06-03).

---

## PCI DSS Control Mapping

Key findings mapped to the PCI DSS requirements they implicate. Where an exact sub-requirement number is not certain, the requirement **family** is cited; treat this as indicative for the QSA/SAQ conversation, not a formal gap assessment.

| Finding | PCI DSS Requirement(s) | Why |
|---|---|---|
| C1 — plaintext PAN + stored CVV | **3.2** (no storage of sensitive auth data / CVV after authorization) and **3.4** (render PAN unreadable at rest) | CVV must never be retained; stored PAN must be encrypted/tokenized/truncated/hashed |
| C2 — plaintext passwords; emailed cleartext | **8.2.x** (strong cryptography for authentication credentials / one-way hashing of passwords) | App credentials must be hashed, never reversible or emailed |
| C3 — SQL injection (`quo()`) | **6.5.1** (injection flaws) | Address injection in secure-coding practices |
| C4 — reflected XSS | **6.5.7** (cross-site scripting) | Output encoding / XSS mitigation required |
| C0 / H6 — website connects as `sysadmin`; shared instance; blanket card/password read | **1.2 / 1.3** (segmentation / restrict CDE connectivity) and **7.x** (least privilege / need-to-know) | Flat reach into the CDE and an over-privileged DB login violate segmentation and least-privilege |
| H4 — TLS 1.0/1.1 on the listener | **4.1** (strong cryptography/TLS for cardholder data in transit) | Deprecated TLS on the card-accepting endpoint |
| H5 / Appendix A — no failed-login logging, no audit of auth events | **10.2 / 10.3** (audit logging of access and required log detail) | Auth attempts must be logged with the required fields (user, timestamp, source) |
| H7 — no anti-forgery tokens | **6.5.9** (CSRF) | State-changing requests must be protected against cross-site request forgery |
| H8 — missing CSP/HSTS/etc. | **6.5.x** (secure-coding controls for the public-facing app) | Hardening response headers are part of secure-coding controls for the public-facing app |
| H1 / M1 — dev tooling, source, outdated components on prod | **6.x** (secure systems/components) and **2.x** (minimize/secure system components) | Reduce attack surface; keep components current; don't run dev tooling on the CDE host |
| C5 — `xp_cmdshell` enabled; cleartext domain-admin in `msdb` | **2.2 / 7.x / 8.2.x** (secure configuration / least privilege / protect credentials) | OS-command surface enabled by default and a reusable admin secret stored in the clear |
| H2 — end-of-life RealVNC on the CDE host | **6.2 / 2.x** (patch/replace EOL components; minimize/secure system components) | EOL remote-access software with known vulns on the card-data host |
| H9 — no TDE; CHD backups on a share | **3.4** (render PAN unreadable at rest); **9.x** (protect backup media) | Stored PAN and its backups are unencrypted |
| H10 — flat network; firewall off; linked-server mesh | **1.2 / 1.3** (segmentation / restrict CDE connectivity) | Lateral reach across subnets from the CDE |

> The website **processes and transmits** cardholder data (see Overall Risk / PCI scope note), so it is in PCI scope today. The merchant's SAQ type and the controls above should be re-confirmed with the acquirer/QSA after remediation.

---

## Assumptions, Scope & Limitations

So the findings are read with the right confidence level:
- **Read-only / passive methods only.** The assessment used config/registry inspection, on-server source review, and read-only/aggregate database inspection via GuruRMM. **No dynamic scanning** and **no exploit verification** were performed beyond the noted **live cross-database reads** (e.g. reading other offices' `cc_file` row counts to confirm C0's cross-DB reach). The SQLi, XSS, CSRF, and IDOR findings are assessed from code/config evidence; working exploits were **not** built.
- **GTIware source and the auto-process DLLs were not decompiled.** `gt_auto_process_2020.dll` / `glaztech_utilities_2020.dll` were not reverse-engineered; their **deployment** was verified, however (2026-06-04): both are present in the live web `Bin\`, load in-process into the public IIS worker, and have no connection strings in their `.dll.config` — so they run on the website's `tom` connection (see C0-Extended). Internal GTIware application source beyond what is on the web box was not reviewed.
- **Database backups were located but not opened.** Backup **destinations** are now known (`d:\sql_backup\prod\` and the `\\192.168.8.52\sql_backup\` SMB share) and at-rest encryption is confirmed absent (no TDE on any of 47 DBs), so backups demonstrably contain plaintext PAN/CVV; the retention schedule, share ACLs, and disposal process were not separately inspected.
- **No cardholder data and no passwords were intentionally retrieved.** Sensitive columns were classified by aggregate only (counts, lengths, hash-likeness). **One exception, handled:** a `glaztech\administrator` domain-admin password was returned *incidentally* inside a SQL Agent job-step command preview (`msdb` metadata); it was immediately redacted from all assessment artifacts, is not reproduced anywhere in this report, and is flagged for rotation (see C0-Extended / Emergency containment).
- **RMM/EDR agent security not deeply tested.** The remote-access/management agents (H2) were inventoried for presence and version, not security-tested for their own configuration or exposure.

---

## Prioritized Remediation Roadmap

**Order-of-operations matters here.** Several of these fixes can take the live site down if done in the wrong sequence — most notably the database-privilege change (C0/H6). The buckets below are ordered for safety, and the high-risk steps carry explicit sequencing and rollback notes. Read the "Sequencing the least-privilege DB migration" callout before touching login `tom`.

> ### Sequencing the least-privilege DB migration (the riskiest step — read first)
> **This callout is the single authoritative sequence for the C0/H6 database-privilege fix.** Where "Now" item 2 and "Short-term" item 9 reference this change, they defer to these steps rather than restating them — follow the order here.
>
> The single most important fix (C0) is also the easiest to break the live site with, because **we have not confirmed that login `tom` is used only by the website.** GTIware (the internal card-on-file engine), internal jobs/configs, and linked servers may authenticate to the same instance as `tom`. Do **not** simply demote or re-password `tom` in place. Correct sequence:
> 1. **Inventory two things at once:** (a) the exact tables/views/stored procs the website's queries actually touch (the 948 parameterized calls + the 59 concatenated statements give the object list), and (b) **every consumer of the `tom` login** — the GTIware card-on-file engine, internal jobs/scheduled tasks/configs, and any linked servers — since `tom`'s password must later be rotated and every consumer updated in the same change window.
> 2. **Create a NEW least-privilege login** for the website with grants on **only** the website's objects, plus **explicit `DENY`** on `cc_file`, `web_security`, every **other office's** databases, the `*_archive` databases, and the system DBs (`master`, `msdb`, etc.).
> 3. **Stage the `Web.config` swap** to the new login with **error monitoring** in place (watch the app and SQL error logs) so any missed object permission surfaces immediately and can be granted or rolled back. Confirm the new login is proven in production.
> 4. **Only THEN** revoke `sysadmin` / `securityadmin` / `dbcreator` from `tom` — and only after confirming nothing else depends on those rights.
> 5. **Rotate the `tom` password** so the plaintext `sysadmin` credential is no longer sitting in `Web.config`. Do this **only after** the new website login is proven (steps 2–3) **and** all `tom` consumers from step 1b are identified and updated to the new password **in the same change window**. Rotating `tom` before its consumers are inventoried and updated will break live internal systems.
>
> **Getting this wrong breaks the live website.** Treat steps 2–5 as a change with a validation + rollback plan (revert the `Web.config` connection string), not a one-shot edit.

**Emergency containment (THIS WEEK, before everything below) — closes the trivially-exploitable internet-to-domain-admin path while it is open (see C0-Extended). These are mostly server-config/account actions, not application code:**
- **E1. Remove `Everyone:(R)`** from `D:\web\glaztech_4`, its `bin`, and `Web.config` (scope to the app-pool identity + required admins) so local principals can no longer read the `tom` SQL password and the CyberSource/PNC secrets.
- **E2. Rotate `glaztech\administrator`** and remove every embedded cleartext use of it (the `xp_cmdshell 'net use …'` backup-copy Agent job steps), updating all of them in the same change window; pause those jobs until they use a dedicated low-privilege account.
- **E3. Disable `xp_cmdshell`** on `GTI-INV-SQL` (`EXEC sp_configure 'show advanced options',1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell',0; RECONFIGURE;`) to remove the direct RCE step; verify on the linked instances too.
- **E4. De-privilege the SQL Agent service account** off domain-admin to the minimum needed to run `gt_console_apps.exe`; **disable the built-in `sa`** login (or confirm a strong, unique password and restricted use).
- **E5. Turn the Private/Public host-firewall profiles back On**, **replace/disable the end-of-life RealVNC** (5900/5800 listener — Steve's tool, coordinate with him), and restrict WinRM/MSMQ/Dell-OMSA to specific management IPs.

**Now (days) — start immediately; lower-risk items, EXCEPT item 2 which must follow the sequencing callout:**
1. **Stop CVV storage, then purge stored CVV** (`cc_file.cc_code`):
   - **Immediate, safe now — STOP WRITING new CVV.** A code change to the GTIware `save_cc_data*` path so `cc_code` is no longer populated going forward. *(This is a GTIware/internal-billing-system change, tracked as a separate workstream — see Structural item 17.)*
   - **Immediate, after a quick confirmation — PURGE existing `cc_file.cc_code` values.** CVV must never be stored (PCI Req 3.2), and card-on-file recharges normally do **not** re-submit CVV at charge time, so this should be doable right away. **Caveat:** first confirm the auto-charge engine (`gt_auto_process`) does not re-submit CVV when it runs a saved card — if it does, defer the value-purge until that path is fixed. (Full **PAN** purge is **not** here — it is strictly post-tokenization; see Structural item 16.)
2. **Stand up the new least-privilege website login and revoke `tom`'s `sysadmin` rights — strictly per the "Sequencing the least-privilege DB migration" callout above (do not improvise the order).** This is the **riskiest** step in this bucket: getting it wrong breaks the live website. The `tom` password rotation that removes the plaintext `sysadmin` credential from `Web.config` is part of that sequence (callout step 5) and must wait until the new login is proven **and** every `tom` consumer has been inventoried and updated in the same change window.
3. **Turn on failed-login logging** (timestamp, username, source IP) **and basic throttling/lockout immediately** — this closes the detection blind spot (H5 / Appendix A) and slows guessing while the deeper fixes land.
4. **Interim defense-in-depth:** consider a **WAF or IIS request-filtering rule** in front of the app as a stop-gap against SQLi/XSS **while the code is being fixed** — not a substitute for the code fixes, but valuable coverage during the window.
5. `debug="false"`, `customErrors="On"`; HTML-encode `gt_errorpage` output; stop echoing exceptions (H3/C4).
6. Add the **security response headers** (CSP first — partial mitigation for C4 — plus HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy) (H8). *(Server-config only, no code change — quick win; this is why H8 lands in the "Now" bucket ahead of the code-level CSRF fix H7, despite H7 being scored higher.)*
7. Remove **RealVNC 4.2.8** and the stale **ScreenConnect v6** client (H2).
8. Disable **TLS 1.0/1.1** on the listener (H4) — **with a rollback plan** (re-enable if a legacy client breaks); confirm no legacy client dependency first.

**Short term (weeks) — code and credential fixes (validate + rollback each; see testing gates):**
9. **Complete the least-privilege DB migration** per the "Sequencing the least-privilege DB migration" callout — finish callout steps 4–5 (revoke `sysadmin`/`securityadmin`/`dbcreator` from `tom`, then rotate `tom`'s password with all consumers updated in the same change window) once the new login is proven in production. *(Refer to the callout for the authoritative step order; not restated here.)*
10. **Parameterize the concatenated SQL** (payment pages first); delete `quo()` (C3).
11. **Convert passwords to salted hashes** (PBKDF2/bcrypt/Argon2) and replace the email-the-password flow with **time-limited reset tokens**; **never email passwords** (C2). *Rollout nuance:* a forced **global reset for ~9,000 customers + staff is operationally heavy** — prefer a lower-disruption path: hash on the fly via **"force change on next successful login"** plus time-limited reset tokens, rather than invalidating everyone at once.
12. **Add per-request anti-forgery (CSRF) tokens** to the payment/ACH/quick-pay POSTs and validate them server-side (H7).
13. Secure+HttpOnly cookies; **session regeneration on login** (H5).
14. **Verify and, if needed, fix the `get_cc_data` IDOR** (the TO-VERIFY item) — bind `@acctno` to the session server-side so a user can only read their own cards.
15. **Encrypt the `Web.config` connection strings** / move to proper secret management (H6) — so the DB credential is not plaintext on disk even on the new login.

**Structural (project) — the durable fixes:**
16. **Move card-on-file to a tokenized / processor-hosted model.** Prefer the processor's **hosted fields / direct-API vault so the PAN never touches Glaztech servers** over "collect the card ourselves and store a token after." Then **purge / encrypt the historical PAN columns** and decommission CyberSource (ties to the Current-State migration note).
    - **Prerequisite — the GTIware token migration (item 17) MUST be complete before the historical PAN columns are purged.** The auto-charge engine still reads the full PAN to bill saved cards; purging PAN while it still depends on the full number would break auto-billing and risk data loss. Until the GTIware side lands, expect **new PANs to keep accumulating** — the purge is a one-time action taken only after writes have stopped and tokens have replaced the stored numbers.
17. **GTIware-side work (PARALLEL, REQUIRED — not the website team's job):** stop the GTIware engine writing `cc_file` / `cof_payments_header`, and migrate `save_cc_data*` and `gt_auto_process_2020.dll` to tokens. This is a separate workstream from the website fixes and must be tracked and resourced as such; the website remediation does **not** cover it.
    - **Dependency — this must complete before the historical PAN purge in item 16.** The token migration is the gate: only once `gt_auto_process` bills by token (no longer reading full PAN) can the stored PAN columns be safely purged. New plaintext PANs may keep accumulating until this lands.
18. **Encrypt or securely destroy the CHD-laden database backups** per the retention policy as part of the data purge — every existing backup of the affected DBs contains plaintext PAN + CVV and is a perpetual liability until handled.
19. **Network segmentation:** restrict the SQL listener (`192.168.8.62,3436`) via host firewall / ACL to **only the web server IP(s)** and required internal hosts; the network currently appears **flat**. This is part of the C0/H6 segmentation fix.
20. Separate development from the production host; deploy **precompiled**; remove dev tooling and source from prod (H1).
21. Enable **TDE** at rest; re-scope / re-confirm the merchant **PCI SAQ** with the acquirer/QSA after remediation. (The website now clearly hosts in-process card-on-file logic and is a multi-backend credential hub — the SAQ type must reflect that.)
22. **Decouple the GTIware card engine from the public host (C0-Extended).** Run `gt_auto_process_2020` / `glaztech_utilities_2020` off the internet-facing IIS process — on a non-CDE host, with its own dedicated **low-privilege** DB login (no `sysadmin`, explicit `DENY` on other offices, `msdb`, and the linked servers). You cannot safely swap only the website's connection string while the engine shares the same process and config.
23. **Audit and minimize the 7 linked servers.** Remove any not strictly required; restrict `is_remote_login_enabled` / data access and apply least privilege on the remote ends. Extend the segmentation in item 19 to the backup shares (`192.168.8.52` / `.212`), MAS (`192.168.0.55`), and TimeForce so the CDE is not flatly trusted across subnets.
24. **Move the CyberSource + PNC gateway secrets out of `Web.config` `appSettings`** into proper secret management (or encrypted config / DPAPI) and **rotate** them as part of the change.
25. **Lock down `/webhooks` and `/webhooks1` (Samsara).** Confirm authentication/authorization, restrict source IPs where possible, verify they allow no unauthenticated DB or card operations, and remove the `webhooks_test` app from production.
26. **Recon the second host `\\192.168.0.147\web\glaztech_4`** (surfaced by a cleanup job) — same code/secrets? also public-facing? same GTIware DLLs? — and bring it into scope.

### Testing & rollback gates (applies to all of the above)
- **There may be NO staging environment.** Finding **H1** (dev tooling and source on the live box) strongly implies development happens on production. **Assume there is no separate test environment** until confirmed, and plan accordingly.
- **Require a validation + rollback plan** for each higher-risk change: the **DB-login change** (revert the `Web.config` connection string), the **`quo()` removal / parameterization** (per-page validation that queries still return correct results), the **error-path changes** (confirm `customErrors`/encoding don't break legitimate error flows), and the **TLS 1.0/1.1 disablement** (re-enable if a legacy client breaks).
- **Do NOT make in-place hotfixes on prod.** Stand up at least a minimal validation environment (or a maintenance-window + immediate-rollback procedure) before applying code or DB-permission changes. In-place edits to a live payment site with no rollback are how these fixes cause an outage.

### Open follow-up tasks
- **`cc_file` "Invalid object name" anomaly (corp DB).** The Current-State check found `cc_file` returning "Invalid object name" in the `corp` DB though it existed there earlier the same day (per-office DBs unaffected). **Action:** confirm whether the object was dropped/renamed, moved, or is a permissions/visibility artifact — and whether any process is mid-migration. Track to closure; it changes the purge inventory if `corp` no longer holds that table.
- **IDOR verification on `get_cc_data(@acctno)`** (see Attack Path / item 14) — close this out as a concrete task.

---

## Appendix A — Intrusion / Brute-Force Log Review (2026-06-04)

**Question asked:** Is there evidence in the logs of anyone trying to brute-force the website logins (or the server)?
**Method (read-only, via GuruRMM):** 7 days of IIS request logs (`C:\inetpub\logs\LogFiles\W3SVC4`, ~52,000 requests, May 29 – Jun 4) plus the Windows Security event log (4625 failed logons / 4624 successful logons, 7-day window; log retains 65 days back to 2026-03-31). No data was modified.

**Bottom line: NO evidence of a brute-force attack — not against the website logins, and not against the Windows server.**

### Website logins (IIS)
Two login endpoints on the live site: `/customer_login.aspx` (customer portal) and `/emp/employee-login.aspx` (staff portal).

| Endpoint | Success | Fail | Reading |
|---|---|---|---|
| `/customer_login.aspx` | 2,547 (HTTP 302) | 78 (HTTP 200) | ~97% success — normal traffic across 740 distinct IPs, each logging in a few times/week |
| `/emp/employee-login.aspx` | 77 (302) | 381 (200) | **Artifact, not failures** — see below |

- **Important interpretation correction:** the employee login returns **HTTP 200 on BOTH success and failure** (it does not use the post-redirect-get/302 pattern the customer login uses). Status code alone therefore does **not** indicate a failed staff login. The apparent "381 failures" is a measurement artifact. Confirmed by pulling full request timelines: the top "suspect" IP `160.3.157.9` (24 hits, "0 successes" by the 302 metric) is a single legitimate employee on an iPhone (consistent iOS 18.7 Safari UA all week) who POSTs the login and then loads `/emp/check-hours2.aspx` + `/emp/index.aspx` with real content — i.e. a logged-in employee checking a timecard. Every "failure-heavy" employee IP fits the same benign shape (residential IP, single consistent device UA, reaches protected pages).
- **No brute-force / credential-stuffing signature** anywhere: no single IP firing rapid high-volume login POSTs, no username-cycling at machine speed, no scanner/bot user-agents hammering the login.
- **Minor observation (not an attack):** scattered bursts of **HTTP 500** on post-login pages (`invoices.aspx`, `quotes.aspx`, `place_orders.aspx`, `billing-statements.aspx`, `online-payment-pnc.aspx`) — several IPs firing 8–17 identical 500s within a single second (automated, not human clicking). Source IPs incl. `201.146.179.166`, `64.178.182.162`, `205.185.107.49`, `172.87.137.60`. These read more like a buggy page than a campaign (different pages, different residential IPs, normal browser UAs), **but** given C3 (the SQL-injectable `quo()` path), a 500 on a login/data page is exactly what a quote character in input produces against that code — worth an app-side look at what those pages throw. IIS does not log POST bodies, so the attempted inputs are not recoverable from these logs.

### Windows server (Security log)
- **13** failed logons (4625) in 7 days — trivial volume (an internet-exposed Windows host sees thousands/day).
- **All 13 were LogonType 3 (SMB/network), 100% from internal LAN IPs, zero from any public IP.** No RDP (type 10) failures at all.
- Targeted usernames: 12 blank (null/anonymous SMB) + 1 `tomabens` (internal — a stale cached credential or service, not an attack).
- **No successful remote logons from any external IP** (4624 type 3/8/10).
- **Inference:** the absence of *any* external 4625 strongly indicates RDP/SMB are **not internet-exposed** (a reachable RDP port produces a relentless type-10 failure stream within hours). The only meaningful internet-facing attack surface is the web application on 443 — which is exactly where the risk in this report lives.

### Takeaway for remediation
The review found no attacker, but it confirmed the **detection gap** noted in H5: 200-on-both responses + no lockout + no failed-login logging mean a slow guessing attack would be invisible here. **Add failed-login logging and account lockout** (already on the roadmap) — without them, "has anyone tried to break in?" cannot be answered with confidence going forward. Re-run this same log review periodically.

*Read-only review. No card numbers, passwords, or POST bodies were retrieved or are reproduced.*

---

**Status:** Assessment complete 2026-06-03; intrusion/brute-force log review added 2026-06-04 (Appendix A); **deep host + SQL-infrastructure recon added 2026-06-04 (C0-Extended; findings C5/H9/H10; Emergency-containment roadmap bucket).** **No changes were made to the application, database, or data** (read-only throughout). Findings to be reviewed with the client (Steve Eastman / Tom) as priority security and PCI remediation. This report contains no card numbers or passwords; one incidentally-returned domain-admin password was redacted and is not reproduced.
