Wednesday 26 August 2015

Palo Alto UID Agent with NPS RADIUS SSO

Don't get me wrong. I love my Palo Alto Firewall. Best decision i have ever made to be purchased, its a brilliant device, but the SSO functionality when it comes to RADIUS is a bit shit because well it isn't there.

The scenario I will give you is if you have NPS(RADIUS) running for your wireless and you would like SSO authenticated internet for you non-domain users, ie BYOD, then you are shit out of luck with Palo Alto, it will prompt the user for a login. Not the end of the world, but still annoying.

Thus this amazing script came which watches the NPS log file, comparing it to the DHCP leases, and will send and xml request through the Palo Alto UID(download UID with your Palo Alto login at link).

It does create a little bit of load on the server and I think it could do with some refining to allow it to run a lot faster. If I ever get around to it I will make a multi threaded version allowing multiple subnets to be scanned at the same time and also do some subnet mapping based off the SSID of the wireless to reduce the load of requesting subnets from the DHCP which appears to be the most CPU intensive and the part that slows the script down the most.

There are two prereqs with 
- Remote DHCP Admin Console, which adds the Powershell Plugins used in the script
- Palo Alto UID Agent with the following turned on

and here is the script
#original script from https://github.com/cesanetwan/uid-radius-script-ps
# modified to remove config file to reduce load as well as fixed a few errors in the code
param([string]$global:strEventUser, [string]$global:strCallingStation)
$ErrorActionPreference = "Stop"
$global:strVersion = "5.8ps"
#create arrays - you can leave them as is
$global:aClientIPs = @()
$global:aExclusions = @()
$global:aDHCPServers = @()
#domain name
$global:strDomain = "DOMAIN"
#location of your NPS Log file
$global:strLogPath = "C:\System32\LogFiles\"
#IP Address of the Palo Alto UID Script
#Runs best if it is installed on the same server as NPS
$global:strAgentServer = "127.0.0.1"
$global:strAgentPort = "5006"
#subnets to exclude from the DHCP Request
$global:aExclusions += ""
#leave this as DHCP, its now the only option that works
$global:strLogFormat = "DHCP"
#DHCP Servers, if you set them up for resiliance you shuld only need one here
$global:aDHCPServers += "PDC"
$global:aDHCPServers += "DC"
#settings for the UID Agent, leave them as is
$global:strVsys = "vsys2"
$global:strAPIKey = "key"
$global:blnAgent = "1"
$global:strTimeout = "120"
#debug to create a log file in the following C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log
$global:strDebug = "0"
#the address of your Palo Alto for the API
$global:strPostAddr = "https://192.168.1.1/api/"
#do you have a proxy server set in the UID Agent?
$global:strProxy = "0"
#do you want it to run multiple passes? if you are running one DHCP you can leave this as 0
$global:blnMultipass = "0"
Function PostToAgent
{
param([string]$strUserAgentData)
If ([int]$global:blnAgent -eq 1)
{
$url = "https://" + $global:strAgentServer + ":" + $global:strAgentPort + "/"
[System.Net.HttpWebRequest]$request = [System.Net.HttpWebRequest] [System.Net.WebRequest]::Create($url)
$request.Method = "PUT"
If ([int]$global:strDebug -gt 0)
{
$message = "Local agent installed, posting data to " + $url
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
}
Else
{
If ($global:strProxy -eq "1")
{
       $url = $global:strPostAddr
If ([int]$global:strDebug -gt 0)
{
$message = "Posting to XMLAPIProxy, URL: " + $url
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
}
Else
{
   $url = $global:strPostAddr + "?key=" + $global:strAPIKey + "&type=user-id&action=set&vsys=" + $global:strVsys + "&client=wget&file-name=UID.xml"
If ([int]$global:strDebug -gt 0)
{
$message = "Posting to XMLAPI on firewall, URL: " + $url
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
}
[System.Net.HttpWebRequest]$request = [System.Net.HttpWebRequest] [System.Net.WebRequest]::Create($url)
$request.Method = "POST"
}
If ([int]$global:strDebug -gt 0)
{
$message = "Starting post"
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
$request.ContentType = "text/xml"
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
$bytes = [System.Text.Encoding]::UTF8.GetBytes($strUserAgentData)
$request.ContentLength = $bytes.Length
[System.IO.Stream] $outputStream = [System.IO.Stream]$request.GetRequestStream()
        $outputStream.Write($bytes,0,$bytes.Length)
        $outputStream.Close()
If ([int]$global:strDebug -gt 0)
{
$message = "Finished Post"
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
try
    {
        [System.Net.HttpWebResponse]$response = [System.Net.HttpWebResponse]$request.GetResponse()  
        $sr = New-Object System.IO.StreamReader($response.GetResponseStream())    
        $txt = $sr.ReadToEnd()
If ([int]$global:strDebug -gt 0)
{
$message = "Response: " + $txt
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
    }
    catch [Net.WebException] {
        [System.Net.HttpWebResponse] $resp = [System.Net.HttpWebResponse] $_.Exception.Response
        add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uidradiuserrors.log" -Value $resp -Force
    }
}
Function CleanMac
{
param([string]$strMac)
$strMac = $strMac -replace "-", ""
        $strMac = $strMac -replace "\.", ""
        $strMac = $strMac -replace ":", ""
        $strMac = $strMac.ToLower()
        return $strMac
}

Function ProcessDHCPClients
{
If ($global:strEventUser.contains("\"))
{
$pos = $global:strEventUser.IndexOf("\")
$global:strEventUser = $global:strEventUser.Substring($pos+1)
} ElseIf ($global:strEventUser.contains("@"))
{
$pos = $global:strEventUser.IndexOf("@")
$global:strEventUser = $global:strEventUser.Substring(0,$pos)
}
If (-Not ($global:strEventUser.contains("$")) -and -Not ($global:strEventUser.contains("host/")) )
{
If ($global:strCallingStation -match "\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b")
{
$aMatchedIPs = @()
$aMatchedIPs += $global:strCallingStation
If ([int]$global:strDebug -gt 0)
{
$message = "CallingStation is IP, no need for DHCP query"
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
}
Else
{
If ($global:blnMultipass -eq "0") {
If ([int]$global:strDebug -gt 0)
{
$message = "No MultiPass required, performing single pass"
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
$aMatchedIPs = @()
foreach ($DHCPServer in $global:aDHCPServers)
{
If ([int]$global:strDebug -gt 0)
{
$message = "Querying DHCP Server: " + [string]$DHCPServer
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
$scopes = Get-DhcpServerv4Scope -CN $DHCPServer | select ScopeId
foreach ($scope in $scopes)
                                        {
If ([int]$global:strDebug -gt 0)
{
$message = "Checking Scope: " + [string]$scope
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
                                               $aReservations = Get-DhcpServerv4Lease -CN $DHCPServer -ScopeId $scope.ScopeID -AllLeases | select IPAddress, ClientID
                                               foreach ($reservation in $aReservations)
                                               {
                                                    $MAC = CleanMac($reservation.ClientID)
   If ([int]$global:strDebug -gt 1)
   {
$message = $MAC
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
   }
                                                    $global:strCallingStation = CleanMac($global:strCallingStation)
                                                    If ($global:strCallingStation -eq $MAC)
                                                    {
If ([int]$global:strDebug -gt 0)
       {
   $message = "MAC found, IP is: " + [string]$reservation.IPAddress
   add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
       }
                                                        $aMatchedIPs += $reservation.IPAddress
                                                    }
                                                }
                                        }
}
}
Else
{
If ([int]$global:strDebug -gt 0)
{
$message = "MultiPass required, performing two passes"
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
$aMatchedIPs = @()
$mp = 0
While ($mp -lt 2) {
If ([int]$global:strDebug -gt 0)
{
$message = "Pass " + [string]$mp + ":"
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
foreach ($DHCPServer in $global:aDHCPServers)
{
If ([int]$global:strDebug -gt 0)
{
$message = "Querying DHCP Server: " + [string]$DHCPServer
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
$scopes = Get-DhcpServerv4Scope -CN $DHCPServer | select ScopeId
foreach ($scope in $scopes)
                                        {
If ([int]$global:strDebug -gt 0)
{
$message = "Checking Scope: " + [string]$scope
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
                                                $aReservations = Get-DhcpServerv4Lease -ScopeId $scope.ScopeID -AllLeases | select IPAddress, ClientID
                                                foreach ($reservation in $aReservations)
                                                {
                                                    $MAC = CleanMac($reservation.ClientID)
If ([int]$global:strDebug -gt 1)
    {
$message = $MAC
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
    }
                                                    $global:strCallingStation = CleanMac($global:strCallingStation)
                                                    If ($global:strCallingStation -eq $MAC)
                                                    {
If ([int]$global:strDebug -gt 0)
        {
    $message = "MAC found, IP is: " + [string]$reservation.IPAddress
    add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
        }
                                                        $aMatchedIPs += $reservation.IPAddress
                                                    }
                                                }
                                        }
}
$mp = $mp + 1
}
}
}
foreach ($address in $aMatchedIPs)
{
If ($global:strProxy -eq "1")
{
[string]$strXMLLine = "<uid-message><version>1.0</version><scriptv>" + $global:strVersion + "</scriptv><type>update</type><payload><login>"
}
Else
{
[string]$strXMLLine = "<uid-message><version>1.0</version><type>update</type><payload><login>"
}
If ($global:blnAgent -eq "1")
{
$strXMLLine = $strXMLLine + "<entry name=""" + $global:strDomain + "\" + $global:strEventUser + """ ip=""" + $address + """/>"
}
Else
{
If ($global:strProxy -eq "1")
{
$strXMLLine = $strXMLLine + "<entry name=""" + $global:strDomain + "\" + $global:strEventUser + """ ip=""" + $address + """ timeout=""" + $global:strTimeout + """ vsys=""" + $global:strVsys + """/>"
}
Else
{
$strXMLLine = $strXMLLine + "<entry name=""" + $global:strDomain + "\" + $global:strEventUser + """ ip=""" + $address + """ timeout=""" + $global:strTimeout + """/>"
}
}
$strXMLLine = $strXMLLine + "</login></payload></uid-message>"
If ([int]$global:strDebug -gt 0)
{
$message = "Posting mapping: " + $strXMLLine
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
PostToAgent $strXMLLine
}
}
}
Try
{
If ([int]$global:strDebug -gt 0)
{
$ct = Get-Date
$message = "Script launched at " + [string]$ct + " with arguments " + $global:strEventUser + " & " + $global:strCallingStation
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value "==========================================================================" -Force
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value "Config Loaded Successfully" -Force
}
If ($global:strLogFormat -eq "DTS")
{
#ProcessDTSLog
}
ElseIf ($global:strLogFormat -eq "IAS")
{
#ProcessIASLog
}
ElseIf ($global:strLogFormat -eq "DHCP")
{
If ([int]$global:strDebug -gt 0)
{
$message = "Script in DHCP mode, starting DHCP Process"
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
ProcessDHCPClients
If ([int]$global:strDebug -gt 0)
{
$message = "Finished processing DHCP Clients"
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
}
}
If ([int]$global:strDebug -gt 0)
{
$ct = Get-Date
$message = "Script finished at: " + [string]$ct
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value $message -Force
add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uiddebug.log" -Value "==========================================================================" -Force
}
}
Catch
{
    $ErrorMessage = $_.Exception.Message
    $FailedItem = $_.Exception.ItemName
    $ErrorLog = $FailedItem + " failed with message " + $ErrorMessage
    add-content -Path "C:\Program Files (x86)\Palo Alto Networks\User-ID Agent\uidradiuserrors.log" -Value $ErrorLog -Force
    Break
}

you will then need to create a task schedule which i have exported for you so you will need to change the script location and the user running

The original script was from https://github.com/cesanetwan/uid-radius-script-ps but it had quite a few errors in it that I have cleaned up and removed the config file from it as it was causing the script to slow down quite a lot. The next stage is to multi thread the sub net scanning to increase the rate at which the script can run and do mapping of subnets to wireless SSIDs coming through the NPS log file. I will most likely also remove all the debugging if statements and have a running/debug version.

anywho i thought it would be a good script to share and credit to the cesanetwan script for the base of this one.

Tuesday 25 August 2015

Reset User Passwords with AD Self Service Portal

I ran across a problem when we wanted specific users to be able to reset there passwords through some sort of self service mechanism.

After looking at many services(very expensive services!) to reset passwords which a lot of required enrolling, and where a pain to setup, which  I stumbled across the following page 

Although I was explicitly looking for a method of password reset via email I went about and modified the script to suit my needs.

so without further ado i present

function Create-RandomString()
{
  $aChars = @()
  $aChars = "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "C", "b", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "2", "3", "4", "5", "6", "7", "8", "9", "_", ";"
  $intUpperLimit = Get-Random -minimum 8 -maximum 10

  $x = 0
  $strString = ""
  while ($x -lt $intUpperLimit)
  {
     $a = Get-Random -minimum 0 -maximum $aChars.getupperbound(0)
     $strString += $aChars[$a]
     $x += 1
  }

  return $strString
}

#prereqs need to be installed
# - Remote Admin tools for AD - RSAT
# - Exchange pluggins http://www.microsoft.com/en-us/download/confirmation.aspx?id=42951
#original script from
#http://deployhappiness.com/reset-user-passwords-with-ad-self-service-portal/
#password change script for changing the reset password
#https://unopasscore.codeplex.com/
#Configuration Block
#SMTP Server that mail will go OUT through
$SmtpServer = "smtp.email.com"
#email address that will be used as the REPLY email address
$ResetEmail = "donotreply@email.com"
#the email account to check
$Username = "PassReset@email.com"
$Password = "P@ASSWORD"
#exchange web address, this "should" work for any o365 install by changing the username and password above
$MailServer = "https://outlook.office365.com/ews/exchange.asmx"
$ExchangeVersion = "Exchange2013"
#AD Field that the recovery email address is stored in AD. The email recieved needs to come from the email address specified in the AD Field
$ADMailField = "mail"
#AD Server to send the password reset to. You can use the FQN or IP Address
$ADServer = "DC.local"

#User who shold recieve email notifications that a password was reset or an invalid request was sent.
$LoggingUser = "log@email.com"

#Download for file is here: http://www.microsoft.com/en-us/download/confirmation.aspx?id=42951
[Reflection.Assembly]::LoadFile("C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll")

$email = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::$ExchangeVersion)
$email.Credentials = New-Object Net.NetworkCredential($Username, $Password)
$uri=[system.URI] $MailServer
$email.Url = $uri
$inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($email,[Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox)

if ($inbox.UnreadCount -gt 0) {
 $PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
 $PropertySet.RequestedBodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::Text;
  # Set search criteria - unread only
 $SearchForUnRead = New-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::IsRead, $false)
 $items = $inbox.FindItems($SearchForUnRead,10)  #return only 10 unread mail items
  Import-Module -Name ActiveDirectory

 foreach ($item in $items.Items) {
   # load the property set to allow us to view the body
  $item.load($PropertySet)

  if($item.Body.text -Like "*") {
   $EmailAddress = $item.From.address
   $user = Get-ADUser -Filter {$ADMailField -eq $EmailAddress} -Properties $ADMailField -Server $ADServer
 
   If ($user -ne $null) {
    $PW = Create-RandomString
    if ($PW.length -gt 1) {
     Set-ADAccountPassword -identity $user.SamAccountName -Server $ADServer -Reset -NewPassword (ConvertTo-SecureString -AsPlainText $PW -Force)
     Unlock-ADAccount -identity $user.SamAccountName -Server $ADServer
   
     $PasswordAge = (Get-ADUser $user -Properties PasswordLastSet | Select PasswordLastSet)
     if ($PasswordAge.PasswordLastSet -ge (Get-Date).AddMinutes(-1)) {
     $Body = "Password reset for " + $user.SamAccountName + "-" + $user.DistinguishedName
     $UsernameReset = $user.SamAccountName
     send-mailmessage -to $LoggingUser -from $ResetEmail -subject "Password Reset - $PW" -body $Body -SmtpServer $SmtpServer
     send-mailmessage -to $item.From.address -from $ResetEmail -subject "Pass Login" -body "Your Username and Password are the following,`nUsername: $UsernameReset `nPassword: $PW`nTo change yout password please visit `n`nIf you are still having problems with your login please contact " -SmtpServer $SmtpServer
     }
    }
   }
   else {
    send-mailmessage -to $LoggingUser -from $ResetEmail -subject "Invalid email" -body "email address was not found " $item.From.address -SmtpServer $SmtpServer
    send-mailmessage -to $item.From.address -from $ResetEmail -subject "Invalid email " -body "Your email was not found." -SmtpServer $SmtpServer
   }
  }
  $item.Isread = $true
  $item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite)
 }
}

The script works by doing the following,
- An email is sent from the account that cannot login to a specific account such as above PassReset@email.com, this is important that the email was sent FROM the account that has the associated email address you want to reset
- AD lookup is done filtering by the email address that it was received from.
- email sent back from a different address such as donotreply@email.com to prevent a mail loop from automatic replies with the username and password reset

This works off the concept that the email address that is sending the email is secure and as such is the authoritative source to find the account and provide details for.

I then used the following
link
to allow the user to change there password once they have logged in and that's it! we are all done.

EDIT:
I was also suggested this one which looks like it would work, over kill but would work.
https://github.com/jrivard/pwm

EDIT2:
I updated the script with a couple more variables to make it easier to configure
forgot to update the download the link

EDIT3: moved the function to the top

EDIT4: updated with a prereq