Uncover Starred-Out Passwords

Uncover Starred-Out Passwords

I was inside my unifi controller a while back and the new update had starred out the radius password. The documentation had yet to occur on the radius password. So, I used a simple HTML trick with my firefox to change the stars to clear text. This is how you can Uncover Starred-Out Passwords in firefox.

Developer Options

F12 will trigger the developer options. Inside Google Chrome, the developer window will appear on the right-hand side. In Firefox, the developer window will appear at the bottom of the window. These options allow you to change your current view of the site. If you want to change the background to hot pink, you can. if you want to change a password field to plain text, you can.

Developer Options in Firefox

What we are looking at

Stared-Out Passwords
The view area.

This is the page we are viewing. I want the starred-out shared secret that you see in the red box. Click the edit button to the right of the shared secret. Notice the password is still starred out. To get this information, we need to start the Developer options. Inside the developer options on the left-hand side, you will see a mouse cursor in a box, also known as an inspector. We still have Starred-Out Passwords

Once you click the inspector tool, you can move the mouse over the shared secret and click on it. Inside the developer options, you will see the HTML itself highlighted. Notice the highlighted HTML code is the input object. We are looking for the word “type”. All we have to do is replace type=”password” to type=”text”. Once you type in text, click enter. The password is now exposed.

That’s pretty much it. The key secret is to make sure you are editing the input and not just the div. I you see div code instead of input code, then you do not have it set in edit mode. This process can be used for just about any website out there. This is how we Uncover Starred-Out Passwords.

Continue Reading

Find Forwarding Rules

Find Forwarding Rules

Not too long ago, I needed to do some rule auditing for forwarders in a client’s exchange online. They believed someone had a rule in their exchange account that was forwarded to a spammer. They believed this because new employees were receiving emails within a few days of creation. So, it’s time for some PowerShell magic to save the day. It’s time to Find Forwarding Rules in your mailboxes with PowerShell.

The Script

Connect-ExchangeOnline
$Mailboxes = Get-Mailbox -ResultSize Unlimited
$ForwarderRules = foreach ($Mailbox in $Mailboxes) {
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
        if (($null -ne $rule.ForwardTo) -or ($null -ne $rule.ForwardAsAttachmentTo)) {
            [pscustomobject][ordered]@{
                Username = $Mailbox.Alias
                Rule = $Rule.name
                ID = $Rule.RuleIdentity
                Enabled = $rule.enabled
                ForwardTo = $rule.ForwardTo | where-object {$_ -like "*@*"}
                ForwardAsAttachmentTo = $rule.ForwardAsAttachmentTo | where-object {$_ -like "*@*"}
            }
        }
    }
}
$ats = $ForwarderRules | where-object {($null -ne $_.ForwardTo) -or ($null -ne $_.ForwardAsAttachmentTo)}
$ats

The Breakdown

The script today requires the Exchange Online Module to be installed. If you don’t have it, go install it. Once you have it, you will need to connect using the Connect-ExchangeOnline commandlet.

Connect-ExchangeOnline

By doing it this way, MFA will be triggered and we want MFA to be at this level. Security first yall. This brings me to my next point, soon exo 3 will come out and security will be improved greatly.

Once you are connected, we need now to pull all the mailboxes from the system. This command can take some time if you have a large company. In fact, this script with only 300 users took around an hour. The Larger your company is, the longer it will take. Plan ahead accordingly.

$Mailboxes = Get-Mailbox -ResultSize Unlimited

Now we have all the mailboxes, we need to go through each mailbox and get the inbox rules for that mailbox. We start a for each loop of the mailboxes.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 

}

Next, we will need to grab the inbox rules for that mailbox. We do this with the Get-InboxRule commandlet and we feed it the mailbox alias.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
}

Normally a mailbox has more than one rule. Thus, we need to make another for each loop for the rules inside our main foreach loop.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
    
    }
}

Afterward, we need to pull the data out of the rules and make it useful. The amount of output is large, breaking it down and making it useful is important. That’s the whole goal of this. We want to find out who has forwarders and we want to know if those forwarders are forwarding out to someone else. I want to break it up as well so I can look at all the forwarders and just the ones with email addresses.

Gathering Information

