Exploiting Internet Explorer's MS15-106, Part I: VBScript Filter Type Confusion Vulnerability (CVE-2015-6055)

In October 13, 2015 Microsoft published security bulletin MS15-106, addressing multiple vulnerabilities in Internet Explorer. Zero Day Initiative published advisory ZDI-15-521 for one of those vulnerabilities affecting IE: Microsoft Windows VBScript Filter Function Remote Code Execution Vulnerability (CVE-2015-6055), so I decided to take a shot at it. Quoting ZDI's advisory:

The vulnerability relates to the Filter function in VBScript. By passing unexpected arguments to this function, 
 an attacker can cause an integer to be incorrectly interpreted as a pointer to an object in memory. An attacker 
 can leverage this vulnerability to execute code under the context of the current process.
 

I started by downloading both the patched version of IE 11 for Windows 8.1 x64 (KB3093983 - MS15-106), and the last vulnerable version of IE 11 for Windows 8.1 x64 (KB3087038 - MS15-094). Therefore the analysis provided here is based on Internet Explorer 11 on Windows 8.1 x64, with vbscript.dll 5.8.9600.18052 as the fixed version, and vbscript.dll 5.8.9600.18036 as the vulnerable version.

Image
Internet Explorer-version

Text

Binary diffing

The ZDI advisory states that the vulnerability is related to the Filter function in VBScript. The Filter function exposed by VBScript is implemented in the vbscript!VbsFilter function at the binary level. By binary diffing the vulnerable version against the patched version, we can confirm that VbsFilter is one of the few functions being patched (I'm using Turbodiff by Nicolae Economou as my diffing tool):

Image
vbscript-changed-functions

Text

We can see that vbscript!VbsFilter is heavily modified between vulnerable (left) and patched (right) versions:

Image
vbsfilter-sidetoside

Text

This is the MSDN's description of VBScript's Filter function:

Image
filter-msdn

Text

Its implementation, that is, the vbscript!VbsFilter function at the binary level, does the following:

  • It starts by validating the number of received parameters. It accepts a minimum of 2 and a maximum of 4 parameters (the last 2 ones, Include and Compare, are optional). If Include is not provided, its default value is set to True; if Compare is not provided, its default value is set to 0 (vbBinaryCompare).
  • Then it verifies if the second argument (Value) is a string.
  • Then it obtains the VarType of the first argument (InputStrings), and checks if it's an array.
  • After performing all of these parameters validation, it calls vbscript!rtFilter to do the actual work.

I found out that the key difference between vulnerable version and patched version is how the first argument (InputStrings) is validated. According to the function's documentation, the first argument for Filter is supposed to be a "One-dimensional array of strings to be searched." Let's see the difference in the code that validates the first argument passed to the Filter function. (Click to enlarge - Left: vulnerable version / Right: Patched version)

Image
vbsfilter-patch

Text

In both the old and new versions, in the first basic block in yellow, the code calls the VAR::PvarGetVarVal function. That function returns a pointer to the given object's information; the first two bytes pointed by that pointer indicate the type of the object, as returned by the VarType function, e.g.: vbNull (1)vbLong (3)vbString (8)vbBoolean (11), etc. On the left side (vulnerable version) we can see that it grabs the VarType of the first argument, and it applies a mask to it (AND EAX, 2000h, in the red basic block). This way, the vulnerable code is checking if the first argument for Filter is ANY kind of array, since 0x2000 is the base VarType for VBScript arrays. To better understand this, let's read a fragment from the VarType function documentation:

The VarType function never returns the value for Array by itself. It is always added to some other value 
to indicate an array of a particular type[...]
For example, the value returned for an array of integers is calculated as 2 + 8192, or 8194.

Now, the interesting thing is that, although VBScript can deal with different types of arrays, arrays created by VBScript are ALWAYS implemented as an array of Variants no matter the element type of the array; so if you call VarType(arr) being arr any kind of array created from VBScript, it ALWAYS returns 0x200c, that is, 0x2000 | 0x0C, being 0x2000 = vbArray, and 0x0C = vbVariant. On the other hand, on the right side (the fixed version), we can see that checking if the first argument of Filter is an array is performed in a more strict way: the VarType must be exactly 0x200c or 0x600c (see the red basic blocks). 0x6000 isn't properly documented in MSDN, but it's the base VarType for multi-dimensional arrays in VBScript. So, the patched version of the VBScript engine makes sure that the first argument of Filter is either a uni-dimensional array of Variants, or a multi-dimensional array of Variants. No other array base type is accepted. In other words, it's ensuring that the first argument of Filter is a uni- or multi-dimensional array created from within the VBscript engine. It seems like this patch is fixing some kind of type confusion vulnerability.

Reproducing the bug

So at this point, the question is: is it possible to reach the code shown above with an array whose base type is NOT Variant? In order to do that, we'll need an array created by someone else than the VBScript engine. I googled for "vbscript vartype 8204" (8204 == 0x200C), and the first hit was this thread from Stack Overflow, where some guy asks "Why does VarType() ALWAYS return 8204 for Arrays?". One of the sub-comments is extremely interesting:

Image
stackoverflow-comment

Text

This seems promising! VBScript can eventually operate with non-Variant arrays, if they come from other origins rather than the VBScript engine, for example, from an ActiveX object. I tried to obtain a non-Variant array by instantiating some ADODB ActiveX objects as suggested there, like ADODB.Connection and ADODB.Recordset, which are marked as Safe for Scripting. However, despite being Safe for Scripting, when running in the Internet Zone, the user is asked for permission to instantiate these ActiveX objects, so that wasn't going to work, and I dropped the idea of getting non-Variant arrays from ActiveX. After some more googling I found another page, which mentions that content retrieved using XMLHttpRequest, that is, the responseBody property of a XMLHttpRequest object, is an array of bytes, with VarType == 8209 (8209 == 0x2011 == 0x2000 (vbArray) | 0x11 (vbByte)):

Image
vbs-forum

Text

That's great, because using XMLHttpRequest doesn't ask for user consent! Here's some example code:

function read_file(filename){
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.open("GET", filename, false);
    xmlhttp.send();
    return xmlhttp.responseBody;
}

So I wrote a first Proof-of-Concept that reaches the vulnerable code with a user-controlled array whose VarType is 0x2011, different than assumed 0x200C. It creates a XMLHttpRequest object from JS, uses it to obtain arbitrary data from our web server, and the resulting responseBody array is passed on to the VBScript code. The VBScript code shows a MessageBox to confirm that the VarType of said array is different than 0x200C, and it finally calls the Filter vulnerable function, with the responseBody array as the first argument.

<html>
<head>
  <meta http-equiv="x-ua-compatible" content="IE=10">
  <title>First PoC for MS15-106</title>
</head>
<body>
<script type="text/vbscript">
Function show_var_type(arg)
    Dim result

    '&H2011 = &H2000 (vbArray) | &H11 (vbByte)
    MsgBox(Hex(VarType(arg)))
    result = Filter(arg, "w00tw00t", 1, 1)
End Function

</script>

<script type="text/javascript">
function triggerjs(){
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.open("GET", "/some_data", false);
    xmlhttp.send();
    /* XMLHttpRequest.responseBody is a VBArray object containing the raw bytes. */
    return xmlhttp.responseBody;

}
</script>

<form>
<input type="button" value="PoC" name="conjs" onClick="javascript:show_var_type(triggerjs())"/>
</form>
</body>
</html>

Gaining EIP control

After reaching the vulnerable code using this PoC, the VBScript engine checks if the given array has 1 single dimension, and if that condition is true, then it calls vbscript!rtFilter:

Image
call-rtfilter

Text

After entering vbscript!rtFilter, we reach the following code. The ECX register is pointing directly to the raw data we've requested via XMLHttpRequest, and the VAR::BstrGetVal function is called. This function attempts to return the string representation of any kind of VBScript object. That means that at this exact point, and due to the type confusion vulnerability, arbitrary data controlled by us will be interpreted as a VBScript object.

Image
call-bstrgetval

Text

Let's enter VAR::BstrGetVal, which in turn calls VAR::PvarGetVarVal:

Image
bstrgetval

Text

VAR::PvarGetVarVal gets the VarType of our (fake) object, and if it's 9 (that is, vbObject), it calls VAR::ObjGetDefault:

Image
pvargetvarval

Text

Note that two instructions before calling VAR::ObjGetDefault (highlighted in yellow) there's a MOV ECX, DWORD [ECX+8] instruction. Remember that at that point ECX was pointing directly to our data, so that instruction loads a DWORD from offset 8 of our data into ECX. We enter VAR::ObjGetDefault, and we reach this exciting basic block:

Image
objgetdefault-icall

Text

The cool part is that we've got full control over the ESI register and there's an indirect call based on it, so that means remote code execution (assuming that we can find a way to bypass ASLR); the bad news is that, before that indirect call, there's a call to the Control Flow Guard validation function. So I created this simple Python script, which generates a file called "some_data", which is the one requested using XMLHttpRequest, containing our specially crafted fake VBScript object:

import struct

with open("some_data", "wb") as f:
    f.write(struct.pack('<H', 0x0009))        # varType == vbObject
    f.write(struct.pack('>H', 0x5051))        # dummy
    f.write(struct.pack('<L', 0x41414141))    # dummy

    # this is interpreted as a pointer and dereferenced twice to call a function pointer. 
    # On Windows 8.1, dword[0x7ffe0270] == 0x00000003  --> call dword[0x00000003]
    f.write(struct.pack('<L', 0x7ffe0270))
    f.write(struct.pack('<L', 0x43434343))    # dummy

Note that the first word of our data has value 0x0009, thus defining our fake object as being of type vbObject. This way we can reach the code path that leads us to arbitrary code execution: VbsFilter -> rtFilter -> VAR::BstrGetVal -> VAR::PvarGetVarVal -> VAR::ObjGetDefault -> CALL DWORD [ESI]. Also, the dword at offset 8 of our data is the one being interpreted as a pointer and dereferenced twice to call a function pointer. In this case, for demonstration purposes, our arbitrary pointer has value 0x7ffe0270, which is the fixed address of the NtMinorVersion field of the nt!_KUSER_SHARED_DATA data structure. That address holds the value 0x00000003 on Windows 8.1. If you run this PoC with the debugger attached to the browser process, you'll see that IE crashes here, when the CFG stub tries to load into ECX the function pointer stored at address 0x00000003, right before calling the CFG validation function: vbscript!VAR::ObjGetDefault+0x6f: 64af9179 8b0e            mov     ecx,dword ptr [esi]  ds:0023:00000003=???????? 0:006> u @eip vbscript!VAR::ObjGetDefault+0x6f: 64af9179 8b0e            mov     ecx,dword ptr [esi] 64af917b ff1534e3b364    call    dword ptr [vbscript!__guard_check_icall_fptr (64b3e334)] 64af9181 ff16            call    dword ptr [esi] 64af9183 3bfc            cmp     edi,esp 64af9185 0f8529590000    jne     vbscript!VAR::ObjGetDefault+0x7d (64afeab4) 64af918b 85c0            test    eax,eax 64af918d 0f883d230100    js      vbscript!VAR::ObjGetDefault+0x123c6 (64b0b4d0) 64af9193 8d442410        lea     eax,[esp+10h] Clearly, at this point we need to deal with ASLR first, and then CFG in order to get code execution.

Trying to use the same vulnerability to bypass ASLR

To make a long story short, I tried to take advantage of this vulnerability to both bypass ASLR and gain code execution. By playing with the type of our fake VBScript object (the one requested using XMLHttpRequest) it is possible to, for example, convert the dword stored at an arbitrary address to a string with that value in decimal representation. So the vulnerability looked very promising for ASLR bypassing purposes; however, being able to access leaked memory contents from our VBScript code requires us to return from vbscript!rtFilter with a non-negative value, otherwise the VBScript's Filter function won't return the results containing our leaked information. According to my tests, there's an (almost) unsatisfiable condition that needs to be satisfied in order to return from vbscript!rtFilter with a non-negative value: due to the type confusion issue, vbscript!rtFilter will loop through the number of elements of our fake array, assuming that the size of each element of our array is 0x10 bytes (the size of vbVariant); however, the size of each element of our array is 1 byte, since it's an array of vbByte elements. Let's say that we have a crafted array of 10 bytes; element count is 10, element size is 1, so total size is 10 bytes. However, for vbscript!rtFilter, element count is 10, but it assumes that element size is 0x10, so it will loop beyond our array, thinking that we've provided 160 bytes of data, trying to process that out-of-bounds data as Variant objects. There are some chances to overcome that seemingly unsatisfiable condition, like using heap manipulation techniques in order to put specially crafted data after our array of vbByte elements, so when vbscript!rtFilter runs beyond the end of our array it still manages to parse that data as well-formed Variant objects. However, this idea looked like a significant effort to me, so I decided to focus my energy on trying to exploit a second vulnerability from the same MS15-106 bulletin in order to bypass ASLR.

Conclusion

This type confusion vulnerability affecting the VBScript engine can be leveraged to gain code execution in the context of Internet Explorer in a straightforward way; by providing the vulnerable Filter function with a VBScript array with element type different than vbVariant as its first argument, it's possible for attacker-controlled data to get interpreted as a VBScript object, which ultimately leads to an indirect call which gets fully controlled by the attacker. Beyond clearly allowing for remote code execution, this vulnerability also looked promising for bypassing ASLR. However, due to the difference between expected array element size (0x10) and provided array element size (1), I wasn't able to make the vulnerable function return with no error, and that prevented my exploit code from being able to access leaked memory contents. That's why I decided to move on and try to exploit a second vulnerability from the same MS15-106 security bulletin in order to bypass the first hurdle: Address Space Layout RandomizationOh, and we'll still need to bypass Control Flow Guard if we are hoping to take this bug all the way to remote code execution... So stay tuned for the second part of these blogposts installments, in which we are going to discuss how to exploit a second vulnerability in the MS15-106 bulletin, this time a memory disclosure one, in order to bypass ASLR! Do you want to take smarter steps to reduce the vulnerabilities in your network? Core Impact Pro is packed with hundreds of unique CVEs with more added on a continuous basis. To further expand library of CVEs, Core offers the ability to integrate with open source frameworks, further increasing the amount of unique CVEs offered.