Repository: ring04h/thorns Branch: master Commit: 65dda993878e Files: 28 Total size: 164.8 KB Directory structure: gitextract_4m8vr42h/ ├── ElasticsearchPlugin.py ├── README.md ├── libnmap/ │ ├── __init__.py │ ├── diff.py │ ├── objects/ │ │ ├── __init__.py │ │ ├── cpe.py │ │ ├── host.py │ │ ├── os.py │ │ ├── report.py │ │ └── service.py │ ├── parser.py │ ├── plugins/ │ │ ├── __init__.py │ │ ├── backend_host.py │ │ ├── backend_service.py │ │ ├── backendplugin.py │ │ ├── backendpluginFactory.py │ │ ├── es.py │ │ ├── mongodb.py │ │ ├── s3.py │ │ └── sql.py │ ├── process.py │ └── reportjson.py ├── run.py ├── src/ │ ├── supervisord_client.conf │ └── supervisord_server.conf ├── tasks.py ├── wyfunc.py └── wyportmap.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: ElasticsearchPlugin.py ================================================ #!/usr/bin/env python from libnmap.parser import NmapParser from libnmap.reportjson import ReportDecoder from libnmap.plugins.es import NmapElasticsearchPlugin from datetime import datetime import json nmap_report = NmapParser.parse_fromfile('libnmap/test/files/1_hosts.xml') mindex = datetime.fromtimestamp(nmap_report.started).strftime('%Y-%m-%d') db = NmapElasticsearchPlugin(index=mindex) dbid = db.insert(nmap_report) nmap_json = db.get(dbid) nmap_obj = json.loads(json.dumps(nmap_json), cls=ReportDecoder) print(nmap_obj) #print(db.getall()) ================================================ FILE: README.md ================================================ # thorns thorns_project 分布式异步队列系统 运行流程 ----------------------------------- * 启动redis内存服务器,作为队列存储数据库使用 * 配置芹菜(celery)运行环境,并连接redis队列内存,读取执行任务,并返回结果存储到后端MySQL数据库 * 配置任务控制台花花(flower),并连接redis队列内存,管理所有worker客户端与执行的任务队列 * 通过run.py脚本调用celery向队列压入任务 * 通过flower的http api脚本调用api向队列压入任务 * 任务执行的结果自动存入后端数据库 反馈 ----------------------------------- > 微博:http://weibo.com/ringzero
> 邮箱:ringzero@0x557.org
运行环境 ----------------------------------- * CentOS、Kali Linux、Ubuntu、Debian * Python 2.7.x * Redis * MysQL * Celery * Tornado * Supervisord 安装配置说明 ----------------------------------- ## CentOS 服务端 #### 安装 Redis-Server $ wget http://download.redis.io/releases/redis-2.8.19.tar.gz $ tar xzf redis-2.8.19.tar.gz $ cd redis-2.8.19 $ make $ sudo cp src/redis-server /usr/bin/ $ sudo cp redis.conf /etc/redis.conf /* 修改 /etc/redis.conf 37行,将daemonize no改为daemonize yes,让redis后台运行 */ $ sudo vim /etc/redis.conf daemonize yes # 启动Redis-Server $ sudo redis-server /etc/redis.conf #### 安装 pip $ wget https://pypi.python.org/packages/source/p/pip/pip-6.0.8.tar.gz $ tar zvxf pip-6.0.8.tar.gz $ cd pip-6.0.8 $ sudo python setup.py install #### 安装 MySQL-python $ sudo yum -y install python-devel mysql-devel subversion-devel $ sudo pip install MySQL-python SQLAlchemy #### 安装 Celery $ sudo pip install -U celery[redis] #### 安装 Flower & 下载thorns运行环境代码 $ cd /home/ $ sudo yum -y install git $ git clone https://github.com/ring04h/thorns.git $ cd /home/thorns/src $ tar zvxf flower.tar.gz $ cd flower-0.7.3 $ python setup.py install /* 启动Flower 这里的redis ip可以配置为你的外网的IP */ $ celery flower --port=8080 --broker=redis://127.0.0.1:6379/0 & 建议使用Supervisord的守护进程来启动Flower,确保系统7*24小时的稳定性 #### 安装 Supervisord $ sudo pip install supervisor $ sudo cp /home/thorns/src/supervisord_server.conf /etc/supervisord.conf /* 修改 /etc/supervisord.conf 141行 修改redis ip为你自己的ip --broker=redis://127.0.0.1:6379/0 */ /* 修改 /etc/supervisord.conf 153行 修改programe为你想定义的worker名称 [program:worker-ringzero] */ $ sudo vim /etc/supervisord.conf /* 启动 supervisord */ $ supervisord -c /etc/supervisord.conf http://127.0.0.1:9001/ 可以在线守护管理thorns的进程,实现远程重启 #### 检查各服务是否正常启动后,开始配置客户端任务脚本 1、http://youip:8080/ thorns 控制台 2、http://youip:9001/ supervisord 控制台 3、修改tasks.py内的芹菜配置 对应你自己的redis-server服务器IP BROKER_URL = 'redis://120.132.54.90:6379/0', 对应你自己的MySQL-server服务器IP CELERY_RESULT_BACKEND = 'db+mysql://celery:celery1@3Wscan@42.62.52.62:443/wscan', 配置完毕后,就可以部署多台客户端进行分布式任务执行了 ## CentOS 客户端(建议大规模部署) # 安装 git & 下载 thorns_project $ sudo yum -y install git $ cd /home/ $ git clone https://github.com/ring04h/thorns.git # 安装 pip $ wget https://pypi.python.org/packages/source/p/pip/pip-6.0.8.tar.gz $ tar zvxf pip-6.0.8.tar.gz $ cd pip-6.0.8 $ sudo python setup.py install # 安装 MySQL-python $ sudo yum -y install python-devel mysql-devel subversion-devel $ sudo pip install MySQL-python SQLAlchemy # 安装 nmap # 32位系统 $ sudo rpm -vhU https://nmap.org/dist/nmap-6.47-1.i386.rpm # 64位系统 $ sudo rpm -vhU https://nmap.org/dist/nmap-6.47-1.x86_64.rpm # 安装 Celery $ sudo pip install -U celery[redis] # 安装 Supervisord $ sudo pip install supervisor $ sudo cp /home/thorns/src/supervisord_client.conf /etc/supervisord.conf /* 修改 /etc/supervisord.conf 140行 修改programe为你想定义的worker名称 [program:worker-ringzero] */ $ sudo vim /etc/supervisord.conf /* 启动 supervisord */ $ supervisord -c /etc/supervisord.conf http://127.0.0.1:9001/ 可以在线守护管理thorns的进程,实现远程重启 # 修改tasks.py内的芹菜配置(分布式任务关键配置项) 对应你自己的redis-server服务器IP BROKER_URL = 'redis://120.132.54.90:6379/0', 对应你自己的MySQL-server服务器IP CELERY_RESULT_BACKEND = 'db+mysql://celery:celery1@3Wscan@42.62.52.62:443/wscan', # 环境搭建完毕,这时候访问thorns project的控制台,就会发现worker客户端已经出现在那里 演示地址:http://thorns.wuyun.org:8080/ 你的请访问:http://youip:8080/ 使用说明(可客户端发起任务也可http api发起任务) ----------------------------------- #### 命令行调用 在你的任意一台worker客户端,或者thorns服务端 $ cd /home/thorns/ $ python run.py 42.62.52.1-42.62.62.254 188 $ python run.py 42.62.52.1-254 189 均可以向redis压入nmap扫描任务,worker客户端的分布式集群会自动分发任务执行,并存储到后台数据库 记得修改wyportmap.py里面的扫描结果,存到你自己的数据库 reinhard-mbp:thorns reinhard$ python run.py 42.62.52.1-254 189 -------------------------------------------------- * push 42.62.52.1 to Redis * AsyncResult:23147d02-294d-41e5-84e5-5e1b15e72fc4 -------------------------------------------------- * push 42.62.52.2 to Redis * AsyncResult:542984a4-4434-475f-9a62-bfc81206ea57 -------------------------------------------------- * push 42.62.52.3 to Redis * AsyncResult:7d005661-d719-41ef-babc-4c853b2c49cc -------------------------------------------------- * push 42.62.52.4 to Redis * AsyncResult:ddcf9486-09d9-4dd2-9bb4-2618e6a161b8 -------------------------------------------------- wyportmap相关帮助: https://github.com/ring04h/wyportmap #### HTTP API 远程调用 重启 worker 线程池: $ curl -X POST http://thorns.wuyun.org:8080/api/worker/pool/restart/myworker 远程调用HTTP API启动一个nmap扫描任务: $ curl -X POST -d '{"args":["42.62.52.62",2222]}' http://thorns.wuyun.org:8080/api/task/send-task/tasks.nmap_dispath 强制结束一个正在执行的任务: $ curl -X POST -d 'terminate=True' http://thorns.wuyun.org:8088/api/task/revoke/a9361c1b-fd1d-4f48-9be2-8656a57e906b ================================================ FILE: libnmap/__init__.py ================================================ # -*- coding: utf-8 -*- __author__ = 'Ronald Bister, Mike Boutillier' __credits__ = ['Ronald Bister', 'Mike Boutillier'] __maintainer__ = 'Ronald Bister' __email__ = 'mini.pelle@gmail.com' __license__ = 'CC-BY' __version__ = '0.6.1' ================================================ FILE: libnmap/diff.py ================================================ # -*- coding: utf-8 -*- class DictDiffer(object): """ Calculate the difference between two dictionaries as: (1) items added (2) items removed (3) keys same in both but changed values (4) keys same in both and unchanged values """ def __init__(self, current_dict, past_dict): self.current_dict = current_dict self.past_dict = past_dict self.set_current = set(current_dict.keys()) self.set_past = set(past_dict.keys()) self.intersect = self.set_current.intersection(self.set_past) def added(self): return self.set_current - self.intersect def removed(self): return self.set_past - self.intersect def changed(self): return (set(o for o in self.intersect if self.past_dict[o] != self.current_dict[o])) def unchanged(self): return (set(o for o in self.intersect if self.past_dict[o] == self.current_dict[o])) class NmapDiff(DictDiffer): """ NmapDiff compares two objects of same type to enable the user to check: - what has changed - what has been added - what has been removed - what was kept unchanged NmapDiff inherit from DictDiffer which makes the actual comparaison. The different methods from DictDiffer used by NmapDiff are the following: - NmapDiff.changed() - NmapDiff.added() - NmapDiff.removed() - NmapDiff.unchanged() Each of the returns a python set() of key which have changed in the compared objects. To check the different keys that could be returned, refer to the get_dict() method of the objects you which to compare (i.e: libnmap.objects.NmapHost, NmapService,...). """ def __init__(self, nmap_obj1, nmap_obj2): """ Constructor of NmapDiff: - Checks if the two objects are of the same class - Checks if the objects are "comparable" via a call to id() (dirty) - Inherits from DictDiffer and """ if(nmap_obj1.__class__ != nmap_obj2.__class__ or nmap_obj1.id != nmap_obj2.id): raise NmapDiffException("Comparing objects with non-matching id") self.object1 = nmap_obj1.get_dict() self.object2 = nmap_obj2.get_dict() DictDiffer.__init__(self, self.object1, self.object2) def __repr__(self): return ("added: [{0}] -- changed: [{1}] -- " "unchanged: [{2}] -- removed [{3}]".format(self.added(), self.changed(), self.unchanged(), self.removed())) class NmapDiffException(Exception): def __init__(self, msg): self.msg = msg ================================================ FILE: libnmap/objects/__init__.py ================================================ # -*- coding: utf-8 -*- from libnmap.objects.report import NmapReport from libnmap.objects.host import NmapHost from libnmap.objects.service import NmapService __all__ = ['NmapReport', 'NmapHost', 'NmapService'] ================================================ FILE: libnmap/objects/cpe.py ================================================ # -*- coding: utf-8 -*- class CPE(object): """ CPE class offers an API for basic CPE objects. These objects could be found in NmapService or in tag within NmapHost. :todo: interpret CPE string and provide appropriate API """ def __init__(self, cpestring): self._cpestring = cpestring self.cpedict = {} zk = ['cpe', 'part', 'vendor', 'product', 'version', 'update', 'edition', 'language'] self._cpedict = dict((k, '') for k in zk) splitup = cpestring.split(':') self._cpedict.update(dict(zip(zk, splitup))) @property def cpestring(self): """ Accessor for the full CPE string. """ return self._cpestring def __repr__(self): return self._cpestring def get_part(self): """ Returns the cpe part (/o, /h, /a) """ return self._cpedict['part'] def get_vendor(self): """ Returns the vendor name """ return self._cpedict['vendor'] def get_product(self): """ Returns the product name """ return self._cpedict['product'] def get_version(self): """ Returns the version of the cpe """ return self._cpedict['version'] def get_update(self): """ Returns the update version """ return self._cpedict['update'] def get_edition(self): """ Returns the cpe edition """ return self._cpedict['edition'] def get_language(self): """ Returns the cpe language """ return self._cpedict['language'] def is_application(self): """ Returns True if cpe describes an application """ return (self.get_part() == '/a') def is_hardware(self): """ Returns True if cpe describes a hardware """ return (self.get_part() == '/h') def is_operating_system(self): """ Returns True if cpe describes an operating system """ return (self.get_part() == '/o') ================================================ FILE: libnmap/objects/host.py ================================================ # -*- coding: utf-8 -*- from libnmap.diff import NmapDiff from libnmap.objects.os import NmapOSFingerprint class NmapHost(object): """ NmapHost is a class representing a host object of NmapReport """ def __init__(self, starttime='', endtime='', address=None, status=None, hostnames=None, services=None, extras=None): """ NmapHost constructor :param starttime: unix timestamp of when the scan against that host started :type starttime: string :param endtime: unix timestamp of when the scan against that host ended :type endtime: string :param address: dict ie :{'addr': '127.0.0.1', 'addrtype': 'ipv4'} :param status: dict ie:{'reason': 'localhost-response', 'state': 'up'} :return: NmapHost: """ self._starttime = starttime self._endtime = endtime self._hostnames = hostnames if hostnames is not None else [] self._status = status if status is not None else {} self._services = services if services is not None else [] self._extras = extras if extras is not None else {} self._osfingerprinted = False self.os = None if 'os' in self._extras: self.os = NmapOSFingerprint(self._extras['os']) self._osfingerprinted = True else: self.os = NmapOSFingerprint({}) self._ipv4_addr = None self._ipv6_addr = None self._mac_addr = None self._vendor = None for addr in address: if addr['addrtype'] == "ipv4": self._ipv4_addr = addr['addr'] elif addr['addrtype'] == 'ipv6': self._ipv6_addr = addr['addr'] elif addr['addrtype'] == 'mac': self._mac_addr = addr['addr'] if 'vendor' in addr: self._vendor = addr['vendor'] self._main_address = self._ipv4_addr or self._ipv6_addr or '' self._address = address def __eq__(self, other): """ Compare eq NmapHost based on : - hostnames - address - if an associated services has changed :return: boolean """ rval = False if(self.__class__ == other.__class__ and self.id == other.id): rval = (self.changed(other) == 0) return rval def __ne__(self, other): """ Compare ne NmapHost based on: - hostnames - address - if an associated services has changed :return: boolean """ rval = True if(self.__class__ == other.__class__ and self.id == other.id): rval = (self.changed(other) > 0) return rval def __repr__(self): """ String representing the object :return: string """ return "{0}: [{1} ({2}) - {3}]".format(self.__class__.__name__, self.address, " ".join(self._hostnames), self.status) def __hash__(self): """ Hash is needed to be able to use our object in sets :return: hash """ return (hash(self.status) ^ hash(self.address) ^ hash(frozenset(self._services)) ^ hash(frozenset(" ".join(self._hostnames)))) def changed(self, other): """ return the number of attribute who have changed :param other: NmapHost object to compare :return int """ return len(self.diff(other).changed()) def save(self, backend): if backend is not None: _id = backend.insert(self) else: raise RuntimeError return _id @property def starttime(self): """ Accessor for the unix timestamp of when the scan was started :return: string """ return self._starttime @property def endtime(self): """ Accessor for the unix timestamp of when the scan ended :return: string """ return self._endtime @property def address(self): """ Accessor for the IP address of the scanned host :return: IP address as a string """ return self._main_address @address.setter def address(self, addrdict): """ Setter for the address dictionnary. :param addrdict: valid dict is {'addr': '1.1.1.1', 'addrtype': 'ipv4'} """ if addrdict['addrtype'] == 'ipv4': self._ipv4_addr = addrdict['addr'] elif addrdict['addrtype'] == 'ipv6': self._ipv6_addr = addrdict['addr'] elif addrdict['addrtype'] == 'mac': self._mac_addr = addrdict['addr'] if 'vendor' in addrdict: self._vendor = addrdict['vendor'] self._main_address = self._ipv4_addr or self._ipv6_addr or '' self._address = addrdict @property def ipv4(self): """ Accessor for the IPv4 address of the scanned host :return: IPv4 address as a string """ return self._ipv4_addr or '' @property def mac(self): """ Accessor for the MAC address of the scanned host :return: MAC address as a string """ return self._mac_addr or '' @property def vendor(self): """ Accessor for the vendor attribute of the scanned host :return: string (vendor) of empty string if no vendor defined """ return self._vendor or '' @property def ipv6(self): """ Accessor for the IPv6 address of the scanned host :return: IPv6 address as a string """ return self._ipv6_addr or '' @property def status(self): """ Accessor for the host's status (up, down, unknown...) :return: string """ return self._status['state'] @status.setter def status(self, statusdict): """ Setter for the status dictionnary. :param statusdict: valid dict is {"state": "open", "reason": "syn-ack", "reason_ttl": "0"} 'state' is the only mandatory key. """ self._status = statusdict def is_up(self): """ method to determine if host is up or not :return: bool """ rval = False if self.status == 'up': rval = True return rval @property def hostnames(self): """ Accessor returning the list of hostnames (array of strings). :return: array of string """ return self._hostnames @property def services(self): """ Accessor for the array of scanned services for that host. An array of NmapService objects is returned. :return: array of NmapService """ return self._services def get_ports(self): """ Retrieve a list of the port used by each service of the NmapHost :return: list: of tuples (port,'proto') ie:[(22,'tcp'),(25, 'tcp')] """ return [(p.port, p.protocol) for p in self._services] def get_open_ports(self): """ Same as get_ports() but only for open ports :return: list: of tuples (port,'proto') ie:[(22,'tcp'),(25, 'tcp')] """ return ([(p.port, p.protocol) for p in self._services if p.state == 'open']) def get_service(self, portno, protocol='tcp'): """ :param portno: int the portnumber :param protocol='tcp': string ('tcp','udp') :return: NmapService or None """ plist = [p for p in self._services if p.port == portno and p.protocol == protocol] if len(plist) > 1: raise Exception("Duplicate services found in NmapHost object") return plist.pop() if len(plist) else None def get_service_byid(self, service_id): """ Returns a NmapService by providing its id. The id of a nmap service is a python tupl made of (protocol, port) """ rval = None for _tmpservice in self._services: if _tmpservice.id == service_id: rval = _tmpservice return rval def os_class_probabilities(self): """ Returns an array of possible OS class detected during the OS fingerprinting. :return: Array of NmapOSClass objects """ rval = [] if self.os is not None: rval = self.os.osclasses return rval def os_match_probabilities(self): """ Returns an array of possible OS match detected during the OS fingerprinting :return: array of NmapOSMatches objects """ rval = [] if self.os is not None: rval = self.os.osmatches return rval @property def os_fingerprinted(self): """ Specify if the host has OS fingerprint data available :return: Boolean """ return self._osfingerprinted @property def os_fingerprint(self): """ Returns the fingerprint of the scanned system. :return: string """ rval = '' if self.os is not None: rval = "\n".join(self.os.fingerprints) return rval def os_ports_used(self): """ Returns an array of the ports used for OS fingerprinting :return: array of ports used: [{'portid': '22', 'proto': 'tcp', 'state': 'open'},] """ rval = [] try: rval = self._extras['os']['ports_used'] except (KeyError, TypeError): pass return rval @property def tcpsequence(self): """ Returns the difficulty to determine remotely predict the tcp sequencing. return: string """ rval = '' try: rval = self._extras['tcpsequence']['difficulty'] except (KeyError, TypeError): pass return rval @property def ipsequence(self): """ Return the class of ip sequence of the remote hosts. :return: string """ rval = '' try: rval = self._extras['ipidsequence']['class'] except (KeyError, TypeError): pass return rval @property def uptime(self): """ uptime of the remote host (if nmap was able to determine it) :return: string (in seconds) """ rval = 0 try: rval = int(self._extras['uptime']['seconds']) except (KeyError, TypeError): pass return rval @property def lastboot(self): """ Since when the host was booted. :return: string """ rval = '' try: rval = self._extras['uptime']['lastboot'] except (KeyError, TypeError): pass return rval @property def distance(self): """ Number of hops to host :return: int """ rval = 0 try: rval = int(self._extras['distance']['value']) except (KeyError, TypeError): pass return rval @property def scripts_results(self): """ Scripts results specific to the scanned host :return: array of