Skip to content

Remove PHP Install (Package)

A Tanium package that deletes an old PHP folder from a Windows computer. A common example is C:\Program Files (x86)\PHP\v7.1, often left behind by the old Microsoft Web Platform Installer. The package also removes PHP from the installed programs list and from the system PATH, so inventory and vulnerability scans stop reporting it.

It deletes only the one folder you type in. It doesn't look at the PHP version. To protect other PHP copies, it only deletes folders directly inside C:\Program Files (x86)\PHP or C:\Program Files\PHP.

Test status

Tested in a lab through Tanium. The script correctly refused to remove PHP while IIS was still using it, and correctly refused a folder outside Program Files\PHP. The removal itself has not been tested yet. See Test results.


What the script does

First it checks that it's safe to delete. If any check fails, it stops and changes nothing:

  1. Is it the right kind of folder? The folder must be directly inside C:\Program Files (x86)\PHP or C:\Program Files\PHP, and must contain php.exe.
  2. Is a website using it? If IIS (the Windows web server) is set up to run PHP from this folder, the script stops. Deleting the folder would break those websites. Force does not skip this check. Remove the IIS settings first with Remove IIS PHP Handler.
  3. Is PHP running right now? If a program is running from the folder, the script stops. If you set Force to Yes, it closes those programs, waits 5 seconds, and checks again. If they started again on their own, something (a service or scheduled task) keeps launching them, and the script stops.

If everything is safe, it:

  1. Runs PHP's uninstaller, if PHP was installed with one (an MSI installer).
  2. Deletes the folder.
  3. Removes PHP from the installed programs list, if it's still there.
  4. Removes the folder from the system PATH.

Every step is written to the Tanium action log and to C:\Windows\Temp\Remove-PHPInstall.log on the computer.


Exit codes

The exit code tells you how the action ended. You'll find it at the end of the action log.

Exit code Meaning
0 The folder was removed, or it was already gone.
1 Something went wrong. Look for lines with [ERROR] in the action log.
2 The script stopped on purpose because it wasn't safe to delete. IIS uses the folder, PHP is running (or started again after Force), or the folder isn't in an allowed place. Nothing was changed.

Create the package

  1. Save the script at the bottom of this page as Remove-PHPInstall.ps1.
  2. In the Tanium Console, go to Administration > Content > Packages.
  3. Click Create Package and fill in:
Field Value
Package Name <prefix> - Remove PHP Install (Windows)
Display Name <prefix> - Remove PHP Install (Windows)
Description Deletes one old PHP folder inside Program Files\PHP or Program Files (x86)\PHP, and removes it from the installed programs list and PATH. Stops if IIS uses the folder. Force = Yes closes PHP programs running from the folder. Exit 0 removed or already gone, 1 failed, 2 stopped because it was not safe.
Content Set The content set your team uses for custom content
Command cmd.exe /d /c powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -NoProfile -File Remove-PHPInstall.ps1 "$1" "$2"
Command Timeout 10 minutes
Download Timeout 5 minutes
Ignore Action Lock Unchecked
Launch this package command in a process group Checked. If the uninstaller hangs, Tanium closes it when the time runs out
Files Click Add File, choose Local File, upload Remove-PHPInstall.ps1
  1. Add the parameters:
# Type Label Values
1 Text Input PHP install folder Default value C:\Program Files (x86)\PHP\v7.1. Copy the folder from Comply: open the CVE, select a test, and look at path under Actual Values
2 Drop-Down List Force kill running PHP processes No first, then Yes
  1. Click Save.

Failed to create process: Error code 0x00000057

If an action fails with this before the script runs, the Command field starts with a space or tab, usually from copying. Edit the package, clear the field, and paste the command again with nothing before cmd.exe.


Deploy

  1. First, ask the IIS - PHP Usage sensor about the computer. If a website still uses this PHP folder and people are visiting it, stop here and talk to the website owner.
  2. In Interact, find the computer, for example:
    Get Computer Name from all machines with Computer Name contains "<server name>"
    
  3. Select the computer, click Deploy Action, and choose <prefix> - Remove PHP Install (Windows).
  4. Type the folder. Leave Force at No the first time.
  5. Click Show preview to continue, then Deploy Action.
  6. To see what happened, go to Administration > Actions > Action History, open the action, select the computer, and read the action log.

