Sitemap
Anouar RIYANE

Anouar RIYANE

Security Researcher, Bug Bounty Hunter

Investigating PHP2Shell: A SOC Analyst’s Journey from Apache Logs to Remote Code Execution

11 min read5 days ago
Press enter or click to view image in full size

When I started the dOOMs Day Sherlock, I had three useful sources of evidence: an Apache access log, an Apache error log, and a packet capture. The challenge looked like a web attack, but I didn’t want to guess which IP address was responsible or jump straight to the answers. I wanted to reconstruct what actually happened: where the suspicious traffic started, how it affected PHP and Apache, and what the attacker did after gaining access.

What made this investigation interesting was the way the evidence connected. The access log showed who was making requests and when. Wireshark exposed what those requests contained. The error log showed PHP’s memory problems and Apache worker crashes. Finally, the same PCAP contained the reverse-shell conversation and the commands typed on the compromised server.

  1. Evidence and investigation plan

The supplied archive contained:

  • Capture.pcap: network traffic between the attacker and server, including HTTP and later shell traffic.
  • logs/access.log: HTTP request timestamps, source addresses, methods, paths and response codes.
  • logs/error.log: Apache and PHP diagnostic messages, including memory exhaustion and worker exits.
  • logs/other_vhosts_access.log: present in the archive, but empty in this case.

I also opened the raw Apache access log before applying any filters. The original view was crowded, which is why I started narrowing it down with simple shell commands rather than reading line by line.

Press enter or click to view image in full size

My workflow was simple: identify an abnormal source → verify its HTTP requests → reconstruct the exploitation sequence → prove code execution → inspect post-exploitation activity. I started with access.log because it provided an efficient overview before opening a large PCAP.

2. Identifying the attacker’s IP

First I counted requests per source:

awk '{print $1}' access.log | sort | uniq -c | sort -nr
Press enter or click to view image in full size

The busiest client, 192.168.72.164, had 259 requests, followed by .145 with 245. That did not make either one malicious. A busy browser can generate many ordinary requests. I needed to compare behavior, not just volume.

The next command counted HTTP methods for each IP:

awk '{print $1, $6}' access.log | sort | uniq -c | sort -nr

This was the turning point. Most activity from .164 and .145 was GET traffic. The local IPv6 address ::1 generated OPTIONS requests. But 192.168.72.1 sent 160 POST requests and not a single GET. That was worth investigating.

awk '$1=="192.168.72.1" {print $4, $6, $7, $9}' access.log | sort | head -20

The requests targeted only /index.php and /api/health.php. More importantly, dozens arrived within seconds. The endpoint breakdown was 149 POSTs to /index.php and 11 POSTs to /api/health.php.

Press enter or click to view image in full size

At this point, I classified 192.168.72.1 as the leading suspect, not yet a confirmed attacker. Automated POSTs could be legitimate in another application. The payload and server impact would decide.

3. Finding the first exploitation request

Once 192.168.72.1 became my main suspect, I wanted the first anomalous HTTP request, not just the earliest TCP SYN. I sorted the corresponding Apache entries by time:

grep '^192\.168\.72\.1 ' access.log | sort -k4,4 | head -5

The earliest entry was:

192.168.72.1 - - [28/Oct/2026:08:42:52 +0000] "POST /index.php HTTP/1.1" 200 0 "-" "-"

So the suspicious POST sequence began at 2026–10–28 08:42:52 UTC. I opened Capture.pcap and started with a broad filter for traffic headed to the web server:

ip.src == 192.168.72.1 && tcp.dstport == 80

The first broad Wireshark filter showed many TCP connections and retransmissions around the suspicious window. This is a useful reminder not to confuse the earliest TCP handshake with the earliest malicious HTTP request.

Press enter or click to view image in full size

I then searched the TCP payload itself:

ip.src == 192.168.72.1 && tcp contains "POST /index.php"

Figure 6 is useful because it shows the same suspicious source participating in a larger multipart sequence. In its lower packet details, the form uses the boundary BNDRY12345 and includes PHP_SESSION_UPLOAD_PROGRESS. That belongs to a later targeted request; I didn’t confuse it with the earlier stalled-upload boundary or use its timestamp as the initial attack time. To establish the start, I went back to the earlier /index.php requests in the following packet-level views.

4. Understanding the stalled multipart uploads

Following the stream made the abnormal behavior much clearer. The request began like this:

POST /index.php HTTP/1.1
Host: 192.168.72.50:80
Content-Type: multipart/form-data; boundary=ZZZZZZZZZZ
Transfer-Encoding: chunked
Press enter or click to view image in full size

