QR Code with suspicious indicators
This rule flags messages with QR codes in attachments when there are three or fewer attachments. If no attachments are present, the rule captures a screenshot of the message for analysis. Additional triggers include: sender's name containing the recipient's SLD, recipient's email mentioned in the body, an empty message body, a suspicious subject, or undisclosed recipients.
Sublime rule (View on GitHub)
1name: "QR Code with suspicious indicators"
2description: |
3 This rule flags messages with QR codes in attachments when there are three or fewer attachments. If no attachments are present, the rule captures a screenshot of the message for analysis. Additional triggers include: sender's name containing the recipient's SLD, recipient's email mentioned in the body, an empty message body, a suspicious subject, or undisclosed recipients.
4type: "rule"
5severity: "high"
6source: |
7 type.inbound
8 and (
9 (
10 length(attachments) <= 3
11 or (
12 any(attachments, length(ml.logo_detect(.).brands) > 0)
13 and length(attachments) <= 10
14 )
15 )
16 and (
17 any(attachments,
18 (
19 .file_type in $file_types_images
20 or .file_extension in $file_extensions_macros
21 or .file_type == "pdf"
22 )
23 and (
24 any(file.explode(.),
25 (
26 .scan.qr.type is not null
27 and regex.contains(.scan.qr.data, '\.')
28 and not strings.starts_with(.scan.qr.data,
29 "https://qr.skyqr.co.za/"
30 )
31 and not (
32 strings.icontains(.scan.qr.data, ',')
33 and .scan.qr.type == 'undefined'
34 )
35 // not a json string
36 and not (
37 strings.starts_with(.scan.qr.data, '{')
38 and strings.ends_with(.scan.qr.data, '}')
39 )
40 // exclude images taken with mobile cameras and screenshots from android
41 and not any(.scan.exiftool.fields,
42 .key == "Model"
43 or (
44 .key == "Software"
45 and strings.starts_with(.value, "Android")
46 )
47 )
48 // exclude images taken with mobile cameras and screenshots from Apple
49 and not any(.scan.exiftool.fields,
50 .key == "DeviceManufacturer"
51 and .value == "Apple Computer Inc."
52 )
53 // exclude images from WhatsApp (mobile)
54 and not regex.match(.file_name,
55 'WhatsApp Image \d\d\d\d-\d\d-\d\d at.*.jpe?g'
56 )
57 and not (
58 (
59 .scan.exiftool.image_height > 3000
60 and .scan.exiftool.image_height is not null
61 )
62 or (
63 .scan.exiftool.image_width > 3000
64 and .scan.exiftool.image_width is not null
65 )
66 )
67 // exclude contact cards
68 and not strings.istarts_with(.scan.qr.data, "BEGIN:VCARD")
69
70 // negate QR codes to legit Servicio de Administración Tributaria (SAT) Gov links
71 and not (
72 .scan.qr.url.domain.root_domain is not null
73 and .scan.qr.url.domain.root_domain in ('sat.gob.mx')
74 )
75 and not (
76 .scan.qr.data is not null
77 and strings.icontains(.scan.qr.data, 'sat.gob.mx')
78 )
79 )
80 or (
81 regex.icontains(.scan.ocr.raw, 'scan|camera')
82 and regex.icontains(.scan.ocr.raw, '\bQR\b|Q\.R\.|barcode')
83 )
84 )
85 )
86 )
87 or (
88 length(attachments) == 0
89 and any(file.explode(beta.message_screenshot()),
90 .scan.exiftool.image_height < 2000
91 and .scan.exiftool.image_width < 2000
92 and .scan.qr.type is not null
93 and regex.contains(.scan.qr.data, '\.')
94 // exclude contact cards
95 and not strings.istarts_with(.scan.qr.data, "BEGIN:VCARD")
96 )
97 )
98 )
99 and (
100 any(recipients.to,
101 strings.icontains(sender.display_name, .email.domain.sld)
102 )
103 or length(body.current_thread.text) is null
104 or (
105 body.current_thread.text == ""
106 and (
107 (
108 (length(headers.references) > 0 or headers.in_reply_to is null)
109 and not (
110 (
111 strings.istarts_with(subject.subject, "RE:")
112 or strings.istarts_with(subject.subject, "RES:")
113 or strings.istarts_with(subject.subject, "R:")
114 or strings.istarts_with(subject.subject, "ODG:")
115 or strings.istarts_with(subject.subject, "答复:")
116 or strings.istarts_with(subject.subject, "AW:")
117 or strings.istarts_with(subject.subject, "TR:")
118 or strings.istarts_with(subject.subject, "FWD:")
119 or regex.imatch(subject.subject,
120 '(\[[^\]]+\]\s?){0,3}(re|fwd?|automat.*)\s?:.*'
121 )
122 )
123 )
124 )
125 or length(headers.references) == 0
126 )
127 )
128 or regex.contains(subject.subject,
129 "(Authenticat(e|or|ion)|2fa|Multi.Factor|(qr|bar).code|action.require|alert|Att(n|ention):)"
130 )
131 or (any(recipients.to, strings.icontains(subject.subject, .display_name)))
132 or (
133 regex.icontains(subject.subject,
134 "termination.*notice",
135 "38417",
136 ":completed",
137 "[il1]{2}mit.*ma[il1]{2} ?bo?x",
138 "[il][il][il]egai[ -]",
139 "[li][li][li]ega[li] attempt",
140 "[ng]-?[io]n .*block",
141 "[ng]-?[io]n .*cancel",
142 "[ng]-?[io]n .*deactiv",
143 "[ng]-?[io]n .*disabl",
144 "action.*required",
145 "abandon.*package",
146 "about.your.account",
147 "acc(ou)?n?t (is )?on ho[li]d",
148 "acc(ou)?n?t.*terminat",
149 "acc(oun)?t.*[il1]{2}mitation",
150 "access.*limitation",
151 "account (will be )?block",
152 "account.*de-?activat",
153 "account.*locked",
154 "account.*re-verification",
155 "account.*security",
156 "account.*suspension",
157 "account.has.been",
158 "account.has.expired",
159 "account.will.be.blocked",
160 "account v[il]o[li]at",
161 "activity.*acc(oun)?t",
162 "almost.full",
163 "app[li]e.[il]d",
164 "authenticate.*account",
165 "been.*suspend",
166 "bonus",
167 "clos.*of.*account.*processed",
168 "confirm.your.account",
169 "courier.*able",
170 "crediential.*notif",
171 "deactivation.*in.*progress",
172 "delivery.*attempt.*failed",
173 "document.received",
174 "documented.*shared.*with.*you",
175 "dropbox.*document",
176 "e-?ma[il1]+ .{010}suspen",
177 "e-?ma[il1]{1} user",
178 "e-?ma[il1]{2} acc",
179 "e-?ma[il1]{2}.*up.?grade",
180 "e.?ma[il1]{2}.*server",
181 "e.?ma[il1]{2}.*suspend",
182 "email.update",
183 "faxed you",
184 "fraud(ulent)?.*charge",
185 "from.helpdesk",
186 "fu[il1]{2}.*ma[il1]+[ -]?box",
187 "has.been.*suspended",
188 "has.been.limited",
189 "have.locked",
190 "he[li]p ?desk upgrade",
191 "heipdesk",
192 "i[il]iega[il]",
193 "ii[il]ega[il]",
194 "incoming e?mail",
195 "incoming.*fax",
196 "lock.*security",
197 "ma[il1]{1}[ -]?box.*quo",
198 "ma[il1]{2}[ -]?box.*fu[il1]",
199 "ma[il1]{2}box.*[il1]{2}mit",
200 "ma[il1]{2}box stor",
201 "mail on.?hold",
202 "mail.*box.*migration",
203 "mail.*de-?activat",
204 "mail.update.required",
205 "mails.*pending",
206 "messages.*pending",
207 "missed.*shipping.*notification",
208 "missed.shipment.notification",
209 "must.update.your.account",
210 "new [sl][io]g?[nig][ -]?in from",
211 "new voice ?-?mail",
212 "notifications.*pending",
213 "office.*3.*6.*5.*suspend",
214 "office365",
215 "on google docs with you",
216 "online doc",
217 "password.*compromised",
218 "payment advice",
219 "periodic maintenance",
220 "potential(ly)? unauthorized",
221 "refund not approved",
222 "report",
223 "revised.*policy",
224 "scam",
225 "scanned.?invoice",
226 "secured?.update",
227 "security breach",
228 "securlty",
229 "seguranca",
230 "signed.*delivery",
231 "status of your .{314}? ?delivery",
232 "susp[il1]+c[il1]+ous.*act[il1]+v[il1]+ty",
233 "suspicious.*sign.*[io]n",
234 "suspicious.activit",
235 "temporar(il)?y deactivate",
236 "temporar[il1]{2}y disab[li]ed",
237 "temporarily.*lock",
238 "un-?usua[li].activity",
239 "unable.*deliver",
240 "unauthorized.*activit",
241 "unauthorized.device",
242 "undelivered message",
243 "unread.*doc",
244 "unusual.activity",
245 "upgrade.*account",
246 "upgrade.notice",
247 "urgent message",
248 "urgent.verification",
249 "v[il1]o[li1]at[il1]on security",
250 "va[il1]{1}date.*ma[il1]{2}[ -]?box",
251 "verification ?-?require",
252 "verification( )?-?need",
253 "verify.your?.account",
254 "web ?-?ma[il1]{2}",
255 "web[ -]?ma[il1]{2}",
256 "will.be.suspended",
257 "your (customer )?account .as",
258 "your.office.365",
259 "your.online.access"
260 )
261 or any($suspicious_subjects, strings.icontains(subject.subject, .))
262 or regex.icontains(sender.display_name,
263 "Accounts.?Payable",
264 "Admin",
265 "Administrator",
266 "Alert",
267 "Assistant",
268 "Billing",
269 "Benefits",
270 "Bonus",
271 "CEO",
272 "CFO",
273 "CIO",
274 "CTO",
275 "Chairman",
276 "Claim",
277 "Confirm",
278 "Critical",
279 "Customer Service",
280 "Deal",
281 "Discount",
282 "Director",
283 "Exclusive",
284 "Executive",
285 "Fax",
286 "Free",
287 "Gift",
288 "/bHR/b",
289 "Helpdesk",
290 "Human Resources",
291 "Immediate",
292 "Important",
293 "Info",
294 "Information",
295 "Invoice",
296 '\bIT\b',
297 "Legal",
298 "Lottery",
299 "Management",
300 "Manager",
301 "Member Services",
302 "Notification",
303 "Offer",
304 "Operations",
305 "Order",
306 "Partner",
307 "Payment",
308 "Payroll",
309 "President",
310 "Premium",
311 "Prize",
312 "Receipt",
313 "Refund",
314 "Registrar",
315 "Required",
316 "Reward",
317 "Sales",
318 "Secretary",
319 "Security",
320 "Service",
321 "Signature",
322 "Storage",
323 "Support",
324 "Sweepstakes",
325 "System",
326 "Tax",
327 "Tech Support",
328 "Update",
329 "Upgrade",
330 "Urgent",
331 "Validate",
332 "Verify",
333 "VIP",
334 "Webmaster",
335 "Winner",
336 )
337 )
338 or (
339 (
340 length(recipients.to) == 0
341 or all(recipients.to, .display_name == "Undisclosed recipients")
342 )
343 and length(recipients.cc) == 0
344 and length(recipients.bcc) == 0
345 )
346 or any(file.explode(beta.message_screenshot()),
347 (
348 .scan.qr.url.domain.tld in $suspicious_tlds
349 and .scan.qr.url.domain.root_domain != "app.link"
350 )
351 or
352 // linkanalysis phishing disposition
353 any([ml.link_analysis(.scan.qr.url)],
354 .credphish.disposition == "phishing"
355 )
356 )
357 or any(attachments,
358 (
359 .file_type in $file_types_images
360 or .file_extension in $file_extensions_macros
361 or .file_type == "pdf"
362 )
363 and any(file.explode(.),
364 (
365 .scan.qr.url.domain.tld in $suspicious_tlds
366 and .scan.qr.url.domain.root_domain != "app.link"
367 and .scan.qr.url.domain.root_domain != "qr.link"
368 and .scan.qr.url.domain.root_domain != "skyqr.co.za"
369 )
370 and .scan.qr.url.domain.root_domain not in $org_domains
371 )
372 )
373 or sender.email.domain.tld in $suspicious_tlds
374 )
375 )
376
377 // sender profile is new or outlier
378 and (
379 profile.by_sender_email().any_messages_malicious_or_spam
380 or (
381 sender.email.domain.domain in $org_domains
382 and not coalesce(headers.auth_summary.dmarc.pass, false)
383 )
384 or (
385 profile.by_sender_email().prevalence in ("new", "outlier")
386 and not profile.by_sender_email().solicited
387 )
388 )
389 and not profile.by_sender_email().any_messages_benign
390 // negate highly trusted sender domains unless they fail DMARC authentication
391 and (
392 (
393 sender.email.domain.root_domain in $high_trust_sender_root_domains
394 and not headers.auth_summary.dmarc.pass
395 )
396 or sender.email.domain.root_domain not in $high_trust_sender_root_domains
397 )
398attack_types:
399 - "Credential Phishing"
400tactics_and_techniques:
401 - "QR code"
402 - "Social engineering"
403detection_methods:
404 - "Content analysis"
405 - "Header analysis"
406 - "Computer Vision"
407 - "Natural Language Understanding"
408 - "QR code analysis"
409 - "Sender analysis"
410 - "URL analysis"
411id: "04f5c34f-6518-512d-916c-4c2c2827c6a9"