Roasting

Dark image of three snarling, three-headed wolf with glowing orange eyes and sharp teeth, in a menacing posture.

AS-REP Roasting

AS-REP (Authentication Service Response) roasting targets user accounts that do not require Kerberos pre-authentication. When a client requests a TGT for such an account, the Key Distribution Centre (KDC) returns an AS-REP without first verifying the user's identity through pre-authentication. This AS-REP contains data encrypted with a key derived from the user's password. Attackers can capture this response and perform offline brute-force attacks to recover the user's credentials. RC4-encrypted responses are the most commonly targeted due its weak algorithm and therefore faster cracking time, while AES-encrypted responses are more secure but can still be cracked if weak passwords are in use.

For this attack to be successful, pre authentication must be disabled on the target account/s.

Pre-authentication modification events can be viewed in the below Security Events.

I will come back and build a query for this!!

5136 - A directory service object was modified.

4662 - An operation was performed on an object.

4738 - A user account was changed.

In the discovery phase for accounts with pre authentication disabled, tools like Rubeus will perform the following LDAP query:

(&(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=4194304))

Screenshot of a programming code snippet checking if 'userName' is null or empty and setting a 'userSearchFilter' string with user account type and control information.

samAccountType=805306368

  • Filters for user accounts only

  • 805306368 = SAM_USER_OBJECT

  • Ensures you're only querying actual user principals

userAccountControl:1.2.840.113556.1.4.803:=4194304

  • This is a bitwise match filter:

    • userAccountControl is a bitmask of user settings

    • 1.2.840.113556.1.4.803 is the OID for LDAP_MATCHING_RULE_BIT_AND

    • 4194304 = 0x00400000 = DONT_REQ_PREAUTH flag

This returns all user accounts with pre-authentication disabled. LdapSearch will provide telemetry of endpoint originating requests. The same can be done for IdentityQueryEvents to capture from ADs PoV. This is useful in scenario where an actor may be using virtualisation.

DeviceEvents
| where ActionType == "LdapSearch"
| extend Parsed = parse_json(AdditionalFields)
| extend SearchFilter = tostring(Parsed.SearchFilter)
| extend ScopeOfSearch = tostring(Parsed.ScopeOfSearch)
| where SearchFilter startswith "(&(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=4194304"
IdentityQueryEvents
| where ActionType == 'LDAP query'
| extend Parsed = parse_json(AdditionalFields)
| extend ActorDevice = tostring(Parsed.["ACTOR.DEVICE"])
| extend LdapSearchScope = tostring(Parsed.LdapSearchScope)
| extend SearchFilter = tostring(Parsed.SearchFilter)
| where SearchFilter has '(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=4194304'

Within TGT requests we should see the preauth value 0 meaning none - pre auth not used.

SecurityEvent
| where EventID == 4768 // TGT
| parse EventData with * 'PreAuthType">' PreAuthType '<' * 
| where PreAuthType == 0

One of the highest win rate queries - 0x40800010 ticket options. Rubeus creates Kerberos requests manually instead of using native Windows API functions, the given TGT ticket options follows a hard coded structure that creates the perfect indicator.

Shoutout https://www.intrinsec.com/kerberos_opsec_part_1_kerberoasting for the in-depth explanation.

The bit structure of 0x40800010 relates to the following:

Bitmask - 4 -> Flag - 0x10 -> Meaning - Forwardable

Bitmask - 23 -> Flag - 0x800000 -> Meaning - Renewable

Bitmask - 30 -> Flag - 0x40000000 -> Meaning - Renewable-OK

SecurityEvent
| where EventID == 4768 // TGT
| parse EventData with * 'TicketOptions">' TicketOptions '<' * 
| where TicketOptions == "0x40800010"
| extend StatusValue = case(
   Status == "0x0", "Success",
   Status == "0x6", "Client not found (KDC_ERR_C_PRINCIPAL_UNKNOWN)",
   Status == "0x7", "Server not found (KDC_ERR_S_PRINCIPAL_UNKNOWN)",
   Status == "0x18", "Pre-authentication failed (KDC_ERR_PREAUTH_FAILED)",
   Status == "0x19", "Pre-authentication required (KDC_ERR_PREAUTH_REQUIRED)",
   Status == "0x10", "KDC has no support for PA data type (KDC_ERR_PADATA_TYPE_NOSUPP)",
   Status == "0x12", "Client revoked (KDC_ERR_CLIENT_REVOKED)",
   Status == "0x1F", "Ticket expired (KRB_AP_ERR_TKT_EXPIRED)",
   Status == "0x20", "Ticket not yet valid (KRB_AP_ERR_TKT_NYV)",
   Status == "0x21", "Request is a replay (KRB_AP_ERR_REPEAT)",
   "Unknown")

