| | 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 to get account username... ") |
---|
| | var mainUser string = "" |
---|
| | filter := fmt.Sprintf("(CN=%s)", ldap.EscapeFilter(*username)) |
---|
| | searchReq := ldap.NewSearchRequest(baseDN, 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) |
---|
| | |
---|
| | // SEARCH FOR ALL UERNAME'S |
---|
| | fmt.Print("[+] ALL usernames... ") |
---|
| | filter = fmt.Sprintf("(&(objectCategory=person)(objectClass=user)(SamAccountName=*))") |
---|
| | searchReq = ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName", "whenCreated", "whenChanged", "lastLogon",}, []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 |
---|
| | 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") ) |
---|
| | } |
---|
| | |
---|
| | // SEARCH FOR ALL LOCKED OUT ACCOUNTS |
---|
| | fmt.Print("[+] locked accounts... ") |
---|
| | filter = fmt.Sprintf("(&(sAMAccountType=805306368)(lockoutTime>=1))") |
---|
| | searchReq = ldap.NewSearchRequest(baseDN, 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(baseDN, 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(baseDN, 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(baseDN, 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(baseDN, 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(baseDN, 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(baseDN, 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") |
---|
| | } |
---|
| | } |
---|
| | 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 |
---|
| | 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 |
---|
| | } |
---|
| | |
---|
| | |