diff --git a/.gitignore b/.gitignore index b6d5261..9fe8afd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +#ide stuff +*.idea +.idea/ + # Binaries for programs and plugins *.exe *.exe~ diff --git a/embed/linux/triage/firewall.sh b/embed/linux/triage/firewall.sh old mode 100644 new mode 100755 diff --git a/triage/domain_info_linux.go b/triage/domain_info_linux.go new file mode 100644 index 0000000..f9cc735 --- /dev/null +++ b/triage/domain_info_linux.go @@ -0,0 +1,25 @@ +package triage + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +func getDomainInfo() string { + // Check 1: sssd.conf + content, err := os.ReadFile("/etc/sssd/sssd.conf") + if err == nil { + fmt.Println("sssd.conf") + return strings.TrimSpace(string(content)) + "\t" + } + + // Check 2: realm list (if available) + out, err := exec.Command("realm", "list").Output() + if err == nil && len(out) > 0 { + return strings.TrimSpace(string(out)) + "\t" + } + + return "Not Domain Jointed" + "\t" +} diff --git a/triage/domain_info_windows.go b/triage/domain_info_windows.go new file mode 100644 index 0000000..06dd70e --- /dev/null +++ b/triage/domain_info_windows.go @@ -0,0 +1,27 @@ +package triage + +import ( + "os/exec" + "strings" +) + +func getDomainInfo() string { + out, err := exec.Command("wmic", "computersystem", "get", "domain").Output() + if err != nil { + return "Not Domain Joined" + "\t" + } + + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || line == "Domain" { + continue + } + if line == "WORKGROUP" { + return "Not Domain Joined" + "\t" + } + return line + "\t" + } + + return "Not Domain Joined" + "\t" +} diff --git a/triage/firewall_linux.go b/triage/firewall_linux.go index c823f88..162711c 100644 --- a/triage/firewall_linux.go +++ b/triage/firewall_linux.go @@ -1,7 +1,22 @@ package triage -import "github.com/UT-CTF/landschaft/util" +import ( + "strings" -func runFirewallTriage() { - util.RunAndPrintScript("triage/firewall.sh") + "github.com/UT-CTF/landschaft/util" +) + +func runFirewallTriage() string { + result := util.RunAndPrintScript("triage/firewall.sh") + result = strings.ReplaceAll(result, "\n", " ") + result = strings.ReplaceAll(result, "\r", "") + result = strings.Join(strings.Fields(result), " ") + result = strings.ReplaceAll(result, "=", "") + result += "\t" + + if strings.Contains(result, "No supported firewall") { + result = "No Firewall!\t" + } + + return result } diff --git a/triage/firewall_windows.go b/triage/firewall_windows.go index 86d78f7..b63c855 100644 --- a/triage/firewall_windows.go +++ b/triage/firewall_windows.go @@ -1,9 +1,96 @@ package triage import ( + "fmt" + "strings" + "github.com/UT-CTF/landschaft/util" ) -func runFirewallTriage() { - util.RunAndPrintScript("triage/firewall.ps1") +func runFirewallTriage() string { + result := parseFirewall(util.RunAndPrintScript("triage/firewall.ps1")) + "\t" + return result +} + +func parseFirewall(result string) string { + lines := strings.Split(result, "\n") + + type iface struct { + name string + alias string + category string + } + + var interfaces []iface + profileEnabled := make(map[string]bool) + profileState := make(map[string]string) + profilePolicy := make(map[string]string) + currentProfile := "" + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + + if strings.HasPrefix(trimmed, "Name") || strings.HasPrefix(trimmed, "----") || trimmed == "" || strings.HasPrefix(trimmed, "Interfaces:") { + continue + } + + fields := strings.Fields(trimmed) + + if strings.HasPrefix(trimmed, "Profile:") { + parts := strings.Split(trimmed, " - ") + profileName := strings.TrimSpace(strings.TrimPrefix(parts[0], "Profile:")) + if len(parts) > 1 { + profileEnabled[profileName] = strings.TrimSpace(parts[1]) == "Enabled" + } + continue + } + + if strings.HasSuffix(trimmed, "Profile Settings:") { + currentProfile = strings.TrimSuffix(trimmed, " Profile Settings:") + continue + } + + if currentProfile != "" { + if strings.HasPrefix(trimmed, "State") { + profileState[currentProfile] = strings.TrimSpace(strings.TrimPrefix(trimmed, "State")) + continue + } + if strings.HasPrefix(trimmed, "Firewall Policy") { + profilePolicy[currentProfile] = strings.TrimSpace(strings.TrimPrefix(trimmed, "Firewall Policy")) + continue + } + } + + // interface line - last field is category, second to last is alias, rest is name + if len(fields) >= 3 { + category := fields[len(fields)-1] + alias := fields[len(fields)-2] + name := strings.Join(fields[:len(fields)-2], " ") + interfaces = append(interfaces, iface{name, alias, category}) + } + } + + var parts []string + for _, i := range interfaces { + enabled := "Disabled" + check := false + if profileEnabled[i.category] { + enabled = "Enabled" + check = true + } + state := profileState[i.category] + policy := profilePolicy[i.category] + + if check { + parts = append(parts, fmt.Sprintf("%s - %s \n\t%s - %s \n\tState %s \n\tFirewall Policy: %s", + i.name, i.alias, i.category, enabled, state, policy)) + } else { + parts = append(parts, fmt.Sprintf("%s - %s \n\t%s - %s", + i.name, i.alias, i.category, enabled)) + } + + } + + return "\"" + strings.Join(parts, "\n\n ") + "\"" + } diff --git a/triage/main.go b/triage/main.go index 79c46b4..89d59e9 100644 --- a/triage/main.go +++ b/triage/main.go @@ -2,17 +2,74 @@ package triage import ( "fmt" + "os" + "path/filepath" + "runtime" + "strings" "github.com/UT-CTF/landschaft/util" ) func Run() { + file, err := os.Create("triage.tsv") + if err != nil { + fmt.Println("Error creating csv file:", err) + } + fmt.Println(util.TitleColor.Render("Network")) - runNetworkTriage() - fmt.Println(util.TitleColor.Render("Users & Groups")) - runUsersTriage() + + hostname, csv := runNetworkTriage() + fmt.Println(util.TitleColor.Render("OS Version")) - runOSVersionTriage() + + if _, err := file.Write([]byte(hostname + "\t" + runOSVersionTriage() + "\t" + csv)); err != nil { + // ignore error + } + + fmt.Println(util.TitleColor.Render("Users & Groups")) + + if _, err := file.Write([]byte(runUsersTriage())); err != nil { + // ignore error + } + fmt.Println(util.TitleColor.Render("Firewall")) - runFirewallTriage() + + if _, err := file.Write([]byte(runFirewallTriage())); err != nil { + // ignore error + } + + if _, err := file.Write([]byte(getDomainInfo())); err != nil { + // ignore error + } + + errF := file.Close() + if errF != nil { + + } + + printCopyInstructions() + +} + +func printCopyInstructions() { + sshConn := os.Getenv("SSH_CONNECTION") + fields := strings.Fields(sshConn) + serverUser := os.Getenv("USER") + serverIP := "" + if len(fields) >= 3 { + serverIP = fields[2] + } + + exePath, _ := os.Executable() + tsvPath := filepath.Join(filepath.Dir(exePath), "triage.tsv") + + catCmd := "cat" + if runtime.GOOS == "windows" { + catCmd = "type" + serverUser = os.Getenv("USERNAME") + } + + fmt.Println("To copy to clipboard:") + fmt.Printf("\tIf your host is linux: ssh %s@%s \"%s %s\" | xclip -selection clipboard\n\n", serverUser, serverIP, catCmd, tsvPath) + fmt.Printf("\tIf your host is windows: ssh %s@%s \"%s %s\" | clip.exe\n\n\n", serverUser, serverIP, catCmd, tsvPath) } diff --git a/triage/network_info.go b/triage/network_info.go index 12d92f2..71d1bbc 100644 --- a/triage/network_info.go +++ b/triage/network_info.go @@ -9,12 +9,13 @@ import ( "github.com/charmbracelet/log" ) -func runNetworkTriage() { - var hostname = getAndPrintHostname() - printDNSName(hostname) - printIPAddrs() - printNetstat() +func runNetworkTriage() (string, string) { + hostname := getAndPrintHostname() + csv := printDNSName(hostname) + "\t" + csv = csv + printIPAddrs() + "\t" + csv = csv + printNetstat() fmt.Println() + return hostname, csv } func getAndPrintHostname() string { @@ -27,31 +28,40 @@ func getAndPrintHostname() string { return hostname } -func printDNSName(hostname string) { +func printDNSName(hostname string) string { + var result string addrs, err := net.LookupHost(hostname) if err != nil { fmt.Println("error looking up hostname") - return + return "err" } + var check = false for _, addr := range addrs { names, err := net.LookupAddr(addr) if err != nil { - return + return "N/A" } for _, name := range names { - if name == "localhost" { + if name == "localhost" || addr == "127.0.1.1" || addr == "127.0.0.1" { continue } fmt.Printf("FQDN for %s: %s\n", addr, name) + result = result + addr + ": " + name + "\n" + check = true } } + if !check { + return "N/A" + } + return "\"" + result + "\"" } -func printIPAddrs() { +func printIPAddrs() string { + var result string var interfaces, err = net.Interfaces() if err != nil { log.Error("Failed to get network interfaces", "err", err) - return + return "err" } for _, iface := range interfaces { // skip loopback address @@ -62,14 +72,14 @@ func printIPAddrs() { var addrs, ipErr = iface.Addrs() if ipErr != nil { log.Error("Failed to get IP addresses", "err", ipErr) - return + return "err" } var ipv4Addrs, ipv6Addrs []string for _, addr := range addrs { ipNet, ok := addr.(*net.IPNet) if !ok { log.Error("Failed to cast address to IPNet", "addr", addr) - return + return "err" } if ipNet.IP.To4() != nil { ipv4Addrs = append(ipv4Addrs, ipNet.IP.String()) @@ -77,61 +87,80 @@ func printIPAddrs() { ipv6Addrs = append(ipv6Addrs, ipNet.IP.String()) } } - printAddrs(ipv4Addrs, " IPv4 Addresses:") + temp := printAddrs(ipv4Addrs, " IPv4 Addresses:") + "\n\n" + if len(temp) > 2 { + result += iface.Name + temp + } + //result = result + "\t" + printAddrs(ipv6Addrs, " IPv6 Addresses:") printAddrs(ipv6Addrs, " IPv6 Addresses:") } + return "\"" + result + "\"" } -func printAddrs(list []string, msg string) { +func printAddrs(list []string, msg string) string { + var result string if len(list) > 0 { fmt.Println(" ", msg) for _, ip := range list { fmt.Println(" -", ip) + result += fmt.Sprintf("\n\t%s", ip) } } + return result } -func printSockets(title string, sockets []netstat.SockTabEntry) { +func printSockets(title string, sockets []netstat.SockTabEntry) string { + var result = "" if len(sockets) > 0 { - fmt.Println(title) + fmt.Print(title) for _, e := range sockets { if e.State.String() == "LISTEN" && !e.LocalAddr.IP.IsLoopback() { fmt.Printf("%s %s %d %s\n", e.LocalAddr.String(), e.State.String(), e.UID, e.Process) + result += fmt.Sprintf("%d\t%s\n", e.LocalAddr.Port, e.Process) } } } + + if len(result) == 0 { + result = "NONE" + } + + return result } -func printNetstat() { +func printNetstat() string { + var result string // Get TCP IPv4 sockets tcpSocks, err := netstat.TCPSocks(netstat.NoopFilter) if err != nil { fmt.Print(err) - return + result += "err" + } else { + result += "## TCP ##\n" + printSockets("\nTCP IPv4 Sockets:", tcpSocks) } - printSockets("\nTCP IPv4 Sockets:", tcpSocks) - // Get UDP IPv4 sockets udpSocks, err := netstat.UDPSocks(netstat.NoopFilter) if err != nil { fmt.Print(err) - return + result += "err" + } else { + result += "\n## UDP ##\n" + printSockets("\nUDP IPv4 Sockets:", udpSocks) } - printSockets("\nUDP IPv4 Sockets:", udpSocks) - // Get TCP IPv6 sockets tcp6Socks, err := netstat.TCP6Socks(netstat.NoopFilter) if err != nil { fmt.Print(err) - return + //result += "err" + } else { + printSockets("\nTCP IPv6 Sockets:", tcp6Socks) } - printSockets("\nTCP IPv6 Sockets:", tcp6Socks) - // Get UDP IPv6 sockets udp6Socks, err := netstat.UDP6Socks(netstat.NoopFilter) if err != nil { fmt.Print(err) - return + //result += "err" + } else { + printSockets("\nUDP IPv6 Sockets:", udp6Socks) } - printSockets("\nUDP IPv6 Sockets:", udp6Socks) + return "\"" + result + "\"\t" } diff --git a/triage/os_version_linux.go b/triage/os_version_linux.go index f3172f0..88aced8 100644 --- a/triage/os_version_linux.go +++ b/triage/os_version_linux.go @@ -7,22 +7,26 @@ import ( "strings" ) -func runOSVersionTriage() { +func runOSVersionTriage() string { file, err := os.Open("/etc/os-release") if err != nil { fmt.Println("Error reading OS release:", err) - return + return "err" } defer file.Close() + var result string scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() parts := strings.Split(line, "=") if parts[0] == "NAME" { + result += strings.Trim(parts[1], `"`) + "\n" fmt.Println("OS:", strings.Trim(parts[1], `"`)) } else if parts[0] == "VERSION" { fmt.Println("Version:", strings.Trim(parts[1], `"`)) + result += strings.Trim(parts[1], `"`) } } + return "\"" + result + "\"" } diff --git a/triage/os_version_windows.go b/triage/os_version_windows.go index 6b49f1a..14a6782 100644 --- a/triage/os_version_windows.go +++ b/triage/os_version_windows.go @@ -1,9 +1,33 @@ package triage import ( + "fmt" + "strings" + "github.com/UT-CTF/landschaft/util" ) -func runOSVersionTriage() { - util.RunAndPrintScript("triage/os_version.ps1") +func runOSVersionTriage() string { + return parseOSVersion(util.RunAndPrintScript("triage/os_version.ps1")) +} + +func parseOSVersion(result string) string { + lines := strings.Split(result, "\n") + var osName, osVersion, osConf string + + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "OS Name:") { + osName = strings.TrimSpace(strings.TrimPrefix(line, "OS Name:")) + } + if strings.HasPrefix(line, "OS Version:") { + osVersion = strings.TrimSpace(strings.TrimPrefix(line, "OS Version:")) + } + + if strings.HasPrefix(line, "OS Configuration:") { + osConf = strings.TrimSpace(strings.TrimPrefix(line, "OS Configuration:")) + } + } + + return fmt.Sprintf("\"%s\n%s\n\n%s\"", osName, osVersion, osConf) } diff --git a/triage/users_linux.go b/triage/users_linux.go index aaca756..e05a895 100644 --- a/triage/users_linux.go +++ b/triage/users_linux.go @@ -9,9 +9,12 @@ import ( "github.com/UT-CTF/landschaft/util" ) -func runUsersTriage() { - printUsers() - printGroups() +func runUsersTriage() string { + csv := printUsers() + csv += "\t" + csv += printGroups() + csv += "\t" + return csv } type user struct { @@ -27,11 +30,11 @@ type group struct { users []string } -func printUsers() { +func printUsers() string { file, err := os.Open("/etc/passwd") if err != nil { fmt.Println("Error reading /etc/passwd:", err) - return + return "err" } defer file.Close() scanner := bufio.NewScanner(file) @@ -52,20 +55,23 @@ func printUsers() { } // convert to [][]string + var result string userListStr := make([][]string, len(userList)) for i, u := range userList { userListStr[i] = []string{u.name, u.uid, u.gid, u.shell} + result += fmt.Sprintf("%s (%s, %s, %s) \n", u.name, u.uid, u.gid, u.shell) } t := util.StyledTable().Rows(userListStr...) fmt.Println(t.Render()) + return "\"" + result + "\"" } -func printGroups() { +func printGroups() string { file, err := os.Open("/etc/group") if err != nil { fmt.Println("Error reading /etc/group:", err) - return + return "err" } defer file.Close() @@ -90,11 +96,14 @@ func printGroups() { } // convert to [][]string + var result string groupListStr := make([][]string, len(groupList)) for i, g := range groupList { groupListStr[i] = []string{g.name, g.gid, strings.Join(g.users, ",")} + result += fmt.Sprintf("%s (GID: %s) - %s\n\n", g.name, g.gid, strings.Join(g.users, ", ")) } t := util.StyledTable().Headers("group", "gid", "users").Rows(groupListStr...) fmt.Println(t.Render()) + return "\"" + result + "\"" } diff --git a/triage/users_windows.go b/triage/users_windows.go index 11f3029..94fdf73 100644 --- a/triage/users_windows.go +++ b/triage/users_windows.go @@ -1,9 +1,77 @@ package triage import ( + "fmt" + "strings" + "github.com/UT-CTF/landschaft/util" ) -func runUsersTriage() { - util.RunAndPrintScript("triage/users.ps1") +func runUsersTriage() string { + return parseUsersAndGroups(util.RunAndPrintScript("triage/users.ps1")) + "\t" + +} + +func parseUsersAndGroups(result string) string { + lines := strings.Split(result, "\n") + var enabled, disabled []string + var enabledCount, disabledCount string + current := "" + inGroups := false + + groups := make(map[string][]string) + var groupOrder []string + currentGroup := "" + + for _, line := range lines { + line = strings.TrimSpace(line) + + if line == "Groups:" { + inGroups = true + continue + } + if line == "" || line == "--------------------------------------------------" || line == "Users:" { + continue + } + + if !inGroups { + if strings.HasPrefix(line, "Enabled Local Users") { + current = "enabled" + enabledCount = strings.TrimPrefix(line, "Enabled Local Users ") + continue + } + if strings.HasPrefix(line, "Disabled Local Users") { + current = "disabled" + disabledCount = strings.TrimPrefix(line, "Disabled Local Users: ") + continue + } + if current == "enabled" { + enabled = append(enabled, line) + } else if current == "disabled" { + disabled = append(disabled, line) + } + } else { + if strings.Contains(line, "(") && strings.Contains(line, ")") && !strings.Contains(line, "\\") { + currentGroup = line + groupOrder = append(groupOrder, currentGroup) + groups[currentGroup] = []string{} + } else if currentGroup != "" { + groups[currentGroup] = append(groups[currentGroup], line) + } + } + } + + var groupParts []string + for _, g := range groupOrder { + groupParts = append(groupParts, fmt.Sprintf("%s \n\t%s", g, strings.Join(groups[g], "\n\t"))) + } + + return fmt.Sprintf("\"Enabled Local Users%s \n\t%s\n"+ + "\nDisabled Local Users %s: \n\t%s\"", + enabledCount, + strings.Join(enabled, "\n\t"), + disabledCount, + strings.Join(disabled, "\n\t"), + ) + "\t" + fmt.Sprintf("\"%s\"", strings.Join(groupParts, "\n\n")) + } diff --git a/util/run_script.go b/util/run_script.go index c77ef5e..021e4dd 100644 --- a/util/run_script.go +++ b/util/run_script.go @@ -7,14 +7,15 @@ import ( "github.com/charmbracelet/log" ) -func RunAndPrintScript(scriptPath string, args ...string) { +func RunAndPrintScript(scriptPath string, args ...string) string { fmt.Println("Executing " + scriptPath) scriptOut, err := embed.ExecuteScript(scriptPath, false, args...) if err != nil { log.Error("Failed to execute script", "script", scriptPath, "err", err) - return + return "err" } fmt.Println(scriptOut) + return scriptOut } func RunAndRedirectScript(scriptPath string, args ...string) {