elastic / detection-rules

Home Page:https://www.elastic.co/guide/en/security/current/detection-engine-overview.html

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Meta] Prepare 20 Linux ES|QL Hunts

Aegrah opened this issue · comments

Meta Summary

The goal of this meta is to create ~20 Linux ES|QL hunts.

Estimated Time to Complete

1 sprint - 2 weeks

Tasklist

Meta Tasks

Resources / References

https://github.com/elastic/ia-trade-team/issues/302

Initial ideas pastebin:

  • Capitalized process executable execution
  • Unusual amount of data exfiltration (should check whether we can do something for UDP here as well)
  • Persistence via any of the common persistence methods;
    • Systemd (timer), init.d, rc.local, motd, cron, bashrc, LKM, LD_preload, etc.
  • Execution from suspicious location (/tmp, /dev/shm/, /var/run/, etc.)
  • Execution from different web roots or by different default web users (www-data, etc.)
  • Base64 executions (or other evasive executions)
  • Suspicious file downloads from an IP instead of a domain (local & public?)
  • Long-lasting connections from suspicious utilities (capable of creating rev shells) (https://github.com/elastic/ia-trade-team/issues/302#issuecomment-1960048584)
  • Unusual command proxying for defense evasion (identify unusual process parents for uncommon processes?) (https://github.com/elastic/ia-trade-team/issues/302)
  • Network Discovery via sensitive ports by unusual process (https://github.com/elastic/ia-trade-team/issues/302#issuecomment-1957216751)
  • Unusual SSH root connections by host
  • Unusual execution of GTFObins
  • Bufferoverflow attack based on X amount of segfaults --> check whether ES|QL can query message field correctly.
  • ...

Uncommon process execution from suspicious directory

FROM logs-endpoint.events.process-*
| WHERE @timestamp > NOW() - 7 day
| WHERE host.os.type == "linux" and event.type == "start" and event.action == "exec" and (
// Add paths to monitor from your environment here
  (process.executable like "/dev/shm/*") or
  (process.executable like "/var/www/*") or
  (process.executable like "/boot/*") or
  (process.executable like "/srv/*") or
  ((process.executable rlike "/tmp/[^/]+" or process.executable rlike "/var/tmp/[^/]+")) or
  (process.executable rlike "/run/[^/]+") or
  (process.executable rlike "/var/run/[^/]+")
) and not (
  // Exclude noisy (parent) processes, users or directories from your environment here
  (process.parent.executable in ("/usr/sbin/dpkg-preconfigure")) or
  // Exclude /tmp and /var/tmp instances starting or ending with digits (usually benign files)
  (process.executable rlike "/tmp/[0-9].*" or process.executable rlike "/tmp/.*[0-9]/?") or
  (process.executable rlike "/var/tmp/[0-9].*" or process.executable rlike "/var/tmp/.*[0-9]/?")
)
| STATS process_count = COUNT(process.executable), parent_process_count = COUNT(process.parent.executable), host_count = COUNT(host.name) by process.executable, process.parent.executable, host.name, user.id
// Alter this threshold to make sense for your environment 
| WHERE (process_count <= 3 or parent_process_count <= 3) and host_count <= 3
| SORT process_count asc
| LIMIT 100

image

Notes:

  • Excluded /tmp, /var/tmp, /run, /var/run subdirectories to exclude noise
  • Excluded /tmp, /var/tmp files starting or ending with digits to exclude real temporary files
  • Include a process or parent process count of <= 3, and a host count of <= 3 to eliminate common processes across different hosts.
  • Screenshot was taken from last 365 days of detonate data, only TP malware data is visible in the screenshot. Telemetry was analyzed as well, only 7 hits last 75 days; which are FPs that are easily tuned in customer environments, but should not be tuned for a general threat hunting query.

Hidden process execution

FROM logs-endpoint.events.process-*
| WHERE @timestamp > NOW() - 180 day
| WHERE host.os.type == "linux" and event.type == "start" and event.action == "exec" and (
  (process.executable rlike "/[^/]+/\\.[^/]+")
) 
| STATS process_count = COUNT(process.executable), parent_process_count = COUNT(process.parent.executable), host_count = COUNT(host.name) by process.executable, process.parent.executable, host.name, user.id
// Alter this threshold to make sense for your environment 
| WHERE (process_count <= 3 or parent_process_count <= 3) and host_count <= 3
| SORT process_count asc
| LIMIT 100

image

  • Included only hidden files, excluded hidden directories, as this is common in Unix
  • Include a process or parent process count of <= 3, and a host count of <= 3 to eliminate common processes across different hosts.
  • Only TPs in testing stack, 0 FPs in telemetry last 75 days and detonate last 365 days.

Potential defense evasion via multi-dot process execution

FROM logs-endpoint.events.process-*
| WHERE @timestamp > NOW() - 7 day
| WHERE host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.executable rlike """.*\.{3,}.*"""
| STATS process_count = COUNT(process.executable), host_count = COUNT(host.name) by process.executable
// Alter this threshold to make sense for your environment 
| WHERE process_count <= 10
| SORT process_count asc
| LIMIT 100

image

  • This one probably does not make sense for ES|QL hunt --> might be an interesting detection rule; probably not noisy and therefore doesn't specifically need a process_count to be valuable. Will leave it here for reference.

Defense evasion via capitalized process execution

FROM logs-endpoint.events.process-*
| WHERE @timestamp > NOW() - 10 day
| WHERE host.os.type == "linux" and event.type == "start" and event.action == "exec" and (
  (process.name rlike """[A-Z]{2,}[a-z]{1,}[0-9]{0,}""") or
  (process.name rlike """[A-Z]{1,}[0-9]{0,}""")
)
| STATS process_count = COUNT(process.name), host_count = COUNT(host.name) by process.name
// Alter this threshold to make sense for your environment 
| WHERE process_count <= 3 and host_count <= 3
| LIMIT 100

image

  • Detects processes that have 2 or more capital letters within its process name, and 0 or more digits. Using 2 or more capital letters will exclude most common processes starting with a capital letter
  • Will detect defense evasion techniques where binaries are capitalized to evade detections.
  • Many default payloads contain random capitalization, such as Metasploit payloads (as seen in the screenshot).
  • Include a process count of <= 3, and a host count of <= 3 to eliminate common processes across different hosts.

Unusual process command lines for web server user

FROM logs-endpoint.events.process-*
| WHERE @timestamp > NOW() - 30 day
| WHERE host.os.type == "linux" and event.type == "start" and user.name in ("www-data", "apache", "nginx", "httpd", "tomcat", "lighttpd", "glassfish", "weblogic")
| STATS process_cli_count = COUNT(process.command_line), host_count = COUNT(host.name) by process.command_line, process.name, user.name, host.name
| WHERE process_cli_count <= 3 and host_count <= 2
| SORT process_cli_count asc
| LIMIT 100
image
  • Detects process command executions through a commonly used web server user account.
  • This was never doable in EQL without creating noise, as many web servers execute commands by default, and without explicitly leveraging whitelisting or a huge blacklist for every single web server technology, we would either be overly or underly exclusive. Leveraging process command line counting in conjunction with host counting, we can easily minimize the number of FPs caused by common web server command executions, while still capturing true TPs.
  • Some FPs remain, however, these are easily mitigated on an on-environment basis.

Unusual file creations by web server user

FROM logs-endpoint.events.file-*
| WHERE @timestamp > NOW() - 50 day
| WHERE host.os.type == "linux" and event.type == "creation" and user.name in ("www-data", "apache", "nginx", "httpd", "tomcat", "lighttpd", "glassfish", "weblogic") and (
  file.path like "/var/www/*" or
  file.path like "/var/tmp/*" or
  file.path like "/tmp/*" or
  file.path like "/dev/shm/*"
)
| STATS file_count = COUNT(file.path), host_count = COUNT(host.name) by file.path, host.name, process.name, user.name
// Alter this threshold to make sense for your environment 
| WHERE file_count <= 5
| SORT file_count asc
| LIMIT 100

image

  • Might not be a useful ES|QL query. Might be good as a detection rule, or as an endpoint rule if made more specific. Will keep it here as a reference.

Unusual file downloads from ....

FROM logs-endpoint.events.process-*
| WHERE @timestamp > NOW() - 7 day
| WHERE host.os.type == "linux" and event.type == "start" and process.name in ("curl", "wget") and process.command_line rlike """.*[0-9]{1,3}(\.[0-9]{1,3}){3}.*"""
| STATS process_cli_count = COUNT(process.command_line), host_count = COUNT(host.name) by process.command_line, process.executable, host.name
| WHERE process_cli_count <= 10 and host_count <= 5
| SORT process_cli_count asc
| LIMIT 100

image

Segmentation Fault & Potential Buffer Overflow Hunting

FROM logs-system.syslog*
| WHERE @timestamp > NOW() - 12 hour
| WHERE host.os.type == "linux" and process.name == "kernel" and message like "*segfault*"
| GROK message "\\[%{NUMBER:timestamp}\\] %{WORD:process}\\[%{NUMBER:pid}\\]: segfault at %{BASE16NUM:segfault_address} ip %{BASE16NUM:instruction_pointer} sp %{BASE16NUM:stack_pointer} error %{NUMBER:error_code} in %{DATA:so_file}\\[%{BASE16NUM:so_base_address}\\+%{BASE16NUM:so_offset}\\]"
| KEEP timestamp, process, pid, so_file, segfault_address, instruction_pointer, stack_pointer, error_code, so_base_address, so_offset

image

  • Detects segfaults, parses the syslog related to the segfault, and shows which process segfaulted in conjunction with which shared object file crashed and some additional information regarding the crash.
FROM logs-system.syslog*
| WHERE host.os.type == "linux" and process.name == "kernel" and message like "*segfault*"
| WHERE @timestamp > NOW() - 12 hour
| GROK message "\\[%{DATA:timestamp}\\] %{WORD:process}\\[%{NUMBER:pid}\\]: segfault at %{BASE16NUM:segfault_address} ip %{BASE16NUM:instruction_pointer} sp %{BASE16NUM:stack_pointer} error %{NUMBER:error_code} in %{DATA:so_name}\\[%{BASE16NUM:so_base_address}\\+%{BASE16NUM:so_offset}\\] likely on CPU %{NUMBER:cpu} \\(core %{NUMBER:core}, socket %{NUMBER:socket}\\)"
| EVAL timestamp = REPLACE(timestamp, "\\s+", "")
| KEEP timestamp, process, pid, segfault_address, instruction_pointer, stack_pointer, error_code, so_name, so_base_address, so_offset, cpu, core, socket
| STATS process_count = COUNT(process), so_count = COUNT(so_name) by process, so_name
// Alter this threshold to make sense for your environment 
| WHERE process_count > 100
| LIMIT 10

image

  • Syslog doesn't log all the same every time, sometimes it adds prepending spaces. We can use EVAL to remove those
  • Using GROK and STATS, we can count occurences of segfaults within a plain text message field, potentially detecting buffer overflow attacks
  • We can detect unsuccessful process injection attempts

Persistence via Cron

FROM logs-endpoint.events.file-*
| WHERE @timestamp > NOW() - 7 day
| WHERE host.os.type == "linux" and event.type in ("creation", "change") and (file.path in ("/usr/sbin/cron", "/usr/sbin/anacron") or file.path like "/etc/cron*")
| EVAL persistence = CASE(
    file.path in ("/usr/sbin/cron", "/usr/sbin/anacron") or
    file.path like "/etc/cron*",
    process.name,
    null
)
| STATS cc = COUNT(*), pers_count = COUNT(persistence), agent_count = COUNT(agent.id) by process.executable
| WHERE pers_count > 0 and pers_count <= 3 and agent_count <= 3
| SORT cc asc
| LIMIT 100

Persistence via Systemd (timers)

FROM logs-endpoint.events.file-*
| WHERE @timestamp > NOW() - 7 day
| WHERE host.os.type == "linux" and event.type in ("creation", "change") and (
    file.path like "/etc/systemd/system/*" or
    file.path like "/usr/local/lib/systemd/system/*" or 
    file.path like "/lib/systemd/system/*" or 
    file.path like "/usr/lib/systemd/system/*" or 
    file.path like "/home/*/.config/systemd/user/*"
)
| EVAL persistence = CASE(
    file.path like "/etc/systemd/system/*" or
    file.path like "/usr/local/lib/systemd/system/*" or 
    file.path like "/lib/systemd/system/*" or 
    file.path like "/usr/lib/systemd/system/*" or 
    file.path like "/home/*/.config/systemd/user/*",
    process.name,
    null
)
| STATS cc = COUNT(*), systemd_pers_count = COUNT(persistence), agent_count = COUNT(agent.id) by process.executable
| WHERE systemd_pers_count > 0 and systemd_pers_count <= 3 and agent_count <= 3
| SORT cc asc
| LIMIT 100

Persistence via message-of-the-day

FROM logs-endpoint.events.file-*
| WHERE @timestamp > NOW() - 30 day
| WHERE host.os.type == "linux" and event.type in ("creation", "change") and (file.path like "/etc/update-motd.d/*" or file.path like "/usr/lib/update-notifier/*")
| EVAL persistence = CASE(
    file.path like "/etc/update-motd.d/*" or
    file.path like "/usr/lib/update-notifier/*",
    process.name,
    null
)
| STATS cc = COUNT(*), motd_pers_count = COUNT(persistence), agent_count = COUNT(agent.id) by process.executable
| WHERE motd_pers_count > 0 and motd_pers_count <= 3 and agent_count <= 3
| SORT cc asc
| LIMIT 100

Persistence via rc.local

FROM logs-endpoint.events.file-*
| WHERE @timestamp > NOW() - 30 day
| WHERE host.os.type == "linux" and event.type in ("creation", "change") and (file.path like "/etc/rc?.d/*" or file.path == "/etc/rc.local")
| EVAL persistence = CASE(
    file.path == "/etc/rc.local" or
    file.path like  "/etc/rc?.d/*",
    process.name,
    null
)
| STATS cc = COUNT(*), rc_pers_count = COUNT(persistence), agent_count = COUNT(agent.id) by process.executable
| WHERE rc_pers_count > 0 and rc_pers_count <= 3 and agent_count <= 3
| SORT cc asc
| LIMIT 100

Persistence via init.d

FROM logs-endpoint.events.file-*
| WHERE @timestamp > NOW() - 365 day
| WHERE host.os.type == "linux" and event.type in ("creation", "change") and file.path like "/etc/init.d/*"
| EVAL persistence = CASE(
    file.path like "/etc/init.d/*",
    process.name,
    null
)
| STATS cc = COUNT(*), initd_pers_count = COUNT(persistence), agent_count = COUNT(agent.id) by process.executable
| WHERE initd_pers_count > 0 and initd_pers_count <= 3 and agent_count <= 3
| SORT cc asc
| LIMIT 100

Drivers load with low occurrence frequency

FROM logs-auditd_manager.auditd-*, logs-auditd.log-*, auditbeat-*
| WHERE @timestamp > NOW() - 365 day
| WHERE host.os.type == "linux" and event.category == "driver" and event.action == "loaded-kernel-module" and auditd.data.syscall in ("init_module", "finit_module")
| STATS host_count = COUNT_DISTINCT(host.id), total_count = COUNT(*), ko_count = COUNT_DISTINCT(auditd.data.name) by auditd.data.name, process.executable, process.name
| WHERE host_count == 1 and total_count == 1 and ko_count == 1
| LIMIT 100 
| SORT auditd.data.name asc
  • This event will soon be added to Elastic Defend as well, allowing me to rewrite this to enhance usage.

Network connections with low occurence frequency for unique agent.id

FROM logs-endpoint.events.network-*
| WHERE @timestamp > NOW() - 7 day
| WHERE host.os.type == "linux" and event.type == "start" and event.action in ("connection_attempted", "connection_accepted") and destination.ip IS NOT null and not CIDR_MATCH(destination.ip, "10.0.0.0/8", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.0.0/29", "192.0.0.8/32", "192.0.0.9/32", "192.0.0.10/32", "192.0.0.170/32", "192.0.0.171/32", "192.0.2.0/24", "192.31.196.0/24", "192.52.193.0/24", "192.168.0.0/16", "192.88.99.0/24", "224.0.0.0/4", "100.64.0.0/10", "192.175.48.0/24","198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4", "::1","FE80::/10", "FF00::/8")
| STATS process_count = COUNT(process.name), agent_count = COUNT_DISTINCT(agent.id) by process.name
// Alter this threshold to make sense for your environment 
| WHERE agent_count == 1 and process_count > 0 and process_count <= 3
| LIMIT 100 
| SORT process_count asc

Taking into account GTFOBins

FROM logs-endpoint.events.network-*
| WHERE @timestamp > NOW() - 7 day
| WHERE host.os.type == "linux" and event.type == "start" and event.action in ("connection_attempted", "connection_accepted") and (
    // Add additional LoLbins here
    (process.name in ("bash", "dash", "sh", "tcsh", "csh", "zsh", "ksh", "fish", "socat", "java", "awk", "gawk", "mawk", "nawk", "openssl", "nc", "ncat", "netcat", "telnet")) or
    (process.name like "python*") or
    (process.name like "perl*") or
    (process.name like "ruby*") or
    (process.name like "lua*") or
    (process.name like "php*")
) and
destination.ip IS NOT null and not CIDR_MATCH(destination.ip, "10.0.0.0/8", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.0.0/29", "192.0.0.8/32", "192.0.0.9/32", "192.0.0.10/32", "192.0.0.170/32", "192.0.0.171/32", "192.0.2.0/24", "192.31.196.0/24", "192.52.193.0/24", "192.168.0.0/16", "192.88.99.0/24", "224.0.0.0/4", "100.64.0.0/10", "192.175.48.0/24","198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4", "::1","FE80::/10", "FF00::/8")
| STATS process_count = COUNT(process.name), agent_count = COUNT_DISTINCT(agent.id) by process.name
// Alter this threshold to make sense for your environment 
| WHERE agent_count <= 3 and process_count > 0 and process_count <= 5
| LIMIT 100 
| SORT process_count asc

Taking into account suspicious directories

FROM logs-endpoint.events.network-*
| WHERE @timestamp > NOW() - 90 day
| WHERE host.os.type == "linux" and event.type == "start" and event.action in ("connection_attempted", "connection_accepted") and (
    (process.executable like "./") or
    (process.executable like "/dev/shm/*") or
    (process.executable like "/var/www/*") or
    (process.executable like "/boot/*") or
    (process.executable like "/srv/*") or
    ((process.executable rlike "/tmp/[^/]+" or process.executable rlike "/var/tmp/[^/]+")) or
    (process.executable rlike "/run/[^/]+") or
    (process.executable rlike "/var/run/[^/]+")
) and
destination.ip IS NOT null and not CIDR_MATCH(destination.ip, "127.0.0.0/8", "169.254.0.0/16", "224.0.0.0/4", "::1")
| STATS process_count = COUNT(process.name), agent_count = COUNT_DISTINCT(agent.id) by process.executable
// Alter this threshold to make sense for your environment 
| WHERE agent_count <= 3 and process_count > 0 and process_count <= 5
| LIMIT 100 
| SORT process_count asc

Excessive SSH network activity to unique destinations

FROM logs-endpoint.events.network-*
| WHERE @timestamp > NOW() - 7 day
| WHERE host.os.type == "linux" and event.category == "network" and network.transport == "tcp" and destination.port == 22 and source.port >= 49152 
| KEEP destination.ip, host.id, user.name
| STATS count_unique_dst = COUNT_DISTINCT(destination.ip) by host.id, user.name
// Alter this threshold to make sense for your environment 
| WHERE count_unique_dst >= 10
| LIMIT 100 
| SORT user.name asc

Shell execution from low occurrence suspicious process parent

FROM logs-endpoint.events.process-*
| WHERE @timestamp > NOW() - 30 day
| WHERE host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.name in ("bash", "dash", "sh", "tcsh", "csh", "zsh", "ksh", "fish") and not process.parent.pid == 1 and (
    (process.parent.executable like "./") or
    (process.parent.executable like "/dev/shm/*") or
    (process.parent.executable like "/var/www/*") or
    (process.parent.executable like "/boot/*") or
    (process.parent.executable like "/srv/*") or
    ((process.parent.executable rlike "/tmp/[^/]+" or process.parent.executable rlike "/var/tmp/[^/]+")) or
    (process.parent.executable rlike "/run/[^/]+") or
    (process.parent.executable rlike "/var/run/[^/]+")
)
| STATS agent_count = COUNT_DISTINCT(agent.id), cc = COUNT(*) by process.parent.executable
// Alter this threshold to make sense for your environment 
| WHERE agent_count <= 3 and cc <= 5
| LIMIT 100 
| SORT cc asc

Logon activity by source IP

FROM logs-system.auth-*
| WHERE @timestamp > NOW() - 7 day
| WHERE host.os.type == "linux" and event.category == "authentication" and event.action in ("ssh_login", "user_login") and event.outcome == "failure" and source.ip IS NOT null and not CIDR_MATCH(source.ip, "127.0.0.0/8", "169.254.0.0/16", "224.0.0.0/4", "::1")
| EVAL failed = CASE(event.outcome == "failure", source.ip, null), success = CASE(event.outcome == "success", source.ip, null)
| STATS count_failed = COUNT(failed), count_success = COUNT(success), count_user = count_distinct(user.name) by source.ip
 /* below threshold should be adjusted to your env logon patterns */
| WHERE count_failed >= 100 and count_success <= 10 and count_user >= 20

Low volume external network connections from process by unique agent

FROM logs-endpoint.events.network-*
| WHERE  @timestamp > now() - 7 day 
| WHERE host.os.type == "linux" and event.category == "network" and event.type == "start" and event.action == "connection_attempted" and not process.name is null and
    not CIDR_MATCH(destination.ip, "10.0.0.0/8", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.0.0/29", "192.0.0.8/32", "192.0.0.9/32", "192.0.0.10/32", "192.0.0.170/32", "192.0.0.171/32", "192.0.2.0/24", "192.31.196.0/24", "192.52.193.0/24", "192.168.0.0/16", "192.88.99.0/24", "224.0.0.0/4", "100.64.0.0/10", "192.175.48.0/24","198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "240.0.0.0/4", "::1","FE80::/10", "FF00::/8")
| STATS connection_count = COUNT(*), unique_agent_count = COUNT_DISTINCT(agent.id) by process.name
| WHERE connection_count <= 5 and unique_agent_count == 1
| LIMIT 100 
| SORT connection_count, unique_agent_count asc

Low volume root external network connections from process by unique agent

FROM logs-endpoint.events.network-*
| WHERE  @timestamp > now() - 7 day 
| WHERE host.os.type == "linux" and event.category == "network" and event.type == "start" and event.action == "connection_attempted" and user.id == "0" and not process.name is null and
    not CIDR_MATCH(destination.ip, "10.0.0.0/8", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.0.0/29", "192.0.0.8/32", "192.0.0.9/32", "192.0.0.10/32", "192.0.0.170/32", "192.0.0.171/32", "192.0.2.0/24", "192.31.196.0/24", "192.52.193.0/24", "192.168.0.0/16", "192.88.99.0/24", "224.0.0.0/4", "100.64.0.0/10", "192.175.48.0/24","198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "240.0.0.0/4", "::1","FE80::/10", "FF00::/8")
| STATS connection_count = COUNT(*), unique_agent_count = COUNT_DISTINCT(agent.id) by process.name
| WHERE connection_count <= 5 and unique_agent_count == 1
| LIMIT 100 
| SORT connection_count, unique_agent_count asc

Low volume modifications to critical system binaries by unique host

FROM logs-endpoint.events.file-*
| WHERE @timestamp > NOW() - 7 day
| WHERE host.os.type == "linux" and (
  (file.path like "/bin/*") or
  (file.path like "/usr/bin/*") or
  (file.path like "/sbin/*") or
  (file.path like "/usr/sbin/*")
) and not (
  // Exclude expected update processes, e.g., package managers
  (process.executable in ("/usr/bin/apt", "/usr/bin/dpkg", "/usr/bin/yum", "/usr/bin/rpm", "/usr/bin/pacman", "/usr/bin/pamac-daemon", "/usr/bin/update-alternatives", "/usr/bin/dockerd", "/usr/bin/microdnf", "/sbin/apk")) or
  // Exclude certain benign or expected modification patterns, if applicable
  (file.path like "/usr/bin/gzip*") // Example exclusion, adjust based on your environment
)
| STATS modification_count = COUNT(file.path), unique_files_modified = COUNT_DISTINCT(file.path), host_count = COUNT(host.name) by process.executable, host.name, user.name
// Alter this threshold based on typical behavior in your environment 
| WHERE modification_count >= 1 and host_count == 1
| SORT modification_count asc
| LIMIT 100

Low volume process injection-related syscalls by process executable

FROM logs-auditd_manager.auditd-*, logs-auditd.log-*, auditbeat-*
| WHERE @timestamp > NOW() - 30 day
| WHERE host.os.type == "linux" and auditd.data.syscall in ("ptrace", "memfd_create")
| STATS cc = COUNT(*) by process.executable, auditd.data.syscall
| WHERE cc <= 10
| LIMIT 100 
| SORT cc asc

Low volume GTFOBins external network connections

FROM logs-endpoint.events.network-*
| WHERE @timestamp > NOW() - 7 day
| WHERE host.os.type == "linux" and event.type == "start" and process.name in (
  "ab", "aria2c", "bash", "cpan", "curl", "easy_install", "finger", "ftp",
  "gdb", "gimp", "irb", "jjs", "jrunscript", "julia", "ksh", "lua", "lwp-download",
  "nc", "nmap", "node", "openssl", "php", "pip", "python", "ruby", "rview", "rvim",
  "scp", "sftp", "smbclient", "socat", "ssh", "tar", "tftp", "view", "vim", "vimdiff",
  "wget", "whois", "yum"
) and
destination.ip IS NOT null and not CIDR_MATCH(destination.ip, "10.0.0.0/8", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.0.0/29", "192.0.0.8/32", "192.0.0.9/32", "192.0.0.10/32", "192.0.0.170/32", "192.0.0.171/32", "192.0.2.0/24", "192.31.196.0/24", "192.52.193.0/24", "192.168.0.0/16", "192.88.99.0/24", "224.0.0.0/4", "100.64.0.0/10", "192.175.48.0/24","198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4", "::1","FE80::/10", "FF00::/8") 
| KEEP process.name, destination.port, destination.ip, user.name, host.name
| STATS cc = COUNT(*) by destination.port, process.name, host.name, user.name
| WHERE cc <= 5
| SORT cc asc, destination.port