Example output (illustrative)

This run stopped because IIS still used the folder:

2026-11-05T10:27:59 [INFO] Start. Target folder: C:\Program Files (x86)\PHP\v7.1 | Force: True
2026-11-05T10:27:59 [INFO] php.exe version: 7.1.33
2026-11-05T10:27:59 [ERROR] Blocked: IIS applicationHost.config references 'C:\Program Files (x86)\PHP\v7.1'. Remove the FastCGI application and handler mapping first. Force does not override this.

Things to know

  • Force can break things. If a scheduled task or script was running that PHP, it fails the next time it runs, because the folder is gone. Tell its owner first.
  • Only the folder you type. PHP in other places, such as C:\PHP or D:\php, is refused. This stops a typo from deleting the PHP a website still needs.
  • Don't use this for current PHP. If the PHP version still gets security fixes, update it instead. See PHP on IIS.

Test results

What was tested Expected Result
Script checked for syntax errors (Windows PowerShell 5.1) No errors Pass
IIS uses the folder, Force = Yes (earlier version) Stops with exit 2, nothing changed Pass
IIS uses the folder, folder left blank, Force = No Uses the default folder, stops with exit 2 Pass
Folder outside Program Files\PHP (C:\PHP) Stops with exit 2 Pass. Tanium sent the folder as C%3a%5cPHP and the script read it correctly as C:\PHP
Folder not used by IIS, PHP not running Folder, installed programs entry, and PATH entry removed, exit 0 Not tested yet
PHP running from the folder, Force = No Stops with exit 2 Not tested yet
PHP running from the folder, Force = Yes PHP closed, folder removed, exit 0 Not tested yet
Folder with spaces and brackets typed in Tanium Read correctly Not tested yet

Script

<#
.SYNOPSIS
    Removes a legacy PHP install folder from a Windows endpoint. Built for a Tanium package.

.DESCRIPTION
    Usage: Remove-PHPInstall.ps1 "<PHP folder>" "<Force: Yes|No>"
    Default folder: C:\Program Files (x86)\PHP\v7.1
    Default Force:  No

    Safety checks, in order. Any failure stops the script before anything is changed:
      1. Folder must sit directly under C:\Program Files (x86)\PHP or C:\Program Files\PHP
         and must contain php.exe.
      2. IIS applicationHost.config must not reference the folder (FastCGI app or handler mapping).
         Force does NOT override this check.
      3. No running process may be executing from the folder.
         Force = Yes kills those processes, waits, and re-checks. If they come back
         (restarted by a service or IIS), the script stops.

    Removal:
      - Runs the MSI uninstall if a matching uninstall entry exists.
      - Deletes the folder.
      - Removes any leftover uninstall entry so inventory stops reporting it.
      - Removes the folder from the machine PATH.

.NOTES
    Exit codes:
      0  Removed, or folder was not present
      1  Failed
      2  Blocked: in use, referenced by IIS, processes restarted after kill, or path failed validation
    Log: C:\Windows\Temp\Remove-PHPInstall.log (also written to stdout)
#>

param(
    [string]$PhpDir,
    [string]$Force
)

$ErrorActionPreference = 'Stop'
$DefaultDir = 'C:\Program Files (x86)\PHP\v7.1'
$LogFile    = Join-Path $env:windir 'Temp\Remove-PHPInstall.log'

function Write-Log {
    param([string]$Message, [string]$Level = 'INFO')
    $line = '{0} [{1}] {2}' -f (Get-Date -Format 's'), $Level, $Message
    Write-Output $line
    Add-Content -Path $LogFile -Value $line -ErrorAction SilentlyContinue
}

function Get-PhpProcesses {
    param([string]$Dir)
    @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {
        $_.ExecutablePath -and $_.ExecutablePath.StartsWith("$Dir\", [StringComparison]::OrdinalIgnoreCase)
    })
}

