Detection rules › Splunk

RDP Brute-force Detection (Windows Event Log)

Group by
_time, host
Source
github.com/anvilogic-forge/armory

Detects possible Brute force attempt over Windows Remote Desktop Protocol (RDP)

MITRE ATT&CK coverage

Event coverage

Rule body yaml

id: '5455.5610'
title: RDP Brute-force Detection
description: 'Detects possible Brute force attempt over Windows Remote Desktop Protocol
  (RDP) -- Software Association: BianLian, Conti, LockBit, SamSam, Snatch'
logic_format: Splunk
logic: ' `get_endpoint_data` `get_endpoint_data_winevent` AND ((TERM(EventCode=5156)
  OR "<EventID>5156<") AND Destination_Port="3389" AND Direction="Inbound" AND Protocol="6")
  OR ((TERM(EventCode=4625) OR "<EventID>4625<") AND (Failure_Reason="Unknown user
  name or bad password." OR FailureReason="%%2313" AND Logon_Type="3")) | rex field=Message
  (?<result>"(?i)An account failed to log on.")| rex field=Failure_Reason (?<result_reason>"(?i)Unknown
  user name or bad password.")| eval note=mvappend(result, result_reason)| table _time,
  host, user dest_port, note, process_name, process_path, signature_id, src_country,
  src_dns, src_ip | bin span=20s | stats values(*) as * by _time, host | where match(signature_id,"5156")
  AND match(signature_id,"4625")| where event_count > 3 | lookup dnslookup clientip
  as src_ip OUTPUT clienthost as src_dns | iplocation prefix="src_" src_ip | rename
  src_Country as src_country '
techniques:
- credential-access:brute force:password guessing
- initial-access:external remote services
technique_id:
- T1110.001
- T1133
data_category:
- Windows event logs
references: null

Stages and Predicates

Stage 1: search

`get_endpoint_data` `get_endpoint_data_winevent` AND ((TERM(EventCode=5156) OR "<EventID>5156<") AND Destination_Port="3389" AND Direction="Inbound" AND Protocol="6") OR ((TERM(EventCode=4625) OR "<EventID>4625<") AND (Failure_Reason="Unknown user name or bad password." OR FailureReason="%%2313" AND Logon_Type="3"))

Stage 2: rex

| rex field=Message (?<result>"(?i)An account failed to log on.")

Stage 3: rex

| rex field=Failure_Reason (?<result_reason>"(?i)Unknown user name or bad password.")

Stage 4: eval

| eval note=mvappend(result, result_reason)

Stage 5: table

| table _time, host, user dest_port, note, process_name, process_path, signature_id, src_country, src_dns, src_ip

Stage 6: bucket

| bin span=20s

Stage 7: stats

| stats values(*) as * by _time, host

Stage 8: where

| where match(signature_id,"5156") AND match(signature_id,"4625")

Stage 9: where

| where event_count > 3

Stage 10: lookup

| lookup dnslookup clientip as src_ip OUTPUT clienthost as src_dns
Lookup table
dnslookup
Key field
clientip as src_ip
Output columns
['clienthost', 'src_dns']

Stage 11: search

| iplocation prefix="src_" src_ip

Stage 12: rename

| rename src_Country as src_country

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
Destination_Porteq
  • "3389"
Directioneq
  • "Inbound"
EventCodeeq
  • 4625 corpus 15 (splunk 11, chronicle 2, kusto 2)
  • 5156 corpus 15 (splunk 13, kusto 2)
FailureReasoneq
  • "%%2313"
Failure_Reasoneq
  • "Unknown user name or bad password."
Logon_Typeeq
  • "3" corpus 40 (splunk 13, sigma 12, elastic 9, kusto 6)
Protocoleq
  • "6"
event_countgt
  • 3
prefixeq
  • "src_" corpus 5 (splunk 5)
signature_idmatch
  • "4625"
  • "5156"

Search terms

Bare-string tokens in the SPL search body. Splunk matches each token against _raw (the untyped raw event text) anywhere it appears, not against a specific field. These don't surface in the Indicators table because they aren't predicates on a known field.

StageTerm
1TERM
1"<EventID>5156<"
1TERM
1"<EventID>4625<"
11iplocation
11src_ip