Talos Vulnerability Report


libxls read_MSAT Code Execution Vulnerability

November 15, 2017
CVE Number



An exploitable out-of-bounds write vulnerability exists in the read_MSAT function of libxls 1.4. A specially crafted XLS file can cause a memory corruption resulting in remote code execution. An attacker can send malicious XLS file to trigger this vulnerability.

Tested Versions

libxls 1.4 readxl package 1.0.0 for R (tested using Microsoft R 4.3.1)

Product URLs


CVSSv3 Score

8.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H CVSSv3 Calculator: https://www.first.org/cvss/calculator/3.0


CWE-787: Out-of-bounds Write


libxls is a C library supported on Windows, Mac and Linux which can read Microsoft Excel File Format (XLS) files. The library is used by the readxl package that can be installed in the R programming language. An out-of-bounds write appears in the read_MSAT function. Let's take a look at the vulnerable code:

Line 425    // Read MSAT
Line 426    static int read_MSAT(OLE2* ole2, OLE2Header* oleh)
Line 427    {
Line 428        int sectorNum;
Line 429
Line 430        // reconstitution of the MSAT
Line 431        ole2->SecID = malloc(ole2->cfat*ole2->lsector);
Line 432        (...)
Line 433           int posInSector;
Line 434
Line 435           // read MSAT sector
Line 436           sector_read(ole2, sector, sid);      
Line 437           // read content
Line 438           for (posInSector = 0; posInSector < (ole2->lsector-4)/4; posInSector++)
Line 439             {
Line 440              unsigned int s = *(int*)(sector + posInSector*4);
Line 441
Line 442              if (s != FREESECT)
Line 443                {
Line 444                 sector_read(ole2, (BYTE*)(ole2->SecID)+sectorNum*ole2->lsector, s);
Line 445                 sectorNum++;
Line 446                }
Line 447             }

As we can see in line 431 ole2->SecID buffer is allocated based on cfat and lsector value. The cfat value is read directly from the file (in our PoC cfat has value 0x1) where lsector has fixed size 0x200. Next in lines 438-447 we see that further "sectors" are read from the file (via sector_read) to the ole2->SecID buffer in the amount of (ole2->lsector-4)/4. We can observe a lack of any check whether the new calculated offset for "sector" inside the SecID buffer does not exceed the buffer size allocated earlier. This thus leads to out of bounds writes and heap memory corruption, which can potentially lead to arbitrary code execution.

Crash Information

Crash in Microsoft R platform:

path <- readxl_example("509075387a944995bb90bf109fe8191b.xls")
lapply(excel_sheets(path), read_excel, path = path)
fread: wanted 1 got 0 loc=68988965376
fread: wanted 1 got 0 loc=68988965376
fread: wanted 1 got 0 loc=68988965376
fread: wanted 1 got 0 loc=68988965376
fread: wanted 1 got 0 loc=68988965376
fread: wanted 1 got 0 loc=541435367936
fread: wanted 1 got 0 loc=1319413954048
fread: wanted 1 got 0 loc=547393582080
fread: wanted 1 got 0 loc=8355054592
fread: wanted 1 got 0 loc=132608
fread: wanted 1 got 0 loc=75611136
fread: wanted 1 got 0 loc=838861312
fread: wanted 1 got 0 loc=1536
fread: wanted 1 got 0 loc=67160064
fread: wanted 1 got 0 loc=637534720
fread: wanted 1 got 0 loc=137438955008
fread: wanted 1 got 0 loc=537149952

 *** caught segfault ***
address 0x30895e0, cause 'memory not mapped'
Segmentation fault

directly in libxls lib:

Starting program: /home/icewall/bugs/libxls-1.4.0/build/bin/xls2csv ./crashes/509075387a944995bb90bf109fe8191b

Program received signal SIGSEGV, Segmentation fault.
__mempcpy_sse2 () at ../sysdeps/x86_64/memcpy.S:125
125     ../sysdeps/x86_64/memcpy.S: No such file or directory.
(gdb) bt
#0  __mempcpy_sse2 () at ../sysdeps/x86_64/memcpy.S:125
#1  0x00007ffff787903e in __GI__IO_file_xsgetn (fp=0x603310, data=<optimized out>, n=512) at fileops.c:1392
#2  0x00007ffff786e236 in __GI__IO_fread (buf=<optimized out>, size=512, count=1, fp=0x603310) at iofread.c:38
#3  0x00007ffff7bd03e6 in sector_read (ole2=0x6032b0, buffer=0x623f50 '\b' <repeats 35 times>, "?\232\231\231\231\231\231\271?
       \001", sid=0) at ole.c:421
#4  0x00007ffff7bd0525 in read_MSAT (ole2=0x6032b0, oleh=0x6030a0) at ole.c:462
#5  0x00007ffff7bcfe33 in ole2_open (file=0x7fffffffe12c "./crashes/509075387a944995bb90bf109fe8191b", charset=0x400fce 
       "iso-8859-15//TRANSLIT") at ole.c:327
