Talos Vulnerability Report

TALOS-2019-0793

Foxit PDF Reader JavaScript Array.includes remote code execution vulnerability

September 30, 2019
CVE Number

CVE-2019-5031

Summary

An exploitable memory corruption vulnerability exists in the JavaScript engine of Foxit Software's Foxit PDF Reader, version 9.4.1.16828. A specially crafted PDF document can trigger an out-of-memory condition which isn't handled properly, resulting in arbitrary code execution. An attacker needs to trick the user to open the malicious file to trigger this vulnerability. If the browser plugin extension is enabled, visiting a malicious site can also trigger the vulnerability.

Tested Versions

Foxit Software Foxit PDF Reader 9.4.1.16828.

Product URLs

https://www.foxitsoftware.com/products/pdf-reader/

CVSSv3 Score

8.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CWE

CWE-703: Improper Check or Handling of Exceptional Conditions

Details

Foxit PDF Reader is one of the most popular PDF document readers, and has a widespread user base. It aims to have feature parity with Adobe’s Acrobat Reader. As a complete and feature-rich PDF reader, it supports JavaScript for interactive documents and dynamic forms. JavaScript support poses an additional attack surface. Foxit Reader uses V8 JavaScript engine.

A PDF document can execute JavaScript at different events. One of these examples occurs when the user opens a PDF document and it runs first. Another is on page open or close which can run when user navigates to the page. A page open event is triggered for the first page when the document is opened and rendered.

A bug exists in V8 version 7.5.45 and previous that results in a large amount of memory to be allocated, which quickly uses up all available memory. This would usually result in an out-of-memory state being detected and the process would be terminated. In this instance, this is not the case as the large allocation is the result of the following bug:

Array.prototype.length = 0xffffffff;
Array.prototype.includes(0); 

Setting the array length to INT32_MAX value changes the array's apparent size, and even though the array is empty, due to a bug, includes() takes a very slow path that visits every, undefined, element of the array while allocating exuberant amount of memory in the process. If we take a look at the properties of this array before and after changing its length we can see the following:

$./d8 --allow-natives-syntax
d8> Array.prototype.length
0
d8> %DebugPrint(Array.prototype)
DebugPrint: 0x1f0050210ff1: [JSArray] in OldSpace
 - map: 0x1f00c1382de9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1f0050201f71 <Object map = 0x1f00c1380229>
 - elements: 0x1f00d0300c21 <FixedArray[0]> [HOLEY_ELEMENTS]
 - length: 0
0x1f00c1382de9: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 32
 - inobject properties: 0
 - elements kind: HOLEY_ELEMENTS                    [1]
 - unused property fields: 2
 - enum length: invalid
 - stable_map
 - prototype_map
 - prototype info: 0x1f0050211011 <PrototypeInfo>
 - prototype_validity cell: 0x1f00a2380609 <Cell value= 1>
 - instance descriptors (own) #35: 0x1f0050211049 <DescriptorArray[35]>
 - layout descriptor: 0x0
 - prototype: 0x1f0050201f71 <Object map = 0x1f00c1380229>
 - constructor: 0x1f0050201fa9 <JSFunction Object (sfi = 0x1f00a2389931)>
 - dependent code: 0x1f00d03002c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

At [1], we can see type of the array is HOLEY_ELEMENTS and after growing the array:

d8> Array.prototype.length = 0xffffffff;
4294967295
d8> %DebugPrint(Array.prototype)
DebugPrint: 0x1f0050210ff1: [JSArray] in OldSpace
 - map: 0x1f00c138a8b9 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
 - prototype: 0x1f0050201f71 <Object map = 0x1f00c1380229>
 - elements: 0x1f00b178ff39 <NumberDictionary[16]> [DICTIONARY_ELEMENTS]
 - length: 0x1f00b1790329 <HeapNumber 4.29497e+09>
