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 || len(result.Entries) == 0{
		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
}
