Newer
Older
LdapUsrEnum / LdapUsrEnum.go
  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "os"
  6. "runtime"
  7. "crypto/tls"
  8. "strings"
  9. "strconv"
  10. "math"
  11. "time"
  12.  
  13. "github.com/akamensky/argparse"
  14. "github.com/go-ldap/ldap/v3"
  15. )
  16.  
  17. func main() {
  18.  
  19. parser := argparse.NewParser("print", "AD/LDAP Account Brute-forcer ("+runtime.GOOS+") \n ex: ./progName -d <ip> -w <domain.com> --username=\"<username>\" --password=\"<password>\"")
  20. dc := parser.String("d", "dc", &argparse.Options{Required: true, Help: "DC to connect to, use IP or full hostname ex. --dc=\"dc.test.local\""})
  21. domain := parser.String("w", "domain", &argparse.Options{Required: true, Help: "domain ex. --domain=\"test.local\""})
  22. username := parser.String("u", "username", &argparse.Options{Required: false, Help: "username to connect with ex. --username=\"testuser\""})
  23. password := parser.String("p", "password", &argparse.Options{Required: false, Help: "password to connect with ex. --password=\"testpass!\""})
  24. var dispGroup *bool = parser.Flag("g", "groups", &argparse.Options{Required: false, Help: "display user/group relationships. -g"})
  25. //target := parser.String("t", "target", &argparse.Options{Required: true, Help: "username to bruteforce -target=\"testuser2\""})
  26. err := parser.Parse(os.Args)
  27. if err != nil {
  28. fmt.Print(parser.Usage(err))
  29. os.Exit(1)
  30. }
  31.  
  32. if len(*dc) == 0 || len(*domain) == 0{
  33. fmt.Print(parser.Usage(err))
  34. fmt.Println("[-] Provide DC & domain minimum")
  35. os.Exit(1)
  36. }
  37.  
  38. fmt.Printf("[i] DC/AD: %v\n", *dc)
  39. fmt.Printf("[i] domain: %v\n", *domain)
  40. fmt.Println("[!] trying plaintext auth")
  41. l, err := ldap.DialURL("ldap://" + *dc)
  42. if err != nil {
  43. fmt.Println(err)
  44. fmt.Println("[!] trying TLS auth")
  45. l.Close()
  46. ldapURL := "ldaps://"+ *dc
  47. l, err2 := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}))
  48. if err2 != nil {
  49. fmt.Println(err2)
  50. fmt.Println("[-] could not connect")
  51. os.Exit(1)
  52. }
  53. defer l.Close()
  54. }
  55. defer l.Close()
  56.  
  57. if len(*username) == 0{
  58. *username = "read-only-admin"
  59. }
  60. dcParts := strings.Split(*domain, ".")
  61. dcFmt := "CN="+*username
  62. var baseDN string
  63. for _, element := range dcParts {
  64. baseDN += ",DC="+element
  65. }
  66. dcFmt += baseDN
  67. baseDN = baseDN[1:]
  68. fmt.Println("[+] using: "+dcFmt)
  69.  
  70. if len(*password) == 0{
  71. fmt.Println("[!] trying to connect with unauth client")
  72. err = l.UnauthenticatedBind(dcFmt)
  73. if err != nil {
  74. fmt.Println("[-] unauth session failed - requires credentials")
  75. os.Exit(1)
  76. }
  77. }else{
  78. fmt.Println("[!] trying to connect with supplied password")
  79. err = l.Bind(*username+"@"+*domain, *password) // FFS this stumped me for SO LONG! >.<
  80. if err != nil {
  81. fmt.Println(err)
  82. fmt.Println("[-] auth session failed - check credentials")
  83. os.Exit(1)
  84. }
  85. }
  86.  
  87.  
  88. // SEARCH FOR USERNAME'S "sAMAccountName" (basic test)
  89. // Filters must start and finish with ()!
  90. fmt.Print("[+] query to get account username... ")
  91. var mainUser string = ""
  92. filter := fmt.Sprintf("(CN=%s)", ldap.EscapeFilter(*username))
  93. searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName","distinguishedName", "primaryGroupID"}, []ldap.Control{})
  94. result, err := l.Search(searchReq)
  95. if err != nil {
  96. fmt.Println("[-] failed to query LDAP: %w", err)
  97. os.Exit(1)
  98. }
  99. if len(result.Entries) == 0{
  100. fmt.Println("[-] user can't query LDAP")
  101. os.Exit(1)
  102. }
  103. for _, entry := range result.Entries {
  104. mainUser = entry.GetAttributeValue("sAMAccountName")
  105. }
  106. fmt.Println( mainUser )
  107. //result.PrettyPrint(2)
  108.  
  109. // SEARCH FOR ALL UERNAME'S
  110. fmt.Print("[+] ALL usernames... ")
  111. filter = fmt.Sprintf("(&(objectCategory=person)(objectClass=user)(SamAccountName=*))")
  112. searchReq = ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName", "whenCreated", "whenChanged", "lastLogon",}, []ldap.Control{})
  113. result, err = l.Search(searchReq)
  114. if err != nil {
  115. fmt.Println("[-] failed to query LDAP: %w", err)
  116. os.Exit(1)
  117. }
  118. fmt.Println( len(result.Entries))
  119. //result.PrettyPrint(2)
  120.  
  121. var foundUsers []string
  122. var foundUCreated []string
  123. var foundUChanged []string
  124. var foundULogon []string
  125. for _, entry := range result.Entries {
  126. //fmt.Printf("%s: %v\n", entry.GetAttributeValues("memberOf"), entry.GetAttributeValue("sAMAccountName"))
  127. foundUsers = append(foundUsers, entry.GetAttributeValue("sAMAccountName") )
  128. foundUCreated = append(foundUCreated, entry.GetAttributeValue("whenCreated") )
  129. foundUChanged = append(foundUChanged, entry.GetAttributeValue("whenChanged") )
  130. foundULogon = append(foundULogon, entry.GetAttributeValue("lastLogon") )
  131. }
  132.  
  133. // SEARCH FOR ALL LOCKED OUT ACCOUNTS
  134. fmt.Print("[+] locked accounts... ")
  135. filter = fmt.Sprintf("(&(sAMAccountType=805306368)(lockoutTime>=1))")
  136. searchReq = ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName"}, []ldap.Control{})
  137. result, err = l.Search(searchReq)
  138. if err != nil {
  139. fmt.Println("[-] failed to query LDAP: %w", err)
  140. os.Exit(1)
  141. }
  142. fmt.Println( len(result.Entries))
  143.  
  144. var lockedUsers = map[string]int{}
  145. for i := 0; i < len(foundUsers); i++ {
  146. lockedUsers[foundUsers[i]] = 0
  147. }
  148. for _, entry := range result.Entries {
  149. //fmt.Printf("%s: %v\n", entry.GetAttributeValues("memberOf"), entry.GetAttributeValue("sAMAccountName"))
  150. lockedUsers[entry.GetAttributeValue("sAMAccountName")] = 1
  151. }
  152.  
  153. // SEARCH FOR ALL DISABLED ACCOUNTS
  154. fmt.Print("[+] disabled accounts... ")
  155. filter = fmt.Sprintf("(&(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=2))")
  156. searchReq = ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName"}, []ldap.Control{})
  157. result, err = l.Search(searchReq)
  158. if err != nil {
  159. fmt.Println("[-] failed to query LDAP: %w", err)
  160. os.Exit(1)
  161. }
  162. fmt.Println(len(result.Entries))
  163.  
  164. var disabledUsers = map[string]int{}
  165. for i := 0; i < len(foundUsers); i++ {
  166. disabledUsers[foundUsers[i]] = 0
  167. }
  168. for _, entry := range result.Entries {
  169. disabledUsers[entry.GetAttributeValue("sAMAccountName")] = 1
  170. }
  171.  
  172. // SEARCH FOR ALL PASSWORD NEVER EXPIRE
  173. fmt.Print("[+] non-expire passwords... ")
  174. filter = fmt.Sprintf("(&(samAccountType=805306368)(|(UserAccountControl:1.2.840.113556.1.4.803:=65536)(msDS-UserDontExpirePassword=TRUE)))")
  175. searchReq = ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName"}, []ldap.Control{})
  176. result, err = l.Search(searchReq)
  177. if err != nil {
  178. fmt.Println("[-] failed to query LDAP: %w", err)
  179. os.Exit(1)
  180. }
  181. fmt.Println( len(result.Entries))
  182.  
  183. var neverExpireUsers = map[string]int{}
  184. for i := 0; i < len(foundUsers); i++ {
  185. neverExpireUsers[foundUsers[i]] = 0
  186. }
  187. for _, entry := range result.Entries {
  188. neverExpireUsers[entry.GetAttributeValue("sAMAccountName")] = 1
  189. }
  190.  
  191. var groups = map[string][]string{}
  192.  
  193. // SEARCH FOR ALL groups NOT "Default users" (rid 513)
  194. fmt.Print("[+] groups... ")
  195. filter = fmt.Sprintf("(&(objectCategory=group)(objectClass=group))")
  196. //filter = fmt.Sprintf("(&(CN=\"Administrator\"))")
  197. searchReq = ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"objectCategory", "sAMAccountName", "distinguishedName"}, []ldap.Control{})
  198. result, err = l.Search(searchReq)
  199. if err != nil {
  200. fmt.Println("[-] failed to query LDAP: %w", err)
  201. os.Exit(1)
  202. }
  203. //result.PrettyPrint(2)
  204.  
  205. // for each group
  206. fmt.Println( len(result.Entries))
  207. fmt.Print("[+] Matching users to groups.. this could take a while!\n")
  208. for _, entry := range result.Entries {
  209.  
  210. // Search for all users of that group
  211. filter = fmt.Sprintf("(&(objectCategory=person)(objectClass=user)(SamAccountName=*)(memberOf:1.2.840.113556.1.4.1941:=%v))", strings.Trim(entry.GetAttributeValue("distinguishedName"), "\t \n" ))
  212. searchReq2 := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName"}, []ldap.Control{})
  213. result2, err := l.Search(searchReq2)
  214. if err != nil {
  215. fmt.Println("[-] failed to query LDAP: %w", err)
  216. os.Exit(1)
  217. }
  218. if len(result2.Entries) != 0 {
  219. for _, entry2 := range result2.Entries {
  220. groups[entry.GetAttributeValue("sAMAccountName")] = append(groups[entry.GetAttributeValue("sAMAccountName")], entry2.GetAttributeValue("sAMAccountName"))
  221. }
  222. }
  223. //os.Exit(1)
  224. }
  225.  
  226. // All users of Default Group (RID 513)
  227. filter = fmt.Sprintf("(&(objectCategory=person)(objectClass=user)(primaryGroupID=513))")
  228. searchReq2 := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName"}, []ldap.Control{})
  229. result2, err := l.Search(searchReq2)
  230. if err != nil {
  231. fmt.Println("[-] failed to query LDAP: %w", err)
  232. os.Exit(1)
  233. }
  234. if len(result2.Entries) != 0 {
  235. for _, entry2 := range result2.Entries {
  236. groups["Default Group (RID 513)"] = append(groups["Default Group (RID 513)"], entry2.GetAttributeValue("sAMAccountName"))
  237. }
  238. }
  239.  
  240.  
  241. wantedUsers := []string{}
  242. for x, _ := range groups { // for each group
  243. addGroup := true
  244. for i := range groups[x] {
  245. if groups[x][i] == mainUser { // if user in group
  246. addGroup = false // dont add these users to list
  247. }
  248. }
  249. if addGroup == true{
  250. for i := range groups[x] { // add the people to a list of accounts want to crack
  251. _, found := Find(wantedUsers, groups[x][i])
  252. if !found {
  253. wantedUsers = append(wantedUsers, groups[x][i] )
  254. }
  255. }
  256. }
  257. }
  258.  
  259. if *dispGroup == true{ // display which users belong to which
  260. for x, _ := range groups {
  261. fmt.Printf("[+] group: %v \n", x)
  262. for i := range groups[x] {
  263. fmt.Println("[+] ", groups[x][i])
  264. }
  265. }
  266. }
  267.  
  268. // SEARCH FOR PASSWORD POLICY
  269. fmt.Println("[+] password policy ")
  270. filter = fmt.Sprintf("(objectClass=domainDNS)")
  271. searchReq = ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"minPwdLength","minPwdAge","maxPwdAge","pwdHistoryLength","lockoutThreshold","lockoutDuration","lockOutObservationWindow"}, []ldap.Control{})
  272. result, err = l.Search(searchReq)
  273. if err != nil {
  274. fmt.Println("[-] failed to query LDAP: %w", err)
  275. os.Exit(1)
  276. }
  277. fmt.Println("--- pwd pol ---")
  278. var foundGPO []string
  279. for _, entry := range result.Entries {
  280. foundGPO = []string{
  281. entry.GetAttributeValue("minPwdLength"),
  282. entry.GetAttributeValue("minPwdAge"),
  283. convertPwdAge(entry.GetAttributeValue("maxPwdAge")),
  284. entry.GetAttributeValue("pwdHistoryLength"),
  285. entry.GetAttributeValue("lockoutThreshold"),
  286. convertLockout(entry.GetAttributeValue("lockoutDuration")),
  287. convertLockout(entry.GetAttributeValue("lockOutObservationWindow")),
  288. }
  289. }
  290. fmt.Printf("minPwdLength: %v \n",foundGPO[0])
  291. fmt.Printf("minPwdAge: %v \n",foundGPO[1])
  292. fmt.Printf("maxPwdAge: %v \n",foundGPO[2])
  293. fmt.Printf("pwdHistoryLength: %v \n",foundGPO[3])
  294. fmt.Printf("lockoutThreshold: %v \n",foundGPO[4])
  295. fmt.Printf("lockoutDuration: %v \n",foundGPO[5])
  296. fmt.Printf("lockOutObservationWindow: %v \n",foundGPO[6])
  297.  
  298. // display results
  299. fmt.Println("--- results ---")
  300. for i := 0; i < len(foundUsers); i++ {
  301. fmt.Printf("%-15v", foundUsers[i])
  302. if lockedUsers[foundUsers[i]] == 1{ fmt.Print("Locked ")}
  303. if disabledUsers[foundUsers[i]] == 1{ fmt.Print("Disabled ")}
  304. if neverExpireUsers[foundUsers[i]] == 1{ fmt.Print("PassNevExp ")}
  305. fmt.Print("\n")
  306. lasLog := foundULogon[i]
  307. if lasLog != "0" {
  308. fmt.Printf(" %v ", convertLDAPDate(lasLog) )
  309. fmt.Printf(" (%v)\n", ldapDiff(lasLog) )
  310. }else{
  311. fmt.Println(" Never Logged In")
  312. }
  313. }
  314. fmt.Println("--- to try (ALL) ---")
  315. toTry := ""
  316. for i := 0; i < len(foundUsers); i++ {
  317. if lockedUsers[foundUsers[i]] == 0{ // account not locked
  318. if disabledUsers[foundUsers[i]] == 0{ // account not disabled
  319. if foundUsers[i] != mainUser{ // account not one used for scanning
  320. toTry += ", "+foundUsers[i]
  321. }
  322. }
  323. }
  324. }
  325. toTry = toTry[2:] // remove first ","
  326. fmt.Println(toTry)
  327.  
  328. fmt.Println("--- to try (Diff Group) ---")
  329. toTry = ""
  330. for i := 0; i < len(wantedUsers); i++ {
  331. if lockedUsers[wantedUsers[i]] == 0{ // account not locked
  332. if disabledUsers[wantedUsers[i]] == 0{ // account not disabled
  333. toTry += ", "+wantedUsers[i]
  334. }
  335. }
  336. }
  337. toTry = toTry[2:] // remove first ","
  338. fmt.Println(toTry)
  339.  
  340.  
  341. }
  342.  
  343. func ldapDiff(timestamp string)string{
  344. i, _ := strconv.ParseInt(timestamp, 10, 64)
  345. date := ( i / 10000000 ) - 11644473600;
  346. t2 := time.Unix(date, 0)
  347. t1 := time.Now()
  348.  
  349. diff := t1.Sub(t2)
  350. t := time.Time{}.Add(diff)
  351. // fmt.Println(diff) // DEBUG
  352. formatted := fmt.Sprintf("%d day(s) %02d:%02d:%02d",RoundTime(diff.Seconds()/86400),t.Hour(), t.Minute(), t.Second())
  353. return formatted
  354. }
  355. func RoundTime(input float64) int {
  356. var result float64
  357. if input < 0 {
  358. result = math.Ceil(input - 0.5)
  359. } else {
  360. result = math.Floor(input + 0.5)
  361. }
  362. // only interested in integer, ignore fractional
  363. i, _ := math.Modf(result)
  364.  
  365. return int(i)
  366. }
  367. func convertLDAPDate(timestamp string)string{
  368. //baseTime := time.Date(1601, 1, 1, 0, 0, 0, 0, time.UTC)
  369. i, _ := strconv.ParseInt(timestamp, 10, 64)
  370. date := ( i / 10000000 ) - 11644473600;
  371. t := time.Unix(date, 0)
  372. formatted := fmt.Sprintf(" %d-%02d-%02d %02d:%02d:%02d",
  373. t.Year(), t.Month(), t.Day(),
  374. t.Hour(), t.Minute(), t.Second())
  375. return formatted
  376. }
  377. func convertPwdAge(pwdage string) string {
  378. f, _ := strconv.ParseFloat((strings.Replace(pwdage, "-", "", -1)), 64)
  379. age := ((f / (60 * 10000000)) / 60) / 24
  380. flr := math.Floor(age)
  381. s := strconv.Itoa(int(flr))
  382.  
  383. return s
  384. }
  385. func convertLockout(lockout string) string {
  386. i, _ := strconv.Atoi(strings.Replace(lockout, "-", "", -1))
  387. age := i / (60 * 10000000)
  388. s := strconv.Itoa(age)
  389.  
  390. return s
  391. }
  392. func after(value string, a string) string {
  393. // Get substring after a string.
  394. pos := strings.LastIndex(value, a)
  395. if pos == -1 {
  396. return ""
  397. }
  398. adjustedPos := pos + len(a)
  399. if adjustedPos >= len(value) {
  400. return ""
  401. }
  402. return value[adjustedPos:len(value)]
  403. }
  404.  
  405. func Find(slice []string, val string) (int, bool) {
  406. for i, item := range slice {
  407. if item == val {
  408. return i, true
  409. }
  410. }
  411. return -1, false
  412. }
Buy Me A Coffee