0x1f00c138a8b9: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 32
 - inobject properties: 0
 - elements kind: DICTIONARY_ELEMENTS                       [2]
 - unused property fields: 2
 - enum length: invalid
 - stable_map
 - prototype_map
 - prototype info: 0x1f0050211011 <PrototypeInfo>
 - prototype_validity cell: 0x1f00a2380609 <Cell value= 1>
 - instance descriptors (own) #35: 0x1f00b178ffc9 <DescriptorArray[35]>
 - layout descriptor: 0x0
 - prototype: 0x1f0050201f71 <Object map = 0x1f00c1380229>
 - constructor: 0x1f0050201fa9 <JSFunction Object (sfi = 0x1f00a2389931)>
 - dependent code: 0x1f00d03002c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

At [2], we see that the array is now of type DICTIONARY_ELEMENTS. After navigating through the implementation of Array.prototype.includes in V8's CSA we can see that HOLEY_ELEMENTS and DICTIONARY_ELEMENTS are handled differently, and code ends up calling a runtime implementation of includes() for DICTIONARY_ELEMENTS:

BIND(&call_runtime);
{
Node* start_from =
    args.GetOptionalArgumentValue(kFromIndexArg, UndefinedConstant());
Runtime::FunctionId function = variant == kIncludes
                                   ? Runtime::kArrayIncludes_Slow
                                   : Runtime::kArrayIndexOf;
args.PopAndReturn(
    CallRuntime(function, context, array, search_element, start_from));
}

In the runtime implementation of kArrayIncludes_Slow we eventually end up at the following check:

if (!object->map()->IsSpecialReceiverMap() && len < kMaxUInt32 &&             [3]
    JSObject::PrototypeHasNoElements(isolate, JSObject::cast(*object))) {
  Handle<JSObject> obj = Handle<JSObject>::cast(object);
  ElementsAccessor* elements = obj->GetElementsAccessor();
  Maybe<bool> result = elements->IncludesValue(isolate, obj, search_element,
                                               static_cast<uint32_t>(index),
                                               static_cast<uint32_t>(len));
  MAYBE_RETURN(result, ReadOnlyRoots(isolate).exception());
  return *isolate->factory()->ToBoolean(result.FromJust());
}

