186 lines
3.7 KiB
Go
186 lines
3.7 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
systemd.go
|
|
|
|
Scans systemd unit files for persistence indicators
|
|
|
|
Enumerates service and timer units across system and user directories,
|
|
parses Exec directives, and flags units that show signs of attacker
|
|
installation: suspicious commands, non-standard paths, world-writable
|
|
files, and recent modification timestamps.
|
|
|
|
MITRE ATT&CK:
|
|
T1543.002 - Create or Modify System Process: Systemd Service
|
|
T1053.006 - Scheduled Task/Job: Systemd Timers
|
|
*/
|
|
|
|
package scanner
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/CarterPerez-dev/sentinel/pkg/types"
|
|
)
|
|
|
|
const (
|
|
systemdScannerName = "systemd"
|
|
mitreSystemd = "T1543.002"
|
|
mitreTimer = "T1053.006"
|
|
mitrePath = "T1543.002"
|
|
)
|
|
|
|
var systemdDirs = []string{
|
|
"/etc/systemd/system",
|
|
"/run/systemd/system",
|
|
"/usr/lib/systemd/system",
|
|
}
|
|
|
|
var execDirectives = []string{
|
|
"ExecStart=",
|
|
"ExecStartPre=",
|
|
"ExecStartPost=",
|
|
"ExecStop=",
|
|
"ExecStopPost=",
|
|
"ExecReload=",
|
|
}
|
|
|
|
func init() {
|
|
Register(&SystemdScanner{})
|
|
}
|
|
|
|
type SystemdScanner struct{}
|
|
|
|
func (s *SystemdScanner) Name() string {
|
|
return systemdScannerName
|
|
}
|
|
|
|
func (s *SystemdScanner) Scan(
|
|
root string,
|
|
) []types.Finding {
|
|
var findings []types.Finding
|
|
|
|
for _, dir := range systemdDirs {
|
|
resolved := ResolveRoot(root, dir)
|
|
findings = append(
|
|
findings,
|
|
s.scanDir(resolved)...,
|
|
)
|
|
}
|
|
|
|
for _, home := range FindUserDirs(root) {
|
|
userDir := filepath.Join(
|
|
home, ".config", "systemd", "user",
|
|
)
|
|
findings = append(
|
|
findings,
|
|
s.scanDir(userDir)...,
|
|
)
|
|
}
|
|
|
|
return findings
|
|
}
|
|
|
|
func (s *SystemdScanner) scanDir(
|
|
dir string,
|
|
) []types.Finding {
|
|
var findings []types.Finding
|
|
|
|
files := ListFiles(dir)
|
|
for _, path := range files {
|
|
ext := filepath.Ext(path)
|
|
if ext != ".service" && ext != ".timer" &&
|
|
ext != ".socket" && ext != ".path" {
|
|
continue
|
|
}
|
|
|
|
mitre := mitreSystemd
|
|
switch ext {
|
|
case ".timer":
|
|
mitre = mitreTimer
|
|
case ".path":
|
|
mitre = mitrePath
|
|
}
|
|
|
|
findings = append(
|
|
findings,
|
|
s.analyzeUnit(path, mitre)...,
|
|
)
|
|
}
|
|
|
|
entries := ListDir(dir)
|
|
for _, e := range entries {
|
|
if e.IsDir() && strings.HasSuffix(e.Name(), ".d") {
|
|
dropinDir := filepath.Join(dir, e.Name())
|
|
for _, f := range ListFiles(dropinDir) {
|
|
if strings.HasSuffix(f, ".conf") {
|
|
findings = append(
|
|
findings,
|
|
s.analyzeUnit(f, mitreSystemd)...,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return findings
|
|
}
|
|
|
|
func (s *SystemdScanner) analyzeUnit(
|
|
path, mitre string,
|
|
) []types.Finding {
|
|
var findings []types.Finding
|
|
lines := ReadLines(path)
|
|
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
for _, directive := range execDirectives {
|
|
if !strings.HasPrefix(trimmed, directive) {
|
|
continue
|
|
}
|
|
cmd := strings.TrimPrefix(trimmed, directive)
|
|
cmd = strings.TrimPrefix(cmd, "-")
|
|
|
|
matched, sev, label := MatchLine(cmd)
|
|
if matched {
|
|
findings = append(findings, types.Finding{
|
|
Scanner: systemdScannerName,
|
|
Severity: sev,
|
|
Title: "Suspicious " +
|
|
strings.TrimSuffix(
|
|
directive, "=",
|
|
) + ": " + label,
|
|
Path: path,
|
|
Evidence: trimmed,
|
|
MITRE: mitre,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if IsWorldWritable(path) {
|
|
findings = append(findings, types.Finding{
|
|
Scanner: systemdScannerName,
|
|
Severity: types.SeverityMedium,
|
|
Title: "World-writable unit file",
|
|
Path: path,
|
|
Evidence: "File permissions allow any user to modify this unit",
|
|
MITRE: mitre,
|
|
})
|
|
}
|
|
|
|
if ModifiedWithin(path, 24*time.Hour) {
|
|
findings = append(findings, types.Finding{
|
|
Scanner: systemdScannerName,
|
|
Severity: types.SeverityMedium,
|
|
Title: "Recently modified unit file",
|
|
Path: path,
|
|
Evidence: "Modified within the last 24 hours",
|
|
MITRE: mitre,
|
|
})
|
|
}
|
|
|
|
return findings
|
|
}
|