Did you know that it is possible to perform every step in Entra’s OAuth 2.0 Device Code flow — including the user authentication steps — without a browser?
您是否知道无需浏览器即可执行 Entra OAuth 2.0 设备代码流程中的每个步骤(包括用户身份验证步骤)?
Why that matters: 为什么这很重要:
- Automating authentication flows enables and accelerates comprehensive and ongoing offensive research
自动化身份验证流程可实现并加速全面且持续的攻击性研究 - Headless authentication frees red teamers and pentesters from requiring browser or cookie access
无头身份验证使红队人员和渗透测试人员无需浏览器或 cookie 访问 - Demonstrating and explaining the automated flow enables future research and tooling by other parties, including automation of other flows
演示和解释自动化流程使其他方能够进行未来的研究和工具,包括其他流程的自动化
Yes, but: 对,但是:
- These systems change. While this automation works today, slight changes in the future may require updates to the code in this blog post.
这些系统发生变化。虽然这种自动化现在可以工作,但未来的细微变化可能需要更新本博客文章中的代码。 - This code does not support any sort of MFA challenge a user may be subjected to during authentication.
此代码不支持用户在身份验证期间可能遇到的任何类型的 MFA 质询。
Automating the Flow 流程自动化
Automating device code flow requires five requests:
自动化设备代码流需要五个请求:
Request One: POST to https://login.microsoftonline.com/common/oauth2/devicecode?api-version=1.0
请求一:POST 到 https://login.microsoftonline.com/common/oauth2/devicecode?api-version=1.0
In this request, the application initiates the flow and receives a “user code” back from the devicecode API. This “user code” is what a human being would enter into the browser later:
在此请求中,应用程序启动流程并接收从设备代码 API 返回的“用户代码”。这个“用户代码”是人们稍后在浏览器中输入的内容:
# Device Code OAuth flow begins:
$body = @{
"client_id" = "1b730954–1685–4b74–9bfd-dac224a7b894"
"resource" = "https://graph.microsoft.com"
}
$UserAgent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Mobile Safari/537.36"
$Headers=@{}
$Headers["User-Agent"] = $UserAgent
$authResponse = Invoke-RestMethod `
-UseBasicParsing `
-Method Post `
-Uri "https://login.microsoftonline.com/common/oauth2/devicecode?api-version=1.0" `
-Headers $Headers `
-Body $body
$OTC = $authResponse.user_code
Request Two: GET to https://login.microsoftonline.com/common/oauth2/deviceauth
请求二:GET 到 https://login.microsoftonline.com/common/oauth2/deviceauth
The next request is performed “as the user” and is a simple request to the initial deviceauth page:
下一个请求“作为用户”执行,是对初始 deviceauth 页面的简单请求:
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$session.UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36"
$SecondRequest = $null
$SecondRequest = Invoke-WebRequest `
-UseBasicParsing `
-Uri "https://login.microsoftonline.com/common/oauth2/deviceauth" `
-WebSession $session `
The response to this request includes our first collection of cookies and unique identifiers that we must keep track of during the flow:
对此请求的响应包括我们在流程中必须跟踪的第一个 Cookie 和唯一标识符集合:
x-ms-request-id — The value of this response header is required in later requests as the value for “hpgrequestid”
x-ms-request-id — 在以后的请求中需要此响应标头的值作为“hpgrequestid”的值
fpc — This cookie is required in later requests
fpc——以后的请求需要这个cookie
esctx — This cookie is required in later requests
esctx — 后续请求需要此 cookie
hpgid — A four-digit code that is required in later requests and must be parsed out of the response body HTML
hpgid — 后续请求中需要的四位代码,必须从响应正文 HTML 中解析出来
canary — A token required in later requests, must be parsed from the response body HTML
canary — 后续请求中所需的令牌,必须从响应正文 HTML 中解析
We can assign all of these cookies, tokens, and other identifiers to their relevant variables with some basic parsing of the response headers and body in PowerShell:
我们可以通过在 PowerShell 中对响应标头和正文进行一些基本解析,将所有这些 cookie、令牌和其他标识符分配给它们的相关变量:
$hpgrequestid = $SecondRequest.Headers.'x-ms-request-id'
$CookieFPC = (($SecondRequest.Headers.'Set-Cookie' | select -first 1) -Split '; ') -Split '=' | Select -First 2 | Select -Last 1
$CookieESCTX = (($SecondRequest.Headers.'Set-Cookie' | select -first 2 | Select -Last 1) -Split '; ') -Split '=' | Select -First 2 | Select -Last 1
$html = $SecondRequest.Content
$pattern = ',"hpgid":(.*?),"pgid"'
$match = $html | Select-String -Pattern $pattern
if ($match) {
$HPGID = $match.Matches.Groups[1].Value
Write-Output "HPGID: $HPGID"
} else {
Write-Output "HPGID not found in the HTML."
}
$pattern = '","canary":"(.*?)","sCanaryTokenName"'
$match = $html | Select-String -Pattern $pattern
if ($match) {
$desiredString = $match.Matches.Groups[1].Value
$Canary = [System.Web.HttpUtility]::UrlEncode($desiredString)
Write-Output "Canary: $Canary"
} else {
Write-Output "Canary not found in the HTML."
}
This code block shows what each value looks like:
此代码块显示了每个值的样子:
$hpgrequestid: e2a89eff-5dd0–4262-b27b-4ee2468a5900
$CookieFPC AuxOg1aj0H5EpEVcclZOs4Q
$CookieESCTX PAQABAAEAAAD - DLA3VO7QrddgJg7WevrzJCyhs37r8aEEog6PXOivCF953PRt68FvlHkjFnSplN2mNHQwqEBcTTmf5EPXTIRQCQXFrA27_cEk2l3YG0F1JreF8T9WwL5PJldV5XZjdy2RF-A-EtDsFx_MHGWSV-FSw1Prci4lcfiDl7vsxQMqXKGgaUSdNPbA9iJPFfIh8cgAA
$HPGID 1119
$Canary t%2frFkhW25KShBl3S2O7pyXaB6GA3P3orfpFUxom3RH4%3d7%3a1%3aCANARY%3acDaoWAl7lE%2f5UyKpxyvQpCptwytSp3fwGEJMnTyNAXQ%3d
Request Three: POST to https://login.microsoftonline.com/common/oauth2/deviceauth
请求三:POST 到 https://login.microsoftonline.com/common/oauth2/deviceauth
In this request, we include the ESCTX cookie, One-Time Code (OTC), Canary, and hpgrequestid values when POSTing to the deviceauth endpoint:
在此请求中,我们在 POST 到 deviceauth 端点时包含 ESCTX cookie、一次性代码 (OTC)、Canary 和 hpgrequestid 值:
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$session.UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36"
$session.Cookies.Add((New-Object System.Net.Cookie("esctx", $CookieESCTX, "/", ".login.microsoftonline.com")))
$ThirdRequest = $null
$ThirdRequest = Invoke-WebRequest `
-UseBasicParsing `
-Uri "https://login.microsoftonline.com/common/oauth2/deviceauth" `
-Method "POST" `
-WebSession $session `
-ContentType "application/x-www-form-urlencoded" `
-Body "otc=$($OTC)&canary=$($Canary)&flowToken=&hpgrequestid=$($hpgrequestid)"
The response will include NEW values for hpgrequestid and ESCTX, so we need to update those values before we use them in subsequent requests:
响应将包含 hpgrequestid 和 ESCTX 的新值,因此我们需要在后续请求中使用这些值之前更新它们:
$hpgrequestid = $ThirdRequest.Headers.'x-ms-request-id'
$CookieESCTX = (($ThirdRequest.Headers.'Set-Cookie' | select -First 4 | Select -Last 1) -Split '; ') -Split '=' | Select -First 2 | Select -Last 1
We need to extract the updated value for “canary” from the request body to use in subsequent requests:
我们需要从请求正文中提取“canary”的更新值以在后续请求中使用:
# Find the value of "canary"
$pattern = '","canary":"(.*?)","sCanaryTokenName"'
$match = $html | Select-String -Pattern $pattern
if ($match) {
$desiredString = $match.Matches.Groups[1].Value
$Canary = [System.Web.HttpUtility]::UrlEncode($desiredString)
Write-Output "Canary: $Canary"
} else {
Write-Output "Canary not found in the HTML."
}
We also need to extract the value of “sCtx” from the body “sFT”, as they are used in subsequent requests for the values of “OriginalRequest” and “sFT”:
我们还需要从主体“sFT”中提取“sCtx”的值,因为它们在后续请求中使用“OriginalRequest”和“sFT”的值:
# Find the value of "sCtx"
$html = $ThirdRequest.Content
$pattern = 'login\.microsoftonline\.com%2fcommon%2freprocess%3fctx%3d(.*?)\\u0026mkt=en-US\\u0026hosted=1'
$match = $html | Select-String -Pattern $pattern
if ($match) {
$desiredString = $match.Matches.Groups[1].Value
$OriginalRequest = [System.Web.HttpUtility]::UrlDecode($desiredString)
Write-Output "Original request: $OriginalRequest"
} else {
Write-Output "Original request not found in the HTML."
}
# Find the value of "sFT"
$pattern = '","sFT":"(.*?)","sFTName"'
$match = $html | Select-String -Pattern $pattern
if ($match) {
$sFT = $match.Matches.Groups[1].Value
Write-Output "sFT: $sFT"
} else {
Write-Output "sFT not found in the HTML."
}
Request Four: GET to https://login.microsoftonline.com/common/login
请求四:GET 到 https://login.microsoftonline.com/common/login
This is the request where we finally send the username and password. This request requires the ESCTX, Canary, OriginalRequest, hpgrequestid, and sFT cookies/tokens as well:
这是我们最终发送用户名和密码的请求。此请求还需要 ESCTX、Canary、OriginalRequest、hpgrequestid 和 sFT cookie/令牌:
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$session.UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Req5"
$session.Cookies.Add((New-Object System.Net.Cookie("esctx", $CookieESCTX, "/", ".login.microsoftonline.com")))
$FourthRequest = $null
$FourthRequest = Invoke-WebRequest `
-UseBasicParsing `
-Uri "https://login.microsoftonline.com/common/login" `
-Method "POST" `
-WebSession $session `
-ContentType "application/x-www-form-urlencoded" `
-Body "i13=0&login=jasonfrank%40specterdev.onmicrosoft.com&loginfmt=jasonfrank%40specterdev.onmicrosoft.com&type=11&LoginOptions=3&lrt=&lrtPartition=&hisRegion=&hisScaleUnit=&passwd=<cleartext password goes here>&ps=2&psRNGCDefaultType=&psRNGCEntropy=&psRNGCSLK=&canary=$($Canary)&ctx=$($OriginalRequest)&hpgrequestid=$($hpgrequestid)&flowToken=$($sFT)&PPSX=&NewUser=1&FoundMSAs=&fspost=0&i21=0&CookieDisclosure=0&IsFidoSupported=1&isSignupPost=0&i19=125513"
Next, we must update the values of ESCTX and sFT from this request’s response:
接下来,我们必须根据该请求的响应更新 ESCTX 和 sFT 的值:
$CookieESCTX = (($FourthRequest.Headers.'Set-Cookie' | select -First 2 | Select -Last 1) -Split '; ') -Split '=' | Select -First 2 | Select -Last 1
# Find the value of "sFT"
$html = $FourthRequest.Content
$pattern = '","sFT":"(.*?)","sFTName"'
$match = $html | Select-String -Pattern $pattern
if ($match) {
$sFT = $match.Matches.Groups[1].Value
Write-Output "sFT: $sFT"
} else {
Write-Output "sFT not found in the HTML."
}
Request Five: POST to https://login.microsoftonline.com/appverify
请求五:POST 到 https://login.microsoftonline.com/appverify
In this request, we supply the ESCTX, OriginalRequest, hprequestid, sFT, and Canary cookies/tokens:
在此请求中,我们提供 ESCTX、OriginalRequest、hprequestid、sFT 和 Canary cookie/令牌:
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$session.UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Req6"
$session.Cookies.Add((New-Object System.Net.Cookie("esctx", $CookieESCTX, "/", ".login.microsoftonline.com")))
$FifthRequest = Invoke-WebRequest `
-UseBasicParsing `
-Uri "https://login.microsoftonline.com/appverify" `
-Method "POST" `
-WebSession $session `
-ContentType "application/x-www-form-urlencoded" `
-Body "ContinueAuth=true&ctx=$($OriginalRequest)&hpgrequestid=$($hpgrequestid)&flowToken=$($sFT)&iscsrfspeedbump=false&canary=$($Canary)&i19=43609"
This is equivalent to clicking “Yes” when the browser is asking if you are trying to log into the application. We do not need anything from this request’s response.
这相当于当浏览器询问您是否尝试登录应用程序时单击“是”。我们不需要此请求的响应中的任何内容。
Request Six: POST to https://login.microsoftonline.com/Common/oauth2/token?api-version=1.0
请求六:POST 到 https://login.microsoftonline.com/Common/oauth2/token?api-version=1.0
Here, we are going back to the context of submitting requests as the application, not the user. In this request, we supply the “device code” we retrieved from the very first request:
在这里,我们将回到以应用程序而不是用户身份提交请求的上下文。在此请求中,我们提供从第一个请求中检索到的“设备代码”:
$body=@{
"client_id" = "1b730954–1685–4b74–9bfd-dac224a7b894"
"grant_type" = "urn:ietf:params:oauth:grant-type:device_code"
"code" = $authResponse.device_code
}
$SixthRequest = Invoke-RestMethod `
-UseBasicParsing `
-Method Post `
-Uri "https://login.microsoftonline.com/Common/oauth2/token?api-version=1.0" `
-Headers $Headers `
-Body $body
If Requests Two through Five resulted in valid authentication, this request response includes a refresh token, access token, and ID token for the user:
如果请求 2 到 5 导致有效的身份验证,则此请求响应包括用户的刷新令牌、访问令牌和 ID 令牌:
$SixthRequest
token_type : Bearer
scope : Agreement.Read.All Agreement.ReadWrite.All AgreementAcceptance.Read AgreementAcceptance.Read.All AuditLog.Read.All Directory.AccessAsUser.All
Directory.ReadWrite.All Group.ReadWrite.All IdentityProvider.ReadWrite.All Policy.ReadWrite.TrustFramework PrivilegedAccess.ReadWrite.AzureAD
PrivilegedAccess.ReadWrite.AzureADGroup PrivilegedAccess.ReadWrite.AzureResources TrustFrameworkKeySet.ReadWrite.All User.Invite.All
expires_in : 4593
ext_expires_in : 4593
expires_on : 1689961479
not_before : 1689956585
resource : https://graph.microsoft.com
access_token : eyJ0eX…ba0hpg
refresh_token : 0.AVEA…h2P7mA
id_token : eyJ0eX…IiOiIx
Conclusion and What’s Next
结论和下一步
In this blog post, I’m sharing my current knowledge and testing regarding automating Entra’s OAuth 2.0 Device Code flow. Please feel free to build on, correct, or adapt this information to your own needs. I’m sharing this now because, frankly, it took a long time to figure out and I want to save others the pain I went through.
在这篇博文中,我将分享我目前关于自动化 Entra 的 OAuth 2.0 设备代码流程的知识和测试。请随意根据您自己的需要来构建、更正或调整此信息。我现在分享这个是因为,坦率地说,我花了很长时间才弄清楚,我想避免其他人经历我所经历的痛苦。
I’m using this to research other Entra systems such as Conditional Access. Automating this flow means I can very quickly perform tests against Conditional Access to validate, for example, how the backend systems determine a user’s browser and OS types from the signals in these requests.
我正在用它来研究其他 Entra 系统,例如条件访问。自动化此流程意味着我可以非常快速地针对条件访问执行测试来验证,例如,后端系统如何根据这些请求中的信号确定用户的浏览器和操作系统类型。
原文始发于Andy Robbins:Browserless Entra Device Code Flow