┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
CVE-2019-13288: Xpdf
~ CuB3y0nd
# CVE-2019-13288

## Description

CVE: https://www.cve.org/CVERecord?id=CVE-2019-13288

# Compile

## Download

 [Xpdf Source Code - xpdf 3.02](https://www.xpdfreader.com/old-versi
ons.html) _4.01.01_

```shellsession
wget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz
tar -zxvf xpdf-3.02.tar.gz
```

## Build

 LTO mode 

```shellsession
cd xpdf-3.02
CC=afl-clang-lto CXX=afl-clang-lto++ ./configure --prefix="$PWD/install"
make -j`nproc`
make install
```



```shellsession
CC=clang CXX=clang++ CFLAGS="-O0 -g -gdwarf-4 -fno-inline -fno-builtin -fno-omit
-frame-pointer" CXXFLAGS="$CFLAGS" ./configure --prefix="$PWD/install-dbg"
make -j`nproc`
make install
```

# Samples

 python 

```shellsession
uv venv .venv
uv pip install reportlab pillow
```

使 `uv run python .venv/gen_corpus.py` 

```python title="gen_corpus.py"
#!/usr/bin/env python3

import os
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from PIL import Image

OUTDIR = "corpus/pdf"
FONT_PATH = "/usr/share/fonts/TTF/Inconsolata-Black.ttf"

os.makedirs(OUTDIR, exist_ok=True)

def path(name):
    return os.path.join(OUTDIR, name)

def gen_image(fname):
    img = Image.new("RGB", (32, 32), color=(255, 0, 0))
    img.save(fname, "JPEG")

def gen_minimal():
    c = canvas.Canvas(path("id_000000_minimal.pdf"))
    c.drawString(10, 10, "hi")
    c.save()

def gen_text():
    c = canvas.Canvas(path("id_000001_text.pdf"))
    for i in range(5):
        c.drawString(50, 800 - i * 20, f"text line {i}")
    c.save()

def gen_multipage():
    c = canvas.Canvas(path("id_000002_multipage.pdf"))
    for i in range(5):
        c.drawString(100, 700, f"page {i}")
        c.showPage()
    c.save()

def gen_image_pdf():
    img_path = path("tmp.jpg")
    gen_image(img_path)

    c = canvas.Canvas(path("id_000003_image.pdf"), pagesize=A4)
    c.drawImage(img_path, 100, 500, width=100, height=100)
    c.save()

    os.remove(img_path)

def gen_font_pdf():
    pdfmetrics.registerFont(TTFont("FuzzFont", FONT_PATH))
    c = canvas.Canvas(path("id_000004_font.pdf"))
    c.setFont("FuzzFont", 12)
    c.drawString(100, 700, "font fuzz test")
    c.save()

def gen_stream_filter():
    # Hand-written PDF: stream + FlateDecode (parser favorite)
    data = b"""%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /Contents 4 0 R >>
endobj
4 0 obj
<< /Length 5 /Filter /FlateDecode >>
stream
xx9cx03x00x00x00x00x01
endstream
endobj
xref
0 5
0000000000 65535 f
0000000010 00000 n
0000000060 00000 n
0000000115 00000 n
0000000175 00000 n
trailer
<< /Root 1 0 R >>
startxref
240
%%EOF
"""
    with open(path("id_000005_stream_filter.pdf"), "wb") as f:
        f.write(data)

def main():
    gen_minimal()
    gen_text()
    gen_multipage()
    gen_image_pdf()
    gen_font_pdf()
    gen_stream_filter()
    print(f"[+] PDF corpus generated in ./{OUTDIR}")

if __name__ == "__main__":
    main()
```

# Fuzzing

 fuzz 

```shellsession
afl-fuzz -i corpus/ -o out/ -s 1337 -- ./install/bin/pdftotext @@ -
```

 crashes
 33  crashes  crash 
 corpus  CVE-2019-13288 
 DoS 
# Analysis

 `Parser::getObj` 使 `objNum=7, ob
jGen=0`  `Object::fetch`  `XRef::fetch`  `Parser::ge
tObj` backtrace 
 /  `Parser::getObj` 

 `Parser.cc:94`  `makeStream` `n` 
 `addFilters`
 `addFilters` 

