Use PowerShell to check if (shortened) URL link is safe

It just happens a lot: you receive a (shortened) URL and are unsure if it’s safe to open. Sometimes, I open it in Windows Sandbox or use the urlscan.io website. In this blog post, I will show you how you can automate the check of a URL and see if it’s safe or not 🙂

What is URL Shortening?

URL shortening is a technique on the World Wide Web in which a Uniform Resource Locator (URL) may be made substantially shorter and still direct to the required page. This is achieved by using a redirect which links to the web page that has a long URL.”

Source: https://en.wikipedia.org/wiki/URL_shortening

How does the script work?

You can see what the (shortened) link redirects you to, and that’s what the script will check first. If it gets a valid response, it will use an API request to check on urlscan.io if the link is malicious. The results will be returned to your PowerShell session.

Parameters for the script are:

  • -APIKEY You can use this to specify an API Key from urlscan.io; see the instructions below on how to get one. The script can also hardcode it if it is not supplied as a default value.
  • – Seconds By default, it submits the URL(s) it found and will wait 45 seconds before retrieving the results. If your requests fail, you can increase the default by using the -Seconds Parameter or changing the script to a higher default value.
  • -URL You can specify one URL or multiple if needed by separating them by a comma. For example, -URL aka.ms/mfasetup, aka.ms/admin
  • -Type This is important. It will specify whether or not your request will be visible in the urlscan.io dashboard. Values are public, private, or unlisted. The default value is Private.

    These values mean:

    Public: The URL does not contain PII or confidential data, and you want other researchers to be able to discover it.
    Unlisted: The site might contain PII or mildly sensitive data, but you want security vendors and reputable researchers to be able to detect it to improve their products and take action (for example, takedown requests). The URL does not contain PII or confidential data, and you want other researchers to be able to discover it.
    Private: Nobody but you should be able to see the scan results.

Note: The default scan type is Private, limited to 50 per day. If you need more scans, please switch to Public, which allows 5K scans daily!

Registering an API Key

The script uses urlscan.io for its results. The steps to get the API key are as follows:

  • Go to https://urlscan.io/user/signup
  • Enter your details and select Create Account
  • Check the supplied email Inbox to complete the signup. Click on the Confirm Email & Finish Signup! Button.
  • Login using the created account and password
  • Select Settings & API
  • Select New API key
  • Enter a description, PowerShell, for example, and select Create API key
  • Copy the API key, replace XXX-XXX-XXX with that key in the script, and save it