Firstly, we need to ask the question, Are we forwarding to someone as an email or an attachment? The properties we want to look at are, forwardto and forwardasattachmentto. If either of these are not null, then we want to look at that information. This allows us to Find Forwarding Rules.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
        if (($null -ne $rule.ForwardTo) -or ($null -ne $rule.ForwardAsAttachmentTo)) {
        
        }
    }
}

Now we are looking at a rule object that has a forwarder of some sort. It’s time to let the end user know. Next, we will create a PowerShell Custom Object. Almost every get command I have come across has produced one of these objects.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
        if (($null -ne $rule.ForwardTo) -or ($null -ne $rule.ForwardAsAttachmentTo)) {
            [pscustomobject][ordered]@{

            }
        }
    }
}

The object is ready for us. It’s time to fill it in with useful information. We need the mailbox name, the rule name, the rule’s id, if it’s enabled, and finally the forwarder information. The forwarder information is broken up into two. The “ForwardTo” and the “ForwardAsAttachmentTo”. The first forwards the email to a person. The second wraps up the email into an attachment and sends it to the person. We need to see both.

These items are arrays of email addresses and references. If the forwarder points to an external email address it will contain the @ symbol like most email addresses do. If the forwarder points to an internal address like bob in accounting, then it will not have an @ symbol unless told otherwise. This is useful. We can use a where object to pull out the lines with an @ symbol.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) {
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
        if (($null -ne $rule.ForwardTo) -or ($null -ne $rule.ForwardAsAttachmentTo)) {
            [pscustomobject][ordered]@{
                Username = $Mailbox.Alias
                Rule = $Rule.name
                ID = $Rule.RuleIdentity
                Enabled = $rule.enabled
                ForwardTo = $rule.ForwardTo | where-object {$_ -like "*@*"}
                ForwardAsAttachmentTo = $rule.ForwardAsAttachmentTo | where-object {$_ -like "*@*"}
            }
        }
    }
}

Sorting the Sorted Information

Now it’s time to sort the sorted information. First why? Why not add it to the loop above? Two reasons. First is the time it takes to process. Second, I want to run $ForwarderRules to get information and I want to run the next line of code to see the more focused information. I like having options. Now we will take the forwarder rules we created and filter out the nulls of the forwarders. Finally, we want to display the information.

$ats = $ForwarderRules | where-object {($null -ne $_.ForwardTo) -or ($null -ne $_.ForwardAsAttachmentTo)}
$ats

Finally, you have all the email addresses and rules that have a forwarder that forwards to a real email address. You can run through each one and audit them for security.

Future Reading

Images created with Mid Journey AI

Share Point File Audit

Share Point File Audit

While reading on Reddit, I found a common thread. People need a quick way to do a Share Point File Audit. I have a PowerShell function for this in my toolbox. This tool heavily uses the Search-UnifiedAuditLog command let. The most common items I tend to audit are file modifications and deletions. This function goes through, modified, moved, renamed, downloaded, uploaded, accessed, synced, malware detection, restored from trash, locked, and finally unlocked. The Search-UnifiedAuditLog is an exchange online command at the time of this writing. Thus, you need to connect to exchange online. In this function, I am using the switch command. I will follow that structure for the breakdown. Lets first jump in with the function.

The Function

