Why we built this lab
The primary goal was to validate an existing IBM QRadar correlation rule intended to identify web-server processes spawning command interpreters or common system utilities—a useful signal for webshells and web application remote code execution.
PHP2Shell provided a realistic scenario. A Linux Apache/mod_php proof of concept had already succeeded in a separate lab, prompting a question: could the same behaviour be reproduced against Windows IIS with PHP FastCGI, and would the SOC rule fire?
Lab environment and architecture
The lab VMs were hosted in a Proxmox environment. A separate Linux Apache/mod_php target was used for the previously successful Linux PoC; it was not the Windows IIS host shown above.
Windows web-server configuration
| Component | Observed configuration |
|---|---|
| Operating system | Windows Server 2022 Core |
| Web server | Microsoft IIS 10; application pool DefaultAppPool |
| PHP runtime | PHP 8.4.26, C:\PHP\php-cgi.exe, SAPI cgi-fcgi |
| Configuration file | C:\PHP\php.ini |
| PHP error log | C:\PHP\logs\php-error.log |
| IIS access log | C:\inetpub\logs\LogFiles\W3SVC1\u_ex261024.log |
| PHP web root | C:\inetpub\wwwroot |
session.upload_progress.enabled = 1
session.upload_progress.cleanup = 1
session.save_handler = files
memory_limit = 128M
post_max_size = 8M
upload_max_filesize = 2M
SAPI = cgi-fcgiThe server time zone was configured as GMT Standard Time (Ireland). IIS W3C and PHP error timestamps were recorded in UTC, while local Windows event displays could show UTC+1 on the test date. This distinction matters during cross-source investigations.
From IIS and Sysmon to QRadar
WinCollect 10.1.15.2 forwarded Windows Application, System, Security, Sysmon, PowerShell, and IIS W3C data to QRadar. The IIS collector required an explicit local source configuration, with recursive monitoring of the IIS log directory.
<LocalSources Name="IIS" Type="MSIIS" xmlns="event_collection/WinCollect">
<Source Name="IIS-W3C">
<Parameter name="Protocol.W3C" value="true" />
<Parameter name="MonitorSubdirectories" value="true" />
<Parameter name="RootDirectory" value="C:\inetpub\logs\LogFiles" />
<Parameter name="FileStaleDuration" value="0" />
</Source>
<Target Destination="QRadar" />
</LocalSources>The Sysmon collector was also corrected to use a valid XPath filter. The agent configuration was backed up before changes, including copies named AgentConfig-before-IIS-fix.xml and AgentConfig-pre-deploy.xml.
IIS W3C access logs
Verified HTTP request method, URI, client IP, status, and timestamp. A distinctive request marker (SOC-IIS-TEST-20261024-105956) was located in QRadar.
Sysmon + Security events
Sysmon Event ID 1 recorded process creation and parent/child relationships; Event ID 5 recorded process termination. Windows Security 4688 provided an additional process-creation source.
The team's QRadar rule
The existing rule correlates the immediate parent executable with the executable being created. The custom properties must be populated from the relevant process-creation event.
Parent Process Name (custom)
php-cgi.exe, php.exe, httpd.exe, nginx.exeAND Process Name (custom)
cmd.exe, powershell.exe, pwsh.exe, bash.exe, sh.exe,
python.exe, python3.exe, perl.exe, ruby.exe, node.exe,
curl.exe, wget.exe, nc.exe, ncat.exe, socat.exe,
certutil.exe, bitsadmin.exe, whoami.exe, hostname.exe,
ipconfig.exeFor Windows IIS, a php-cgi.exe → cmd.exe event satisfies the stated process-name conditions. This is a behavioural rule, not a signature uniquely identifying PHP2Shell.
Experiments and observed outputs
Test 1 — Prove IIS → PHP → Windows command execution
A deliberately benign process-test.php called PHP's exec() to run a fixed marker command. The endpoint returned HTTP 200, and Sysmon exposed the resulting process lineage.
exec('cmd.exe /c echo PHP2SHELL-PROCESS-TEST-001', ...);
Observed process tree:
w3wp.exe PID 1340
└─ php-cgi.exe PID 1316
└─ cmd.exe PID 3568
└─ cmd.exe PID 1692Sysmon recorded command-line and parent-process information. Execution was associated with IIS APPPOOL\DefaultAppPool. A high-integrity label was observed, but this alone is not evidence of administrator-level compromise.
Test 2 — Confirm PHP upload-progress functionality
An upload-test.php endpoint initialized a PHP session and reported the number of uploaded files. Kali submitted a multipart request with an upload-progress field and a small file.
curl -b "PHPSESSID=soclabtest001" \
-F "PHP_SESSION_UPLOAD_PROGRESS=SOC-UPLOAD-TEST-001" \
-F "file=@/etc/hostname" \
http://192.168.1.141/upload-test.php
Observed: session ID matched; Files received: 1
QRadar IIS W3C: POST /upload-test.php; client 192.168.1.101; HTTP 200Upload processing confirmedTest 3 — Request-level PHP memory exhaustion
A memory-test.php endpoint set memory_limit to 16 MB for the request, then repeatedly allocated memory. The client received HTTP 200 with an empty response, while PHP logged a fatal error.
[24-Oct-2026 11:11:16 UTC] PHP Fatal error:
Allowed memory size of 16777216 bytes exhausted
(tried to allocate 1052672 bytes)
in C:\inetpub\wwwroot\memory-test.php on line 11No corresponding PHP worker PID change or Windows Application crash was observed. A PHP memory-limit failure is therefore not equivalent to a FastCGI worker crash.
Request-level OOM characterisedTest 4 — Establish normal worker recycle behaviour
The IIS application pool was intentionally recycled using appcmd. Sysmon Event ID 5 recorded termination of the earlier workers, and subsequent requests started fresh processes.
appcmd.exe recycle apppool /apppool.name:"DefaultAppPool"
Before recycle:
w3wp.exe PID 1340
php-cgi.exe PID 2156
After recycle and next PHP request:
w3wp.exe PID 1648 Parent PID 2660 12:15:30 local
php-cgi.exe PID 4060 Parent PID 1648 12:16:24 localThe recycle produced an identifiable normal-termination baseline, which can be compared with abnormal worker failure in later investigations.
Lifecycle baseline establishedTest 5 — Test oversized multipart POST handling
A 9 MiB test file was posted to upload-test.php, exceeding the configured post_max_size = 8M. IIS returned HTTP 200, but PHP did not populate the uploaded-files collection.
HTTP/1.1 100 Continue
HTTP/1.1 200 OK
Server: Microsoft-IIS/10.0
X-Powered-By: PHP/8.4.26
Session ID: soclabsize001
Upload progress enabled: 1
POST received: POST
Files received: 0
[24-Oct-2026 11:21:41 UTC] PHP Warning:
PHP Request Startup: POST Content-Length of 9437545 bytes
exceeds the limit of 8388608 bytes in Unknown on line 0This demonstrates that HTTP 200 does not imply successful PHP multipart parsing. It also identifies a concrete reason why the PoC's approximately 15 MB first request would not be processed as intended under the unchanged configuration.
POST limit confirmedWhat the PHP2Shell PoC assumed
The examined remote_cve.py described a three-part exploit chain following worker rotation: disclose a heap/chunk base, disclose a PHP library address, then corrupt PHP session-related structures to redirect a destructor callback to command execution. Its stated validated target was Apache prefork + mod_php on Debian 12, PHP 8.4.26.
The rotate() implementation first generated 1,001 form fields of 3,000 bytes each, included a null-byte PHPSESSID form field and an upload-progress parameter, attached a small file, and appended a 12 MiB field. It then sent a smaller follow-up multipart request.
# Relevant Stage 0 structure (abridged; illustrative)
parts = [{"name": f"f{i:04d}"[:5], "data": b"V" * 3000}
for i in range(1001)]
parts += [
{"name": "PHPSESSID", "data": b"\x00"},
{"name": "PHP_SESSION_UPLOAD_PROGRESS", "data": b"pwn"},
self.file_part(),
self.big() # 12 MiB payload field
]
self.t.post(multipart(parts, b"S" * 17), b"S" * 17, ...)Why Linux success did not translate to Windows
| Exploit assumption | Linux Apache/mod_php PoC | Windows IIS/FastCGI lab |
|---|---|---|
| PHP execution model | PHP loaded into Apache worker via mod_php | Separate php-cgi.exe worker launched by IIS |
| Worker lifecycle | PoC assumes Apache prefork replacement behaviour | IIS FastCGI lifecycle; recycle and PHP request-level OOM tested, but exploit-style worker abort not demonstrated |
| Binary and symbols | Linux ELF / libphp.so symbol offsets | Windows PHP binaries; offsets and symbol assumptions are not portable |
| Memory layout | PoC relies on Linux-target-specific heap and address disclosures | No Windows-specific disclosure or corruption primitives validated |
| Multipart request limits | PoC designed around its validated target configuration | 8 MB post_max_size rejects the oversized Stage 0 POST data |
| Observed exploitation | Previously successful in the separate Linux lab | No successful Windows exploit demonstrated |
Even if the HTTP inputs are platform-neutral, a memory-corruption exploit can depend heavily on runtime internals, binary layout, calling conventions, allocator behaviour, and worker lifecycle. A successful Linux PoC cannot be treated as evidence of equivalent Windows exploitability.
What we concluded—and what we did not
Demonstrated
- Functional Windows IIS/PHP FastCGI target.
- IIS W3C and Sysmon telemetry forwarded into QRadar.
- Benign PHP-to-
cmd.exeexecution with qualifying parent/child values. - Session upload handling with a small multipart request.
- Distinct PHP OOM, IIS recycle, and POST-size-limit behaviours.
Not demonstrated
- PHP2Shell remote code execution against Windows.
- Successful Windows memory disclosure or corruption.
- Portability of Linux Apache exploit offsets or worker assumptions.
- Confirmed QRadar correlation-rule firing or offense creation.
- Comprehensive detection coverage for every possible exploit execution path.
We chose to stop the Windows exploit attempt because the existing PoC was specifically engineered for a Linux Apache/mod_php environment. Porting it would require new Windows-specific exploit research, well beyond the SOC detection-validation objective. Continuing to tweak POST limits alone would not resolve those architectural differences.
Lessons for SOC analysts
- Separate detection logic from rule execution. A process tree that satisfies the written conditions is not the same as a confirmed QRadar rule match. Check custom property extraction, rule response and offense/event correlation.
- Use multiple telemetry layers. IIS access logs establish the HTTP context; Sysmon Event ID 1 shows execution; Event ID 5 provides process termination context; PHP errors explain request-level failures.
- Do not interpret HTTP 200 as exploit success. An oversized POST returned 200 while PHP discarded its contents and logged a warning.
- Know the immediate-parent limitation. The rule is strongest for direct
php-cgi.exe → cmd.exeorhttpd.exe → shpatterns. Indirect execution chains or in-process activity may evade it. - Account for legitimate application behaviour. Some PHP applications intentionally invoke utilities. Production rules need baselines, asset context, exclusions where justified, and careful tuning.
- Correlate time zones. IIS/PHP UTC timestamps and Windows local display times can differ by an hour during daylight-saving periods.
- Validate on the platform where the exploit actually works. Use the Linux Apache PoC to confirm exploit-faithful detection, then treat Windows tests as distinct behavioural coverage.
Evidence preservation and close-out
Preserve representative IIS W3C entries, Sysmon Event IDs 1 and 5, Security 4688 where available, PHP error excerpts, the WinCollect configuration backup, and QRadar searches or screenshots demonstrating event parsing. Record whether the QRadar rule actually matched once verified.
Remove temporary web-accessible scripts such as process-test.php, upload-test.php, memory-test.php, and config-test.php when testing is complete; they should not remain accessible on a persistent host. Retain a lab snapshot if further controlled research is planned.
Evidence basis: the recorded lab commands, process output, configuration values, PHP error messages, and QRadar observations supplied during this testing session. Explanations of portability are engineering analysis; they are not a claim that Windows is unaffected by the underlying vulnerability. Dates shown are from the 24 October 2026 lab session.