├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── assets
├── cd3.png
├── usage1.png
├── usage2.png
├── usage_csv.png
└── usage_json.png
├── go.mod
├── internal
├── findCommonBackdoors.go
├── findCronJobs.go
├── findEnvironmentVariables.go
├── findKernelModules.go
├── findLocalUsers.go
├── findSSHKeys.go
├── findShellConfig.go
├── findStartupServices.go
├── findSuspiciousCommandline.go
├── findSuspiciousConnections.go
├── findUserStartupScripts.go
├── findWebShells.go
├── helpers
│ ├── fileSystemHelpers.go
│ └── stringHelpers.go
├── internalTypes.go
└── internalVars.go
└── main.go
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | # Go workspace file
21 | go.work
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 panscan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ```
2 | __ __
3 | ______________ ______/ /______/ /___ _ ______
4 | / ___/ ___/ __ '/ ___/ //_/ __ / __ \ | /| / / __ \
5 | / /__/ / / /_/ / /__/ ,< / /_/ / /_/ / |/ |/ / / / /
6 | \___/_/ \__,_/\___/_/|_|\__,_/\____/|__/|__/_/ /_/
7 | ```
8 |
9 |
10 |
11 |
12 |
13 | crackdown - Linux Persistence Hunter
14 |
15 |
16 | #### What is it?
17 |
18 | crackdown is a command-line utility designed to aid Incident Responders in the detection of potential adversary persistence mechanisms on Linux-based operating systems.
19 |
20 | It must be run as sudo to be effective.
21 |
22 | #### Usage
23 |
24 | Just download the release and run as sudo:
25 | ```
26 | sudo crackdown
27 | sudo crackdown -quiet
28 | ```
29 | Use the '-quiet' flag to suppress detections at the command-line - all detections are output to CSV/JSON output in the current working directory.
30 |
31 | CSV output will store detection metadata in a JSON string within the Metadata column - other columns are normal strings.
32 |
33 |
34 |
35 |
36 | StdOut Output Example
37 |
38 |
39 |
40 | CSV Output Example
41 |
42 |
43 |
44 | JSON Output Example
45 |
46 |
47 |
48 |
49 | #### What is inspected?
50 |
51 | * **Running Processes** [T1059]
52 | * Suspicious Keywords, IP/Domain in Commandline, Running process with non-existent executable
53 | * **Active TCP Connections** [T1071]
54 | * Suspicious Ports, Unusual processes with connections
55 | * **Cron Jobs** [T1053.003]
56 | * Suspicious Keywords, General Command Review
57 | * **Local Users** [T1136.001]
58 | * General Review, Privileged Users [TODO]
59 | * **SSH Authorized Keys** [T1098.004]
60 | * Recent Modifications, General Review
61 | * **Recently Modified Kernel Modules** [T1547.006]
62 | * **Modified Shell Configuration Files** [T1546.004]
63 | * **Created/Modified Service Files/Service Confs** [T1543.002]
64 | * /etc/systemd/system|user
65 | * /run/systemd/system|user
66 | * /lib/systemd/system|user
67 | * **Environment Variable Scanning**
68 | * **apt/git/doas/motd/Startup/at.allow|deny,etc Backdoors** [T1037.005]
69 | * **Webshell Scan** [T1505.003]
70 | * /var/www
71 | * /etc/nginx
72 | * /etc/apache*
73 | * **Broad Scanning for Suspicious Files**
74 | * /etc/update-motd.d
75 | * /var/run/motd
76 | * /etc/init.d
77 | * /etc/rc.d
78 | * /sbin/init.d
79 | * /etc/rc.local
80 | * /etc/apt/apt.conf.d
81 | * /usr/share/unattended-upgrades
82 | * /home/*/.gitconfig
83 | * /etc/at.allow
84 | * /etc/at.deny
85 | * /etc/doas.conf
86 | * /home/*/. Scripts|Confs
87 | * /root/.*
88 |
89 | #### MITRE Techniques Evaluated
90 | * T1547.006 - Boot or Logon Autostart Execution: Kernel Modules and Extensions
91 | * T1546.004 - Event Triggered Execution: Unix Shell Configuration Modification
92 | * T1098.004 - Account Manipulation: SSH Authorized Keys
93 | * T1136.001 - Create Account: Local Account
94 | * T1053.003 - Scheduled Task/Job: Cron
95 | * T1071 - Application Layer Protocol
96 | * T1059 - Command and Scripting Interpreter
97 | * T1543.002 - Create or Modify System Process: Systemd Service
98 | * T1037.005 - Boot or Logon Initialization Scripts: Startup Items
99 | * T1505.003 - Server Software Component: Web Shell
100 |
101 | TODO:
102 | * git hooks/config finding
103 | * at allow config file
104 | * NOPSSWD Sudoers
105 | * Recently Modified Binaries
106 | * Privileged User Highlight
107 |
108 |
109 | #### General References
110 | * https://www.ibm.com/docs/en/zos/2.5.0?topic=daemon-format-authorized-keys-file
111 | * https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Linux%20-%20Persistence.md
112 | * https://www.linode.com/docs/guides/linux-red-team-persistence-techniques/
113 | * https://cyberblueteam.medium.com/blue-team-tips-linux-os-finding-evil-running-process-3f12b17c3b8e
114 | * https://pberba.github.io/assets/posts/common/20220201-linux-persistence.png
115 | * https://github.com/xl7dev/WebShell
116 | * https://medium.com/kernel-space/linux-fundamentals-a-to-z-of-a-sudoers-file-a5da99a30e7f
117 | *
--------------------------------------------------------------------------------
/assets/cd3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeavanzato/crackdown/78a1667d6adae9114da90e8349016e340448abbf/assets/cd3.png
--------------------------------------------------------------------------------
/assets/usage1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeavanzato/crackdown/78a1667d6adae9114da90e8349016e340448abbf/assets/usage1.png
--------------------------------------------------------------------------------
/assets/usage2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeavanzato/crackdown/78a1667d6adae9114da90e8349016e340448abbf/assets/usage2.png
--------------------------------------------------------------------------------
/assets/usage_csv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeavanzato/crackdown/78a1667d6adae9114da90e8349016e340448abbf/assets/usage_csv.png
--------------------------------------------------------------------------------
/assets/usage_json.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeavanzato/crackdown/78a1667d6adae9114da90e8349016e340448abbf/assets/usage_json.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/javanzato/crackdown
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/bastjan/netstat v1.0.0
7 | github.com/mitchellh/go-ps v1.0.0
8 | github.com/rs/zerolog v1.31.0
9 | golang.org/x/text v0.13.0
10 | )
11 |
12 | require (
13 | github.com/ElyKar/golang-kmod v0.0.0-20170829154754-808c2b3797f5 // indirect
14 | github.com/google/go-cmp v0.6.0 // indirect
15 | github.com/mattn/go-colorable v0.1.13 // indirect
16 | github.com/mattn/go-isatty v0.0.20 // indirect
17 | golang.org/x/sys v0.13.0 // indirect
18 | )
19 |
--------------------------------------------------------------------------------
/internal/findCommonBackdoors.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "github.com/javanzato/crackdown/internal/helpers"
5 | "github.com/rs/zerolog"
6 | "io/fs"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 | )
11 |
12 | var commonBackdoorFiles = make([]string, 10)
13 |
14 | func CheckCommonBackdoors(logger zerolog.Logger, detections chan<- Detection, waitGroup *WaitGroupCount) {
15 | defer waitGroup.Done()
16 | logger.Info().Msg("Checking Common Backdoor Locations...")
17 | getBackdoorFiles(logger)
18 | for _, file := range commonBackdoorFiles {
19 | if file == "" {
20 | continue
21 | }
22 | if CheckFileIsScanned(file) {
23 | continue
24 | }
25 | fileStat, err := os.Stat(file)
26 | fileModificationTime := "NA"
27 | if err != nil {
28 | logger.Error().Err(err)
29 | } else {
30 | fileModificationTime = fileStat.ModTime().UTC().String()
31 | }
32 |
33 | tmp_ := map[string]interface{}{
34 | "File": strings.TrimSpace(file),
35 | "LastModified": fileModificationTime,
36 | }
37 | detection := Detection{
38 | Name: "",
39 | Severity: 0,
40 | Tip: "Verify validity of script file.",
41 | Technique: "T1543.002",
42 | Metadata: tmp_,
43 | }
44 | fileSlice := helpers.ReadFileToSlice(file, logger)
45 | result := false
46 | lineCheck:
47 | for _, line := range fileSlice {
48 | detection.Metadata["Line"] = line
49 | detection.Name = "Webshell Pattern in Script File"
50 | result = checkWebshellContent(detection, detections, line)
51 | if result {
52 | break lineCheck
53 | }
54 | detection.Name = "Suspicious Pattern in Script File"
55 | result = checkSuspiciousContent(detection, detections, line)
56 | if result {
57 | break lineCheck
58 | }
59 | detection.Name = "IP Address Pattern in Script File"
60 | result = checkIPContent(detection, detections, line)
61 | if result {
62 | break lineCheck
63 | }
64 | detection.Name = "Domain Pattern in Script File"
65 | result = checkDomainContent(detection, detections, line)
66 | if result {
67 | break lineCheck
68 | }
69 | }
70 | if result == false && fileModificationTime != "NA" {
71 | // No detection yet on this file - check for recent modification
72 | dayDiff := int(timestampNow.Sub(fileStat.ModTime().UTC()).Hours() / 24)
73 | if dayDiff <= 30 {
74 | // File modified within last 30 days
75 | detection.Name = "Script File modified within last 30 days."
76 | detection.Metadata["DaysAgo"] = dayDiff
77 | detection.Severity = 1
78 | detections <- detection
79 | }
80 | }
81 |
82 | }
83 | }
84 |
85 | func getBackdoorFiles(logger zerolog.Logger) {
86 | backDoorFiles := []string{
87 | "/etc/at.allow",
88 | "/etc/at.deny",
89 | "/etc/doas.conf",
90 | "/etc/yum.conf",
91 | }
92 | backdoorDirs := []string{
93 | "/etc/update-motd.d",
94 | "/var/run/motd",
95 | "/etc/init.d",
96 | "/etc/rc.d",
97 | "/sbin/init.d",
98 | "/etc/rc.local",
99 | "/etc/apt/apt.conf.d",
100 | "/usr/share/unattended-upgrades",
101 | "/etc/yum.repos.d",
102 | }
103 | f1, err := filepath.Glob("/home/*/.gitconfig")
104 | if err != nil {
105 | logger.Error().Err(err)
106 | } else {
107 | backdoorDirs = append(backdoorDirs, f1...)
108 | }
109 | for _, path := range backdoorDirs {
110 | filepath.WalkDir(path, walkf)
111 | }
112 | commonBackdoorFiles = append(commonBackdoorFiles, backDoorFiles...)
113 | }
114 |
115 | func walkf(s string, d fs.DirEntry, err error) error {
116 | if err != nil {
117 | return err
118 | }
119 | if !d.IsDir() {
120 | commonBackdoorFiles = append(commonBackdoorFiles, s)
121 | }
122 | return nil
123 | }
124 |
--------------------------------------------------------------------------------
/internal/findCronJobs.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "fmt"
5 | "github.com/javanzato/crackdown/internal/helpers"
6 | "github.com/rs/zerolog"
7 | "os"
8 | "regexp"
9 | "strings"
10 | )
11 |
12 | func FindCronJobs(logger zerolog.Logger, detections chan<- Detection, waitGroup *WaitGroupCount) {
13 | defer waitGroup.Done()
14 | logger.Info().Msg("Finding Cron Jobs...")
15 | cronDirs := []string{"/var/spool/cron/crontabs", "/etc/cron.daily", "/etc/cron.hourly", "/etc/cron.monthly", "/etc/cron.weekly", "/etc/cron.d"}
16 | cronFilePaths := make([]string, 10)
17 | for _, path := range cronDirs {
18 | files, err := os.ReadDir(path)
19 | if err != nil {
20 | logger.Error().Err(err)
21 | continue
22 | }
23 | for _, file := range files {
24 | tmp_name := fmt.Sprintf("%s/%s", path, file.Name())
25 | cronFilePaths = append(cronFilePaths, tmp_name)
26 | }
27 | }
28 | // root crontab on ubuntu-like systems
29 | if helpers.FileExists("/etc/crontab") {
30 | cronFilePaths = append(cronFilePaths, "/etc/crontab")
31 | }
32 |
33 | cronRegex := regexp.MustCompile(`[\d*]{1,4}\s[\d*]{1,4}\s[\d*]{1,4}\s[\d*]{1,4}\s[\d*]{1,4}\s(?P.*?)\s.*?(?P.+)`)
34 | for _, cronFile := range cronFilePaths {
35 | // TODO - IP Check in Cronjob Line
36 | // TODO - General Cleanup
37 | // slice capacity grows beyond actual files that we store in it due to go adding more than is required when increasing cap
38 | if cronFile != "" {
39 | lines := helpers.ReadFileToSlice(cronFile, logger)
40 | for _, line := range lines {
41 | if strings.HasPrefix(line, "#") {
42 | continue
43 | }
44 | match := cronRegex.FindStringSubmatch(line)
45 | if match == nil {
46 | continue
47 | }
48 |
49 | results := map[string]string{}
50 | for i, name := range match {
51 | results[cronRegex.SubexpNames()[i]] = name
52 | }
53 | tmp_ := map[string]interface{}{
54 | "User": "root",
55 | "Command": results["command"],
56 | "File": cronFile,
57 | }
58 | detection := Detection{
59 | Name: "Cronjob Review",
60 | Severity: 1,
61 | Tip: "Verify validity of cronjob.",
62 | Technique: "T1053.003",
63 | Metadata: tmp_,
64 | }
65 |
66 | // Suspicious String Cronjob Detection
67 | susPatternMatch := false
68 | patternMatch:
69 | for _, pattern := range suspiciousPatterns {
70 | if strings.Contains(line, pattern) {
71 | detection.Severity = 2
72 | detection.Name = "Suspicious Pattern in Cronjob Command"
73 | detection.Metadata["Pattern"] = pattern
74 | detection.Metadata["User"] = results["user"]
75 | detections <- detection
76 | susPatternMatch = true
77 | break patternMatch
78 | }
79 | }
80 | if susPatternMatch {
81 | continue
82 | }
83 |
84 | // IP/Domain Regex Detection
85 | ipv4Match, _ := regexp.MatchString(ipv4Regex+`|`+ipv6Regex, line)
86 | if ipv4Match {
87 | detection.Metadata["User"] = results["user"]
88 | detection.Metadata["Line"] = line
89 | detection.Name = "IP Address Pattern in Cron line"
90 | detections <- detection
91 | continue
92 | }
93 | domainMatch, _ := regexp.MatchString(domainRegex, line)
94 | if domainMatch {
95 | detection.Metadata["User"] = results["user"]
96 | detection.Metadata["Line"] = line
97 | detection.Name = "Domain Pattern in Cron line"
98 | detections <- detection
99 | continue
100 | }
101 |
102 | // Root Cronjob Detection - Should be last as a system-wide catch-all
103 | if results["user"] == "root" {
104 | detection.Metadata["User"] = "root"
105 | detection.Name = "Root Cronjob Review"
106 | detections <- detection
107 | continue
108 | }
109 |
110 | // TODO - Review - Removing generic cronjob review for now.
111 | /*tmp_ := map[string]interface{}{
112 | "User": results["user"],
113 | "Command": results["command"],
114 | "File": cronFile,
115 | }
116 | detection := Detection{
117 | Name: "Cronjob Review",
118 | Severity: 0,
119 | Tip: "Verify validity of cronjob.",
120 | Technique: "T1053.003",
121 | Metadata: tmp_,
122 | }*/
123 | //detections <- detection
124 | }
125 | }
126 | }
127 | return
128 |
129 | }
130 |
--------------------------------------------------------------------------------
/internal/findEnvironmentVariables.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "github.com/rs/zerolog"
5 | "os"
6 | "strings"
7 | )
8 |
9 | func CheckEnvironmentVariables(logger zerolog.Logger, detections chan<- Detection, waitGroup *WaitGroupCount) {
10 | defer waitGroup.Done()
11 | logger.Info().Msg("Checking Environment Variables...")
12 | envs := os.Environ()
13 | for _, v := range envs {
14 | envSplit := strings.SplitN(v, "=", 2)
15 | envKey := "NA"
16 | envValue := v
17 | if len(envSplit) == 2 {
18 | envKey = envSplit[0]
19 | envValue = envSplit[1]
20 | }
21 | tmp_ := map[string]interface{}{
22 | "EnvKey": envKey,
23 | "EnvValue": envValue,
24 | }
25 | detection := Detection{
26 | Name: "Suspicious Environment Variable",
27 | Severity: 3,
28 | Tip: "Investigate the variable to determine validity.",
29 | Technique: "T1574",
30 | Metadata: tmp_,
31 | }
32 |
33 | detection.Name = "Suspicious Pattern in Environment Variable"
34 | result := checkSuspiciousContent(detection, detections, envValue)
35 | if result {
36 | continue
37 | }
38 | detection.Name = "IP Address Pattern in Environment Variable"
39 | result = checkIPContent(detection, detections, envValue)
40 | if result {
41 | continue
42 | }
43 | detection.Name = "Domain Pattern in Environment Variable"
44 | result = checkDomainContent(detection, detections, envValue)
45 | if result {
46 | continue
47 | }
48 |
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/internal/findKernelModules.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "fmt"
5 | "github.com/rs/zerolog"
6 | "os"
7 | "os/exec"
8 | "strings"
9 | )
10 |
11 | type KernelModule struct {
12 | Name string
13 | Filename string
14 | Author string
15 | Signer string
16 | Modified string
17 | Created string
18 | }
19 |
20 | func FindKernelModules(logger zerolog.Logger, detections chan<- Detection, waitGroup *WaitGroupCount) {
21 | defer waitGroup.Done()
22 | logger.Info().Msg("Checking Kernel Modules...")
23 | cmd := exec.Command("lsmod")
24 | stdout, err := cmd.Output()
25 | if err != nil {
26 | logger.Error().Err(err)
27 | return
28 | }
29 | lines := strings.Split(string(stdout), "\n")
30 | for _, l := range lines {
31 | if strings.HasPrefix(l, "Module") {
32 | continue
33 | }
34 | lineSplit := strings.Split(l, " ")
35 | moduleName := strings.TrimSpace(lineSplit[0])
36 | if moduleName == "" {
37 | continue
38 | }
39 | //fmt.Println(moduleName)
40 | modcmd := exec.Command("modinfo", moduleName)
41 | modInfoStdout, err := modcmd.Output()
42 | if err != nil {
43 | logger.Error().Err(err)
44 | continue
45 | }
46 | linesMod := strings.Split(string(modInfoStdout), "\n")
47 | modInfo := KernelModule{
48 | Name: "",
49 | Filename: "",
50 | Author: "",
51 | Signer: "",
52 | Modified: "NA",
53 | }
54 | for _, ll := range linesMod {
55 | if strings.HasPrefix(ll, "filename") {
56 | modInfo.Filename = fmt.Sprintf("%v", strings.TrimSpace(strings.Split(ll, ":")[1]))
57 | } else if strings.HasPrefix(ll, "signer") {
58 | modInfo.Signer = strings.TrimSpace(strings.Split(ll, ":")[1])
59 | } else if strings.HasPrefix(ll, "author") {
60 | modInfo.Author = strings.TrimSpace(strings.Split(ll, ":")[1])
61 | } else if strings.HasPrefix(ll, "name") {
62 | modInfo.Name = strings.TrimSpace(strings.Split(ll, ":")[1])
63 | }
64 | }
65 | modInfo.Modified = "NA"
66 | fileStat, err := os.Stat(modInfo.Filename)
67 | if err != nil {
68 | logger.Error().Err(err)
69 | } else {
70 | modInfo.Modified = fileStat.ModTime().UTC().String()
71 | }
72 | if modInfo.Modified != "NA" {
73 | dayDiff := int(timestampNow.Sub(fileStat.ModTime().UTC()).Hours() / 24)
74 | if dayDiff <= 30 {
75 | // Kernel File modified within last 30 days
76 | tmp_ := map[string]interface{}{
77 | "ModuleName": modInfo.Name,
78 | "Filename": modInfo.Filename,
79 | "Author": modInfo.Author,
80 | "Signer": modInfo.Signer,
81 | "Modified": modInfo.Modified,
82 | "DaysAgo": dayDiff,
83 | }
84 | detection := Detection{
85 | Name: "Kernel Module modified within last 30 days",
86 | Severity: 2,
87 | Tip: "Investigate module to determine validity.",
88 | Technique: "T1547.006",
89 | Metadata: tmp_,
90 | }
91 | detections <- detection
92 | }
93 | }
94 | }
95 |
96 | }
97 |
--------------------------------------------------------------------------------
/internal/findLocalUsers.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "bufio"
5 | "github.com/rs/zerolog"
6 | "io"
7 | "os"
8 | "os/user"
9 | "strings"
10 | )
11 |
12 | func FindLocalUsers(logger zerolog.Logger, detections chan<- Detection, waitGroup *WaitGroupCount) {
13 | // https://socketloop.com/tutorials/golang-get-all-local-users-and-print-out-their-home-directory-description-and-group-id
14 | defer waitGroup.Done()
15 | logger.Info().Msg("Finding Local Users...")
16 | file, err := os.Open("/etc/passwd")
17 | defer file.Close()
18 | if err != nil {
19 | logger.Error().Err(err)
20 | return
21 | }
22 | // TODO - Abstract the reading of file away from user extraction
23 | // TODO - Parse user privileges to find users with sudo, etc.
24 | reader := bufio.NewReader(file)
25 | var Users []string
26 | for {
27 | line, err := reader.ReadString('\n')
28 | if equal := strings.Index(line, "#"); equal < 0 {
29 | lineSlice := strings.FieldsFunc(line, func(divide rune) bool {
30 | return divide == ':'
31 | })
32 | if len(lineSlice) > 0 {
33 | Users = append(Users, lineSlice[0])
34 | }
35 | }
36 | if err == io.EOF {
37 | break
38 | }
39 | if err != nil {
40 | logger.Error().Err(err)
41 | return
42 | }
43 | }
44 | for _, name := range Users {
45 | usr, err := user.Lookup(name)
46 | if err != nil {
47 | logger.Error().Err(err)
48 | }
49 |
50 | tmp_ := map[string]interface{}{
51 | "Username": strings.TrimSpace(usr.Username),
52 | "_HomeDir": strings.TrimSpace(usr.HomeDir),
53 | "_GroupID": strings.TrimSpace(usr.Gid),
54 | "_DisplayName": strings.TrimSpace(usr.Name),
55 | }
56 | detection := Detection{
57 | Name: "Local User Account",
58 | Severity: 0,
59 | Tip: "Verify validity of user account.",
60 | Technique: "T1136.001",
61 | Metadata: tmp_,
62 | }
63 | //detections = append(detections, detection)
64 | detections <- detection
65 | //logger.Info(detection)
66 | /*fmt.Printf("username:%s\n", usr.Username)
67 | fmt.Printf("homedir:%s\n", usr.HomeDir)
68 | fmt.Printf("groupID:%s\n", usr.Gid)
69 | fmt.Printf("DisplayName:%s\n", usr.Name)
70 | fmt.Println("*********************************")*/
71 |
72 | }
73 | return
74 | }
75 |
--------------------------------------------------------------------------------
/internal/findSSHKeys.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "github.com/javanzato/crackdown/internal/helpers"
5 | "github.com/rs/zerolog"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 | )
10 |
11 | func FindSSHAuthorizedKeys(logger zerolog.Logger, detections chan<- Detection, waitGroup *WaitGroupCount) {
12 | defer waitGroup.Done()
13 | // TODO - Add additional possible properties to detection metadata
14 | // https://www.ibm.com/docs/en/zos/2.5.0?topic=daemon-format-authorized-keys-file
15 | logger.Info().Msg("Finding Authorized SSH Keys...")
16 | files, err := filepath.Glob("/home/*/.ssh/authorized_keys")
17 | files = append(files, "/root/.ssh/authorized_keys")
18 | if err != nil {
19 | logger.Error().Err(err)
20 | return
21 | }
22 | validKeyTypes := []string{
23 | "ssh-dss", "ssh-rsa", "ecdsa-sha2-nistp256", "ecdsa-sha2- nistp384", "ecdsa-sha2-nistp521", "ssh-ed25519",
24 | }
25 | for _, file := range files {
26 | if helpers.FileExists(file) == false {
27 | continue
28 | }
29 | lines := helpers.ReadFileToSlice(file, logger)
30 | // File Modification Check
31 | fileStat, err := os.Stat(file)
32 | fileModificationTime := "NA"
33 | if err != nil {
34 | logger.Error().Err(err)
35 | } else {
36 | fileModificationTime = fileStat.ModTime().UTC().String()
37 | }
38 |
39 | // Authorized Key Parsing
40 | for _, l := range lines {
41 | keySplit := strings.Split(strings.TrimSpace(l), " ")
42 | if keySplit[0] == "" {
43 | continue
44 | }
45 | keyTypeExists := false
46 | keyType := ""
47 | keyName := ""
48 | for _, v := range keySplit {
49 | if helpers.SliceContains(validKeyTypes, v) {
50 | keyType = v
51 | keyTypeExists = true
52 | }
53 | }
54 | keyName = keySplit[len(keySplit)-1]
55 | if len(keyName) > 30 {
56 | // We can probably assume this is actually the public key data rather than the 'name'
57 | keyName = ""
58 | }
59 | if keyTypeExists == true {
60 | tmp_ := map[string]interface{}{
61 | "KeyType": strings.TrimSpace(keyType),
62 | "KeyName": strings.TrimSpace(keyName),
63 | "File": strings.TrimSpace(file),
64 | "LastModified": fileModificationTime,
65 | }
66 | detection := Detection{
67 | Name: "SSH Key Review",
68 | Severity: 0,
69 | Tip: "Verify validity of authorized key",
70 | Technique: "T1098.004",
71 | Metadata: tmp_,
72 | }
73 | detections <- detection
74 | }
75 | }
76 |
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/internal/findShellConfig.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "github.com/javanzato/crackdown/internal/helpers"
5 | "github.com/rs/zerolog"
6 | "os"
7 | "path/filepath"
8 | "regexp"
9 | )
10 |
11 | func CheckShellConfigs(logger zerolog.Logger, detections chan<- Detection, waitGroup *WaitGroupCount) {
12 | defer waitGroup.Done()
13 | logger.Info().Msg("Checking .bashrc Files...")
14 | files, err := filepath.Glob("/home/*/.bashrc")
15 | if err != nil {
16 | logger.Error().Err(err)
17 | return
18 | }
19 | zfiles, zerr := filepath.Glob("/home/*/.zshrc")
20 | if zerr != nil {
21 | logger.Error().Err(zerr)
22 | return
23 | }
24 | files = append(files, zfiles...)
25 | files = append(files, "/root/.bashrc")
26 | files = append(files, "/etc/bash.bashrc")
27 | files = append(files, "/etc/zsh/zshrc")
28 | for _, file := range files {
29 | if helpers.FileExists(file) == false {
30 | continue
31 | }
32 | fileContents := helpers.ReadFileToString(file, logger)
33 | // File Modification Check
34 | fileStat, err := os.Stat(file)
35 | fileModificationTime := "NA"
36 | if err != nil {
37 | logger.Error().Err(err)
38 | } else {
39 | fileModificationTime = fileStat.ModTime().UTC().String()
40 | }
41 | tmp_ := map[string]interface{}{
42 | "Modified": fileModificationTime,
43 | "File": file,
44 | }
45 | detection := Detection{
46 | Name: "Suspicious Shell Configuration File",
47 | Severity: 2,
48 | Tip: "Investigate file to determine validity.",
49 | Technique: "T1546.004",
50 | Metadata: tmp_,
51 | }
52 | patternMatch:
53 | for _, pattern := range suspiciousPatterns {
54 | if helpers.SearchStringContains(fileContents, pattern) {
55 | detection.Name = "Suspicious Pattern in Shell Configuration File"
56 | detection.Metadata["Pattern"] = pattern
57 | detections <- detection
58 | break patternMatch
59 | }
60 | }
61 | ipv4Match, _ := regexp.MatchString(ipv4Regex+`|`+ipv6Regex, fileContents)
62 | if ipv4Match {
63 | detection.Name = "IP Address Pattern in Shell Configuration File"
64 | detections <- detection
65 | continue
66 | }
67 | domainMatch, _ := regexp.MatchString(domainRegex, fileContents)
68 | if domainMatch {
69 | detection.Name = "Domain Pattern in Shell Configuration File"
70 | detections <- detection
71 | continue
72 | }
73 | if fileModificationTime != "NA" {
74 | dayDiff := int(timestampNow.Sub(fileStat.ModTime().UTC()).Hours() / 24)
75 | if dayDiff <= 30 {
76 | // File modified within last 30 days
77 | detection.Name = "Shell Config modified within last 30 days."
78 | detection.Metadata["DaysAgo"] = dayDiff
79 | detections <- detection
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/internal/findStartupServices.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "github.com/javanzato/crackdown/internal/helpers"
5 | "github.com/rs/zerolog"
6 | "io/fs"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 | )
11 |
12 | var serviceFilePaths = make([]string, 10)
13 |
14 | func CheckStartupServices(logger zerolog.Logger, detections chan<- Detection, waitGroup *WaitGroupCount) {
15 | // https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
16 | // ^ Good description of linux service confs
17 | defer waitGroup.Done()
18 | logger.Info().Msg("Finding System Services...")
19 | err := getServiceFiles(logger)
20 | if err != nil {
21 | logger.Error().Err(err)
22 | return
23 | }
24 | for _, file := range serviceFilePaths {
25 |
26 | if CheckFileIsScanned(file) {
27 | continue
28 | }
29 |
30 | fileStat, err := os.Stat(file)
31 | fileModificationTime := "NA"
32 | if err != nil {
33 | logger.Error().Err(err)
34 | } else {
35 | fileModificationTime = fileStat.ModTime().UTC().String()
36 | }
37 |
38 | tmp_ := map[string]interface{}{
39 | "File": strings.TrimSpace(file),
40 | "LastModified": fileModificationTime,
41 | }
42 | detection := Detection{
43 | Name: "",
44 | Severity: 0,
45 | Tip: "Verify validity of installed service/configuration file.",
46 | Technique: "T1543.002",
47 | Metadata: tmp_,
48 | }
49 | fileSlice := helpers.ReadFileToSlice(file, logger)
50 | result := false
51 | lineCheck:
52 | for _, line := range fileSlice {
53 | if strings.HasPrefix(line, "Exec") || strings.HasPrefix(line, "Environment") {
54 | lineData := strings.SplitN(line, "=", 2)
55 | detection.Metadata[lineData[0]] = line
56 | detection.Metadata["ConfigType"] = lineData[0]
57 | detection.Metadata["Line"] = line
58 |
59 | detection.Name = "Webshell Pattern in Service Configuration"
60 | result = checkWebshellContent(detection, detections, lineData[1])
61 | if result {
62 | break lineCheck
63 | }
64 | detection.Name = "Suspicious Pattern in Service Configuration"
65 | result = checkSuspiciousContent(detection, detections, lineData[1])
66 | if result {
67 | break lineCheck
68 | }
69 | detection.Name = "IP Address Pattern in Service Configuration"
70 | result = checkIPContent(detection, detections, lineData[1])
71 | if result {
72 | break lineCheck
73 | }
74 | detection.Name = "Domain Pattern in Service Configuration"
75 | result = checkDomainContent(detection, detections, lineData[1])
76 | if result {
77 | break lineCheck
78 | }
79 | }
80 | }
81 | if result == false && fileModificationTime != "NA" {
82 | // No detection yet on this file - check for recent modification
83 | dayDiff := int(timestampNow.Sub(fileStat.ModTime().UTC()).Hours() / 24)
84 | if dayDiff <= 30 {
85 | // File modified within last 30 days
86 | detection.Name = "Service File modified within last 30 days."
87 | detection.Metadata["DaysAgo"] = dayDiff
88 | detection.Severity = 2
89 | detections <- detection
90 | }
91 | }
92 | }
93 | }
94 |
95 | func getServiceFiles(logger zerolog.Logger) error {
96 | serviceDirs := []string{"/etc/systemd/system", "/etc/systemd/user", "/run/systemd/system", "/run/systemd/user", "/lib/systemd/system", "/lib/systemd/user"}
97 | for _, path := range serviceDirs {
98 | filepath.WalkDir(path, walk)
99 | }
100 | return nil
101 | }
102 |
103 | func walk(s string, d fs.DirEntry, err error) error {
104 | if err != nil {
105 | return err
106 | }
107 | if !d.IsDir() && (strings.HasSuffix(s, ".conf") || strings.HasSuffix(s, ".service")) {
108 | serviceFilePaths = append(serviceFilePaths, s)
109 | }
110 | return nil
111 | }
112 |
--------------------------------------------------------------------------------
/internal/findSuspiciousCommandline.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "github.com/javanzato/crackdown/internal/helpers"
7 | "github.com/mitchellh/go-ps"
8 | "github.com/rs/zerolog"
9 | "os"
10 | "os/exec"
11 | "strconv"
12 | "strings"
13 | )
14 |
15 | func FindSuspiciousCommandlines(logger zerolog.Logger, detections chan<- Detection, waitGroup *WaitGroupCount) {
16 | defer waitGroup.Done()
17 | logger.Info().Msg("Finding Suspicious Processes...")
18 | processList, err := ps.Processes()
19 | if err != nil {
20 | logger.Error().Err(err)
21 | return
22 | }
23 | // https://detection.fyi/sigmahq/sigma/linux/builtin/lnx_shell_susp_rev_shells/
24 | // TODO - Port Forwarding
25 | // TODO - Suspicious Executable Locations
26 | // TODO - Check for IP Address in commandline
27 |
28 | for x := range processList {
29 | var process ps.Process
30 | process = processList[x]
31 | cmdlineLocation := fmt.Sprintf("/proc/%d/cmdline", process.Pid())
32 | cmdline := helpers.ReadFileToString(cmdlineLocation, logger)
33 | fullCommandLine := fmt.Sprintf("%s %s", process.Executable(), cmdline)
34 | lastModified := "NA"
35 | fileExists := true
36 | exePath, err2 := GetExePathFromPID(process.Pid())
37 | var fileStat os.FileInfo
38 | if err2 == nil {
39 | if fileStat, err = os.Stat(exePath); err == nil {
40 | lastModified = fileStat.ModTime().UTC().String()
41 | } else if errors.Is(err, os.ErrNotExist) {
42 | fileExists = false
43 | } else {
44 | }
45 | }
46 |
47 | tmp_ := map[string]interface{}{
48 | "Commandline": fullCommandLine,
49 | "PID": strconv.Itoa(process.Pid()),
50 | "ExeLastModified": lastModified,
51 | "ExePath": exePath,
52 | }
53 | detection := Detection{
54 | Name: "Suspicious Commandline",
55 | Severity: 2,
56 | Tip: "Investigate process to determine validity.",
57 | Technique: "T1059",
58 | Metadata: tmp_,
59 | }
60 | if fileExists == false && !strings.HasSuffix(process.Executable(), "kworker/") {
61 | detection.Name = "Running Process with Non-Existent File"
62 | detection.Severity = 3
63 | detections <- detection
64 | }
65 | if fileExists == true && lastModified != "NA" {
66 | dayDiff := int(timestampNow.Sub(fileStat.ModTime().UTC()).Hours() / 24)
67 | if dayDiff <= 30 {
68 | detection.Name = "Running Binary Modified within Last 30 Days"
69 | detection.Metadata["DaysAgo"] = dayDiff
70 | detection.Severity = 2
71 | detections <- detection
72 | }
73 | }
74 |
75 | result := false
76 | detection.Name = "Suspicious Pattern in Commandline"
77 | result = checkSuspiciousContent(detection, detections, fullCommandLine)
78 | if result {
79 | continue
80 | }
81 | detection.Name = "IP Address Pattern in Commandline"
82 | result = checkIPContent(detection, detections, fullCommandLine)
83 | if result {
84 | continue
85 | }
86 | detection.Name = "Domain Pattern in Commandline"
87 | result = checkDomainContent(detection, detections, fullCommandLine)
88 | if result {
89 | continue
90 | }
91 |
92 | //logger.Printf("PID: %d, CMDLINE: %s", process.Pid(), fullCommandLine)
93 | }
94 | return
95 | }
96 |
97 | func GetExePathFromPID(pid int) (string, error) {
98 | command := fmt.Sprintf("ls -al /proc/%v/exe", pid)
99 | cmd := exec.Command(command)
100 | stdout, err := cmd.Output()
101 | if err != nil {
102 | return "", err
103 | }
104 | exePath := strings.SplitN(string(stdout), "exe ->", 2)[1]
105 | return exePath, nil
106 | }
107 |
--------------------------------------------------------------------------------
/internal/findSuspiciousConnections.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "github.com/bastjan/netstat"
5 | "github.com/javanzato/crackdown/internal/helpers"
6 | "github.com/rs/zerolog"
7 | "net"
8 | "strconv"
9 | "strings"
10 | )
11 |
12 | var suspiciousPorts = map[int]bool{
13 | 20: true,
14 | 21: true,
15 | 22: true,
16 | 23: true,
17 | 25: true,
18 | 53: true,
19 | 137: true,
20 | 139: true,
21 | 445: true,
22 | 3389: true,
23 | }
24 |
25 | func FindSuspiciousConnections(logger zerolog.Logger, detections chan<- Detection, waitGroup *WaitGroupCount) {
26 | defer waitGroup.Done()
27 | // TODO - Suspicious Ports
28 | // TODO - Suspicious Executable with Network Connection
29 | logger.Info().Msg("Finding Suspicious Connections...")
30 | connections, err := netstat.TCP.Connections()
31 | if err != nil {
32 | logger.Error().Err(err)
33 | return
34 | }
35 | skipIPs := map[string]bool{
36 | "0.0.0.0": true,
37 | "127.0.0.1": true,
38 | }
39 | suspiciousExeLocations := []string{
40 | "/home/games",
41 | "/home/lib",
42 | "/home/local",
43 | "/home/sbin",
44 | "/home/share",
45 | "/home/src",
46 | "/usr/",
47 | }
48 | appAllowList := []string{
49 | "usr/lib/firefox/firefox",
50 | }
51 | tenDot := net.IPNet{
52 | IP: net.ParseIP("10.0.0.0"),
53 | Mask: net.CIDRMask(8, 32),
54 | }
55 | sevenTwoDot := net.IPNet{
56 | IP: net.ParseIP("172.16.0.0"),
57 | Mask: net.CIDRMask(12, 32),
58 | }
59 | oneNineTwoDot := net.IPNet{
60 | IP: net.ParseIP("192.168.0.0"),
61 | Mask: net.CIDRMask(16, 32),
62 | }
63 |
64 | for _, v := range connections {
65 | if skipIPs[v.RemoteIP.String()] {
66 | continue
67 | }
68 | // Skip private-network connections
69 | if tenDot.Contains(v.RemoteIP) || sevenTwoDot.Contains(v.RemoteIP) || oneNineTwoDot.Contains(v.RemoteIP) {
70 | continue
71 | }
72 | tmp_ := map[string]interface{}{
73 | "RemoteAddress": v.RemoteIP.String(),
74 | "LocalPort": strconv.Itoa(v.Port),
75 | "RemotePort": strconv.Itoa(v.RemotePort),
76 | "Executable": v.Exe,
77 | "Commandline": strings.Join(v.Cmdline, " "),
78 | }
79 | detection := Detection{
80 | Name: "Executable in Home Directory with Network Connection",
81 | Severity: 2,
82 | Tip: "Investigate connection to determine validity.",
83 | Technique: "T1071",
84 | Metadata: tmp_,
85 | }
86 | allowMatch := false
87 | allowPattern:
88 | for _, loc := range appAllowList {
89 | if helpers.SearchStringContains(v.Exe, loc) {
90 | allowMatch = true
91 | break allowPattern
92 | }
93 | }
94 | if allowMatch {
95 | continue
96 | }
97 |
98 | if suspiciousPorts[v.Port] {
99 | detection.Name = "Connection on Suspicious Port"
100 | detections <- detection
101 | continue
102 | }
103 | patternMatch:
104 | for _, loc := range suspiciousExeLocations {
105 | if helpers.SearchStringContains(v.Exe, loc) {
106 | detection.Name = "Executable in abnormal directory with network connection"
107 | detection.Metadata["Pattern"] = loc
108 | detections <- detection
109 | break patternMatch
110 | }
111 | }
112 | //logger.Info(v)
113 | }
114 | return
115 | }
116 |
--------------------------------------------------------------------------------
/internal/findUserStartupScripts.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "github.com/javanzato/crackdown/internal/helpers"
5 | "github.com/rs/zerolog"
6 | "os"
7 | "path/filepath"
8 | "regexp"
9 | )
10 |
11 | func CheckUserStartupScripts(logger zerolog.Logger, detections chan<- Detection, waitGroup *WaitGroupCount) {
12 | defer waitGroup.Done()
13 | logger.Info().Msg("Checking User Startup Files...")
14 | files, err := filepath.Glob("/home/*/.*")
15 | if err != nil {
16 | logger.Error().Err(err)
17 | return
18 | }
19 | rootFiles, err2 := filepath.Glob("/root/.*")
20 | if err2 != nil {
21 | logger.Error().Err(err2)
22 | return
23 | }
24 | files = append(files, rootFiles...)
25 | for _, file := range files {
26 | if CheckFileIsScanned(file) {
27 | continue
28 | }
29 | if helpers.FileExists(file) == false {
30 | continue
31 | }
32 |
33 | // File Modification Check
34 | fileStat, err := os.Stat(file)
35 | fileModificationTime := "NA"
36 | if err != nil {
37 | logger.Error().Err(err)
38 | } else {
39 | fileModificationTime = fileStat.ModTime().UTC().String()
40 | }
41 | tmp_ := map[string]interface{}{
42 | "Modified": fileModificationTime,
43 | "File": file,
44 | }
45 | detection := Detection{
46 | Name: "Suspicious User Startup Script",
47 | Severity: 0,
48 | Tip: "Investigate file to determine validity.",
49 | Technique: "T1546",
50 | Metadata: tmp_,
51 | }
52 | fileSlice := helpers.ReadFileToSlice(file, logger)
53 | result := false
54 | lineCheck:
55 | for _, line := range fileSlice {
56 | result = checkScriptLine(logger, detection, detections, line)
57 | if result {
58 | break lineCheck
59 | }
60 |
61 | }
62 | if result == false && fileModificationTime != "NA" {
63 | dayDiff := int(timestampNow.Sub(fileStat.ModTime().UTC()).Hours() / 24)
64 | if dayDiff <= 30 {
65 | // File modified within last 30 days
66 | detection.Severity = 1
67 | detection.Name = "User Startup Script modified within last 30 days."
68 | detection.Metadata["DaysAgo"] = dayDiff
69 | detections <- detection
70 | }
71 | }
72 | }
73 | }
74 |
75 | func checkScriptLine(logger zerolog.Logger, detection Detection, detections chan<- Detection, lineContent string) bool {
76 | detection.Metadata["Line"] = lineContent
77 | for _, pattern := range suspiciousPatterns {
78 | if helpers.SearchStringContains(lineContent, pattern) {
79 | detection.Severity = 3
80 | detection.Name = "Suspicious Pattern in User Startup Script"
81 | detection.Metadata["Pattern"] = pattern
82 | detections <- detection
83 | return true
84 | }
85 | }
86 | ipv4Match, _ := regexp.MatchString(ipv4Regex+`|`+ipv6Regex, lineContent)
87 | if ipv4Match {
88 | detection.Severity = 2
89 | detection.Name = "IP Address Pattern in User Startup Script"
90 | detections <- detection
91 | return true
92 | }
93 | domainMatch, _ := regexp.MatchString(domainRegex, lineContent)
94 | if domainMatch {
95 | detection.Severity = 2
96 | detection.Name = "Domain Pattern in User Startup Script"
97 | detections <- detection
98 | return true
99 | }
100 | return false
101 | }
102 |
--------------------------------------------------------------------------------
/internal/findWebShells.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "github.com/javanzato/crackdown/internal/helpers"
5 | "github.com/rs/zerolog"
6 | "io/fs"
7 | "path/filepath"
8 | "strings"
9 | )
10 |
11 | // TODO - Scan /var/www/html, /etc/nginx, /etc/apache2 for suspicious files, recently created.
12 | var webshellFilePaths = make([]string, 10)
13 | var webshellExtensions = []string{
14 | ".aar",
15 | ".ascx",
16 | ".ashx",
17 | ".asmx",
18 | ".asp",
19 | ".aspx",
20 | ".cfm",
21 | ".cgi",
22 | ".js",
23 | ".jsp",
24 | ".jspx",
25 | ".php",
26 | ".jar",
27 | }
28 |
29 | func FindWebShells(logger zerolog.Logger, detections chan<- Detection, waitGroup *WaitGroupCount) {
30 | defer waitGroup.Done()
31 | logger.Info().Msg("Finding Web Shells...")
32 | getWebshellFiles(logger)
33 | for _, v := range webshellFilePaths {
34 | if v == "" {
35 | continue
36 | }
37 | lastModified := "NA"
38 | lastModifiedTimestamp, err := helpers.GetFileLastModified(v, logger)
39 | if err == nil {
40 | lastModified = lastModifiedTimestamp.String()
41 | }
42 |
43 | tmp_ := map[string]interface{}{
44 | "File": strings.TrimSpace(v),
45 | "LastModified": lastModified,
46 | }
47 | detection := Detection{
48 | Name: "",
49 | Severity: 2,
50 | Tip: "Verify validity of web-based file.",
51 | Technique: "T1505.003",
52 | Metadata: tmp_,
53 | }
54 | fileLines := helpers.ReadFileToSlice(v, logger)
55 | fileScan:
56 | for _, line := range fileLines {
57 | detection.Metadata["Line"] = line
58 |
59 | detection.Name = "Potential Webshell - Webshell Pattern in Content"
60 | result := checkWebshellContent(detection, detections, line)
61 | if result {
62 | break fileScan
63 | }
64 |
65 | detection.Name = "Potential Webshell - Suspicious Pattern in Content"
66 | result = checkSuspiciousContent(detection, detections, line)
67 | if result {
68 | break fileScan
69 | }
70 | detection.Name = "Potential Webshell - IP Address patterrn in Content"
71 | result = checkIPContent(detection, detections, line)
72 | if result {
73 | break fileScan
74 | }
75 | detection.Name = "Potential Webshell - Domain pattern in Content"
76 | result = checkDomainContent(detection, detections, line)
77 | if result {
78 | break fileScan
79 | }
80 | }
81 | }
82 | }
83 |
84 | func getWebshellFiles(logger zerolog.Logger) {
85 | webshellDirs := []string{
86 | "/var/www/html",
87 | "/etc/nginx",
88 | "/etc/apache*",
89 | }
90 | for _, path := range webshellDirs {
91 | filepath.WalkDir(path, walkWebShells)
92 | }
93 | }
94 |
95 | func walkWebShells(s string, d fs.DirEntry, err error) error {
96 | if err != nil {
97 | return err
98 | }
99 | if !d.IsDir() && helpers.SliceContains(webshellExtensions, s) {
100 | webshellFilePaths = append(webshellFilePaths, s)
101 | }
102 | return nil
103 | }
104 |
--------------------------------------------------------------------------------
/internal/helpers/fileSystemHelpers.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "bufio"
5 | "github.com/rs/zerolog"
6 | "io"
7 | "os"
8 | "strings"
9 | "time"
10 | )
11 |
12 | func FileExists(filename string) bool {
13 | _, err := os.Stat(filename)
14 | if os.IsNotExist(err) {
15 | return false
16 | }
17 | return true
18 | }
19 |
20 | func ReadFileToSlice(filename string, logger zerolog.Logger) []string {
21 | file, err := os.Open(filename)
22 | defer file.Close()
23 | if err != nil {
24 | logger.Error().Err(err)
25 | return make([]string, 0)
26 | }
27 | reader := bufio.NewReader(file)
28 | var lines []string
29 | for {
30 | line, err := reader.ReadString('\n')
31 | lines = append(lines, strings.TrimSpace(line))
32 | if err == io.EOF {
33 | break
34 | }
35 | if err != nil {
36 | logger.Error().Err(err)
37 | return make([]string, 0)
38 | }
39 | }
40 | return lines
41 | //reader := bufio.NewReader(file)
42 | }
43 |
44 | func ReadFileToString(filename string, logger zerolog.Logger) string {
45 | file, err := os.Open(filename)
46 | defer file.Close()
47 | if err != nil {
48 | logger.Error().Err(err)
49 | return ""
50 | }
51 | reader := bufio.NewReader(file)
52 | var lines string
53 | for {
54 | line, err := reader.ReadString('\n')
55 | lines += strings.TrimSpace(line)
56 | if err == io.EOF {
57 | break
58 | }
59 | if err != nil {
60 | logger.Error().Err(err)
61 | return ""
62 | }
63 | }
64 | return lines
65 | //reader := bufio.NewReader(file)
66 | }
67 |
68 | func GetFileLastModified(filename string, logger zerolog.Logger) (time.Time, error) {
69 | fileStat, err := os.Stat(filename)
70 | if err != nil {
71 | logger.Error().Err(err)
72 | return time.Now(), err
73 | } else {
74 | lastModtime := fileStat.ModTime().UTC()
75 | return lastModtime, nil
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/internal/helpers/stringHelpers.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "golang.org/x/text/language"
5 | "golang.org/x/text/search"
6 | )
7 |
8 | func SearchStringContains(str string, substr string) bool {
9 | m := search.New(language.English, search.IgnoreCase)
10 | start, _ := m.IndexString(str, substr)
11 | if start == -1 {
12 | return false
13 | }
14 | return true
15 | }
16 |
17 | func SliceContains(s []string, str string) bool {
18 | for _, v := range s {
19 | if v == str {
20 | return true
21 | }
22 | }
23 | return false
24 | }
25 |
--------------------------------------------------------------------------------
/internal/internalTypes.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/javanzato/crackdown/internal/helpers"
7 | "github.com/rs/zerolog"
8 | "reflect"
9 | "regexp"
10 | "sort"
11 | "sync"
12 | "sync/atomic"
13 | )
14 |
15 | type Detection struct {
16 | Name string
17 | Severity int
18 | Tip string
19 | Technique string
20 | Metadata map[string]interface{}
21 | }
22 |
23 | func (d Detection) MarshalZerologObject(e *zerolog.Event) {
24 | e.Str("name", d.Name).
25 | Int("severity", d.Severity).
26 | Str("tip", d.Tip).
27 | Str("technique", d.Technique).
28 | Fields(d.Metadata)
29 | }
30 |
31 | func (d Detection) MetaToJSON() string {
32 | if len(d.Metadata) != 0 {
33 | content, err := json.MarshalIndent(d.Metadata, "", "")
34 | if err != nil {
35 | return "{}"
36 | }
37 | return string(content)
38 | } else {
39 | return "{}"
40 | }
41 | }
42 |
43 | func (d Detection) MetaToPairs(separator string) string {
44 | if len(d.Metadata) != 0 {
45 | baseString := ""
46 | for k, v := range d.Metadata {
47 | baseString += fmt.Sprintf("%s: %v %v ", k, v, separator)
48 | }
49 | return baseString
50 | } else {
51 | return ""
52 | }
53 | }
54 |
55 | func (d Detection) String() string {
56 | // Format the string of a detection to properly iterate over the Metadata when rendering
57 | // TODO - Ths second loop after sort for some reason is introducing empty keys to the array - not sure why.
58 | // - I think the above is because we are not trimming /n - now we are doing this properly in each string read from files.
59 | var base string
60 | dv := reflect.ValueOf(d)
61 | typeOfS := dv.Type()
62 | for i := 0; i < dv.NumField(); i++ {
63 | k := typeOfS.Field(i).Name
64 | v := dv.Field(i).Interface()
65 | if k == "Metadata" {
66 | base += "Metadata: "
67 |
68 | keys := make([]string, len(d.Metadata))
69 | for j, _ := range d.Metadata {
70 | if j != "" {
71 | keys = append(keys, j)
72 | }
73 | }
74 | sort.Strings(keys)
75 |
76 | for _, jj := range keys {
77 | if jj != "" {
78 | base += fmt.Sprintf("%s: %s, ", jj, d.Metadata[jj])
79 | }
80 | }
81 |
82 | /*for kk, vv := range d.Metadata {
83 | base += fmt.Sprintf("%s: %s, ", kk, vv)
84 | }*/
85 | } else if k == "Severity" {
86 | base += fmt.Sprintf("%s: %d, ", k, v)
87 | } else {
88 | base += fmt.Sprintf("%s: %s, ", k, v)
89 | }
90 | }
91 | return base
92 | }
93 |
94 | func checkDomainContent(detection Detection, detections chan<- Detection, lineContent string) bool {
95 | domainMatch, _ := regexp.MatchString(domainRegex, lineContent)
96 | if domainMatch {
97 | detections <- detection
98 | return true
99 | }
100 | return false
101 | }
102 |
103 | func checkSuspiciousContent(detection Detection, detections chan<- Detection, lineContent string) bool {
104 | for _, pattern := range suspiciousPatterns {
105 | if helpers.SearchStringContains(lineContent, pattern) {
106 | detection.Metadata["Pattern"] = pattern
107 | detections <- detection
108 | return true
109 | }
110 | }
111 | return false
112 | }
113 |
114 | func checkWebshellContent(detection Detection, detections chan<- Detection, lineContent string) bool {
115 | for _, pattern := range webshellIndicatorStrings {
116 | if helpers.SearchStringContains(lineContent, pattern) {
117 | detection.Metadata["Pattern"] = pattern
118 | detections <- detection
119 | return true
120 | }
121 | }
122 | return false
123 | }
124 |
125 | func checkIPContent(detection Detection, detections chan<- Detection, lineContent string) bool {
126 | ipv4Match, _ := regexp.MatchString(ipv4Regex+`|`+ipv6Regex, lineContent)
127 | if ipv4Match {
128 | detections <- detection
129 | return true
130 | }
131 | return false
132 | }
133 |
134 | type WaitGroupCount struct {
135 | sync.WaitGroup
136 | count int64
137 | }
138 |
139 | func (wg *WaitGroupCount) Add(delta int) {
140 | atomic.AddInt64(&wg.count, int64(delta))
141 | wg.WaitGroup.Add(delta)
142 | }
143 |
144 | func (wg *WaitGroupCount) Done() {
145 | atomic.AddInt64(&wg.count, -1)
146 | wg.WaitGroup.Done()
147 | }
148 |
149 | func (wg *WaitGroupCount) GetCount() int {
150 | return int(atomic.LoadInt64(&wg.count))
151 | }
152 |
153 | var filesScannedGlobal = make([]string, 10)
154 |
155 | func CheckFileIsScanned(filename string) bool {
156 | if helpers.SliceContains(filesScannedGlobal, filename) {
157 | return true
158 | } else {
159 | filesScannedGlobal = append(filesScannedGlobal, filename)
160 | return false
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/internal/internalVars.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import "time"
4 |
5 | var webshellIndicatorStrings = []string{
6 | "WebHandler Language=\"C#\" class=",
7 | "<%response.clear:execute request",
8 | "%Response.Write(eval(Request.Item",
9 | "= new Process();",
10 | ".CreateCommand();",
11 | "StartInfo.FileName = \"cmd.exe\";",
12 | "StartInfo.RedirectStandardOutput = true;",
13 | "-executionpolicy bypass",
14 | " new ProcessStartInfo();",
15 | ".StartInfo.UseShellExecute",
16 | "GetLogicalDrives()",
17 | "System.IO.DirectoryInfo",
18 | "克隆文件的最后修改时间",
19 | ".ServiceProcess.ServiceController.GetServices",
20 | "Exec Cmd",
21 | "HKEY_LOCAL_MACHINE",
22 | "HKEY_CLASSES_ROOT",
23 | "HKEY_CURRENT_USER",
24 | "HKLM",
25 | "HKCR",
26 | "HKCU",
27 | "runat=\"server\"",
28 | "new DeflateStream(",
29 | "New-Object IO.StreamReader",
30 | ".BinaryWrite",
31 | "Scripting.FileSystemObject",
32 | ", \"rb\")",
33 | " popen(",
34 | "echo <<< EOF",
35 | "PF_INET, SOCK_STREAM",
36 | "exec(",
37 | "shell_exec",
38 | "echo system(",
39 | ".getRuntime().exec(",
40 | "getOutputStream()",
41 | "cmd.exe",
42 | "powershell.exe",
43 | "wscript.exe",
44 | "cscript.exe",
45 | ";eval(",
46 | "new ActiveXObject(",
47 | ".StdOut.ReadAll()",
48 | }
49 |
50 | var suspiciousPatterns = []string{
51 | "BEGIN {s = \"/inet/tcp/0/",
52 | "bash -i >& /dev/tcp/",
53 | "bash -i >& /dev/udp/",
54 | "sh -i >$ /dev/udp/",
55 | "sh -i >$ /dev/tcp/",
56 | "&& while read line 0<&5; do",
57 | "/bin/bash -c exec 5<>/dev/tcp/",
58 | "/bin/bash -c exec 5<>/dev/udp/",
59 | "nc -e /bin/sh ",
60 | "/bin/sh | nc",
61 | "rm -f backpipe; mknod /tmp/backpipe p && nc ",
62 | ";socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in($p,inet_aton($i))))",
63 | ";STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;",
64 | "/bin/sh -i <&3 >&3 2>&3",
65 | "uname -a; w; id; /bin/bash -i",
66 | "([text.encoding]::ASCII).GetBytes",
67 | "$stream.Flush()",
68 | "exec sprintf(\"/bin/sh -i <&%d >&%d 2>&%d\",f,f,f)",
69 | "while(cmd=c.gets);IO.popen(cmd,\"r\"){|io|c.print",
70 | "socat exec:''bash -li'',pty,stderr,setsid,sigint,sane tcp",
71 | "rm -f /tmp/p; mknod /tmp/p p &&",
72 | "/bin/bash | telnet",
73 | "echo=0,raw tcp-listen:",
74 | "nc -lvvp",
75 | "xterm -display 1",
76 | "ncat",
77 | "alias sudo",
78 | "https://",
79 | "/bin/sh -c",
80 | "bash -i >&",
81 | "$(dig",
82 | "/etc/shadow",
83 | "/etc/passwd",
84 | }
85 |
86 | var timestampNow = time.Now()
87 |
88 | var ipv6Regex = `^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$`
89 | var ipv4Regex = `^(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4})`
90 | var domainRegex = `^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$`
91 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // TODO - Startup Files ~/.config/autostart/
4 | // https://arcolinux.com/how-to-autostart-any-application-on-any-linux-desktop/
5 | // TODO - Driver checks - covered by kernel checks?
6 | // TODO - apt.conf.d directory for recent files/suspicious patterns
7 | // TODO - Check ~/.gitconfig for suspicious patterns/modifications
8 | // TODO - Check .git/hooks for suspicious patterns/recent files
9 | // TODO - Inspect SUDOers file for NOPASSWD Entries
10 | // https://research.splunk.com/endpoint/ab1e0d52-624a-11ec-8e0b-acde48001122/
11 | // https://askubuntu.com/questions/334318/sudoers-file-enable-nopasswd-for-user-all-commands
12 | // TODO - Find better way to abstract generic content checking for suspicious-ness - to much re-use of same logic
13 |
14 | import (
15 | "encoding/csv"
16 | "encoding/json"
17 | "flag"
18 | "fmt"
19 | "github.com/javanzato/crackdown/internal"
20 | "github.com/rs/zerolog"
21 | "io"
22 | "os"
23 | "strings"
24 | "time"
25 | )
26 |
27 | const logFile = "crackdown.log"
28 |
29 | var severityMap = map[int]string{
30 | 0: "INFO",
31 | 1: "LOW",
32 | 2: "MEDIUM",
33 | 3: "HIGH",
34 | 4: "CRITICAL",
35 | }
36 |
37 | type anyMap map[string]interface{}
38 |
39 | func setupLogger() zerolog.Logger {
40 | logFileName := logFile
41 | logFile, err := os.OpenFile(logFileName, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
42 | if err != nil {
43 | _, err := fmt.Fprintf(os.Stderr, "Couldn't Initialize Log File: %s", err)
44 | if err != nil {
45 | panic(nil)
46 | }
47 | panic(err)
48 | }
49 | cw := zerolog.ConsoleWriter{
50 | Out: os.Stdout,
51 | TimeFormat: time.RFC3339,
52 | FormatLevel: func(i interface{}) string {
53 | return strings.ToUpper(fmt.Sprintf("[%s]", i))
54 | },
55 | }
56 | mw := io.MultiWriter(cw, logFile)
57 | logger := zerolog.New(mw).Level(zerolog.TraceLevel)
58 | logger = logger.With().Timestamp().Logger()
59 | return logger
60 | }
61 |
62 | func printLogo() {
63 | fmt.Println(` __ __ `)
64 | fmt.Println(` ______________ ______/ /______/ /___ _ ______ `)
65 | fmt.Println(" / ___/ ___/ __ '/ ___/ //_/ __ / __ \\ | /| / / __ \\")
66 | fmt.Println(` / /__/ / / /_/ / /__/ ,< / /_/ / /_/ / |/ |/ / / / /`)
67 | fmt.Println(` \___/_/ \__,_/\___/_/|_|\__,_/\____/|__/|__/_/ /_/ `)
68 | fmt.Println(" crackdown: Linux Persistence Hunting")
69 | fmt.Println(" github.com/joeavanzato/crackdown")
70 | }
71 |
72 | func listenDetections(logger zerolog.Logger, c chan internal.Detection) ([]internal.Detection, int) {
73 | detectionCount := 0
74 | total := 0
75 | detections := make([]internal.Detection, 20)
76 | detectionListen:
77 | for {
78 | detection, ok := <-c
79 | if !ok {
80 | break detectionListen
81 | } else {
82 | detections = append(detections, detection)
83 | detectionCount += 1
84 | total += 1
85 | }
86 | if total%20 == 0 {
87 | total = 0
88 | curMsg := fmt.Sprintf("Waiting for Detections: %v", detectionCount)
89 | logger.Info().Msg(curMsg)
90 | }
91 | }
92 | return detections, detectionCount
93 | }
94 |
95 | func closeChannelWhenDone(c chan internal.Detection, waitGroup *internal.WaitGroupCount) {
96 | waitGroup.Wait()
97 | close(c)
98 | }
99 |
100 | func parseArgs(logger zerolog.Logger) anyMap {
101 | quiet := flag.Bool("quiet", false, "Suppress most Console Logging")
102 |
103 | flag.Parse()
104 | arguments := anyMap{
105 | "quiet": *quiet,
106 | }
107 | return arguments
108 | }
109 |
110 | func writeJSONOut(logger zerolog.Logger, detections []internal.Detection, detectionCount int) {
111 | detections = detections[len(detections)-detectionCount:]
112 | content, err := json.MarshalIndent(detections, "", "\t")
113 | if err != nil {
114 | logger.Error().Msg(err.Error())
115 | }
116 | f, err := os.Create("detections.json")
117 | if err != nil {
118 | panic(err)
119 | }
120 | _, err = f.Write(content)
121 | if err != nil {
122 | logger.Error().Msg(err.Error())
123 | }
124 | }
125 |
126 | func writeCSVOut(logger zerolog.Logger, detections []internal.Detection, detectionCount int) {
127 | detections = detections[len(detections)-detectionCount:]
128 | f, err := os.Create("detections.csv")
129 | if err != nil {
130 | panic(err)
131 | }
132 | headers := []string{
133 | "Name",
134 | "Severity",
135 | "Tip",
136 | "Technique",
137 | "Metadata",
138 | }
139 | w := csv.NewWriter(f)
140 | err = w.Write(headers)
141 | if err != nil {
142 | logger.Error().Msg(err.Error())
143 | return
144 | }
145 | for i := 0; i < detectionCount; i++ {
146 | v := detections[i]
147 | strSlice := []string{
148 | v.Name,
149 | severityMap[v.Severity],
150 | v.Tip,
151 | v.Technique,
152 | v.MetaToJSON(),
153 | //v.MetaToPairs("|||"),
154 | }
155 | //fmt.Println(i)
156 | //fmt.Println(strSlice)
157 | err := w.Write(strSlice)
158 | if err != nil {
159 | fmt.Println(err)
160 | }
161 | }
162 | }
163 |
164 | func main() {
165 | logger := setupLogger()
166 | arguments := parseArgs(logger)
167 | printLogo()
168 | // Channel Initial Allocation necessary?
169 | receiveDetections := make(chan internal.Detection)
170 | var waitGroup internal.WaitGroupCount
171 | waitGroup.Add(12)
172 | go internal.FindLocalUsers(logger, receiveDetections, &waitGroup)
173 | go internal.FindCronJobs(logger, receiveDetections, &waitGroup)
174 | go internal.FindSuspiciousCommandlines(logger, receiveDetections, &waitGroup)
175 | go internal.FindSuspiciousConnections(logger, receiveDetections, &waitGroup)
176 | go internal.FindSSHAuthorizedKeys(logger, receiveDetections, &waitGroup)
177 | go internal.FindKernelModules(logger, receiveDetections, &waitGroup)
178 | go internal.CheckShellConfigs(logger, receiveDetections, &waitGroup)
179 | go internal.CheckStartupServices(logger, receiveDetections, &waitGroup)
180 | go internal.CheckUserStartupScripts(logger, receiveDetections, &waitGroup)
181 | go internal.CheckCommonBackdoors(logger, receiveDetections, &waitGroup)
182 | go internal.CheckEnvironmentVariables(logger, receiveDetections, &waitGroup)
183 | go internal.FindWebShells(logger, receiveDetections, &waitGroup)
184 | go closeChannelWhenDone(receiveDetections, &waitGroup)
185 | detections, detectionCount := listenDetections(logger, receiveDetections)
186 | logger.Info().Msgf("Detection Count: %d", detectionCount)
187 | if arguments["quiet"] == false {
188 | for _, i := range detections {
189 | if i.Metadata != nil {
190 | // The slice might be longer than the 'real' elements present.
191 | logger.Info().
192 | Str(" Name", i.Name).
193 | Str(" Severity", severityMap[i.Severity]).
194 | Str("Tip", i.Tip).
195 | Fields(i.Metadata).Msg("")
196 | }
197 | }
198 | }
199 | writeJSONOut(logger, detections, detectionCount)
200 | writeCSVOut(logger, detections, detectionCount)
201 | }
202 |
--------------------------------------------------------------------------------