function Invoke-SharePointFileAudit {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true)][validateset("Deleted", "Modified", "Moved", "Renamed", "Downloaded", "Uploaded", "Synced", "Accessed", "MalwareDetected", "Restored", "Locked", "unLocked")][string]$Type,
        [parameter(Mandatory = $false)][switch]$KeepAlive,
        [switch]$SharePointOnline,
        [switch]$OneDrive, 
        [Nullable[DateTime]]$StartDate,
        [Nullable[DateTime]]$EndDate,
        [string]$Outfile,
        [int]$ResultSize = 5000
    )
    Begin {
        $Module = Get-Module ExchangeOnlineManagement -ListAvailable
        if ($Module.count -eq 0) {Install-Module ExchangeOnlineManagement -Repository PSGallery -AllowClobber -Force}

        $getsessions = Get-PSSession | Select-Object -Property State, Name
        $isconnected = (@($getsessions) -like '@{State=Opened; Name=ExchangeOnlineInternalSession*').Count -gt 0
        If ($isconnected -ne "false") {
            try { 
                Connect-ExchangeOnline
            }
            catch {
                Write-Error "Exchange Online Failed. Ending"
                end
            }
        }
        #Auto Generates Start and Finish dates
        if ($Null -eq $StartDate) { $StartDate = ((Get-Date).AddDays(-89)).Date }
        if ($Null -eq $EndDate) { $EndDate = (Get-Date).Date }
        #Tests if end date is before start date.
        if ($EndDate -lt $StartDate) { $StartDate = ((Get-Date).AddDays(-89)).Date }
        if ($EndDate -gt (Get-Date).Date) { $EndDate = (Get-Date).Date }
        
    }
    Process {
        switch ($Type) {
            "Deleted" {
                $DeletedRecords = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "FileDeleted,FileDeletedFirstStageRecycleBin,FileDeletedSecondStageRecycleBin,FileVersionsAllDeleted,FileRecycled" -SessionId deleted -SessionCommand ReturnLargeSet -ResultSize 5000
                $Return = foreach ($DeletedRecord in $DeletedRecords) {
                    $JSONInfo = $DeletedRecord.AuditData | convertfrom-json
                    [pscustomobject][ordered]@{
                        TimeStampe  = ($JSONInfo.creationtime).tolocaltime()
                        UserName    = $DeletedRecord.UserIds
                        ClientIP    = $JSONInfo.ClientIP
                        Source      = $JSONInfo.EventSource
                        Workload    = $JSONInfo.Workload
                        Operation   = $JSONInfo.Operation
                        SiteURL     = $JSONInfo.SiteURL
                        RelativeURL = $JSONInfo.SourceRelativeUrl
                        FileName    = $JSONInfo.SourceFileName
                        ObjectID    = $JSONInfo.ObjectId
                    }
                }
            }
            "Modified" {
                $ModifiedRecords = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "FileModified,FileModifiedExtended" -SessionId Modified -SessionCommand ReturnLargeSet -ResultSize 5000
                $Return = foreach ($ModifiedRecord in $ModifiedRecords) {
                    $JSONInfo = $ModifiedRecord.AuditData | convertfrom-json
                    [pscustomobject][ordered]@{
                        TimeStamp   = ($JSONInfo.creationtime).tolocaltime()
                        UserName    = $ModifiedRecord.UserIds
                        ClientIP    = $JSONInfo.ClientIP
                        Source      = $JSONInfo.EventSource
                        Workload    = $JSONInfo.Workload
                        Operation   = $JSONInfo.Operation
                        SiteURL     = $JSONInfo.SiteURL
                        RelativeURL = $JSONInfo.SourceRelativeUrl
                        FileName    = $JSONInfo.SourceFileName
                        ObjectID    = $JSONInfo.ObjectId
                    }
                }
            }
            "Moved" {
                $MovedRecords = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "FileMoved" -SessionId Moved -SessionCommand ReturnLargeSet -ResultSize 5000
                $Return = foreach ($MovedRecord in $MovedRecords) {
                    $JSONInfo = $MovedRecord.AuditData | convertfrom-json
                    [pscustomobject][ordered]@{
                        TimeStamp              = ($JSONInfo.creationtime).tolocaltime()
                        UserName               = $MovedRecord.UserIds
                        ClientIP               = $JSONInfo.ClientIP
                        Source                 = $JSONInfo.EventSource
                        Workload               = $JSONInfo.Workload
                        Operation              = $JSONInfo.Operation
                        SiteURL                = $JSONInfo.SiteURL
                        SourceRelativeURL      = $JSONInfo.SourceRelativeUrl
                        DestinationRelativeURL = $JSONInfo.DestinationRelativeURL
                        FileName               = $JSONInfo.SourceFileName
                        ObjectID               = $JSONInfo.ObjectId
                    }
                }
            }
            "Renamed" {
                $RenamedRecords = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "FileRenamed" -SessionId Renamed -SessionCommand ReturnLargeSet -ResultSize 5000
                $Return = foreach ($RenamedRecord in $RenamedRecords) {
                    $JSONInfo = $RenamedRecord.AuditData | convertfrom-json
                    [pscustomobject][ordered]@{
                        TimeStamp           = ($JSONInfo.creationtime).tolocaltime()
                        UserName            = $RenamedRecord.UserIds
                        ClientIP            = $JSONInfo.ClientIP
                        Source              = $JSONInfo.EventSource
                        Workload            = $JSONInfo.Workload
                        Operation           = $JSONInfo.Operation
                        SiteURL             = $JSONInfo.SiteURL
                        SourceRelativeURL   = $JSONInfo.SourceRelativeUrl
                        SourceFileName      = $JSONInfo.SourceFileName
                        DestinationFileName = $JSONInfo.DestinationFileName
                        ObjectID            = $JSONInfo.ObjectId
                    }
                }
            }
            "Downloaded" {
                $DownloadedRecords = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "FileDownloaded" -SessionId Downloaded -SessionCommand ReturnLargeSet -ResultSize 5000
                $Return = foreach ($DownloadedRecord in $DownloadedRecords) {
                    $JSONInfo = $DownloadedRecord.AuditData | convertfrom-json
                    [pscustomobject][ordered]@{
                        TimeStamp         = ($JSONInfo.creationtime).tolocaltime()
                        UserName          = $DownloadedRecord.UserIds
                        ClientIP          = $JSONInfo.ClientIP
                        Source            = $JSONInfo.EventSource
                        Workload          = $JSONInfo.Workload
                        Operation         = $JSONInfo.Operation
                        SiteURL           = $JSONInfo.SiteURL
                        SourceRelativeURL = $JSONInfo.SourceRelativeUrl
                        SourceFileName    = $JSONInfo.SourceFileName
                        ObjectID          = $JSONInfo.ObjectId
                    }
                }
            }
            "Uploaded" {
                $UploadedRecords = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "FileUploaded" -SessionId Uploaded -SessionCommand ReturnLargeSet -ResultSize 5000
                $Return = foreach ($UploadedRecord in $UploadedRecords) {
                    $JSONInfo = $UploadedRecord.AuditData | convertfrom-json
                    [pscustomobject][ordered]@{
                        TimeStamp         = ($JSONInfo.creationtime).tolocaltime()
                        UserName          = $UploadedRecord.UserIds
                        ClientIP          = $JSONInfo.ClientIP
                        Source            = $JSONInfo.EventSource
                        Workload          = $JSONInfo.Workload
                        Operation         = $JSONInfo.Operation
                        SiteURL           = $JSONInfo.SiteURL
                        SourceRelativeURL = $JSONInfo.SourceRelativeUrl
                        SourceFileName    = $JSONInfo.SourceFileName
                        ObjectID          = $JSONInfo.ObjectId
                    }
                }
            }
            "Synced" {
                $SyncedRecords = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "FileSyncDownloadedFull,FileSyncUploadedFull" -SessionId Synced -SessionCommand ReturnLargeSet -ResultSize 5000
                $Return = foreach ($SyncedRecord in $SyncedRecords) {
                    $JSONInfo = $SyncedRecord.AuditData | convertfrom-json
                    [pscustomobject][ordered]@{
                        TimeStamp         = ($JSONInfo.creationtime).tolocaltime()
                        UserName          = $SyncedRecord.UserIds
                        ClientIP          = $JSONInfo.ClientIP
                        Source            = $JSONInfo.EventSource
                        Workload          = $JSONInfo.Workload
                        Operation         = $JSONInfo.Operation
                        SiteURL           = $JSONInfo.SiteURL
                        SourceRelativeURL = $JSONInfo.SourceRelativeUrl
                        SourceFileName    = $JSONInfo.SourceFileName
                        ObjectID          = $JSONInfo.ObjectId
                    }
                }
            }
            "Accessed" {
                $AccessedRecords = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "FileAccessed,FileAccessedExtended" -SessionId Accessed -SessionCommand ReturnLargeSet -ResultSize 5000
                $Return = foreach ($AccessedRecord in $AccessedRecords) {
                    $JSONInfo = $AccessedRecord.AuditData | convertfrom-json
                    [pscustomobject][ordered]@{
                        TimeStamp         = ($JSONInfo.creationtime).tolocaltime()
                        UserName          = $AccessedRecord.UserIds
                        ClientIP          = $JSONInfo.ClientIP
                        Source            = $JSONInfo.EventSource
                        Workload          = $JSONInfo.Workload
                        Operation         = $JSONInfo.Operation
                        SiteURL           = $JSONInfo.SiteURL
                        SourceRelativeURL = $JSONInfo.SourceRelativeUrl
                        SourceFileName    = $JSONInfo.SourceFileName
                        ObjectID          = $JSONInfo.ObjectId
                    }
                }
            }
            "MalwareDetected" {
                $MalewareRecords = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "FileMalwareDetected" -SessionId MalewareRecords -SessionCommand ReturnLargeSet -ResultSize 5000
                $Return = foreach ($MalewareRecord in $MalewareRecords) {
                    $JSONInfo = $MalewareRecord.AuditData | convertfrom-json
                    [pscustomobject][ordered]@{
                        TimeStamp   = ($JSONInfo.creationtime).tolocaltime()
                        UserName    = $MalewareRecord.UserIds
                        ClientIP    = $JSONInfo.ClientIP
                        Source      = $JSONInfo.EventSource
                        Workload    = $JSONInfo.Workload
                        Operation   = $JSONInfo.Operation
                        SiteURL     = $JSONInfo.SiteURL
                        RelativeURL = $JSONInfo.SourceRelativeUrl
                        FileName    = $JSONInfo.SourceFileName
                        ObjectID    = $JSONInfo.ObjectId
                    }
                }
            }
            "Restored" {
                $RestoredRecords = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "FileRestored" -SessionId RestoredRecords -SessionCommand ReturnLargeSet -ResultSize 5000
                $Return = foreach ($RestoredRecord in $RestoredRecords) {
                    $JSONInfo = $RestoredRecord.AuditData | convertfrom-json
                    [pscustomobject][ordered]@{
                        TimeStamp   = ($JSONInfo.creationtime).tolocaltime()
                        UserName    = $RestoredRecord.UserIds
                        ClientIP    = $JSONInfo.ClientIP
                        Source      = $JSONInfo.EventSource
                        Workload    = $JSONInfo.Workload
                        Operation   = $JSONInfo.Operation
                        SiteURL     = $JSONInfo.SiteURL
                        RelativeURL = $JSONInfo.SourceRelativeUrl
                        FileName    = $JSONInfo.SourceFileName
                        ObjectID    = $JSONInfo.ObjectId
                    }
                }
            }
            "Locked" {
                $LockedRecords = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "LockRecord" -SessionId Locked -SessionCommand ReturnLargeSet -ResultSize 5000
                $Return = foreach ($LockedRecord in $LockedRecords) {
                    $JSONInfo = $LockedRecord.AuditData | convertfrom-json
                    [pscustomobject][ordered]@{
                        TimeStamp   = ($JSONInfo.creationtime).tolocaltime()
                        UserName    = $LockedRecord.UserIds
                        ClientIP    = $JSONInfo.ClientIP
                        Source      = $JSONInfo.EventSource
                        Workload    = $JSONInfo.Workload
                        Operation   = $JSONInfo.Operation
                        SiteURL     = $JSONInfo.SiteURL
                        RelativeURL = $JSONInfo.SourceRelativeUrl
                        FileName    = $JSONInfo.SourceFileName
                        ObjectID    = $JSONInfo.ObjectId
                    }
                }
            }
            "unLocked" {
                $unLockedRecords = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "UnlockRecord" -SessionId UnlockRecord -SessionCommand ReturnLargeSet -ResultSize 5000
                $Return = foreach ($unLockedRecord in $unLockedRecords) {
                    $JSONInfo = $unLockedRecord.AuditData | convertfrom-json
                    [pscustomobject][ordered]@{
                        TimeStamp   = ($JSONInfo.creationtime).tolocaltime()
                        UserName    = $unLockedRecord.UserIds
                        ClientIP    = $JSONInfo.ClientIP
                        Source      = $JSONInfo.EventSource
                        Workload    = $JSONInfo.Workload
                        Operation   = $JSONInfo.Operation
                        SiteURL     = $JSONInfo.SiteURL
                        RelativeURL = $JSONInfo.SourceRelativeUrl
                        FileName    = $JSONInfo.SourceFileName
                        ObjectID    = $JSONInfo.ObjectId
                    }
                }
            }
        }
        
    }
    end {
        if (!($SharePointOnline -and $OneDrive) -or ($SharePointOnline -and $OneDrive)) {
            if ($PSBoundParameters.ContainsKey("OutFile")) {
                $Return | Export-Csv ./$Outfile.CSV
            }
            else {
                $Return
            }
        }
        elseif ($SharePointOnline) {
            if ($PSBoundParameters.ContainsKey("OutFile")) {
                $Return | where-object { $_.workload -like "SharePoint" } | Export-Csv ./$Outfile.CSV
            }
            else {
                $Return | where-object { $_.workload -like "SharePoint" }
            }
        }
        elseif ($OneDrive) { 
            if ($PSBoundParameters.ContainsKey("OutFile")) {
                $Return | where-object { $_.workload -like "OneDrive" } | Export-Csv ./$Outfile.CSV
            }
            else {
                $Return | where-object { $_.workload -like "OneDrive" }
            }
        }
        if (!($KeepAlive)) {
            Disconnect-ExchangeOnline -Confirm:$false -InformationAction Ignore -ErrorAction SilentlyContinue
        }
    }
}

