
Credentials hiding in plain sight: policy text and the auth surface the form does not guard
Most credential wins on an engagement are not cracked. They are read off a page that told you the password, or recovered by sending a credential the web form rejected to a different door on the same application. Both are one-minute checks that testers skip because the signal does not look like a vulnerability. Here they are, with the defensive fix for each.
The stated policy is the credential
An intranet, help-desk, onboarding, or IT-policy page whose text describes a procedure involving a password is configuration, not prose. Read it that way:
- "We will reset your password to be the same as your username" means
user:userfor every name you can find. Spray username equals password before touching any exploit. - "New accounts start with Welcome2026!" or "ChangeMe123" means that literal, sprayed across the user list.
- "Your temporary password is your employee ID / date of birth" means generate the format and spray it.
The user list comes from the same site: a /team page, mailto: links, author tags, ticket signatures, and crucially the images. A screenshot of a ticket or a chat window pasted into a support page routinely shows a real username, so fetch every image and read it, do not just parse the HTML.
# pair the Nth user with the Nth password (user == pass); --no-bruteforce avoids lockouts
netexec smb dc1.domain.local -u users.txt -p users.txt --no-bruteforce --continue-on-success
If that fails with an NTLM error, the credential is probably fine and NTLM is disabled: switch to Kerberos rather than retiring the password. One valid low-privilege login is usually enough to reach the rest of the domain (see the modern Active Directory attack surface).
The form is one auth surface, not the verdict
You hold a credential you have good reason to trust (from a config, an SNMP walk, a database dump, git history) and the web login form rejects it. The reflex is to call the password stale. That reflex is wrong often enough to be worth a minute, because authentication in most applications is not one implementation. An account can be disabled, locked, expired, or pending approval and still be a valid credential pair: the form checks the flag, and a REST or token endpoint on the same app frequently does not.
# the form says no...
curl -sk -X POST 'https://TARGET/login.php' -d 'username=svc&password=PASS'
# ...the API says yes
curl -sk -X POST 'https://TARGET/api/v1/authenticate' -d 'username=svc&password=PASS&valid_min=500'
# -> {"username":"svc","auth_token":"<hex>","valid_min":500}
Try the same pair against every surface before declaring the credential dead:
- API and token endpoints:
/api/v1/authenticate,/api/login,/rest/,/oauth/token,/graphql, Basic auth on the same path, and any mobile or legacy route (/m/,/v1/,/legacy/). - Non-web services: SSH, FTP, SMB, WinRM, an LDAP bind, the database. A web-app flag has no bearing on these at all.
Then convert the token into whatever the next step needs. A token is often accepted as a token= parameter or turned into a browser session by visiting an entry page with it, which is how you reach UI-only functionality with an account that cannot use the UI's own login. Ask for a long token lifetime when the option exists (valid_min=500): a short default will expire mid-way through a slow follow-on like a time-based SQLi dump, and that failure looks like "not injectable" rather than "logged out."
Record a rejected form as a delivery difference, never as a credential failure.
Defense
Both halves are the same root cause: the security decision lives in one place and the rest of the system does not consult it.
- Do not put passwords in documentation. A reset convention, a default starter password, or a derivable temporary-password format is a published credential. Reset to a random per-user value delivered out of band, and require a change on first use.
- Enforce authentication and account state centrally. Every surface (web form, API, token endpoint, mobile, legacy) must call the same identity provider and honor the same disabled/locked/expired flag. A separate auth implementation per endpoint is how "disabled" stops meaning disabled. Lockout and MFA belong at that shared layer, not per form.
- Disable means disable everywhere. When an account is disabled, its API tokens and service access must drop too, not just its ability to use the login page.
Detection
A successful authentication on an API or token endpoint for an account that is disabled, or that just failed at the web form, is a precise, high-value signal: alert on the state mismatch across surfaces. So is a burst of logins where the username equals the password, and a spray that follows a visit to a help or onboarding page. Correlate the same credential pair appearing across the form, the API, and SMB in a short window.
Use
Authorized and educational use only: your own systems, a lab you control, or an engagement with written permission. These are recon and credential-validation techniques for an environment you are authorized to assess, shown with the defensive fix so the asymmetry gets closed.