commit deb85fe10d3e14324731c23c05b3ea5a51255861 Author: maride Date: Thu Oct 26 13:40:55 2023 +0200 Init diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd3fa88 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Ghost + +*Your friendly helper with /etc/hosts, written in Go!* + +## Features + +- Extending existing lines +- Deduplication +- Format check +- `SUID` safe! + +## Usage + +To add two domain names, `domain1.foo` and `domain2.bar`, for the IP addres `10.0.0.1`: + +`./ghost 10.0.0.1 domain1.foo domain2.bar` + +The added line will look like this: + +``` +10.0.0.1 domain1.foo domain2.bar # Added by ghost on 26-10-2023 +``` + +## Notes + +The tool is safe to use with the [SUID bit](https://www.redhat.com/sysadmin/suid-sgid-sticky-bit), which is very handy for not entering your password when adding hostnames to your `/etc/hosts` file every time. + +While this is very handy for CTFs or platforms like Hack The Box, this is also a big security issue, e.g. if you are on a multi-user machine. **Use with caution!** + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b991399 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.maride.cc/maride/ghost + +go 1.21.3 diff --git a/main.go b/main.go new file mode 100644 index 0000000..c05ce51 --- /dev/null +++ b/main.go @@ -0,0 +1,130 @@ +package main + +import ( + "fmt" + "net" + "os" + "regexp" + "strings" + "time" +) + +func main() { + targetIP, domains := parseArgs() + motd() + process(targetIP, domains) +} + +func parseArgs() (net.IP, []string) { + // Check for arguments + if len(os.Args) < 3 { + usage() + os.Exit(1) + } + + // Parse IP + rawIP := os.Args[1] + ip := net.ParseIP(rawIP) + if ip == nil { + fmt.Fprintf(os.Stderr, "Not an IP address: %s\n", rawIP) + usage() + os.Exit(1) + } + + // Check for formatting of domain names + domainExpr, _ := regexp.Compile("^([a-zA-Z0-9]+[a-zA-Z0-9\\-]*[a-zA-Z0-9]+.{0,1})+$") + for _, a := range os.Args[2:] { + if !domainExpr.MatchString(a) { + fmt.Fprintf(os.Stderr, "Doesn't look like a valid domain: %s\n", a) + usage() + os.Exit(1) + } + } + + // All parsed and correct + return ip, os.Args[2:] +} + +// process goes over the /etc/hosts and tries to best-fit the domain names for the IP +func process(ip net.IP, domains []string) { + // Read file + bytesEtcHosts, readErr := os.ReadFile("/etc/hosts") + if readErr != nil { + fmt.Fprintf(os.Stderr, "Failed to read /etc/hosts: %s\n", readErr.Error()) + os.Exit(1) + } + etcHosts := string(bytesEtcHosts) + + // Iterate over lines, find the first match for the IP + lines := strings.Split(etcHosts, "\n") + ipString := ip.String() + found := false + for lPos, l := range lines { + if strings.HasPrefix(l, ipString) { + // Matching line, append our domains. + + // Avoid duplicates + fields := strings.Fields(l) + newDomainsArray := []string{} + ignore := false + for _, d := range domains { + for _, f := range fields { + if d == f { + // this domain name is already in the hosts file, skip + ignore = true + break + } + } + if !ignore { + newDomainsArray = append(newDomainsArray, d) + } + } + newDomains := strings.Join(newDomainsArray, " ") + + // Preserve comments and construct the line again + hostLine, comment, hasComment := strings.Cut(l, "#") + if hasComment { + lines[lPos] = fmt.Sprintf("%s %s # %s", hostLine, newDomains, comment) + } else { + lines[lPos] = fmt.Sprintf("%s %s", hostLine, newDomains) + } + found = true + } + } + + // If a fitting line was not found in the previous for loop, append a new line + if !found { + newDomains := strings.Join(domains, " ") + date := time.Now().Format("02-01-2006") + newLine := fmt.Sprintf("%s\t%s # Added by ghost on %s", ipString, newDomains, date) + lines = append(lines, newLine) + } + + // Write out again + newHosts := strings.Join(lines, "\n") + writeErr := os.WriteFile("/etc/hosts", []byte(newHosts), 0o644) + if writeErr != nil { + fmt.Fprintf(os.Stderr, "Failed to write /etc/hosts: %s\n", writeErr.Error()) + os.Exit(1) + } +} + +// usage prints the usage +func usage() { + fmt.Fprintf(os.Stderr, "Usage: ghost 10.0.1.1 server1.gh ...\n") +} + +// motd prints the banner at the start, fully easter-egg free +func motd() { + fmt.Println("+-------------------------+") + if(time.Now().Month() == time.October) { + // I mean, it's called 'ghost' after all... + fmt.Println("👻 🕷️ 🎃 G H O S T 🎃 🕷️ 👻") + fmt.Println(" your spooooky helper with ") + } else { + fmt.Println(" G H O S T ") + fmt.Println(" your friendly helper with ") + } + fmt.Println(" /etc/hosts, written in Go!") + fmt.Println("+-------------------------+") +}