The Breakdown of Share Point File Audit

I’m glad you came to the breakdown. It means you want to know how the code works. This means you truly care about learning. Thank you. This code repeats itself a few times in different ways. So, I will call out the differences, but not the likes after the first time explaining something. The first section is our Parameters.

Parameters

We have 8 Parameters, and only one of them is mandatory. Firstly, we have the Type parameter. This mandatory validate set allows you to select from a list of commands we will be using in this function.

  • Deleted
  • Modified
  • Created
  • Moved
  • Renamed
  • Downloaded
  • Uploaded
  • Synced
  • Accessed
  • MalwareDetected
  • Restored
  • Locked
  • UnLocked

Afterward, we have Keep Alive. This allows us to run the command multiple times without signing back into the system. So, if you want to keep your session alive, flip that flag. Next, we have two switches. The first Switch is to pull only items edited in SharePoint itself. The next is for one drive. They are named accordingly. After that, we have a start date and an end date. These values are nullable. Basically, you don’t need them. The outfile is asking for just the name of the file. We are using the “./” to save it wherever you run the command from. Finally, we have the result size. If you want the max number of results, 5000. However, you can make this number smaller.

Begin

In our begin section, we want to test the Exchange Online Management Module. Secondly, we want to validate exchange connectivity. After that, we want to gather the date information for the start and end dates. Let’s take a look at the exchange part first.