Not tested however if you’re not logging 4768, you may be able to use IdentityLogonEvents.

IdentityLogonEvents
| where Protocol == "Kerberos"
| extend Parsed = parse_json(AdditionalFields)
| extend KdcOptions = tostring(Parsed.KdcOptions)
| extend KdcArray = split(KdcOptions, ",")
| where array_length(KdcArray) == 3
| where KdcArray has_all ("forwardable", "renewable", "canonicalize")

If you have a well-kept environment where RC4 is not supported and AES is the standard (it should be). Just the presence of RC4 can be used as an outlier for investigation such as roasting, downgrade attacks etc

SecurityEvent
| where EventID == 4769 // TGS
| parse EventData with * 'TicketOptions">' TicketOptions '<' * 
| parse EventData with * 'TicketEncryptionType">' TicketEncryptionType '<' * 
| where TicketEncryptionType == '0x17'
IdentityLogonEvents
| where Protocol == 'Kerberos'
| extend ParsedFields = parse_json(AdditionalFields)
| extend KdcOptions = tostring(ParsedFields.KdcOptions)
| extend KerberosType = tostring(ParsedFields.KerberosType)
| extend Spns = tostring(ParsedFields.Spns)
| extend EncryptionType = tostring(ParsedFields.EncryptionType)
| where KerberosType == 'KerberosTgs'
| where EncryptionType contains "rc4"

TGS - Kerberoasting

TGS Kerberoasting is a post compromise attack where any valid domain user requests service tickets (TGS) for accounts with Service Principal Names (SPNs), which are unique identifiers mapping services to Active Directory accounts. The ticket is encrypted with the service account's password hash, which the attacker extracts and cracks offline. Since machine accounts and the Kerberos service account krbtgt use randomly generated strong password that are automatically rotated, they are out of scope. While RC4 is most commonly the target due to its weak encryption, AES is still feasible especially if the underlying password is weak.

Using Rubeus again for this example. The standard format when not provided a specified user, is the following LDAP query:

(&(samAccountType=805306368)(servicePrincipalName=*){0}{1})", userFilter, encFilter);

Screenshot of programming code in a text editor, showing a conditional 'else' statement and a string formatting line in a script.

(samAccountType=805306368)

  • Filters for user accounts only.

  • 805306368 is the constant value for SAM_USER_OBJECT.

(servicePrincipalName=*)

  • Filters for accounts that have at least one SPN (Service Principal Name).

  • Only accounts with SPNs can be Kerberoasted.

{0} → userFilter

A placeholder for an optional sub-filter on usernames.

{1} → encFilter

A placeholder for an optional sub-filter on encryption types.

DeviceEvents
| where ActionType == "LdapSearch"
 | extend Parsed = parse_json(AdditionalFields)
| extend SearchFilter = tostring(Parsed.SearchFilter)
| extend ScopeOfSearch = tostring(Parsed.ScopeOfSearch)
| where SearchFilter startswith "(&(samAccountType=805306368)(servicePrincipalName=*)"
IdentityQueryEvents
| where ActionType == 'LDAP query'
| extend Parsed = parse_json(AdditionalFields)
| extend ActorDevice = tostring(Parsed.["ACTOR.DEVICE"])
| extend LdapSearchScope = tostring(Parsed.LdapSearchScope)
| extend SearchFilter = tostring(Parsed.SearchFilter)
| where SearchFilter has '(samAccountType=805306368)(servicePrincipalName=*)'

An attacker may target specific accounts, but it is more common to request as many SPNs as possible to increase the chances of successful cracking. With this context, we can hunt for a high volume of TGS requests within a short time window.

Environment context needs to be applied here. If RC4 is available, scope TicketEncryptionType to 0x17. Define thresholds for SPNCount based on what is considered excessive in your environment.

