[Meta] Prepare 20 Linux ES|QL Hunts
Aegrah opened this issue · comments
Ruben Groenewoud commented
Meta Summary
The goal of this meta is to create ~20 Linux ES|QL hunts.
Estimated Time to Complete
1 sprint - 2 weeks
Tasklist
Resources / References
Ruben Groenewoud commented
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.
- ...
Ruben Groenewoud commented
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
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.
Ruben Groenewoud commented
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
- 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.
Ruben Groenewoud commented
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
- 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.
Ruben Groenewoud commented
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
- 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.
Ruben Groenewoud commented
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
- 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.
Ruben Groenewoud commented
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
- 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.
Ruben Groenewoud commented
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
Ruben Groenewoud commented
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
- 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
- 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
Ruben Groenewoud commented
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
Ruben Groenewoud commented
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
Ruben Groenewoud commented
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
Ruben Groenewoud commented
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
Ruben Groenewoud commented
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
Ruben Groenewoud commented
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.
Ruben Groenewoud commented
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
Ruben Groenewoud commented
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
Ruben Groenewoud commented
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
Ruben Groenewoud commented
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
Ruben Groenewoud commented
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
Ruben Groenewoud commented
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
Ruben Groenewoud commented
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
Ruben Groenewoud commented
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