$Module = Get-Module ExchangeOnlineManagement -ListAvailable

The Get-Module command works with PowerShell 5.1. However, I have seen PowerShell flak with this command failing to pull the information. I am going to assume your PowerShell is up to date with your current version.

if ($Module.count -eq 0) {
    Install-Module ExchangeOnlineManagement -Repository PSGallery -AllowClobber -Force
}

Afterward, we want to install the exchange online management module if we don’t detect the module. We are using the count to see how many objects are inside our module variable. If it’s 0, it’s time to install. We install it from the PSGallery.

$getsessions = Get-PSSession | Select-Object -Property State, Name
$isconnected = (@($getsessions) -like '@{State=Opened; Name=ExchangeOnlineInternalSession*').Count -gt 0

Now, we test exchange connections. We use the Get-PSSession to review the current connections. Next, we test if the connections with the name “ExchangeOnlineInternalSession” is greater than zero. “isconnected” will produce a true or false statement.

If ($isconnected -ne "false") {
    try { 
        Connect-ExchangeOnline
    } catch {
        Write-Error "Exchange Online Failed. Ending"
        end
    }
}

After which, we can test with. False, we try to connect. However, if there is an error, we end the script and let the user know. We are not using a credential object to authenticate because MFA should always be a thing.

#Auto Generates Start and Finish dates
if ($Null -eq $StartDate) { $StartDate = ((Get-Date).AddDays(-89)).Date }
if ($Null -eq $EndDate) { $EndDate = (Get-Date).Date }
#Tests if end date is before start date.
if ($EndDate -lt $StartDate) { $StartDate = ((Get-Date).AddDays(-89)).Date }
if ($EndDate -gt (Get-Date).Date) { $EndDate = (Get-Date).Date }

