From 128a27dac5cf2c95bc14985e30125212f0aa5f25 Mon Sep 17 00:00:00 2001 From: Macmod Date: Mon, 24 Jun 2024 21:54:47 -0300 Subject: [PATCH] Organizing the project & implementing ADIDNS viewer./g --- README.md | 11 +- TODO.md | 4 +- godap.go | 61 ++ pkg/adidns/colors.go | 35 + pkg/adidns/formats.go | 107 ++ pkg/adidns/types.go | 939 ++++++++++++++++++ utils/ldaptime.go => pkg/formats/time.go | 2 +- .../ldaputils/actions.go | 138 ++- .../ldapcolors.go => pkg/ldaputils/colors.go | 2 +- .../ldapemojis.go => pkg/ldaputils/emojis.go | 2 +- {utils => pkg/ldaputils}/formats.go | 117 ++- {utils => pkg/ldaputils}/misc.go | 2 +- utils/ldapvars.go => pkg/ldaputils/vars.go | 2 +- {sdl => pkg/sdl}/ADGuids.go | 0 {sdl => pkg/sdl}/AceFieldMaps.go | 0 {sdl => pkg/sdl}/AceTypeStructures.go | 24 +- {sdl => pkg/sdl}/SDTypeStructures.go | 28 +- {sdl => pkg/sdl}/SecurityDescriptorFuncs.go | 10 +- ace.go => tui/ace.go | 30 +- cache.go => tui/cache.go | 2 +- dacl.go => tui/dacl.go | 36 +- tui/dns.go | 725 ++++++++++++++ explorer.go => tui/explorer.go | 24 +- finder.go => tui/finder.go | 2 +- gpo.go => tui/gpo.go | 8 +- group.go => tui/group.go | 46 +- help.go => tui/help.go | 12 +- main.go => tui/main.go | 424 ++++---- main_test.go => tui/main_test.go | 2 +- search.go => tui/search.go | 12 +- theme.go => tui/theme.go | 2 +- tree.go => tui/tree.go | 46 +- utils/ldapformat.go | 121 --- 33 files changed, 2464 insertions(+), 512 deletions(-) create mode 100644 godap.go create mode 100644 pkg/adidns/colors.go create mode 100644 pkg/adidns/formats.go create mode 100644 pkg/adidns/types.go rename utils/ldaptime.go => pkg/formats/time.go (97%) rename utils/ldapactions.go => pkg/ldaputils/actions.go (84%) rename utils/ldapcolors.go => pkg/ldaputils/colors.go (99%) rename utils/ldapemojis.go => pkg/ldaputils/emojis.go (98%) rename {utils => pkg/ldaputils}/formats.go (51%) rename {utils => pkg/ldaputils}/misc.go (88%) rename utils/ldapvars.go => pkg/ldaputils/vars.go (99%) rename {sdl => pkg/sdl}/ADGuids.go (100%) rename {sdl => pkg/sdl}/AceFieldMaps.go (100%) rename {sdl => pkg/sdl}/AceTypeStructures.go (86%) rename {sdl => pkg/sdl}/SDTypeStructures.go (78%) rename {sdl => pkg/sdl}/SecurityDescriptorFuncs.go (95%) rename ace.go => tui/ace.go (96%) rename cache.go => tui/cache.go (99%) rename dacl.go => tui/dacl.go (94%) create mode 100644 tui/dns.go rename explorer.go => tui/explorer.go (97%) rename finder.go => tui/finder.go (99%) rename gpo.go => tui/gpo.go (97%) rename group.go => tui/group.go (88%) rename help.go => tui/help.go (93%) rename main.go => tui/main.go (52%) rename main_test.go => tui/main_test.go (98%) rename search.go => tui/search.go (97%) rename theme.go => tui/theme.go (99%) rename tree.go => tui/tree.go (93%) delete mode 100644 utils/ldapformat.go diff --git a/README.md b/README.md index 5b969e5..1cbcf6d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ * 🗑ī¸ Supports searching deleted & recycled objects * 📁 Supports exporting specific subtrees of the directory into JSON files * 📜 GPO Viewer +* 🌐 ADIDNS Viewer * 🕹ī¸ Interactive userAccountControl editor * đŸ”Ĩ Interactive DACL editor * đŸ§Ļ SOCKS support @@ -47,6 +48,12 @@ $ go install . **Bind with username and password** +```bash +$ godap -u -p -d +``` + +or + ```bash $ godap -u @ -p ``` @@ -131,7 +138,7 @@ You can also change the address of your proxy using the `l` keybinding. | l | Global | Change current server address & credentials | | Ctrl + r | Global | Reconnect to the server | | Ctrl + u | Global | Upgrade connection to use TLS (with StartTLS) | -| Ctrl + f | LDAP Explorer & Object Search pages | Open the finder to search for cached objects & attributes with regex | +| Ctrl + f | Explorer & Search pages | Open the finder to search for cached objects & attributes with regex | | Right Arrow | Explorer panel | Expand the children of the selected object | | Left Arrow | Explorer panel | Collapse the children of the selected object | | r | Explorer panel | Reload the attributes and children of the selected object | @@ -155,6 +162,8 @@ You can also change the address of your proxy using the `l` keybinding. | Ctrl + e | DACL entries panel | Edit the selected ACE of the current DACL | | Delete | DACL entries panel | Deletes the selected ACE of the current DACL | | Ctrl + s | GPO page | Export the current GPOs and their links into a JSON file | +| Ctrl + s | DNS zones panel | Export the selected zones and their child DNS nodes into a JSON file | +| r | DNS zones panel | Reload the nodes of the selected zone / the records of the selected node | | h | Global | Show/hide headers | | q | Global | Exit the program | diff --git a/TODO.md b/TODO.md index 59681e3..17aa267 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,7 @@ # TODO (priority) * Feature: Support deleting specific attribute values (not the entire attribute) -* Feature: View/modify ADIDNS dnsZones and dnsNodes +* Feature: Modify ADIDNS dnsZones and dnsNodes * Feature: Options to manipulate (edit/create/delete) gpLinks visually * Feature: Load initial cache from file @@ -10,7 +10,7 @@ * Feature: Improve object creation form (implement customizations) * Feature: Custom themes * Feature: Customizable keybindings -* Refactor: Improve the organization of files, functions and structures (among other ideas, remove the "utils" package) +* Wish: Add tests for core functions to make sure everything is in order * Wish: Mini tool to convert godap exports into bloodhound dumps * Wish: Monitor object for real-time changes (DirSync/SyncRepl) * Wish: Some way to copy data from panels (not implemented in tview, only for the "textarea" primitive) diff --git a/godap.go b/godap.go new file mode 100644 index 0000000..d853c23 --- /dev/null +++ b/godap.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "github.com/Macmod/godap/v2/tui" + "github.com/spf13/cobra" +) + +func main() { + rootCmd := &cobra.Command{ + Use: "godap ", + Short: "A complete TUI for LDAP.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + tui.LdapServer = args[0] + tui.SetupApp() + }, + } + + rootCmd.Flags().IntVarP(&tui.LdapPort, "port", "P", 389, "LDAP server port") + rootCmd.Flags().StringVarP(&tui.LdapUsername, "username", "u", "", "LDAP username") + rootCmd.Flags().StringVarP(&tui.LdapPassword, "password", "p", "", "LDAP password") + rootCmd.Flags().StringVarP(&tui.LdapPasswordFile, "passfile", "", "", "Path to a file containing the LDAP password") + rootCmd.Flags().StringVarP(&tui.DomainName, "domain", "d", "", "Domain for NTLM / Kerberos authentication") + rootCmd.Flags().StringVarP(&tui.NtlmHash, "hashes", "H", "", "NTLM hash") + rootCmd.Flags().BoolVarP(&tui.Kerberos, "kerberos", "k", false, "Use Kerberos ticket for authentication (CCACHE specified via KRB5CCNAME environment variable)") + rootCmd.Flags().StringVarP(&tui.TargetSpn, "spn", "t", "", "Target SPN to use for Kerberos bind (usually ldap/dchostname)") + rootCmd.Flags().StringVarP(&tui.NtlmHashFile, "hashfile", "", "", "Path to a file containing the NTLM hash") + rootCmd.Flags().StringVarP(&tui.RootDN, "rootDN", "r", "", "Initial root DN") + rootCmd.Flags().StringVarP(&tui.SearchFilter, "filter", "f", "(objectClass=*)", "Initial LDAP search filter") + rootCmd.Flags().BoolVarP(&tui.Emojis, "emojis", "E", true, "Prefix objects with emojis") + rootCmd.Flags().BoolVarP(&tui.Colors, "colors", "C", true, "Colorize objects") + rootCmd.Flags().BoolVarP(&tui.FormatAttrs, "format", "F", true, "Format attributes into human-readable values") + rootCmd.Flags().BoolVarP(&tui.ExpandAttrs, "expand", "A", true, "Expand multi-value attributes") + rootCmd.Flags().IntVarP(&tui.AttrLimit, "limit", "L", 20, "Number of attribute values to render for multi-value attributes when -expand is set true") + rootCmd.Flags().BoolVarP(&tui.CacheEntries, "cache", "M", true, "Keep loaded entries in memory while the program is open and don't query them again") + rootCmd.Flags().BoolVarP(&tui.Deleted, "deleted", "D", false, "Include deleted objects in all queries performed") + rootCmd.Flags().Int32VarP(&tui.Timeout, "timeout", "T", 10, "Timeout for LDAP connections in seconds") + rootCmd.Flags().BoolVarP(&tui.LoadSchema, "schema", "s", false, "Load schema GUIDs from the LDAP server during initialization") + rootCmd.Flags().Uint32VarP(&tui.PagingSize, "paging", "G", 800, "Default paging size for regular queries") + rootCmd.Flags().BoolVarP(&tui.Insecure, "insecure", "I", false, "Skip TLS verification for LDAPS/StartTLS") + rootCmd.Flags().BoolVarP(&tui.Ldaps, "ldaps", "S", false, "Use LDAPS for initial connection") + rootCmd.Flags().StringVarP(&tui.SocksServer, "socks", "x", "", "Use a SOCKS proxy for initial connection") + rootCmd.Flags().StringVarP(&tui.KdcHost, "kdc", "", "", "Address of the KDC to use with Kerberos authentication (optional: only if the KDC differs from the specified LDAP server)") + rootCmd.Flags().StringVarP(&tui.TimeFormat, "timefmt", "", "", "Time format for LDAP timestamps") + + versionCmd := &cobra.Command{ + Use: "version", + Short: "Print the version number of the application", + DisableFlagsInUseLine: true, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(tui.GodapVer) + }, + } + + rootCmd.AddCommand(versionCmd) + + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + } +} diff --git a/pkg/adidns/colors.go b/pkg/adidns/colors.go new file mode 100644 index 0000000..8b59711 --- /dev/null +++ b/pkg/adidns/colors.go @@ -0,0 +1,35 @@ +package adidns + +func GetPropCellColor(propId uint32, cellValue string) (string, bool) { + switch cellValue { + case "Enabled": + return "green", true + case "Disabled", "None": + return "red", true + case "Unknown", "Not specified": + return "gray", true + } + + switch propId { + case 0x00000001: + switch cellValue { + case "PRIMARY": + return "green", true + case "CACHE": + return "blue", true + } + case 0x00000002: + switch cellValue { + case "None": + return "red", true + case "Nonsecure and secure": + return "yellow", true + case "Secure only": + return "green", true + default: + return "gray", true + } + } + + return "", false +} diff --git a/pkg/adidns/formats.go b/pkg/adidns/formats.go new file mode 100644 index 0000000..ad60362 --- /dev/null +++ b/pkg/adidns/formats.go @@ -0,0 +1,107 @@ +package adidns + +import ( + "encoding/binary" + "fmt" + "math" + "net" + "time" +) + +func ParseIP(data []byte) string { + ip := net.IP(data) + return ip.String() +} + +func ParseAddrArray(data []byte) []string { + if len(data) == 0 { + return nil + } + + numIPs := int(data[0]) + if len(data) < 32*numIPs+32 { + return nil + } + + addrArr := data[32:] + + ips := make([]string, numIPs) + for x := 0; x < numIPs; x += 1 { + family := binary.LittleEndian.Uint16(addrArr[:4]) + + var ip net.IP + if family == 0x0002 { + // IPv4 + ip = net.IP(addrArr[x*32+4 : x*32+8]) + } else if family == 0x0017 { + // IPv6 + ip = net.IP(addrArr[x*32+8 : x*32+24]) + } else { + continue + } + + ips[x] = ip.String() + } + + return ips +} + +func ParseIP4Array(data []byte) []string { + if len(data) == 0 { + return nil + } + + numIP4s := int(data[0]) + if len(data) < 4*numIP4s+1 { + return nil + } + + ip4s := make([]string, numIP4s) + for x := 0; x < numIP4s; x += 1 { + ip := net.IP(data[1+x*4 : 1+(x+1)*4]) + ip4s = append(ip4s, ip.String()) + } + + return ip4s +} + +func FormatHours(val uint64) string { + days := 0 + if val > 24 { + days = int(math.Floor(float64(val / 24))) + } + + text := "" + if days > 0 { + text = fmt.Sprintf("%d days", days) + if val%24 != 0 { + text += fmt.Sprintf(", %d hours", val%24) + } + } else { + text = fmt.Sprintf("%d hours", val) + } + + return text +} + +// msTime is defined as the number of seconds since Jan 1th of 1601 +// to calculate it we just compute a unix timestamp after +// removing the difference in seconds +// between 01/01/1601 and 01/01/1970 +func MSTimeToUnixTimestamp(msTime uint64) int64 { + if msTime == 0 { + return -1 + } + + baseTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) + + secondsSince := msTime - uint64(11644473600) + + elapsedDuration := time.Duration(secondsSince) * time.Second + + targetTime := baseTime.Add(elapsedDuration) + + unixTimestamp := targetTime.Unix() + + return unixTimestamp +} diff --git a/pkg/adidns/types.go b/pkg/adidns/types.go new file mode 100644 index 0000000..ea837e8 --- /dev/null +++ b/pkg/adidns/types.go @@ -0,0 +1,939 @@ +package adidns + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "io/ioutil" + "reflect" + "strings" + "time" +) + +// Helper values +var DnsRecordTypes map[uint16]string = map[uint16]string{ + 0x0000: "ZERO", + 0x0001: "A", + 0x0002: "NS", + 0x0003: "MD", + 0x0004: "MF", + 0x0005: "CNAME", + 0x0006: "SOA", + 0x0007: "MB", + 0x0008: "MG", + 0x0009: "MR", + 0x000A: "NULL", + 0x000B: "WKS", + 0x000C: "PTR", + 0x000D: "HINFO", + 0x000E: "MINFO", + 0x000F: "MX", + 0x0010: "TXT", + 0x0011: "RP", + 0x0012: "AFSDB", + 0x0013: "X25", + 0x0014: "ISDN", + 0x0015: "RT", + 0x0018: "SIG", + 0x0019: "KEY", + 0x001C: "AAAA", + 0x001D: "LOC", + 0x001E: "NXT", + 0x0021: "SRV", + 0x0022: "ATMA", + 0x0023: "NAPTR", + 0x0027: "DNAME", + 0x002B: "DS", + 0x002E: "RRSIG", + 0x002F: "NSEC", + 0x0030: "DNSKEY", + 0x0031: "DHCID", + 0x0032: "NSEC3", + 0x0033: "NSEC3PARAM", + 0x0034: "TLSA", + 0x00FF: "ALL", + 0xFF01: "WINS", + 0xFF02: "WINSR", +} + +type DcPromoFlag struct { + Value uint32 + Description string +} + +var dcPromoFlags = []DcPromoFlag{ + {0x00000000, "No change to existing zone storage."}, + {0x00000001, "Zone is to be moved to the DNS domain partition."}, + {0x00000002, "Zone is to be moved to the DNS forest partition."}, +} + +func FindDcPromoDescription(value uint32) string { + for _, flag := range dcPromoFlags { + if flag.Value == value { + return flag.Description + } + } + return "Unknown DcPromo flag" +} + +type DNSPropertyId struct { + Id uint32 + Name string +} + +var DnsPropertyIds = []DNSPropertyId{ + {0x00000001, "TYPE"}, + {0x00000002, "ALLOW_UPDATE"}, + {0x00000008, "SECURE_TIME"}, + {0x00000010, "NOREFRESH_INTERVAL"}, + {0x00000020, "REFRESH_INTERVAL"}, + {0x00000040, "AGING_STATE"}, + {0x00000011, "SCAVENGING_SERVERS"}, + {0x00000012, "AGING_ENABLED_TIME"}, + {0x00000080, "DELETED_FROM_HOSTNAME"}, + {0x00000081, "MASTER_SERVERS"}, + {0x00000082, "AUTO_NS_SERVERS"}, + {0x00000083, "DCPROMO_CONVERT"}, + {0x00000090, "SCAVENGING_SERVERS_DA"}, + {0x00000091, "MASTER_SERVERS_DA"}, + {0x00000092, "AUTO_NS_SERVERS_DA"}, + {0x00000100, "NODE_DBFLAGS"}, +} + +func FindPropName(id uint32) string { + for _, propertyId := range DnsPropertyIds { + if propertyId.Id == id { + return propertyId.Name + } + } + return "UNKNOWN" +} + +// ADIDNS Structures +type DNSRecord struct { + DataLength uint16 + Type uint16 + Version uint8 + Rank uint8 + Flags uint16 + Serial uint32 + TTLSeconds uint32 + Reserved uint32 + Timestamp uint32 + Data []byte +} + +type DNSProperty struct { + DataLength uint32 + NameLength uint32 + Flag uint32 + Version uint32 + Id uint32 + Data []byte + Name uint8 +} + +func (prop *DNSProperty) Format(timeFormat string) string { + var propDataArr [8]byte + copy(propDataArr[:], prop.Data) + propVal := binary.LittleEndian.Uint64(propDataArr[:]) + + switch prop.Id { + case 0x00000001: + // DSPROPERTY_ZONE_TYPE + switch propVal { + case 0: + return "CACHE" + case 1: + return "PRIMARY" + case 2: + return "SECONDARY" + case 3: + return "STUB" + case 4: + return "FORWARDER" + case 5: + return "SECONDARY_CACHE" + default: + return "UNKNOWN" + } + case 0x00000002: + // DSPROPERTY_ZONE_ALLOW_UPDATE + switch propVal { + case 0: + return "None" + case 1: + return "Nonsecure and secure" + case 2: + return "Secure only" + default: + return "Unknown" + } + case 0x00000008: + unixTimestamp := MSTimeToUnixTimestamp(propVal) + + if unixTimestamp != -1 { + timeObj := time.Unix(unixTimestamp, 0) + return timeObj.Format(timeFormat) + } else { + return "Not specified" + } + case 0x00000010, 0x00000020: + // DSPROPERTY_ZONE_NOREFRESH_INTERVAL + // DSPROPERTY_ZONE_REFRESH_INTERVAL + return FormatHours(propVal) + case 0x00000012: + // DSPROPERTY_ZONE_AGING_ENABLED_TIME + msTime := propVal * 3600 + unixTimestamp := MSTimeToUnixTimestamp(msTime) + + if unixTimestamp != -1 { + timeObj := time.Unix(unixTimestamp, 0) + return timeObj.Format(timeFormat) + } else { + return "Not specified" + } + + //return hex.EncodeToString(prop.Data) + case 0x00000080: + // DSPROPERTY_ZONE_DELETED_FROM_HOSTNAME + return string(propVal) + case 0x00000040: + // DSPROPERTY_ZONE_AGING_STATE + if propVal == 1 { + return "Enabled" + } else { + return "Disabled" + } + case 0x00000090, 0x00000091, 0x00000092: + // DSPROPERTY_ZONE_SCAVENGING_SERVERS_DA + // DSPROPERTY_ZONE_MASTER_SERVERS_DA + // DSPROPERTY_ZONE_AUTO_NS_SERVERS_DA + return fmt.Sprintf("%v", ParseAddrArray(prop.Data)) + case 0x00000083: + // DSPROPERTY_ZONE_DCPROMO_CONVERT + switch propVal { + case 0: + return "No change" + case 1: + return "Move to DNS domain partition" + case 2: + return "Move to DNS forest partition" + default: + return "Unknown" + } + case 0x00000082, 0x00000011: + // DSPROPERTY_ZONE_SCAVENGING_SERVERS + // DSPROPERTY_ZONE_AUTO_NS_SERVERS + return fmt.Sprintf("%v", ParseIP4Array(prop.Data)) + default: + // DSPROPERTY_ZONE_NODE_DBFLAGS + // Or other unknown codes + return fmt.Sprintf("%v", prop.Data) + } +} + +type DNSZone struct { + DN string + Name string + Props []DNSProperty +} + +type DNSNode struct { + DN string + Name string + Records []DNSRecord +} + +func (d *DNSRecord) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + if err := binary.Write(buf, binary.LittleEndian, d.DataLength); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, d.Type); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, d.Version); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, d.Rank); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, d.Flags); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, d.Serial); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.BigEndian, d.TTLSeconds); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, d.Reserved); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, d.Timestamp); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, d.Data); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func (d *DNSRecord) Decode(data []byte) error { + buf := bytes.NewReader(data) + if err := binary.Read(buf, binary.LittleEndian, &d.DataLength); err != nil { + return err + } + if err := binary.Read(buf, binary.LittleEndian, &d.Type); err != nil { + return err + } + if err := binary.Read(buf, binary.LittleEndian, &d.Version); err != nil { + return err + } + if err := binary.Read(buf, binary.LittleEndian, &d.Rank); err != nil { + return err + } + if err := binary.Read(buf, binary.LittleEndian, &d.Flags); err != nil { + return err + } + if err := binary.Read(buf, binary.LittleEndian, &d.Serial); err != nil { + return err + } + if err := binary.Read(buf, binary.BigEndian, &d.TTLSeconds); err != nil { + return err + } + if err := binary.Read(buf, binary.LittleEndian, &d.Reserved); err != nil { + return err + } + if err := binary.Read(buf, binary.LittleEndian, &d.Timestamp); err != nil { + return err + } + d.Data = make([]byte, d.DataLength) + if err := binary.Read(buf, binary.LittleEndian, &d.Data); err != nil { + return err + } + return nil +} + +func (d *DNSRecord) PrintType() string { + recordType, found := DnsRecordTypes[d.Type] + if !found { + recordType = "Unknown" + } + + return recordType +} + +func (d *DNSRecord) UnixTimestamp() int64 { + msTime := uint64(d.Timestamp) * 3600 + return MSTimeToUnixTimestamp(msTime) +} + +// DNS_RPC_NAME parser +func ParseRpcName(buf *bytes.Reader) (string, error) { + var nameLen uint8 + if err := binary.Read(buf, binary.LittleEndian, &nameLen); err != nil { + return "", err + } + + nameBuf := make([]byte, nameLen) + if _, err := io.ReadFull(buf, nameBuf); err != nil { + return "", err + } + + return string(nameBuf[:]), nil +} + +func ParseRpcNameSingle(data []byte) (string, error) { + buf := bytes.NewReader(data) + return ParseCountName(buf) +} + +// DNS_COUNT_NAME parser +func ParseCountName(buf *bytes.Reader) (string, error) { + var rawNameLen uint8 + var labelCnt uint8 + var labLen uint8 + + if err := binary.Read(buf, binary.LittleEndian, &rawNameLen); err != nil { + return "", err + } + + if err := binary.Read(buf, binary.LittleEndian, &labelCnt); err != nil { + return "", err + } + + labels := make([]string, labelCnt) + + for cnt := uint8(0); cnt < labelCnt; cnt += 1 { + if err := binary.Read(buf, binary.LittleEndian, &labLen); err != nil { + return "", err + } + + labBuf := make([]byte, labLen) + if _, err := io.ReadFull(buf, labBuf); err != nil { + return "", err + } + + labels[cnt] = string(labBuf) + } + + // Consume the NULL terminator + buf.ReadByte() + + return strings.Join(labels, "."), nil +} + +func ParseCountNameSingle(data []byte) (string, error) { + buf := bytes.NewReader(data) + return ParseCountName(buf) +} + +func (p *DNSProperty) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + if err := binary.Write(buf, binary.LittleEndian, p.DataLength); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, p.NameLength); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, p.Flag); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, p.Version); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, p.Id); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, p.Data); err != nil { + return nil, err + } + if err := binary.Write(buf, binary.LittleEndian, p.Name); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func (p *DNSProperty) Decode(data []byte) error { + buf := bytes.NewReader(data) + if err := binary.Read(buf, binary.LittleEndian, &p.DataLength); err != nil { + return err + } + if err := binary.Read(buf, binary.LittleEndian, &p.NameLength); err != nil { + return err + } + if err := binary.Read(buf, binary.LittleEndian, &p.Flag); err != nil { + return err + } + if err := binary.Read(buf, binary.LittleEndian, &p.Version); err != nil { + return err + } + if err := binary.Read(buf, binary.LittleEndian, &p.Id); err != nil { + return err + } + p.Data = make([]byte, p.DataLength) + if err := binary.Read(buf, binary.LittleEndian, &p.Data); err != nil { + return err + } + if err := binary.Read(buf, binary.LittleEndian, &p.Name); err != nil { + return err + } + return nil +} + +// ADIDNS Record Types +// {Reference} MS-DNSP 2.2.2.2.4 DNS_RPC_RECORD_DATA +// IP addresses (v4 or v6) are stored using their string representations + +// Interface/structure to hold the parsed record fields +type FriendlyRecord interface { + // Parses a record from its byte array in the Data field of the + // DNSRecord attribute + Parse([]byte) +} + +type RecordContainer struct { + Name string + Contents FriendlyRecord +} + +type Field struct { + Name any + Value any +} + +// Using a bit of reflection so that +// I don't have to manually implement a DumpField +// method on every type +func (rc RecordContainer) DumpFields() []Field { + result := make([]Field, 0) + + v := reflect.ValueOf(rc.Contents).Elem() + for i := 0; i < v.NumField(); i++ { + result = append(result, Field{v.Type().Field(i).Name, v.Field(i).Interface()}) + } + + return result +} + +// 2.2.2.2.4.2 DNS_RPC_RECORD_NODE_NAME +type RecordNodeName struct { + NameNode string +} + +func (rnn *RecordNodeName) Parse(data []byte) { + parsedName, err := ParseCountNameSingle(data) + if err == nil { + rnn.NameNode = parsedName + } +} + +// 2.2.2.2.4.6 DNS_RPC_RECORD_STRING +type RecordString struct { + StrData []string +} + +func (rs *RecordString) Parse(data []byte) { + result := make([]string, 0) + + buf := bytes.NewReader(data) + for buf.Len() > 0 { + parsedName, err := ParseRpcName(buf) + if err == nil { + result = append(result, parsedName) + } + } + + rs.StrData = result +} + +// 2.2.2.2.4.7 DNS_RPC_RECORD_MAIL_ERROR +type RecordMailError struct { + MailBX string + ErrorMailBX string +} + +func (rs *RecordMailError) Parse(data []byte) { + buf := bytes.NewReader(data) + + mailBX, err := ParseCountName(buf) + if err == nil { + rs.MailBX = mailBX + } + + errorMailBX, err := ParseCountName(buf) + if err == nil { + rs.ErrorMailBX = errorMailBX + } +} + +// 2.2.2.2.4.8 DNS_RPC_RECORD_NAME_PREFERENCE +type RecordNamePreference struct { + Preference uint16 + Exchange string +} + +func (rnp *RecordNamePreference) Parse(data []byte) { + rnp.Preference = binary.BigEndian.Uint16(data[:2]) + parsedName, err := ParseCountNameSingle(data[2:]) + if err == nil { + rnp.Exchange = parsedName + } +} + +type NSRecord = RecordNodeName +type MDRecord = RecordNodeName +type MFRecord = RecordNodeName +type CNAMERecord = RecordNodeName +type MBRecord = RecordNodeName +type MGRecord = RecordNodeName +type MRRecord = RecordNodeName +type PTRRecord = RecordNodeName +type DNAMERecord = RecordNodeName + +type HINFORecord = RecordString +type ISDNRecord = RecordString +type TXTRecord = RecordString +type X25Record = RecordString +type LOCRecord = RecordString + +type MINFORecord = RecordMailError +type RPRecord = RecordMailError + +type MXRecord = RecordNamePreference +type AFSDBRecord = RecordNamePreference +type RTRecord = RecordNamePreference + +// 2.2.2.2.4.23 DNS_RPC_RECORD_TS +type ZERORecord struct{} + +func (zr *ZERORecord) Parse(data []byte) {} + +// 2.2.2.2.4.1 DNS_RPC_RECORD_A +type ARecord struct { + Address string // Parsed from a [4]byte +} + +func (v4r *ARecord) Parse(data []byte) { + v4r.Address = ParseIP(data) +} + +// 2.2.2.2.4.16 DNS_RPC_RECORD_AAAA +type AAAARecord struct { + Address string // Parsed from a [16]byte +} + +func (v6r *AAAARecord) Parse(data []byte) { + v6r.Address = ParseIP(data) +} + +// 2.2.2.2.4.3 DNS_RPC_RECORD_SOA +type SOARecord struct { + Serial uint32 + Refresh uint32 + Retry uint32 + Expire uint32 + MinimumTTL uint32 + NamePrimaryServer string + ZoneAdminEmail string +} + +func (r *SOARecord) Parse(data []byte) { + r.Serial = binary.BigEndian.Uint32(data[:4]) + r.Refresh = binary.BigEndian.Uint32(data[4:8]) + r.Retry = binary.BigEndian.Uint32(data[8:12]) + r.Expire = binary.BigEndian.Uint32(data[12:16]) + r.MinimumTTL = binary.BigEndian.Uint32(data[16:20]) + + buf := bytes.NewReader(data[20:]) + parsedName, err := ParseCountName(buf) + if err == nil { + r.NamePrimaryServer = parsedName + } + + parsedName, err = ParseCountName(buf) + if err == nil { + r.ZoneAdminEmail = parsedName + } +} + +// 2.2.2.2.4.4 DNS_RPC_RECORD_NULL +type NULLRecord struct { + Data []byte +} + +func (r *NULLRecord) Parse(data []byte) { + r.Data = data +} + +// 2.2.2.2.4.5 DNS_RPC_RECORD_WKS +type WKSRecord struct { + Address string + Protocol uint8 + BitMask []byte +} + +func (r *WKSRecord) Parse(data []byte) { + r.Address = ParseIP(data[:4]) + r.Protocol = data[4] + r.BitMask = data[5:] +} + +// 2.2.2.2.4.9 DNS_RPC_RECORD_SIG +type SIGRecord struct { + TypeCovered uint16 + Algorithm uint8 + Labels uint8 + OriginalTTL uint32 + SigExpiration uint32 + SigInception uint32 + KeyTag uint16 + NameSigner string + SignatureInfo []byte +} + +func (r *SIGRecord) Parse(data []byte) { + r.TypeCovered = binary.BigEndian.Uint16(data[:2]) + r.Algorithm = data[2] + r.Labels = data[3] + r.OriginalTTL = binary.BigEndian.Uint32(data[4:8]) + r.SigExpiration = binary.BigEndian.Uint32(data[8:12]) + r.SigInception = binary.BigEndian.Uint32(data[12:16]) + r.KeyTag = binary.BigEndian.Uint16(data[16:18]) + + buf := bytes.NewReader(data[18:]) + parsedName, err := ParseCountName(buf) + if err == nil { + r.NameSigner = parsedName + } + + sigInfo, err := ioutil.ReadAll(buf) + r.SignatureInfo = sigInfo +} + +// 2.2.2.2.4.13 DNS_RPC_RECORD_KEY +type KEYRecord struct { + Flags uint16 + Protocol uint8 + Algorithm uint8 + Key []byte +} + +func (r *KEYRecord) Parse(data []byte) { + r.Flags = binary.BigEndian.Uint16(data[:2]) + r.Protocol = data[2] + r.Algorithm = data[3] + r.Key = data[4:] +} + +// 2.2.2.2.4.17 DNS_RPC_RECORD_NXT +type NXTRecord struct { + NumRecordTypes uint16 + TypeWords []uint16 + NextName string +} + +func (r *NXTRecord) Parse(data []byte) { + // This type does not seem to be following MS spec properly. + // I'll just ignore it for the moment and hope to figure it out later. + + r.NumRecordTypes = binary.LittleEndian.Uint16(data[:2]) + r.NextName = "" + + /* + r.TypeWords = make([]uint16, r.NumRecordTypes) + + offset := 2 + for i := uint16(0); i < r.NumRecordTypes; i++ { + r.TypeWords[i] = binary.LittleEndian.Uint16(data[offset : offset+2]) + offset += 2 + } + + parsedName, err := ParseRpcNameSingle(data[offset:]) + if err == nil { + r.NextName = parsedName + } + */ +} + +// 2.2.2.2.4.18 DNS_RPC_RECORD_SRV +type SRVRecord struct { + Priority uint16 + Weight uint16 + Port uint16 + NameTarget string +} + +func (r *SRVRecord) Parse(data []byte) { + r.Priority = binary.BigEndian.Uint16(data[:2]) + r.Weight = binary.BigEndian.Uint16(data[2:4]) + r.Port = binary.BigEndian.Uint16(data[4:6]) + + parsedName, err := ParseCountNameSingle(data[6:]) + if err == nil { + r.NameTarget = parsedName + } +} + +// 2.2.2.2.4.19 DNS_RPC_RECORD_ATMA +type ATMARecord struct { + Format uint8 + Address string +} + +func (r *ATMARecord) Parse(data []byte) { + r.Format = data[0] + + r.Address = string(data[1:]) +} + +// 2.2.2.2.4.20 DNS_RPC_RECORD_NAPTR +type NAPTRRecord struct { + Order uint16 + Preference uint16 + Flags string + Service string + Substitution string + Replacement string +} + +func (r *NAPTRRecord) Parse(data []byte) { + var err error + + r.Order = binary.BigEndian.Uint16(data[:2]) + r.Preference = binary.BigEndian.Uint16(data[2:4]) + + buf := bytes.NewReader(data[4:]) + + flags, err := ParseRpcName(buf) + if err == nil { + r.Flags = flags + } + + service, err := ParseRpcName(buf) + if err == nil { + r.Service = service + } + + subst, err := ParseRpcName(buf) + if err == nil { + r.Substitution = subst + } + + replacement, err := ParseCountName(buf) + if err == nil { + r.Replacement = replacement + } +} + +// 2.2.2.2.4.12 DNS_RPC_RECORD_DS +type DSRecord struct { + KeyTag uint16 + Algorithm uint8 + DigestType uint8 + Digest []byte +} + +func (r *DSRecord) Parse(data []byte) { + r.KeyTag = binary.BigEndian.Uint16(data[:2]) + r.Algorithm = data[2] + r.DigestType = data[3] + r.Digest = data[4:] +} + +// 2.2.2.2.4.10 DNS_RPC_RECORD_RRSIG +type RRSIGRecord = SIGRecord + +// 2.2.2.2.4.11 DNS_RPC_RECORD_NSEC +type NSECRecord struct { + NameSigner string + NSECBitmap []byte +} + +func (r *NSECRecord) Parse(data []byte) { + buf := bytes.NewReader(data) + parsedName, err := ParseCountName(buf) + if err == nil { + r.NameSigner = parsedName + } + + binary.Read(buf, binary.LittleEndian, &r.NSECBitmap) +} + +// 2.2.2.2.4.15 DNS_RPC_RECORD_DNSKEY +type DNSKEYRecord struct { + Flags uint16 + Protocol uint8 + Algorithm uint8 + Key []byte +} + +func (r *DNSKEYRecord) Parse(data []byte) { + r.Flags = binary.BigEndian.Uint16(data[:2]) + r.Protocol = data[2] + r.Algorithm = data[3] + r.Key = data[4:] +} + +// 2.2.2.2.4.14 DNS_RPC_RECORD_DHCID +type DHCIDRecord struct { + Digest []byte +} + +func (r *DHCIDRecord) Parse(data []byte) { + r.Digest = data +} + +// 2.2.2.2.4.24 DNS_RPC_RECORD_NSEC3 +type NSEC3Record struct { + Algorithm uint8 + Flags uint8 + Iterations uint16 + SaltLength uint8 + HashLength uint8 + Salt []byte + NextHashedOwnerName []byte + Bitmaps []byte +} + +func (r *NSEC3Record) Parse(data []byte) { + r.Algorithm = data[0] + r.Flags = data[1] + r.Iterations = binary.BigEndian.Uint16(data[2:4]) + r.SaltLength = data[4] + r.HashLength = data[5] + r.Salt = data[6 : 6+int(r.SaltLength)] + r.NextHashedOwnerName = data[6+int(r.SaltLength) : 6+int(r.SaltLength)+int(r.HashLength)] + r.Bitmaps = data[6+int(r.SaltLength)+int(r.HashLength):] +} + +// 2.2.2.2.4.25 DNS_RPC_RECORD_NSEC3PARAM +type NSEC3PARAMRecord struct { + Algorithm uint8 + Flags uint8 + Iterations uint16 + SaltLength uint8 + Salt []byte +} + +func (r *NSEC3PARAMRecord) Parse(data []byte) { + r.Algorithm = data[0] + r.Flags = data[1] + r.Iterations = binary.BigEndian.Uint16(data[2:4]) + r.SaltLength = data[4] + r.Salt = data[5 : 5+int(r.SaltLength)] +} + +// 2.2.2.2.4.26 DNS_RPC_RECORD_TLSA +type TLSARecord struct { + CertificateUsage uint8 + Selector uint8 + MatchingType uint8 + CertificateAssociationData []byte +} + +func (r *TLSARecord) Parse(data []byte) { + r.CertificateUsage = data[0] + r.Selector = data[1] + r.MatchingType = data[2] + r.CertificateAssociationData = data[3:] +} + +// 2.2.2.2.4.21 DNS_RPC_RECORD_WINS +type WINSRecord struct { + MappingFlag uint32 + LookupTimeout uint32 + CacheTimeout uint32 + WinsServers [4]uint32 +} + +func (r *WINSRecord) Parse(data []byte) { + r.MappingFlag = binary.BigEndian.Uint32(data[:4]) + r.LookupTimeout = binary.BigEndian.Uint32(data[4:8]) + r.CacheTimeout = binary.BigEndian.Uint32(data[8:12]) + for i := 0; i < 4; i++ { + r.WinsServers[i] = binary.BigEndian.Uint32(data[12+i*4 : 16+i*4]) + } +} + +// 2.2.2.2.4.22 DNS_RPC_RECORD_WINSR +type WINSRRecord struct { + Mapping uint32 + LookupTimeout uint32 + CacheTimeout uint32 + NameResultDomain string +} + +func (r *WINSRRecord) Parse(data []byte) { + r.Mapping = binary.BigEndian.Uint32(data[:4]) + r.LookupTimeout = binary.BigEndian.Uint32(data[4:8]) + r.CacheTimeout = binary.BigEndian.Uint32(data[8:12]) + + parsedName, err := ParseCountNameSingle(data[12:]) + if err == nil { + r.NameResultDomain = parsedName + } +} diff --git a/utils/ldaptime.go b/pkg/formats/time.go similarity index 97% rename from utils/ldaptime.go rename to pkg/formats/time.go index ee9a18b..7998daf 100644 --- a/utils/ldaptime.go +++ b/pkg/formats/time.go @@ -1,4 +1,4 @@ -package utils +package formats import ( "fmt" diff --git a/utils/ldapactions.go b/pkg/ldaputils/actions.go similarity index 84% rename from utils/ldapactions.go rename to pkg/ldaputils/actions.go index c7e4e7c..623e35a 100644 --- a/utils/ldapactions.go +++ b/pkg/ldaputils/actions.go @@ -1,4 +1,4 @@ -package utils +package ldaputils import ( "crypto/tls" @@ -7,6 +7,8 @@ import ( "net" "strings" + "github.com/Macmod/godap/v2/pkg/adidns" + ber "github.com/go-asn1-ber/asn1-ber" "github.com/go-ldap/ldap/gssapi" "github.com/go-ldap/ldap/v3" @@ -449,6 +451,140 @@ func (lc *LDAPConn) AddUser(objectName string, parentDN string) error { return lc.Conn.Add(addRequest) } +func (lc *LDAPConn) AddADIDNSZone(objectName string, props []adidns.DNSProperty) error { + addRequest := ldap.NewAddRequest("DC="+objectName+",CN=MicrosoftDNS,DC=DomainDNSZones,"+lc.RootDN, nil) + addRequest.Attribute("objectClass", []string{"top", "dnsZone"}) + addRequest.Attribute("cn", []string{"Zone"}) + addRequest.Attribute("name", []string{objectName}) + + var dNSPropertyList []string + for _, prop := range props { + encodedProp, err := prop.Encode() + if err == nil { + dNSPropertyList = append(dNSPropertyList, string(encodedProp)) + } + } + + addRequest.Attribute("dNSProperty", dNSPropertyList) + + return lc.Conn.Add(addRequest) +} + +func (lc *LDAPConn) GetADIDNSZones(name string, isForest bool) ([]adidns.DNSZone, error) { + zoneContainer := "DomainDNSZones" + if isForest { + zoneContainer = "ForestDNSZones" + } + + queryDN := fmt.Sprintf("CN=MicrosoftDNS,DC=%s,%s", zoneContainer, lc.RootDN) + queryFilter := "(objectClass=dnsZone)" + if name != "" { + queryFilter = fmt.Sprintf("(&%s(name=%s))", queryFilter, ldap.EscapeFilter(name)) + } + + zoneEntries, err := lc.Query(queryDN, queryFilter, ldap.ScopeSingleLevel, false) + if err != nil { + return nil, err + } + + zones := make([]adidns.DNSZone, 0) + for _, zoneEntry := range zoneEntries { + zoneDN := zoneEntry.DN + zoneName := zoneEntry.GetAttributeValue("name") + dnsPropsStrs := zoneEntry.GetAttributeValues("dNSProperty") + + props := make([]adidns.DNSProperty, 0) + for _, propStr := range dnsPropsStrs { + dnsProp := new(adidns.DNSProperty) + dnsProp.Decode([]byte(propStr)) + + props = append(props, *dnsProp) + } + + zones = append(zones, adidns.DNSZone{zoneDN, zoneName, props}) + } + + return zones, nil +} + +func (lc *LDAPConn) GetADIDNSNode(nodeDN string) (adidns.DNSNode, error) { + var node adidns.DNSNode + + nodeEntries, err := lc.Query(nodeDN, "(objectClass=dnsNode)", ldap.ScopeBaseObject, false) + if err != nil { + return node, err + } + + if len(nodeEntries) > 0 { + nodeEntry := nodeEntries[0] + + node.DN = nodeEntry.DN + node.Name = nodeEntry.GetAttributeValue("name") + + dnsRecsStrs := nodeEntry.GetAttributeValues("dnsRecord") + records := make([]adidns.DNSRecord, 0) + + for _, recordStr := range dnsRecsStrs { + dnsRec := new(adidns.DNSRecord) + dnsRec.Decode([]byte(recordStr)) + + records = append(records, *dnsRec) + } + + node.Records = records + } else { + return node, fmt.Errorf("Node not found") + } + + return node, nil +} + +func (lc *LDAPConn) GetADIDNSNodes(zoneDN string) ([]adidns.DNSNode, error) { + nodeEntries, err := lc.Query(zoneDN, "(objectClass=dnsNode)", ldap.ScopeSingleLevel, false) + if err != nil { + return nil, err + } + + nodes := make([]adidns.DNSNode, 0) + for _, nodeEntry := range nodeEntries { + nodeDN := nodeEntry.DN + nodeName := nodeEntry.GetAttributeValue("name") + dnsRecsStrs := nodeEntry.GetAttributeValues("dnsRecord") + + records := make([]adidns.DNSRecord, 0) + + for _, recordStr := range dnsRecsStrs { + dnsRec := new(adidns.DNSRecord) + dnsRec.Decode([]byte(recordStr)) + + records = append(records, *dnsRec) + } + + nodes = append(nodes, adidns.DNSNode{nodeDN, nodeName, records}) + } + + return nodes, nil +} + +func (lc *LDAPConn) AddADIDNSNode(objectName string, records []adidns.DNSRecord) error { + addRequest := ldap.NewAddRequest("DC="+objectName+",CN=MicrosoftDNS,DC=DomainDNSZones,"+lc.RootDN, nil) + addRequest.Attribute("objectClass", []string{"top", "dnsNode"}) + addRequest.Attribute("cn", []string{"Zone"}) + addRequest.Attribute("name", []string{objectName}) + + var dNSRecordList []string + for _, record := range records { + encodedProp, err := record.Encode() + if err == nil { + dNSRecordList = append(dNSRecordList, string(encodedProp)) + } + } + + addRequest.Attribute("dnsRecord", dNSRecordList) + + return lc.Conn.Add(addRequest) +} + // Attributes func (lc *LDAPConn) AddAttribute(targetDN string, attributeToAdd string, attributeValues []string) error { var err error diff --git a/utils/ldapcolors.go b/pkg/ldaputils/colors.go similarity index 99% rename from utils/ldapcolors.go rename to pkg/ldaputils/colors.go index 766cfc8..3b6ee92 100644 --- a/utils/ldapcolors.go +++ b/pkg/ldaputils/colors.go @@ -1,4 +1,4 @@ -package utils +package ldaputils import ( "strconv" diff --git a/utils/ldapemojis.go b/pkg/ldaputils/emojis.go similarity index 98% rename from utils/ldapemojis.go rename to pkg/ldaputils/emojis.go index beaed89..2ef4748 100644 --- a/utils/ldapemojis.go +++ b/pkg/ldaputils/emojis.go @@ -1,4 +1,4 @@ -package utils +package ldaputils var EmojiMap = map[string]string{ "root": "đŸŒŗ", diff --git a/utils/formats.go b/pkg/ldaputils/formats.go similarity index 51% rename from utils/formats.go rename to pkg/ldaputils/formats.go index ee37230..f5fbc41 100644 --- a/utils/formats.go +++ b/pkg/ldaputils/formats.go @@ -1,12 +1,17 @@ -package utils +package ldaputils import ( "encoding/binary" "encoding/hex" "fmt" + "sort" "strconv" "strings" + "time" "unicode" + + "github.com/Macmod/godap/v2/pkg/formats" + "github.com/go-ldap/ldap/v3" ) func HexToOffset(hex string) (integer int64) { @@ -142,3 +147,113 @@ func EncodeGUID(guid string) (string, error) { result += tokens[4] return result, nil } + +func FormatLDAPTime(val, format string) string { + layout := "20060102150405.0Z" + t, err := time.Parse(layout, val) + if err != nil { + return "Invalid date format" + } + + distString := formats.GetTimeDistString(time.Since(t)) + + return fmt.Sprintf("%s %s", t.Format(format), distString) +} + +func FormatLDAPAttribute(attr *ldap.EntryAttribute, timeFormat string) []string { + var formattedEntries = attr.Values + + if len(attr.Values) == 0 { + return []string{"(Empty)"} + } + + for idx, val := range attr.Values { + switch attr.Name { + case "objectSid": + formattedEntries = []string{"SID{" + ConvertSID(hex.EncodeToString(attr.ByteValues[idx])) + "}"} + case "objectGUID", "schemaIDGUID": + formattedEntries = []string{"GUID{" + ConvertGUID(hex.EncodeToString(attr.ByteValues[idx])) + "}"} + case "whenCreated", "whenChanged": + formattedEntries = []string{ + FormatLDAPTime(val, timeFormat), + } + case "lastLogonTimestamp", "accountExpires", "badPasswordTime", "lastLogoff", "lastLogon", "pwdLastSet", "creationTime", "lockoutTime": + if val == "0" { + return []string{"(Never)"} + } + + if attr.Name == "accountExpires" && val == "9223372036854775807" { + return []string{"(Never Expire)"} + } + + intValue, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return []string{"(Invalid)"} + } + + unixTime := (intValue - 116444736000000000) / 10000000 + t := time.Unix(unixTime, 0).UTC() + + distString := formats.GetTimeDistString(time.Since(t)) + + formattedEntries = []string{fmt.Sprintf("%s %s", t.Format(timeFormat), distString)} + case "userAccountControl": + uacInt, _ := strconv.Atoi(val) + + formattedEntries = []string{} + + uacFlagKeys := make([]int, 0) + for k, _ := range UacFlags { + uacFlagKeys = append(uacFlagKeys, k) + } + sort.Ints(uacFlagKeys) + + for _, flag := range uacFlagKeys { + curFlag := UacFlags[flag] + if uacInt&flag != 0 { + if curFlag.Present != "" { + formattedEntries = append(formattedEntries, curFlag.Present) + } + } else { + if curFlag.NotPresent != "" { + formattedEntries = append(formattedEntries, curFlag.NotPresent) + } + } + } + case "primaryGroupID": + rId, _ := strconv.Atoi(val) + + groupName, ok := RidMap[rId] + + if ok { + formattedEntries = []string{groupName} + } + case "sAMAccountType": + sAMAccountTypeId, _ := strconv.Atoi(val) + + accountType, ok := SAMAccountTypeMap[sAMAccountTypeId] + + if ok { + formattedEntries = []string{accountType} + } + case "groupType": + groupTypeId, _ := strconv.Atoi(val) + groupType, ok := GroupTypeMap[groupTypeId] + + if ok { + formattedEntries = []string{groupType} + } + case "instanceType": + instanceTypeId, _ := strconv.Atoi(val) + instanceType, ok := InstanceTypeMap[instanceTypeId] + + if ok { + formattedEntries = []string{instanceType} + } + default: + formattedEntries = attr.Values + } + } + + return formattedEntries +} diff --git a/utils/misc.go b/pkg/ldaputils/misc.go similarity index 88% rename from utils/misc.go rename to pkg/ldaputils/misc.go index 47d1dab..aaad3e5 100644 --- a/utils/misc.go +++ b/pkg/ldaputils/misc.go @@ -1,4 +1,4 @@ -package utils +package ldaputils func IndexOf[T comparable](collection []T, el T) int { for i, x := range collection { diff --git a/utils/ldapvars.go b/pkg/ldaputils/vars.go similarity index 99% rename from utils/ldapvars.go rename to pkg/ldaputils/vars.go index f68b5aa..478ff25 100644 --- a/utils/ldapvars.go +++ b/pkg/ldaputils/vars.go @@ -1,4 +1,4 @@ -package utils +package ldaputils // Constants for userAccountControl flags const ( diff --git a/sdl/ADGuids.go b/pkg/sdl/ADGuids.go similarity index 100% rename from sdl/ADGuids.go rename to pkg/sdl/ADGuids.go diff --git a/sdl/AceFieldMaps.go b/pkg/sdl/AceFieldMaps.go similarity index 100% rename from sdl/AceFieldMaps.go rename to pkg/sdl/AceFieldMaps.go diff --git a/sdl/AceTypeStructures.go b/pkg/sdl/AceTypeStructures.go similarity index 86% rename from sdl/AceTypeStructures.go rename to pkg/sdl/AceTypeStructures.go index 5f885aa..8571e8f 100644 --- a/sdl/AceTypeStructures.go +++ b/pkg/sdl/AceTypeStructures.go @@ -1,10 +1,10 @@ package sdl -//utils.HexToInt(ace.Header.ACEFlags) +//ldaputils.HexToInt(ace.Header.ACEFlags) import ( "fmt" - "github.com/Macmod/godap/v2/utils" + "github.com/Macmod/godap/v2/pkg/ldaputils" ) // ACE Header @@ -51,11 +51,11 @@ func (ace *BASIC_ACE) GetHeader() *ACEHEADER { } func (ace *BASIC_ACE) GetMask() int { - return utils.HexToInt(utils.EndianConvert(ace.Mask)) + return ldaputils.HexToInt(ldaputils.EndianConvert(ace.Mask)) } func (ace *BASIC_ACE) GetSID() string { - return utils.ConvertSID(ace.SID) + return ldaputils.ConvertSID(ace.SID) } func (ace *BASIC_ACE) SetHeader(header *ACEHEADER) { @@ -63,11 +63,11 @@ func (ace *BASIC_ACE) SetHeader(header *ACEHEADER) { } func (ace *BASIC_ACE) SetMask(mask int) { - ace.Mask = utils.EndianConvert(fmt.Sprintf("%08x", mask)) + ace.Mask = ldaputils.EndianConvert(fmt.Sprintf("%08x", mask)) } func (ace *BASIC_ACE) SetSID(sid string) error { - encodedSid, err := utils.EncodeSID(sid) + encodedSid, err := ldaputils.EncodeSID(sid) if err == nil { ace.SID = encodedSid @@ -106,7 +106,7 @@ func (ace *OBJECT_ACE) Parse(rawACE string) { ace.ObjectType = "" ace.InheritedObjectType = "" - switch utils.EndianConvert(ace.Flags) { + switch ldaputils.EndianConvert(ace.Flags) { case "00000001": ace.ObjectType = rawACE[24:56] case "00000002": @@ -139,16 +139,16 @@ func (ace *OBJECT_ACE) Encode() string { } func (ace *OBJECT_ACE) GetObjectAndInheritedType() (objectTypeGUID string, inheritedObjectTypeGUID string) { - switch utils.EndianConvert(ace.Flags) { + switch ldaputils.EndianConvert(ace.Flags) { case "00000001": - objectTypeGUID = utils.ConvertGUID(ace.ObjectType) + objectTypeGUID = ldaputils.ConvertGUID(ace.ObjectType) inheritedObjectTypeGUID = "" case "00000002": - inheritedObjectTypeGUID = utils.ConvertGUID(ace.InheritedObjectType) + inheritedObjectTypeGUID = ldaputils.ConvertGUID(ace.InheritedObjectType) objectTypeGUID = "" case "00000003": - objectTypeGUID = utils.ConvertGUID(ace.ObjectType) - inheritedObjectTypeGUID = utils.ConvertGUID(ace.InheritedObjectType) + objectTypeGUID = ldaputils.ConvertGUID(ace.ObjectType) + inheritedObjectTypeGUID = ldaputils.ConvertGUID(ace.InheritedObjectType) } return diff --git a/sdl/SDTypeStructures.go b/pkg/sdl/SDTypeStructures.go similarity index 78% rename from sdl/SDTypeStructures.go rename to pkg/sdl/SDTypeStructures.go index 0eb8a17..f773c63 100644 --- a/sdl/SDTypeStructures.go +++ b/pkg/sdl/SDTypeStructures.go @@ -4,7 +4,7 @@ import ( "fmt" "strconv" - "github.com/Macmod/godap/v2/utils" + "github.com/Macmod/godap/v2/pkg/ldaputils" ) // ACL @@ -17,7 +17,7 @@ func (acl *ACL) Parse(aclStr string) { acl.Header = getACLHeader(aclStr[:16]) rawACES := aclStr[16:] - aceCount, _ := strconv.Atoi(utils.HexToDecimalString(utils.EndianConvert(acl.Header.ACECount))) + aceCount, _ := strconv.Atoi(ldaputils.HexToDecimalString(ldaputils.EndianConvert(acl.Header.ACECount))) rawACESList := make([]string, aceCount) for i := 0; i < aceCount; i++ { @@ -123,28 +123,28 @@ func NewSD(sdStr string) *SecurityDescriptor { sd.Owner = "" sd.Group = "" - ownerOffset := int(utils.HexToOffset(sd.Header.OffsetOwner)) - ownerLen := utils.HexToInt(sdStr[ownerOffset+2:ownerOffset+4])*2*4 + 16 + ownerOffset := int(ldaputils.HexToOffset(sd.Header.OffsetOwner)) + ownerLen := ldaputils.HexToInt(sdStr[ownerOffset+2:ownerOffset+4])*2*4 + 16 if int(ownerOffset+ownerLen) <= len(sdStr) { sd.Owner = sdStr[ownerOffset : ownerOffset+ownerLen] } - groupOffset := int(utils.HexToOffset(sd.Header.OffsetGroup)) - groupLen := utils.HexToInt(sdStr[groupOffset+2:groupOffset+4])*2*4 + 16 + groupOffset := int(ldaputils.HexToOffset(sd.Header.OffsetGroup)) + groupLen := ldaputils.HexToInt(sdStr[groupOffset+2:groupOffset+4])*2*4 + 16 if int(groupOffset+groupLen) <= len(sdStr) { sd.Group = sdStr[groupOffset : groupOffset+groupLen] } // SACL sd.SACL = new(ACL) - saclOffset := utils.HexToOffset(sd.Header.OffsetSacl) + saclOffset := ldaputils.HexToOffset(sd.Header.OffsetSacl) if saclOffset != 0 { sd.SACL.Parse(sdStr[saclOffset:]) } // DACL sd.DACL = new(ACL) - daclOffset := utils.HexToOffset(sd.Header.OffsetDacl) + daclOffset := ldaputils.HexToOffset(sd.Header.OffsetDacl) if daclOffset != 0 { sd.DACL.Parse(sdStr[daclOffset:]) } @@ -153,21 +153,21 @@ func NewSD(sdStr string) *SecurityDescriptor { } func (sd *SecurityDescriptor) updateMetadata() { - sd.DACL.Header.ACECount = utils.EndianConvert(fmt.Sprintf("%04x", len(sd.DACL.Aces))) + sd.DACL.Header.ACECount = ldaputils.EndianConvert(fmt.Sprintf("%04x", len(sd.DACL.Aces))) mainDaclPart := sd.Header.Encode() + sd.SACL.Encode() + sd.DACL.Encode() - sd.Header.OffsetOwner = utils.EndianConvert(fmt.Sprintf("%08x", int(len(mainDaclPart)/2))) - sd.Header.OffsetGroup = utils.EndianConvert(fmt.Sprintf("%08x", int(len(mainDaclPart+sd.Owner)/2))) + sd.Header.OffsetOwner = ldaputils.EndianConvert(fmt.Sprintf("%08x", int(len(mainDaclPart)/2))) + sd.Header.OffsetGroup = ldaputils.EndianConvert(fmt.Sprintf("%08x", int(len(mainDaclPart+sd.Owner)/2))) - sd.DACL.Header.ACLSizeBytes = utils.EndianConvert(fmt.Sprintf("%04x", len(sd.DACL.Encode())/2)) + sd.DACL.Header.ACLSizeBytes = ldaputils.EndianConvert(fmt.Sprintf("%04x", len(sd.DACL.Encode())/2)) } func (sd *SecurityDescriptor) GetControl() int { - return utils.HexToInt(utils.EndianConvert(sd.Header.Control)) + return ldaputils.HexToInt(ldaputils.EndianConvert(sd.Header.Control)) } func (sd *SecurityDescriptor) SetControl(control int) { - sd.Header.Control = utils.EndianConvert(fmt.Sprintf("%04x", control)) + sd.Header.Control = ldaputils.EndianConvert(fmt.Sprintf("%04x", control)) } func (sd *SecurityDescriptor) SetOwnerAndGroup(ownerSID string, groupSID string) { diff --git a/sdl/SecurityDescriptorFuncs.go b/pkg/sdl/SecurityDescriptorFuncs.go similarity index 95% rename from sdl/SecurityDescriptorFuncs.go rename to pkg/sdl/SecurityDescriptorFuncs.go index 6b4aa4d..002b6ec 100644 --- a/sdl/SecurityDescriptorFuncs.go +++ b/pkg/sdl/SecurityDescriptorFuncs.go @@ -4,7 +4,7 @@ import ( "strconv" "strings" - "github.com/Macmod/godap/v2/utils" + "github.com/Macmod/godap/v2/pkg/ldaputils" ) // References @@ -13,7 +13,7 @@ import ( // - https://devblogs.microsoft.com/oldnewthing/20040315-00/?p=40253 func getACE(rawACE string) (ACE string) { - aceLengthBytes, _ := strconv.Atoi(utils.HexToDecimalString(utils.EndianConvert(rawACE[4:8]))) + aceLengthBytes, _ := strconv.Atoi(ldaputils.HexToDecimalString(ldaputils.EndianConvert(rawACE[4:8]))) aceLength := aceLengthBytes * 2 ACE = rawACE[:aceLength] @@ -37,7 +37,7 @@ func combinePerms(rights []int, rightNames []string, mask int) string { } } - return utils.Capitalize(strings.Join(combined, "/")) + return ldaputils.Capitalize(strings.Join(combined, "/")) } // At the moment this is an experimental & testing accuracy of the parser is hard. @@ -200,7 +200,7 @@ func AceMaskToText(mask int, guid string) ([]string, int) { func AceFlagsToText(flagsStr string, guidStr string) string { propagationString := "" - flags := utils.HexToInt(flagsStr) + flags := ldaputils.HexToInt(flagsStr) objectClassStr := "" if guidStr != "" { objectClassStr = ClassGuids[guidStr] + " " @@ -220,5 +220,5 @@ func AceFlagsToText(flagsStr string, guidStr string) string { propagationString += "descendant " + objectClassStr + "objects" } - return utils.Capitalize(propagationString) + return ldaputils.Capitalize(propagationString) } diff --git a/ace.go b/tui/ace.go similarity index 96% rename from ace.go rename to tui/ace.go index 6321287..02980f6 100644 --- a/ace.go +++ b/tui/ace.go @@ -1,4 +1,4 @@ -package main +package tui import ( "encoding/hex" @@ -6,8 +6,8 @@ import ( "sort" "strconv" - "github.com/Macmod/godap/v2/sdl" - "github.com/Macmod/godap/v2/utils" + "github.com/Macmod/godap/v2/pkg/ldaputils" + "github.com/Macmod/godap/v2/pkg/sdl" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) @@ -179,25 +179,25 @@ func createOrUpdateAce(aceIdx int, newAllowOrDeny bool, newACEFlags int, newMask if newObjectGuid != "" { newACE = new(sdl.OBJECT_ACE) - newObjectGuidEncoded, err := utils.EncodeGUID(newObjectGuid) + newObjectGuidEncoded, err := ldaputils.EncodeGUID(newObjectGuid) if err != nil { newObjectGuidEncoded = "" } - newInheritedGuidEncoded, err := utils.EncodeGUID(newInheritedGuid) + newInheritedGuidEncoded, err := ldaputils.EncodeGUID(newInheritedGuid) if err != nil { newInheritedGuidEncoded = "" } newFlags := getFlags(newObjectGuid, newInheritedGuid) - newACE.(*sdl.OBJECT_ACE).Flags = utils.EndianConvert(fmt.Sprintf("%08x", newFlags)) + newACE.(*sdl.OBJECT_ACE).Flags = ldaputils.EndianConvert(fmt.Sprintf("%08x", newFlags)) newACE.(*sdl.OBJECT_ACE).ObjectType = newObjectGuidEncoded newACE.(*sdl.OBJECT_ACE).InheritedObjectType = newInheritedGuidEncoded } else { newACE = new(sdl.BASIC_ACE) } - newACEHeader.ACEFlags = utils.EndianConvert(fmt.Sprintf("%02x", newACEFlags)) + newACEHeader.ACEFlags = ldaputils.EndianConvert(fmt.Sprintf("%02x", newACEFlags)) // Set ACE Mask newACE.SetMask(newMask) @@ -213,7 +213,7 @@ func createOrUpdateAce(aceIdx int, newAllowOrDeny bool, newACEFlags int, newMask // Update ACE size aceSizeBytes := len(newACE.Encode()) / 2 - newACE.GetHeader().AceSizeBytes = utils.EndianConvert(fmt.Sprintf("%04x", aceSizeBytes)) + newACE.GetHeader().AceSizeBytes = ldaputils.EndianConvert(fmt.Sprintf("%04x", aceSizeBytes)) var updatedAces []sdl.ACEInt @@ -484,7 +484,7 @@ func loadAceEditorForm(aceIdx int) { selectedPrincipal = aceEntry.SamAccountName // ACE Type - valType = utils.HexToInt(aceHeader.ACEType) + valType = ldaputils.HexToInt(aceHeader.ACEType) if valType == 0 || valType == 5 { selectedType = 0 } else { @@ -518,7 +518,7 @@ func loadAceEditorForm(aceIdx int) { valFlags = getFlags(valObjectGuid, valInheritedGuid) // ACE Scope - valACEFlags = utils.HexToInt(aceHeader.ACEFlags) + valACEFlags = ldaputils.HexToInt(aceHeader.ACEFlags) newACEFlags = valACEFlags if valACEFlags&sdl.AceFlagsMap["NO_PROPAGATE_INHERIT_ACE"] != 0 { @@ -540,7 +540,7 @@ func loadAceEditorForm(aceIdx int) { switch valKind { case 1: // Object - selectedObject = utils.IndexOf(classVals, sdl.ClassGuids[valObjectGuid]) + selectedObject = ldaputils.IndexOf(classVals, sdl.ClassGuids[valObjectGuid]) if valMask&rights["RIGHT_DS_CREATE_CHILD"] != 0 { selectedObjectRight = 0 if valMask&rights["RIGHT_DS_DELETE_CHILD"] != 0 { @@ -556,7 +556,7 @@ func loadAceEditorForm(aceIdx int) { propertyName, ok = sdl.PropertySetGuids[valObjectGuid] } - selectedProperty = utils.IndexOf(attributesVals, propertyName) + selectedProperty = ldaputils.IndexOf(attributesVals, propertyName) if newMask&rights["RIGHT_DS_READ_PROPERTY"] != 0 { selectedPropertyRight = 0 if newMask&rights["RIGHT_DS_WRITE_PROPERTY"] != 0 { @@ -566,12 +566,12 @@ func loadAceEditorForm(aceIdx int) { selectedPropertyRight = 1 } case 3: - selectedControlRight = utils.IndexOf( + selectedControlRight = ldaputils.IndexOf( extendedVals, sdl.ExtendedGuids[valObjectGuid], ) case 4: - selectedValidatedWriteRight = utils.IndexOf( + selectedValidatedWriteRight = ldaputils.IndexOf( validatedWriteRightsVals, sdl.ValidatedWriteGuids[valObjectGuid], ) } @@ -782,7 +782,7 @@ func loadAceEditorForm(aceIdx int) { if err == nil { updatePrincipalCell(newAceTable, newPrincipalSID) } else { - if utils.IsSID(principal) { + if ldaputils.IsSID(principal) { newPrincipalSID = principal } else { newPrincipalSID = "" diff --git a/cache.go b/tui/cache.go similarity index 99% rename from cache.go rename to tui/cache.go index 48078ea..4a305ed 100644 --- a/cache.go +++ b/tui/cache.go @@ -1,4 +1,4 @@ -package main +package tui import ( "regexp" diff --git a/dacl.go b/tui/dacl.go similarity index 94% rename from dacl.go rename to tui/dacl.go index eeb7dd9..df25c3e 100644 --- a/dacl.go +++ b/tui/dacl.go @@ -1,4 +1,4 @@ -package main +package tui import ( "encoding/hex" @@ -10,8 +10,8 @@ import ( "sync" "time" - "github.com/Macmod/godap/v2/sdl" - "github.com/Macmod/godap/v2/utils" + "github.com/Macmod/godap/v2/pkg/ldaputils" + "github.com/Macmod/godap/v2/pkg/sdl" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) @@ -44,7 +44,7 @@ func parseAces(dst *[]ParsedACE, srcSD *sdl.SecurityDescriptor) { var ACEFlags int switch aceVal := ace.(type) { case *sdl.BASIC_ACE: - sid := utils.ConvertSID(aceVal.SID) + sid := ldaputils.ConvertSID(aceVal.SID) if aceVal.Header.ACEType == "00" { entry.Type = "Allow" @@ -65,7 +65,7 @@ func parseAces(dst *[]ParsedACE, srcSD *sdl.SecurityDescriptor) { entry.SamAccountName = samAccountName } - ACEFlags = utils.HexToInt(aceVal.Header.ACEFlags) + ACEFlags = ldaputils.HexToInt(aceVal.Header.ACEFlags) if ACEFlags&sdl.AceFlagsMap["INHERITED_ACE"] != 0 { entry.Inheritance = true } @@ -74,11 +74,11 @@ func parseAces(dst *[]ParsedACE, srcSD *sdl.SecurityDescriptor) { entry.NoPropagate = true } - permissions := utils.HexToInt(utils.EndianConvert(aceVal.Mask)) + permissions := ldaputils.HexToInt(ldaputils.EndianConvert(aceVal.Mask)) entry.Mask, entry.Severity = sdl.AceMaskToText(permissions, "") case *sdl.OBJECT_ACE: - sid := utils.ConvertSID(aceVal.SID) + sid := ldaputils.ConvertSID(aceVal.SID) if aceVal.Header.ACEType == "05" { entry.Type = "Allow" @@ -98,7 +98,7 @@ func parseAces(dst *[]ParsedACE, srcSD *sdl.SecurityDescriptor) { entry.SamAccountName = samAccountName } - ACEFlags = utils.HexToInt(aceVal.Header.ACEFlags) + ACEFlags = ldaputils.HexToInt(aceVal.Header.ACEFlags) if ACEFlags&sdl.AceFlagsMap["INHERITED_ACE"] != 0 { entry.Inheritance = true } @@ -107,7 +107,7 @@ func parseAces(dst *[]ParsedACE, srcSD *sdl.SecurityDescriptor) { entry.NoPropagate = true } - permissions := utils.HexToInt(utils.EndianConvert(aceVal.Mask)) + permissions := ldaputils.HexToInt(ldaputils.EndianConvert(aceVal.Mask)) objectType, inheritedObjectType := aceVal.GetObjectAndInheritedType() entry.Mask, entry.Severity = sdl.AceMaskToText(permissions, objectType) entry.Scope = sdl.AceFlagsToText(aceVal.Header.ACEFlags, inheritedObjectType) @@ -294,14 +294,14 @@ func updateDaclEntries() { controlFlags := sd.GetControl() controlFlagsTextView.SetText(strconv.Itoa(controlFlags)) - ownerSID := utils.ConvertSID(sd.Owner) + ownerSID := ldaputils.ConvertSID(sd.Owner) ownerPrincipal, err = lc.FindSamForSID(ownerSID) if err == nil { daclOwnerTextView.SetText(ownerPrincipal) } else { daclOwnerTextView.SetText("[red]" + ownerSID) } - groupPrincipal, err = lc.FindSamForSID(utils.ConvertSID(sd.Group)) + groupPrincipal, err = lc.FindSamForSID(ldaputils.ConvertSID(sd.Group)) // For AD, groupPrincipal is not relevant, // so there's no need to show it in the UI @@ -342,7 +342,7 @@ func updateDaclEntries() { } principalName := entry.SamAccountName - if utils.IsSID(principalName) { + if ldaputils.IsSID(principalName) { principalName = "[red]" + principalName } @@ -399,7 +399,7 @@ func loadChangeOwnerForm() { newGroupSID := "" text := newOwnerFormItem.(*tview.InputField).GetText() - if utils.IsSID(text) { + if ldaputils.IsSID(text) { _, err := lc.FindSamForSID(text) if err == nil { newOwnerSID = text @@ -437,14 +437,14 @@ func loadChangeOwnerForm() { return } - encodedOwnerSID, err := utils.EncodeSID(newOwnerSID) + encodedOwnerSID, err := ldaputils.EncodeSID(newOwnerSID) if err != nil { updateLog(fmt.Sprint(err), "red") app.SetRoot(appPanel, true).SetFocus(daclEntriesPanel) return } - encodedGroupSID, err := utils.EncodeSID(newGroupSID) + encodedGroupSID, err := ldaputils.EncodeSID(newGroupSID) if err != nil { updateLog(fmt.Sprint(err), "red") app.SetRoot(appPanel, true).SetFocus(daclEntriesPanel) @@ -505,7 +505,7 @@ func loadChangeControlFlagsForm() { AddTextView("Raw ControlFlag Value", strconv.Itoa(checkboxState), 0, 1, false, true) controlFlagsKeys := make([]int, 0) - for key, _ := range utils.SDControlFlags { + for key, _ := range ldaputils.SDControlFlags { controlFlagsKeys = append(controlFlagsKeys, key) } sort.Ints(controlFlagsKeys) @@ -513,7 +513,7 @@ func loadChangeControlFlagsForm() { for _, val := range controlFlagsKeys { flagVal := val updateControlFlagsForm.AddCheckbox( - utils.SDControlFlags[flagVal], + ldaputils.SDControlFlags[flagVal], controlFlags&flagVal != 0, func(checked bool) { if checked { @@ -535,7 +535,7 @@ func loadChangeControlFlagsForm() { }). AddButton("Update", func() { - sd.Header.Control = utils.EndianConvert(fmt.Sprintf("%04x", checkboxState)) + sd.Header.Control = ldaputils.EndianConvert(fmt.Sprintf("%04x", checkboxState)) newSd, _ := hex.DecodeString(sd.Encode()) err = lc.ModifyDACL(object, string(newSd)) diff --git a/tui/dns.go b/tui/dns.go new file mode 100644 index 0000000..eb261fc --- /dev/null +++ b/tui/dns.go @@ -0,0 +1,725 @@ +package tui + +/* +{Reference} +- [MS-DNSP]: Domain Name Service (DNS) Server Management Protocol + https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/f97756c9-3783-428b-9451-b376f877319a +*/ + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "regexp" + "strings" + "sync" + "time" + + "github.com/Macmod/godap/v2/pkg/adidns" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +var ( + dnsTreePanel *tview.TreeView + dnsQueryPanel *tview.InputField + + dnsSidePanel *tview.Pages + dnsZoneProps *tview.Table + dnsNodeRecords *tview.TreeView + + dnsNodeFilter *tview.InputField + dnsZoneFilter *tview.InputField + + dnsPage *tview.Flex + + dnsRunControl sync.Mutex + dnsRunning bool +) + +var domainZones []adidns.DNSZone +var forestZones []adidns.DNSZone + +var zoneCache = make(map[string]adidns.DNSZone, 0) +var nodeCache = make(map[string]adidns.DNSNode, 0) + +var recordCache = make(map[string][]adidns.RecordContainer, 0) + +func getParentZone(objectDN string) (adidns.DNSZone, error) { + objectDNParts := strings.Split(objectDN, ",") + + if len(objectDNParts) > 1 { + parentZoneDN := strings.Join(objectDNParts[1:], ",") + parentZone, zoneOk := zoneCache[parentZoneDN] + if zoneOk { + return parentZone, nil + } else { + return adidns.DNSZone{}, fmt.Errorf("Parent zone not found in the cache") + } + } + + return adidns.DNSZone{}, fmt.Errorf("Object DN too small to contain a parent zone") +} + +func exportADIDNSToFile(currentNode *tview.TreeNode, outputFilename string) { + exportMap := make(map[string]any) + + currentNode.Walk(func(node, parent *tview.TreeNode) bool { + if node.GetReference() != nil { + objectDN := node.GetReference().(string) + + zone, zoneOk := zoneCache[objectDN] + node, nodeOk := nodeCache[objectDN] + + nodesMap := make(map[string]any, 0) + + if zoneOk { + zoneProps := make(map[string]any, 0) + for _, prop := range zone.Props { + propName := adidns.FindPropName(prop.Id) + zoneProps[propName] = prop.Data + } + + exportMap[objectDN] = map[string]any{ + "Zone": map[string]any{ + "Name": zone.Name, + "DN": zone.DN, + "Props": zoneProps, + }, + "Nodes": nodesMap, + } + } else if nodeOk { + records, _ := recordCache[objectDN] + + recordsObj := make([]any, 0) + for idx, rec := range records { + recordType := node.Records[idx].PrintType() + recordsObj = append(recordsObj, map[string]any{ + "Type": recordType, + "Name": rec.Name, + "Contents": rec.Contents, + }) + } + + parentZone, err := getParentZone(objectDN) + if err == nil { + // Since we're walking the tree it's safe to assume that + // zones will come before their child nodes, therefore + // all zone exports will fall in the alreadyExported branch + // The only way to get the other branch (alreadyExported) is if + // the user exports a node itself, in which case + // we must fetch the parent zone's properties + // to include in the export + _, alreadyExported := exportMap[parentZone.DN] + if !alreadyExported { + exportMap[parentZone.DN] = map[string]any{ + "Zone": parentZone, + "Nodes": nodesMap, + } + } + + parentZone := (exportMap[parentZone.DN]).(map[string]any) + parentZoneNodes := parentZone["Nodes"].(map[string]any) + parentZoneNodes[node.DN] = recordsObj + } + } + } + return true + }) + + jsonExportMap, _ := json.MarshalIndent(exportMap, "", " ") + + err := ioutil.WriteFile(outputFilename, jsonExportMap, 0644) + + if err != nil { + updateLog(fmt.Sprintf("%s", err), "red") + } else { + updateLog("File '"+outputFilename+"' saved successfully!", "green") + } +} + +func showZoneOrNodeDetails(objectDN string) { + zone, ok := zoneCache[objectDN] + if ok { + dnsSidePanel.SetTitle("dnsZone Properties") + dnsSidePanel.SwitchToPage("zone-props") + + propsMap := make(map[uint32]adidns.DNSProperty, 0) + for _, prop := range zone.Props { + propsMap[prop.Id] = prop + } + + dnsZoneProps.SetCell(0, 0, tview.NewTableCell("Id").SetSelectable(false)) + dnsZoneProps.SetCell(0, 1, tview.NewTableCell("Description").SetSelectable(false)) + dnsZoneProps.SetCell(0, 2, tview.NewTableCell("Value").SetSelectable(false)) + + idx := 1 + for _, prop := range adidns.DnsPropertyIds { + dnsZoneProps.SetCell(idx, 0, tview.NewTableCell(fmt.Sprint(prop.Id))) + dnsZoneProps.SetCell(idx, 1, tview.NewTableCell(prop.Name)) + + mappedProp, ok := propsMap[prop.Id] + if ok { + mappedPropStr := fmt.Sprintf("%v", mappedProp.Data) + if FormatAttrs { + mappedPropStr = mappedProp.Format(TimeFormat) + } + + if Colors { + color, change := adidns.GetPropCellColor(mappedProp.Id, mappedPropStr) + if change { + mappedPropStr = fmt.Sprintf("[%s]%s[c]", color, mappedPropStr) + } + } + + dnsZoneProps.SetCell(idx, 2, tview.NewTableCell(mappedPropStr)) + } else { + notSpecifiedVal := "Not specified" + if Colors { + notSpecifiedVal = fmt.Sprintf("[gray]%s[c]", notSpecifiedVal) + } + + dnsZoneProps.SetCell(idx, 2, tview.NewTableCell(notSpecifiedVal)) + } + idx += 1 + } + + return + } + + node, ok := nodeCache[objectDN] + if ok { + parsedRecords, _ := recordCache[objectDN] + parentZone, err := getParentZone(objectDN) + if err == nil { + dnsSidePanel.SetTitle(fmt.Sprintf("dnsNode Records (%s)", parentZone.Name)) + } else { + dnsSidePanel.SetTitle("dnsNode Records") + } + + dnsSidePanel.SwitchToPage("node-records") + + rootNode := tview.NewTreeNode(node.Name) + dnsNodeRecords.SetRoot(rootNode) + + for idx, record := range node.Records { + unixTimestamp := record.UnixTimestamp() + timeObj := time.Unix(unixTimestamp, 0) + + formattedTime := fmt.Sprintf("%d", unixTimestamp) + timeDistance := time.Since(timeObj) + if FormatAttrs { + if unixTimestamp != -1 { + formattedTime = timeObj.Format(TimeFormat) + } else { + formattedTime = "static" + } + } + + if Colors { + daysDiff := timeDistance.Hours() / 24 + color := "gray" + if unixTimestamp != -1 { + if daysDiff <= 7 { + color = "green" + } else if daysDiff <= 90 { + color = "yellow" + } else { + color = "red" + } + } + + formattedTime = fmt.Sprintf("[%s]%s[c]", color, formattedTime) + } + + nodeName := fmt.Sprintf( + "%s [TTL=%d] (%s)", + record.PrintType(), + record.TTLSeconds, + formattedTime, + ) + + recordTreeNode := tview.NewTreeNode(nodeName). + SetSelectable(true) + + parsedRecord := parsedRecords[idx] + recordFields := parsedRecord.DumpFields() + for _, field := range recordFields { + fieldName := tview.Escape(fmt.Sprintf("%s=%v", field.Name, field.Value)) + fieldTreeNode := tview.NewTreeNode(fieldName) + recordTreeNode.AddChild(fieldTreeNode) + } + + rootNode.AddChild(recordTreeNode) + } + } +} + +func storeNodeRecords(node adidns.DNSNode) { + records := make([]adidns.RecordContainer, 0) + var fRec adidns.FriendlyRecord + + for _, record := range node.Records { + switch record.Type { + case 0x0000: + fRec = new(adidns.ZERORecord) + case 0x0001: + fRec = new(adidns.ARecord) + case 0x0002: + fRec = new(adidns.NSRecord) + case 0x0003: + fRec = new(adidns.MDRecord) + case 0x0004: + fRec = new(adidns.MFRecord) + case 0x0005: + fRec = new(adidns.CNAMERecord) + case 0x0006: + fRec = new(adidns.SOARecord) + case 0x0007: + fRec = new(adidns.MBRecord) + case 0x0008: + fRec = new(adidns.MGRecord) + case 0x0009: + fRec = new(adidns.MRRecord) + case 0x000A: + fRec = new(adidns.NULLRecord) + case 0x000B: + fRec = new(adidns.WKSRecord) + case 0x000C: + fRec = new(adidns.PTRRecord) + case 0x000D: + fRec = new(adidns.HINFORecord) + case 0x000E: + fRec = new(adidns.MINFORecord) + case 0x000F: + fRec = new(adidns.MXRecord) + case 0x0010: + fRec = new(adidns.TXTRecord) + case 0x0011: + fRec = new(adidns.RPRecord) + case 0x0012: + fRec = new(adidns.AFSDBRecord) + case 0x0013: + fRec = new(adidns.X25Record) + case 0x0014: + fRec = new(adidns.ISDNRecord) + case 0x0015: + fRec = new(adidns.RTRecord) + case 0x0018: + fRec = new(adidns.SIGRecord) + case 0x0019: + fRec = new(adidns.KEYRecord) + case 0x001C: + fRec = new(adidns.AAAARecord) + case 0x001D: + fRec = new(adidns.LOCRecord) + case 0x001E: + fRec = new(adidns.NXTRecord) + case 0x0021: + fRec = new(adidns.SRVRecord) + case 0x0022: + fRec = new(adidns.ATMARecord) + case 0x0023: + fRec = new(adidns.NAPTRRecord) + case 0x0027: + fRec = new(adidns.DNAMERecord) + case 0x002B: + fRec = new(adidns.DSRecord) + case 0x002E: + fRec = new(adidns.RRSIGRecord) + case 0x002F: + fRec = new(adidns.NSECRecord) + case 0x0030: + fRec = new(adidns.DNSKEYRecord) + case 0x0031: + fRec = new(adidns.DHCIDRecord) + case 0x0032: + fRec = new(adidns.NSEC3Record) + case 0x0033: + fRec = new(adidns.NSEC3PARAMRecord) + case 0x0034: + fRec = new(adidns.TLSARecord) + case 0xFF01: + fRec = new(adidns.WINSRecord) + case 0xFF02: + fRec = new(adidns.WINSRRecord) + default: + continue + } + + fRec.Parse(record.Data) + + container := adidns.RecordContainer{ + node.Name, + fRec, + } + + records = append(records, container) + } + + recordCache[node.DN] = records +} + +func loadZoneNodes(zoneNode *tview.TreeNode) int { + zoneDN := zoneNode.GetReference().(string) + _, isZone := zoneCache[zoneDN] + if !isZone { + updateLog("The selected tree node is not a DNS zone", "red") + return -1 + } + + nodes, err := lc.GetADIDNSNodes(zoneDN) + if err != nil { + updateLog(fmt.Sprint(err), "red") + return -1 + } + + zoneNode.ClearChildren() + + nodeFilter := dnsNodeFilter.GetText() + nodeRegexp, err := regexp.Compile(nodeFilter) + + for _, node := range nodes { + nodeMatch := nodeRegexp.FindStringIndex(node.Name) + if nodeMatch == nil { + continue + } + + nodeCache[node.DN] = node + + nodeName := node.Name + if Emojis { + nodeName = "📃" + nodeName + } + + treeNode := tview.NewTreeNode(nodeName). + SetReference(node.DN). + SetSelectable(true). + SetExpanded(false) + + zoneNode.AddChild(treeNode) + storeNodeRecords(node) + } + + return len(nodes) +} + +func initADIDNSPage() { + dnsQueryPanel = tview.NewInputField() + dnsQueryPanel. + SetPlaceholder("Type a DNS zone or leave it blank and hit enter to query all zones"). + SetPlaceholderStyle(placeholderStyle). + SetPlaceholderTextColor(placeholderTextColor). + SetFieldBackgroundColor(fieldBackgroundColor). + SetTitle("Zone Search"). + SetBorder(true) + + dnsNodeFilter = tview.NewInputField() + dnsNodeFilter. + SetPlaceholder("Regex for dnsNode name"). + SetPlaceholderStyle(placeholderStyle). + SetPlaceholderTextColor(placeholderTextColor). + SetFieldBackgroundColor(fieldBackgroundColor). + SetTitle("dnsNode Filter"). + SetBorder(true) + + dnsZoneFilter = tview.NewInputField() + dnsZoneFilter. + SetPlaceholder("Regex for dnsZone name"). + SetPlaceholderStyle(placeholderStyle). + SetPlaceholderTextColor(placeholderTextColor). + SetFieldBackgroundColor(fieldBackgroundColor). + SetTitle("dnsZone Filter"). + SetBorder(true) + + dnsZoneProps = tview.NewTable(). + SetSelectable(true, true). + SetEvaluateAllRows(true) + + dnsNodeRecords = tview.NewTreeView() + + dnsTreePanel = tview.NewTreeView() + dnsTreePanel. + SetTitle("Search Results"). + SetBorder(true) + + dnsTreePanel.SetChangedFunc(func(objNode *tview.TreeNode) { + dnsZoneProps.Clear() + + objNodeRef := objNode.GetReference() + if objNodeRef == nil { + return + } + + nodeDN := objNodeRef.(string) + showZoneOrNodeDetails(nodeDN) + }) + + dnsZoneFilter.SetChangedFunc(func(text string) { + rebuildDnsTree(dnsTreePanel.GetRoot()) + }) + + dnsNodeFilter.SetChangedFunc(func(text string) { + rebuildDnsTree(dnsTreePanel.GetRoot()) + }) + + dnsQueryPanel.SetDoneFunc(dnsQueryDoneHandler) + + dnsTreePanel.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + currentNode := dnsTreePanel.GetCurrentNode() + if currentNode == nil || currentNode.GetReference() == nil { + return event + } + + objectDN := currentNode.GetReference().(string) + + switch event.Rune() { + case 'r', 'R': + if currentNode == dnsTreePanel.GetRoot() { + return nil + } + + go func() { + level := currentNode.GetLevel() + if level == 1 { + updateLog("Fetching nodes for zone '"+objectDN+"'...", "yellow") + + numLoadedNodes := loadZoneNodes(currentNode) + + if numLoadedNodes >= 0 { + updateLog(fmt.Sprintf("Loaded %d nodes (%s)", numLoadedNodes, objectDN), "green") + } + + if len(currentNode.GetChildren()) != 0 && !currentNode.IsExpanded() { + currentNode.SetExpanded(true) + } + } else if level == 2 { + node, err := lc.GetADIDNSNode(objectDN) + + if err == nil { + updateLog(fmt.Sprintf("Loaded node '%s'", node.DN), "green") + } else { + updateLog(fmt.Sprint(err), "red") + } + + storeNodeRecords(node) + showZoneOrNodeDetails(node.DN) + } + + app.Draw() + }() + + return nil + } + + switch event.Key() { + case tcell.KeyRight: + if len(currentNode.GetChildren()) != 0 && !currentNode.IsExpanded() { + currentNode.SetExpanded(true) + } + return nil + case tcell.KeyLeft: + if currentNode.IsExpanded() { // Collapse current node + currentNode.SetExpanded(false) + dnsTreePanel.SetCurrentNode(currentNode) + } else { // Collapse parent node + pathToCurrent := dnsTreePanel.GetPath(currentNode) + if len(pathToCurrent) > 1 { + parentNode := pathToCurrent[len(pathToCurrent)-2] + parentNode.SetExpanded(false) + dnsTreePanel.SetCurrentNode(parentNode) + } + } + return nil + case tcell.KeyDelete: + if currentNode.GetReference() != nil { + openDeleteObjectForm(currentNode, nil) + } + case tcell.KeyCtrlS: + unixTimestamp := time.Now().UnixMilli() + outputFilename := fmt.Sprintf("%d_dns.json", unixTimestamp) + exportADIDNSToFile(currentNode, outputFilename) + case tcell.KeyCtrlN: + /* + TODO: Create zones or nodes + */ + case tcell.KeyCtrlE: + /* + TODO: Edit node records or zone properties + */ + } + + return event + }) + + dnsSidePanel = tview.NewPages() + dnsSidePanel. + AddPage("zone-props", dnsZoneProps, true, true). + AddPage("node-records", dnsNodeRecords, true, true). + SetBorder(true) + + dnsPage = tview.NewFlex().SetDirection(tview.FlexRow). + AddItem( + tview.NewFlex(). + AddItem(dnsQueryPanel, 0, 2, false). + AddItem(dnsNodeFilter, 0, 1, false). + AddItem(dnsZoneFilter, 0, 1, false), + 3, 0, false, + ). + AddItem( + tview.NewFlex(). + AddItem(dnsTreePanel, 0, 1, false). + AddItem(dnsSidePanel, 0, 1, false), + 0, 8, false, + ) + + dnsPage.SetInputCapture(dnsPageKeyHandler) +} + +func dnsPageKeyHandler(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyTab || event.Key() == tcell.KeyBacktab { + dnsRotateFocus() + return nil + } + + return event +} + +func rebuildDnsTree(rootNode *tview.TreeNode) int { + expandedZones := make(map[string]bool) + childrenZones := rootNode.GetChildren() + for _, child := range childrenZones { + ref, ok := child.GetReference().(string) + if ok && child.IsExpanded() { + expandedZones[ref] = true + } + } + rootNode.ClearChildren() + + zoneFilter := dnsZoneFilter.GetText() + zoneRegexp, err := regexp.Compile(zoneFilter) + if err != nil { + updateLog("Invalid zone filter '"+zoneFilter+"' specified", "red") + return -1 + } + + totalNodes := 0 + for _, zone := range domainZones { + zoneCache[zone.DN] = zone + zoneMatch := zoneRegexp.FindStringIndex(zone.Name) + + if zoneMatch == nil { + continue + } + + zoneNodeName := zone.Name + if Emojis { + zoneNodeName = "🌐" + zoneNodeName + } + + childNode := tview.NewTreeNode(zoneNodeName). + SetReference(zone.DN). + SetExpanded(expandedZones[zone.DN]). + SetSelectable(true) + + totalNodes += loadZoneNodes(childNode) + rootNode.AddChild(childNode) + } + + for _, zone := range forestZones { + zoneCache[zone.DN] = zone + zoneMatch := zoneRegexp.FindStringIndex(zone.Name) + + if zoneMatch == nil { + continue + } + + zoneNodeName := zone.Name + if Emojis { + zoneNodeName = "🌲" + zoneNodeName + } + + childNode := tview.NewTreeNode(zoneNodeName). + SetReference(zone.DN). + SetExpanded(expandedZones[zone.DN]). + SetSelectable(true) + + totalNodes += loadZoneNodes(childNode) + rootNode.AddChild(childNode) + } + + go func() { + app.Draw() + }() + return totalNodes +} + +func dnsQueryDoneHandler(key tcell.Key) { + clear(nodeCache) + clear(zoneCache) + clear(domainZones) + clear(forestZones) + clear(recordCache) + + go func() { + dnsRunControl.Lock() + if dnsRunning { + dnsRunControl.Unlock() + updateLog("Another query is still running...", "yellow") + return + } + dnsRunning = true + dnsRunControl.Unlock() + + updateLog("Querying ADIDNS zones...", "yellow") + + targetZone := dnsQueryPanel.GetText() + + domainZones, _ = lc.GetADIDNSZones(targetZone, false) + forestZones, _ = lc.GetADIDNSZones(targetZone, true) + + totalZones := len(domainZones) + len(forestZones) + if totalZones == 0 { + updateLog("No ADIDNS zones found", "red") + rootNode.ClearChildren() + app.Draw() + + dnsRunControl.Lock() + dnsRunning = false + dnsRunControl.Unlock() + return + } + + // Setting up root node + rootNode := tview.NewTreeNode(lc.RootDN). + SetReference(lc.RootDN). + SetSelectable(true) + dnsTreePanel. + SetRoot(rootNode). + SetCurrentNode(rootNode) + + totalNodes := rebuildDnsTree(rootNode) + + updateLog(fmt.Sprintf("Found %d ADIDNS zones and %d nodes", totalZones, totalNodes), "green") + app.SetFocus(dnsTreePanel) + + app.Draw() + + dnsRunControl.Lock() + dnsRunning = false + dnsRunControl.Unlock() + }() +} + +func dnsRotateFocus() { + currentFocus := app.GetFocus() + + switch currentFocus { + case dnsTreePanel: + app.SetFocus(dnsQueryPanel) + case dnsQueryPanel: + app.SetFocus(dnsZoneProps) + case dnsZoneProps: + app.SetFocus(dnsTreePanel) + } +} diff --git a/explorer.go b/tui/explorer.go similarity index 97% rename from explorer.go rename to tui/explorer.go index b92d9a2..7b7e614 100644 --- a/explorer.go +++ b/tui/explorer.go @@ -1,4 +1,4 @@ -package main +package tui import ( "encoding/json" @@ -8,7 +8,7 @@ import ( "strconv" "time" - "github.com/Macmod/godap/v2/utils" + "github.com/Macmod/godap/v2/pkg/ldaputils" "github.com/gdamore/tcell/v2" "github.com/go-ldap/ldap/v3" "github.com/rivo/tview" @@ -33,20 +33,20 @@ func initExplorerPage() { treePanel = tview.NewTreeView() - rootNode = renderPartialTree(rootDN, searchFilter) + rootNode = renderPartialTree(RootDN, SearchFilter) treePanel.SetRoot(rootNode).SetCurrentNode(rootNode) explorerAttrsPanel = tview.NewTable().SetSelectable(true, true) searchFilterInput = tview.NewInputField(). SetFieldBackgroundColor(fieldBackgroundColor). - SetText(searchFilter) + SetText(SearchFilter) searchFilterInput.SetTitle("Expand Filter") searchFilterInput.SetBorder(true) rootDNInput = tview.NewInputField(). SetFieldBackgroundColor(fieldBackgroundColor). - SetText(rootDN) + SetText(RootDN) rootDNInput.SetTitle("Root DN") rootDNInput.SetBorder(true) @@ -57,7 +57,7 @@ func initExplorerPage() { // Event Handlers searchFilterInput.SetDoneFunc(func(key tcell.Key) { - searchFilter = searchFilterInput.GetText() + SearchFilter = searchFilterInput.GetText() reloadExplorerPage() }) @@ -125,7 +125,7 @@ func expandTreeNode(node *tview.TreeNode) { func collapseTreeNode(node *tview.TreeNode) { node.SetExpanded(false) - if !cacheEntries { + if !CacheEntries { unloadChildren(node) } } @@ -253,7 +253,7 @@ func openUpdateUacForm(node *tview.TreeNode, cache *EntryCache, done func()) { AddTextView("Raw UAC Value", strconv.Itoa(checkboxState), 0, 1, false, true) uacValues := make([]int, 0) - for key, _ := range utils.UacFlags { + for key, _ := range ldaputils.UacFlags { uacValues = append(uacValues, key) } sort.Ints(uacValues) @@ -261,7 +261,7 @@ func openUpdateUacForm(node *tview.TreeNode, cache *EntryCache, done func()) { for _, val := range uacValues { uacValue := val updateUacForm.AddCheckbox( - utils.UacFlags[uacValue].Present, + ldaputils.UacFlags[uacValue].Present, checkboxState&uacValue != 0, func(checked bool) { if checked { @@ -498,7 +498,7 @@ func treePanelKeyHandler(event *tcell.EventKey) *tcell.EventKey { }) case tcell.KeyCtrlN: openCreateObjectForm(currentNode, func() { - reloadExplorerAttrsPanel(currentNode, cacheEntries) + reloadExplorerAttrsPanel(currentNode, CacheEntries) unloadChildren(currentNode) loadChildren(currentNode) @@ -533,7 +533,7 @@ func treePanelKeyHandler(event *tcell.EventKey) *tcell.EventKey { func treePanelChangeHandler(node *tview.TreeNode) { go func() { // TODO: Implement cancellation - reloadExplorerAttrsPanel(node, cacheEntries) + reloadExplorerAttrsPanel(node, CacheEntries) }() } @@ -649,7 +649,7 @@ func explorerPageKeyHandler(event *tcell.EventKey) *tcell.EventKey { } treePanel.SetCurrentNode(otherNodeToSelect) - reloadExplorerAttrsPanel(otherNodeToSelect, cacheEntries) + reloadExplorerAttrsPanel(otherNodeToSelect, CacheEntries) }) } diff --git a/finder.go b/tui/finder.go similarity index 99% rename from finder.go rename to tui/finder.go index de56824..6a8ea2e 100644 --- a/finder.go +++ b/tui/finder.go @@ -1,4 +1,4 @@ -package main +package tui import ( "regexp" diff --git a/gpo.go b/tui/gpo.go similarity index 97% rename from gpo.go rename to tui/gpo.go index ab36366..6e9dcc6 100644 --- a/gpo.go +++ b/tui/gpo.go @@ -1,4 +1,4 @@ -package main +package tui import ( "encoding/json" @@ -10,7 +10,7 @@ import ( "sync" "time" - "github.com/Macmod/godap/v2/utils" + "github.com/Macmod/godap/v2/pkg/ldaputils" "github.com/gdamore/tcell/v2" "github.com/go-ldap/ldap/v3" "github.com/rivo/tview" @@ -308,8 +308,8 @@ func updateGPOEntries() { gpoChanged := entry.GetAttributeValue("whenChanged") gpoListPanel.SetCellSimple(idx+1, 0, gpoName) - gpoListPanel.SetCellSimple(idx+1, 1, utils.FormatLDAPTime(gpoCreated, timeFormat)) - gpoListPanel.SetCellSimple(idx+1, 2, utils.FormatLDAPTime(gpoChanged, timeFormat)) + gpoListPanel.SetCellSimple(idx+1, 1, ldaputils.FormatLDAPTime(gpoCreated, TimeFormat)) + gpoListPanel.SetCellSimple(idx+1, 2, ldaputils.FormatLDAPTime(gpoChanged, TimeFormat)) gpoListPanel.SetCellSimple(idx+1, 3, gpoGuid) } diff --git a/group.go b/tui/group.go similarity index 88% rename from group.go rename to tui/group.go index 8591123..7280b0c 100644 --- a/group.go +++ b/tui/group.go @@ -1,4 +1,4 @@ -package main +package tui import ( "encoding/json" @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/Macmod/godap/v2/utils" + "github.com/Macmod/godap/v2/pkg/ldaputils" "github.com/gdamore/tcell/v2" "github.com/go-ldap/ldap/v3" "github.com/rivo/tview" @@ -100,7 +100,7 @@ func initGroupPage() { membersPanel.Clear() queryGroup = groupNameInput.GetText() - samOrDn, isSam := utils.SamOrDN(queryGroup) + samOrDn, isSam := ldaputils.SamOrDN(queryGroup) groupDN = queryGroup if isSam { @@ -133,14 +133,14 @@ func initGroupPage() { var category string if len(categoryDN) > 0 { category = categoryDN[0] - if emojis { + if Emojis { switch category { case "CN=Person": - category = utils.EmojiMap["person"] + category = ldaputils.EmojiMap["person"] case "CN=Group": - category = utils.EmojiMap["group"] + category = ldaputils.EmojiMap["group"] case "CN=Computer": - category = utils.EmojiMap["computer"] + category = ldaputils.EmojiMap["computer"] } } } else { @@ -253,38 +253,38 @@ func exportCurrentMembers() { } func groupsKeyHandler(event *tcell.EventKey) *tcell.EventKey { - row, col := groupsPanel.GetSelection() + row, col := groupsPanel.GetSelection() switch event.Key() { case tcell.KeyCtrlS: exportCurrentGroups() return nil - case tcell.KeyCtrlG: - selCell := groupsPanel.GetCell(row, col) - if selCell != nil && selCell.GetReference() != nil { - baseDN := selCell.GetReference().(string) - openAddMemberToGroupForm(baseDN) - } - return nil + case tcell.KeyCtrlG: + selCell := groupsPanel.GetCell(row, col) + if selCell != nil && selCell.GetReference() != nil { + baseDN := selCell.GetReference().(string) + openAddMemberToGroupForm(baseDN) + } + return nil } return event } func membersKeyHandler(event *tcell.EventKey) *tcell.EventKey { - row, col := membersPanel.GetSelection() + row, col := membersPanel.GetSelection() switch event.Key() { case tcell.KeyCtrlS: exportCurrentMembers() return nil - case tcell.KeyCtrlG: - selCell := membersPanel.GetCell(row, col) - if selCell != nil && selCell.GetReference() != nil { - baseDN := selCell.GetReference().(string) - openAddMemberToGroupForm(baseDN) - } - return nil + case tcell.KeyCtrlG: + selCell := membersPanel.GetCell(row, col) + if selCell != nil && selCell.GetReference() != nil { + baseDN := selCell.GetReference().(string) + openAddMemberToGroupForm(baseDN) + } + return nil } return event diff --git a/help.go b/tui/help.go similarity index 93% rename from help.go rename to tui/help.go index 4dbf2e5..ab8e3b1 100644 --- a/help.go +++ b/tui/help.go @@ -1,11 +1,13 @@ -package main +package tui import ( "github.com/rivo/tview" ) -var helpPage *tview.Flex -var keybindingsPanel *tview.Table +var ( + helpPage *tview.Flex + keybindingsPanel *tview.Table +) func initHelpPage() { helpText := `[blue] @@ -18,7 +20,7 @@ func initHelpPage() { | (___) || (___) || (__/ )| ) ( || ) (_______)(_______)(______/ |/ \||/ -` + godapVer +` + GodapVer keybindings := [][]string{ {"Ctrl + Enter", "Global", "Next panel"}, @@ -53,6 +55,8 @@ func initHelpPage() { {"Ctrl + e", "DACL entries panel", "Edit the selected ACE of the current DACL"}, {"Delete", "DACL entries panel", "Deletes the selected ACE of the current DACL"}, {"Ctrl + s", "GPO page", "Export the current GPOs and their links into a JSON file"}, + {"Ctrl + s", "DNS zones panel", "Export the selected zones and their child DNS nodes into a JSON file"}, + {"r", "DNS zones panel", "Reload the nodes of the selected zone / the records of the selected node"}, {"h", "Global", "Show/hide headers"}, {"q", "Global", "Exit the program"}, } diff --git a/main.go b/tui/main.go similarity index 52% rename from main.go rename to tui/main.go index 2cc1600..71e34d8 100644 --- a/main.go +++ b/tui/main.go @@ -1,4 +1,4 @@ -package main +package tui import ( "crypto/tls" @@ -10,51 +10,47 @@ import ( "strings" "time" - "github.com/Macmod/godap/v2/utils" + "github.com/Macmod/godap/v2/pkg/ldaputils" "github.com/gdamore/tcell/v2" "github.com/go-ldap/ldap/v3" "github.com/rivo/tview" - "github.com/spf13/cobra" "h12.io/socks" ) -var godapVer = "Godap v2.6.0" +var GodapVer = "Godap v2.7.0" var ( - ldapServer string - ldapPort int - ldapUsername string - ldapPassword string - ldapPasswordFile string - ntlmHash string - ntlmHashFile string - domainName string - socksServer string - targetSpn string - kdcHost string - timeFormat string - - kerberos bool - emojis bool - colors bool - formatAttrs bool - expandAttrs bool - cacheEntries bool - deleted bool - loadSchema bool - pagingSize uint32 - timeout int32 - insecure bool - ldaps bool - searchFilter string - rootDN string - - tlsConfig *tls.Config - lc *utils.LDAPConn - err error - - page int - showHeader bool + LdapServer string + LdapPort int + LdapUsername string + LdapPassword string + LdapPasswordFile string + NtlmHash string + NtlmHashFile string + DomainName string + SocksServer string + TargetSpn string + KdcHost string + TimeFormat string + + Kerberos bool + Emojis bool + Colors bool + FormatAttrs bool + ExpandAttrs bool + AttrLimit int + CacheEntries bool + Deleted bool + LoadSchema bool + PagingSize uint32 + Timeout int32 + Insecure bool + Ldaps bool + SearchFilter string + RootDN string + ShowHeader bool + + page int ) var ( @@ -70,9 +66,16 @@ var ( colorFlagPanel *tview.TextView expandFlagPanel *tview.TextView deletedFlagPanel *tview.TextView + + tlsConfig *tls.Config + lc *ldaputils.LDAPConn + err error ) -var attrLimit int +type GodapPage struct { + prim tview.Primitive + title string +} var app = tview.NewApplication() @@ -84,124 +87,64 @@ var insecureTlsConfig = &tls.Config{InsecureSkipVerify: true} var secureTlsConfig = &tls.Config{InsecureSkipVerify: false} -func updateStateBox(target *tview.TextView, control bool) { - go func() { - app.QueueUpdateDraw(func() { - if control { - target.SetText("ON") - target.SetTextColor(tcell.GetColor("green")) - } else { - target.SetText("OFF") - target.SetTextColor(tcell.GetColor("red")) - } - }) - }() -} - -func updateLog(msg string, color string) { - currentTime := time.Now() - formattedTime := currentTime.Format("2006-01-02 15:04:05") - - logPanel.SetText("[" + formattedTime + "] " + msg).SetTextColor(tcell.GetColor(color)) -} - -func setPageFocus() { - switch page { - case 0: - app.SetFocus(treePanel) - case 1: - app.SetFocus(searchTreePanel) - case 2: - app.SetFocus(membersPanel) - case 3: - app.SetFocus(daclEntriesPanel) - case 4: - app.SetFocus(gpoListPanel) - case 5: - app.SetFocus(keybindingsPanel) - } -} - -func appKeyHandler(event *tcell.EventKey) *tcell.EventKey { - _, isTextArea := app.GetFocus().(*tview.TextArea) - _, isInputField := app.GetFocus().(*tview.InputField) - - if isTextArea || isInputField { - return event - } - - if event.Key() == tcell.KeyCtrlJ { - dstPage := (page + 1) % pages.GetPageCount() - info.Highlight(strconv.Itoa(dstPage)) - return nil - } - - if event.Rune() == 'q' { - app.Stop() - return nil - } - - return event -} - func toggleFlagF() { - formatAttrs = !formatAttrs - updateStateBox(formatFlagPanel, formatAttrs) + FormatAttrs = !FormatAttrs + updateStateBox(formatFlagPanel, FormatAttrs) nodeExplorer := treePanel.GetCurrentNode() if nodeExplorer != nil { - reloadExplorerAttrsPanel(nodeExplorer, cacheEntries) + reloadExplorerAttrsPanel(nodeExplorer, CacheEntries) } nodeSearch := searchTreePanel.GetCurrentNode() if nodeSearch != nil { - reloadSearchAttrsPanel(nodeSearch, cacheEntries) + reloadSearchAttrsPanel(nodeSearch, CacheEntries) } } func toggleFlagE() { - emojis = !emojis - updateStateBox(emojiFlagPanel, emojis) + Emojis = !Emojis + updateStateBox(emojiFlagPanel, Emojis) updateEmojis() } func toggleFlagC() { - colors = !colors - updateStateBox(colorFlagPanel, colors) + Colors = !Colors + updateStateBox(colorFlagPanel, Colors) nodeExplorer := treePanel.GetCurrentNode() if nodeExplorer != nil { - reloadExplorerAttrsPanel(nodeExplorer, cacheEntries) + reloadExplorerAttrsPanel(nodeExplorer, CacheEntries) } nodeSearch := searchTreePanel.GetCurrentNode() if nodeSearch != nil { - reloadSearchAttrsPanel(nodeSearch, cacheEntries) + reloadSearchAttrsPanel(nodeSearch, CacheEntries) } } func toggleFlagA() { - expandAttrs = !expandAttrs - updateStateBox(expandFlagPanel, expandAttrs) + ExpandAttrs = !ExpandAttrs + updateStateBox(expandFlagPanel, ExpandAttrs) nodeExplorer := treePanel.GetCurrentNode() if nodeExplorer != nil { - reloadExplorerAttrsPanel(nodeExplorer, cacheEntries) + reloadExplorerAttrsPanel(nodeExplorer, CacheEntries) } nodeSearch := searchTreePanel.GetCurrentNode() if nodeSearch != nil { - reloadSearchAttrsPanel(nodeSearch, cacheEntries) + reloadSearchAttrsPanel(nodeSearch, CacheEntries) } } func toggleFlagD() { - deleted = !deleted - updateStateBox(deletedFlagPanel, deleted) + Deleted = !Deleted + updateStateBox(deletedFlagPanel, Deleted) } func toggleHeader() { - showHeader = !showHeader - if showHeader { + ShowHeader = !ShowHeader + if ShowHeader { appPanel.RemoveItem(headerPanel) } else { appPanel.RemoveItem(pages) @@ -233,26 +176,26 @@ func reconnectLdap() { func openConfigForm() { credsForm := NewXForm() credsForm. - AddInputField("Server", ldapServer, 20, nil, nil). - AddInputField("Port", strconv.Itoa(ldapPort), 20, nil, nil). - AddInputField("Username", ldapUsername, 20, nil, nil). - AddPasswordField("Password", ldapPassword, 20, '*', nil). - AddCheckbox("LDAPS", ldaps, nil). - AddCheckbox("IgnoreCert", insecure, nil). - AddInputField("SOCKSProxy", socksServer, 20, nil, nil). + AddInputField("Server", LdapServer, 20, nil, nil). + AddInputField("Port", strconv.Itoa(LdapPort), 20, nil, nil). + AddInputField("Username", LdapUsername, 20, nil, nil). + AddPasswordField("Password", LdapPassword, 20, '*', nil). + AddCheckbox("LDAPS", Ldaps, nil). + AddCheckbox("IgnoreCert", Insecure, nil). + AddInputField("SOCKSProxy", SocksServer, 20, nil, nil). AddButton("Go Back", func() { app.SetRoot(appPanel, false).SetFocus(treePanel) }). AddButton("Update", func() { - ldapServer = credsForm.GetFormItemByLabel("Server").(*tview.InputField).GetText() - ldapPort, _ = strconv.Atoi(credsForm.GetFormItemByLabel("Port").(*tview.InputField).GetText()) - ldapUsername = credsForm.GetFormItemByLabel("Username").(*tview.InputField).GetText() - ldapPassword = credsForm.GetFormItemByLabel("Password").(*tview.InputField).GetText() + LdapServer = credsForm.GetFormItemByLabel("Server").(*tview.InputField).GetText() + LdapPort, _ = strconv.Atoi(credsForm.GetFormItemByLabel("Port").(*tview.InputField).GetText()) + LdapUsername = credsForm.GetFormItemByLabel("Username").(*tview.InputField).GetText() + LdapPassword = credsForm.GetFormItemByLabel("Password").(*tview.InputField).GetText() - ldaps = credsForm.GetFormItemByLabel("LDAPS").(*tview.Checkbox).IsChecked() - insecure = credsForm.GetFormItemByLabel("IgnoreCert").(*tview.Checkbox).IsChecked() + Ldaps = credsForm.GetFormItemByLabel("LDAPS").(*tview.Checkbox).IsChecked() + Insecure = credsForm.GetFormItemByLabel("IgnoreCert").(*tview.Checkbox).IsChecked() - socksServer = credsForm.GetFormItemByLabel("SOCKSProxy").(*tview.InputField).GetText() + SocksServer = credsForm.GetFormItemByLabel("SOCKSProxy").(*tview.InputField).GetText() app.SetRoot(appPanel, false).SetFocus(treePanel) }) @@ -316,27 +259,27 @@ func setupLDAPConn() error { } tlsConfig = secureTlsConfig - if insecure { + if Insecure { tlsConfig = insecureTlsConfig } var proxyConn net.Conn = nil var err error = nil - if socksServer != "" { - proxyDial := socks.Dial(socksServer) - proxyConn, err = proxyDial("tcp", fmt.Sprintf("%s:%s", ldapServer, strconv.Itoa(ldapPort))) + if SocksServer != "" { + proxyDial := socks.Dial(SocksServer) + proxyConn, err = proxyDial("tcp", fmt.Sprintf("%s:%s", LdapServer, strconv.Itoa(LdapPort))) if err != nil { updateLog(fmt.Sprint(err), "red") return err } } - ldap.DefaultTimeout = time.Duration(timeout) * time.Second + ldap.DefaultTimeout = time.Duration(Timeout) * time.Second - lc, err = utils.NewLDAPConn( - ldapServer, ldapPort, - ldaps, tlsConfig, pagingSize, rootDN, + lc, err = ldaputils.NewLDAPConn( + LdapServer, LdapPort, + Ldaps, tlsConfig, PagingSize, RootDN, proxyConn, ) @@ -346,23 +289,27 @@ func setupLDAPConn() error { updateLog("Connection success", "green") var bindType string - if kerberos { + if Kerberos { ccachePath := os.Getenv("KRB5CCNAME") - var kdcAddr string - if kdcHost != "" { - kdcAddr = kdcHost + var KdcAddr string + if KdcHost != "" { + KdcAddr = KdcHost } else { - kdcAddr = ldapServer + KdcAddr = LdapServer } - err = lc.KerbBindWithCCache(ccachePath, kdcAddr, domainName, targetSpn, "aes") + err = lc.KerbBindWithCCache(ccachePath, KdcAddr, DomainName, TargetSpn, "aes") bindType = "Kerberos" - } else if ntlmHash != "" { - err = lc.NTLMBindWithHash(domainName, ldapUsername, ntlmHash) + } else if NtlmHash != "" { + err = lc.NTLMBindWithHash(DomainName, LdapUsername, NtlmHash) bindType = "NTLM" } else { - err = lc.LDAPBind(ldapUsername, ldapPassword) + if !strings.Contains(LdapUsername, "@") && DomainName != "" { + LdapUsername += "@" + DomainName + } + + err = lc.LDAPBind(LdapUsername, LdapPassword) bindType = "LDAP" } @@ -374,14 +321,38 @@ func setupLDAPConn() error { } updateStateBox(statusPanel, err == nil) - if ldaps { + if Ldaps { updateStateBox(tlsPanel, err == nil) } return err } -func setupApp() { +func appKeyHandler(event *tcell.EventKey) *tcell.EventKey { + _, isTextArea := app.GetFocus().(*tview.TextArea) + _, isInputField := app.GetFocus().(*tview.InputField) + + if isTextArea || isInputField { + return event + } + + if event.Key() == tcell.KeyCtrlJ { + dstPage := (page + 1) % pages.GetPageCount() + info.Highlight(strconv.Itoa(dstPage)) + return nil + } + + if event.Rune() == 'q' { + app.Stop() + return nil + } + + return event +} + +func SetupApp() { + tview.Styles = baseTheme + logPanel = tview.NewTextView() logPanel.SetTitle("Last Log") logPanel.SetTextAlign(tview.AlignCenter).SetBorder(true) @@ -429,40 +400,46 @@ func setupApp() { SetBorder(true) // Time format setup - timeFormat = setupTimeFormat(timeFormat) + TimeFormat = setupTimeFormat(TimeFormat) err := setupLDAPConn() if err != nil { log.Fatal(err) } - if rootDN == "" { - rootDN, err = lc.FindRootDN() + if RootDN == "" { + RootDN, err = lc.FindRootDN() if err != nil { log.Fatal(err) } } - lc.RootDN = rootDN + lc.RootDN = RootDN // Pages setup + // TODO: Refactor this chunk initExplorerPage() initSearchPage() initGroupPage() - initDaclPage(loadSchema) + initDaclPage(LoadSchema) initGPOPage() + initADIDNSPage() initHelpPage() - pages.AddPage("page-0", explorerPage, true, true) - - pages.AddPage("page-1", searchPage, true, false) - - pages.AddPage("page-2", groupPage, true, false) - - pages.AddPage("page-3", daclPage, true, false) + pageVars := []GodapPage{ + GodapPage{explorerPage, "Explorer"}, + GodapPage{searchPage, "Search"}, + GodapPage{groupPage, "Groups"}, + GodapPage{daclPage, "DACLs"}, + GodapPage{gpoPage, "GPOs"}, + GodapPage{dnsPage, "ADIDNS"}, + GodapPage{helpPage, "Help"}, + } - pages.AddPage("page-4", gpoPage, true, false) + for idx, page := range pageVars { + pages.AddPage("page-"+strconv.Itoa(idx), page.prim, true, false) + } - pages.AddPage("page-5", helpPage, true, false) + pages.ShowPage("page-0") info.SetDynamicColors(true). SetRegions(true). @@ -479,12 +456,9 @@ func setupApp() { } }) - fmt.Fprintf(info, `%d ["%s"][darkcyan]%s[white][""] `, 1, "0", "LDAP Explorer") - fmt.Fprintf(info, `%d ["%s"][darkcyan]%s[white][""] `, 2, "1", "Object Search") - fmt.Fprintf(info, `%d ["%s"][darkcyan]%s[white][""] `, 3, "2", "Group Lookups") - fmt.Fprintf(info, `%d ["%s"][darkcyan]%s[white][""] `, 4, "3", "DACL Editor") - fmt.Fprintf(info, `%d ["%s"][darkcyan]%s[white][""] `, 5, "4", "GPO Viewer") - fmt.Fprintf(info, `%d ["%s"][darkcyan]%s[white][""] `, 6, "5", "Help") + for idx, page := range pageVars { + fmt.Fprintf(info, `%d ["%s"][darkcyan]%s[white][""] `, idx+1, strconv.Itoa(idx), page.title) + } info.Highlight("0") @@ -507,13 +481,13 @@ func setupApp() { app.EnableMouse(true) app.SetInputCapture(appKeyHandler) - updateStateBox(tlsPanel, ldaps) + updateStateBox(tlsPanel, Ldaps) updateStateBox(statusPanel, true) - updateStateBox(formatFlagPanel, formatAttrs) - updateStateBox(colorFlagPanel, colors) - updateStateBox(emojiFlagPanel, emojis) - updateStateBox(expandFlagPanel, expandAttrs) - updateStateBox(deletedFlagPanel, deleted) + updateStateBox(formatFlagPanel, FormatAttrs) + updateStateBox(colorFlagPanel, Colors) + updateStateBox(emojiFlagPanel, Emojis) + updateStateBox(expandFlagPanel, ExpandAttrs) + updateStateBox(deletedFlagPanel, Deleted) if err := app.SetRoot(appPanel, true).SetFocus(treePanel).Run(); err != nil { log.Fatal(err) @@ -538,74 +512,42 @@ func setupTimeFormat(f string) string { return f } -func main() { - tview.Styles = baseTheme - - rootCmd := &cobra.Command{ - Use: "godap ", - Short: "A complete TUI for LDAP.", - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - ldapServer = args[0] - - if ldapPasswordFile != "" { - pw, err := os.ReadFile(ldapPasswordFile) - if err != nil { - log.Fatal(err) - } - ldapPassword = strings.TrimSpace(string(pw)) - } - if ntlmHashFile != "" { - hash, err := os.ReadFile(ntlmHashFile) - if err != nil { - log.Fatal(err) - } - ntlmHash = strings.TrimSpace(string(hash)) +func updateStateBox(target *tview.TextView, control bool) { + go func() { + app.QueueUpdateDraw(func() { + if control { + target.SetText("ON") + target.SetTextColor(tcell.GetColor("green")) + } else { + target.SetText("OFF") + target.SetTextColor(tcell.GetColor("red")) } + }) + }() +} - setupApp() - }, - } - - rootCmd.Flags().IntVarP(&ldapPort, "port", "P", 389, "LDAP server port") - rootCmd.Flags().StringVarP(&ldapUsername, "username", "u", "", "LDAP username") - rootCmd.Flags().StringVarP(&ldapPassword, "password", "p", "", "LDAP password") - rootCmd.Flags().StringVarP(&ldapPasswordFile, "passfile", "", "", "Path to a file containing the LDAP password") - rootCmd.Flags().StringVarP(&domainName, "domain", "d", "", "Domain for NTLM / Kerberos authentication") - rootCmd.Flags().StringVarP(&ntlmHash, "hashes", "H", "", "NTLM hash") - rootCmd.Flags().BoolVarP(&kerberos, "kerberos", "k", false, "Use Kerberos ticket for authentication (CCACHE specified via KRB5CCNAME environment variable)") - rootCmd.Flags().StringVarP(&targetSpn, "spn", "t", "", "Target SPN to use for Kerberos bind (usually ldap/dchostname)") - rootCmd.Flags().StringVarP(&ntlmHashFile, "hashfile", "", "", "Path to a file containing the NTLM hash") - rootCmd.Flags().StringVarP(&rootDN, "rootDN", "r", "", "Initial root DN") - rootCmd.Flags().StringVarP(&searchFilter, "filter", "f", "(objectClass=*)", "Initial LDAP search filter") - rootCmd.Flags().BoolVarP(&emojis, "emojis", "E", true, "Prefix objects with emojis") - rootCmd.Flags().BoolVarP(&colors, "colors", "C", true, "Colorize objects") - rootCmd.Flags().BoolVarP(&formatAttrs, "format", "F", true, "Format attributes into human-readable values") - rootCmd.Flags().BoolVarP(&expandAttrs, "expand", "A", true, "Expand multi-value attributes") - rootCmd.Flags().IntVarP(&attrLimit, "limit", "L", 20, "Number of attribute values to render for multi-value attributes when -expand is set true") - rootCmd.Flags().BoolVarP(&cacheEntries, "cache", "M", true, "Keep loaded entries in memory while the program is open and don't query them again") - rootCmd.Flags().BoolVarP(&deleted, "deleted", "D", false, "Include deleted objects in all queries performed") - rootCmd.Flags().Int32VarP(&timeout, "timeout", "T", 10, "Timeout for LDAP connections in seconds") - rootCmd.Flags().BoolVarP(&loadSchema, "schema", "s", false, "Load schema GUIDs from the LDAP server during initialization") - rootCmd.Flags().Uint32VarP(&pagingSize, "paging", "G", 800, "Default paging size for regular queries") - rootCmd.Flags().BoolVarP(&insecure, "insecure", "I", false, "Skip TLS verification for LDAPS/StartTLS") - rootCmd.Flags().BoolVarP(&ldaps, "ldaps", "S", false, "Use LDAPS for initial connection") - rootCmd.Flags().StringVarP(&socksServer, "socks", "x", "", "Use a SOCKS proxy for initial connection") - rootCmd.Flags().StringVarP(&kdcHost, "kdc", "", "", "Address of the KDC to use with Kerberos authentication (optional: only if the KDC differs from the specified LDAP server)") - rootCmd.Flags().StringVarP(&timeFormat, "timefmt", "", "", "Time format for LDAP timestamps") - - versionCmd := &cobra.Command{ - Use: "version", - Short: "Print the version number of the application", - DisableFlagsInUseLine: true, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println(godapVer) - }, - } +func updateLog(msg string, color string) { + currentTime := time.Now() + formattedTime := currentTime.Format("2006-01-02 15:04:05") - rootCmd.AddCommand(versionCmd) + logPanel.SetText("[" + formattedTime + "] " + msg).SetTextColor(tcell.GetColor(color)) +} - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) +func setPageFocus() { + switch page { + case 0: + app.SetFocus(treePanel) + case 1: + app.SetFocus(searchTreePanel) + case 2: + app.SetFocus(membersPanel) + case 3: + app.SetFocus(daclEntriesPanel) + case 4: + app.SetFocus(gpoListPanel) + case 5: + app.SetFocus(dnsTreePanel) + case 6: + app.SetFocus(keybindingsPanel) } } diff --git a/main_test.go b/tui/main_test.go similarity index 98% rename from main_test.go rename to tui/main_test.go index 0ab5623..e542c8a 100644 --- a/main_test.go +++ b/tui/main_test.go @@ -1,4 +1,4 @@ -package main +package tui import ( "testing" diff --git a/search.go b/tui/search.go similarity index 97% rename from search.go rename to tui/search.go index 1b1efc3..2dbcc72 100644 --- a/search.go +++ b/tui/search.go @@ -1,4 +1,4 @@ -package main +package tui import ( "fmt" @@ -7,7 +7,7 @@ import ( "sync" "time" - "github.com/Macmod/godap/v2/utils" + "github.com/Macmod/godap/v2/pkg/ldaputils" "github.com/gdamore/tcell/v2" "github.com/go-ldap/ldap/v3" "github.com/rivo/tview" @@ -91,7 +91,7 @@ func initSearchPage() { predefinedLdapQueriesKeys := []string{"Security", "Users", "Computers", "Enum"} for _, key := range predefinedLdapQueriesKeys { - children := utils.PredefinedLdapQueries[key] + children := ldaputils.PredefinedLdapQueries[key] childNode := tview.NewTreeNode(key). SetSelectable(false). @@ -257,7 +257,7 @@ func searchQueryDoneHandler(key tcell.Key) { running = true runControl.Unlock() - entries, _ := lc.Query(lc.RootDN, searchQuery, ldap.ScopeWholeSubtree, deleted) + entries, _ := lc.Query(lc.RootDN, searchQuery, ldap.ScopeWholeSubtree, Deleted) firstLeaf := true @@ -286,8 +286,8 @@ func searchQueryDoneHandler(key tcell.Key) { SetExpanded(false). SetSelectable(true) - if colors { - color, changed := utils.GetEntryColor(entry) + if Colors { + color, changed := ldaputils.GetEntryColor(entry) if changed { childNode.SetColor(color) } diff --git a/theme.go b/tui/theme.go similarity index 99% rename from theme.go rename to tui/theme.go index 45eeafa..1fe23b4 100644 --- a/theme.go +++ b/tui/theme.go @@ -1,4 +1,4 @@ -package main +package tui import ( "github.com/gdamore/tcell/v2" diff --git a/tree.go b/tui/tree.go similarity index 93% rename from tree.go rename to tui/tree.go index db4f4f1..d02fa6c 100644 --- a/tree.go +++ b/tui/tree.go @@ -1,4 +1,4 @@ -package main +package tui import ( "bytes" @@ -9,7 +9,7 @@ import ( "strconv" "strings" - "github.com/Macmod/godap/v2/utils" + "github.com/Macmod/godap/v2/pkg/ldaputils" "github.com/gdamore/tcell/v2" "github.com/go-ldap/ldap/v3" "github.com/rivo/tview" @@ -26,8 +26,8 @@ func createTreeNodeFromEntry(entry *ldap.Entry) *tview.TreeNode { SetSelectable(true) // Helpful node coloring for deleted and disabled objects - if colors { - color, changed := utils.GetEntryColor(entry) + if Colors { + color, changed := ldaputils.GetEntryColor(entry) if changed { node.SetColor(color) } @@ -62,7 +62,7 @@ func unloadChildren(parentNode *tview.TreeNode) { // Loads child nodes and their attributes directly from LDAP func loadChildren(node *tview.TreeNode) { baseDN := node.GetReference().(string) - entries, err := lc.Query(baseDN, searchFilter, ldap.ScopeSingleLevel, deleted) + entries, err := lc.Query(baseDN, SearchFilter, ldap.ScopeSingleLevel, Deleted) if err != nil { updateLog(fmt.Sprint(err), "red") return @@ -349,7 +349,7 @@ func reloadAttributesPanel(node *tview.TreeNode, attrsTable *tview.Table, useCac return fmt.Errorf("Couldn't reload attributes: node not cached") } } else { - entries, err := lc.Query(baseDN, searchFilter, ldap.ScopeBaseObject, deleted) + entries, err := lc.Query(baseDN, SearchFilter, ldap.ScopeBaseObject, Deleted) if err != nil { updateLog(fmt.Sprint(err), "red") return err @@ -373,17 +373,17 @@ func reloadAttributesPanel(node *tview.TreeNode, attrsTable *tview.Table, useCac attrsTable.SetCell(row, 0, tview.NewTableCell(cellName)) - if formatAttrs { - cellValues = utils.FormatLDAPAttribute(attribute, timeFormat) + if FormatAttrs { + cellValues = ldaputils.FormatLDAPAttribute(attribute, TimeFormat) } else { cellValues = attribute.Values } - if !expandAttrs { + if !ExpandAttrs { myCell := tview.NewTableCell(strings.Join(cellValues, "; ")) - if colors { - color, ok := utils.GetAttrCellColor(cellName, attribute.Values[0]) + if Colors { + color, ok := ldaputils.GetAttrCellColor(cellName, attribute.Values[0]) if ok { myCell.SetTextColor(tcell.GetColor(color)) } @@ -397,15 +397,15 @@ func reloadAttributesPanel(node *tview.TreeNode, attrsTable *tview.Table, useCac for idx, cellValue := range cellValues { myCell := tview.NewTableCell(cellValue) - if colors { + if Colors { var refValue string - if !expandAttrs || len(cellValues) == 1 { + if !ExpandAttrs || len(cellValues) == 1 { refValue = attribute.Values[idx] } else { refValue = cellValue } - color, ok := utils.GetAttrCellColor(cellName, refValue) + color, ok := ldaputils.GetAttrCellColor(cellName, refValue) if ok { myCell.SetTextColor(tcell.GetColor(color)) @@ -415,11 +415,11 @@ func reloadAttributesPanel(node *tview.TreeNode, attrsTable *tview.Table, useCac if idx == 0 { attrsTable.SetCell(row, 1, myCell) } else { - if expandAttrs { - if attrLimit == -1 || idx < attrLimit { + if ExpandAttrs { + if AttrLimit == -1 || idx < AttrLimit { attrsTable.SetCell(row, 0, tview.NewTableCell("")) attrsTable.SetCell(row, 1, myCell) - if idx == attrLimit-1 { + if idx == AttrLimit-1 { attrsTable.SetCell(row+1, 1, tview.NewTableCell("[entries hidden]")) row = row + 2 break @@ -483,7 +483,7 @@ func getNodeName(entry *ldap.Entry) string { isDomain = true } - if emoji, ok := utils.EmojiMap[objectClass]; ok { + if emoji, ok := ldaputils.EmojiMap[objectClass]; ok { classEmojisBuf.WriteString(emoji) } } @@ -493,10 +493,10 @@ func getNodeName(entry *ldap.Entry) string { entryMarker := regexp.MustCompile("DEL:[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$") if len(emojisPrefix) == 0 { - emojisPrefix = utils.EmojiMap["container"] + emojisPrefix = ldaputils.EmojiMap["container"] } - if emojis { + if Emojis { return emojisPrefix + entryMarker.ReplaceAllString(getName(entry), "") } @@ -548,7 +548,7 @@ func updateEmojis() { } func renderPartialTree(rootDN string, searchFilter string) *tview.TreeNode { - rootEntry, err := lc.Query(rootDN, "(objectClass=*)", ldap.ScopeBaseObject, deleted) + rootEntry, err := lc.Query(rootDN, "(objectClass=*)", ldap.ScopeBaseObject, Deleted) if err != nil { updateLog(fmt.Sprint(err), "red") return nil @@ -575,7 +575,7 @@ func renderPartialTree(rootDN string, searchFilter string) *tview.TreeNode { } var rootEntries []*ldap.Entry - rootEntries, err = lc.Query(rootDN, searchFilter, ldap.ScopeSingleLevel, deleted) + rootEntries, err = lc.Query(rootDN, searchFilter, ldap.ScopeSingleLevel, Deleted) if err != nil { updateLog(fmt.Sprint(err), "red") return nil @@ -600,7 +600,7 @@ func reloadExplorerPage() { explorerAttrsPanel.Clear() explorerCache.Clear() - rootNode = renderPartialTree(lc.RootDN, searchFilter) + rootNode = renderPartialTree(lc.RootDN, SearchFilter) if rootNode != nil { numChildren := len(rootNode.GetChildren()) updateLog("Tree updated successfully ("+strconv.Itoa(numChildren)+" objects found)", "green") diff --git a/utils/ldapformat.go b/utils/ldapformat.go deleted file mode 100644 index 3c35979..0000000 --- a/utils/ldapformat.go +++ /dev/null @@ -1,121 +0,0 @@ -package utils - -import ( - "encoding/hex" - "fmt" - "sort" - "strconv" - "time" - - "github.com/go-ldap/ldap/v3" -) - -func FormatLDAPTime(val, format string) string { - layout := "20060102150405.0Z" - t, err := time.Parse(layout, val) - if err != nil { - return "Invalid date format" - } - - distString := GetTimeDistString(time.Since(t)) - - return fmt.Sprintf("%s %s", t.Format(format), distString) -} - -func FormatLDAPAttribute(attr *ldap.EntryAttribute, timeFormat string) []string { - var formattedEntries = attr.Values - - if len(attr.Values) == 0 { - return []string{"(Empty)"} - } - - for idx, val := range attr.Values { - switch attr.Name { - case "objectSid": - formattedEntries = []string{"SID{" + ConvertSID(hex.EncodeToString(attr.ByteValues[idx])) + "}"} - case "objectGUID", "schemaIDGUID": - formattedEntries = []string{"GUID{" + ConvertGUID(hex.EncodeToString(attr.ByteValues[idx])) + "}"} - case "whenCreated", "whenChanged": - formattedEntries = []string{ - FormatLDAPTime(val, timeFormat), - } - case "lastLogonTimestamp", "accountExpires", "badPasswordTime", "lastLogoff", "lastLogon", "pwdLastSet", "creationTime", "lockoutTime": - if val == "0" { - return []string{"(Never)"} - } - - if attr.Name == "accountExpires" && val == "9223372036854775807" { - return []string{"(Never Expire)"} - } - - intValue, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return []string{"(Invalid)"} - } - - unixTime := (intValue - 116444736000000000) / 10000000 - t := time.Unix(unixTime, 0).UTC() - - distString := GetTimeDistString(time.Since(t)) - - formattedEntries = []string{fmt.Sprintf("%s %s", t.Format(timeFormat), distString)} - case "userAccountControl": - uacInt, _ := strconv.Atoi(val) - - formattedEntries = []string{} - - uacFlagKeys := make([]int, 0) - for k, _ := range UacFlags { - uacFlagKeys = append(uacFlagKeys, k) - } - sort.Ints(uacFlagKeys) - - for _, flag := range uacFlagKeys { - curFlag := UacFlags[flag] - if uacInt&flag != 0 { - if curFlag.Present != "" { - formattedEntries = append(formattedEntries, curFlag.Present) - } - } else { - if curFlag.NotPresent != "" { - formattedEntries = append(formattedEntries, curFlag.NotPresent) - } - } - } - case "primaryGroupID": - rId, _ := strconv.Atoi(val) - - groupName, ok := RidMap[rId] - - if ok { - formattedEntries = []string{groupName} - } - case "sAMAccountType": - sAMAccountTypeId, _ := strconv.Atoi(val) - - accountType, ok := SAMAccountTypeMap[sAMAccountTypeId] - - if ok { - formattedEntries = []string{accountType} - } - case "groupType": - groupTypeId, _ := strconv.Atoi(val) - groupType, ok := GroupTypeMap[groupTypeId] - - if ok { - formattedEntries = []string{groupType} - } - case "instanceType": - instanceTypeId, _ := strconv.Atoi(val) - instanceType, ok := InstanceTypeMap[instanceTypeId] - - if ok { - formattedEntries = []string{instanceType} - } - default: - formattedEntries = attr.Values - } - } - - return formattedEntries -}