Headline
CVE-2017-2897: TALOS-2017-0404 || Cisco Talos Intelligence Group
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.
Summary
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
http://libxls.sourceforge.net/
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
CWE-787: Out-of-bounds Write
Details
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:
library(readxl)
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);
[----------------------------------registers-----------------------------------]
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)
[-------------------------------------code-------------------------------------]
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>
[------------------------------------stack-------------------------------------]
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
gdb-peda$
Timeline
2017-08-29 - Vendor Disclosure
2017-11-15 - Public Release
Discovered by Marcin ‘Icewall’ Noga of Cisco Talos.