KIPHP2Shell unter Windows: Prüfskripte für Einzelplatz, Test und Monitoring
Kostenlose PowerShell-Skripte gegen PHP2Shell: PHP auf Windows finden, bewerten und absichern – Einzelplatzprüfung, Testumgebung und Monitoring.
- Security
- Server
- Monitoring
PHP2Shell betrifft auch Windows-Server: PHP läuft dort im IIS, in Ticket- oder Zeiterfassungssystemen, in Fachanwendungen oder als vergessene Testinstallation – und taucht in keinem Patch-Management auf. Dafür veröffentlichen wir drei PowerShell-Skripte, die wir selbst bei Kunden einsetzen, zum Kopieren und Anpassen.
Kernaussage
Die Skripte finden jede PHP-Installation, bewerten die Version, zeigen die Nutzung und suchen nach Spuren der Angriffswelle. Die Prüfung ist rein lesend; Update und php.ini-Absicherung bietet nur das Einzelplatz-Skript an, ausgeführt erst nach Bestätigung und mit Sicherung.
Laufende Aktualisierung
Stand der Skripte: 25.10.2026Neue Angriffsmerkmale und Versionen pflegen wir zeitnah ein und veröffentlichen hier angepasste Fassungen. Aufruf, Parameter und Ausgaben bleiben dabei gleich, eingerichtete Abläufe funktionieren weiter. Die Fassung steht im Kopf jedes Skripts und über dem Codeblock; beim Monitoring das Prüfskript nach einem Update einfach neu erzeugen.
Was es gibt
Einzelplatzprüfung
Test-PHP2Shell.ps1 prüft einen Rechner interaktiv, erstellt einen Bericht und bietet Update und php.ini-Absicherung an.
Testumgebung
Setup-PHP2ShellTestumgebung.ps1 baut auf einem Testrechner veraltetes PHP und harmlose Testspuren auf – ohne IIS.
Monitoring-Generator
New-PHP2ShellMonitorScript.ps1 erzeugt ein Prüfskript zur Verteilung, das sich in jedes Monitoring einbinden lässt.
Was die Skripte prüfen
| Prüfung | Wie |
|---|---|
| PHP-Installationen | Suche nach php.exe und php-cgi.exe auf allen lokalen Festplatten; ein Ordner mit beiden Programmen zählt als eine Installation |
| Version | Aus den Dateieigenschaften – PHP wird dafür nicht ausgeführt |
| Bewertung | Gegen 8.2.35, 8.3.36, 8.4.27 und 8.5.12; Versionen bis 8.1 erhalten keine offizielle Korrektur |
| Nutzung | IIS-FastCGI und IIS-Handler, laufende PHP-Prozesse, Apache-Dienst mit PHP-Modul |
| php.ini | Ob session.upload_progress.enabled = 0 gesetzt ist (CVE-2026-115560) |
| Protokolle | IIS-Protokolle der letzten 30 Tage auf bekannte Merkmale der Angriffswelle |
| Session-Dateien | Session-Ordner jeder Installation auf die Webshell aus einem dokumentierten Angriffsversuch |
| Übergangsschutz | Erkennt die Community-Erweiterung php_uploadprogress_guard (ändert die Bewertung nicht) |
Wie weit zurück prüfen?
Standardmäßig werden die IIS-Protokolle der letzten 30 Tage durchsucht. Sophos empfiehlt, exponierte Systeme ab dem 1. August zu prüfen – für die Erstprüfung deshalb .\Test-PHP2Shell.ps1 -LogDays 90 aufrufen, beim Monitoring den Wert LogTage im Konfigurationsblock anpassen. Für die laufende Überwachung reichen danach 30 Tage.
Einzelprüfung: Test-PHP2Shell.ps1
- Ablegen: Skript in einen eigenen Ordner legen, empfohlen C:\PHP2ShellCheck – dort landen Protokolle, Berichte und Sicherungen.
- Als Administrator starten: Auf Wunsch verschickt das Skript den Bericht per Mail, mit Exchange über das Pickup-Verzeichnis, sonst per SMTP.
- Angebote bestätigen: Für verwundbare Versionen bietet es das passende offizielle Windows-Paket an, danach für jeden Ordner die php.ini-Absicherung.
- Bericht lesen: Am Ende öffnet sich der HTML-Bericht mit Ampel und Empfehlung je Installation.
Test-PHP2Shell.ps1
Einzelplatzprüfung mit Bericht, Update-Angebot und php.ini-Absicherung. Version 1.11, Stand 25.10.2026.
#Requires -Version 5.1
<#
.SYNOPSIS
Prueft einen Windows-Rechner auf PHP2Shell (CVE-2026-109618, CVE-2026-109619)
und CVE-2026-115560, erstellt Protokoll und Bericht, bietet - interaktiv und nur
nach Bestaetigung - das offizielle Windows-Update an und verschickt den Bericht
auf Wunsch selbst per Mail.
.DESCRIPTION
EINRICHTUNG
1. Ordner C:\PHP2ShellCheck anlegen
2. Dieses Skript in den Ordner legen
(Das Skript arbeitet immer im eigenen Ordner; ein anderer Ort ist moeglich,
empfohlen und in dieser Anleitung verwendet wird C:\PHP2ShellCheck.)
3. Als Administrator starten:
powershell.exe -ExecutionPolicy Bypass -File .\Test-PHP2Shell.ps1
ABLAUF (Einzelplatzpruefung)
1. Abfrage: Bericht nur im Skriptordner ablegen oder zusaetzlich per Mail senden.
Bei Mail: vorhandene Einstellungen bestaetigen oder neu eingeben
(Exchange-Pickup, falls Exchange installiert, sonst SMTP mit Anmeldedaten).
2. Pruefung, bei verwundbarem PHP Angebot fuer Update und php.ini-Absicherung.
3. Bericht wird immer im Skriptordner abgelegt, bei Wunsch per Mail versendet
und anschliessend im Browser geoeffnet.
ABLAGE (alles im Ordner des Skripts)
PHP2Shell_<Rechner>_<Zeit>.log Protokoll jedes Laufs
PHP2Shell_Bericht_<Rechner>_<Zeit>.html Bericht (auch als Mailtext)
PHP2Shell_Bericht_<Rechner>_<Zeit>.txt Bericht als Text
PHP2Shell_<Rechner>_<Zeit>.json Ergebnis maschinenlesbar
PHP2Shell_Verlauf.csv Verlauf aller Laeufe
config.json Mail-Einstellungen
smtp-credential.xml SMTP-Kennwort, verschluesselt (nur falls noetig)
Sicherungen\ Sicherungen von PHP-Ordnern und php.ini
PRUEFUNG (immer, rein lesend)
1. Suche nach php.exe / php-cgi.exe auf allen lokalen Festplatten
2. Version aus den Dateieigenschaften (PHP wird nicht ausgefuehrt)
3. Bewertung gegen 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12, bis 8.1 = ohne offizielle Korrektur
4. Nutzung: IIS-FastCGI und -Handler je Site, laufende Prozesse, Apache mit PHP-Modul
5. php.ini: session.upload_progress.enabled (CVE-2026-115560)
6. Webserver-Protokolle: bekannte Angriffsmerkmale
6a. Session-Ordner jeder PHP-Installation: Webshell aus dem bekannten Angriffsversuch
7. Abgleich mit dem offiziellen Windows-Paketverzeichnis von PHP
AENDERUNGEN (nur interaktiv, nur nach ausdruecklicher Bestaetigung)
- Update auf das offizielle, korrigierte Paket desselben Zweigs
(Download, SHA256-Pruefung, Sicherung, IIS/Apache stoppen, ersetzen, starten, pruefen)
- session.upload_progress.enabled = 0 in der php.ini (mit Sicherung)
MAILVERSAND (das Skript versendet selbst)
Interaktiv:
- Das Skript prueft, ob Exchange auf dem Server installiert ist. Nur dann wird der
Versand ueber das Pickup-Verzeichnis angeboten (kein Relay, keine Zugangsdaten).
- Auf allen anderen Servern fragt es SMTP-Server, Port und Anmeldedaten ab.
Das Kennwort wird verschluesselt (DPAPI) fuer das angemeldete Konto gespeichert.
Monitoring / RMM (unbeaufsichtigt):
- SMTP-Daten im Block "MAIL-EINSTELLUNGEN FUER DAS MONITORING" weiter unten
eintragen und dort Aktiv = $true setzen.
- Empfohlen: interner Relay ohne Anmeldung oder ein eigenes Postfach, das nur senden darf.
- Ein Kennwort im Skript ist fuer jeden lesbar, der den Ordner lesen darf. Dann den
Ordner auf Administratoren und SYSTEM beschraenken, z. B.:
icacls C:\PHP2ShellCheck /inheritance:r /grant:r *S-1-5-32-544:(OI)(CI)F *S-1-5-18:(OI)(CI)F
(S-1-5-32-544 = Administratoren, S-1-5-18 = SYSTEM; funktioniert in jeder Sprache)
- Versand je nach "SendenAb" (Immer / Warnung / Kritisch / Nie), mit -SendMail immer.
Reihenfolge: config.json (interaktiv eingerichtet) vor dem Block im Skript.
.PARAMETER LogDays
Tage, die Webserver-Protokolle rueckwirkend durchsucht werden. Standard: 30
.PARAMETER AdditionalLogPath
Weitere Protokollverzeichnisse (z. B. Apache).
.PARAMETER SkipLogScan
Protokollsuche ueberspringen.
.PARAMETER SkipOnlineCheck
Kein Abgleich mit dem PHP-Paketverzeichnis (Offline-Betrieb).
.PARAMETER NoPrompt
Keine Rueckfragen (fuer Monitoring/RMM). Automatisch bei nicht interaktiver Sitzung.
.PARAMETER Quiet
Nur die Zusammenfassungszeile auf der Konsole (impliziert -NoPrompt).
.PARAMETER SetupMail
Nur die Mail-Einstellungen einrichten und eine Testmail senden.
.PARAMETER SendMail
Bericht in jedem Fall per Mail senden (unabhaengig von "SendenAb").
.OUTPUTS
Exit-Codes: 0 = OK, 1 = WARNUNG, 2 = KRITISCH, 3 = FEHLER
.NOTES
Codekunst Systems GmbH - Version 1.11 - 25.10.2026
HAFTUNGSAUSSCHLUSS
Dieses Skript wird ohne Gewaehr bereitgestellt. Die Pruefung ist rein lesend; Aenderungen
(Update, php.ini) erfolgen nur nach ausdruecklicher Bestaetigung und mit vorheriger Sicherung.
Vor dem Einsatz auf produktiven Systemen in einer Testumgebung pruefen. Eingesetzt auf
eigene Verantwortung des Betreibers. Ersetzt keine forensische Untersuchung.
Windows PowerShell 5.1, Administratorrechte empfohlen (fuer Patch erforderlich).
Hintergrund: codekunst-systems.com/aktuell/php2shell-statusuebersicht
#>
[CmdletBinding()]
param(
[int] $LogDays = 30,
[string[]]$AdditionalLogPath = @(),
[switch] $SkipLogScan,
[switch] $SkipOnlineCheck,
[switch] $NoPrompt,
[switch] $Quiet,
[switch] $SetupMail,
[switch] $SendMail
)
$ErrorActionPreference = 'Stop'
# Umlauts for user-visible texts, built from character codes so this file stays pure ASCII
# (distinct names: PowerShell variable names are case-insensitive)
$c_ae = [char]0xE4; $c_oe = [char]0xF6; $c_ue = [char]0xFC; $c_sz = [char]0xDF
$c_AeGross = [char]0xC4; $c_OeGross = [char]0xD6; $c_UeGross = [char]0xDC
$ScriptVersion = '1.11'
if ($Quiet) { $NoPrompt = $true }
if (-not [Environment]::UserInteractive) { $NoPrompt = $true }
# =============================================================================
# MAIL-EINSTELLUNGEN FUER DAS MONITORING (pro Kunde anpassen)
# Wird nur verwendet, wenn Aktiv = $true und keine config.json vorhanden ist.
# Benutzer leer lassen = Versand ohne Anmeldung (interner Relay / Connector).
# ACHTUNG: Ein hier eingetragenes Kennwort ist im Klartext lesbar. Nur ein eigenes
# Postfach verwenden, das ausschliesslich senden darf, und Ordnerrechte einschraenken.
# =============================================================================
$ScriptMail = @{
Aktiv = $false
Empfaenger = @('it@kunde.de') # mehrere: @('a@x.de','b@x.de')
Absender = 'php2shell@kunde.de'
SmtpServer = 'smtp.office365.com' # oder interner Relay
SmtpPort = 587
Verschluesselung = $true # STARTTLS/TLS
Benutzer = '' # leer = ohne Anmeldung
Kennwort = '' # siehe ACHTUNG oben
SendenAb = 'Warnung' # Immer | Warnung | Kritisch | Nie
}
# =============================================================================
# --- Reference data ---------------------------------------------------------
$FixedVersions = @{ '8.2' = 35; '8.3' = 36; '8.4' = 27; '8.5' = 12 }
$Indicators = @(
'php2shell-', 'B7kQpoc618', 'B7kQpoc619', 'p2s9rdme2st6guc9g0rafic', 'php://filter',
'xqsuvwz3n97wu8j3emz1axuo5y',
'85.11.167.243', '217.60.242.76', '45.198.224.169', '221.14.159.134', '91.90.123.25',
'87.121.84.174', '181.215.186.252', '185.196.220.132', '185.161.210.29', '152.233.51.69',
'45.134.225.16', '107.189.14.87', '193.47.62.24', '103.147.220.17',
'2409:40d2:204e:4be:2a1f:f9c9:a055:e00b', '206.188.197.105',
'92.118.112.100', '134.199.154.64', 'godofthunder.st'
)
# Unclear sources, rated as warning (investigate, do not block):
# 109.71.252.97 - user agent claims a self-assessment (not verifiable)
# Amazon list - anonymization services, also used legitimately
$InfoIndicators = @('109.71.252.97',
'103.216.220.92', '31.223.184.166', '37.19.200.5', '134.209.159.64', '103.114.163.188', '95.217.98.37')
# Content of the web shell seen in a documented attempt (session file)
$SessionShellPatterns = @('PWNED::', 'system($_GET')
$ExcludedDirNames = @('$Recycle.Bin', 'System Volume Information', 'WinSxS', 'Installer')
$ReleasesUrl = 'https://downloads.php.net/~windows/releases/releases.json'
$DownloadBase = 'https://downloads.php.net/~windows/releases/'
# --- Folders: everything lives next to the script ---------------------------
$BaseDirectory = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
$BackupDirectory = Join-Path $BaseDirectory 'Sicherungen'
$ConfigPath = Join-Path $BaseDirectory 'config.json'
$CredentialPath = Join-Path $BaseDirectory 'smtp-credential.xml'
$Start = Get-Date
$Stamp = $Start.ToString('yyyyMMdd-HHmmss')
$Computer = $env:COMPUTERNAME
$LogPath = Join-Path $BaseDirectory "PHP2Shell_$Computer`_$Stamp.log"
function Write-Log {
param([string]$Text, [ValidateSet('INFO','WARN','ERROR','ACTION')][string]$Level = 'INFO')
$line = '{0} [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Text
for ($try = 0; $try -lt 3; $try++) {
try { Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8 -ErrorAction Stop; break }
catch { Start-Sleep -Milliseconds 200 }
}
if (-not $Quiet) {
switch ($Level) {
'WARN' { Write-Host $Text -ForegroundColor Yellow }
'ERROR' { Write-Host $Text -ForegroundColor Red }
'ACTION' { Write-Host $Text -ForegroundColor Cyan }
default { Write-Host $Text }
}
}
}
function Test-IsAdmin {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
return (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Read-YesNo {
param([string]$Question)
return ((Read-Host "$Question [j/N]") -match '^(j|ja|y|yes)$')
}
function ConvertTo-HtmlSafe {
param([string]$Text)
if ($null -eq $Text) { return '' }
return [System.Net.WebUtility]::HtmlEncode($Text)
}
function Get-UniqueBackupPath {
# Never overwrite an existing backup: append _2, _3, ... if needed
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path)) { return $Path }
$n = 2
while (Test-Path -LiteralPath "$Path`_$n") { $n++ }
return "$Path`_$n"
}
function New-BackupDirectory {
if (-not (Test-Path -LiteralPath $BackupDirectory)) {
New-Item -ItemType Directory -Path $BackupDirectory -Force | Out-Null
}
}
# --- Discovery --------------------------------------------------------------
function Find-PhpBinaries {
$found = New-Object System.Collections.Generic.List[string]
$roots = Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' | ForEach-Object { $_.DeviceID + '\' }
foreach ($root in $roots) {
$stack = New-Object System.Collections.Generic.Stack[string]
$stack.Push($root)
while ($stack.Count -gt 0) {
$dir = $stack.Pop()
if ($dir -ieq $BackupDirectory) { continue } # own backups are not installations
try {
foreach ($file in [System.IO.Directory]::GetFiles($dir)) {
$name = [System.IO.Path]::GetFileName($file)
if ($name -ieq 'php.exe' -or $name -ieq 'php-cgi.exe') { $found.Add($file) }
}
foreach ($sub in [System.IO.Directory]::GetDirectories($dir)) {
if ($ExcludedDirNames -contains [System.IO.Path]::GetFileName($sub)) { continue }
if ([System.IO.File]::GetAttributes($sub) -band [System.IO.FileAttributes]::ReparsePoint) { continue }
$stack.Push($sub)
}
} catch { }
}
}
return $found
}
function Get-PhpVersion {
param([string]$Path)
try {
$vi = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($Path)
foreach ($candidate in @($vi.ProductVersion, $vi.FileVersion)) {
if ($candidate -and $candidate -match '^(\d+)\.(\d+)\.(\d+)') {
return [pscustomobject]@{
Text = '{0}.{1}.{2}' -f $Matches[1], $Matches[2], $Matches[3]
Major = [int]$Matches[1]; Minor = [int]$Matches[2]; Patch = [int]$Matches[3]
}
}
}
} catch { }
return $null
}
function Get-VersionStatus {
param($Version)
if ($null -eq $Version) { return 'UNBEKANNT' }
$branch = '{0}.{1}' -f $Version.Major, $Version.Minor
if ($Version.Major -lt 8 -or ($Version.Major -eq 8 -and $Version.Minor -lt 2)) { return 'EOL' }
if ($FixedVersions.ContainsKey($branch)) {
if ($Version.Patch -ge $FixedVersions[$branch]) { return 'GEPATCHT' } else { return 'VERWUNDBAR' }
}
return 'GEPATCHT'
}
function Get-PeArchitecture {
param([string]$Path)
try {
$fs = [System.IO.File]::OpenRead($Path)
try {
$br = New-Object System.IO.BinaryReader($fs)
$fs.Position = 0x3C
$peOffset = $br.ReadInt32()
$fs.Position = $peOffset + 4
switch ($br.ReadUInt16()) { 0x8664 { return 'x64' } 0x14c { return 'x86' } default { return 'unbekannt' } }
} finally { $fs.Dispose() }
} catch { return 'unbekannt' }
}
function Get-ThreadSafety {
param([string]$PhpDir)
if (Get-ChildItem -LiteralPath $PhpDir -Filter 'php*ts.dll' -ErrorAction SilentlyContinue) { return 'ts' }
if (Get-ChildItem -LiteralPath $PhpDir -Filter 'php?.dll' -ErrorAction SilentlyContinue) { return 'nts' }
return 'unbekannt'
}
function Get-UploadProgressSetting {
param([string]$PhpDir)
$ini = Join-Path $PhpDir 'php.ini'
if (-not (Test-Path -LiteralPath $ini)) { return 'an (Standard, keine php.ini)' }
$value = $null
foreach ($line in [System.IO.File]::ReadAllLines($ini)) {
if ($line -match '^\s*session\.upload_progress\.enabled\s*=\s*"?([^";\s]+)') { $value = $Matches[1] }
}
if ($null -eq $value) { return 'an (Standard, nicht gesetzt)' }
if ($value -match '^(0|off|false|no)$') { return 'aus' }
return 'an'
}
function Get-IisContext {
$ctx = [pscustomobject]@{ FastCgiPaths = @(); Handlers = @(); LogDirectories = @() }
$cfgPath = Join-Path $env:windir 'System32\inetsrv\config\applicationHost.config'
if (-not (Test-Path -LiteralPath $cfgPath)) { return $ctx }
try {
[xml]$cfg = [System.IO.File]::ReadAllText($cfgPath)
$apps = $cfg.configuration.'system.webServer'.fastCgi.application
if ($apps) { $ctx.FastCgiPaths = @($apps | ForEach-Object { [Environment]::ExpandEnvironmentVariables($_.fullPath) }) }
$handlerList = New-Object System.Collections.Generic.List[object]
foreach ($h in @($cfg.configuration.'system.webServer'.handlers.add)) {
if ($h -and $h.scriptProcessor) {
$handlerList.Add([pscustomobject]@{ Ebene = 'Server'; Name = $h.name
Prozessor = [Environment]::ExpandEnvironmentVariables(($h.scriptProcessor -split '\|')[0]) })
}
}
foreach ($loc in @($cfg.configuration.location)) {
if (-not $loc) { continue }
foreach ($h in @($loc.'system.webServer'.handlers.add)) {
if ($h -and $h.scriptProcessor) {
$handlerList.Add([pscustomobject]@{ Ebene = "Site: $($loc.path)"; Name = $h.name
Prozessor = [Environment]::ExpandEnvironmentVariables(($h.scriptProcessor -split '\|')[0]) })
}
}
}
$ctx.Handlers = $handlerList
$logDir = $cfg.configuration.'system.applicationHost'.sites.siteDefaults.logFile.directory
if (-not $logDir) { $logDir = '%SystemDrive%\inetpub\logs\LogFiles' }
$ctx.LogDirectories = @([Environment]::ExpandEnvironmentVariables($logDir))
} catch { Write-Log "IIS-Konfiguration nicht lesbar: $($_.Exception.Message)" 'WARN' }
return $ctx
}
function Get-ApacheServices {
@(Get-CimInstance -ClassName Win32_Service -ErrorAction SilentlyContinue | Where-Object { $_.PathName -match 'httpd\.exe' })
}
function Search-Indicators {
param([string[]]$Directories, [int]$Days)
$hits = New-Object System.Collections.Generic.List[object]
$since = (Get-Date).AddDays(-$Days)
foreach ($dir in $Directories | Where-Object { $_ -and (Test-Path -LiteralPath $_) }) {
$files = Get-ChildItem -LiteralPath $dir -Recurse -File -Include *.log -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -ge $since }
foreach ($f in $files) {
$m = Select-String -LiteralPath $f.FullName -Pattern ($Indicators + $InfoIndicators) -SimpleMatch -ErrorAction SilentlyContinue | Select-Object -First 20
foreach ($x in $m) {
$line = $x.Line
$kind = if (@($InfoIndicators | Where-Object { $line.Contains($_) }).Count -gt 0) { 'Unklar' } else { 'Angriffsmerkmal' }
$hits.Add([pscustomobject]@{ Datei = $f.FullName; Zeile = $x.LineNumber; Merkmal = $x.Pattern; Einstufung = $kind
Auszug = $x.Line.Substring(0, [Math]::Min(300, $x.Line.Length)) })
}
}
}
return $hits
}
function Get-SessionDirectories {
# session.save_path from php.ini of each installation, plus the Windows default
param($Installs)
$dirs = New-Object System.Collections.Generic.List[string]
foreach ($i in $Installs) {
$ini = Join-Path $i.Ordner 'php.ini'
if (Test-Path -LiteralPath $ini) {
foreach ($line in [System.IO.File]::ReadAllLines($ini)) {
if ($line -match '^\s*session\.save_path\s*=\s*"?([^"]+?)"?\s*$') {
$v = $Matches[1]
if ($v -match ';') { $v = ($v -split ';')[-1] } # "N;/path" or "N;MODE;/path"
if ($v) { $dirs.Add([Environment]::ExpandEnvironmentVariables($v.Trim())) }
}
}
}
}
$dirs.Add((Join-Path $env:windir 'Temp'))
return @($dirs | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -Unique)
}
function Search-SessionWebshells {
param([string[]]$Directories)
$hits = New-Object System.Collections.Generic.List[object]
foreach ($dir in $Directories) {
$files = Get-ChildItem -LiteralPath $dir -Recurse -File -Filter 'sess_*' -ErrorAction SilentlyContinue
foreach ($f in $files) {
$known = $f.Name -eq 'sess_xqsuvwz3n97wu8j3emz1axuo5y'
$m = Select-String -LiteralPath $f.FullName -Pattern $SessionShellPatterns -SimpleMatch -ErrorAction SilentlyContinue | Select-Object -First 1
if ($known -or $m) {
$hits.Add([pscustomobject]@{ Datei = $f.FullName; Geaendert = $f.LastWriteTime.ToString('yyyy-MM-dd HH:mm')
Merkmal = if ($m) { $m.Pattern } else { 'bekannter Dateiname' } })
}
}
}
return $hits
}
# --- Official Windows packages ------------------------------------------------
function Get-WindowsReleases {
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
return Invoke-RestMethod -Uri $ReleasesUrl -UseBasicParsing -TimeoutSec 30
} catch {
Write-Log "Abgleich mit dem PHP-Paketverzeichnis nicht m${c_oe}glich: $($_.Exception.Message)" 'WARN'
return $null
}
}
function Find-OfficialPackage {
param($Releases, $Version, [string]$Ts, [string]$Arch)
if ($null -eq $Releases -or $null -eq $Version) { return $null }
$branch = '{0}.{1}' -f $Version.Major, $Version.Minor
$entry = $Releases.$branch
if (-not $entry) { return [pscustomobject]@{ Verfuegbar = $false; Grund = "Zweig $branch nicht im Paketverzeichnis" } }
if (-not ($entry.version -match '^(\d+)\.(\d+)\.(\d+)')) { return $null }
$latestPatch = [int]$Matches[3]
if (-not $FixedVersions.ContainsKey($branch) -or $latestPatch -lt $FixedVersions[$branch]) {
return [pscustomobject]@{ Verfuegbar = $false; Grund = "Neuestes Windows-Paket f${c_ue}r $branch ist $($entry.version) (ohne Korrektur)" }
}
if ($Version.Patch -ge $latestPatch) { return [pscustomobject]@{ Verfuegbar = $false; Grund = 'Bereits aktuell' } }
$pick = $entry.PSObject.Properties | Where-Object { $_.Name -match "^$Ts-v[sc](\d+)-$Arch$" } |
Sort-Object { [int]([regex]::Match($_.Name, 'v[sc](\d+)').Groups[1].Value) } -Descending |
Select-Object -First 1
if (-not $pick) { return [pscustomobject]@{ Verfuegbar = $false; Grund = "Kein Paket f${c_ue}r $Ts/$Arch gefunden" } }
return [pscustomobject]@{
Verfuegbar = $true; Version = $entry.version; Variante = $pick.Name
Url = $DownloadBase + $pick.Value.zip.path; Sha256 = $pick.Value.zip.sha256; Datei = $pick.Value.zip.path
}
}
# --- Changes (interactive only) ---------------------------------------------
function Set-UploadProgressOff {
param([string]$PhpDir)
$ini = Join-Path $PhpDir 'php.ini'
if (-not (Test-Path -LiteralPath $ini)) {
Write-Log "Keine php.ini in $PhpDir - bitte php.ini-production als php.ini anlegen und Einstellung setzen." 'WARN'
return $false
}
New-BackupDirectory
$backup = Get-UniqueBackupPath (Join-Path $BackupDirectory ('php.ini_{0}_{1}' -f (Split-Path -Leaf $PhpDir), $Stamp))
Copy-Item -LiteralPath $ini -Destination $backup
$lines = [System.IO.File]::ReadAllLines($ini)
$changed = $false
for ($n = 0; $n -lt $lines.Count; $n++) {
if ($lines[$n] -match '^\s*;?\s*session\.upload_progress\.enabled\s*=') {
$lines[$n] = 'session.upload_progress.enabled = 0'; $changed = $true
}
}
if (-not $changed) { $lines += 'session.upload_progress.enabled = 0' }
[System.IO.File]::WriteAllLines($ini, $lines)
Write-Log "php.ini angepasst: session.upload_progress.enabled = 0 ($ini, Sicherung: $backup)" 'ACTION'
return $true
}
function Stop-PhpConsumers {
param($Install)
$state = [pscustomobject]@{ IisStopped = $false; ApacheServices = @() }
if ($Install.Nutzung -match 'IIS') {
Write-Log 'Stoppe IIS (iisreset /stop) ...' 'ACTION'
& iisreset.exe /stop | Out-Null
$state.IisStopped = $true
}
foreach ($svc in Get-ApacheServices) {
if ($svc.State -eq 'Running') {
Write-Log "Stoppe Dienst $($svc.Name) ..." 'ACTION'
Stop-Service -Name $svc.Name -Force
$state.ApacheServices += $svc.Name
}
}
Start-Sleep -Seconds 3
$left = Get-CimInstance Win32_Process -Filter "Name='php-cgi.exe' OR Name='php.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.ExecutablePath -and ((Split-Path -Parent $_.ExecutablePath) -ieq $Install.Ordner) }
foreach ($p in $left) {
Write-Log "Beende verbleibenden Prozess $($p.ProcessId) ($($p.ExecutablePath))" 'ACTION'
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
}
return $state
}
function Start-PhpConsumers {
param($State)
foreach ($name in $State.ApacheServices) { Write-Log "Starte Dienst $name ..." 'ACTION'; Start-Service -Name $name }
if ($State.IisStopped) { Write-Log 'Starte IIS (iisreset /start) ...' 'ACTION'; & iisreset.exe /start | Out-Null }
}
function Invoke-PhpUpdate {
param($Install)
$pkg = $Install.Paket
$tmpZip = Join-Path $env:TEMP $pkg.Datei
$tmpDir = Join-Path $env:TEMP "php-update-$Stamp"
New-BackupDirectory
$backup = Get-UniqueBackupPath (Join-Path $BackupDirectory ('{0}_{1}' -f (Split-Path -Leaf $Install.Ordner), $Stamp))
$state = $null
try {
Write-Log "Lade $($pkg.Url) ..." 'ACTION'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri $pkg.Url -OutFile $tmpZip -UseBasicParsing -TimeoutSec 300
$hash = (Get-FileHash -LiteralPath $tmpZip -Algorithm SHA256).Hash
if (-not $pkg.Sha256 -or $hash -ine $pkg.Sha256) {
Write-Log "Pr${c_ue}fsumme stimmt nicht (erwartet $($pkg.Sha256), erhalten $hash) - Abbruch, nichts ver${c_ae}ndert." 'ERROR'
return $false
}
Write-Log "Pr${c_ue}fsumme SHA256 korrekt." 'ACTION'
Expand-Archive -LiteralPath $tmpZip -DestinationPath $tmpDir -Force
Write-Log "Sichere $($Install.Ordner) nach $backup ..." 'ACTION'
Copy-Item -LiteralPath $Install.Ordner -Destination $backup -Recurse
$state = Stop-PhpConsumers -Install $Install
Write-Log 'Ersetze Programmdateien (php.ini und eigene Erweiterungen bleiben erhalten) ...' 'ACTION'
Get-ChildItem -LiteralPath $tmpDir | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $Install.Ordner -Recurse -Force
}
$newVersion = Get-PhpVersion -Path $Install.Referenz
if ($newVersion -and (Get-VersionStatus $newVersion) -eq 'GEPATCHT') {
Write-Log "Update erfolgreich: $($Install.Version) -> $($newVersion.Text)" 'ACTION'
$Install.Version = $newVersion.Text; $Install.Status = 'GEPATCHT'
$Install.Dateidatum = (Get-Item -LiteralPath $Install.Referenz).LastWriteTime.ToString('yyyy-MM-dd')
$Install.Aktion = "Aktualisiert auf $($newVersion.Text), Sicherung: $backup"
return $true
}
Write-Log "Version nach dem Update nicht wie erwartet - bitte pr${c_ue}fen, Sicherung: $backup" 'ERROR'
return $false
} catch {
Write-Log "Update fehlgeschlagen: $($_.Exception.Message). Sicherung: $backup" 'ERROR'
return $false
} finally {
if ($state) { Start-PhpConsumers -State $state }
Remove-Item -LiteralPath $tmpZip -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
# --- Assessment -------------------------------------------------------------
function Get-Severity {
param($Install)
$sev = 0
if ($Install.Status -in 'VERWUNDBAR', 'EOL') { if ($Install.AktivGenutzt) { $sev = 2 } else { $sev = 1 } }
if ($Install.Status -eq 'UNBEKANNT') { $sev = [Math]::Max($sev, 1) }
if ($Install.UploadFortschritt -ne 'aus') { $sev = [Math]::Max($sev, 1) }
return $sev
}
function Test-GuardExtension {
# Detects the community mitigation php_uploadprogress_guard (Hosted Power)
param([string]$PhpDir)
$ini = Join-Path $PhpDir 'php.ini'
if (Test-Path -LiteralPath $ini) {
foreach ($line in [System.IO.File]::ReadAllLines($ini)) {
if ($line -match '^\s*(zend_)?extension\s*=\s*"?[^";]*uploadprogress_guard') { return $true }
}
}
$confD = Join-Path $PhpDir 'conf.d'
if (Test-Path -LiteralPath $confD) {
foreach ($f in Get-ChildItem -LiteralPath $confD -Filter '*.ini' -ErrorAction SilentlyContinue) {
if (Select-String -LiteralPath $f.FullName -Pattern '^\s*(zend_)?extension\s*=.*uploadprogress_guard' -Quiet) { return $true }
}
}
return $false
}
function Get-Advice {
param($Install)
$a = New-Object System.Collections.Generic.List[string]
switch ($Install.Status) {
'VERWUNDBAR' {
if ($Install.Plesk) { $a.Add("${c_UeGross}ber den Plesk-Updater aktualisieren.") }
elseif ($Install.Paket -and $Install.Paket.Verfuegbar) { $a.Add("Offizielles Paket $($Install.Paket.Version) ($($Install.Paket.Variante)) verf${c_ue}gbar.") }
elseif ($Install.Paket) { $a.Add("$($Install.Paket.Grund). Bis dahin vom Netz nehmen oder absichern.") }
else { $a.Add('Auf die korrigierte Version des Zweigs aktualisieren.') }
}
'EOL' {
if ($Install.Plesk) { $a.Add("Plesk-Altversion: Build-Datum ab 21.10.2026 pr${c_ue}fen, mittelfristig Version wechseln.") }
else { $a.Add('Keine offizielle Korrektur: auf PHP 8.3 oder 8.4 wechseln, bis dahin nicht aus dem Internet erreichbar betreiben.') }
}
'UNBEKANNT' { $a.Add("Version manuell pr${c_ue}fen (php -v im PHP-Ordner).") }
}
if ($Install.UploadFortschritt -ne 'aus') { $a.Add('session.upload_progress.enabled = 0 setzen (CVE-2026-115560).') }
if ($Install.Uebergangsschutz -and $Install.Status -ne 'GEPATCHT') { $a.Add("${c_UeGross}bergangsschutz php_uploadprogress_guard ist aktiv - bleibt eine Br${c_ue}cke, Update bzw. Versionswechsel weiterhin n${c_oe}tig.") }
if (-not $Install.AktivGenutzt -and $Install.Status -ne 'GEPATCHT') { $a.Add("Nicht aktiv genutzt gefunden: entfernen, falls nicht ben${c_oe}tigt.") }
return ($a -join ' ')
}
# --- Reports ----------------------------------------------------------------
function New-Reports {
param($Installs, $LogHits, [string]$Overall, [bool]$IsAdmin)
$base = Join-Path $BaseDirectory "PHP2Shell_Bericht_$Computer`_$Stamp"
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine("PHP2Shell-Pr${c_ue}fbericht - $Computer")
[void]$sb.AppendLine("Erstellt: $($Start.ToString('dd.MM.yyyy HH:mm')) - Skriptversion $ScriptVersion - Codekunst Systems")
[void]$sb.AppendLine(('=' * 72))
[void]$sb.AppendLine("Gesamtergebnis: $Overall")
if (-not $IsAdmin) { [void]$sb.AppendLine("Hinweis: ohne Administratorrechte ausgef${c_ue}hrt, Ergebnis evtl. unvollst${c_ae}ndig.") }
[void]$sb.AppendLine('')
if ($Installs.Count -eq 0) { [void]$sb.AppendLine('Keine PHP-Installation (php.exe / php-cgi.exe) gefunden.') }
foreach ($i in $Installs) {
[void]$sb.AppendLine("- $($i.Pfad) [$($i.Programme)]")
[void]$sb.AppendLine(" Version: $($i.Version) ($($i.Variante)) Status: $($i.Status) Dateidatum: $($i.Dateidatum)")
[void]$sb.AppendLine(" Genutzt: $(if ($i.AktivGenutzt) { $i.Nutzung } else { 'nicht festgestellt' })")
[void]$sb.AppendLine(" Upload-Fortschrittsanzeige: $($i.UploadFortschritt)")
if ($i.Uebergangsschutz) { [void]$sb.AppendLine(" ${c_UeGross}bergangsschutz: php_uploadprogress_guard geladen (Community-Erweiterung, ersetzt kein Update)") }
if ($i.Empfehlung) { [void]$sb.AppendLine(" Empfehlung: $($i.Empfehlung)") }
if ($i.Aktion) { [void]$sb.AppendLine(" Durchgef${c_ue}hrt: $($i.Aktion)") }
[void]$sb.AppendLine('')
}
if ($SkipLogScan) { [void]$sb.AppendLine("Protokollsuche: ${c_ue}bersprungen.") }
elseif ($LogHits.Count -eq 0) { [void]$sb.AppendLine("Protokollsuche ($LogDays Tage): keine bekannten Angriffsmerkmale.") }
else {
[void]$sb.AppendLine("Protokollsuche ($LogDays Tage): $($LogHits.Count) Treffer - Angriffsversuch, nicht zwingend erfolgreich.")
[void]$sb.AppendLine("Vor weiteren ${c_AeGross}nderungen Beweise sichern und den Vorfall pr${c_ue}fen lassen.")
foreach ($h in ($LogHits | Select-Object -First 25)) { [void]$sb.AppendLine(" $($h.Datei):$($h.Zeile) [$($h.Merkmal)]") }
}
if ($infoHits.Count -gt 0) {
[void]$sb.AppendLine("Au${c_sz}erdem $($infoHits.Count) Treffer von Adressen mit unklarer Einstufung (Anonymisierungsdienste laut Amazon bzw. nicht ${c_ue}berpr${c_ue}fbarer Pr${c_ue}fscanner) - untersuchen, nicht automatisch sperren.")
}
[void]$sb.AppendLine('')
if ($sessionHits.Count -eq 0) {
[void]$sb.AppendLine('Session-Ordner: keine Webshell-Merkmale gefunden.')
} else {
[void]$sb.AppendLine("Session-Ordner: $($sessionHits.Count) Datei(en) mit Webshell-Merkmalen - starker Hinweis auf eine Kompromittierung!")
[void]$sb.AppendLine("System nicht neu starten und nichts l${c_oe}schen: zuerst Beweise sichern (Speicherabbild, Dateien, Protokolle) und den Vorfall pr${c_ue}fen lassen.")
foreach ($h in $sessionHits) { [void]$sb.AppendLine(" $($h.Datei) (ge${c_ae}ndert $($h.Geaendert)) [$($h.Merkmal)]") }
}
[void]$sb.AppendLine('')
[void]$sb.AppendLine("Rein lesende Pr${c_ue}fung, sofern oben keine Aktion aufgef${c_ue}hrt ist. Ersetzt keine Pr${c_ue}fung")
[void]$sb.AppendLine('beim Hoster oder in Containern und keine forensische Untersuchung.')
$txtPath = "$base.txt"
$sb.ToString() | Set-Content -LiteralPath $txtPath -Encoding UTF8
$color = @{ 'OK' = '#2e7d32'; 'WARNUNG' = '#ef6c00'; 'KRITISCH' = '#c62828' }[$Overall]
$rows = foreach ($i in $Installs) {
$c = @('#2e7d32', '#ef6c00', '#c62828')[$i.Schweregrad]
"<tr><td>$(ConvertTo-HtmlSafe $i.Pfad)<br><small>$(ConvertTo-HtmlSafe $i.Programme)</small></td><td>$(ConvertTo-HtmlSafe $i.Version)<br><small>$(ConvertTo-HtmlSafe $i.Variante)</small></td>" +
"<td style='color:$c;font-weight:bold'>$(ConvertTo-HtmlSafe $i.Status)$(if ($i.Uebergangsschutz) { '<br><small>Übergangsschutz aktiv</small>' })</td>" +
"<td>$(if ($i.AktivGenutzt) { ConvertTo-HtmlSafe $i.Nutzung } else { 'nicht festgestellt' })</td>" +
"<td>$(ConvertTo-HtmlSafe $i.UploadFortschritt)</td>" +
"<td>$(ConvertTo-HtmlSafe $i.Empfehlung)$(if ($i.Aktion) { '<br><b>Durchgeführt:</b> ' + (ConvertTo-HtmlSafe $i.Aktion) })</td></tr>"
}
$hitRows = foreach ($h in ($LogHits | Select-Object -First 50)) {
"<tr><td>$(ConvertTo-HtmlSafe $h.Datei)</td><td>$($h.Zeile)</td><td>$(ConvertTo-HtmlSafe $h.Merkmal)</td><td><code>$(ConvertTo-HtmlSafe $h.Auszug)</code></td></tr>"
}
$logText = if ($SkipLogScan) { 'Protokollsuche übersprungen.' }
elseif ($LogHits.Count -eq 0) { "Keine bekannten Angriffsmerkmale in den Protokollen der letzten $LogDays Tage." }
else { "<b>$($LogHits.Count) Treffer</b> auf bekannte Angriffsmerkmale – ein Angriffsversuch, nicht zwingend ein erfolgreicher Angriff. Vor weiteren Änderungen Beweise sichern." }
$adminNote = if (-not $IsAdmin) { '<p><b>Hinweis:</b> ohne Administratorrechte ausgeführt, Ergebnis eventuell unvollständig.</p>' } else { '' }
$table = if ($Installs.Count -eq 0) { '<p>Keine PHP-Installation gefunden.</p>' } else {
"<table><tr><th>Pfad</th><th>Version</th><th>Status</th><th>Genutzt von</th><th>Upload-Fortschritt</th><th>Empfehlung</th></tr>$($rows -join '')</table>" }
$hitTable = if ($LogHits.Count -gt 0) { "<table><tr><th>Datei</th><th>Zeile</th><th>Merkmal</th><th>Auszug</th></tr>$($hitRows -join '')</table>" } else { '' }
$html = @"
<!DOCTYPE html><html lang="de"><head><meta charset="utf-8"><title>PHP2Shell-Prüfbericht $Computer</title>
<style>body{font-family:Segoe UI,Arial,sans-serif;font-size:14px;color:#222;margin:24px}
h1{font-size:20px}table{border-collapse:collapse;width:100%;margin:12px 0}
th,td{border:1px solid #ccc;padding:6px 8px;text-align:left;vertical-align:top}th{background:#f0f0f0}
.badge{display:inline-block;padding:4px 12px;border-radius:4px;color:#fff;background:$color;font-weight:bold}
small{color:#666}</style></head><body>
<h1>PHP2Shell-Prüfbericht – $Computer</h1>
<p>Erstellt am $($Start.ToString('dd.MM.yyyy')) um $($Start.ToString('HH:mm')) Uhr · Skriptversion $ScriptVersion · Codekunst Systems GmbH</p>
<p>Gesamtergebnis: <span class="badge">$Overall</span></p>
$adminNote
<h2>Gefundene PHP-Installationen ($($Installs.Count))</h2>
$table
<h2>Webserver-Protokolle</h2><p>$logText</p>
$(if ($infoHits.Count -gt 0) { "<p><small>Außerdem $($infoHits.Count) Treffer von Adressen mit unklarer Einstufung (Anonymisierungsdienste laut Amazon bzw. nicht überprüfbarer Prüfscanner) – untersuchen, nicht automatisch sperren.</small></p>" })
$hitTable
<h2>Session-Ordner</h2>
$(if ($sessionHits.Count -eq 0) { '<p>Keine Webshell-Merkmale in den Session-Dateien gefunden.</p>' } else {
"<p style='color:#c62828'><b>$($sessionHits.Count) Session-Datei(en) mit Webshell-Merkmalen – starker Hinweis auf eine Kompromittierung.</b> System nicht neu starten und nichts löschen: zuerst Beweise sichern und den Vorfall prüfen lassen.</p><ul>" + (($sessionHits | ForEach-Object { '<li>' + (ConvertTo-HtmlSafe $_.Datei) + ' (geändert ' + $_.Geaendert + ') [' + (ConvertTo-HtmlSafe $_.Merkmal) + ']</li>' }) -join '') + '</ul>' })
<h2>Bewertungsgrundlage</h2>
<p>Korrigierte Versionen: 8.2.35, 8.3.36, 8.4.27, 8.5.12 (PHP-Sicherheitsupdate vom 21.10.2026). Versionen bis 8.1 erhalten vom PHP-Projekt keine Korrektur.
Zusätzlich sollte <code>session.upload_progress.enabled = 0</code> gesetzt sein (CVE-2026-115560).
Hintergrund: <a href="https://www.codekunst-systems.com/aktuell/php2shell-statusuebersicht">codekunst-systems.com/aktuell/php2shell-statusuebersicht</a></p>
<p><small>Rein lesende Prüfung, sofern keine durchgeführte Aktion aufgeführt ist. Ersetzt keine Prüfung beim Hoster oder in Containern und keine forensische Untersuchung.</small></p>
</body></html>
"@
$htmlPath = "$base.html"
$html | Set-Content -LiteralPath $htmlPath -Encoding UTF8
return [pscustomobject]@{ Txt = $txtPath; Html = $htmlPath; HtmlContent = $html }
}
# --- Mail -------------------------------------------------------------------
function Get-ExchangePickupDirectory {
# Same approach as the Exchange monitoring script: no relay, no credentials
if (-not $env:ExchangeInstallPath) { return $null }
$candidate = Join-Path $env:ExchangeInstallPath 'TransportRoles\Pickup'
if (Test-Path -LiteralPath $candidate) { return $candidate }
return $null
}
function Get-MailConfig {
if (Test-Path -LiteralPath $ConfigPath) {
try { return Get-Content -LiteralPath $ConfigPath -Raw | ConvertFrom-Json } catch {
Write-Log "config.json nicht lesbar: $($_.Exception.Message)" 'WARN'
}
}
if ($ScriptMail.Aktiv) {
$useAuth = [bool]$ScriptMail.Benutzer
if ($useAuth -and $ScriptMail.Kennwort) {
Write-Log "Mailversand mit Kennwort aus dem Skript - Ordnerrechte pr${c_ue}fen (nur Administratoren/SYSTEM)." 'WARN'
}
return [pscustomobject]@{
Empfaenger = @($ScriptMail.Empfaenger); Absender = $ScriptMail.Absender; Methode = 'SMTP'
SmtpServer = $ScriptMail.SmtpServer; SmtpPort = $ScriptMail.SmtpPort
Verschluesselung = $ScriptMail.Verschluesselung; Anmeldung = $useAuth
Benutzer = $ScriptMail.Benutzer; KennwortKlartext = $ScriptMail.Kennwort
SendenAb = $ScriptMail.SendenAb; Quelle = 'Skript'
}
}
return $null
}
function New-MailConfig {
Write-Host ''
Write-Host 'Einrichtung Mailversand' -ForegroundColor Cyan
$to = (Read-Host "Empf${c_ae}nger (mehrere mit Komma getrennt)") -split '\s*,\s*' | Where-Object { $_ }
$from = Read-Host 'Absenderadresse (z. B. php2shell@kunde.de)'
$pickup = Get-ExchangePickupDirectory
$method = 'SMTP'
if ($pickup) {
Write-Host "Exchange auf diesem Server gefunden ($pickup)." -ForegroundColor Cyan
if (Read-YesNo "${c_UeGross}ber das Exchange-Pickup-Verzeichnis versenden (kein Relay, keine Zugangsdaten)?") { $method = 'Pickup' }
} else {
Write-Host "Kein Exchange auf diesem Server - Versand ${c_ue}ber SMTP." -ForegroundColor Cyan
}
$server = ''; $port = 587; $ssl = $true; $auth = $false
if ($method -eq 'SMTP') {
$server = Read-Host 'SMTP-Server (z. B. smtp.office365.com oder interner Relay)'
$p = Read-Host 'Port [587]'; if ($p) { $port = [int]$p }
$ssl = -not ((Read-Host "Verschl${c_ue}sselung STARTTLS/TLS verwenden? [J/n]") -match '^(n|nein|no)$')
$user = Read-Host "SMTP-Benutzer (leer lassen f${c_ue}r Relay ohne Anmeldung)"
if ($user) {
$pw = Read-Host "Kennwort f${c_ue}r $user" -AsSecureString
$cred = New-Object System.Management.Automation.PSCredential($user, $pw)
$cred | Export-Clixml -LiteralPath $CredentialPath
$auth = $true
Write-Log "SMTP-Anmeldung verschl${c_ue}sselt gespeichert ($CredentialPath, nur f${c_ue}r Konto $env:USERNAME auf $Computer lesbar)." 'ACTION'
}
}
$sendOn = 'Nie' # unattended sending is configured separately for monitoring
$config = [pscustomobject]@{
Empfaenger = @($to); Absender = $from; Methode = $method
SmtpServer = $server; SmtpPort = $port; Verschluesselung = $ssl; Anmeldung = $auth
SendenAb = $sendOn; Eingerichtet = (Get-Date).ToString('s'); EingerichtetVon = "$env:USERDOMAIN\$env:USERNAME"
}
$config | ConvertTo-Json | Set-Content -LiteralPath $ConfigPath -Encoding UTF8
Write-Log "Mail-Einstellungen gespeichert: $ConfigPath" 'ACTION'
return $config
}
function Send-ReportMail {
param($Config, [string]$Subject, [string]$HtmlBody, [string[]]$Attachments)
# Attach copies so that no file used by this script stays locked during sending
$tempDir = Join-Path $env:TEMP ('php2shell-mail-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
$sendList = New-Object System.Collections.Generic.List[string]
foreach ($a in $Attachments | Where-Object { $_ -and (Test-Path -LiteralPath $_) }) {
$copy = Join-Path $tempDir ([System.IO.Path]::GetFileName($a))
if ($a -like '*.log') { $copy = "$copy.txt" }
Copy-Item -LiteralPath $a -Destination $copy -Force
$sendList.Add($copy)
}
$ok = $false; $err = $null
$msg = New-Object System.Net.Mail.MailMessage
try {
$msg.From = $Config.Absender
foreach ($r in $Config.Empfaenger) { $msg.To.Add($r) }
$msg.Subject = $Subject
$msg.SubjectEncoding = [System.Text.Encoding]::UTF8
$msg.Body = $HtmlBody
$msg.BodyEncoding = [System.Text.Encoding]::UTF8
$msg.IsBodyHtml = $true
foreach ($a in $sendList) { $msg.Attachments.Add((New-Object System.Net.Mail.Attachment($a))) }
$client = New-Object System.Net.Mail.SmtpClient
if ($Config.Methode -eq 'Pickup') {
$pickup = Get-ExchangePickupDirectory
if (-not $pickup) { throw 'Exchange-Pickup-Verzeichnis nicht gefunden.' }
$client.DeliveryMethod = [System.Net.Mail.SmtpDeliveryMethod]::SpecifiedPickupDirectory
$client.PickupDirectoryLocation = $pickup
} else {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$client.Host = $Config.SmtpServer
$client.Port = [int]$Config.SmtpPort
$client.EnableSsl = [bool]$Config.Verschluesselung
if ($Config.Anmeldung) {
if ($Config.PSObject.Properties['KennwortKlartext'] -and $Config.KennwortKlartext) {
$client.Credentials = New-Object System.Net.NetworkCredential($Config.Benutzer, $Config.KennwortKlartext)
} else {
if (-not (Test-Path -LiteralPath $CredentialPath)) { throw 'Gespeicherte SMTP-Anmeldung fehlt - Einrichtung mit -SetupMail wiederholen.' }
$cred = Import-Clixml -LiteralPath $CredentialPath
$client.Credentials = $cred.GetNetworkCredential()
}
}
}
$client.Send($msg)
$ok = $true
} catch {
$err = $_.Exception.Message
if ($_.Exception.InnerException) { $err += ' / ' + $_.Exception.InnerException.Message }
} finally {
$msg.Dispose()
Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
if ($ok) {
Write-Log "Bericht per Mail versendet an $($Config.Empfaenger -join ', ') ($($Config.Methode))." 'ACTION'
} else {
Write-Log "Mailversand fehlgeschlagen: $err" 'ERROR'
if ($Config.SmtpServer -match 'office365|outlook') {
Write-Log "Hinweis Microsoft 365: SMTP-Anmeldung (SMTP AUTH) ist oft deaktiviert oder per MFA blockiert. Postfach-Einstellung pr${c_ue}fen oder Connector/Relay nutzen." 'WARN'
}
}
return $ok
}
# --- Main -------------------------------------------------------------------
try {
# Mail setup only
if ($SetupMail) {
$cfg = New-MailConfig
$subject = "PHP2Shell-Pr${c_ue}fung $Computer - Testmail"
$body = "<p>Testmail der PHP2Shell-Pr${c_ue}fung auf <b>$Computer</b>. Der Mailversand ist eingerichtet.</p>"
if (Send-ReportMail -Config $cfg -Subject $subject -HtmlBody $body -Attachments @()) { exit 0 } else { exit 3 }
}
$isAdmin = Test-IsAdmin
Write-Log "PHP2Shell-Pr${c_ue}fung $ScriptVersion auf $Computer gestartet (Ordner: $BaseDirectory, Admin: $isAdmin)"
# Interactive: decide delivery up front
$wantMail = $false
$config = $null
if (-not $NoPrompt) {
Write-Host ''
Write-Host 'Bericht:' -ForegroundColor Cyan
Write-Host " [1] Nur im Skriptordner ablegen ($BaseDirectory)"
Write-Host " [2] Ablegen und zus${c_ae}tzlich per Mail senden"
if ((Read-Host 'Auswahl [1]') -eq '2') {
$wantMail = $true
$config = Get-MailConfig
if ($config) {
$via = if ($config.Methode -eq 'Pickup') { 'Exchange-Pickup' } else { "$($config.SmtpServer):$($config.SmtpPort)" }
Write-Host "Vorhandene Mail-Einstellungen: an $($config.Empfaenger -join ', ') ${c_ue}ber $via"
if (-not (Read-YesNo 'Diese Einstellungen verwenden?')) { $config = New-MailConfig }
} else {
$config = New-MailConfig
}
}
}
if (-not $isAdmin) { Write-Log "Ohne Administratorrechte: Pr${c_ue}fung eventuell unvollst${c_ae}ndig, Patchen nicht m${c_oe}glich." 'WARN' }
$iis = Get-IisContext
$processes = @(Get-CimInstance Win32_Process -Filter "Name='php-cgi.exe' OR Name='php.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.ExecutablePath } | ForEach-Object { $_.ExecutablePath })
$apache = Get-ApacheServices
$releases = $null
if (-not $SkipOnlineCheck) { $releases = Get-WindowsReleases }
Write-Log 'Suche PHP-Installationen auf allen lokalen Festplatten ...'
$binaries = Find-PhpBinaries
Write-Log "$($binaries.Count) PHP-Programmdatei(en) gefunden."
# One installation per folder (php.exe and php-cgi.exe in the same folder are one PHP)
$installs = @(foreach ($grp in ($binaries | Group-Object { (Split-Path -Parent $_).ToLowerInvariant() })) {
$bins = @($grp.Group | Sort-Object)
$dir = Split-Path -Parent $bins[0]
$ref = ($bins | Where-Object { $_ -like '*\php.exe' } | Select-Object -First 1)
if (-not $ref) { $ref = $bins[0] }
$version = Get-PhpVersion -Path $ref
$usage = New-Object System.Collections.Generic.List[string]
foreach ($bin in $bins) {
if ($iis.FastCgiPaths | Where-Object { $_ -ieq $bin }) { $usage.Add('IIS-FastCGI') }
foreach ($h in $iis.Handlers | Where-Object { $_.Prozessor -ieq $bin }) { $usage.Add("IIS-Handler $($h.Name) ($($h.Ebene))") }
if ($processes | Where-Object { $_ -ieq $bin }) { $usage.Add("laufender Prozess ($(Split-Path -Leaf $bin))") }
}
if (($apache | Where-Object { $_.State -eq 'Running' }) -and
(Get-ChildItem -LiteralPath $dir -Filter 'php*apache*.dll' -ErrorAction SilentlyContinue)) {
$usage.Add("Apache-Dienst l${c_ae}uft, PHP-Modul vorhanden")
}
$ts = Get-ThreadSafety -PhpDir $dir
$arch = Get-PeArchitecture -Path $ref
$obj = [pscustomobject]@{
Pfad = $dir; Ordner = $dir; Referenz = $ref
Programme = (($bins | ForEach-Object { Split-Path -Leaf $_ }) -join ', ')
Version = if ($version) { $version.Text } else { '' }
Variante = "$ts / $arch"
Status = Get-VersionStatus -Version $version
AktivGenutzt = $usage.Count -gt 0
Nutzung = (($usage | Select-Object -Unique) -join ', ')
UploadFortschritt = Get-UploadProgressSetting -PhpDir $dir
Uebergangsschutz = Test-GuardExtension -PhpDir $dir
Plesk = [bool]($dir -match '\\Plesk\\|\\Parallels\\')
Dateidatum = (Get-Item -LiteralPath $ref).LastWriteTime.ToString('yyyy-MM-dd')
Paket = $null; Empfehlung = ''; Aktion = ''; Schweregrad = 0
}
if ($obj.Status -eq 'VERWUNDBAR' -and -not $obj.Plesk -and $ts -ne 'unbekannt' -and $arch -ne 'unbekannt') {
$obj.Paket = Find-OfficialPackage -Releases $releases -Version $version -Ts $ts -Arch $arch
}
Write-Log ("Gefunden: {0} ({1}) | {2} | {3} | {4} | Upload-Fortschritt: {5}" -f $dir, $obj.Programme, $obj.Version, $obj.Status,
$(if ($obj.AktivGenutzt) { $obj.Nutzung } else { 'nicht genutzt festgestellt' }), $obj.UploadFortschritt)
$obj
})
# Interactive remediation offers
if (-not $NoPrompt -and $isAdmin) {
foreach ($i in $installs) {
if ($i.Status -eq 'VERWUNDBAR' -and $i.Paket -and $i.Paket.Verfuegbar) {
Write-Host ''
Write-Host "F${c_ue}r $($i.Pfad) ($($i.Version)) gibt es das offizielle Paket $($i.Paket.Version) [$($i.Paket.Variante)]." -ForegroundColor Cyan
if ($i.AktivGenutzt) { Write-Host "Achtung: W${c_ae}hrend des Updates werden IIS bzw. Apache kurz gestoppt (Ausfall der Webseiten)." -ForegroundColor Yellow }
if (Read-YesNo "Jetzt aktualisieren (Sicherung nach $BackupDirectory)?") {
[void](Invoke-PhpUpdate -Install $i)
} else { Write-Log "Update f${c_ue}r $($i.Pfad) vom Benutzer abgelehnt." }
}
if ($i.UploadFortschritt -ne 'aus') {
if (Read-YesNo "session.upload_progress.enabled = 0 in der php.ini von $($i.Ordner) setzen (mit Sicherung)?") {
if (Set-UploadProgressOff -PhpDir $i.Ordner) {
$i.UploadFortschritt = 'aus'
$i.Aktion = (@($i.Aktion, "Upload-Fortschrittsanzeige abgeschaltet (IIS/Apache-Neustart n${c_oe}tig)") | Where-Object { $_ }) -join '; '
}
}
}
}
}
$logHits = @(); $infoHits = @()
if (-not $SkipLogScan) {
Write-Log "Durchsuche Webserver-Protokolle der letzten $LogDays Tage ..."
$allHits = @(Search-Indicators -Directories (@($iis.LogDirectories) + $AdditionalLogPath) -Days $LogDays)
$logHits = @($allHits | Where-Object { $_.Einstufung -eq 'Angriffsmerkmal' })
$infoHits = @($allHits | Where-Object { $_.Einstufung -eq 'Unklar' })
if ($logHits.Count -gt 0) { Write-Log "$($logHits.Count) Treffer auf bekannte Angriffsmerkmale!" 'WARN' }
if ($infoHits.Count -gt 0) { Write-Log "$($infoHits.Count) Treffer von Adressen mit unklarer Einstufung (untersuchen)." 'WARN' }
}
$sessionHits = @()
if ($installs.Count -gt 0) {
$sessionDirs = Get-SessionDirectories -Installs $installs
Write-Log "Durchsuche Session-Ordner nach Webshells: $($sessionDirs -join ', ')"
$sessionHits = @(Search-SessionWebshells -Directories $sessionDirs)
if ($sessionHits.Count -gt 0) { Write-Log "$($sessionHits.Count) Session-Datei(en) mit Webshell-Merkmalen gefunden - starker Hinweis auf Kompromittierung!" 'ERROR' }
}
foreach ($i in $installs) { $i.Schweregrad = Get-Severity -Install $i; $i.Empfehlung = Get-Advice -Install $i }
$overall = 0
if ($installs.Count -gt 0) { $overall = ($installs | Measure-Object -Property Schweregrad -Maximum).Maximum }
if ($infoHits.Count -gt 0 -and $overall -lt 1) { $overall = 1 }
if ($logHits.Count -gt 0 -or $sessionHits.Count -gt 0) { $overall = 2 }
if (-not $isAdmin -and $overall -eq 0) { $overall = 1 }
$overallText = @('OK', 'WARNUNG', 'KRITISCH')[$overall]
$jsonPath = Join-Path $BaseDirectory "PHP2Shell_$Computer`_$Stamp.json"
[pscustomobject]@{
Skriptversion = $ScriptVersion; Computer = $Computer; Zeitpunkt = $Start.ToString('s'); Administrator = $isAdmin
Gesamtergebnis = $overallText; ExitCode = $overall
Installationen = @($installs | Select-Object Pfad, Programme, Version, Variante, Status, AktivGenutzt, Nutzung, UploadFortschritt, Uebergangsschutz, Plesk, Dateidatum, Empfehlung, Aktion, Schweregrad)
Protokolltreffer = $logHits
UnklareTreffer = $infoHits
SessionTreffer = $sessionHits
} | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $jsonPath -Encoding UTF8
$csvPath = Join-Path $BaseDirectory 'PHP2Shell_Verlauf.csv'
$csvRows = if ($installs.Count -gt 0) {
$installs | ForEach-Object { [pscustomobject]@{ Zeitpunkt = $Start.ToString('s'); Computer = $Computer; Pfad = $_.Pfad
Version = $_.Version; Status = $_.Status; AktivGenutzt = $_.AktivGenutzt; UploadFortschritt = $_.UploadFortschritt
Aktion = $_.Aktion; Protokolltreffer = $logHits.Count; Ergebnis = $overallText } }
} else {
[pscustomobject]@{ Zeitpunkt = $Start.ToString('s'); Computer = $Computer; Pfad = ''; Version = ''; Status = 'KEIN PHP'
AktivGenutzt = $false; UploadFortschritt = ''; Aktion = ''; Protokolltreffer = $logHits.Count; Ergebnis = $overallText }
}
$csvRows | Export-Csv -LiteralPath $csvPath -NoTypeInformation -Append -Encoding UTF8 -Delimiter ';'
$reports = New-Reports -Installs $installs -LogHits $logHits -Overall $overallText -IsAdmin $isAdmin
Write-Log "Bericht erstellt: $($reports.Html)"
if ($isAdmin) {
try {
$src = 'PHP2ShellCheck'
if (-not [System.Diagnostics.EventLog]::SourceExists($src)) { New-EventLog -LogName Application -Source $src }
Write-EventLog -LogName Application -Source $src -EventId @(1000, 2000, 3000)[$overall] `
-EntryType @('Information', 'Warning', 'Error')[$overall] `
-Message "PHP2Shell-Pr${c_ue}fung: $overallText. Installationen: $($installs.Count), Protokolltreffer: $($logHits.Count), Session-Treffer: $($sessionHits.Count). Bericht: $($reports.Html)"
} catch { Write-Log "Ereignisprotokoll nicht beschreibbar: $($_.Exception.Message)" 'WARN' }
}
$attachments = @($reports.Html, $reports.Txt, $LogPath)
$subject = "PHP2Shell-Pr${c_ue}fung $Computer - $overallText - $($Start.ToString('dd.MM.yyyy HH:mm'))"
if ($NoPrompt) {
# Unattended: send according to configuration
$config = Get-MailConfig
if ($config) {
$threshold = switch ($config.SendenAb) { 'Immer' { 0 } 'Warnung' { 1 } 'Kritisch' { 2 } default { 99 } }
if ($SendMail -or $overall -ge $threshold) {
[void](Send-ReportMail -Config $config -Subject $subject -HtmlBody $reports.HtmlContent -Attachments $attachments)
}
}
} else {
Write-Host ''
Write-Host (Get-Content -LiteralPath $reports.Txt -Raw)
Write-Host "Bericht abgelegt: $($reports.Html)" -ForegroundColor Cyan
if ($wantMail -and $config) {
[void](Send-ReportMail -Config $config -Subject $subject -HtmlBody $reports.HtmlContent -Attachments $attachments)
}
try { Invoke-Item -LiteralPath $reports.Html } catch { Write-Log "Bericht konnte nicht automatisch ge${c_oe}ffnet werden: $($_.Exception.Message)" 'WARN' }
}
Write-Log "Pr${c_ue}fung beendet: $overallText"
Write-Output ("PHP2SHELL {0} | {1} | PHP: {2} | Treffer: {3} | {4}" -f $overallText, $Computer, $installs.Count, $logHits.Count, $reports.Html)
exit $overall
}
catch {
try { Write-Log "Abbruch: $($_.Exception.Message)" 'ERROR' } catch { }
Write-Output ("PHP2SHELL FEHLER | {0} | {1}" -f $env:COMPUTERNAME, $_.Exception.Message)
exit 3
}
Den gesamten Inhalt mit der Kopieren-Schaltfläche übernehmen, in einem Editor einfügen und als Test-PHP2Shell.ps1 speichern, am besten im Ordner C:\PHP2ShellCheck. Das Skript enthält nur ASCII-Zeichen; Umlaute in Bericht und Konsole werden zur Laufzeit erzeugt. Ausführen in Windows PowerShell 5.1 als Administrator.
| Schritt beim Update | Was passiert |
|---|---|
| Download | Offizielles Paket aus dem PHP-Paketverzeichnis |
| Prüfsumme | SHA256-Vergleich; bei Abweichung Abbruch, nichts wird verändert |
| Sicherung | Vollständige Kopie des PHP-Ordners in den Unterordner Sicherungen; vorhandene Sicherungen werden nie überschrieben |
| Dienste | IIS bzw. Apache stoppen, Dateien ersetzen (php.ini und eigene Erweiterungen bleiben erhalten), Dienste wieder starten |
| Kontrolle | Neue Version wird geprüft und im Bericht vermerkt |
Vor dem Update beachten
Während des Updates sind die Webseiten des Servers kurz nicht erreichbar – also außerhalb der Geschäftszeiten aktualisieren. Plesk-Installationen aktualisiert das Skript nicht, das übernimmt Plesk selbst. Für Versionen bis 8.1 gibt es kein offizielles Paket. Und bringt eine Fachanwendung ihr PHP selbst mit, vor dem Update den Hersteller fragen.
| Ergebnis | Bedeutung |
|---|---|
| OK | Kein PHP gefunden oder alle Installationen gepatcht und abgesichert |
| WARNUNG | Veraltetes PHP, das nicht aktiv genutzt wird; Upload-Fortschrittsanzeige aktiv; Version nicht bestimmbar; Treffer von Adressen mit unklarer Einstufung |
| KRITISCH | Verwundbares PHP wird aktiv genutzt, bekannte Angriffsmerkmale in den Protokollen oder Webshell-Merkmale in Session-Dateien |
Testumgebung: Setup-PHP2ShellTestumgebung.ps1
Vor dem Einsatz bei Kunden lässt sich auf einem Testrechner prüfen, ob alles richtig erkannt wird – auch auf einem normalen Windows-11-Rechner ohne IIS. Das Skript lädt PHP aus dem offiziellen Archiv mit Prüfsummenkontrolle und startet den eingebauten Webserver nur auf 127.0.0.1; die Testspuren enthalten keinen ausführbaren Code. Aufruf: .\Setup-PHP2ShellTestumgebung.ps1 -MitTestspuren, aufräumen mit -Aufraeumen.
Setup-PHP2ShellTestumgebung.ps1
Baut auf einem Testrechner eine Umgebung mit absichtlich veralteten PHP-Versionen und harmlosen Testspuren auf. Stand 25.10.2026.
#Requires -Version 5.1
<#
.SYNOPSIS
Richtet auf einem Windows-Rechner eine lokale Testumgebung fuer das
PHP2Shell-Pruefskript ein - ohne IIS.
.DESCRIPTION
1. Laedt PHP 8.3.30 (verwundbar, Update moeglich) und PHP 8.1.34 (EOL)
als NTS-x64-Pakete aus dem offiziellen PHP-Archiv
2. Entpackt nach C:\php83 und C:\php81 und legt je eine php.ini an
3. Prueft, ob PHP startet (Visual C++ Runtime vorhanden?)
4. Startet den eingebauten PHP-Webserver NUR auf 127.0.0.1:8080
5. Prueft per HTTP, ob die Testseite erreichbar ist
Optional:
-MitTestspuren legt eine Test-Protokolldatei und eine harmlose Session-Datei
mit den bekannten Merkmalen an (nur Text, kein ausfuehrbarer Code)
-Stoppen beendet nur den Testserver
-Aufraeumen beendet den Testserver und loescht alle Testordner
ACHTUNG: Die installierten PHP-Versionen sind absichtlich verwundbar.
Nur auf einem Testrechner verwenden. Der Webserver lauscht nur lokal.
.EXAMPLE
.\Setup-PHP2ShellTestumgebung.ps1 -MitTestspuren
.\Setup-PHP2ShellTestumgebung.ps1 -Stoppen
.\Setup-PHP2ShellTestumgebung.ps1 -Aufraeumen
.NOTES
Codekunst Systems GmbH - 25.10.2026 - nur fuer Testzwecke
#>
[CmdletBinding()]
param(
[switch]$MitTestspuren,
[switch]$Stoppen,
[switch]$Aufraeumen,
[int]$Port = 8080
)
$ErrorActionPreference = 'Stop'
$ArchiveBase = 'https://downloads.php.net/~windows/releases/archives/'
$Packages = @(
[pscustomobject]@{ Name = 'PHP 8.3.30'; Zip = 'php-8.3.30-nts-Win32-vs16-x64.zip'; Ziel = 'C:\php83'; Webserver = $true
Sha256 = '42637B42B38B9C0D731E59C5CB8B755693A01B110CD2F31951F67DE5CB4CD129' }
[pscustomobject]@{ Name = 'PHP 8.1.34'; Zip = 'php-8.1.34-nts-Win32-vs16-x64.zip'; Ziel = 'C:\php81'; Webserver = $false
Sha256 = '9CFE246CB144076C16F5913A3EF88A474C3DD7E60F0F0C8BB95FAF68674016CC' }
)
$LogDir = 'C:\php2test-logs'
$PidFile = Join-Path $env:TEMP 'php2shell-testserver.pid'
function Write-Step { param([string]$Text) Write-Host "==> $Text" -ForegroundColor Cyan }
function Write-Ok { param([string]$Text) Write-Host " OK: $Text" -ForegroundColor Green }
function Write-Warn { param([string]$Text) Write-Host " ACHTUNG: $Text" -ForegroundColor Yellow }
function Stop-TestServer {
if (Test-Path -LiteralPath $PidFile) {
$procId = Get-Content -LiteralPath $PidFile -ErrorAction SilentlyContinue
if ($procId) { Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue }
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
}
# Safety net: any php.exe started from the test folders
Get-CimInstance Win32_Process -Filter "Name='php.exe' OR Name='php-cgi.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.ExecutablePath -and ($_.ExecutablePath -like 'C:\php83\*' -or $_.ExecutablePath -like 'C:\php81\*') } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
Write-Ok 'Testserver beendet (falls er lief).'
}
# --- Stop / cleanup ------------------------------------------------------------
if ($Stoppen -or $Aufraeumen) {
Write-Step 'Testserver beenden'
Stop-TestServer
if ($Aufraeumen) {
Write-Step 'Testordner loeschen'
foreach ($d in @('C:\php83', 'C:\php81', $LogDir)) {
if (Test-Path -LiteralPath $d) { Remove-Item -LiteralPath $d -Recurse -Force; Write-Ok "$d geloescht" }
}
Write-Warn 'Den Ordner "Sicherungen" im Ordner des Pruefskripts bei Bedarf von Hand loeschen.'
}
return
}
# --- 1./2. Download and extract -------------------------------------------------
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
foreach ($p in $Packages) {
Write-Step "$($p.Name) einrichten"
if (Test-Path -LiteralPath (Join-Path $p.Ziel 'php.exe')) {
Write-Ok "$($p.Ziel) existiert bereits - Download uebersprungen."
} else {
$zip = Join-Path $env:TEMP $p.Zip
Write-Host " Lade $($p.Zip) ..."
Invoke-WebRequest -Uri ($ArchiveBase + $p.Zip) -OutFile $zip -UseBasicParsing -TimeoutSec 600
$hash = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash
if ($hash -ne $p.Sha256) {
Remove-Item -LiteralPath $zip -Force
throw "Pruefsumme von $($p.Zip) stimmt nicht (erwartet $($p.Sha256), erhalten $hash). Abbruch."
}
Write-Ok 'Pruefsumme stimmt.'
Expand-Archive -LiteralPath $zip -DestinationPath $p.Ziel -Force
Remove-Item -LiteralPath $zip -Force
Write-Ok "Entpackt nach $($p.Ziel)"
}
$ini = Join-Path $p.Ziel 'php.ini'
if (-not (Test-Path -LiteralPath $ini)) {
Copy-Item -LiteralPath (Join-Path $p.Ziel 'php.ini-production') -Destination $ini
Write-Ok 'php.ini aus php.ini-production angelegt.'
}
# --- 3. Does PHP start? ---
$out = & (Join-Path $p.Ziel 'php.exe') -n -v 2>&1
if ($LASTEXITCODE -ne 0 -or -not ($out -match '^PHP \d')) {
Write-Warn "PHP startet nicht. Meist fehlt die Visual C++ Runtime 2015-2022 (x64):"
Write-Host ' https://aka.ms/vs/17/release/vc_redist.x64.exe - installieren und Skript erneut starten.'
return
}
Write-Ok (($out | Select-Object -First 1) -join '')
}
# --- 4. Start built-in web server (localhost only) ------------------------------
Write-Step "Eingebauten PHP-Webserver auf 127.0.0.1:$Port starten"
$www = 'C:\php83\www'
if (-not (Test-Path -LiteralPath $www)) { New-Item -ItemType Directory -Path $www | Out-Null }
Set-Content -LiteralPath (Join-Path $www 'index.php') -Value '<?php echo "PHP2Shell-Testumgebung laeuft: PHP " . PHP_VERSION;' -Encoding ASCII
Stop-TestServer
$proc = Start-Process -FilePath 'C:\php83\php.exe' -ArgumentList @('-S', "127.0.0.1:$Port", '-t', $www) `
-WindowStyle Hidden -PassThru
Set-Content -LiteralPath $PidFile -Value $proc.Id
Start-Sleep -Seconds 2
# --- 5. Reachability test -------------------------------------------------------
Write-Step 'Erreichbarkeit pruefen'
$ok = $false
for ($i = 0; $i -lt 5 -and -not $ok; $i++) {
try {
$r = Invoke-WebRequest -Uri "http://127.0.0.1:$Port/" -UseBasicParsing -TimeoutSec 5
if ($r.StatusCode -eq 200 -and $r.Content -match 'Testumgebung laeuft') { $ok = $true; Write-Ok $r.Content }
} catch { Start-Sleep -Seconds 1 }
}
if (-not $ok) {
Write-Warn "Testseite nicht erreichbar. Ist Port $Port belegt? Mit -Port 8081 erneut versuchen."
return
}
$listen = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1
if ($listen -and $listen.LocalAddress -ne '127.0.0.1') {
Write-Warn "Der Server lauscht auf $($listen.LocalAddress) statt nur lokal - bitte pruefen!"
} else {
Write-Ok 'Server lauscht nur lokal (127.0.0.1).'
}
# --- Optional test traces -------------------------------------------------------
if ($MitTestspuren) {
Write-Step 'Testspuren anlegen (nur Text, kein ausfuehrbarer Code)'
if (-not (Test-Path -LiteralPath $LogDir)) { New-Item -ItemType Directory -Path $LogDir | Out-Null }
Set-Content -LiteralPath (Join-Path $LogDir 'test.log') -Encoding ASCII -Value @(
'2026-10-21 08:40:00 POST /index.php - 80 - 85.11.167.243 php2shell-scan/109618 200'
'2026-10-17 10:00:00 POST /index.php - 80 - 103.216.220.92 Mozilla 200'
)
Write-Ok "Protokoll-Testdatei: $LogDir\test.log (1 Angriffsmerkmal, 1 unklarer Treffer)"
$sess = 'C:\php83\sessions'
if (-not (Test-Path -LiteralPath $sess)) { New-Item -ItemType Directory -Path $sess | Out-Null }
$ini = 'C:\php83\php.ini'
if (-not (Select-String -LiteralPath $ini -Pattern '^\s*session\.save_path\s*=\s*"C:\\php83\\sessions"' -Quiet)) {
Add-Content -LiteralPath $ini -Value 'session.save_path = "C:\php83\sessions"'
}
Set-Content -LiteralPath (Join-Path $sess 'sess_test123') -Value 'upload_progress|PWNED:: nur ein Test' -Encoding ASCII
Write-Ok 'Session-Testdatei: C:\php83\sessions\sess_test123'
}
# --- Summary -------------------------------------------------------------------
Write-Host ''
Write-Host 'Testumgebung bereit:' -ForegroundColor Green
Write-Host " C:\php83 PHP 8.3.30, laeuft als Webserver auf http://127.0.0.1:$Port -> erwartet: VERWUNDBAR, aktiv genutzt"
Write-Host ' C:\php81 PHP 8.1.34, nicht gestartet -> erwartet: EOL, nicht aktiv genutzt'
Write-Host ''
Write-Host 'Naechste Schritte:'
if ($MitTestspuren) {
Write-Host ' .\Test-PHP2Shell.ps1 -AdditionalLogPath C:\php2test-logs'
} else {
Write-Host ' .\Test-PHP2Shell.ps1'
}
Write-Host ' Monitoring: mit New-PHP2ShellMonitorScript.ps1 erzeugtes Skript mit -Mode Full starten'
Write-Host ' Danach: .\Setup-PHP2ShellTestumgebung.ps1 -Aufraeumen'
Den gesamten Inhalt mit der Kopieren-Schaltfläche übernehmen, in einem Editor einfügen und als Setup-PHP2ShellTestumgebung.ps1 speichern. Das Skript enthält nur ASCII-Zeichen. Nur auf Testrechnern verwenden.
| Prüfpunkt | Erwartung beim Einzelplatz-Skript |
|---|---|
| Installationen | 2 – C:\php83 und C:\php81, je mit php.exe und php-cgi.exe |
| C:\php83 | 8.3.30, VERWUNDBAR, laufender Prozess, Update-Angebot auf 8.3.36 |
| C:\php81 | 8.1.34, EOL, nicht genutzt, kein Update-Angebot, Angebot für die php.ini |
| Protokolle (mit -AdditionalLogPath C:\php2test-logs) | 1 Angriffsmerkmal und 1 unklarer Treffer |
| Session-Ordner | 1 Datei mit Webshell-Merkmalen |
| Gesamtergebnis | KRITISCH |
Nur für Testrechner
Die installierten PHP-Versionen sind absichtlich verwundbar. Die Testumgebung nur auf einem Testrechner einsetzen, der nicht aus dem Internet erreichbar ist, und danach wieder entfernen.
Monitoring: New-PHP2ShellMonitorScript.ps1
Für viele Rechner erzeugt der Generator ein Skript mit eingebauter Konfiguration: Meldung nur per Exit-Code, Bericht lokal oder auf einer Freigabe, optional Mail mit Schwelle und „nur bei Änderung“. Das erzeugte Skript fragt nichts, verändert nichts und läuft als SYSTEM – über RMM, Softwareverteilung oder geplante Aufgabe, wöchentlich mit -Mode Full, täglich mit -Mode Quick.
New-PHP2ShellMonitorScript.ps1
Generator für das Monitoring-Skript. Version 1.3, Stand 25.10.2026.
#Requires -Version 5.1
<#
.SYNOPSIS
Erzeugt ein konfiguriertes PHP2Shell-Monitoring-Skript zur Verteilung
(RMM, Gruppenrichtlinie, geplante Aufgabe).
.DESCRIPTION
Fragt interaktiv ab:
- Kunde (Name/Kuerzel)
- Ausgabe: nur Exit-Code | Bericht lokal (C:\PHP2ShellCheck) | Bericht auf Freigabe
- Mailversand ja/nein (SMTP, Empfaenger, Schwelle, nur bei Aenderung)
- Standardmodus Full/Quick
und erzeugt im Ausgabeordner:
- PHP2Shell-Monitor_<Kunde>.ps1 (zu verteilendes Skript, Konfiguration eingebaut)
- PHP2Shell-Monitor_<Kunde>_Verteilung.txt (Aufrufe und Hinweise)
Das erzeugte Skript ist rein lesend, fragt nichts ab und meldet sein Ergebnis
ueber Exit-Code (0 OK, 1 WARNUNG, 2 KRITISCH, 3 FEHLER), eine Ergebniszeile auf der
Standardausgabe, das Windows-Ereignisprotokoll und je nach Konfiguration ueber
Bericht und Mail. Damit laesst es sich in beliebige Monitoring- und RMM-Systeme
einbinden.
.PARAMETER Ausgabeordner
Wohin das erzeugte Skript geschrieben wird. Standard: Ordner dieses Generators.
.NOTES
Codekunst Systems GmbH - Generator 1.3 - 25.10.2026
Zielgruppe: Administratoren. Ohne Gewaehr.
#>
[CmdletBinding()]
param(
[string]$Ausgabeordner = $(if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path })
)
$ErrorActionPreference = 'Stop'
$GeneratorVersion = '1.3'
# =============================================================================
# TEMPLATE OF THE DISTRIBUTED MONITORING SCRIPT
# The line '#__KONFIGURATION__#' is replaced by the generated configuration.
# =============================================================================
$Template = @'
#Requires -Version 5.1
<#
.SYNOPSIS
PHP2Shell-Monitoring (unbeaufsichtigt, rein lesend) - erzeugt mit dem
Codekunst-Generator. Konfiguration siehe Block KONFIGURATION unten.
.DESCRIPTION
-Mode Full Suche nach php.exe / php-cgi.exe auf allen lokalen Festplatten (z. B. woechentlich)
-Mode Quick bekannte Fundorte, IIS, laufende Prozesse, uebliche Ordner (z. B. taeglich)
Bewertung gegen 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12, Nutzung (IIS, Prozess, Apache),
php.ini (session.upload_progress.enabled), IIS-Protokolle, Session-Ordner (Webshell),
Erkennung des Uebergangsschutzes php_uploadprogress_guard.
Ausgabe fuer Monitoring-Systeme:
Exit-Code 0 OK, 1 WARNUNG, 2 KRITISCH, 3 FEHLER
Stdout eine Zeile: PHP2SHELL <STATUS> | <Kunde> | <Rechner> | PHP: n | Treffer: n
Eventlog Anwendung, Quelle PHP2ShellCheck, ID 1000 / 2000 / 3000
.NOTES
Codekunst Systems GmbH - Monitoring 1.2 - als SYSTEM ausfuehren.
HAFTUNGSAUSSCHLUSS: Bereitgestellt ohne Gewaehr. Die Pruefung ist rein lesend.
Vor dem Einsatz auf produktiven Systemen in einer Testumgebung pruefen.
Einsatz in eigener Verantwortung des Betreibers. Ersetzt keine forensische Untersuchung.
#>
[CmdletBinding()]
param(
[ValidateSet('Full', 'Quick', '')][string]$Mode = ''
)
$ErrorActionPreference = 'Stop'
$ScriptVersion = 'MON-1.2'
#__KONFIGURATION__#
if (-not $Mode) { $Mode = $Config.StandardModus }
# --- Reference data (Stand 25.10.2026) --------------------------------------------
$FixedVersions = @{ '8.2' = 35; '8.3' = 36; '8.4' = 27; '8.5' = 12 }
$Indicators = @(
'php2shell-', 'B7kQpoc618', 'B7kQpoc619', 'p2s9rdme2st6guc9g0rafic', 'php://filter',
'xqsuvwz3n97wu8j3emz1axuo5y',
'85.11.167.243', '217.60.242.76', '45.198.224.169', '221.14.159.134', '91.90.123.25',
'87.121.84.174', '181.215.186.252', '185.196.220.132', '185.161.210.29', '152.233.51.69',
'45.134.225.16', '107.189.14.87', '193.47.62.24', '103.147.220.17',
'2409:40d2:204e:4be:2a1f:f9c9:a055:e00b', '206.188.197.105',
'92.118.112.100', '134.199.154.64', 'godofthunder.st'
)
$InfoIndicators = @('109.71.252.97',
'103.216.220.92', '31.223.184.166', '37.19.200.5', '134.209.159.64', '103.114.163.188', '95.217.98.37')
$SessionShellPatterns = @('PWNED::', 'system($_GET')
$ExcludedDirNames = @('$Recycle.Bin', 'System Volume Information', 'WinSxS', 'Installer')
$CommonRoots = @('C:\php', 'C:\Program Files\PHP', 'C:\Program Files (x86)\PHP', 'C:\xampp', 'C:\laragon',
'C:\wamp64', 'C:\wamp', 'C:\inetpub', 'C:\Program Files\iis express')
# --- Paths -------------------------------------------------------------------------
$Start = Get-Date
$Stamp = $Start.ToString('yyyyMMdd-HHmmss')
$Computer = $env:COMPUTERNAME
$StateDir = Join-Path $env:ProgramData 'PHP2ShellCheck'
if (-not (Test-Path -LiteralPath $StateDir)) { New-Item -ItemType Directory -Path $StateDir -Force | Out-Null }
$StatePath = Join-Path $StateDir 'monitor-state.json'
$LogPath = Join-Path $StateDir 'monitor.log'
function Write-Log {
param([string]$Text, [string]$Level = 'INFO')
$line = '{0} [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Text
for ($t = 0; $t -lt 3; $t++) {
try { Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8 -ErrorAction Stop; break } catch { Start-Sleep -Milliseconds 200 }
}
}
# --- Discovery and assessment --------------------------------------------------------
function Find-PhpInPaths {
param([string[]]$Roots)
$found = New-Object System.Collections.Generic.List[string]
foreach ($root in $Roots | Where-Object { $_ -and (Test-Path -LiteralPath $_) }) {
$stack = New-Object System.Collections.Generic.Stack[string]
$stack.Push($root)
while ($stack.Count -gt 0) {
$dir = $stack.Pop()
if ($dir -like 'C:\PHP2ShellCheck\Sicherungen*') { continue }
try {
foreach ($file in [System.IO.Directory]::GetFiles($dir)) {
$name = [System.IO.Path]::GetFileName($file)
if ($name -ieq 'php.exe' -or $name -ieq 'php-cgi.exe') { $found.Add($file) }
}
foreach ($sub in [System.IO.Directory]::GetDirectories($dir)) {
if ($ExcludedDirNames -contains [System.IO.Path]::GetFileName($sub)) { continue }
if ([System.IO.File]::GetAttributes($sub) -band [System.IO.FileAttributes]::ReparsePoint) { continue }
$stack.Push($sub)
}
} catch { }
}
}
return $found
}
function Get-PhpVersion {
param([string]$Path)
try {
$vi = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($Path)
foreach ($c in @($vi.ProductVersion, $vi.FileVersion)) {
if ($c -and $c -match '^(\d+)\.(\d+)\.(\d+)') {
return [pscustomobject]@{ Text = '{0}.{1}.{2}' -f $Matches[1], $Matches[2], $Matches[3]
Major = [int]$Matches[1]; Minor = [int]$Matches[2]; Patch = [int]$Matches[3] }
}
}
} catch { }
return $null
}
function Get-VersionStatus {
param($Version)
if ($null -eq $Version) { return 'UNBEKANNT' }
$branch = '{0}.{1}' -f $Version.Major, $Version.Minor
if ($Version.Major -lt 8 -or ($Version.Major -eq 8 -and $Version.Minor -lt 2)) { return 'EOL' }
if ($FixedVersions.ContainsKey($branch)) { if ($Version.Patch -ge $FixedVersions[$branch]) { return 'GEPATCHT' } else { return 'VERWUNDBAR' } }
return 'GEPATCHT'
}
function Get-UploadProgressSetting {
param([string]$PhpDir)
$ini = Join-Path $PhpDir 'php.ini'
if (-not (Test-Path -LiteralPath $ini)) { return 'an' }
$value = $null
foreach ($line in [System.IO.File]::ReadAllLines($ini)) {
if ($line -match '^\s*session\.upload_progress\.enabled\s*=\s*"?([^";\s]+)') { $value = $Matches[1] }
}
if ($value -and $value -match '^(0|off|false|no)$') { return 'aus' }
return 'an'
}
function Test-GuardExtension {
param([string]$PhpDir)
$ini = Join-Path $PhpDir 'php.ini'
if (Test-Path -LiteralPath $ini) {
foreach ($line in [System.IO.File]::ReadAllLines($ini)) {
if ($line -match '^\s*(zend_)?extension\s*=\s*"?[^";]*uploadprogress_guard') { return $true }
}
}
return $false
}
function Get-IisContext {
$ctx = [pscustomobject]@{ Paths = @(); LogDirectories = @() }
$cfgPath = Join-Path $env:windir 'System32\inetsrv\config\applicationHost.config'
if (-not (Test-Path -LiteralPath $cfgPath)) { return $ctx }
try {
[xml]$cfg = [System.IO.File]::ReadAllText($cfgPath)
$p = New-Object System.Collections.Generic.List[string]
foreach ($a in @($cfg.configuration.'system.webServer'.fastCgi.application)) { if ($a -and $a.fullPath) { $p.Add([Environment]::ExpandEnvironmentVariables($a.fullPath)) } }
foreach ($x in @($cfg.configuration.'system.webServer'.handlers.add)) { if ($x -and $x.scriptProcessor) { $p.Add([Environment]::ExpandEnvironmentVariables(($x.scriptProcessor -split '\|')[0])) } }
foreach ($loc in @($cfg.configuration.location)) {
if (-not $loc) { continue }
foreach ($x in @($loc.'system.webServer'.handlers.add)) { if ($x -and $x.scriptProcessor) { $p.Add([Environment]::ExpandEnvironmentVariables(($x.scriptProcessor -split '\|')[0])) } }
}
$ctx.Paths = @($p | Select-Object -Unique)
$logDir = $cfg.configuration.'system.applicationHost'.sites.siteDefaults.logFile.directory
if (-not $logDir) { $logDir = '%SystemDrive%\inetpub\logs\LogFiles' }
$ctx.LogDirectories = @([Environment]::ExpandEnvironmentVariables($logDir))
} catch { Write-Log "IIS-Konfiguration nicht lesbar: $($_.Exception.Message)" 'WARN' }
return $ctx
}
function Search-Indicators {
param([string[]]$Directories, [int]$Days)
$hits = New-Object System.Collections.Generic.List[object]
$since = (Get-Date).AddDays(-$Days)
foreach ($dir in $Directories | Where-Object { $_ -and (Test-Path -LiteralPath $_) }) {
foreach ($f in Get-ChildItem -LiteralPath $dir -Recurse -File -Include *.log -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -ge $since }) {
foreach ($x in (Select-String -LiteralPath $f.FullName -Pattern ($Indicators + $InfoIndicators) -SimpleMatch -ErrorAction SilentlyContinue | Select-Object -First 20)) {
$line = $x.Line
$kind = if (@($InfoIndicators | Where-Object { $line.Contains($_) }).Count -gt 0) { 'Unklar' } else { 'Angriffsmerkmal' }
$hits.Add([pscustomobject]@{ Datei = $f.FullName; Zeile = $x.LineNumber; Merkmal = $x.Pattern; Einstufung = $kind
Auszug = $line.Substring(0, [Math]::Min(300, $line.Length)) })
}
}
}
return $hits
}
function Search-SessionWebshells {
param($Installs)
$dirs = New-Object System.Collections.Generic.List[string]
foreach ($i in $Installs) {
$ini = Join-Path $i.Ordner 'php.ini'
if (Test-Path -LiteralPath $ini) {
foreach ($line in [System.IO.File]::ReadAllLines($ini)) {
if ($line -match '^\s*session\.save_path\s*=\s*"?([^"]+?)"?\s*$') {
$v = $Matches[1]; if ($v -match ';') { $v = ($v -split ';')[-1] }
if ($v) { $dirs.Add([Environment]::ExpandEnvironmentVariables($v.Trim())) }
}
}
}
}
$dirs.Add((Join-Path $env:windir 'Temp'))
$hits = New-Object System.Collections.Generic.List[object]
foreach ($dir in @($dirs | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -Unique)) {
foreach ($f in Get-ChildItem -LiteralPath $dir -Recurse -File -Filter 'sess_*' -ErrorAction SilentlyContinue) {
$m = Select-String -LiteralPath $f.FullName -Pattern $SessionShellPatterns -SimpleMatch -ErrorAction SilentlyContinue | Select-Object -First 1
if ($m -or $f.Name -eq 'sess_xqsuvwz3n97wu8j3emz1axuo5y') {
$hits.Add([pscustomobject]@{ Datei = $f.FullName; Geaendert = $f.LastWriteTime.ToString('yyyy-MM-dd HH:mm'); Merkmal = if ($m) { $m.Pattern } else { 'bekannter Dateiname' } })
}
}
}
return $hits
}
function Get-Advice {
# Short, actionable recommendation per installation.
# Umlauts are built from character codes so this file stays pure ASCII
# (safe for Windows PowerShell 5.1 regardless of file encoding).
param($Install)
$ae = [char]0xE4; $oe = [char]0xF6; $ue = [char]0xFC; $UEc = [char]0xDC; $ss = [char]0xDF
$a = New-Object System.Collections.Generic.List[string]
switch ($Install.Status) {
'VERWUNDBAR' {
$fix = ''
if ($Install.Version -match '^(\d+\.\d+)\.') { $b = $Matches[1]; if ($FixedVersions.ContainsKey($b)) { $fix = " $b.$($FixedVersions[$b])" } }
if ($Install.Nutzung -ne 'ungenutzt') { $a.Add("Dringend: auf die korrigierte Version$fix aktualisieren.") }
else { $a.Add("Auf die korrigierte Version$fix aktualisieren oder entfernen, falls nicht ben${oe}tigt.") }
}
'EOL' {
if ($Install.Nutzung -ne 'ungenutzt') { $a.Add("Dringend: keine offizielle Korrektur - auf PHP 8.3 oder 8.4 wechseln, bis dahin nicht aus dem Internet erreichbar betreiben.") }
else { $a.Add("Keine offizielle Korrektur. Nicht genutzt: entfernen, falls nicht ben${oe}tigt, sonst auf PHP 8.3 oder 8.4 wechseln.") }
}
'UNBEKANNT' { $a.Add("Version nicht bestimmbar: im PHP-Ordner php -v ausf${ue}hren und pr${ue}fen.") }
}
if ($Install.Upload -ne 'aus') { $a.Add("In der php.ini session.upload_progress.enabled = 0 setzen (CVE-2026-115560), danach Webserver neu starten.") }
if ($Install.Uebergangsschutz -and $Install.Status -ne 'GEPATCHT') { $a.Add("${UEc}bergangsschutz aktiv - bleibt eine Br${ue}cke, Update weiterhin n${oe}tig.") }
if ($a.Count -eq 0) { $a.Add("Keine Ma${ss}nahme n${oe}tig.") }
return ($a -join ' ')
}
function Get-StableHash {
param([string]$Text)
$sha = [System.Security.Cryptography.SHA256]::Create()
try { return ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Text))) -replace '-', '').Substring(0, 16) } finally { $sha.Dispose() }
}
function Send-ReportMail {
param([string]$Subject, [string]$HtmlBody, [string[]]$Attachments)
$m = $Config.Mail
$msg = New-Object System.Net.Mail.MailMessage
$tempDir = Join-Path $env:TEMP ('php2shell-mail-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
$ok = $false; $err = $null
try {
$msg.From = $m.Absender
foreach ($r in $m.Empfaenger) { $msg.To.Add($r) }
$msg.Subject = $Subject; $msg.SubjectEncoding = [Text.Encoding]::UTF8
$msg.Body = $HtmlBody; $msg.BodyEncoding = [Text.Encoding]::UTF8; $msg.IsBodyHtml = $true
foreach ($a in $Attachments | Where-Object { $_ -and (Test-Path -LiteralPath $_) }) {
$copy = Join-Path $tempDir ([IO.Path]::GetFileName($a)); Copy-Item -LiteralPath $a -Destination $copy -Force
$msg.Attachments.Add((New-Object System.Net.Mail.Attachment($copy)))
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$client = New-Object System.Net.Mail.SmtpClient($m.SmtpServer, [int]$m.SmtpPort)
$client.EnableSsl = [bool]$m.Verschluesselung
if ($m.Benutzer) { $client.Credentials = New-Object System.Net.NetworkCredential($m.Benutzer, $m.Kennwort) }
$client.Send($msg); $ok = $true
} catch {
$err = $_.Exception.Message; if ($_.Exception.InnerException) { $err += ' / ' + $_.Exception.InnerException.Message }
} finally {
$msg.Dispose(); Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
if ($ok) { Write-Log "Mail versendet an $($m.Empfaenger -join ', ')." } else { Write-Log "Mailversand fehlgeschlagen: $err" 'ERROR' }
}
# --- Main --------------------------------------------------------------------------
$exitCode = 3
try {
Write-Log "Pruefung $ScriptVersion ($Mode) fuer $($Config.Kunde) auf $Computer gestartet"
$state = $null
if (Test-Path -LiteralPath $StatePath) { try { $state = Get-Content -LiteralPath $StatePath -Raw | ConvertFrom-Json } catch { } }
$iis = Get-IisContext
$processes = @(Get-CimInstance Win32_Process -Filter "Name='php-cgi.exe' OR Name='php.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.ExecutablePath } | ForEach-Object { $_.ExecutablePath })
$apacheUp = [bool](Get-CimInstance Win32_Service -ErrorAction SilentlyContinue | Where-Object { $_.PathName -match 'httpd\.exe' -and $_.State -eq 'Running' })
if ($Mode -eq 'Full') {
$binaries = Find-PhpInPaths -Roots (Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' | ForEach-Object { $_.DeviceID + '\' })
} else {
$cand = New-Object System.Collections.Generic.List[string]
if ($state -and $state.BekanntePfade) { foreach ($p in $state.BekanntePfade) { $cand.Add($p) } }
foreach ($p in @($iis.Paths) + $processes) { if ($p) { $cand.Add($p) } }
foreach ($p in Find-PhpInPaths -Roots $CommonRoots) { $cand.Add($p) }
$binaries = @($cand | Where-Object { $_ -match '\\php(-cgi)?\.exe$' -and (Test-Path -LiteralPath $_) })
}
$binaries = @($binaries | Select-Object -Unique)
$installs = @(foreach ($grp in ($binaries | Group-Object { (Split-Path -Parent $_).ToLowerInvariant() })) {
$bins = @($grp.Group | Sort-Object); $dir = Split-Path -Parent $bins[0]
$ref = ($bins | Where-Object { $_ -like '*\php.exe' } | Select-Object -First 1); if (-not $ref) { $ref = $bins[0] }
$ver = Get-PhpVersion -Path $ref
$use = New-Object System.Collections.Generic.List[string]
foreach ($b in $bins) {
if ($iis.Paths | Where-Object { $_ -ieq $b }) { $use.Add('IIS') }
if ($processes | Where-Object { $_ -ieq $b }) { $use.Add('Prozess') }
}
if ($apacheUp -and (Get-ChildItem -LiteralPath $dir -Filter 'php*apache*.dll' -ErrorAction SilentlyContinue)) { $use.Add('Apache') }
$status = Get-VersionStatus -Version $ver; $upload = Get-UploadProgressSetting -PhpDir $dir; $inUse = $use.Count -gt 0
$sev = 0
if ($status -in 'VERWUNDBAR', 'EOL') { if ($inUse) { $sev = 2 } else { $sev = 1 } }
if ($status -eq 'UNBEKANNT' -or $upload -ne 'aus') { $sev = [Math]::Max($sev, 1) }
[pscustomobject]@{ Ordner = $dir; Programme = (($bins | ForEach-Object { Split-Path -Leaf $_ }) -join ', ')
Version = if ($ver) { $ver.Text } else { '?' }; Status = $status
Nutzung = if ($inUse) { (($use | Select-Object -Unique) -join '+') } else { 'ungenutzt' }
Upload = $upload; Uebergangsschutz = Test-GuardExtension -PhpDir $dir; Schweregrad = $sev; Empfehlung = '' }
})
foreach ($i in $installs) { $i.Empfehlung = Get-Advice -Install $i }
$allHits = @(Search-Indicators -Directories $iis.LogDirectories -Days $Config.LogTage)
$logHits = @($allHits | Where-Object { $_.Einstufung -eq 'Angriffsmerkmal' })
$infoHits = @($allHits | Where-Object { $_.Einstufung -eq 'Unklar' })
$sessHits = @(); if ($installs.Count -gt 0) { $sessHits = @(Search-SessionWebshells -Installs $installs) }
$overall = 0
if ($installs.Count -gt 0) { $overall = ($installs | Measure-Object -Property Schweregrad -Maximum).Maximum }
if ($infoHits.Count -gt 0 -and $overall -lt 1) { $overall = 1 }
if ($logHits.Count -gt 0 -or $sessHits.Count -gt 0) { $overall = 2 }
$statusText = @('OK', 'WARNUNG', 'KRITISCH')[$overall]
# Change detection (for mail)
$key = (@($installs | ForEach-Object { "$($_.Ordner)|$($_.Version)|$($_.Status)|$($_.Upload)" }) +
@($sessHits | ForEach-Object { "S|$($_.Datei)" }) + @($logHits | Select-Object -ExpandProperty Merkmal -Unique | ForEach-Object { "L|$_" }) | Sort-Object) -join ';'
$hash = Get-StableHash ("$statusText;$key")
$changed = (-not $state) -or ($state.LetzterHash -ne $hash)
# Short text
$lines = foreach ($i in $installs) {
$x = @(); if ($i.Upload -ne 'aus') { $x += 'UploadProgress an' }; if ($i.Uebergangsschutz) { $x += "$([char]0xDC)bergangsschutz aktiv" }
'{0} {1} {2} ({3}){4}' -f $i.Ordner, $i.Version, $i.Status, $i.Nutzung, $(if ($x) { '; ' + ($x -join ', ') } else { '' })
}
if ($sessHits.Count -gt 0) { $lines = @("WEBSHELL in Session-Datei: $($sessHits[0].Datei)") + @($lines) }
if ($logHits.Count -gt 0) { $lines = @("Angriffsmerkmale in Protokollen: $($logHits.Count)") + @($lines) }
$funde = if ($lines) { $lines -join ' | ' } else { 'Kein PHP gefunden' }
if ($funde.Length -gt 900) { $funde = $funde.Substring(0, 880) + ' ... (Details im Bericht)' }
# Report files
$reportFiles = @()
$html = $null
if ($Config.Ausgabe -ne 'ExitCode' -or $Config.Mail.Aktiv) {
$rows = ($installs | ForEach-Object { "<tr><td>$([Net.WebUtility]::HtmlEncode($_.Ordner))<br><small>$($_.Programme)</small></td><td>$($_.Version)</td><td style='font-weight:bold;color:$(@('#2e7d32','#ef6c00','#c62828')[$_.Schweregrad])'>$($_.Status)$(if ($_.Uebergangsschutz) { '<br><small>Übergangsschutz aktiv</small>' })</td><td>$($_.Nutzung)</td><td>$($_.Upload)</td><td>$([Net.WebUtility]::HtmlEncode($_.Empfehlung))</td></tr>" }) -join ''
$hitRows = ((@($logHits) + @($infoHits)) | Select-Object -First 50 | ForEach-Object { "<tr><td>$([Net.WebUtility]::HtmlEncode($_.Datei))</td><td>$($_.Zeile)</td><td>$([Net.WebUtility]::HtmlEncode($_.Merkmal))</td><td>$($_.Einstufung)</td></tr>" }) -join ''
$sessRows = ($sessHits | ForEach-Object { "<li>$([Net.WebUtility]::HtmlEncode($_.Datei)) (geändert $($_.Geaendert)) [$([Net.WebUtility]::HtmlEncode($_.Merkmal))]</li>" }) -join ''
$color = @('#2e7d32', '#ef6c00', '#c62828')[$overall]
$html = "<!DOCTYPE html><html lang='de'><head><meta charset='utf-8'><title>PHP2Shell $Computer</title><style>body{font-family:Segoe UI,Arial;font-size:14px;margin:24px}table{border-collapse:collapse;margin:8px 0}td,th{border:1px solid #ccc;padding:5px 8px;text-align:left;vertical-align:top}th{background:#f0f0f0}.b{color:#fff;background:$color;padding:3px 10px;border-radius:4px;font-weight:bold}</style></head><body>" +
"<h1>PHP2Shell-Prüfung $([Net.WebUtility]::HtmlEncode($Config.Kunde)) – $Computer</h1><p>$($Start.ToString('dd.MM.yyyy HH:mm')) · Modus $Mode · $ScriptVersion · Ergebnis: <span class='b'>$statusText</span></p>" +
"<h2>PHP-Installationen ($($installs.Count))</h2>" + $(if ($installs.Count) { "<table><tr><th>Ordner</th><th>Version</th><th>Status</th><th>Nutzung</th><th>Upload-Fortschritt</th><th>Empfehlung</th></tr>$rows</table>" } else { '<p>Kein PHP gefunden.</p>' }) +
"<h2>Webserver-Protokolle</h2><p>Angriffsmerkmale: $($logHits.Count) · unklar (untersuchen, nicht automatisch sperren): $($infoHits.Count)</p>" + $(if ($hitRows) { "<table><tr><th>Datei</th><th>Zeile</th><th>Merkmal</th><th>Einstufung</th></tr>$hitRows</table>" } else { '' }) +
"<h2>Session-Ordner</h2>" + $(if ($sessRows) { "<p style='color:#c62828'><b>Webshell-Merkmale gefunden – starker Hinweis auf eine Kompromittierung.</b> Nichts neu starten oder löschen, zuerst Beweise sichern.</p><ul>$sessRows</ul>" } else { '<p>Keine Webshell-Merkmale gefunden.</p>' }) +
"<p><small>Rein lesende Prüfung. Korrigierte Versionen: 8.2.35, 8.3.36, 8.4.27, 8.5.12; bis 8.1 keine offizielle Korrektur. Hintergrund: codekunst-systems.com/aktuell/php2shell-statusuebersicht</small></p></body></html>"
}
if ($Config.Ausgabe -in 'Lokal', 'Share') {
$target = if ($Config.Ausgabe -eq 'Lokal') { $Config.Ablageordner } else { Join-Path $Config.Ablageordner $Computer }
try {
if (-not (Test-Path -LiteralPath $target)) { New-Item -ItemType Directory -Path $target -Force | Out-Null }
$base = Join-Path $target "PHP2Shell_$Computer`_$Stamp"
$html | Set-Content -LiteralPath "$base.html" -Encoding UTF8
[pscustomobject]@{ Kunde = $Config.Kunde; Computer = $Computer; Zeitpunkt = $Start.ToString('s'); Modus = $Mode; Version = $ScriptVersion
Status = $statusText; Installationen = $installs; Protokolltreffer = $logHits; UnklareTreffer = $infoHits; SessionTreffer = $sessHits } |
ConvertTo-Json -Depth 5 | Set-Content -LiteralPath "$base.json" -Encoding UTF8
$reportFiles = @("$base.html", "$base.json")
Get-ChildItem -LiteralPath $target -File -Filter 'PHP2Shell_*' -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$Config.AufbewahrungTage) } | Remove-Item -Force -ErrorAction SilentlyContinue
} catch { Write-Log "Bericht konnte nicht abgelegt werden ($target): $($_.Exception.Message)" 'ERROR' }
}
# Mail
if ($Config.Mail.Aktiv) {
$threshold = switch ($Config.Mail.SendenAb) { 'Immer' { 0 } 'Warnung' { 1 } default { 2 } }
if ($overall -ge $threshold -and ($changed -or -not $Config.Mail.NurBeiAenderung)) {
Send-ReportMail -Subject "[PHP2Shell] $statusText - $($Config.Kunde) - $Computer" -HtmlBody $html -Attachments $reportFiles
}
}
# State
$known = @($binaries); if ($state -and $state.BekanntePfade -and $Mode -eq 'Quick') { $known = @($known + $state.BekanntePfade | Select-Object -Unique) }
[pscustomobject]@{ LetzterStatus = $statusText; LetzterHash = $hash; BekanntePfade = $known; Zeitpunkt = $Start.ToString('s') } |
ConvertTo-Json | Set-Content -LiteralPath $StatePath -Encoding UTF8
# Event log
try {
if (-not [Diagnostics.EventLog]::SourceExists('PHP2ShellCheck')) { New-EventLog -LogName Application -Source 'PHP2ShellCheck' }
Write-EventLog -LogName Application -Source 'PHP2ShellCheck' -EventId @(1000, 2000, 3000)[$overall] -EntryType @('Information', 'Warning', 'Error')[$overall] `
-Message "PHP2Shell-Monitoring ($($Config.Kunde)): $statusText. $funde"
} catch { Write-Log "Ereignisprotokoll nicht beschreibbar: $($_.Exception.Message)" 'WARN' }
Write-Log "Ergebnis: $statusText (Installationen $($installs.Count), Protokoll $($logHits.Count), Session $($sessHits.Count), unklar $($infoHits.Count))"
Write-Output ("PHP2SHELL {0} | {1} | {2} | PHP: {3} | Treffer: {4}" -f $statusText, $Config.Kunde, $Computer, $installs.Count, ($logHits.Count + $sessHits.Count))
$exitCode = $overall
}
catch {
Write-Log "Abbruch: $($_.Exception.Message)" 'ERROR'
Write-Output ("PHP2SHELL FEHLER | {0} | {1} | {2}" -f $Config.Kunde, $env:COMPUTERNAME, $_.Exception.Message)
$exitCode = 3
}
exit $exitCode
'@
# =============================================================================
# GENERATOR
# =============================================================================
function Read-Choice {
param([string]$Question, [string[]]$Options, [int]$Default = 1)
Write-Host ''
Write-Host $Question -ForegroundColor Cyan
for ($n = 0; $n -lt $Options.Count; $n++) { Write-Host (" [{0}] {1}" -f ($n + 1), $Options[$n]) }
while ($true) {
$a = Read-Host "Auswahl [$Default]"
if (-not $a) { return $Default }
if ($a -match '^\d+$' -and [int]$a -ge 1 -and [int]$a -le $Options.Count) { return [int]$a }
Write-Host 'Bitte eine der angezeigten Zahlen eingeben.' -ForegroundColor Yellow
}
}
function Read-Text {
param([string]$Question, [string]$Default = '', [switch]$Required)
while ($true) {
$a = Read-Host ($(if ($Default) { "$Question [$Default]" } else { $Question }))
if (-not $a) { $a = $Default }
if ($a -or -not $Required) { return $a }
Write-Host 'Eingabe erforderlich.' -ForegroundColor Yellow
}
}
function Read-YesNo {
param([string]$Question, [bool]$Default = $false)
$d = if ($Default) { 'J/n' } else { 'j/N' }
$a = Read-Host "$Question [$d]"
if (-not $a) { return $Default }
return ($a -match '^(j|ja|y|yes)$')
}
function ConvertTo-PsLiteral {
# Single-quoted PowerShell literal
param([string]$Value)
return "'" + ($Value -replace "'", "''") + "'"
}
function New-ConfigBlock {
param($C)
$to = ($C.MailEmpfaenger | ForEach-Object { ConvertTo-PsLiteral $_ }) -join ', '
$lines = @(
'# ============================================================================='
"# KONFIGURATION - erzeugt am $(Get-Date -Format 'dd.MM.yyyy HH:mm') mit Generator $GeneratorVersion"
'# Aenderungen am besten ueber den Generator, nicht von Hand.'
'# ============================================================================='
'$Config = @{'
" Kunde = $(ConvertTo-PsLiteral $C.Kunde)"
" Ausgabe = $(ConvertTo-PsLiteral $C.Ausgabe) # ExitCode | Lokal | Share"
" Ablageordner = $(ConvertTo-PsLiteral $C.Ablageordner)"
" AufbewahrungTage = $($C.AufbewahrungTage)"
" StandardModus = $(ConvertTo-PsLiteral $C.StandardModus)"
" LogTage = $($C.LogTage)"
' Mail = @{'
" Aktiv = `$$($C.MailAktiv.ToString().ToLower())"
" Empfaenger = @($to)"
" Absender = $(ConvertTo-PsLiteral $C.MailAbsender)"
" SmtpServer = $(ConvertTo-PsLiteral $C.SmtpServer)"
" SmtpPort = $($C.SmtpPort)"
" Verschluesselung = `$$($C.Verschluesselung.ToString().ToLower())"
" Benutzer = $(ConvertTo-PsLiteral $C.SmtpBenutzer)"
" Kennwort = $(ConvertTo-PsLiteral $C.SmtpKennwort) # Klartext! Nur Sende-Postfach, besser Relay ohne Anmeldung"
" SendenAb = $(ConvertTo-PsLiteral $C.SendenAb) # Immer | Warnung | Kritisch"
" NurBeiAenderung = `$$($C.NurBeiAenderung.ToString().ToLower())"
' }'
'}'
)
return ($lines -join "`r`n")
}
function New-MonitorScript {
param($C, [string]$OutDir)
$safe = ($C.Kunde -replace '[^A-Za-z0-9_-]', '_')
$scriptPath = Join-Path $OutDir "PHP2Shell-Monitor_$safe.ps1"
$content = $Template.Replace('#__KONFIGURATION__#', (New-ConfigBlock $C))
# UTF-8 with BOM so that Windows PowerShell 5.1 reads it correctly
[IO.File]::WriteAllText($scriptPath, ($content -replace "`r?`n", "`r`n"), (New-Object Text.UTF8Encoding($true)))
return $scriptPath
}
# --- Interactive part --------------------------------------------------------------
if ($MyInvocation.InvocationName -ne '.') {
Write-Host "PHP2Shell-Monitoring - Skriptgenerator $GeneratorVersion" -ForegroundColor Green
Write-Host 'Erzeugt ein konfiguriertes Pruefskript zur Verteilung (RMM, GPO, geplante Aufgabe).'
Write-Host 'Ohne Gewaehr - vor dem Rollout auf einem Testrechner pruefen.'
$C = [ordered]@{
Kunde = ''; Ausgabe = 'ExitCode'; Ablageordner = 'C:\PHP2ShellCheck'; AufbewahrungTage = 30
StandardModus = 'Quick'; LogTage = 30
MailAktiv = $false; MailEmpfaenger = @(); MailAbsender = ''; SmtpServer = ''; SmtpPort = 25
Verschluesselung = $false; SmtpBenutzer = ''; SmtpKennwort = ''; SendenAb = 'Kritisch'; NurBeiAenderung = $true
}
Write-Host ''
$C.Kunde = Read-Text 'Kunde (Name oder Kuerzel, wird Teil des Dateinamens)' -Required
switch (Read-Choice 'Wo sollen die Berichte abgelegt werden?' @(
'Nur Exit-Code (keine Berichtsdateien)',
'Lokal auf dem Zielrechner (C:\PHP2ShellCheck)',
'Auf einer Freigabe (je Rechner ein Unterordner)') 2) {
1 { $C.Ausgabe = 'ExitCode' }
2 { $C.Ausgabe = 'Lokal'; $C.Ablageordner = Read-Text 'Lokaler Ordner' 'C:\PHP2ShellCheck' }
3 {
$C.Ausgabe = 'Share'
while ($true) {
$C.Ablageordner = Read-Text 'Freigabe (UNC-Pfad, z. B. \\fileserver\IT$\PHP2Shell)' -Required
if ($C.Ablageordner -notmatch '^\\\\[^\\]+\\[^\\]+') { Write-Host 'Bitte einen UNC-Pfad angeben (\\Server\Freigabe\...).' -ForegroundColor Yellow; continue }
if (Test-Path -LiteralPath $C.Ablageordner) {
$probe = Join-Path $C.Ablageordner ("schreibtest_" + [guid]::NewGuid().ToString('N') + '.tmp')
try { Set-Content -LiteralPath $probe -Value 'test'; Remove-Item -LiteralPath $probe -Force; Write-Host ' Schreibtest mit Ihrem Konto erfolgreich.' -ForegroundColor Green }
catch { Write-Host " Schreibtest mit Ihrem Konto fehlgeschlagen: $($_.Exception.Message)" -ForegroundColor Yellow }
} else {
Write-Host ' Freigabe von hier aus nicht erreichbar - bitte pruefen.' -ForegroundColor Yellow
if (-not (Read-YesNo 'Trotzdem verwenden?')) { continue }
}
Write-Host ' Hinweis: Laeuft das Skript als SYSTEM, schreibt es mit dem Computerkonto.' -ForegroundColor Yellow
Write-Host ' Freigabe- und NTFS-Rechte daher fuer "Domaenencomputer" (Aendern) setzen.' -ForegroundColor Yellow
break
}
}
}
if ($C.Ausgabe -ne 'ExitCode') {
$d = Read-Text 'Berichte wie viele Tage aufbewahren' '30'
if ($d -match '^\d+$') { $C.AufbewahrungTage = [int]$d }
}
$C.MailAktiv = Read-YesNo 'Bericht per Mail senden?'
if ($C.MailAktiv) {
$C.MailEmpfaenger = @((Read-Text 'Empfaenger (mehrere mit Komma)' -Required) -split '\s*,\s*' | Where-Object { $_ })
$C.MailAbsender = Read-Text 'Absenderadresse' -Required
$C.SmtpServer = Read-Text 'SMTP-Server (interner Relay empfohlen)' -Required
$p = Read-Text 'Port' '25'; if ($p -match '^\d+$') { $C.SmtpPort = [int]$p }
$C.Verschluesselung = Read-YesNo 'STARTTLS/TLS verwenden?' ($C.SmtpPort -eq 587)
$C.SmtpBenutzer = Read-Text 'SMTP-Benutzer (leer = ohne Anmeldung, empfohlen)'
if ($C.SmtpBenutzer) {
Write-Host ' ACHTUNG: Das Kennwort steht im verteilten Skript im Klartext.' -ForegroundColor Yellow
Write-Host ' Nur ein eigenes Postfach verwenden, das ausschliesslich senden darf.' -ForegroundColor Yellow
$sec = Read-Host 'Kennwort' -AsSecureString
$C.SmtpKennwort = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec))
}
switch (Read-Choice 'Ab welchem Ergebnis senden?' @('Immer', 'Ab Warnung', 'Nur bei kritischem Ergebnis') 3) {
1 { $C.SendenAb = 'Immer' } 2 { $C.SendenAb = 'Warnung' } 3 { $C.SendenAb = 'Kritisch' }
}
$C.NurBeiAenderung = Read-YesNo 'Nur senden, wenn sich das Ergebnis geaendert hat (verhindert Mailflut)?' $true
try {
$tcp = New-Object Net.Sockets.TcpClient
$iar = $tcp.BeginConnect($C.SmtpServer, $C.SmtpPort, $null, $null)
if ($iar.AsyncWaitHandle.WaitOne(3000) -and $tcp.Connected) { Write-Host " $($C.SmtpServer):$($C.SmtpPort) von hier erreichbar." -ForegroundColor Green }
else { Write-Host " $($C.SmtpServer):$($C.SmtpPort) von hier nicht erreichbar - von den Zielrechnern aus pruefen." -ForegroundColor Yellow }
$tcp.Close()
} catch { Write-Host ' Verbindungstest nicht moeglich.' -ForegroundColor Yellow }
}
switch (Read-Choice 'Standardmodus, wenn beim Aufruf nichts angegeben ist?' @('Quick (taeglich, schnell)', 'Full (alle Festplatten)') 1) {
1 { $C.StandardModus = 'Quick' } 2 { $C.StandardModus = 'Full' }
}
if ((Read-Choice 'Konfiguration erzeugen?' @('Ja', 'Abbrechen') 1) -eq 2) { Write-Host 'Abgebrochen.'; return }
$path = New-MonitorScript -C $C -OutDir $Ausgabeordner
# Optional signing
$cert = @(Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert -ErrorAction SilentlyContinue | Where-Object { $_.NotAfter -gt (Get-Date) }) | Select-Object -First 1
if ($cert -and (Read-YesNo "Skript mit Zertifikat '$($cert.Subject)' signieren?" $true)) {
$sig = Set-AuthenticodeSignature -FilePath $path -Certificate $cert -TimestampServer 'http://timestamp.digicert.com'
Write-Host " Signatur: $($sig.Status)" -ForegroundColor Green
}
# Deployment notes
$name = Split-Path -Leaf $path
$notes = @(
"PHP2Shell-Monitoring fuer $($C.Kunde) - erzeugt am $(Get-Date -Format 'dd.MM.yyyy HH:mm')"
''
"Skript: $name"
"Ausgabe: $($C.Ausgabe) $(if ($C.Ausgabe -ne 'ExitCode') { '-> ' + $C.Ablageordner })"
"Mail: $(if ($C.MailAktiv) { 'ja, an ' + ($C.MailEmpfaenger -join ', ') + ', ab ' + $C.SendenAb + $(if ($C.NurBeiAenderung) { ', nur bei Aenderung' }) } else { 'nein' })"
''
'Aufruf (als SYSTEM, z. B. ueber RMM oder geplante Aufgabe):'
" powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\$name -Mode Full (woechentlich)"
" powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\$name -Mode Quick (taeglich)"
''
'Einbindung ins Monitoring:'
' Exit-Code 0 OK, 1 WARNUNG, 2 KRITISCH, 3 FEHLER'
' Ausgabe PHP2SHELL <STATUS> | <Kunde> | <Rechner> | PHP: n | Treffer: n'
' Hinweis: Manche Systeme werten jeden Exit-Code ungleich 0 als fehlgeschlagene Aufgabe.'
' Dann den Status aus der Ausgabezeile oder dem Ereignisprotokoll auswerten.'
'Ereignisprotokoll: Anwendung, Quelle PHP2ShellCheck, ID 1000/2000/3000'
'Zustand und Protokoll auf dem Zielrechner: C:\ProgramData\PHP2ShellCheck'
''
'Vor dem Rollout auf einem Testrechner pruefen (Setup-PHP2ShellTestumgebung.ps1).'
)
if ($C.SmtpKennwort) { $notes += ''; $notes += 'ACHTUNG: Das Skript enthaelt ein SMTP-Kennwort im Klartext. Ablage und Verteilweg entsprechend schuetzen.' }
if ($C.Ausgabe -eq 'Share') { $notes += ''; $notes += 'Freigabe: Schreibrechte (Aendern) fuer Domaenencomputer erforderlich, wenn als SYSTEM ausgefuehrt.' }
$notesPath = [IO.Path]::ChangeExtension($path, $null).TrimEnd('.') + '_Verteilung.txt'
$notes | Set-Content -LiteralPath $notesPath -Encoding UTF8
Write-Host ''
Write-Host 'Fertig:' -ForegroundColor Green
Write-Host " $path"
Write-Host " $notesPath"
}
Den gesamten Inhalt mit der Kopieren-Schaltfläche übernehmen, in einem Editor einfügen und als New-PHP2ShellMonitorScript.ps1 speichern. Das Skript enthält nur ASCII-Zeichen. Es läuft auf dem Rechner des Administrators und erzeugt das zu verteilende Skript PHP2Shell-Monitor_<Kunde>.ps1 samt Hinweisdatei zur Verteilung.
| Kanal | Inhalt |
|---|---|
| Exit-Code | 0 = OK, 1 = WARNUNG, 2 = KRITISCH, 3 = FEHLER |
| Ergebniszeile | PHP2SHELL <STATUS> | <Kunde> | <Rechner> | PHP: n | Treffer: n |
| Ereignisprotokoll | Anwendung, Quelle PHP2ShellCheck, ID 1000 / 2000 / 3000 |
| Bericht (optional) | HTML und JSON mit Empfehlung pro Installation, lokal oder auf einer Freigabe (je Rechner ein Unterordner) |
Drei Punkte für die Verteilung
Läuft das Skript als SYSTEM, schreibt es mit dem Computerkonto auf die Freigabe – Freigabe- und NTFS-Rechte deshalb für „Domänencomputer“ setzen. Manche Monitoring-Systeme werten jeden Exit-Code ungleich 0 als fehlgeschlagene Aufgabe; dann die Ergebniszeile oder das Ereignisprotokoll auswerten. Ein SMTP-Kennwort stünde im verteilten Skript im Klartext – besser einen internen Relay ohne Anmeldung nutzen.
Bitte beachten
Bereitgestellt ohne Gewähr, Nutzung auf eigene Verantwortung. Vor dem produktiven Einsatz mit der Testumgebung prüfen. Die Skripte ersetzen keine forensische Untersuchung: Bei Hinweisen auf eine Kompromittierung zuerst Beweise sichern und Fachleute hinzuziehen.
Unterstützung bei der Prüfung?
Wir prüfen Ihre Windows- und Linux-Server, bewerten die Funde und kümmern uns um Updates, Absicherung und Überwachung – mit Bericht als Nachweis.
Kontakt aufnehmen