>
av.Amit VijayanCYBERSECURITY & RESEARCHLet\u2019s connect
Back to the library

Threat Hunting Playbooks

Hunting Cloud Console Brute-Force: Failed Sign-Ins That Precede a Takeover

Thumbnail for: Hunting Cloud Console Brute-Force: Failed Sign-Ins That Precede a Takeover

A cloud console is the front door to everything an organization runs: virtual machines, databases, identity settings, billing, and the keys that unlock the rest. Attackers know this, and they test the door constantly. A brute-force attack against a cloud console is rarely subtle — it leaves a trail of failed sign-ins in Entra sign-in logs or AWS CloudTrail long before any guess succeeds. The hunt in this playbook is about reading that trail while it is still just noise, and acting before one attempt lands.

Why this matters now

Console credentials are among the most valuable an attacker can steal. A single successful console sign-in can lead to new access keys, privilege escalation, and persistence that survives a password reset if the attacker creates their own credentials first. Cloud consoles are also internet-facing by design, so they attract continuous automated guessing from botnets that rotate IPs and user agents. Most of those attempts fail. The danger is not the volume — it is the one attempt that succeeds against an account with weak or missing multi-factor authentication (MFA), and the minutes between that success and the attacker's first privileged action.

This hunt matters because the signal arrives before the damage. Failed sign-ins are logged by default in both Microsoft Entra and AWS, and the pattern of a brute-force attack — many failures, concentrated in time, from one source, against one account — stands out sharply from normal user behaviour. Catch the pattern and you can block the source, force a reset, and check the account before the attacker moves on to creating keys or changing policies.

The technique, step by step

In MITRE ATT&CK terms, this is T1110, Brute Force: T1110.001 Password Guessing (many attempts against one account), T1110.003 Password Spraying (few attempts across many accounts), and T1110.004 Credential Stuffing (previously breached username-password pairs tried at scale). A successful guess becomes T1078, Valid Accounts. Three variants matter, because they look different in the logs:

Password guessing: one account, many attempts

The attacker picks a high-value account — an admin, a service account, or a username harvested from a breach or a public directory — and tries password after password. In Entra sign-in logs this appears as repeated failures with error code 50126 (invalid username or password) for one user principal name, from one or a few source IPs, sometimes over hours. If the account locks, you will also see error code 50053, the smart-lockout signal. Guessing is the loudest variant and the easiest to threshold.

Password spraying: many accounts, few attempts each

Spraying spreads a small number of common passwords across many accounts to stay under lockout thresholds. No single account shows a dramatic failure count, which is why account-level alerting misses it. The tell is at the source level: one IP failing against dozens of accounts. This is the pattern covered in Hunting Password Spraying Against Entra ID with Smart-Lockout Telemetry, and it is worth running alongside this hunt.

Credential stuffing: breached pairs, tried once each

Stuffing replays real username-password pairs from other breaches. Each account may fail only once or twice, and the passwords are not guessable — they are correct for some other service. The tell is again at the source level, plus the occasional success: an account that fails once from an unfamiliar IP and then succeeds from that same IP, with no MFA challenge satisfied, deserves immediate attention.

In AWS, the anchor event is CloudTrail ConsoleLogin. Failed console sign-ins carry an error message of "Failed authentication" along with the source IP, the browser user agent, and whether MFA was used. A brute-force run shows up as a burst of failed ConsoleLogin events for one IAM user — or, more seriously, for the account root user — followed, in the worst case, by a successful ConsoleLogin from the same source. Root-user attempts deserve escalation on their own: the root user should almost never sign in through the console at all.

Why legitimate activity makes this hard: real users mistype passwords, password managers autofill stale credentials after a rotation, and single sign-on misconfigurations can generate failure bursts. A hunt that alerts on "any failed sign-in" will drown you. The discriminators are concentration (many failures, one source, short window), breadth (one account hammered, or many accounts from one source), and outcome (a success that follows failures from the same source).

Hunt 1: Entra brute-force — one account, many failures, one source

Start with the classic guessing signature in Entra sign-in logs. This query groups failures by account and source IP over the last day, and surfaces only accounts with a failure count no legitimate user produces:

// Hunt: Entra console brute-force — one account hammered from one source
let FailedCodes = dynamic([50126, 50053]); // bad password, smart-lockout lock
AADSignInEventsBeta
| where Timestamp > ago(24h)
| where ErrorCode in (FailedCodes)
| where ClientAppUsed == "Browser"
| where isnotempty(IPAddress) and isnotempty(AccountUpn)
| summarize FailedAttempts = count(),
    FirstSeen = min(Timestamp),
    LastSeen = max(Timestamp),
    UserAgents = make_set(UserAgent, 5)
    by AccountUpn, IPAddress
| where FailedAttempts >= 20
| project AccountUpn, IPAddress, FailedAttempts, FirstSeen, LastSeen, UserAgents
| order by FailedAttempts desc

Tune the threshold to your environment: 20 failures in a day is already far beyond normal typing, but in a large tenant you may want to start at 50 and work down. Any hit against an admin account, or any hit that includes 50053 lockouts, goes to the top of the queue.

Hunt 2: The failure-to-success pattern — the attempt that landed

This is the most important query in the playbook. A source that fails repeatedly and then succeeds is either a user who finally remembered their password — or an attacker who finally guessed it. The difference is in the context: unfamiliar IP, new country, odd hour, no MFA, or a user agent that does not match the user's normal browser.

