package main import ( "fmt" "os" "runtime" "crypto/tls" "strings" "strconv" "math" "time" "github.com/akamensky/argparse" "github.com/go-ldap/ldap/v3" ) func main() { parser := argparse.NewParser("print", "AD/LDAP Account Brute-forcer ("+runtime.GOOS+") \n ex: ./progName -d <ip> -w <domain.com> --username=\"<username>\" --password=\"<password>\"") dc := parser.String("d", "dc", &argparse.Options{Required: true, Help: "DC to connect to, use IP or full hostname ex. --dc=\"dc.test.local\""}) domain := parser.String("w", "domain", &argparse.Options{Required: true, Help: "domain ex. --domain=\"test.local\""}) username := parser.String("u", "username", &argparse.Options{Required: false, Help: "username to connect with ex. --username=\"testuser\""}) password := parser.String("p", "password", &argparse.Options{Required: false, Help: "password to connect with ex. --password=\"testpass!\""}) var dispGroup *bool = parser.Flag("g", "groups", &argparse.Options{Required: false, Help: "display user/group relationships. -g"}) //target := parser.String("t", "target", &argparse.Options{Required: true, Help: "username to bruteforce -target=\"testuser2\""}) err := parser.Parse(os.Args) if err != nil { fmt.Print(parser.Usage(err)) os.Exit(1) } if len(*dc) == 0 || len(*domain) == 0{ fmt.Print(parser.Usage(err)) fmt.Println("[-] Provide DC & domain minimum") os.Exit(1) } fmt.Printf("[i] DC/AD: %v\n", *dc) fmt.Printf("[i] domain: %v\n", *domain) fmt.Println("[!] trying plaintext auth") l, err := ldap.DialURL("ldap://" + *dc) if err != nil { fmt.Println(err) fmt.Println("[!] trying TLS auth") l.Close() ldapURL := "ldaps://"+ *dc l, err2 := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true})) if err2 != nil { fmt.Println(err2) fmt.Println("[-] could not connect") os.Exit(1) } defer l.Close() } defer l.Close() if len(*username) == 0{ *username = "read-only-admin" } dcParts := strings.Split(*domain, ".") dcFmt := "CN="+*username var baseDN string for _, element := range dcParts { baseDN += ",DC="+element } dcFmt += baseDN baseDN = baseDN[1:] fmt.Println("[+] using: "+dcFmt) if len(*password) == 0{ fmt.Println("[!] trying to connect with unauth client") err = l.UnauthenticatedBind(dcFmt) if err != nil { fmt.Println("[-] unauth session failed - requires credentials") os.Exit(1) } }else{ fmt.Println("[!] trying to connect with supplied password") err = l.Bind(*username+"@"+*domain, *password) // FFS this stumped me for SO LONG! >.< if err != nil { fmt.Println(err) fmt.Println("[-] auth session failed - check credentials") os.Exit(1) } } // SEARCH FOR USERNAME'S "sAMAccountName" (basic test) // Filters must start and finish with ()! fmt.Print("[+] query shortname to get account username... ") DCQueryName := baseDN var mainUser string = "" filter := fmt.Sprintf("(CN=%s)", ldap.EscapeFilter(*username)) searchReq := ldap.NewSearchRequest(DCQueryName, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName","distinguishedName", "primaryGroupID"}, []ldap.Control{}) result, err := l.Search(searchReq) if err != nil { fmt.Print("[+] query full domain to get account username... ") DCQueryName = *domain filter = fmt.Sprintf("(CN=%s)", ldap.EscapeFilter(*username)) searchReq = ldap.NewSearchRequest(DCQueryName, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName","distinguishedName", "primaryGroupID"}, []ldap.Control{}) result, err = l.Search(searchReq) if err != nil { fmt.Println("[-] failed to query LDAP: %w", err) os.Exit(1) } } if len(result.Entries) == 0{ fmt.Println("[-] user can't query LDAP") os.Exit(1) } for _, entry := range result.Entries { mainUser = entry.GetAttributeValue("sAMAccountName") } fmt.Println( mainUser ) //result.PrettyPrint(2) //os.Exit(1) // SEARCH FOR ALL UERNAME'S fmt.Print("[+] ALL usernames... ") filter = fmt.Sprintf("(&(objectCategory=person)(objectClass=user)(SamAccountName=*))") searchReq = ldap.NewSearchRequest(DCQueryName, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName", "whenCreated", "whenChanged", "lastLogon","description"}, []ldap.Control{}) result, err = l.Search(searchReq) if err != nil { fmt.Println("[-] failed to query LDAP: %w", err) os.Exit(1) } fmt.Println( len(result.Entries)) //result.PrettyPrint(2) var foundUsers []string var foundUCreated []string var foundUChanged []string var foundULogon []string var foundUDesc []string for _, entry := range result.Entries { //fmt.Printf("%s: %v\n", entry.GetAttributeValues("memberOf"), entry.GetAttributeValue("sAMAccountName")) foundUsers = append(foundUsers, entry.GetAttributeValue("sAMAccountName") ) foundUCreated = append(foundUCreated, entry.GetAttributeValue("whenCreated") ) foundUChanged = append(foundUChanged, entry.GetAttributeValue("whenChanged") ) foundULogon = append(foundULogon, entry.GetAttributeValue("lastLogon") ) foundUDesc = append(foundUDesc, entry.GetAttributeValue("description") ) } // SEARCH FOR ALL LOCKED OUT ACCOUNTS fmt.Print("[+] locked accounts... ") filter = fmt.Sprintf("(&(sAMAccountType=805306368)(lockoutTime>=1))") searchReq = ldap.NewSearchRequest(DCQueryName, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName"}, []ldap.Control{}) result, err = l.Search(searchReq) if err != nil { fmt.Println("[-] failed to query LDAP: %w", err) os.Exit(1) } fmt.Println( len(result.Entries)) var lockedUsers = map[string]int{} for i := 0; i < len(foundUsers); i++ { lockedUsers[foundUsers[i]] = 0 } for _, entry := range result.Entries { //fmt.Printf("%s: %v\n", entry.GetAttributeValues("memberOf"), entry.GetAttributeValue("sAMAccountName")) lockedUsers[entry.GetAttributeValue("sAMAccountName")] = 1 } // SEARCH FOR ALL DISABLED ACCOUNTS fmt.Print("[+] disabled accounts... ") filter = fmt.Sprintf("(&(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=2))") searchReq = ldap.NewSearchRequest(DCQueryName, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName"}, []ldap.Control{}) result, err = l.Search(searchReq) if err != nil { fmt.Println("[-] failed to query LDAP: %w", err) os.Exit(1) } fmt.Println(len(result.Entries)) var disabledUsers = map[string]int{} for i := 0; i < len(foundUsers); i++ { disabledUsers[foundUsers[i]] = 0 } for _, entry := range result.Entries { disabledUsers[entry.GetAttributeValue("sAMAccountName")] = 1 } // SEARCH FOR ALL PASSWORD NEVER EXPIRE fmt.Print("[+] non-expire passwords... ") filter = fmt.Sprintf("(&(samAccountType=805306368)(|(UserAccountControl:1.2.840.113556.1.4.803:=65536)(msDS-UserDontExpirePassword=TRUE)))") searchReq = ldap.NewSearchRequest(DCQueryName, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName"}, []ldap.Control{}) result, err = l.Search(searchReq) if err != nil { fmt.Println("[-] failed to query LDAP: %w", err) os.Exit(1) } fmt.Println( len(result.Entries)) var neverExpireUsers = map[string]int{} for i := 0; i < len(foundUsers); i++ { neverExpireUsers[foundUsers[i]] = 0 } for _, entry := range result.Entries { neverExpireUsers[entry.GetAttributeValue("sAMAccountName")] = 1 } var groups = map[string][]string{} // SEARCH FOR ALL groups NOT "Default users" (rid 513) fmt.Print("[+] groups... ") filter = fmt.Sprintf("(&(objectCategory=group)(objectClass=group))") //filter = fmt.Sprintf("(&(CN=\"Administrator\"))") searchReq = ldap.NewSearchRequest(DCQueryName, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"objectCategory", "sAMAccountName", "distinguishedName"}, []ldap.Control{}) result, err = l.Search(searchReq) if err != nil { fmt.Println("[-] failed to query LDAP: %w", err) os.Exit(1) } //result.PrettyPrint(2) // for each group fmt.Println( len(result.Entries)) fmt.Print("[+] Matching users to groups.. this could take a while!\n") for _, entry := range result.Entries { // Search for all users of that group filter = fmt.Sprintf("(&(objectCategory=person)(objectClass=user)(SamAccountName=*)(memberOf:1.2.840.113556.1.4.1941:=%v))", strings.Trim(entry.GetAttributeValue("distinguishedName"), "\t \n" )) searchReq2 := ldap.NewSearchRequest(DCQueryName, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName"}, []ldap.Control{}) result2, err := l.Search(searchReq2) if err != nil { fmt.Println("[-] failed to query LDAP: %w", err) os.Exit(1) } if len(result2.Entries) != 0 { for _, entry2 := range result2.Entries { groups[entry.GetAttributeValue("sAMAccountName")] = append(groups[entry.GetAttributeValue("sAMAccountName")], entry2.GetAttributeValue("sAMAccountName")) } } //os.Exit(1) } // All users of Default Group (RID 513) filter = fmt.Sprintf("(&(objectCategory=person)(objectClass=user)(primaryGroupID=513))") searchReq2 := ldap.NewSearchRequest(DCQueryName, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName"}, []ldap.Control{}) result2, err := l.Search(searchReq2) if err != nil { fmt.Println("[-] failed to query LDAP: %w", err) os.Exit(1) } if len(result2.Entries) != 0 { for _, entry2 := range result2.Entries { groups["Default Group (RID 513)"] = append(groups["Default Group (RID 513)"], entry2.GetAttributeValue("sAMAccountName")) } } wantedUsers := []string{} for x, _ := range groups { // for each group addGroup := true for i := range groups[x] { if groups[x][i] == mainUser { // if user in group addGroup = false // dont add these users to list } } if addGroup == true{ for i := range groups[x] { // add the people to a list of accounts want to crack _, found := Find(wantedUsers, groups[x][i]) if !found { wantedUsers = append(wantedUsers, groups[x][i] ) } } } } if *dispGroup == true{ // display which users belong to which for x, _ := range groups { fmt.Printf("[+] group: %v \n", x) for i := range groups[x] { fmt.Println("[+] ", groups[x][i]) } } } // SEARCH FOR PASSWORD POLICY fmt.Println("[+] password policy ") filter = fmt.Sprintf("(objectClass=domainDNS)") searchReq = ldap.NewSearchRequest(DCQueryName, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"minPwdLength","minPwdAge","maxPwdAge","pwdHistoryLength","lockoutThreshold","lockoutDuration","lockOutObservationWindow"}, []ldap.Control{}) result, err = l.Search(searchReq) if err != nil { fmt.Println("[-] failed to query LDAP: %w", err) os.Exit(1) } fmt.Println("--- pwd pol ---") var foundGPO []string for _, entry := range result.Entries { foundGPO = []string{ entry.GetAttributeValue("minPwdLength"), entry.GetAttributeValue("minPwdAge"), convertPwdAge(entry.GetAttributeValue("maxPwdAge")), entry.GetAttributeValue("pwdHistoryLength"), entry.GetAttributeValue("lockoutThreshold"), convertLockout(entry.GetAttributeValue("lockoutDuration")), convertLockout(entry.GetAttributeValue("lockOutObservationWindow")), } } fmt.Printf("minPwdLength: %v \n",foundGPO[0]) fmt.Printf("minPwdAge: %v \n",foundGPO[1]) fmt.Printf("maxPwdAge: %v \n",foundGPO[2]) fmt.Printf("pwdHistoryLength: %v \n",foundGPO[3]) fmt.Printf("lockoutThreshold: %v \n",foundGPO[4]) fmt.Printf("lockoutDuration: %v \n",foundGPO[5]) fmt.Printf("lockOutObservationWindow: %v \n",foundGPO[6]) // display results fmt.Println("--- results ---") for i := 0; i < len(foundUsers); i++ { fmt.Printf("%-15v", foundUsers[i]) if lockedUsers[foundUsers[i]] == 1{ fmt.Print("Locked ")} if disabledUsers[foundUsers[i]] == 1{ fmt.Print("Disabled ")} if neverExpireUsers[foundUsers[i]] == 1{ fmt.Print("PassNevExp ")} fmt.Print("\n") lasLog := foundULogon[i] if lasLog != "0" { fmt.Printf(" %v ", convertLDAPDate(lasLog) ) fmt.Printf(" (%v)\n", ldapDiff(lasLog) ) }else{ fmt.Println(" Never Logged In") } lasDesc := foundUDesc[i] if lasDesc != "" { fmt.Printf(" (Desc: %v)\n", lasDesc ) } } fmt.Println("--- to try (ALL) ---") toTry := "" for i := 0; i < len(foundUsers); i++ { if lockedUsers[foundUsers[i]] == 0{ // account not locked if disabledUsers[foundUsers[i]] == 0{ // account not disabled if foundUsers[i] != mainUser{ // account not one used for scanning toTry += ", "+foundUsers[i] } } } } toTry = toTry[2:] // remove first "," fmt.Println(toTry) fmt.Println("--- to try (Diff Group) ---") toTry = "" for i := 0; i < len(wantedUsers); i++ { if lockedUsers[wantedUsers[i]] == 0{ // account not locked if disabledUsers[wantedUsers[i]] == 0{ // account not disabled toTry += ", "+wantedUsers[i] } } } toTry = toTry[2:] // remove first "," fmt.Println(toTry) } func ldapDiff(timestamp string)string{ i, _ := strconv.ParseInt(timestamp, 10, 64) date := ( i / 10000000 ) - 11644473600; t2 := time.Unix(date, 0) t1 := time.Now() diff := t1.Sub(t2) t := time.Time{}.Add(diff) // fmt.Println(diff) // DEBUG formatted := fmt.Sprintf("%d day(s) %02d:%02d:%02d",RoundTime(diff.Seconds()/86400),t.Hour(), t.Minute(), t.Second()) return formatted } func RoundTime(input float64) int { var result float64 if input < 0 { result = math.Ceil(input - 0.5) } else { result = math.Floor(input + 0.5) } // only interested in integer, ignore fractional i, _ := math.Modf(result) return int(i) } func convertLDAPDate(timestamp string)string{ //baseTime := time.Date(1601, 1, 1, 0, 0, 0, 0, time.UTC) i, _ := strconv.ParseInt(timestamp, 10, 64) date := ( i / 10000000 ) - 11644473600; t := time.Unix(date, 0) formatted := fmt.Sprintf(" %d-%02d-%02d %02d:%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) return formatted } func convertPwdAge(pwdage string) string { f, _ := strconv.ParseFloat((strings.Replace(pwdage, "-", "", -1)), 64) age := ((f / (60 * 10000000)) / 60) / 24 flr := math.Floor(age) s := strconv.Itoa(int(flr)) return s } func convertLockout(lockout string) string { i, _ := strconv.Atoi(strings.Replace(lockout, "-", "", -1)) age := i / (60 * 10000000) s := strconv.Itoa(age) return s } func after(value string, a string) string { // Get substring after a string. pos := strings.LastIndex(value, a) if pos == -1 { return "" } adjustedPos := pos + len(a) if adjustedPos >= len(value) { return "" } return value[adjustedPos:len(value)] } func Find(slice []string, val string) (int, bool) { for i, item := range slice { if item == val { return i, true } } return -1, false }