Brand impersonation: Capital One

This detection rule identifies inbound messages containing Capital One branding indicators in display names, sender addresses, message content, or embedded logos, while excluding legitimate Capital One domains and authenticated communications from known trusted senders.

Sublime rule (View on GitHub)

  1name: "Brand impersonation: Capital One"
  2description: "This detection rule identifies inbound messages containing Capital One branding indicators in display names, sender addresses, message content, or embedded logos, while excluding legitimate Capital One domains and authenticated communications from known trusted senders."
  3type: "rule"
  4severity: "high"
  5source: |
  6  type.inbound
  7  and (
  8    any([
  9          strings.replace_confusables(sender.display_name),
 10          strings.replace_confusables(subject.subject),
 11          // domain parts of sender
 12          sender.email.local_part,
 13          sender.email.domain.sld
 14        ],
 15        // quick checks first
 16        strings.icontains(., 'Capital One')
 17        or strings.icontains(., 'CapitalOne')
 18  
 19        // slower checks next
 20        or regex.icontains(., 'Capital.?One')
 21        // levenshtein distince similar to captial one
 22        or strings.ilevenshtein(., 'Capital One') <= 2
 23    )
 24    or any(ml.logo_detect(beta.message_screenshot()).brands,
 25           .name == "Capital One Bank" and .confidence != "low"
 26    )
 27  )
 28  and not (
 29    sender.email.domain.root_domain in (
 30      "capitalone.co.uk",
 31      "capitalone.com",
 32      "capitaloneshopping.com",
 33      "capitalonesoftware.com",
 34      "capitalonebooking.com",
 35      "capitalonetravel.com",
 36      "olbanking.com", // a fiserv.one domain
 37      "bynder.com", // Digital Assest Mgmt
 38      "gcs-web.com", // investor relations run by capital one
 39      "capitalonearena.com", // the arena
 40      "monumentalsports.com", // the company that owns a bunch of teams that play at the arena?
 41      "ticketmaster.com", // sell and advertises tickets at Capital One Arena
 42      "credible.com" // known loan marketplace
 43    )
 44    and headers.auth_summary.dmarc.pass
 45  )
 46  // and the sender is not from high trust sender root domains
 47  and (
 48    (
 49      sender.email.domain.root_domain in $high_trust_sender_root_domains
 50      and not headers.auth_summary.dmarc.pass
 51    )
 52    or sender.email.domain.root_domain not in $high_trust_sender_root_domains
 53  )
 54  and // suspicious indicators here
 55   (
 56    // // password theme
 57    (
 58      strings.icontains(body.current_thread.text, "new password")
 59      or regex.icontains(body.current_thread.text,
 60                         '(?:credentials?|password)\s*(?:\w+\s+){0,3}\s*(?:compromise|reset|expir(?:ation|ed)|update|invalid|incorrect|changed|(?:mis)?match)',
 61                         '(?:compromise|reset|expir(?:ation|ed)|update|invalid|incorrect|changed|(?:mis)?match)\s*(?:\w+\s+){0,3}\s*(?:credentials?|password)',
 62                         '(?:short|weak|chang(?:e|ing)|reset)\s*(?:\w+\s+){0,3}\s*(?:credentials?|password)',
 63                         '(?:credentials?|password)\s*(?:\w+\s+){0,3}\s*(?:short|weak|chang(?:e|ing)|reset)',
 64      )
 65    )
 66    // // login failures
 67    or (
 68      strings.icontains(body.current_thread.text, "unusual number of")
 69      or strings.icontains(body.current_thread.text, "security breach")
 70      or (
 71        strings.icontains(body.current_thread.text, "security alert")
 72        // some capital one notiifcaitons include directions to 
 73        // change notificaiton preferences to only security alerts
 74        and (
 75          strings.icount(body.current_thread.text, "security alert") > strings.icount(body.current_thread.text,
 76                                                                                      "sign in to your account and select Security Alerts."
 77          )
 78        )
 79      )
 80      or strings.icontains(body.current_thread.text, "account remains secure")
 81      or strings.icontains(body.current_thread.text, "please verify your account")
 82      or strings.icontains(body.current_thread.text,
 83                           "suspicious activity detected"
 84      )
 85      or strings.icontains(body.current_thread.text, "temporarily locked out")
 86      or regex.icontains(body.current_thread.text,
 87                         '(?:invalid|unrecognized|unauthorized|fail(?:ed|ure)?|suspicious|unusual|attempt(?:ed)?\b|tried to)\s*(?:\w+\s+){0,3}\s*(?:log(?:.?in)?|sign(?:.?in)?|account|access|activity)',
 88                         '(?:log(?:.?in)?|sign(?:.?in)?|account|access|activity)\s*(?:\w+\s+){0,3}\s*(?:invalid|unrecognized|fail(?:ed|ure)?|suspicious|unusual|attempt(?:ed)?\b)'
 89      )
 90    )
 91    // // account locked
 92    or (
 93      strings.icontains(body.current_thread.text, "been suspend")
 94      or strings.icontains(body.current_thread.text, "will be restored")
 95      or strings.icontains(body.current_thread.text, "security reasons")
 96      or strings.icontains(body.current_thread.text,
 97                           "temporarily restricted access"
 98      )
 99      or regex.icontains(body.current_thread.text,
100                         'acc(?:ou)?n?t\s*(?:\w+\s+){0,3}\s*(?:authenticat(?:e|ion)|activity|\bho[li]d\b|terminat|[il1]{2}m[il1]t(?:s|ed|ation)|b?locked|de-?activat|suspen(?:ed|sion)|restrict(?:ed|ion)?|expir(?:ed?|ing)|v[il]o[li]at|verif(?:y|ication))',
101                         '(?:authenticat(?:e|ion)|activity|\bho[li]d\b|terminat|[il1]{2}m[il1]t(?:s|ed|ation)|b?locked|de-?activat|suspen(?:ed|sion)|restrict(?:ed|ion)?|expir(?:ed?|ing)|v[il]o[li]at|verif(?:y|ication))\s*(?:\w+\s+){0,3}\s*acc(?:ou)?n?t\b'
102      )
103    )
104    // // secure messages
105    or (
106      regex.icontains(body.current_thread.text,
107                      '(?:encrypt(?:ion|ed)?|secur(?:ed?|ity)) (?:\w+\s+){0,3}\s*message'
108      )
109      or strings.icontains(body.current_thread.text, "document portal")
110      or regex.icontains(body.current_thread.text,
111                         "has been (?:encrypt|sent secure)"
112      )
113      or regex.icontains(body.current_thread.text,
114                         'encryption (?:\w+\s+){0,3}\s*tech'
115      )
116    )
117    // // documents to view
118    or (
119      // we can skip the regex if the diplay_text doesn't contain document
120      // this might need to be removed if the regex is expanded
121      strings.icontains(body.current_thread.text, 'document')
122      and regex.icontains(body.current_thread.text,
123                          'document\s*(?:\w+\s+){0,3}\s*(?:ready|posted|review|available|online)',
124                          '(?:ready|posted|review|available|online)\s*(?:\w+\s+){0,3}\s*document'
125      )
126    )
127    // // account/profile details 
128    or (
129      strings.icontains(body.current_thread.text, "about your account")
130      or strings.icontains(body.current_thread.text, "action required")
131      or regex.icontains(body.current_thread.text,
132                         '(update|\bedit\b|modify|revise|verif(?:y|ication)|discrepanc(?:y|ies)|mismatch(?:es)?|inconsistenc(?:y|ies)?|difference(?:s)?|anomal(?:y|ies)?|irregularit(?:y|ies)?)\s*(?:\w+\s+){0,4}\s*(?:account|ownership|detail|record|data|info(?:rmation)?)',
133                         '(?:account|ownership|detail|record|data|info(?:rmation)?)\s*(?:\w+\s+){0,4}\s*(update|\bedit\b|modify|revise|verif(?:y|ication)|discrepanc(?:y|ies)|mismatch(?:es)?|inconsistenc(?:y|ies)?|difference(?:s)?|anomal(?:y|ies)?|irregularit(?:y|ies)?)'
134      )
135    )
136    // // other calls to action that are unexpected
137    or (strings.icontains(body.current_thread.text, "download the attachment"))
138  
139    // the links contain suspect wording
140    or (
141      0 < length(body.links) <= 50
142      and any(body.links,
143              (
144                regex.icontains(.display_text, '(?:log|sign).?in')
145                or strings.icontains(.display_text, 'confirm')
146                or strings.icontains(.display_text, 'i recongize it')
147                or strings.icontains(.display_text, "something\'s wrong")
148                or regex.icontains(.display_text,
149                                   '(?:(?:re)?view|see|read)\s*(?:\w+\s*){0,3}\s*(?:document|message|now|account)'
150                )
151                or regex.icontains(.display_text,
152                                   'restore\s*(?:\w+\s*){0,3}\s*(?:account|access)'
153                )
154                or regex.icontains(.display_text,
155                                   'review\s*(?:\w+\s*){0,3}\s*(?:payment)'
156                )
157              )
158              and not regex.icontains(.display_text,
159                                      'confirm\s*(?:\w+\s*){0,3}\s*this message'
160              )
161              and .href_url.domain.root_domain != "capitalone.com"
162      )
163    )
164    // the message contains a disclaimer but isn't from capitalone
165    or (
166      regex.icontains(body.current_thread.text,
167                      'To ensure delivery, add [^\@]+@[^\s]*capitalone.com to your address book.'
168      )
169      and sender.email.domain.root_domain != "capitalone.com"
170    )
171  )
172  // negation of inbound org domains which path eamil auth
173  and not (
174    type.inbound
175    and sender.email.domain.domain in $org_domains
176    and headers.auth_summary.spf.pass
177    and headers.auth_summary.dmarc.pass
178    and not 'fail' in~ distinct(map(headers.hops, .authentication_results.dkim))
179  )
180  and not any(beta.ml_topic(body.html.display_text).topics,
181              (
182                .name in (
183                  // lots of newsletters talk about capital one 
184                  "Newsletters and Digests",
185                  // lots of recruiting mention oppurtunties at capital one, often including the logo
186                  "Professional and Career Development",
187                )
188                and .confidence == "high"
189              )
190              or (
191                .name in (
192                  // Outage events are often news worthy
193                  "News and Current Events"
194                )
195                and .confidence != "low"
196              )
197  )
198  // negating legit replies/forwards
199  // https://github.com/sublime-security/sublime-rules/blob/main/insights/authentication/org_inbound_auth_pass.yml
200  and not (
201    (
202      strings.istarts_with(subject.subject, "RE:")
203      or strings.istarts_with(subject.subject, "FW:")
204      or strings.istarts_with(subject.subject, "FWD:")
205      or regex.imatch(subject.subject,
206                      '(\[[^\]]+\]\s?){0,3}(re|fwd?|automat.*)\s?:.*'
207      )
208      or strings.istarts_with(subject.subject, "Réponse automatique")
209    )
210    and (
211      length(headers.references) > 0
212      and any(headers.hops, any(.fields, strings.ilike(.name, "In-Reply-To")))
213    )
214  )
215  // negate bounce backs
216  and not (
217    strings.like(sender.email.local_part,
218                 "*postmaster*",
219                 "*mailer-daemon*",
220                 "*administrator*"
221    )
222    and any(attachments,
223            .content_type in (
224              "message/rfc822",
225              "message/delivery-status",
226              "text/calendar"
227            )
228    )
229  )  
230attack_types:
231  - "Credential Phishing"
232tactics_and_techniques:
233  - "Impersonation: Brand"
234  - "Lookalike domain"
235  - "Social engineering"
236detection_methods:
237  - "Computer Vision"
238  - "Sender analysis"
239  - "Header analysis"
240id: "d53848e4-fc40-5bd1-ad5e-c9c4e85a669f"
to-top