Brand impersonation: Sharepoint fake file share

This rule detects messages impersonating a Sharepoint file sharing email where no links point to known Microsoft domains.

Sublime rule (View on GitHub)

  1name: "Brand impersonation: Sharepoint fake file share"
  2description: |
  3    This rule detects messages impersonating a Sharepoint file sharing email where no links point to known Microsoft domains.
  4type: "rule"
  5severity: "medium"
  6source: |
  7  type.inbound
  8  
  9  // Sharepoint body content looks like this
 10  and (
 11    (
 12      (
 13        any([body.current_thread.text, body.plain.raw],
 14            strings.ilike(.,
 15                          "*shared a file with you*",
 16                          "*shared with you*",
 17                          "*invited you to access a file*",
 18                          "*received a document*",
 19                          "*shared a document*",
 20                          "*shared a new document*",
 21                          "*shared this document*"
 22            )
 23        )
 24        or any(ml.nlu_classifier(body.current_thread.text).topics,
 25               .name == "File Sharing and Cloud Services"
 26               and .confidence == "high"
 27        )
 28        //
 29        // This rule makes use of a beta feature and is subject to change without notice
 30        // using the beta feature in custom rules is not suggested until it has been formally released
 31        //
 32        or strings.ilike(beta.ocr(file.message_screenshot()).text,
 33                         "*shared a file with you*",
 34                         "*shared with you*",
 35                         "*invited you to access a file*",
 36                         "*received a document*",
 37                         "*shared a document*",
 38                         "*shared a new document*",
 39                         "*shared this document*"
 40        )
 41        //
 42        // This rule makes use of a beta feature and is subject to change without notice
 43        // using the beta feature in custom rules is not suggested until it has been formally released
 44        //
 45        or any(ml.nlu_classifier(beta.ocr(file.message_screenshot()).text).topics,
 46               .name == "File Sharing and Cloud Services"
 47               and .confidence == "high"
 48        )
 49      )
 50      and (
 51        strings.ilike(subject.subject,
 52                      "*shared*",
 53                      "*updated*",
 54                      "*sign*",
 55                      "*review*",
 56                      "*scanned*"
 57        )
 58        or strings.ilike(subject.subject,
 59                         "*Excel*",
 60                         "*SharePoint*",
 61                         "*PowerPoint*",
 62                         "*OneNote*"
 63        )
 64        or strings.ilike(sender.display_name,
 65                         "*Excel*",
 66                         "*SharePoint*",
 67                         "*PowerPoint*",
 68                         "*OneNote*"
 69        )
 70        or any(body.links, strings.icontains(.display_text, "OPEN DOCUMENT"))
 71        or subject.subject is null
 72        or subject.subject == ""
 73        // the org as determined by NLU is in the subject
 74        or any(ml.nlu_classifier(body.current_thread.text).entities,
 75               .name == "org" and strings.icontains(subject.subject, .text)
 76        )
 77      )
 78    )
 79    or any([
 80             "Contigo", // Spanish
 81             "Avec vous", // French
 82             "Mit Ihnen", // German
 83             "Con te", // Italian
 84             "Com você", // Portuguese
 85             "Met u", // Dutch
 86             "С вами", // Russian
 87             "与你", // Chinese (Simplified)
 88             "與您", // Chinese (Traditional)
 89             "あなたと", // Japanese
 90             "당신과", // Korean
 91             "معك", // Arabic
 92             "آپ کے ساتھ", // Urdu
 93             "আপনার সাথে", // Bengali
 94             "आपके साथ", // Hindi
 95             "Sizinle", // Turkish // Azerbaijani
 96             "Med dig", // Swedish
 97             "Z tobą", // Polish
 98             "З вами", // Ukrainian
 99             "Önnel", // Hungarian
100             "Μαζί σας", // Greek
101             "איתך", // Hebrew
102             "กับคุณ", // Thai
103             "Với bạn", // Vietnamese
104             "Dengan Anda", // Indonesian // Malay
105             "Nawe", // Swahili
106             "Cu dumneavoastră", // Romanian
107             "S vámi", // Czech
108             "Med deg", // Norwegian
109             "S vami", // Slovak
110             "Med dig", // Danish
111             "Amb vostè", // Catalan
112             "Teiega", // Estonian
113             "S vama", // Serbian
114           ],
115           strings.icontains(subject.subject, .)
116    )
117  )
118  
119  // contains logic that impersonates Microsoft
120  and (
121    any(ml.logo_detect(file.message_screenshot()).brands,
122        strings.starts_with(.name, "Microsoft")
123    )
124    or any(attachments,
125           .file_type in $file_types_images
126           and any(ml.logo_detect(.).brands,
127                   strings.starts_with(.name, "Microsoft")
128           )
129    )
130    or regex.icontains(body.html.raw,
131                       '<table[^>]*>\s*<tbody[^>]*>\s*<tr[^>]*>\s*(<td[^>]*bgcolor="#[0-9A-Fa-f]{6}"[^>]*>\s*&nbsp;\s*</td>\s*){2}\s*</tr>\s*<tr[^>]*>\s*(<td[^>]*bgcolor="#[0-9A-Fa-f]{6}"[^>]*>\s*&nbsp;\s*</td>\s*){2}'
132    )
133    or 3 of (
134      regex.icontains(body.html.raw, '.password-expiration'),
135      regex.icontains(body.html.raw, 'color: #2672ec;'),
136      regex.icontains(body.html.raw, 'M­ic­ro­so­ft')
137    )
138    or 4 of (
139      regex.icontains(body.html.raw, 'rgb\(246,\s?93,\s?53\)'),
140      regex.icontains(body.html.raw, 'rgb\(129,\s?187,\s?5\)'),
141      regex.icontains(body.html.raw, 'rgb\(4,\s?165,\s?240\)'),
142      regex.icontains(body.html.raw, 'rgb\(255,\s?186,\s?7\)'),
143    )
144    or 4 of (
145      regex.icontains(body.html.raw,
146                      '(background-color:|background:|bgcolor=)(.)red'
147      ),
148      regex.icontains(body.html.raw, 'rgb\(19,\s?186,\s?132\)'),
149      regex.icontains(body.html.raw, 'rgb\(4,\s?166,\s?240\)'),
150      regex.icontains(body.html.raw, 'rgb\(255,\s?186,\s?8\)'),
151    )
152    or 4 of (
153      regex.icontains(body.html.raw, 'rgb\(245,\s?189,\s?67\)'),
154      regex.icontains(body.html.raw, 'rgb\(137,\s?184,\s?57\)'),
155      regex.icontains(body.html.raw, 'rgb\(217,\s?83,\s?51\)'),
156      regex.icontains(body.html.raw, 'rgb\(71,\s?160,\s?218\)')
157    )
158    or 4 of (
159      regex.icontains(body.html.raw, 'rgb\(73,\s?161,\s?232\)'),
160      regex.icontains(body.html.raw, 'rgb\(224,\s?92,\s?53\)'),
161      regex.icontains(body.html.raw, 'rgb\(139,\s?183,\s?55\)'),
162      regex.icontains(body.html.raw, 'rgb\(244,\s?188,\s?65\)')
163    )
164    or 4 of (
165      regex.icontains(body.html.raw, 'rgb\(213,\s?56,\s?62\)'),
166      regex.icontains(body.html.raw, 'rgb\(0,\s?114,\s?30\)'),
167      regex.icontains(body.html.raw, 'rgb\(0,\s?110,\s?173\)'),
168      regex.icontains(body.html.raw, 'rgb\(227,\s?209,\s?43\)'),
169    )
170    or 4 of (
171      regex.icontains(body.html.raw, 'rgb\(246,\s?93,\s?53\)'),
172      regex.icontains(body.html.raw, 'rgb\(129,\s?187,\s?5\)'),
173      regex.icontains(body.html.raw, 'rgb\(4,\s?165,\s?240\)'),
174      regex.icontains(body.html.raw, 'rgb\(255,\s?186,\s?7\)')
175    )
176    or 4 of (
177      regex.icontains(body.html.raw, 'rgb\(242,\s?80,\s?34\)'),
178      regex.icontains(body.html.raw, 'rgb\(127,\s?186,\s?0\)'),
179      regex.icontains(body.html.raw, 'rgb\(0,\s?164,\s?239\)'),
180      regex.icontains(body.html.raw, 'rgb\(255,\s?185,\s?0\)'),
181    )
182    or 4 of (
183      regex.icontains(body.html.raw, 'rgb\(243,\s?83,\s?37\)'),
184      regex.icontains(body.html.raw, 'rgb\(129,\s?188,\s?6\)'),
185      regex.icontains(body.html.raw, 'rgb\(5,\s?166,\s?240\)'),
186      regex.icontains(body.html.raw, 'rgb\(255,\s?186,\s?8\)')
187    )
188    or 4 of (
189      regex.icontains(body.html.raw, 'rgb\(243,\s?80,\s?34\)'),
190      regex.icontains(body.html.raw, 'rgb\(128,\s?187,\s?3\)'),
191      regex.icontains(body.html.raw, 'rgb\(3,\s?165,\s?240\)'),
192      regex.icontains(body.html.raw, 'rgb\(255,\s?185,\s?3\)')
193    )
194    or 4 of (
195      regex.icontains(body.html.raw,
196                      '(background-color:|background:|bgcolor=)(.)?(#)?(FF1940|eb5024|F25022|FF1941|red)'
197      ),
198      regex.icontains(body.html.raw,
199                      '(background-color:|background:|bgcolor=)(.)?(#)?(36ba57|3eb55d|7db606|7FBA00|36ba58|green)'
200      ),
201      regex.icontains(body.html.raw,
202                      '(background-color:|background:|bgcolor=)(.)?#(04a1d6|04B5F0|05a1e8|00A4EF|01a4ef|04a5f0)'
203      ),
204      regex.icontains(body.html.raw,
205                      '(background-color:|background:|bgcolor=)(.)?#(FFCA07|f7b408|FFB900|FFCA08|ffb901|ffba07)'
206      ),
207    )
208    or 4 of (
209      regex.icontains(body.html.raw,
210                      '(background-color:|background:|bgcolor=)(.)?#(f65314|f65d35|49a1e8|E74F23|F35325)'
211      ),
212      regex.icontains(body.html.raw,
213                      '(background-color:|background:|bgcolor=)(.)?#(7cbf42|81bb05|e05c35|7AB206|81BC06)'
214      ),
215      regex.icontains(body.html.raw,
216                      '(background-color:|background:|bgcolor=)(.)?#(00a4ef|0078d7|8bb737|04a5f0|059EE4|05A6F0)'
217      ),
218      regex.icontains(body.html.raw,
219                      '(background-color:|background:|bgcolor=)(.)?#(ffb900|ffba07|f4bc41|F2B108|FFBA08)'
220      ),
221    )
222    // fuzzy approach
223    or 4 of (
224      regex.icontains(body.html.raw,
225                      'rgb\((2[1-4][0-9]|250),\s?(7[0-9]|8[0-9]|9[0-3]),\s?(3[0-9]|4[0-9]|5[0-3])\)'
226      ),
227      regex.icontains(body.html.raw,
228                      'rgb\((12[0-9]|13[0-9]),\s?(18[0-9]|190),\s?([0-9]|10)\)'
229      ),
230      regex.icontains(body.html.raw,
231                      'rgb\(([0-9]|1[0-5]),\s?(16[0-5]|166),\s?(23[0-9]|240)\)'
232      ),
233      regex.icontains(body.html.raw,
234                      'rgb\((25[0-5]),\s?(18[5-9]|19[0-9]),\s?([0-9]|10)\)'
235      )
236    )
237    or 4 of (
238      regex.icontains(body.html.raw, 'rgb\((25[0-5]),\s?(2[0-5]),\s?(6[0-4])\)'),
239      regex.icontains(body.html.raw, 'rgb\((6[0-2]),\s?(18[0-1]),\s?(9[0-3])\)'),
240      regex.icontains(body.html.raw, 'rgb\(([0-4]),\s?(18[0-1]),\s?(24[0])\)'),
241      regex.icontains(body.html.raw, 'rgb\((25[0-5]),\s?(20[0-2]),\s?([0-7])\)')
242    )
243    or (
244      any(recipients.to,
245          strings.icontains(body.current_thread.text,
246                            strings.concat(.email.domain.sld,
247                                           " shared a file with you"
248                            )
249          )
250      )
251    )
252    or (
253      any(recipients.to,
254          strings.icontains(body.current_thread.text,
255                            strings.concat("This link will work for ",
256                                           .email.email
257                            )
258          )
259      )
260    )
261    // contains HTML and wording from the sharepoint template
262    or (
263      (
264        // 
265        // This rule makes use of a beta feature and is subject to change without notice
266        // using the beta feature in custom rules is not suggested until it has been formally released
267        // 
268  
269        // alt text for the global icon
270        length(html.xpath(body.html, '//img[@alt="permission globe icon"]').nodes) > 0
271        // reference to the global icon id
272        or length(html.xpath(body.html, '//img[@id="Picture_x0020_1"]').nodes) > 0
273        // a comment reference the globe icon
274        or strings.icontains(body.html.raw,
275                             ' <!-- Permission globe icon placeholder -->'
276        )
277      )
278      // the wording from the sharepoint share
279      and strings.contains(body.current_thread.text,
280                           'This invite will only work for you and people with existing access'
281      )
282    )
283    or any(html.xpath(body.html,
284                      "//*[contains(translate(@style, 'ABCDEF', 'abcdef'), 'color:#605e5c')]"
285           ).nodes,
286           .display_text =~ "Privacy Statement"
287    )
288    or 2 of (
289      strings.icontains(body.current_thread.text,
290                        'Microsoft respects your privacy'
291      ),
292      strings.icontains(body.current_thread.text,
293                        'please read our Privacy Statement'
294      ),
295      strings.icontains(body.current_thread.text,
296                        'Microsoft Corporation, One Microsoft Way, Redmond, WA 98052'
297      ),
298    )
299  )
300  
301  // Negate messages when the message-id indciates the message is from MS actual. DKIM/SPF domains can be custom and therefore are unpredictable.
302  and not (
303    strings.starts_with(headers.message_id, '<Share-')
304    and strings.ends_with(headers.message_id, '@odspnotify>')
305  )
306  
307  // fake Sharepoint shares are easy to identify if there are any links
308  // that don't point to microsoft[.]com or *.sharepoint[.]com
309  and not all(body.links,
310              .href_url.domain.root_domain in (
311                "1drv.ms",
312                "aka.ms",
313                "microsoft.com",
314                "sharepoint.com"
315              )
316  )
317  // if there is a Sharepoint link, ensure the link doesn't match any org SLDs
318  and not any(body.links,
319              (
320                .href_url.domain.root_domain == "sharepoint.com"
321                and any($org_slds, . == ..href_url.domain.subdomain)
322              )
323              or .href_url.domain.domain in $tenant_domains
324  )
325  and sender.email.domain.root_domain not in $org_domains
326  and sender.email.domain.root_domain not in (
327    "bing.com",
328    "microsoft.com",
329    "microsoftonline.com",
330    "microsoftsupport.com",
331    "microsoft365.com",
332    "office.com",
333    "onedrive.com",
334    "sharepointonline.com",
335    "yammer.com",
336    // ignore microsoft privacy statement links
337    "aka.ms"
338  )
339  and not (
340    (
341      (
342        strings.istarts_with(subject.subject, "RE:")
343        or strings.istarts_with(subject.subject, "R:")
344        or strings.istarts_with(subject.subject, "ODG:")
345        or strings.istarts_with(subject.subject, "答复:")
346        or strings.istarts_with(subject.subject, "AW:")
347        or strings.istarts_with(subject.subject, "TR:")
348        or strings.istarts_with(subject.subject, "FWD:")
349        or regex.imatch(subject.subject, '(\[[^\]]+\]\s?){0,3}(re|fwd?)\s?:')
350        or regex.imatch(subject.subject,
351                        '^\[?(EXT|EXTERNAL)\]?[: ]\s*(RE|FWD?|FW|AW|TR|ODG|答复):.*'
352        )
353      )
354      and (
355        (length(headers.references) > 0 or headers.in_reply_to is not null)
356        // ensure that there are actual threads
357        and (
358          length(body.previous_threads) > 0
359          or (length(body.html.display_text) - length(body.current_thread.text)) > 200
360        )
361      )
362    )
363  )
364  
365  // negate highly trusted sender domains unless they fail DMARC authentication
366  and (
367    (
368      sender.email.domain.root_domain in $high_trust_sender_root_domains
369      and not headers.auth_summary.dmarc.pass
370    )
371    or sender.email.domain.root_domain not in $high_trust_sender_root_domains
372  )
373  and (
374    profile.by_sender().solicited == false
375    or profile.by_sender_email().prevalence == "new"
376    or profile.by_sender_email().days_since.last_contact > 30
377    or (
378      profile.by_sender().any_messages_malicious_or_spam
379      and not profile.by_sender().any_messages_benign
380    )
381    // or it's a spoof of the org_domain
382    or (
383      sender.email.domain.domain in $org_domains
384      and not (
385        headers.auth_summary.spf.pass
386        or coalesce(headers.auth_summary.dmarc.pass, false)
387      )
388    )
389  )
390  and not profile.by_sender().any_messages_benign  
391
392attack_types:
393  - "Credential Phishing"
394  - "Malware/Ransomware"
395detection_methods:
396  - "Content analysis"
397  - "Header analysis"
398  - "URL analysis"
399  - "Computer Vision"
400tactics_and_techniques:
401  - "Impersonation: Brand"
402  - "Social engineering"
403id: "ff8b296b-aa0d-5df0-b4d2-0e599b688f6a"
to-top