The Free plan allows 5,000 Public scans per day, and that should be enough, I guess 😉 (Check https://urlscan.io/pricing/ for more information, the default type in the script is Private, which allows for 50 scans per day. Switch to the public type if you need more)

Running the script

Below is an example of the script where I check for two URLs: aka.ms/mfasetup and aka.ms/admin:

These were two Microsoft URLs, and yes, they are clean 🙂 Below is an example of a malicious site:

As you can see, it has a 100% score that the site is a phishing site. The brand’s values can be:

In this case, it was AT&T:

Note: Your local Anti-Virus solution might stop you from checking the URL. The script will not output any results for those because of that 🙂 Also, some URLs are blocked from scanning, and the script will skip those. If you check that URL manually on urlscan.io, you should see something like:

Wrapping up

In this blog post, I showed you how to use PowerShell and urlscan.io to determine whether a URL might be malicious. It’s always good to check if you have doubts before opening a URL. Have a lovely weekend!

The script

Below is the script. Please save it to c:\scripts\Test-URL.ps1, for example, and run it using the above parameters.

param (
    [Parameter(Mandatory = $false)][string]$APIKEY = 'XXX-XXX-XXX',
    [Parameter(Mandatory = $false)][int]$Seconds = 45,
    [Parameter(Mandatory = $true)][string[]]$URL,
    [Parameter(Mandatory = $false)][validateset("public", "unlisted", "private")][string]$Type = 'private'
)

#Retrieve actual url(s) for $URL
$ProgressPreference = "SilentlyContinue"
$ExpandedURLs = foreach ($Site in $URL) {
    try {
        $LongURL = (Invoke-WebRequest -Uri $Site -MaximumRedirection 0 -ErrorAction SilentlyContinue).Headers.Location
        [PSCustomObject]@{
            ShortURL = $Site
            LongURL  = if ($LongURL) { $LongURL } else { $site }
        }
        Write-Host ("Processed {0}..." -f $Site ) -ForegroundColor Green
    }
    catch {
        Write-Warning ("Specified {0} URL could not be found or expanded, skipping..." -f $Site)
    }
}

#Submit expanded $URL(s) for checking on urlscan.io if $ExpandedURLs returned LongURL(s)
$ProgressPreference = "SilentlyContinue"
$ExpandedURLs = foreach ($Site in $URL) {
    try {
        # Use .NET WebRequest to follow redirects automatically
        $request = [System.Net.WebRequest]::Create($Site)
        $response = $request.GetResponse()
        $LongURL = $response.ResponseUri.ToString()
        $response.Close()

        [PSCustomObject]@{
            ShortURL = $Site
            LongURL  = $LongURL
        }
        Write-Host ("Processed {0}..." -f $Site) -ForegroundColor Green
    }
    catch {
        Write-Warning ("Specified {0} URL could not be found or expanded, skipping..." -f $Site)
    }
}

    #Retrieve results for submitted urls if any, wait for the amount of seconds specified in $Seconds (Default is 45)
    if ($submits) {
        Write-Host ("Sleeping for {0} seconds to wait on results..." -f $Seconds) -ForegroundColor Green
        Start-Sleep -Seconds $Seconds
        $results = foreach ($response in $submits) {
            try {
                $data = Invoke-RestMethod -Uri $response.api -Method Get -ErrorAction Stop
                [PSCustomObject]@{
                    ShortURL    = $response.ShortURL
                    LongURL     = $response.LongURL
                    Score       = $data.verdicts.overall.score
                    Categories  = if ($data.verdicts.overall.categories) { $data.verdicts.overall.categories } else { "None" }
                    Brands      = if ($data.verdicts.overall.brands) { $data.verdicts.overall.brands } else { "None" }
                    Malicious   = if ($data.verdicts.overall.Malicious) { $data.verdicts.overall.Malicious } else { "None" }
                    Hasverdicts = $data.verdicts.overall.hasVerdicts
                }
            }
            catch {
                Write-Warning ("Could not retrieve results for {0}" -f $response.ShortURL)
            }
        }
        return $results | Sort-Object ShortURL | Format-Table -AutoSize
    }
    else {
        Write-Warning ("URLS failed to submit, try to increase time-out or scan manually on urlscan.io. Exiting...")
        return
    }
}
else {
    Write-Warning ("Supplied URL(s) could not be found, exiting..." )
    return
}

Download the script(s) from GitHub here.

15 thoughts on “Use PowerShell to check if (shortened) URL link is safe

  1. Nice! When testing the AT&T url, AVast popped-up their threat warning so the script did not provide details, so glad that worked.

    1. Ah 🙂 Nice, good that the AV already helps you before even thinking of opening that URL! I will add that to the post, there might be others AV who rescue you before trying to check it yourself 😉

      1. I added a visual pop-up; is there a way to determine if AV was activated?
        $pop = New-Object -ComObject wscript.shell…..

        catch {
        $popup = $pop.popup(“$URLnNot Found ornNot Expandable or`nStopped by Anti-Virus”,4,”Closing”,4096)
        return

      2. Changed the script and the blog post again, updated the time-out to 45 seconds and made it more robust (Will skip items if they can’t be processed but will return as much as possible with multiple URLs beign scanned)

  2. When I try checking for known safe sites like “portal.office.com”, I get the below error message”

    WARNING: Specified portal.office.com URL could not be found or expanded, exiting…

    1. Just tested it, I don’t receive an error myself? What does this return when you run it?

      (Invoke-WebRequest -Uri portal.office.com -MaximumRedirection 0 -ErrorAction SilentlyContinue).Headers.location

      1. I am getting a similar issue, when running that command I get an error Invoke-WebRequest: Response status code does not indicate success: 302 (Found) , for this one I made https://shorturl.at/Sk2gm I got the same but 301 Moved Permanently, nothing gets stored in the LongURL property of $expandedURLs, I have tried disabling AV encase that was at play…

    2. It does seem to have intermittent issues while checking for redirection… Sometimes it works, sometimes it doesn’t? Looking into that and fixed a few small things in the process, please download or copy/paste the script again for the latest version

    3. Changed the script and the blog post again, updated the time-out to 45 seconds and made it more robust (Will skip items if they can’t be processed but will return as much as possible with multiple URLs beign scanned)

  3. I suggest to ad screenshot capabilities as urlscan has. It could save results to a folder with screenshot and result 😉

  4. If you add an ErrorVariable to the “$LongURL = (Invoke-WebRequest -Uri ……” part of the code you can add the following to the catch-block: “$IWRError.ErrorRecord.Exception.Response.StatusCode” For a HTTP-Status code in text.

Leave a Reply to Harm VeenstraCancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.