modules/auxiliary/scanner/msmail/onprem_enum.go
//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)
}