The boundary separator was ZZZZZZZZZZ. A boundary is normal in multipart form submissions; the concern here was the repeated filler data and the deliberately incomplete transfer keeping a worker occupied. Notice that this is not the BNDRY12345 boundary seen in Figure 6’s later targeted /api/health.php request.

After seeing the body, I returned to the packet list to connect the stream to the earliest request. With the attacker’s IP and destination port filtered, I reviewed the packets around 08:42:52 and checked for the actual request payload rather than selecting a SYN retransmission.

Press enter or click to view image in full size

I narrowed the search further to packets containing the POST request line:

ip.src == 192.168.72.1 && tcp contains "POST /index.php"

The first relevant HTTP request begins in frame 6089, captured at 08:42:52.530036 UTC, from 192.168.72.1 to 192.168.72.50:80.

Press enter or click to view image in full size

Wireshark may display the frame as TCP rather than HTTP because the full HTTP message is reassembled later in the stream. The raw bytes and Follow TCP Stream confirm the request. This is also why I didn’t treat an earlier TCP handshake or retransmission as the first exploitation request.

I inspected another stalled multipart stream to make sure this was a repeated technique rather than one unusual upload. Its long sequence of filler chunks showed the same pattern.

Press enter or click to view image in full size

The reason for maintaining many stalled connections was to tie up Apache’s worker pool. Keeping workers occupied made later exploitation requests more likely to run on the same available worker, which matters when an attack depends on memory state in a particular process heap.

I counted the stalled requests using the request line:

ip.src == 192.168.72.1 && tcp contains "POST /index.php"
Press enter or click to view image in full size

The important distinction is 149 stalled-upload connections, not all 160 POST requests from the attacker. Eleven other POSTs targeted /api/health.php.

5. The PHP memory-corruption chain

Get Anouar RIYANE’s stories in your inbox

Join Medium for free to get updates from this writer.

The technique in this Sherlock relates to two PHP vulnerabilities commonly grouped under PHP2Shell:

  • CVE-2026–109618: an out-of-memory event can interrupt PHP while request structures are incompletely initialized; cleanup may then operate on invalid state.
  • CVE-2026–109619: request-specific internal state can outlive its backing memory, so another request handled in the same worker can reuse stale references.

The connection to the stalled-upload stage was becoming clearer. The attacker was using worker-pool occupation, crafted multipart inputs, and memory exhaustion to manipulate what happened inside a particular Apache/PHP worker. The packet capture by itself doesn’t label a CVE; the attribution comes from correlating the observed mechanism with the published technical advisories.

To check the out-of-memory condition, I searched error.log for the PHP limit message:

grep -i 'Allowed memory size' error.log | cut -c 1-250 | head
Press enter or click to view image in full size

The log reported:

Allowed memory size of 134217728 bytes exhausted

That is 134,217,728 bytes (128 MiB). Any accompanying tried to allocate number describes a new attempted allocation; it is not the configured memory limit. This log evidence connects the suspicious multipart activity with PHP’s failing memory allocations.

6. Tracing the heap disclosure through the cookie

The next piece was the memory leak. I looked for HTTP responses from the server back to the suspicious client, especially unusual Set-Cookie headers:

ip.src == 192.168.72.50 && ip.dst == 192.168.72.1 && tcp contains "Set-Cookie"

The packet list returned several matches, including ordinary-looking HTTP responses and the more interesting response near 08:43:01 UTC. I inspected the suspicious one instead of treating every Set-Cookie header as malicious.

Press enter or click to view image in full size

The suspicious response used PHPSESSID, the ordinary PHP session-cookie name. Here, though, its value contained very long encoded, memory-like data. The cookie name was normal; the size and contents were not.

In error.log, Apache’s mod_dumpio entries showed the header name, a colon separator, then a buffer length immediately before the cookie data:

... dumpio_out (data-HEAP): Set-Cookie
... dumpio_out (data-HEAP): 2 bytes
... dumpio_out (data-HEAP): :
... dumpio_out (data-HEAP): 773518 bytes
... dumpio_out (data-HEAP): PHPSESSID=WWWWWWWW%FF...
Press enter or click to view image in full size

One detail is easy to miss: 773518 bytes is the output-buffer length reported by Apache, not a figure printed beside the cookie name or a length calculated from the shortened Wireshark display. I confirmed it from the lines immediately surrounding PHPSESSID in the same connection’s error-log entries.

This is the point where I could connect the OOM behavior with a data disclosure: the web server was returning abnormal memory-like content to the attacker’s session cookie, and the exploit could use the leaked heap information to make later memory manipulation more reliable.

7. Confirming remote code execution, not merely an HTTP 200

A successful HTTP status code does not prove that the attacker executed code. I needed independent evidence that the server ran an attacker-controlled process.

I filtered the PCAP for traffic on the reverse-shell port:

tcp.port == 4444
Press enter or click to view image in full size

