- 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
- 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
- }