// Otherwise, perform slow lookups for special receiver types
for (; index < len; ++index) {                                                [4]

At [3], we can see that length of the array is compared against kMaxUInt32 and the code in the if statement is executed if len is strictly less than kMaxUInt32. Since array length is set to kMaxUInt32, execution continues at [4] and ends up in the slowest possible path. The correct check at [3] should be that len should be less than or equal to kMaxUInt32, as is the case in indexOf runtime function implementation.

This quickly results in huge amounts of memory being executed and should result in process termination. In Foxit, however, the out-of-memory condition raises an exception which is caught and "handled," and processing of the PDF can continue:

0:000> sxe e06d7363
0:000> g
...
(754.1588): C++ EH exception - code e06d7363 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for FoxitReader.exe - 
eax=053de308 ebx=053de3b8 ecx=00000003 edx=00000000 esi=039d3ebc edi=03e0be30
eip=764c18a2 esp=053de308 ebp=053de364 iopl=0         nv up ei pl nz ac po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200212
KERNELBASE!RaiseException+0x62:
764c18a2 8b4c2454        mov     ecx,dword ptr [esp+54h] ss:002b:053de35c=583d2f59
0:000> .exr -1
ExceptionAddress: 764c18a2 (KERNELBASE!RaiseException+0x00000062)
   ExceptionCode: e06d7363 (C++ EH exception)
  ExceptionFlags: 00000001
NumberParameters: 3
   Parameter[0]: 19930520
   Parameter[1]: 053de3b8
   Parameter[2]: 03e0be30                                           [5]
unable to find C-Runtime symbols, even with unqualified search
0:000> dd 03e0be30
03e0be30  00000000 00000000 00000000 03e0be40
03e0be40  00000005 03e0be58 03e0be74 03ba583c
03e0be50  03ba5858 03e53224 00000001 03f93408
03e0be60  00000000 ffffffff 00000000 00000004
03e0be70  00000000 00000001 03fc258c 00000000
03e0be80  ffffffff 00000000 00000004 00000000
03e0be90  00000000 00000000 00000000 03e0bea0
03e0bea0  00000005 03e0beb8 03e0be74 03ba583c
0:000> dd 03e0be40 
03e0be40  00000005 03e0be58 03e0be74 03ba583c
03e0be50  03ba5858 03e53224 00000001 03f93408
03e0be60  00000000 ffffffff 00000000 00000004
03e0be70  00000000 00000001 03fc258c 00000000
03e0be80  ffffffff 00000000 00000004 00000000
03e0be90  00000000 00000000 00000000 03e0bea0
03e0bea0  00000005 03e0beb8 03e0be74 03ba583c
03e0beb0  03ba5858 03e53224 00000001 03f926f4
0:000> dd 03e0be58 
03e0be58  00000001 03f93408 00000000 ffffffff
03e0be68  00000000 00000004 00000000 00000001
03e0be78  03fc258c 00000000 ffffffff 00000000
03e0be88  00000004 00000000 00000000 00000000
03e0be98  00000000 03e0bea0 00000005 03e0beb8
03e0bea8  03e0be74 03ba583c 03ba5858 03e53224
03e0beb8  00000001 03f926f4 00000000 ffffffff
03e0bec8  00000000 00000004 00000000 00000000
0:000> da 03f93408+8
03f93410  ".PAVCMemoryException@@"

Since this is a C++ exception, if we follow the second parameter of the exception at [5] through a couple of dereferences, in the end we get that the thrown exception is of type CMemoryException, which is an out-of-memory exception. Continuing the process halts the Javascript execution, but continues to render the PDF.

Now, if our PDF contains a page open action with JavaScript code, JavaScript execution will resume with the engine in an undefined state, which quickly results in memory corruption and the following crash:

0:000> g
(754.1588): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=053deda8 ecx=00000000 edx=0d213ed0 esi=05300000 edi=0d1e62a0
eip=01d4dbf6 esp=053dec44 ebp=053dec54 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00210206
FoxitReader!CFXJSE_Arguments::GetValue+0xb3436:
01d4dbf6 8b4618          mov     eax,dword ptr [esi+18h] ds:002b:05300018=????????
0:000> k 6
 # ChildEBP RetAddr  
WARNING: Stack unwind information not available. Following frames may be wrong.
00 053dec54 01d4dd96 FoxitReader!CFXJSE_Arguments::GetValue+0xb3436
01 053dec6c 01d4d800 FoxitReader!CFXJSE_Arguments::GetValue+0xb35d6
02 053dec84 01d4de99 FoxitReader!CFXJSE_Arguments::GetValue+0xb3040
03 053dec98 01d4cd1f FoxitReader!CFXJSE_Arguments::GetValue+0xb36d9
04 053decb8 01db8ce1 FoxitReader!CFXJSE_Arguments::GetValue+0xb255f
05 053def54 01db81bc FoxitReader!CFXJSE_Arguments::GetValue+0x11e521

In this example, the crash is due to an access violation while trying to read an arbitrary memory address, but with precise control it could be further abused and could ultimately lead to arbitrary code execution.

Opening the supplied PoC PDF in Foxit Reader with PageHeap turned on leads to a quicker crash, but the same can be observed without PageHeap.

Timeline

2019-04-02 - Vendor Disclosure
2019-05-01 - 30 day follow up; Vendor advised issues under review
2019-06-11 - Vendor advised need to upgrade to Google V8 due to it crashing in Google V8
2019-06-12 - Talos informed 90 day mark 2019-07-02
2019-06-13 - Vendor requested disclosure timeline extension>br> 2019-07-30 - 120 day follow up; Talos granted disclosure extension to 2019-08-30
2019-08-08 - Vendor requested extension to 2019-09-30; Talos agreed
2019-09-25 - Vendor patched and provided test version to Talos
2019-09-30 - Public Release

Credit

Discovered by Aleksandar Nikolic of Cisco Talos.