SecurityEvent
| where EventID == 4769 // TGS
| parse EventData with * 'TicketEncryptionType">' TicketEncryptionType '<' * 
| parse EventData with * 'TargetUserName">' TargetUserName '<' * 
| parse EventData with * 'TicketOptions">' TicketOptions '<' * 
| parse EventData with * 'ServiceName">' ServiceName '<' * 
| parse EventData with * 'IpAddress">' IpAddress '<' * 
| where ServiceName !contains "$" // filter machine accounts
| where TargetUserName !contains "$" // filter machine accounts
| where ServiceName !contains "krbtgt" 
| where TicketEncryptionType != "0xffffffff" // filter failures
| summarize SPNCount=dcount(ServiceName), SPNList=make_set(ServiceName), EncType=make_set(TicketEncryptionType) by IpAddress, bin(TimeGenerated, 2m)

Here we use a lookback to calculate a baseline and look for anomalies, including a hardcoded threshold for user/host that do not have historic data to build a sufficient baseline period.

let LookbackWindow = 60d;
let BinSize = 10m;
let Multiplier = 2; // Change up
let Threshold = 8; // Change up
// Get 10-min baseline per user/IP over 60d
let Baseline = 
   SecurityEvent
   | where EventID == 4769 and TimeGenerated >= ago(LookbackWindow)
   | parse EventData with * 'TargetUserName">' TargetUserName '<' * 
   | parse EventData with * 'ServiceName">' ServiceName '<' * 
   | parse EventData with * 'IpAddress">' IpAddress '<' * 
   | where ServiceName !contains "$" // filter machine accounts
   | where TargetUserName !contains "$" // filter machine accounts
   | where ServiceName !contains "krbtgt" 
   | summarize SPNCount=dcount(ServiceName) by TargetUserName, IpAddress, bin(TimeGenerated, BinSize)
   | summarize AvgSPNCount=avg(SPNCount) by TargetUserName, IpAddress;
// Now scan all 10-min bins for spikes vs. their own baseline
SecurityEvent
   | where EventID == 4769 and TimeGenerated >= ago(LookbackWindow)
   | parse EventData with * 'TicketEncryptionType">' TicketEncryptionType '<' * 
   | parse EventData with * 'TargetUserName">' TargetUserName '<' * 
   | parse EventData with * 'TicketOptions">' TicketOptions '<' * 
   | parse EventData with * 'ServiceName">' ServiceName '<' * 
   | parse EventData with * 'IpAddress">' IpAddress '<' * 
   | where ServiceName !contains "$" // filter machine accounts
   | where TargetUserName !contains "$" // filter machine accounts
   | where ServiceName !contains "krbtgt" 
   | summarize SPNCount=dcount(ServiceName), SPNList=make_set(ServiceName), EncType=make_set(TicketEncryptionType) by TargetUserName, IpAddress, bin(TimeGenerated, BinSize)
   | join kind=leftouter (Baseline) on TargetUserName, IpAddress
   | where SPNCount > Threshold or SPNCount > Multiplier * AvgSPNCount
   | project TimeGenerated, TargetUserName, IpAddress, SPNCount, AvgSPNCount, SPNList, EncType

AS-REQ for Service Tickets

The AS Requested Service Tickets attack, as outlined in the Semperis blog, allows attackers to request Service Tickets directly via an AS-REQ without a Ticket Granting Ticket (TGT), bypassing pre-authentication and TGS-REQ. Implemented in Rubeus, it enables Kerberoasting-like attacks from an unauthenticated position. If any account is configured to not require pre-authentication, it is possible to perform Kerberoasting without credentials.

Requesting a service ticket using an AS-REQ does not generate Event ID 4769 but instead produces Event ID 4768 (A Kerberos authentication ticket (TGT) was requested). The premise here is that a TGT should only apply against the krbtgt - Kerberos service and, in some cases, kadmin/changepw for password changes.

SecurityEvent
| where EventID == 4768
| parse EventData with 
    * 'TargetUserName">' TargetUserName '<'
    * 'TargetDomainName">' TargetDomainName '<'
    * 'ServiceName">' ServiceName '<'
    * 'ServiceSid">' ServiceSid '<'
    * 'IpAddress">' IpAddress '<' *
| where not(ServiceName =~ "krbtgt" and ServiceSid startswith "S-1-5-21-" and ServiceSid endswith "-502")
| where ServiceName !~ strcat("krbtgt/", TargetDomainName)
| where ServiceName !~ "kadmin/changepw"