```cpp
Stream *Stream::addFilters(Object *dict) {
  Object obj, obj2;
  Object params, params2;
  Stream *str;
  int i;

  str = this;
  dict->dictLookup("Filter", &obj);
  if (obj.isNull()) {
    obj.free();
    dict->dictLookup("F", &obj);
  }
  dict->dictLookup("DecodeParms", &params);
  if (params.isNull()) {
    params.free();
    dict->dictLookup("DP", &params);
  }
  if (obj.isName()) {
    str = makeFilter(obj.getName(), str, &params);
  } else if (obj.isArray()) {
    for (i = 0; i < obj.arrayGetLength(); ++i) {
      obj.arrayGet(i, &obj2);
      if (params.isArray())
        params.arrayGet(i, &params2);
      else
        params2.initNull();
      if (obj2.isName()) {
        str = makeFilter(obj2.getName(), str, &params2);
      } else {
        error(getPos(), "Bad filter name");
        str = new EOFStream(str);
      }
      obj2.free();
      params2.free();
    }
  } else if (!obj.isNull()) {
    error(getPos(), "Bad 'Filter' attribute in stream");
  }
  obj.free();
  params.free();

  return str;
}
```

 `dictLookup("Filter", &obj)`  `(objNum=7, objGen=0)` 
 _Dictionary_  _Filter_
 crash  _Indirect Object_ 

```plaintext showLineNumbers=false collapse={1-37, 45-67}
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type 
/Font
>>
endobj
3 0 obj
<<
/Contents 7 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 6 0 R /Resources <<
/Font 1 0 R /Pr~cSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<

>>
  /Type /Page
>>
endobj
4 0 obj
<<
/PageMode /UseNone /Pages 6 0 R /Type /Catalog
>>
endobj
5 0 obj
<<
/Author (anonymous) /CreationDate (D:20260210132607+08'00') /Creator (anonymous)
 /erated PDF doModDate (D:20260210132607+08'00') /Producer (ReportLab PDF Librar
y - (opensource))
  /Subject (unspecified) /Title (unti€€€€€€€€€€€€€€€€€€€€€€€>
endobj
6 0 obj
<<
/Count 1 /Kids [ 3 0 R ] /Type /Pages
>>
endobj
7 0 obj
<<
/Filter [ /ASCII85Decode /FlateDects 7 0 R /Meode ] /Length 87
>>
stream
GapQh0E=F,0UH3TpNYT^QKk?tc>IP,;W#U1^23ihPEM_?CW4KISi9!25KZ"cI79neZ[Kb,ht$3`$^8YH
ZB~>endstream
endobj
xref
0 8
00 65535 f
000061 00000 n
000092 00000 n
000199 00000 n
000402 00000 n
00 Off00 n
000731 00000 n
000790 00000 n
trailer
<<
/ID
[<a9e650489693d00e7eßßßßßßßßßßßßab1f137a614fa1><a9e650489693d00e7eab1f137a614fa1
>]
% ReportLab genKeywords () /cument -- digest (opensource)

/Info 5 0 R
/Root 4 0 R
/Size 8
>>
startxref
966
%%EOF
```

 Dictionary Filter  Object  Filter
 4  _Array_
 crash 

 [PDF reference: Adobe portable document format, ver
sion 1.3](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfrefer
ence1.3.pdf)  v1.3  crash  PDF 
 1.3 `%PDF-1.3`
 `else if (obj.isArray())` 
 `Object::arrayGet -> Array::get` 
 fetch `(objNum=7, objGen=0)` 
 for  `i = 0` 
 `0`  `obj
Name` `if (obj2.isName())`  `Object` 
 `tagged union` `obj2.getName()`  `ASCII85Decode`

 `Array::get`  fetch 
 `(objNum=7, objGen=0)`  `i = 2` `b Array::get if i == 2` 
 `7 0 R`
 `Object::fetch`  `type == objRef && xref`  fetc
h  `(objNum=7, objGen=0)`BOOM!

<cneter>
</cneter>

# Fix

 Filter  Name  Name 
 Filter  Indirect Reference Stream ……
 `objRef` 

```cpp ins={22, 33-27}
Stream *Stream::addFilters(Object *dict) {
  Object obj, obj2;
  Object params, params2;
  Stream *str;
  int i;

  str = this;
  dict->dictLookup("Filter", &obj);
  if (obj.isNull()) {
    obj.free();
    dict->dictLookup("F", &obj);
  }
  dict->dictLookup("DecodeParms", &params);
  if (params.isNull()) {
    params.free();
    dict->dictLookup("DP", &params);
  }
  if (obj.isName()) {
    str = makeFilter(obj.getName(), str, &params);
  } else if (obj.isArray()) {
    for (i = 0; i < obj.arrayGetLength(); ++i) {
      obj.arrayGetNF(i, &obj2);
      if (params.isArray())
        params.arrayGet(i, &params2);
      else
        params2.initNull();
      if (obj2.isRef()) {
        error(getPos(), "Indirect reference is not allowed in Filter");
        str = new EOFStream(str);
        obj2.free();
        params2.free();
        break;
      }
      if (obj2.isName()) {
        str = makeFilter(obj2.getName(), str, &params2);
      } else {
        error(getPos(), "Bad filter name");
        str = new EOFStream(str);
      }
      obj2.free();
      params2.free();
    }
  } else if (!obj.isNull()) {
    error(getPos(), "Bad 'Filter' attribute in stream");
  }
  obj.free();
  params.free();

  return str;
}
```

 xD