Afterward, we need to get the dates right. If the start date is null, we are going to pull 90 days back. We do this by using the standard. We do the same with the end date. If it’s null, we grab today’s date. Now to prevent errors, we check the start date and end date. The end date can’t be before the start date. This is similar to the end date. The end date can’t be greater than the current date. We use the if statement to resolve this.

Process

We begin the process by looking directly at our “Type” variable by using a switch command. The switch allows us to go through each “Type” and run the commands accordingly. Let’s look at one of the switch processes.

$DeletedRecords = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations "FileDeleted,FileDeletedFirstStageRecycleBin,FileDeletedSecondStageRecycleBin,FileVersionsAllDeleted,FileRecycled" -SessionId deleted -SessionCommand ReturnLargeSet -ResultSize 5000
$Return = foreach ($DeletedRecord in $DeletedRecords) {
    $JSONInfo = $DeletedRecord.AuditData | convertfrom-json
    [pscustomobject][ordered]@{
        TimeStampe  = ($JSONInfo.creationtime).tolocaltime()
        UserName    = $DeletedRecord.UserIds
        ClientIP    = $JSONInfo.ClientIP
        Source      = $JSONInfo.EventSource
        Workload    = $JSONInfo.Workload
        Operation   = $JSONInfo.Operation
        SiteURL     = $JSONInfo.SiteURL
        RelativeURL = $JSONInfo.SourceRelativeUrl
        FileName    = $JSONInfo.SourceFileName
        ObjectID    = $JSONInfo.ObjectId
    }
}

