Last active
January 9, 2026 15:34
-
-
Save LuemmelSec/208b8ba52b645ec189031d2b5200f76e to your computer and use it in GitHub Desktop.
Script to get SharePoint Version and release date remotely
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Script to check SharePoint version numbers directly with Microsoft | |
| # Usage: | |
| # iex(new-object net.webclient).downloadstring("https://raw.githubusercontent.com/LuemmelSec/Pentest-Tools-Collection/refs/heads/main/tools/Azure/Get-SPVersionInfo.ps1") | |
| # Get-SPVersionInfo -ServerUrl "https://my-sharepointserver" | |
| # Get-SPVersionInfo -InputFile .\targets.txt -SkipCertificateCheck -SkipHttpErrorCheck | |
| function Get-SPVersionInfo { | |
| param ( | |
| [string]$ServerUrl, | |
| [string]$InputFile, | |
| [switch]$SkipCertificateCheck, | |
| [switch]$SkipHttpErrorCheck | |
| ) | |
| if (-not $ServerUrl -and -not $InputFile -and -not $SkipCertificateCheck -and -not $SkipHttpErrorCheck) { | |
| Write-Host "Usage: Get-SPVersionInfo -ServerUrl <URL> | -InputFile <Path> [-SkipCertificateCheck] [-SkipHttpErrorCheck]" -ForegroundColor DarkYellow | |
| return | |
| } | |
| function Normalize-Url { | |
| param ([string]$url) | |
| if ($url -notmatch '^https?://') { | |
| return "https://$url" | |
| } | |
| return $url | |
| } | |
| if ($InputFile) { | |
| if (-not (Test-Path $InputFile)) { | |
| Write-Host "Input file '$InputFile' not found." -ForegroundColor Red | |
| return | |
| } | |
| $targets = Get-Content $InputFile | Where-Object { $_ -and $_.Trim() -ne "" } | |
| foreach ($target in $targets) { | |
| $normalizedTarget = Normalize-Url $target | |
| Write-Host "`n======================" | |
| Write-Host "Checking $normalizedTarget" | |
| Write-Host "======================" | |
| Get-SPVersionInfo -ServerUrl $normalizedTarget -SkipCertificateCheck:$SkipCertificateCheck -SkipHttpErrorCheck:$SkipHttpErrorCheck | |
| } | |
| return | |
| } | |
| if (-not $ServerUrl) { | |
| Write-Host "No server URL specified." -ForegroundColor Red | |
| return | |
| } | |
| $ServerUrl = Normalize-Url $ServerUrl | |
| try { | |
| $uri = [Uri]$ServerUrl | |
| if (-not $uri.Scheme.StartsWith("http")) { | |
| throw "Invalid URL scheme." | |
| } | |
| } catch { | |
| Write-Host "Invalid ServerUrl format: $ServerUrl" -ForegroundColor Red | |
| return | |
| } | |
| $msUpdatePage = "https://learn.microsoft.com/en-us/officeupdates/sharepoint-updates" | |
| $serviceCnfUrl = "$ServerUrl/_vti_pvt/service.cnf" | |
| $startPageUrl = "$ServerUrl/_layouts/15/start.aspx" | |
| $headerVersion = $null | |
| $serviceCnfVersion = $null | |
| $headerVersionTrimmed = $null | |
| $serviceCnfVersionTrimmed = $null | |
| $foundMatchHeader = $false | |
| $foundMatchServiceCnf = $false | |
| $headerFullMatch = $false | |
| $serviceCnfFullMatch = $false | |
| $siteClientTagMatched = $false | |
| function Get-FirstCellValue { | |
| param ($cellHtml) | |
| $first = ($cellHtml -split '(?i)<br\s*/?>')[0] | |
| return ($first -replace '<.*?>', '').Trim() | |
| } | |
| function Get-FirstKbNumber { | |
| param ($cellHtml) | |
| $m = [regex]::Match($cellHtml, 'KB\s*(\d+)', 'IgnoreCase') | |
| if ($m.Success) { return $m.Groups[1].Value } | |
| else { return "N/A" } | |
| } | |
| function GetProductNameFromVersion($versionString) { | |
| # Accepts 3- or 4-segment versions | |
| if ($versionString -match "^16\.0\.(\d{4,5})(?:\.\d+)?$") { | |
| $majorMinor = ($versionString -split '\.')[2] | |
| if ([int]$majorMinor -lt 10000) { return "2016" } | |
| elseif ([int]$majorMinor -ge 10000 -and [int]$majorMinor -lt 18000) { return "2019" } | |
| else { return "Subscription" } | |
| } | |
| elseif ($versionString -match "^15\.0\.(\d+)(?:\.\d+)?$") { | |
| return "2013" | |
| } | |
| elseif ($versionString -match "^14\.0\.(\d+)(?:\.\d+)?$") { | |
| return "2010" | |
| } | |
| elseif ($versionString -match "^12\.0\.(\d+)(?:\.\d+)?$") { | |
| return "2007" | |
| } | |
| else { return $null } | |
| } | |
| function Write-VulnerabilityWarning { | |
| param ( | |
| [string]$product, | |
| [version]$currentVersion, | |
| [version]$patchedVersion, | |
| [bool]$fullMatchVersion | |
| ) | |
| $severity = $fullMatchVersion ? "CONFIRMED VULNERABLE" : "potentially VULNERABLE" | |
| $color = $fullMatchVersion ? 'DarkRed' : 'Red' | |
| Write-Host "`nWARNING: SharePoint $product is $severity to CVE-2025-53770!" -ForegroundColor $color | |
| Write-Host "Detected version: $currentVersion, Required minimum version: $patchedVersion" -ForegroundColor $color | |
| } | |
| function CheckVulnerability { | |
| param ([string]$versionString, [bool]$fullMatchVersion) | |
| $product = GetProductNameFromVersion $versionString | |
| $secureVersions = @{ | |
| "2016" = [version]"16.0.5513.1001" | |
| "2019" = [version]"16.0.10417.20037" | |
| "Subscription" = [version]"16.0.18526.20508" | |
| } | |
| if ($product -eq "2013" -or $product -eq "2010" -or $product -eq "2007") { | |
| Write-Host "`nWARNING: SharePoint $product detected (version: $versionString). You really got balls still exposing this to the internet!" -ForegroundColor DarkRed | |
| return | |
| } | |
| if ($product -and $secureVersions.ContainsKey($product)) { | |
| $currentVersion = try { [version]$versionString } catch { $null } | |
| $patchedVersion = $secureVersions[$product] | |
| if ($currentVersion -and $currentVersion -ge $patchedVersion) { | |
| Write-Host "`nSharePoint $product appears to be patched against CVE-2025-53770." -ForegroundColor Green | |
| } | |
| elseif ($versionString -match "\.0\.\d{4}$") { | |
| Write-Host "`nSharePoint $product has hotfix applied ($versionString), treated as patched." -ForegroundColor Green | |
| } | |
| else { | |
| Write-VulnerabilityWarning -product $product -currentVersion $currentVersion -patchedVersion $patchedVersion -fullMatchVersion:$fullMatchVersion | |
| } | |
| } else { | |
| Write-Host "`nUnable to determine product or patch baseline for version: $versionString" -ForegroundColor Yellow | |
| } | |
| } | |
| function CheckForMatch { | |
| param ( | |
| [string]$versionToCheck, | |
| $blocks, | |
| [ref]$foundMatch, | |
| [ref]$fullMatch, | |
| [ref]$vulnChecked = $null | |
| ) | |
| $matchedBlocks = @() | |
| foreach ($block in $blocks) { | |
| $blockContent = $block.Groups[1].Value | |
| if ($blockContent -match [regex]::Escape($versionToCheck)) { | |
| $matchedBlocks += $blockContent | |
| } | |
| } | |
| if ($matchedBlocks.Count -eq 1) { | |
| $blockContent = $matchedBlocks[0] | |
| $cells = [regex]::Matches($blockContent, "<td.*?>(.*?)</td>") | |
| if ($cells -and $cells.Count -ge 4) { | |
| $product = Get-FirstCellValue $cells[0].Groups[1].Value | |
| $kb = Get-FirstKbNumber $cells[1].Groups[1].Value | |
| $fullVersion = Get-FirstCellValue $cells[2].Groups[1].Value | |
| $releaseDate = Get-FirstCellValue $cells[3].Groups[1].Value | |
| Write-Host "`nMatch Found:" | |
| Write-Host "Product: $product" | |
| Write-Host "KB Number: $kb" | |
| Write-Host "Full Version: $fullVersion" | |
| Write-Host "Release Date: $releaseDate" | |
| $foundMatch.Value = $true | |
| $fullMatch.Value = $true | |
| $script:latestDetectedFullVersion = $fullVersion | |
| } else { | |
| Write-Host "Warning: Could not extract expected update information from table row for version $versionToCheck." -ForegroundColor Yellow | |
| } | |
| } elseif ($matchedBlocks.Count -gt 1) { | |
| $releaseDates = @() | |
| foreach ($blockContent in $matchedBlocks) { | |
| $cells = [regex]::Matches($blockContent, "<td.*?>(.*?)</td>") | |
| if ($cells.Count -gt 3) { | |
| $releaseDate = Get-FirstCellValue $cells[3].Groups[1].Value | |
| $releaseDates += $releaseDate | |
| } | |
| } | |
| $parsedDates = $releaseDates | Where-Object { $_ -match "\w+ \d{4}" } | |
| $parsedDates = $parsedDates | Sort-Object { try { [datetime]::ParseExact($_, 'MMMM yyyy', $null) } catch { $_ } } | |
| if ($parsedDates.Count -gt 1) { | |
| $start = $parsedDates[0] | |
| $end = $parsedDates[-1] | |
| $productInUse = GetProductNameFromVersion $versionToCheck | |
| if ($productInUse) { | |
| Write-Host "`nProduct detected: SharePoint $productInUse (version: $versionToCheck)" | |
| } else { | |
| Write-Host "`nProduct detected: Unknown SharePoint variant (version: $versionToCheck)" | |
| } | |
| Write-Host "`nNote: Version $versionToCheck appears in multiple updates from $start to $end." -ForegroundColor Yellow | |
| Write-Host "Exact patch level cannot be determined. Please verify manually:" | |
| Write-Host "https://learn.microsoft.com/en-us/officeupdates/sharepoint-updates" | |
| $foundMatch.Value = $true | |
| if ($vulnChecked -ne $null) { $vulnChecked.Value = $true } | |
| CheckVulnerability -versionString $versionToCheck -fullMatchVersion:$false | |
| return | |
| } else { | |
| Write-Host "Multiple table rows found for version $versionToCheck, but could not parse release dates." -ForegroundColor Yellow | |
| } | |
| } else { | |
| Write-Host "No 1:1 match against Microsoft data for version $versionToCheck." -ForegroundColor Yellow | |
| } | |
| } | |
| try { | |
| $webContent = Invoke-WebRequest -Uri $msUpdatePage -UseBasicParsing | |
| $contentText = $webContent.Content | |
| $trBlocks = [regex]::Matches($contentText, "<tr>(.*?)</tr>", [System.Text.RegularExpressions.RegexOptions]::Singleline) | |
| try { | |
| $startPageParams = @{ | |
| Uri = $startPageUrl | |
| UseBasicParsing = $true | |
| Headers = @{ "User-Agent" = "Mozilla/5.0" } | |
| ErrorAction = 'Stop' | |
| } | |
| if ($SkipCertificateCheck) { $startPageParams.SkipCertificateCheck = $true } | |
| if ($SkipHttpErrorCheck) { $startPageParams.SkipHttpErrorCheck = $true } | |
| $startPageResponse = Invoke-WebRequest @startPageParams | |
| if ($startPageResponse.Content -match 'siteClientTag\s*:\s*"[^$]*\$\$(\d+\.\d+\.\d+\.\d+)"') { | |
| $siteClientTagVersion = $matches[1] | |
| Write-Host "siteClientTag version: $siteClientTagVersion" | |
| $foundMatchSiteClientTag = $false | |
| $fullMatchSiteClientTag = $false | |
| $script:latestDetectedFullVersion = $null | |
| $vulnChecked = $false | |
| CheckForMatch -versionToCheck $siteClientTagVersion -blocks $trBlocks -foundMatch ([ref]$foundMatchSiteClientTag) -fullMatch ([ref]$fullMatchSiteClientTag) -vulnChecked ([ref]$vulnChecked) | |
| if (-not $vulnChecked) { | |
| if ($script:latestDetectedFullVersion) { | |
| CheckVulnerability -versionString $script:latestDetectedFullVersion -fullMatchVersion:$fullMatchSiteClientTag | |
| } else { | |
| CheckVulnerability -versionString $siteClientTagVersion -fullMatchVersion:$fullMatchSiteClientTag | |
| } | |
| } | |
| return | |
| } else { | |
| Write-Host "Could not extract siteClientTag version from start.aspx." -ForegroundColor Yellow | |
| } | |
| } catch { | |
| Write-Host "Error querying siteClientTag from start.aspx: $_" -ForegroundColor Yellow | |
| } | |
| $requestParams = @{ | |
| Uri = $ServerUrl | |
| Method = 'Get' | |
| Headers = @{ "User-Agent" = "Mozilla/5.0" } | |
| UseBasicParsing = $true | |
| } | |
| if ($SkipCertificateCheck) { $requestParams.SkipCertificateCheck = $true } | |
| if ($SkipHttpErrorCheck) { $requestParams.SkipHttpErrorCheck = $true } | |
| $response = Invoke-WebRequest @requestParams | |
| $headerVersion = $response.Headers["MicrosoftSharePointTeamServices"] | |
| if ($headerVersion) { | |
| $headerVersionTrimmed = $headerVersion -replace "^(\d+\.\d+)\.0\.(\d+)$", '$1.$2' | |
| Write-Host "Header version: $headerVersion" | |
| } | |
| $serviceCnfParams = @{ | |
| Uri = $serviceCnfUrl | |
| UseBasicParsing = $true | |
| ErrorAction = 'Stop' | |
| } | |
| if ($SkipCertificateCheck) { $serviceCnfParams.SkipCertificateCheck = $true } | |
| if ($SkipHttpErrorCheck) { $serviceCnfParams.SkipHttpErrorCheck = $true } | |
| try { | |
| $cnfResponse = Invoke-WebRequest @serviceCnfParams | |
| if ($cnfResponse -match "vti_extenderversion:SR\|(\d+\.\d+\.\d+\.\d+)") { | |
| $serviceCnfVersion = $matches[1] | |
| $serviceCnfVersionTrimmed = $serviceCnfVersion -replace "^(\d+\.\d+)\.0\.(\d+)$", '$1.$2' | |
| Write-Host "Service.cnf version: $serviceCnfVersion" | |
| } | |
| } catch { | |
| Write-Host "Could not retrieve or parse service.cnf: $_" -ForegroundColor Yellow | |
| } | |
| if ($headerVersionTrimmed) { | |
| Write-Host "`nChecking header version against Microsoft sources..." | |
| $script:latestDetectedFullVersion = $null | |
| $vulnChecked = $false | |
| CheckForMatch -versionToCheck $headerVersionTrimmed -blocks $trBlocks -foundMatch ([ref]$foundMatchHeader) -fullMatch ([ref]$headerFullMatch) -vulnChecked ([ref]$vulnChecked) | |
| if (-not $vulnChecked) { | |
| if ($script:latestDetectedFullVersion) { | |
| CheckVulnerability -versionString $script:latestDetectedFullVersion -fullMatchVersion:$headerFullMatch | |
| } else { | |
| CheckVulnerability -versionString $headerVersionTrimmed -fullMatchVersion:$headerFullMatch | |
| } | |
| } | |
| } | |
| if ($serviceCnfVersionTrimmed -and $serviceCnfVersionTrimmed -ne $headerVersionTrimmed) { | |
| Write-Host "`nChecking service.cnf version against Microsoft sources..." | |
| $script:latestDetectedFullVersion = $null | |
| $vulnChecked = $false | |
| CheckForMatch -versionToCheck $serviceCnfVersionTrimmed -blocks $trBlocks -foundMatch ([ref]$foundMatchServiceCnf) -fullMatch ([ref]$serviceCnfFullMatch) -vulnChecked ([ref]$vulnChecked) | |
| if (-not $vulnChecked) { | |
| if ($script:latestDetectedFullVersion) { | |
| CheckVulnerability -versionString $script:latestDetectedFullVersion -fullMatchVersion:$serviceCnfFullMatch | |
| } else { | |
| CheckVulnerability -versionString $serviceCnfVersionTrimmed -fullMatchVersion:$serviceCnfFullMatch | |
| } | |
| } | |
| } | |
| if (-not $foundMatchHeader -and -not $foundMatchServiceCnf) { | |
| Write-Host "`nNo matching update information found for either version." -ForegroundColor Yellow | |
| } | |
| } catch { | |
| Write-Host "An error occurred: $_" -ForegroundColor Red | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment