197 lines
3.9 KiB
Go
197 lines
3.9 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
ssh.go
|
|
|
|
Scans SSH configuration and authorized keys for persistence indicators
|
|
|
|
Checks authorized_keys files for forced command options, sshd_config
|
|
for dangerous directives like PermitRootLogin and non-standard
|
|
AuthorizedKeysFile paths, and detects ~/.ssh/rc login scripts.
|
|
|
|
MITRE ATT&CK:
|
|
T1098.004 - Account Manipulation: SSH Authorized Keys
|
|
*/
|
|
|
|
package scanner
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/CarterPerez-dev/sentinel/pkg/types"
|
|
)
|
|
|
|
const (
|
|
sshScannerName = "ssh"
|
|
mitreSSH = "T1098.004"
|
|
)
|
|
|
|
func init() {
|
|
Register(&SSHScanner{})
|
|
}
|
|
|
|
type SSHScanner struct{}
|
|
|
|
func (s *SSHScanner) Name() string {
|
|
return sshScannerName
|
|
}
|
|
|
|
func (s *SSHScanner) Scan(
|
|
root string,
|
|
) []types.Finding {
|
|
var findings []types.Finding
|
|
|
|
for _, home := range FindUserDirs(root) {
|
|
findings = append(
|
|
findings,
|
|
s.scanAuthorizedKeys(
|
|
filepath.Join(
|
|
home, ".ssh", "authorized_keys",
|
|
),
|
|
)...,
|
|
)
|
|
findings = append(
|
|
findings,
|
|
s.scanAuthorizedKeys(
|
|
filepath.Join(
|
|
home, ".ssh", "authorized_keys2",
|
|
),
|
|
)...,
|
|
)
|
|
|
|
rc := filepath.Join(home, ".ssh", "rc")
|
|
if FileExists(rc) {
|
|
findings = append(findings, types.Finding{
|
|
Scanner: sshScannerName,
|
|
Severity: types.SeverityMedium,
|
|
Title: "SSH rc script detected",
|
|
Path: rc,
|
|
Evidence: "~/.ssh/rc executes on every SSH login",
|
|
MITRE: mitreSSH,
|
|
})
|
|
findings = append(
|
|
findings,
|
|
ScanFileForPatterns(
|
|
rc, sshScannerName, mitreSSH,
|
|
)...,
|
|
)
|
|
}
|
|
}
|
|
|
|
sshdConfig := ResolveRoot(root, "/etc/ssh/sshd_config")
|
|
findings = append(
|
|
findings,
|
|
s.scanSSHDConfig(sshdConfig)...,
|
|
)
|
|
|
|
sshdConfigD := ResolveRoot(
|
|
root, "/etc/ssh/sshd_config.d",
|
|
)
|
|
for _, path := range ListFiles(sshdConfigD) {
|
|
findings = append(
|
|
findings,
|
|
s.scanSSHDConfig(path)...,
|
|
)
|
|
}
|
|
|
|
return findings
|
|
}
|
|
|
|
func (s *SSHScanner) scanAuthorizedKeys(
|
|
path string,
|
|
) []types.Finding {
|
|
lines := ReadLines(path)
|
|
if lines == nil {
|
|
return nil
|
|
}
|
|
|
|
var findings []types.Finding
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(trimmed, "command=") {
|
|
findings = append(findings, types.Finding{
|
|
Scanner: sshScannerName,
|
|
Severity: types.SeverityHigh,
|
|
Title: "Forced command in authorized_keys",
|
|
Path: path,
|
|
Evidence: truncate(trimmed),
|
|
MITRE: mitreSSH,
|
|
})
|
|
}
|
|
|
|
if strings.Contains(trimmed, "environment=") {
|
|
findings = append(findings, types.Finding{
|
|
Scanner: sshScannerName,
|
|
Severity: types.SeverityHigh,
|
|
Title: "Environment override in authorized_keys",
|
|
Path: path,
|
|
Evidence: truncate(trimmed),
|
|
MITRE: mitreSSH,
|
|
})
|
|
}
|
|
}
|
|
return findings
|
|
}
|
|
|
|
func (s *SSHScanner) scanSSHDConfig(
|
|
path string,
|
|
) []types.Finding {
|
|
lines := ReadLines(path)
|
|
if lines == nil {
|
|
return nil
|
|
}
|
|
|
|
var findings []types.Finding
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
lower := strings.ToLower(trimmed)
|
|
|
|
if strings.HasPrefix(lower, "permitrootlogin") &&
|
|
strings.Contains(lower, "yes") {
|
|
findings = append(findings, types.Finding{
|
|
Scanner: sshScannerName,
|
|
Severity: types.SeverityMedium,
|
|
Title: "PermitRootLogin enabled",
|
|
Path: path,
|
|
Evidence: trimmed,
|
|
MITRE: mitreSSH,
|
|
})
|
|
}
|
|
|
|
if strings.HasPrefix(lower, "authorizedkeysfile") {
|
|
val := strings.Fields(trimmed)
|
|
if len(val) >= 2 {
|
|
keyPath := val[1]
|
|
if !strings.Contains(
|
|
keyPath, ".ssh/authorized_keys",
|
|
) {
|
|
findings = append(
|
|
findings, types.Finding{
|
|
Scanner: sshScannerName,
|
|
Severity: types.SeverityHigh,
|
|
Title: "Non-standard AuthorizedKeysFile path",
|
|
Path: path,
|
|
Evidence: trimmed,
|
|
MITRE: mitreSSH,
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return findings
|
|
}
|
|
|
|
const maxKeyLineLen = 120
|
|
|
|
func truncate(s string) string {
|
|
if len(s) <= maxKeyLineLen {
|
|
return s
|
|
}
|
|
return s[:maxKeyLineLen] + "..."
|
|
}
|