The data that search-unifiedauditlog produces a section called “AuditData”. This section has almost every piece of information you will need. The difference between each “Type” will be the Operations, and session id. The operations target the required logs. This creates the backbone of the Share Point File Audit. The graph below will show which operations I am using. Once you gather the operation information, we need to pull the AuditData. This data will be in JSON format. We start off by looping the records with a for each loop. Then we pull the auditdata and pipe it into convertfrom-json. Next, we create our PS Custom Object. Other than Moved, the output of the other logs contains almost the same information. See the script for the information.

Operation Filters

  • Deleted
    • FileDeleted
    • FileDeletedFirstStageRecycleBin
    • FileDeletedSecondStageRecycleBin
    • FileVersionsAllDeleted
    • FileRecycled
  • Modified
    • FileModified
    • FileModifiedExtended
  • Moved
    • FileMoved
  • Renamed
    • FileRenamed
  • Downloaded
    • FileDownloaded
  • Uploaded
    • FileUploaded
  • Synced
    • FileSyncDownloadedFull
    • FileSyncUploadedFull
  • Accessed
    • FileAccessed
    • FileAccessedExtended
  • MalwareDetected
    • FileMalwareDetected
  • Restored
    • FileRestored
  • Locked
    • LockRecord
  • UnLocked
    • UnlockRecord

End

Finally, it’s time for the end block. This is where we will present the data we have gathered. Firstly, we need to determine if the SharePoint or Onedrives were flipped or not.

if (!($SharePointOnline -and $OneDrive) -or ($SharePointOnline -and $OneDrive)) {
    if ($PSBoundParameters.ContainsKey("OutFile")) {
        $Return | Export-Csv ./$Outfile.CSV
    } else {
        $Return
    }
}

Here we checking if both flags are not checked or if both flags are checked. Then we check if the user gave us a filename. If they did, we export our report to a csv file wherever we are executing the function from. However, if the user didn’t give us a filename, we just dump all the results.

elseif ($SharePointOnline) {
            if ($PSBoundParameters.ContainsKey("OutFile")) {
                $Return | where-object { $_.workload -like "SharePoint" } | Export-Csv ./$Outfile.CSV
            }
            else {
                $Return | where-object { $_.workload -like "SharePoint" }
            }
        }
        elseif ($OneDrive) { 
            if ($PSBoundParameters.ContainsKey("OutFile")) {
                $Return | where-object { $_.workload -like "OneDrive" } | Export-Csv ./$Outfile.CSV
            }
            else {
                $Return | where-object { $_.workload -like "OneDrive" }
            }
        }
        if (!($KeepAlive)) {
            Disconnect-ExchangeOnline -Confirm:$false -InformationAction Ignore -ErrorAction SilentlyContinue
        }

