├── .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 | --------------------------------------------------------------------------------