diff --git a/src/components/Modals/AddEdgeModal.jsx b/src/components/Modals/AddEdgeModal.jsx index d9d3c719f..a73229432 100644 --- a/src/components/Modals/AddEdgeModal.jsx +++ b/src/components/Modals/AddEdgeModal.jsx @@ -141,6 +141,7 @@ const AddEdgeModal = () => { edgeValue === 'GenericWrite' || edgeValue === 'AllExtendedRights' || edgeValue === 'AddMember' || + edgeValue === 'AddSelf' || edgeValue === 'ForceChangePassword' || edgeValue === 'Owns' || edgeValue === 'WriteDacl' || @@ -241,6 +242,7 @@ const AddEdgeModal = () => { AllExtendedRights + diff --git a/src/components/Modals/HelpModal.jsx b/src/components/Modals/HelpModal.jsx index 1778a7dae..eb6986b2b 100644 --- a/src/components/Modals/HelpModal.jsx +++ b/src/components/Modals/HelpModal.jsx @@ -9,6 +9,7 @@ import AllExtendedRights from './HelpTexts/AllExtendedRights/AllExtendedRights'; import AdminTo from './HelpTexts/AdminTo/AdminTo'; import HasSession from './HelpTexts/HasSession/HasSession'; import AddMember from './HelpTexts/AddMember/AddMember'; +import AddSelf from './HelpTexts/AddSelf/AddSelf'; import ForceChangePassword from './HelpTexts/ForceChangePassword/ForceChangePassword'; import GenericWrite from './HelpTexts/GenericWrite/GenericWrite'; import Owns from './HelpTexts/Owns/Owns'; @@ -69,6 +70,7 @@ const HelpModal = () => { AdminTo: AdminTo, HasSession: HasSession, AddMember: AddMember, + AddSelf: AddSelf, ForceChangePassword: ForceChangePassword, GenericWrite: GenericWrite, Owns: Owns, diff --git a/src/components/Modals/HelpTexts/AddSelf/Abuse.jsx b/src/components/Modals/HelpTexts/AddSelf/Abuse.jsx new file mode 100755 index 000000000..5fbc2a796 --- /dev/null +++ b/src/components/Modals/HelpTexts/AddSelf/Abuse.jsx @@ -0,0 +1,25 @@ +const Abuse = (sourceName, sourceType, targetName, targetType) => { + let text = `There are at least two ways to execute this attack. The first and most obvious is by using the built-in net.exe binary in Windows (e.g.: net group "Domain Admins" dfm.a /add /domain). See the opsec considerations tab for why this may be a bad idea. The second, and highly recommended method, is by using the Add-DomainGroupMember function in PowerView. This function is superior to using the net.exe binary in several ways. For instance, you can supply alternate credentials, instead of needing to run a process as or logon as the user with the AddMember privilege. Additionally, you have much safer execution options than you do with spawning net.exe (see the opsec tab). + + To abuse this privilege with PowerView's Add-DomainGroupMember, first import PowerView into your agent session or into a PowerShell instance at the console. + + You may need to authenticate to the Domain Controller as ${ + sourceType === 'User' + ? `${sourceName} if you are not running a process as that user` + : `a member of ${sourceName} if you are not running a process as a member` + }. To do this in conjunction with Add-DomainGroupMember, first create a PSCredential object (these examples comes from the PowerView help documentation): + + $SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force + $Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\\dfm.a', $SecPassword) + + Then, use Add-DomainGroupMember, optionally specifying $Cred if you are not already running a process as ${sourceName}: + + Add-DomainGroupMember -Identity 'Domain Admins' -Members 'harmj0y' -Credential $Cred + + Finally, verify that the user was successfully added to the group with PowerView's Get-DomainGroupMember: + + Get-DomainGroupMember -Identity 'Domain Admins'`; + return { __html: text }; +}; + +export default Abuse; diff --git a/src/components/Modals/HelpTexts/AddSelf/AddSelf.jsx b/src/components/Modals/HelpTexts/AddSelf/AddSelf.jsx new file mode 100755 index 000000000..1d2004d35 --- /dev/null +++ b/src/components/Modals/HelpTexts/AddSelf/AddSelf.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Tabs, Tab } from 'react-bootstrap'; +import General from './General'; +import Abuse from './Abuse'; +import Opsec from './Opsec'; +import References from './References'; + +const AddSelf = ({ sourceName, sourceType, targetName, targetType }) => { + return ( + + + + + + + ); +}; + +AddSelf.propTypes = { + sourceName: PropTypes.string, + sourceType: PropTypes.string, + targetName: PropTypes.string, + targetType: PropTypes.string, +}; +export default AddSelf; diff --git a/src/components/Modals/HelpTexts/AddSelf/General.jsx b/src/components/Modals/HelpTexts/AddSelf/General.jsx new file mode 100755 index 000000000..facffb278 --- /dev/null +++ b/src/components/Modals/HelpTexts/AddSelf/General.jsx @@ -0,0 +1,15 @@ +import { groupSpecialFormat, typeFormat } from '../Formatter'; + +const General = (sourceName, sourceType, targetName, targetType) => { + let text = `${groupSpecialFormat( + sourceType, + sourceName + )} the ability to add ${ + sourceType === 'Group' ? 'themselves' : 'itself' + } to the group ${targetName}. Because of security group delegation, the members of a security group have the same privileges as that group. + + By adding itself to the group, ${sourceName} will gain the same privileges that ${targetName} already has.`; + return { __html: text }; +}; + +export default General; diff --git a/src/components/Modals/HelpTexts/AddSelf/Opsec.jsx b/src/components/Modals/HelpTexts/AddSelf/Opsec.jsx new file mode 100755 index 000000000..0fce75309 --- /dev/null +++ b/src/components/Modals/HelpTexts/AddSelf/Opsec.jsx @@ -0,0 +1,10 @@ +const Opsec = () => { + let text = `Executing this abuse with the net binary will require command line execution. If your target organization has command line logging enabled, this is a detection opportunity for their analysts. + + Regardless of what execution procedure you use, this action will generate a 4728 event on the domain controller that handled the request. This event may be centrally collected and analyzed by security analysts, especially for groups that are obviously very high privilege groups (i.e.: Domain Admins). Also be mindful that Powershell 5 introduced several key security features such as script block logging and AMSI that provide security analysts another detection opportunity. + + You may be able to completely evade those features by downgrading to PowerShell v2.`; + return { __html: text }; +}; + +export default Opsec; diff --git a/src/components/Modals/HelpTexts/AddSelf/References.jsx b/src/components/Modals/HelpTexts/AddSelf/References.jsx new file mode 100755 index 000000000..9c117800e --- /dev/null +++ b/src/components/Modals/HelpTexts/AddSelf/References.jsx @@ -0,0 +1,8 @@ +const References = () => { + let text = `https://github.com/PowerShellMafia/PowerSploit/blob/dev/Recon/PowerView.ps1 + https://www.youtube.com/watch?v=z8thoG7gPd0 + https://www.ultimatewindowssecurity.com/securitylog/encyclopedia/event.aspx?eventID=4728`; + return { __html: text }; +}; + +export default References; diff --git a/src/components/SearchContainer/EdgeFilter.jsx b/src/components/SearchContainer/EdgeFilter.jsx index 41b9bb339..3a1b0274e 100644 --- a/src/components/SearchContainer/EdgeFilter.jsx +++ b/src/components/SearchContainer/EdgeFilter.jsx @@ -14,6 +14,7 @@ const EdgeFilter = ({ open }) => { } else if (section === 'acl') { current.AllExtendedRights = false; current.AddMember = false; + current.AddSelf = false; current.ForceChangePassword = false; current.GenericAll = false; current.GenericWrite = false; @@ -50,6 +51,7 @@ const EdgeFilter = ({ open }) => { } else if (section === 'acl') { current.AllExtendedRights = true; current.AddMember = true; + current.AddSelf = true; current.ForceChangePassword = true; current.GenericAll = true; current.GenericWrite = true; @@ -219,6 +221,19 @@ const EdgeFilter = ({ open }) => { AddMember +
+ handleEdgeChange(e)} + /> + +
{ property='Unrolled Object Controllers' target={objectid} baseQuery={ - 'MATCH p=(n)-[r:MemberOf*1..]->(g:Group)-[r1:AddMember|AllExtendedRights|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns]->(u:Computer {objectid:$objectid}) WITH LENGTH(p) as pathLength, p, n WHERE NONE (x in NODES(p)[1..(pathLength-1)] WHERE x.objectid = u.objectid) AND NOT n.objectid = u.objectid' + 'MATCH p=(n)-[r:MemberOf*1..]->(g:Group)-[r1:AddMember|AddSelf|AllExtendedRights|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns]->(u:Computer {objectid:$objectid}) WITH LENGTH(p) as pathLength, p, n WHERE NONE (x in NODES(p)[1..(pathLength-1)] WHERE x.objectid = u.objectid) AND NOT n.objectid = u.objectid' } end={label} distinct @@ -396,7 +396,7 @@ const ComputerNodeData = () => { property='Transitive Object Control' target={objectid} baseQuery={ - 'MATCH (n) WHERE NOT n.objectid=$objectid WITH n MATCH p = shortestPath((c:Computer {objectid: $objectid})-[r:MemberOf|AddMember|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(n))' + 'MATCH (n) WHERE NOT n.objectid=$objectid WITH n MATCH p = shortestPath((c:Computer {objectid: $objectid})-[r:MemberOf|AddMember|AddSelf|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(n))' } start={label} distinct diff --git a/src/components/SearchContainer/Tabs/GPONodeData.jsx b/src/components/SearchContainer/Tabs/GPONodeData.jsx index 2be0844c5..4f22ad5b4 100644 --- a/src/components/SearchContainer/Tabs/GPONodeData.jsx +++ b/src/components/SearchContainer/Tabs/GPONodeData.jsx @@ -125,7 +125,7 @@ const GPONodeData = () => { property='Explicit Object Controllers' target={objectid} baseQuery={ - 'MATCH p = (n)-[r:AddMember|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns]->(g:GPO {objectid: $objectid})' + 'MATCH p = (n)-[r:AddMember|AddSelf|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns]->(g:GPO {objectid: $objectid})' } end={label} distinct @@ -145,7 +145,7 @@ const GPONodeData = () => { property='Transitive Object Controllers' target={objectid} baseQuery={ - 'MATCH (n) WHERE NOT n.objectid= $objectid WITH n MATCH p = shortestPath((n)-[r:MemberOf|AddMember|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(g:GPO {objectid: $objectid}))' + 'MATCH (n) WHERE NOT n.objectid= $objectid WITH n MATCH p = shortestPath((n)-[r:MemberOf|AddMember|AddSelf|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(g:GPO {objectid: $objectid}))' } end={label} distinct diff --git a/src/components/SearchContainer/Tabs/GroupNodeData.jsx b/src/components/SearchContainer/Tabs/GroupNodeData.jsx index f170079a1..d93b4848a 100644 --- a/src/components/SearchContainer/Tabs/GroupNodeData.jsx +++ b/src/components/SearchContainer/Tabs/GroupNodeData.jsx @@ -248,7 +248,7 @@ const GroupNodeData = () => { property='Transitive Object Control' target={objectid} baseQuery={ - 'MATCH (n) WHERE NOT n.objectid=$objectid WITH n MATCH p = shortestPath((g:Group {objectid: $objectid})-[r:MemberOf|AddMember|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(n))' + 'MATCH (n) WHERE NOT n.objectid=$objectid WITH n MATCH p = shortestPath((g:Group {objectid: $objectid})-[r:MemberOf|AddMember|AddSelf|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(n))' } start={label} distinct @@ -259,7 +259,7 @@ const GroupNodeData = () => { property='Explicit Object Controllers' target={objectid} baseQuery={ - 'MATCH p = (n)-[r:AddMember|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns]->(g:Group {objectid: $objectid})' + 'MATCH p = (n)-[r:AddMember|AddSelf|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns]->(g:Group {objectid: $objectid})' } end={label} distinct @@ -279,7 +279,7 @@ const GroupNodeData = () => { property='Transitive Object Controllers' target={objectid} baseQuery={ - 'MATCH (n) WHERE NOT n.objectid=$objectid WITH n MATCH p = shortestPath((n)-[r:MemberOf|AddMember|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(g:Group {objectid: $objectid}))' + 'MATCH (n) WHERE NOT n.objectid=$objectid WITH n MATCH p = shortestPath((n)-[r:MemberOf|AddMember|AddSelf|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(g:Group {objectid: $objectid}))' } end={label} distinct diff --git a/src/components/SearchContainer/Tabs/UserNodeData.jsx b/src/components/SearchContainer/Tabs/UserNodeData.jsx index 60326e414..f08dea8fc 100644 --- a/src/components/SearchContainer/Tabs/UserNodeData.jsx +++ b/src/components/SearchContainer/Tabs/UserNodeData.jsx @@ -283,7 +283,7 @@ const UserNodeData = () => { property='Transitive Object Control' target={objectId} baseQuery={ - 'MATCH (n) WHERE NOT n.objectid=$objectid MATCH p=shortestPath((u:User {objectid: $objectid})-[r1:MemberOf|AddMember|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(n))' + 'MATCH (n) WHERE NOT n.objectid=$objectid MATCH p=shortestPath((u:User {objectid: $objectid})-[r1:MemberOf|AddMember|AddSelf|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(n))' } start={label} distinct @@ -305,7 +305,7 @@ const UserNodeData = () => { property='Unrolled Object Controllers' target={objectId} baseQuery={ - 'MATCH p=(n)-[r:MemberOf*1..]->(g:Group)-[r1:AddMember|AllExtendedRights|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns]->(u:User {objectid: $objectid}) WITH LENGTH(p) as pathLength, p, n WHERE NONE (x in NODES(p)[1..(pathLength-1)] WHERE x.objectid = u.objectid) AND NOT n.objectid = u.objectid' + 'MATCH p=(n)-[r:MemberOf*1..]->(g:Group)-[r1:AddMember|AddSelf|AllExtendedRights|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns]->(u:User {objectid: $objectid}) WITH LENGTH(p) as pathLength, p, n WHERE NONE (x in NODES(p)[1..(pathLength-1)] WHERE x.objectid = u.objectid) AND NOT n.objectid = u.objectid' } end={label} distinct diff --git a/src/index.js b/src/index.js index 728864039..cc1136157 100644 --- a/src/index.js +++ b/src/index.js @@ -167,6 +167,7 @@ global.appStore = { WriteDacl: 'tapered', WriteOwner: 'tapered', AddMember: 'tapered', + AddSelf: 'tapered', TrustedBy: 'curvedArrow', DCSync: 'tapered', Contains: 'tapered', @@ -207,6 +208,7 @@ global.appStore = { WriteDacl: 'line', WriteOwner: 'line', AddMember: 'line', + AddSelf: 'line', TrustedBy: 'curvedArrow', DCSync: 'line', Contains: 'line', @@ -303,6 +305,7 @@ if (typeof conf.get('edgeincluded') === 'undefined') { AdminTo: true, AllExtendedRights: true, AddMember: true, + AddSelf: true, ForceChangePassword: true, GenericAll: true, GenericWrite: true, diff --git a/src/js/newingestion.js b/src/js/newingestion.js index b61c54f2f..5e619fb21 100644 --- a/src/js/newingestion.js +++ b/src/js/newingestion.js @@ -580,6 +580,8 @@ function processAceArrayNew(aces, objectid, objecttype, queries) { rights.push('AllExtendedRights'); } else if (aceType === 'User-Force-Change-Password') { rights.push('ForceChangePassword'); + } else if (aceType === 'AddSelf') { + rights.push('AddSelf'); } else if (aceType === 'AddMember') { rights.push('AddMember'); } else if (aceType === 'AllowedToAct') {