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