Now, if the user selected either or, we present that information. We present those infos by using a where-object. Like before we ask if the user produced an outfile. Finally, we ask if keep alive was set. If it wasn’t we disconnect from the exchange.

Conclusion

In conclusion, auditing shouldn’t be difficult. We can quickly pull the info we need. I hope you enjoy this powerful little tools.

Azure AD Hardening- Revoke disabled sign-in tokens

Azure AD Hardening- Revoke disabled sign-in tokens

As part of my efforts to harden clients’ azure accounts, We need to remove/refresh all the sign-in tokens from the disabled account. Why? That’s very simple, If you don’t, then any signed-in device still has access. For example, if I terminate a user, I block their sign-in in office 365. The block will take effect within 24 hours. This is why we want to Revoke Disabled Sign-in tokens.

The user can steal data if you don’t remove/refresh the sign-in tokens. By removing/refreshing the sign-in tokens, the outlook can no longer authenticate back with office 365. This means no more new emails. Everything on the computer is still a free game. The user can decode the PST files. I revoke/refresh the tokens with a single command from the Azure AD PowerShell module.

Revoke Disabled Sign-in

Get-AzureADUser | where-object {$_.AccountEnabled -eq $False} | foreach-object {Revoke-AzureADUserAllRefreshToken -ObjectId $_.ObjectId}

The Breakdown

It’s time to break down the “Revoke Disabled Sign-in” script. The first step is to grab all the users from the azure ad. We do this with the Get-AzureADUser command. Next, we parse that information through a where-object. We want to filter out the accountEnabled by the value of false. Now that we have all the disabled users we start a foreach-object loop. Inside our loop, we want to trigger the Revoke-AzureADUserAllRefreshToken command using the Object ID. We are grabbing the previous command’s output using the $_ object. The ObjectID is the object we are pulling out.

Conclusion

At the end of the day, this code snippet is a catch-all for standard processes. Whenever you terminate an employee, it’s always a good idea to revoke the sign-in tokens.

I hope this helps. Please let me know if you have any questions.

Microsoft Safety Scanner Powershell One Liner

Microsoft Safety Scanner Powershell One Liner

A client called in and told me a line of information that made me concerned about security. I ran a webroot scan and wanted to give another level of the scan. I am partial to the Microsoft Safety Scanner. It runs well connectwise backstage. You can read more about the safety scanner here. So, lets look at this oneliner.

if (!(Test-Path "c:\Temp")) { New-Item -Path "c:\" -Name Temp -ItemType Directory }; invoke-webrequest -Uri https://go.microsoft.com/fwlink/?LinkId=212732 -OutFile c:\temp\mss.exe -UseBasicParsing; c:\temp\mss.exe /Q /F:Y

The first part of this little script is to test and create the folder that will hold our file. We are doing this by using test-path. Then if the file doesn’t exist, aka !. Then we create it with the new-item.

if (!(Test-Path "c:\Temp")) { New-Item -Path "c:\" -Name Temp -ItemType Directory }

The next part is we are going to download the Microsoft security scanner from Microsoft directly. The link is the direct download. We use invoke-webrequest to download the file. The -outfile flag is where we will download the file at and its name. In this case, we are going to name it something simple. Mss.exe inside our temp folder. We use the -usebasicparsing because most machines only have PowerShell 5.

invoke-webrequest -Uri https://go.microsoft.com/fwlink/?LinkId=212732 -OutFile c:\temp\mss.exe -UseBasicParsing

Then we run the command needed. We start the command with the path. C:\temp\mss.exe. We want it to be quiet and we want to force it. So we use the /Q to quiet, and /F:Y to force.

c:\temp\mss.exe /Q /F:Y

The system will not prompt for any kind of approval. It will run and delete what it needs to delete. This is a simple, deploy and walk away one-liner. So, add it to your deployment scripts and enjoy scanning with a Microsoft safety scanner.