At 08:43:02.401261 UTC, frame 23797 shows a SYN from 192.168.72.50:55914 (the server) to 192.168.72.1:4444 (the attacker). The server initiated the connection back out — exactly the direction I would expect for a reverse shell. A SYN alone would be insufficient proof, but the subsequent stream contains an interactive shell prompt and later command/output exchanges. By 08:43:02.458047 UTC, the conversation shows shell output under the Apache account (www-data).

That confirmed RCE around 2026–10–28 08:43:02 UTC. The difference between the earlier cookie leak and this reverse connection is only about a second, which makes the sequence especially compelling.

8. Explaining the Apache worker crashes

I also wanted to check whether Apache had crashed during the exploitation. But first, I needed to understand what kind of error message Apache records when one of its worker processes crashes. So I asked ChatGPT to explain how Apache logs worker crashes and what keywords I should look for in error.log.

That’s when I learned about segmentation faults (SIGSEGV, signal 11). A segmentation fault generally occurs when a process tries to access an invalid memory location. It’s different from an out-of-memory (OOM) error, although memory corruption can sometimes connect the two.

ChatGPT also explained the meaning of Apache’s AH00052 error message, which reports when a child process exits because of a signal. This helped me understand what to search for in the logs and how to interpret the results.

Press enter or click to view image in full size

I then searched for the actual error-log evidence:

grep 'Segmentation fault' error.log
Press enter or click to view image in full size
08:46:16.516499 — child pid 145 exit signal Segmentation fault (11)
08:47:42.631917 — child pid 170 exit signal Segmentation fault (11)

The second crash was at 08:47:42.631917 UTC. Both crashes occurred after RCE was established at about 08:43:02, so I treated them as subsequent evidence of unstable worker memory behavior, not the first proof that code execution succeeded.

9. Following the post-exploitation commands

I then returned to the reverse-shell stream to see what the attacker did with the access. This part of the analysis is presented after the crash review to follow my original screenshot numbering; in the actual timeline, some shell commands happened before those worker crashes.

The attacker ran commands such as cat /proc/version, uname -a, and ps aux to learn about the host. I searched the shell traffic for find / -type f and found the database-discovery command in frame 201654, at 08:46:00.336542 UTC:

Press enter or click to view image in full size
find / -type f \( -name "*.sqlite" -o -name "*.sqlite3" -o -name "*.db" -o -name "*.db3" -o -name "*.s3db" -o -name "*.sl3" \) 2>/dev/null
Press enter or click to view image in full size

The command returned **`/var/www/data/devnotes.sqlite`**, showing that the attacker had successfully located a SQLite database on the compromised server.

This was the final part of the attacker’s activity that I examined during my investigation. From the evidence I analyzed, I could confirm that the attacker searched for and discovered a database file.

With this final observation, I reached the end of my investigation into the attacker’s activity in the captured traffic.

10. Reconstructing the incident timeline

  • Incident timeline: October 28, 2026 (UTC)
  • 08:42:52: Exploitation traffic begins. The first anomalous POST /index.php from 192.168.72.1; Wireshark frame 6089 records the request at 08:42:52.530036.
  • 08:42:52 onward: Worker-pool occupation. The attacker holds 149 multipart uploads open using the ZZZZZZZZZZ boundary.
  • 08:43:01: Memory exhaustion and heap leak. PHP OOM errors appear alongside an anomalously large PHPSESSID response buffer.
  • 08:43:02: RCE confirmed. The server opens a connection to the attacker on TCP/4444, and the stream carries a working shell.
  • 08:46:00: Database discovery. The attacker runs find / -type f … from the reverse shell.
  • 08:46:16: First worker crash. Apache reports PID 145 exiting with SIGSEGV.
  • 08:47:42: Second worker crash. Apache reports PID 170 exiting with SIGSEGV.

This timeline is why I find multi-source correlation so important. Looking at the access log alone, I would see only unusual POST traffic and 200 responses. Looking at Wireshark alone, I might miss the relationship to PHP’s memory limit and worker exits. Together, they tell a coherent story.

Final thoughts

The biggest lesson from dOOMs Day was that a high volume of HTTP requests is only a starting clue. The real detection came from following the evidence: an unusual POST-only client, a deliberately stalled multipart pattern, PHP exhaustion and heap leakage, then a reverse shell and post-exploitation commands. The later Apache crashes explained more of the exploit’s impact without being confused with the moment RCE first occurred.

This challenge was a useful reminder that DFIR work is not about collecting isolated indicators. It’s about explaining how each artifact supports the next one and knowing which conclusions the evidence does not support.

--

--

Anouar RIYANE
Anouar RIYANE

Written by Anouar RIYANE

Security Researcher, Bug Bounty Hunter