#6  0x00007ffff7bd2e00 in xls_open (file=0x7fffffffe12c "./crashes/509075387a944995bb90bf109fe8191b", charset=0x400fce 
       "iso-8859-15//TRANSLIT") at xls.c:910
#7  0x0000000000400957 in main (pintArgc=2, ptstrArgv=0x7fffffffdd78) at xls2csv.c:45
(gdb) frame 4
#4  0x00007ffff7bd0525 in read_MSAT (ole2=0x6032b0, oleh=0x6030a0) at ole.c:462
462                      sector_read(ole2, (BYTE*)(ole2->SecID)+sectorNum*ole2->lsector, s);
(gdb) p/x sectorNum 
$2 = 0xfd
(gdb) p/x ole2->lsector
$3 = 0x200
(gdb) p/x ole2->SecID 
$4 = 0x604550
(gdb) peda_active
gdb-peda$ vmmap 0x604550
Start              End                Perm      Name
0x00603000         0x00624000         rw-p      [heap]
gdb-peda$ 0x00624000-0x604550
Undefined command: "0x00624000-0x604550".  Try "help".
gdb-peda$ p 0x00624000-0x604550
$5 = 0x1fab0
gdb-peda$ p (BYTE*)(ole2->SecID)+sectorNum*ole2->lsector
$6 = (BYTE *) 0x623f50 '\b' <repeats 35 times>, "?\232\231\231\231\231\231\271?\001"
gdb-peda$ frame 3
#3  0x00007ffff7bd03e6 in sector_read (ole2=0x6032b0, buffer=0x623f50 '\b' <repeats 35 times>, "?\232\231\231\231\231\231\271?
     \001", sid=0x0) at ole.c:421
421         fread(buffer, ole2->lsector, 1, ole2->file);

RAX: 0xffffffffffffffff 
RBX: 0x603310 --> 0xfbad2488 
RCX: 0xffffffffffffffff 
RDX: 0x10 
RSI: 0x6035e3 --> 0xffffffffffffffff 
RDI: 0x623ff3 --> 0xffffffffffffffff 
RBP: 0xb3 
RSP: 0x7fffffffdac8 --> 0x7ffff787903e (<__GI__IO_file_xsgetn+382>:     add    QWORD PTR [rbx+0x8],rbp)
RIP: 0x7ffff788f41a (<__mempcpy_sse2+106>:      mov    QWORD PTR [rdi+0x8],r8)
R8 : 0x6e69ffffffffffff 
R9 : 0xffffffffffffffff 
R10: 0xffffffffffffffff 
R11: 0x246 
R12: 0x14d 
R13: 0x200 
R14: 0x623f50 --> 0x808080808080808 
R15: 0x0
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
   0x7ffff788f410 <__mempcpy_sse2+96>:  mov    rcx,QWORD PTR [rsi]
   0x7ffff788f413 <__mempcpy_sse2+99>:  mov    r8,QWORD PTR [rsi+0x8]
   0x7ffff788f417 <__mempcpy_sse2+103>: mov    QWORD PTR [rdi],rcx
=> 0x7ffff788f41a <__mempcpy_sse2+106>: mov    QWORD PTR [rdi+0x8],r8
   0x7ffff788f41e <__mempcpy_sse2+110>: sub    edx,0x10
   0x7ffff788f421 <__mempcpy_sse2+113>: lea    rsi,[rsi+0x10]
   0x7ffff788f425 <__mempcpy_sse2+117>: lea    rdi,[rdi+0x10]
   0x7ffff788f429 <__mempcpy_sse2+121>: jne    0x7ffff788f410 <__mempcpy_sse2+96>
0000| 0x7fffffffdac8 --> 0x7ffff787903e (<__GI__IO_file_xsgetn+382>:    add    QWORD PTR [rbx+0x8],rbp)
0008| 0x7fffffffdad0 --> 0x603310 --> 0xfbad2488 
0016| 0x7fffffffdad8 --> 0x200 
0024| 0x7fffffffdae0 --> 0x200 
0032| 0x7fffffffdae8 --> 0x1 
0040| 0x7fffffffdaf0 --> 0x0 
0048| 0x7fffffffdaf8 --> 0x7ffff786e236 (<__GI__IO_fread+150>:  test   DWORD PTR [rbx],0x8000)
0056| 0x7fffffffdb00 --> 0x400820 (<_start>:    xor    ebp,ebp)
Legend: code, data, rodata, value
Stopped reason: SIGSEGV


2017-08-29 - Vendor Disclosure
2017-11-15 - Public Release


Discovered by Marcin 'Icewall' Noga of Cisco Talos.