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