Files
claudetools/clients/glaztech/reports/2026-06-03-website-security-assessment.md
Mike Swanson 64b2d9e668 sync: auto-sync from GURU-5070 at 2026-06-04 07:07:43
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-04 07:07:43
2026-06-04 07:07:48 -07:00

26 KiB
Raw Blame History

Glaz-Tech Industries — Website Security Assessment

Classification: CONFIDENTIAL — Security Date: 2026-06-03 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 Method: Authorized read-only assessment via GuruRMM (config/registry inspection, source-code review of the on-server VB.NET source, and read-only/aggregate DB inspection). No cardholder data and no passwords were retrieved — sensitive columns were classified by aggregate only.

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


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 with extensive remote-access and end-of-life software. Multiple findings are independently sufficient to cause a reportable breach.

# 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
H1 Production payment server is also a dev workstation (VS, SDKs, build tools) High
H2 Remote-access sprawl incl. end-of-life RealVNC 4.2.8 + stale ScreenConnect v6 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
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"):

  • 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 todayweb_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 daycc_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 rolenot 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: enable xp_cmdshell to run OS commands on the SQL server (192.168.8.62) as the SQL service account — install malware/ransomware, create OS accounts, exfiltrate files, disable backups and AV; harvest credentials and pivot into the wider GTI/Glaztech network (linked servers, shares, the service account's domain rights); plant persistent SQL backdoors and erase SQL audit trails.

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 immediately. 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). Revoke sysadmin/securityadmin/dbcreator.
  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.
  4. Then: fix the SQL injection (parameterize, remove quo()), add login lockout/rate-limiting, hash passwords.

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 319.
  • 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 reset-token flow; force a global password reset after remediation.

C3 — SQL injection via non-escaping quo() helper

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.

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 LowModerate (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. The website has zero card-handling code (no cc_file/save_cc_data/gt_auto_process references in its .aspx/.vb); its quick-pay pages even disclaim saving cards. 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 disabledSY_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 — Remote-access sprawl, including end-of-life software

Present: RealVNC Enterprise E4.2.8 (≈2009 — critically outdated, known auth-bypass-class issues), ScreenConnect client v6.0.11622 (2018, stale) alongside a current ScreenConnect, Splashtop, Datto RMM + Datto EDR, Syncro, plus GuruRMM. 6+ remote-management agents = large unmonitored access surface. Fix: remove RealVNC and the stale ScreenConnect immediately; rationalize to a single sanctioned remote-access tool; inventory who controls each.

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.


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; tokenization columns exist in the schema but are unused). The plaintext exposure is entirely GTIware's card-on-file feature (the internal PSA, staff-operated, storing into databases the website shares) — not Sage, and not the website's payment pages (which store nothing). The website's role is the access path (C0), not the storer.
  • TLS 1.2 to CyberSource now works (payment outage fixed 2026-06-03).

Prioritized Remediation Roadmap

Now (days):

  1. Purge stored CVV (cc_file.cc_code); stop writing it.
  2. debug="false", customErrors="On"; HTML-encode gt_errorpage output; stop echoing exceptions.
  3. Remove RealVNC 4.2.8 and the stale ScreenConnect v6 client.
  4. Disable TLS 1.0/1.1 on the listener.

Short term (weeks): 5. Convert passwords to salted hashes; replace the email-the-password flow with reset tokens; force a global reset. 6. Parameterize the concatenated SQL (payment pages first); delete quo(). 7. Secure+HttpOnly cookies; session regeneration; login throttling/lockout. 8. Move card-on-file to the CyberSource token vault; purge/encrypt historical PAN columns.

Structural (project): 9. Separate development from the production host; deploy precompiled; remove dev tooling and source from prod. 10. Least-privilege DB accounts, secret management for connection strings, TDE; re-scope the merchant PCI SAQ after remediation.


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 817 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). 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.