// Hunt: Entra source that failed repeatedly, then succeeded
let BadSources = AADSignInEventsBeta
| where Timestamp > ago(24h)
| where ErrorCode in (50126, 50053)
| where ClientAppUsed == "Browser"
| where isnotempty(IPAddress)
| summarize FailedAttempts = count(), Accounts = dcount(AccountUpn) by IPAddress
| where FailedAttempts >= 20
| project IPAddress;
AADSignInEventsBeta
| where Timestamp > ago(24h)
| where IPAddress in (BadSources)
| where ErrorCode == 0
| project Timestamp, AccountUpn, IPAddress, Country, UserAgent, Application
| order by Timestamp desc

Every row this returns is a potential account takeover in progress. Check whether the success used MFA, whether the location and device match the user's history, and what the account did next — role changes, app registrations, and mailbox rule changes are the classic next steps, and the follow-on for a compromised privileged account is covered in Hunting Hijacked Vendor Service Principals.

Hunt 3: AWS CloudTrail — failed console sign-ins by source

The same logic applies to AWS. This query works on CloudTrail events ingested into Sentinel as the AWSCloudTrail table, and groups failed console sign-ins by user and source IP:

// Hunt: AWS console brute-force — failed ConsoleLogin bursts
AWSCloudTrail
| where TimeGenerated > ago(24h)
| where EventName == "ConsoleLogin"
| where ErrorMessage has "Failed authentication"
| extend UserName = tostring(parse_json(UserIdentity).userName),
    AccountId = tostring(parse_json(UserIdentity).accountId)
| where isnotempty(SourceIpAddress)
| summarize FailedAttempts = count(),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated),
    UserAgents = make_set(UserAgent, 5)
    by UserName, AccountId, SourceIpAddress
| where FailedAttempts >= 10
| project UserName, AccountId, SourceIpAddress, FailedAttempts, FirstSeen, LastSeen, UserAgents
| order by FailedAttempts desc

Then run the failure-to-success check: any source IP in the results above that also produced a successful ConsoleLogin for the same user within the window is an incident, not a hunt lead. Pay special attention to rows where the user name is "root" — console sign-ins as root should be rare enough that any failed burst against root is worth a ticket on its own.

Hunt 4: The same behaviour in Splunk (SPL)

For Splunk environments, the Entra and AWS versions of the same hunts look like this. Adapt the index and sourcetype names to your ingestion; the logic — failures grouped by account and source, then failures followed by success — is what matters:

// Splunk: Entra brute-force — one account, many failures, one source
index=azure sourcetype="ms:aad:signin" resultType=50126 OR resultType=50053
| stats count as FailedAttempts, earliest(_time) as FirstSeen, latest(_time) as LastSeen, values(userAgent) as UserAgents by userPrincipalName, src_ip
| where FailedAttempts >= 20
| sort - FailedAttempts
// Splunk: AWS failed console sign-ins by user and source
index=aws sourcetype=aws:cloudtrail eventName=ConsoleLogin errorMessage="Failed authentication"
| stats count as FailedAttempts, earliest(_time) as FirstSeen, latest(_time) as LastSeen, values(userAgent) as UserAgents by userIdentity.userName, src_ip
| where FailedAttempts >= 10
| sort - FailedAttempts

For the failure-to-success check in Splunk, take the source IPs from the first search and run a second search for successful sign-ins from those IPs in the same window, then join on the account. Any success that follows a failure burst from the same source is the row to triage first.

Triage: five questions before you call it

Not every failure burst is an attack. Work through these before escalating:

  1. Did any attempt succeed? A pure failure burst is a blocked attack; a failure burst followed by success from the same source is a takeover until proven otherwise.
  2. Does the source match the user? Known corporate IP, familiar country, normal working hours, and the user's usual browser point to a legitimate user. A new country, a VPN exit, or a scripted user agent points the other way.
  3. Was MFA satisfied? A success with phishing-resistant MFA is a very different event from a success with no MFA challenge. If MFA was bypassed or absent, treat the account as compromised.
  4. What did the account do next? In AWS, look for CreateAccessKey, AttachUserPolicy, or CreateUser after the success. In Entra, look for role assignments, application registrations, and authentication-method changes. Post-login actions are what turn a guessed password into persistence.
  5. Is there a benign explanation? A password rotation, a broken SSO configuration, or a script with a stale credential can all generate bursts. Check with the account owner and the change log — but do not let a plausible explanation close the case before you have checked for a success.

Sign-in anomalies rarely arrive alone. If the account in question is federated, the same log-hunting approach extends to federation sign-ins, as covered in Hunting Golden SAML: Forged Federation Tokens in ADFS Sign-In Logs.

Remediation and hardening

If the triage points to a takeover: reset the account password, revoke all active sessions and tokens, and remove any keys, policies, or authentication methods created after the first suspicious sign-in. In AWS, deactivate access keys created during the incident window and review CloudTrail for actions taken by the account, not just the sign-in itself. In Entra, revoke sessions, require re-registration of MFA, and review audit logs for role and application changes.

To make the next brute-force run fail quietly: enforce MFA on every console sign-in, and prefer phishing-resistant MFA for privileged accounts. Use Conditional Access to block sign-ins from unfamiliar locations for admin accounts, and to require compliant devices for console access. Disable AWS root-user console access in practice by locking the root credentials away and alerting on any root sign-in attempt, successful or not. Set a standing alert on the failure-to-success pattern itself — a threshold alert on failures alone will either fire constantly or be tuned so high it misses the attack; the failure-then-success sequence is the alert worth paging on. Finally, keep the failure data: brute-force sources return, and an IP or user-agent fingerprint from today's blocked run is tomorrow's first indicator when the same actor tries a different account.

Keep exploring.

All articles