├── .gitignore ├── README.md ├── setup.py └── smith ├── __init__.py ├── api.py ├── cli.py ├── scapy_wrapper.py └── wsgiref_wrapper.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smith 2 | A client/server style agent meant for testing connectivity to and from a machine on a network. 3 | 4 | ## Installation 5 | ```python setup.py install``` or ```pip install .``` should install smith. 6 | Note: If you want to use the tcp/udp protocol options, you'll need to install scapy and it's dependencies. 7 | Ubuntu has 'apt-get install python-scapy'. You can also pip install scapy, but I don't know if that 8 | installs all dependencies on all OS's. I didn't include scapy in the requires because the 'rest' option 9 | doesn't utilize it, and is sufficient for a lot of usecases on its own. 10 | 11 | -- 12 | 13 | ## Functions: ping and listen 14 | 15 | ## ping 16 | ```bash 17 | $: smith ping -h 18 | 19 | usage: 20 | Initiate a port-specific ping against a listening agent 21 | 22 | positional arguments: 23 | port The port the remote agent is listening on 24 | destination IPv4 address of the server the remote agent is 25 | listening on 26 | {TCP,UDP,REST} Protocol to use to contact the remote agent. TCP and 27 | UDP use raw sockets which will bypass IPTABLES rules. 28 | 29 | optional arguments: 30 | -h, --help show this help message and exit 31 | -t TIMEOUT, --timeout TIMEOUT 32 | Seconds to wait for response from server before giving 33 | up. Zero means 'wait forever' 34 | ``` 35 | ### Example 36 | ```bash 37 | $: smith ping 12345 127.0.0.1 REST --timeout 10 38 | ``` 39 | --- 40 | 41 | ## listen 42 | ```bash 43 | $: smith listen -h 44 | usage: 45 | Server-side: listen for incoming ping requests from remote client. 46 | 47 | positional arguments: 48 | port The port the remote client is pinging 49 | {TCP,UDP,REST} Protocol to use to contact the remote agent.TCP and UDP use 50 | raw sockets which will bypass IPTABLES rules. 51 | 52 | optional arguments: 53 | '-t', '--timeout'Seconds to wait for request from client before giving up. Zero (default) means 'wait forever' 54 | -h, --help show this help message and exit 55 | ``` 56 | 57 | ### Example 58 | ```bash 59 | $: smith listen 12345 REST --timeout 60 60 | ``` 61 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | # Normal setup stuff 4 | setup( 5 | name='smith', 6 | packages=find_packages(), 7 | zip_safe=False, 8 | entry_points={ 9 | 'console_scripts': [ 10 | 'smith = smith.cli:cli'] 11 | }, 12 | ) 13 | -------------------------------------------------------------------------------- /smith/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jidar/smith/5bc145cc61cd3cd29a2377c45cb53c6aba25bcee/smith/__init__.py -------------------------------------------------------------------------------- /smith/api.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | PROTOCOLS = ['udp', 'tcp', 'rest'] 4 | 5 | def ping(port, destination, protocol, timeout=10): 6 | """ 7 | Send a request appropriate for the requested protocol to destination at port. 8 | Listen for timeout seconds for a well-known response. 9 | """ 10 | 11 | resp = None 12 | if protocol == "rest": 13 | import wsgiref_wrapper 14 | resp = wsgiref_wrapper.get_and_check(destination, port) 15 | return True if resp else False 16 | else: 17 | import scapy_wrapper 18 | resp = scapy_wrapper.send_and_listen( 19 | destination_ip=destination, 20 | destination_port=port, 21 | protocol=protocol, 22 | timeout=timeout) 23 | #TODO: Make this check better 24 | try: 25 | if resp[0].res: 26 | return True 27 | except IndexError: 28 | return False 29 | 30 | 31 | def listen(port, protocol, timeout=3600): 32 | """ 33 | Initiate a listener for the given protocol that will listen for timeout seconds. 34 | """ 35 | 36 | if protocol == 'tcp': 37 | import scapy_wrapper 38 | scapy_wrapper.listen( 39 | port=port, 40 | protocol=protocol, 41 | reaction=scapy_wrapper.Reactions.respond_tcp, 42 | timeout=timeout) 43 | elif protocol == 'udp': 44 | import scapy_wrapper 45 | scapy_wrapper.listen( 46 | port=port, 47 | protocol=protocol, 48 | reaction=scapy_wrapper.Reactions.respond_udp, 49 | timeout=timeout) 50 | elif protocol == 'rest': 51 | import wsgiref_wrapper 52 | wsgiref_wrapper.start_server(port, timeout=timeout) 53 | else: 54 | raise Exception("Unsupported protocol type") 55 | -------------------------------------------------------------------------------- /smith/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from smith import api 3 | 4 | 5 | class Command(object): 6 | def __init__(self, args): 7 | self.args = args 8 | self.run() 9 | 10 | def run(self): 11 | raise NotImplemented 12 | 13 | 14 | class smith_ping(Command): 15 | def run(self): 16 | r = api.ping( 17 | self.args.port, 18 | self.args.destination, 19 | self.args.protocol, 20 | self.args.timeout) 21 | print 'success' if r else 'failure' 22 | exit(0 if r else 1) 23 | 24 | 25 | class smith_listen(Command): 26 | def run(self): 27 | api.listen( 28 | self.args.port, 29 | self.args.protocol, 30 | timeout=self.args.timeout) 31 | 32 | 33 | def cli(): 34 | parser = argparse.ArgumentParser() 35 | subparsers = parser.add_subparsers(metavar="ping, listen") 36 | 37 | # Set up ping command args 38 | ping_parser = subparsers.add_parser( 39 | "ping", usage="\n Initiate a port-specific ping against a listening agent") 40 | ping_parser.add_argument( 41 | "port", type=int, help="The port the remote agent is listening on") 42 | ping_parser.add_argument( 43 | "destination", type=str, help="IPv4 address of the server the remote agent is listening on") 44 | ping_parser.add_argument( 45 | "protocol", 46 | type=str, 47 | choices=api.PROTOCOLS, 48 | help="Protocol to use to contact the remote agent. TCP and UDP use raw " 49 | "sockets which will bypass IPTABLES rules.", 50 | ) 51 | ping_parser.add_argument( 52 | '-t', '--timeout', default=10, type=int, help="Seconds to wait for response from server before giving up. Zero means 'wait forever'") 53 | ping_parser.set_defaults(func=smith_ping) 54 | 55 | # Set up listen command args 56 | listen_parser = subparsers.add_parser( 57 | "listen", usage="\n Server-side: listen for incoming ping requests from remote client.") 58 | listen_parser.add_argument( 59 | "port", type=int, help="The port the remote client is pinging") 60 | listen_parser.add_argument( 61 | "protocol", 62 | type=str, 63 | choices=api.PROTOCOLS, 64 | help=( 65 | "Protocol to use to contact the remote agent." 66 | "TCP and UDP use raw sockets which will bypass IPTABLES rules.")) 67 | listen_parser.add_argument( 68 | '-t', '--timeout', default=0, type=int, help="Seconds to wait for request from client before giving up. Zero means 'wait forever'") 69 | listen_parser.set_defaults(func=smith_listen) 70 | 71 | args = parser.parse_args() 72 | args.func(args) 73 | -------------------------------------------------------------------------------- /smith/scapy_wrapper.py: -------------------------------------------------------------------------------- 1 | from scapy.all import * 2 | 3 | class CONST(object): 4 | """ 5 | These strings serve as the known payloads for the packets the client and server will send 6 | eachother. 7 | """ 8 | initiator_string = "INIT PACKET" 9 | responder_string = "RESP PACKET" 10 | 11 | class Reactions(object): 12 | 13 | @staticmethod 14 | def respond_tcp(packet): 15 | """ 16 | If a TCP initiator packet is detected, create and send a TCP response packet to the 17 | originator of the initiator packet at the originating port. 18 | 19 | A TCP initiator packet is denoted by the initiator_string being it's only payload 20 | """ 21 | 22 | if not packet.payload.load == CONST.initiator_string: 23 | return 24 | 25 | src=None 26 | dst=None 27 | sport=None 28 | dport=None 29 | if 'IP' in packet: 30 | src=packet['IP'].src 31 | dst=packet['IP'].dst 32 | if 'TCP' in packet: 33 | sport=packet['TCP'].sport 34 | dport=packet['TCP'].dport 35 | elif 'UDP' in packet: 36 | sport=packet['UDP'].sport 37 | dport=packet['UDP'].dport 38 | 39 | send( 40 | IP(dst=src, src=dst) 41 | /TCP(dport=sport, sport=dport) 42 | /Raw(load=CONST.responder_string) ) 43 | 44 | @staticmethod 45 | def respond_udp(packet): 46 | """ 47 | If a UDP initiator packet is detected, create and send a UDP response packet to the 48 | originator of the initiator packet at the originating port. 49 | 50 | A UDP initiator packet is denoted by the initiator_string being it's only payload 51 | """ 52 | # create a packet 53 | # send it back to the originator of this packet 54 | 55 | if not packet.payload.load == CONST.initiator_string: 56 | return 57 | 58 | src=None 59 | dst=None 60 | sport=None 61 | dport=None 62 | if 'IP' in packet: 63 | src=packet['IP'].src 64 | dst=packet['IP'].dst 65 | if 'UDP' in packet: 66 | sport=packet['UDP'].sport 67 | dport=packet['UDP'].dport 68 | 69 | send( 70 | IP(dst=src, src=dst) 71 | /UDP(dport=sport, sport=dport) 72 | /Raw(load=CONST.responder_string) ) 73 | 74 | @staticmethod 75 | def return_printable(packet): 76 | # For testing and debug purposes. Eventually, I plan to add more 77 | # methods for doing stuff other than just returning the packet, 78 | # including possibly chaining reaction methods. 79 | return "Packet {0} ==> {1}: {2}".format( 80 | packet[0][1].src, packet[0][1].dst, packet.payload.load) 81 | 82 | 83 | def send_and_listen( 84 | destination_ip=None, 85 | destination_port=None, 86 | protocol=None, 87 | timeout=0): 88 | 89 | """ 90 | Craft and send an initiator packet of type 'protocol' to the given 'destination_ip' at 91 | 'destination_port'. 92 | Once sent, method will listen for a response for 'timeout' seconds (0 == Indefinitely). 93 | If a response packet is detected, response packet is returned. 94 | """ 95 | 96 | resp = None 97 | if protocol == 'tcp': 98 | resp = sr( 99 | IP(dst=destination_ip) 100 | /TCP(dport=destination_port) 101 | /Raw(load=CONST.initiator_string), 102 | timeout=timeout 103 | ) 104 | elif protocol == 'udp': 105 | resp = sr( 106 | IP(dst=destination_ip) 107 | /UDP(dport=destination_port) 108 | /Raw(load=CONST.initiator_string), 109 | timeout=timeout 110 | ) 111 | else: 112 | print( 113 | "Request to send/listen for unsupported " 114 | "protocol: {0}".format(protocol)) 115 | exit() 116 | 117 | return resp 118 | 119 | def listen( 120 | port=None, 121 | protocol=None, 122 | interface=None, 123 | reaction=None, 124 | timeout=None, 125 | bpf_override=None): 126 | """ 127 | Uses scapy lib's sniff() method to filter packets across all interfaces. Sniff() will run the 128 | 'reaction' method when a sniffed packet matches the given parameters: 129 | destination port, interface, or protocol. 130 | 131 | bpf_override is a string that adheres to the Berkeley Packet Filter syntax 132 | (http://biot.com/capstats/bpf.html) 133 | default bpf is 'ip dst port {port}' 134 | """ 135 | 136 | timeout = None if timeout == 0 else timeout 137 | interface_str = "on {0}".format(interface) if interface else "" 138 | proto_str = "{0}".format(protocol) if protocol else "" 139 | port_str = "dst port {0}".format(port) if port else "" 140 | bpf = "{interface} {protocol} {port}".format( 141 | interface = interface_str, protocol=proto_str, port=port_str) 142 | bpf = bpf_override if bpf_override else bpf 143 | bpf = bpf.strip() 144 | 145 | # Setup sniff, filtering for IP traffic 146 | # example: scapy_all.sniff(filter="ip",prn=customAction) 147 | sniff(filter=bpf, prn=reaction, timeout=timeout) 148 | -------------------------------------------------------------------------------- /smith/wsgiref_wrapper.py: -------------------------------------------------------------------------------- 1 | from wsgiref.util import setup_testing_defaults 2 | from wsgiref.simple_server import make_server 3 | from time import sleep 4 | import threading 5 | 6 | class CONST(object): 7 | agent_response='agent-smith-response' 8 | 9 | def simple_app(environ, start_response): 10 | """ 11 | Respond to a GET with a 200 and the known agent_response string. 12 | """ 13 | 14 | setup_testing_defaults(environ) 15 | status = '200 OK' 16 | headers = [('Content-type', 'text/plain')] 17 | start_response(status, headers) 18 | return CONST.agent_response 19 | 20 | def _server_killer(httpd=None, timeout=None): 21 | """ 22 | Used to kill the server daemon after a given timeout. Does not run if 23 | timeout not provided. 24 | """ 25 | if timeout: 26 | sleep(timeout) 27 | httpd.shutdown() 28 | 29 | def start_server(port, timeout=None): 30 | """ 31 | Create and start a wsgiref server daemon. 32 | Initialize a thread to kill the server daemon after 'timeout' seconds 33 | if timeout is provided. 34 | """ 35 | 36 | # Create the simple server 37 | httpd = make_server('', port, simple_app) 38 | 39 | # Start the thread that will kill the server after timeout seconds 40 | threading.Thread( 41 | target=_server_killer, 42 | kwargs={'httpd':httpd, 'timeout':timeout}).start() 43 | 44 | # Start the server 45 | httpd.serve_forever() 46 | 47 | def get_and_check(destination, port): 48 | """ 49 | Send a GET request to 'destination' at 'port'. 50 | If a positive response is detected, return True. 51 | """ 52 | import urllib2 53 | import sys 54 | url ="http://{destination}:{port}".format( 55 | destination=destination, port=port) 56 | 57 | response = urllib2.urlopen(url) 58 | html = response.read() 59 | 60 | if html == CONST.agent_response: 61 | return True 62 | return False 63 | --------------------------------------------------------------------------------