try {
    # Tanium passes parameters URL-encoded (a space arrives as %20), so decode them first.
    $PhpDir = [System.Uri]::UnescapeDataString([string]$PhpDir)
    $Force  = [System.Uri]::UnescapeDataString([string]$Force)
    if ([string]::IsNullOrWhiteSpace($PhpDir)) { $PhpDir = $DefaultDir }
    $PhpDir  = $PhpDir.Trim().Trim('"').TrimEnd('\')
    $forceOn = ([string]$Force).Trim() -match '^(yes|true|1)$'
    Write-Log "Start. Target folder: $PhpDir | Force: $forceOn"

    # --- 1. Path validation -------------------------------------------------
    $allowedRoots = @("$env:SystemDrive\Program Files (x86)\PHP", "$env:SystemDrive\Program Files\PHP")
    $parent = Split-Path -Path $PhpDir -Parent
    if ($allowedRoots -notcontains $parent) {
        Write-Log "Blocked: folder must be directly under one of: $($allowedRoots -join ', ')" 'ERROR'
        exit 2
    }

    if (-not (Test-Path -LiteralPath $PhpDir)) {
        Write-Log 'Folder not present. Nothing to do.'
        exit 0
    }

    $phpExe = Join-Path $PhpDir 'php.exe'
    if (-not (Test-Path -LiteralPath $phpExe)) {
        Write-Log "Blocked: $phpExe not found, folder does not look like a PHP install." 'ERROR'
        exit 2
    }

    $fileVer = (Get-Item -LiteralPath $phpExe).VersionInfo.FileVersion
    $majMin  = (($fileVer -split '\.')[0..1]) -join '.'
    Write-Log "php.exe version: $fileVer"

    # --- 2. IIS reference check (Force does not override) -------------------
    # Use Sysnative when running as a 32-bit process so System32 is not redirected to SysWOW64.
    $sysDir  = if (Test-Path "$env:windir\Sysnative") { "$env:windir\Sysnative" } else { "$env:windir\System32" }
    $appHost = Join-Path $sysDir 'inetsrv\config\applicationHost.config'
    if (Test-Path -LiteralPath $appHost) {
        $cfg = Get-Content -LiteralPath $appHost -Raw
        $envForm = $PhpDir -ireplace [regex]::Escape("$env:SystemDrive\Program Files (x86)"), '%ProgramFiles(x86)%'
        $envForm = $envForm -ireplace [regex]::Escape("$env:SystemDrive\Program Files"), '%ProgramFiles%'
        foreach ($needle in @($PhpDir, $envForm) | Select-Object -Unique) {
            if ($cfg.IndexOf($needle, [StringComparison]::OrdinalIgnoreCase) -ge 0) {
                Write-Log "Blocked: IIS applicationHost.config references '$needle'. Remove the FastCGI application and handler mapping first. Force does not override this." 'ERROR'
                exit 2
            }
        }
        Write-Log 'IIS present, no reference to the folder.'
    } else {
        Write-Log 'IIS not installed.'
    }

    # --- 3. In-use check, with optional kill --------------------------------
    $procs = Get-PhpProcesses -Dir $PhpDir
    if ($procs.Count -gt 0) {
        foreach ($p in $procs) { Write-Log "In use: PID $($p.ProcessId) $($p.ExecutablePath)" 'WARN' }

        if (-not $forceOn) {
            Write-Log 'Blocked: processes are running from the folder. Rerun with Force = Yes to kill them.' 'ERROR'
            exit 2
        }

        foreach ($p in $procs) {
            try {
                Stop-Process -Id $p.ProcessId -Force -ErrorAction Stop
                Write-Log "Killed PID $($p.ProcessId) $($p.ExecutablePath)"
            } catch {
                Write-Log "Could not kill PID $($p.ProcessId): $($_.Exception.Message)" 'WARN'
            }
        }

        Start-Sleep -Seconds 5
        $procs = Get-PhpProcesses -Dir $PhpDir
        if ($procs.Count -gt 0) {
            foreach ($p in $procs) { Write-Log "Still running after kill: PID $($p.ProcessId) $($p.ExecutablePath)" 'WARN' }
            Write-Log 'Blocked: processes restarted or could not be killed. A service or scheduled job is likely relaunching them.' 'ERROR'
            exit 2
        }
        Write-Log 'All processes from the folder stopped.'
    } else {
        Write-Log 'No running processes from the folder.'
    }

    # --- 4. Find uninstall entries (64-bit view sees both hives) -------------
    $hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine', 'Registry64')
    $uninstRoots = @(
        'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
        'SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
    )
    $entries = @()
    foreach ($root in $uninstRoots) {
        $rk = $hklm.OpenSubKey($root)
        if (-not $rk) { continue }
        foreach ($name in $rk.GetSubKeyNames()) {
            $sk = $rk.OpenSubKey($name)
            if (-not $sk) { continue }
            $dn  = [string]$sk.GetValue('DisplayName')
            $dv  = [string]$sk.GetValue('DisplayVersion')
            $loc = ([string]$sk.GetValue('InstallLocation')).TrimEnd('\')
            $byLoc  = $loc -and ($loc -ieq $PhpDir)
            $byName = ($dn -like 'PHP*') -and ($dv -like "$majMin*")
            if ($byLoc -or $byName) {
                $entries += [pscustomobject]@{
                    Root      = $root
                    Key       = $name
                    Name      = $dn
                    Version   = $dv
                    Uninstall = [string]$sk.GetValue('UninstallString')
                }
            }
            $sk.Close()
        }
        $rk.Close()
    }
    Write-Log "Matching uninstall entries: $($entries.Count)"

    # --- 5. MSI uninstall where available -----------------------------------
    foreach ($e in $entries) {
        Write-Log "Entry: '$($e.Name)' $($e.Version) [$($e.Root)\$($e.Key)]"
        if ($e.Uninstall -match 'msiexec' -and $e.Uninstall -match '\{[0-9A-Fa-f-]{36}\}') {
            $guid = $Matches[0]
            Write-Log "Running msiexec /x $guid /qn /norestart"
            $p = Start-Process -FilePath 'msiexec.exe' -ArgumentList "/x $guid /qn /norestart" -Wait -PassThru
            if ($p.ExitCode -in 0, 1605, 3010) {
                Write-Log "msiexec exit code $($p.ExitCode) (OK)"
            } else {
                Write-Log "msiexec exit code $($p.ExitCode). Continuing with folder removal." 'WARN'
            }
        } else {
            Write-Log 'Not an MSI entry. Folder will be removed directly.'
        }
    }

    # --- 6. Delete folder ---------------------------------------------------
    if (Test-Path -LiteralPath $PhpDir) {
        Write-Log 'Deleting folder.'
        Remove-Item -LiteralPath $PhpDir -Recurse -Force
    }
    if (Test-Path -LiteralPath $PhpDir) {
        Write-Log 'Folder still present after delete.' 'ERROR'
        exit 1
    }
    Write-Log 'Folder removed.'

    # --- 7. Remove leftover uninstall entries -------------------------------
    foreach ($e in $entries) {
        $rk = $hklm.OpenSubKey($e.Root, $true)
        if ($rk -and ($rk.GetSubKeyNames() -contains $e.Key)) {
            $rk.DeleteSubKeyTree($e.Key, $false)
            Write-Log "Removed leftover uninstall key $($e.Root)\$($e.Key)"
        }
        if ($rk) { $rk.Close() }
    }

    # --- 8. Clean machine PATH (keeps REG_EXPAND_SZ and %vars% intact) ------
    $envKey = $hklm.OpenSubKey('SYSTEM\CurrentControlSet\Control\Session Manager\Environment', $true)
    $raw    = [string]$envKey.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
    $parts  = @($raw -split ';' | Where-Object { $_ -ne '' })
    $keep   = @($parts | Where-Object {
        [Environment]::ExpandEnvironmentVariables($_).TrimEnd('\') -ine $PhpDir
    })
    if ($keep.Count -ne $parts.Count) {
        $envKey.SetValue('Path', ($keep -join ';'), [Microsoft.Win32.RegistryValueKind]::ExpandString)
        Write-Log "Removed $($parts.Count - $keep.Count) PATH entr(ies) pointing to the folder."
    } else {
        Write-Log 'No PATH entries to remove.'
    }
    $envKey.Close()
    $hklm.Close()

    Write-Log 'Done. Exit 0.'
    exit 0
}
catch {
    Write-Log "Failed: $($_.Exception.Message)" 'ERROR'
    exit 1
}