本文内容:

巡风漏洞扫描系统资产搜集模块源码分析

框架目录

.
├── lib
│ ├── __init__.py
│ ├── cidr.py
│ ├── common.py
│ ├── icmp.py
│ ├── log.py
│ ├── mongo.py
│ ├── scan.py
│ └── start.py
├── nascan.py
└── plugin
└── masscan.py

主函数

读取配置信息

CONFIG_INI = get_config()  # 读取配置
def get_config():
config = {}
config_info = mongo.na_db.Config.find_one({"type": "nascan"})
for name in config_info['config']:
if name in ['Discern_cms', 'Discern_con', 'Discern_lang', 'Discern_server']:
config[name] = format_config(name, config_info['config'][name]['value'])
else:
config[name] = config_info['config'][name]['value']
return config

大概意思是从数据库里找到typenascan的集合

{
"type": "nascan",
"config": {
"Scan_list": {
"value": "",
"info": "网络资产探测列表(必填)",
"help": "指定爬虫引擎探测范围,格式:192.168.1.1-192.168.1.254(修改会立刻触发资产扫描收集)"
},
"Discern_cms": {
"value": "phpmyadmin|file|index|pma_username\nphpmyadmin|file|/phpmyadmin|pma_username\nrouteros|file|index|<title>RouterOS router configuration page<\\/title>\ndestoon|file|index|Powered by DESTOON\ndestoon|file|index|DESTOON B2B SYSTEM\nU-mail|file|index|Power(ed)? by[^>]+U-Mail\nWinmail|file|index|Winmail Mail Server\nCoremail|file|index|Coremail[^>]+<\\/title>\nWinmail|header|Set-Cookie|magicwinmail\nWinmail|file|index|Powered by Winmail Server\nTurboMail|file|index|Powered by TurboMail \nXmail|file|index|\\d{4}-\\d{4}\\s*webmail.idccenter.net\noutlook|header|X-OWA-Version|.*?\noutlook|file|index|Outlook Web (Access|App)\\s*(?=<\\/title>)\nAnymacro|header|Server|AnyWebApp\nAnymacro|file|index|sec.anymacro.com\nExtMail|file|index|powered by.*?Extmail\nLotus|file|index|IBM Lotus iNotes[^>]+(?=<\\/title>)\nLotus|file|index|iwaredir.nsf\nSquirrelMail|file|index|SquirrelMail Project Team\nSquirrelMail|header|Set-Cookie|SQMSESSID\nSquirrelMail|file|index|SquirrelMail\neqmail|file|index|Powered by EQMail\nTMailer|file|index|TMailer Collaboration Suite Web Client \nzimbra|header|Set-Cookie|ZM_TEST\nzimbra|file|index|zimbra[^>]+(?=<\\/title>)\nzimbra|file|index|Zimbra,?\\s*Inc. All rights reserved.\nbxemail|file|index|[email protected]\nHorde|file|index|<title>[^>]+?Horde\nHorde|file|index|\\/themes\\/graphics\\/horde-power1.png\nAtmail|file|index|powered by Atmail\nIlohaMail|header|Set-Cookie|IMAIL_TEST_COOKIE\nIlohaMail|header|SESS_KEY|.*?\nIlohaMail|file|index|powered by[^>]+IlohaMail\nfangmail|file|index|fangmail\nRoundcube|file|index|Roundcube\nmailbase|header|Set-Cookie|\\s*(mb_lang|mb_ui_type|mb_cus_type)\nmailbase|file|index|MailBase[^<>]+(?=<\\/title>)\nKXmail|file|index|Powered By\\s?<[^>]+>\\s?KXmail\ntongda|file|index|href=\"/images/tongda\\.ico\"\ntrs_wcm|file|index|<title[^>]+>TRS WCM[^<]+</title>\ntrs_wcm|file|index|href=\"/wcm/console/auth/reg_newuser.jsp\"\nmymps|file|index|powered by[^&]+Mymps.*?\nmymps|file|index|wcontent=\"mymps\nmailgard|file|index|mailgard\\swebmail\ndiscuz|file|/robots.txt|discuz\ndiscuz|file|/robots.txt|discuz\nphpwind|file|/robots.txt|phpwind\nphpcms|file|/robots.txt|phpcms\nphp168|file|/robots.txt|php168\nqibosoft|file|/robots.txt|qibocms\nemlog|file|/robots.txt|robots.txt for emlog\nwecenter|file|/robots.txt|robots.txt for wecenter\nbbsmax|file|/robots.txt|bbsmax\nshopnc|file|/robots.txt|robots.txt for shopnc\nhdwike|file|/robots.txt|robots.txt for hdwiki\nphpdisk|file|/robots.txt|PHPDisk\ndedecms|file|/data/admin/ver.txt|20110812\ndedecms|file|/data/admin/ver.txt|20111111\ndedecms|file|/data/admin/ver.txt|20120709\ndedecms|file|/data/admin/ver.txt|20140814\ndedecms|file|/data/admin/verifies.txt|20081204\ndedecms|file|/data/admin/verifies.txt|20100324\ndedecms|file|/data/admin/verifies.txt|20100514\ndedecms|file|/data/admin/verifies.txt|20110216\nwordpress|file|/robots.txt|wordpress\nwordpress|file|/license.txt|wordpress\nwordpress|file|/readme.txt|wordpress\nwordpress|file|/help.txt|wordpress\nwordpress|file|/readme.html|wordpress\nwordpress|file|/wp-admin/css/colors-classic.css|wordpress\nwordpress|file|/wp-admin/js/media-upload.dev.js|wordpress\nwordpress|file|/wp-content/plugins/akismet/akismet.js|wordpress\nwordpress|file|/wp-content/themes/classic/rtl.css|wordpress\nwordpress|file|/wp-includes/css/buttons.css|wordpress\nz-blog|file|/license.txt|z-bolg\nz-blog|file|/SCRIPT/common.js|z-bolg\nsouthidc|file|/Ads/left.js|southidc\nsouthidc|file|/Css/Style.css|southidc\nsouthidc|file|/Images/ad.js|southidc\nsouthidc|file|/Script/Html.js|southidc\nsiteserver|file|/robots.txt|\\/SiteFiles\\/\nsiteserver|file|/SiteFiles/Inner/Register/script.js|stlUserRegister\nenableq|file|/License/index.php|<td>EnableQ\nenableq|file|/robots.txt|robots.txt for EnableQ\ntrs_wcm|file|/wcm/app/login.jsp|TRS WCM\ntrs_wcm|file|/wcm/app/login.jsp|href=\"/wcm/console/auth/reg_newuser.jsp\"\nmymps|file|/robots.txt|mymps\nigenus|file|/help/|igenus\nmailgard|file|/help/io_login.html|webmail",
"info": "cms识别规则",
"help": "用于识别WEB的CMS,格式:CMS名称|判断方式|判断对象|判断正则。识别信息保存于tag记录中,可使用tag:dedecms方式进行搜索。"
},
"Discern_con": {
"value": "jboss|header|X-Powered-By|jboss\njboss|file|jboss.css|youcandoit.jpg\njboss|file|is_test|JBossWeb\naxis|file|axis2|axis2-web/images/axis_l.jpg\nweblogic|file|is_test|Hypertext Transfer Protocol\nweblogic|file|console/css/login.css|Login_GC_LoginPage_Bg.gif\nglassfish|file|resource/js/cj.js|glassfish.dev.java.net\nglassfish|header|server|GlassFish\njenkins|header|X-Jenkins|.*?\njenkins|file|index|\\[Jenkins\\]\nresin|header|server|resin\ntomcat|file|is_test|Apache Tomcat\napache|header|server|apache\niis|header|server|iis\njetty|header|server|jetty\nnginx|header|server|nginx\ncisco|header|server|cisco\ncouchdb|header|server|couchdb\ntplink|header|WWW-Authenticate|TP-LINK\nh3c|header|WWW-Authenticate|h3c\nh3c|file|index|/web/device/login\nhuawei|header|WWW-Authenticate|huawei\nnetgear|header|WWW-Authenticate|netgear\nhikvision|header|server|DNVRS-Webs\nhikvision|header|server|App-webs\nhikvision|header|server|DVRDVS-Webs\nhikvision|header|server|Hikvision-Webs\ntengine|header|server|Tengine",
"info": "组件容器识别规则",
"help": "用于识别WEB的容器、中间件等组件信息,格式:组件名称|判断方式|判断对象|判断正则。识别信息保存于tag记录中,可使用tag:tomcat方式进行搜索。"
},
"Discern_lang": {
"value": "php|header|Server|php\nphp|header|X-Powered-By|php\nphp|header|Set-Cookie|PHPSSIONID\njsp|header|Set-Cookie|JSESSIONID\nasp|header|Set-Cookie|ASPSESSION\naspx|header|Set-Cookie|ASP.NET_SessionId\naspx|header|X-AspNet-Version|version\naspx|file|index|<input[^>]+name=\\\"__VIEWSTATE\naspx|file|index|<a[^>]*?href=('|\")[^http].*?\\.aspx(\\?|\\1)\nasp|file|index|<a[^>]*?href=('|\")[^http].*?\\.asp(\\?|\\1)\nphp|file|index|<a[^>]*?href=('|\")[^http].*?\\.php(\\?|\\1)\njsp|file|index|<a[^>]*?href=('|\")[^http].*?\\.jsp(\\?|\\1)",
"info": "代码语言识别规则",
"help": "用于识别WEB的开发语言,识别信息保存于tag记录中,可使用tag:php方式进行搜索。"
},
"Discern_server": {
"value": "ftp|21|banner|^220.*?ftp|^220-|^220 Service|^220 FileZilla\nssh|22|banner|^ssh-\ntelnet|23|banner|^\\xff[\\xfa-\\xfe]|^\\x54\\x65\\x6c|Telnet\nsmtp|25|banner|^220.*?smtp\ndns|53|default|\npop3|110|banner|\\+OK.*?pop3\nnetbios|139|default|\nimap|143|banner|^\\* OK.*?imap\nldap|389|default|\nsmb|445|default|\nsmtps|465|default|\nrsync|873|banner|^@RSYNCD|^@ERROR\nimaps|993|default|\npop3|995|banner|\\+OK\nproxy|1080|\\x05\\x01\\x00\\x01|^\\x05\\x00\npptp|1723|default|\nmssql|1433|\\x12\\x01\\x00\\x34\\x00\\x00\\x00\\x00\\x00\\x00\\x15\\x00\\x06\\x01\\x00\\x1b\\x00\\x01\\x02\\x00\\x1c\\x00\\x0c\\x03\\x00\\x28\\x00\\x04\\xff\\x08\\x00\\x01\\x55\\x00\\x00\\x00\\x4d\\x53\\x53\\x51\\x4c\\x53\\x65\\x72\\x76\\x65\\x72\\x00\\x48\\x0f\\x00\\x00|^\\x04\\x01\noracle|1521|\\x00\\x3a\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x39\\x01\\x2c\\x00\\x00\\x08\\x00\\x7f\\xff\\xc6\\x0e\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x3a\\x00\\x00\\x08\\x00\\x41\\x41\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00|\\x00Y\\x00\nmysql|3306|banner|^.\\0\\0\\0.*?mysql|^.\\0\\0\\0\\n|.*?MariaDB server\nrdp|3389|\\x03\\x00\\x00\\x13\\x0E\\xE0\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x08\\x00\\x03\\x00\\x00\\x00|\\x03\\x00\\x00\\x13\nsvn|3690|default|\npostgresql|5432|\\x00\\x00\\x00\\x54\\x00\\x03\\x00\\x00\\x75\\x73\\x65\\x72\\x00\\x70\\x6f\\x73\\x74\\x67\\x72\\x65\\x73\\x00\\x64\\x61\\x74\\x61\\x62\\x61\\x73\\x65\\x00\\x70\\x6f\\x73\\x74\\x67\\x72\\x65\\x73\\x00\\x61\\x70\\x70\\x6c\\x69\\x63\\x61\\x74\\x69\\x6f\\x6e\\x5f\\x6e\\x61\\x6d\\x65\\x00\\x70\\x73\\x71\\x6c\\x00\\x63\\x6c\\x69\\x65\\x6e\\x74\\x5f\\x65\\x6e\\x63\\x6f\\x64\\x69\\x6e\\x67\\x00\\x55\\x54\\x46\\x38\\x00\\x00|^R\\x00\\x00\\x00\nvnc|5900|banner|^RFB\nredis|6379|info\\r\\n|redis\nelasticsearch|9200|GET /_cat HTTP/1.1\\r\\n\\r\\n|/_cat/master\nmemcache|11211|\\x80\\x0b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00|^\\x81\\x0b\nmongodb|27017|\\x00\\x00\\x00\\xa7A\\x00\\x00\\x00\\x00\\x00\\x00\\xd4\\x07\\x00\\x00\\x00\\x00\\x00\\x00admin.$cmd\\x00\\x00\\x00\\x00\\x00\\xff\\xff\\xff\\xff\\x13\\x00\\x00\\x00\\x10ismaster\\x00\\x01\\x00\\x00\\x00\\x00|ismaster\nzookeeper|2181|stat|Zookeeper version",
"info": "服务类型识别规则",
"help": "用于识别开放端口上所运行的服务信息,格式:服务名称|端口号|匹配模式|匹配正则,结果以正则匹配为优先,无正则内容时使用端口号进行默认匹配,再无结果时即主动发送探测包进行识别,识别结果保存于server记录中,可使用server:ftp方式进行搜索"
},
"Port_list": {
"value": "1|21\n22\n23\n25\n53\n80\n110\n139\n143\n389\n443\n445\n465\n873\n993\n995\n1080\n1311\n1723\n1433\n1521\n3000\n3001\n3002\n3306\n3389\n3690\n4000\n5432\n5900\n6379\n7001\n8000\n8001\n8080\n8081\n8888\n9200\n9300\n9080\n9090\n9999\n11211\n27017",
"info": "端口探测列表(TCP探测)",
"help": "默认探测端口列表,可开启ICMP,开启后只对存活的IP地址进行探测"
},
"Masscan": {
"value": "0|20000|/root/xunfeng/masscan/linux_64/masscan",
"info": "启用MASSCAN",
"help": "可启用MASSCAN(自行安装)代替默认的端口扫描方式,路径地址需配置全路径,MASSCAN探测端口范围为1-65535且强制进行ICMP存活检测,请根据网络实际情况设置发包速率。"
},
"Timeout": {
"value": "8",
"info": "连接超时时间(TCP)",
"help": "WEB请求的超时时间,socket连接超时为值的一半。"
},
"Cycle": {
"value": "1|9",
"info": "资产探测周期",
"help": "设置资产探测的扫描周期,格式:天数|小时,例如 5|16,即每5天的16点开始进行扫描。"
},
"Thread": {
"value": "200",
"info": "最大线程数",
"help": "爬虫引擎的最大线程数限制"
},
"White_list": {
"value": "",
"info": "资产发现白名单",
"help": "不对白名单内的IP列表进行资产发现。格式:x.x.x.x,以行分割"
}
}
}

然后判断是否为['Discern_cms', 'Discern_con', 'Discern_lang', 'Discern_server']这里面的集合

这里分类如下:

  1. Discern_cms:cms识别规则
  2. Discern_con:组件容器识别规则
  3. Discern_lang:代码语言识别规则
  4. Discern_server:服务类型识别规则

是的话就进行config格式化处理:

def format_config(config_name, config_info):
mark_list = []
try:
config_file = config_info.split('\n')
if config_name == 'Discern_server':
for mark in config_file:
name, port, mode, reg = mark.strip().split("|", 3)
mark_list.append([name, port, mode, reg])
else:
for mark in config_file:
name, location, key, value = mark.strip().split("|", 3)
mark_list.append([name.lower(), location, key, value])
except Exception, e:
print e
return mark_list

先每一行分开

如果是服务类型识别规则,就分割成name, port, mode, reg

不是服务类型识别规则的话,就分割成name, location, key, value

然后根据列表方式返回到一个新的列表,最后返回总列表

如果不在这几个识别类型里的话,就直接返回他的查询的value,就比如:

"Timeout": {
"value": "8",
"info": "连接超时时间(TCP)",
"help": "WEB请求的超时时间,socket连接超时为值的一半。"
},

它实际上就是在设置这一块部分

读取统计信息

STATISTICS = get_statistics()  # 读取统计信息
def get_statistics():
date_ = datetime.datetime.now().strftime('%Y-%m-%d')
now_stati = mongo.na_db.Statistics.find_one({"date": date_})
if not now_stati:
now_stati = {date_: {"add": 0, "update": 0, "delete": 0}}
return now_stati
else:
return {date_: now_stati['info']}

根据年月日来读取,所以说他数据不能实时更新,只能每天更新一次统计信息
image-20200327114803112

心跳检测

def monitor(CONFIG_INI, STATISTICS, NACHANGE):
while True:
try:
time_ = datetime.datetime.now()
date_ = time_.strftime('%Y-%m-%d')
mongo.na_db.Heartbeat.update({"name": "heartbeat"}, {"$set": {"up_time": time_}})
if date_ not in STATISTICS: STATISTICS[date_] = {"add": 0, "update": 0, "delete": 0}
mongo.na_db.Statistics.update({"date": date_}, {"$set": {"info": STATISTICS[date_]}}, upsert=True)
new_config = get_config()
if base64.b64encode(CONFIG_INI["Scan_list"]) != base64.b64encode(new_config["Scan_list"]):NACHANGE[0] = 1
CONFIG_INI.clear()
CONFIG_INI.update(new_config)
except Exception, e:
print e
time.sleep(30)

它是对之前的配置信息和统计信息做心跳检测的(每隔30秒)

大概逻辑是判断是否扫描列表更新了,更新了的话就更新字典,设置NACHANGE[0]=1

并且把原先的设置清除了,然后重新获取数据

无效记录删除

thread.start_new_thread(cruise, (STATISTICS, MASSCAN_AC))  # 失效记录删除线程
def cruise(STATISTICS,MASSCAN_AC):
while True:
now_str = datetime.datetime.now()
week = int(now_str.weekday())
hour = int(now_str.hour)
if week >= 1 and week <= 5 and hour >= 9 and hour <= 18: # 非工作时间不删除
try:
data = mongo.NA_INFO.find().sort("time", 1)
for history_info in data:
while True:
if MASSCAN_AC[0]: # 如果masscan正在扫描即不进行清理
time.sleep(10)
else:
break
ip = history_info['ip']
port = history_info['port']
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((ip, int(port)))
sock.close()
except Exception, e:
time_ = datetime.datetime.now()
date_ = time_.strftime('%Y-%m-%d')
mongo.NA_INFO.remove({"ip": ip, "port": port})
log.write('info', None, 0, '%s:%s delete' % (ip, port))
STATISTICS[date_]['delete'] += 1
del history_info["_id"]
history_info['del_time'] = time_
history_info['type'] = 'delete'
mongo.NA_HISTORY.insert(history_info)
except:
pass
time.sleep(3600)

大概逻辑就是判断是否为工作时间或者MASSCAN是否在扫描,然后通过与这些ip和端口尝试进行socket连接来判断是否存活

通过异常捕捉来实现删除,然后写入删除记录

资产扫描

while True:
now_time = time.localtime()
now_hour = now_time.tm_hour
now_day = now_time.tm_mday
now_date = str(now_time.tm_year) + \
str(now_time.tm_mon) + str(now_day)
cy_day, ac_hour = CONFIG_INI['Cycle'].split('|')
log.write('info', None, 0, u'扫描规则: ' + str(CONFIG_INI['Cycle']))
# 判断是否进入扫描时段
if (now_hour == int(ac_hour) and now_day % int(cy_day) == 0 and now_date not in ac_data) or NACHANGE[0]:
ac_data.append(now_date)
NACHANGE[0] = 0
log.write('info', None, 0, u'开始扫描')
s = start(CONFIG_INI)
s.masscan_ac = MASSCAN_AC
s.statistics = STATISTICS
s.run()
time.sleep(60)

先是拼接当前时间,然后获取计划资产扫描的周期

然后写入log当中

然后判断是否为当前计划扫描周期的时间或者有新的扫描任务(通过心跳检测来设置)

(now_hour == int(ac_hour) and now_day % int(cy_day) == 0 and now_date not in ac_data) or NACHANGE[0]

然后就把当前的时间加入ac_data列表,把NACHANGE[0]重置为0,写入日志当中,开始扫描

start.py

s = start(CONFIG_INI)
s.masscan_ac = MASSCAN_AC
s.statistics = STATISTICS
s.run()

这个类有点多,我们分开来看

__init__

def __init__(self, config):  # 默认配置
self.config_ini = config
self.queue = Queue.Queue()
self.thread = int(self.config_ini['Thread'])
self.scan_list = self.config_ini['Scan_list'].split('\n')
self.mode = int(self.config_ini['Masscan'].split('|')[0])
self.icmp = int(self.config_ini['Port_list'].split('|')[0])
self.white_list = self.config_ini.get('White_list', '').split('\n')

读取了扫描的配置信息,然后设置了一个队列,读取了扫描线程、IP列表、Masscan配置信息、端口信息以及白名单

"Scan_list": {
"value": "",
"info": "网络资产探测列表(必填)",
"help": "指定爬虫引擎探测范围,格式:192.168.1.1-192.168.1.254(修改会立刻触发资产扫描收集)"
},
"Masscan": {
"value": "0|20000|/root/xunfeng/masscan/linux_64/masscan",
"info": "启用MASSCAN",
"help": "可启用MASSCAN(自行安装)代替默认的端口扫描方式,路径地址需配置全路径,MASSCAN探测端口范围为1-65535且强制进行ICMP存活检测,请根据网络实际情况设置发包速率。"
},
"Port_list": {
"value": "1|21\n22\n23\n25\n53\n80\n110\n139\n143\n389\n443\n445\n465\n873\n993\n995\n1080\n1311\n1723\n1433\n1521\n3000\n3001\n3002\n3306\n3389\n3690\n4000\n5432\n5900\n6379\n7001\n8000\n8001\n8080\n8081\n8888\n9200\n9300\n9080\n9090\n9999\n11211\n27017",
"info": "端口探测列表(TCP探测)",
"help": "默认探测端口列表,可开启ICMP,开启后只对存活的IP地址进行探测"
},
"White_list": {
"value": "",
"info": "资产发现白名单",
"help": "不对白名单内的IP列表进行资产发现。格式:x.x.x.x,以行分割"
}

run

def run(self):
global AC_PORT_LIST
all_ip_list = []
for ip in self.scan_list:
if "/" in ip:
ip = cidr.CIDR(ip)
if not ip:
continue

cidr.CIDR(ip)这个我想了一会才发现这是子网掩码IP转IP段的

def CIDR(input):
try:
ip = input.split('/')[0]
pos = int(input.split('/')[1])
ipstr = ''
for i in ip.split('.'):
ipstr = ipstr + bin(int(i)).replace('0b', '').zfill(8)
pstr = '1' * pos + '0' * (32 - pos)
res = stringxor(ipstr, pstr)
_ip = getip(res, 0), getip(res[0:pos] + '1' * (32 - pos), 1)
return _ip[0] + "-" + _ip[1]
except:
return

image-20200330003115979

但这里我推荐使用IPy这个库

image-20200330003635402

重复造轮子会导致效率低下

然后根据白名单删除

for white_ip in self.white_list:
if white_ip in ip_list:
ip_list.remove(white_ip)

然后看是否启用Masscan

我们这里先看启用Masscan的情况

if self.mode == 1:
masscan_path = self.config_ini['Masscan'].split('|')[2]
masscan_rate = self.config_ini['Masscan'].split('|')[1]
# 如果用户在前台关闭了ICMP存活探测则进行全IP段扫描
if self.icmp:
ip_list = self.get_ac_ip(ip_list)
self.masscan_ac[0] = 1
# 如果安装了Masscan即使用Masscan进行全端口扫描
AC_PORT_LIST = self.masscan(
ip_list, masscan_path, masscan_rate)
if not AC_PORT_LIST:
continue
self.masscan_ac[0] = 0
for ip_str in AC_PORT_LIST.keys():
self.queue.put(ip_str) # 加入队列
self.scan_start() # 开始扫描
else:
all_ip_list.extend(ip_list)

image-20200330123543500

读取了masscan的路径以及发包速率

然后判断是否icmp存活检测

ip_list = self.get_ac_ip(ip_list)
def get_ac_ip(self, ip_list):
try:
s = icmp.Nscan()
ipPool = set(ip_list)
return s.mPing(ipPool)
except Exception, e:
print 'The current user permissions unable to send icmp packets'
return ip_list

这里看一下icmp的扫描流程

icmp.py

class SendPingThr(threading.Thread):
def __init__(self, ipPool, icmpPacket, icmpSocket, timeout=3):
threading.Thread.__init__(self)
self.Sock = icmpSocket
self.ipPool = ipPool
self.packet = icmpPacket
self.timeout = timeout
self.Sock.settimeout(timeout + 1)

def run(self):
for ip in self.ipPool:
try:
self.Sock.sendto(self.packet, (ip, 0))
except socket.timeout:
break
except:
pass
time.sleep(self.timeout)
class Nscan:
def __init__(self, timeout=3):
self.timeout = timeout
self.__data = struct.pack('d', time.time())
self.__id = os.getpid()
if self.__id >= 65535: self.__id = 65534

@property
def __icmpSocket(self):
Sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp"))
return Sock

def __inCksum(self, packet):
if len(packet) & 1:
packet = packet + '\0'
words = array.array('h', packet)
sum = 0
for word in words:
sum += (word & 0xffff)
sum = (sum >> 16) + (sum & 0xffff)
sum = sum + (sum >> 16)
return (~sum) & 0xffff

@property
def __icmpPacket(self):
header = struct.pack('bbHHh', 8, 0, 0, self.__id, 0)
packet = header + self.__data
chkSum = self.__inCksum(packet)
header = struct.pack('bbHHh', 8, 0, chkSum, self.__id, 0)
return header + self.__data

def mPing(self, ipPool):
Sock = self.__icmpSocket
Sock.settimeout(self.timeout)
packet = self.__icmpPacket
recvFroms = set()
sendThr = SendPingThr(ipPool, packet, Sock, self.timeout)
sendThr.start()
while True:
try:
ac_ip = Sock.recvfrom(1024)[1][0]
if ac_ip not in recvFroms:
log.write("active", ac_ip, 0, None)
recvFroms.add(ac_ip)
except Exception:
pass
finally:
if not sendThr.isAlive():
break
return recvFroms & ipPool

大概流程就是使用原始套接字进行数据包构造

然后实例化一个SendPingThr类进行多线程发包

然后通过set的一个集合来去重,如果收到响应包,说明存活,最后判断线程是否存活,不存货则退出死循环,最终返回我们的扫描结果集合

然后回到我们的start.py

self.masscan_ac[0] = 1
AC_PORT_LIST = self.masscan(
ip_list, masscan_path, masscan_rate)
if not AC_PORT_LIST:
continue
self.masscan_ac[0] = 0


def masscan(self, ip, masscan_path, masscan_rate):
try:
if len(ip) == 0:
return
sys.path.append(sys.path[0] + "/plugin")
m_scan = __import__("masscan")
result = m_scan.run(ip, masscan_path, masscan_rate)
return result
except Exception, e:
print e
print 'No masscan plugin detected'

动态调用masscan,然后启动扫描,masscan放到最后看

然后把扫描结果加入到队列里

if not AC_PORT_LIST:
continue
self.masscan_ac[0] = 0
for ip_str in AC_PORT_LIST.keys():
self.queue.put(ip_str) # 加入队列
self.scan_start() # 开始扫描

看一下scan_start函数

def scan_start(self):
for i in range(self.thread): # 开始扫描
t = ThreadNum(self.queue)
t.setDaemon(True)
t.mode = self.mode
t.config_ini = self.config_ini
t.statistics = self.statistics
t.start()
self.queue.join()
###################################################
class ThreadNum(threading.Thread):
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue

def run(self):
while True:
try:
task_host = self.queue.get(block=False)
except:
break
try:
if self.mode:
port_list = AC_PORT_LIST[task_host]
else:
port_list = self.config_ini['Port_list'].split('|')[
1].split('\n')
_s = scan.scan(task_host, port_list)
_s.config_ini = self.config_ini # 提供配置信息
_s.statistics = self.statistics # 提供统计信息
_s.run()
except Exception, e:
print e
finally:
self.queue.task_done()

大概流程就是实例化了一个ThreadNum

里面从队列中调取IP地址,然后读取扫描的端口调用scan.scan

这个类文件很多,我们简单说一下流程

  1. 使用原始套接字进行端口扫描,讲扫到的端口写入Mongo,合并历史扫描数据
  2. 服务识别,使用defaultbanner区分
  3. 如果识别的服务名为空,则尝试web访问

如果想调用masscan进行扫描的话

#coding:utf-8
#author:wolf
import os
def run(ip_list,path,rate):
try:
ip_file = open('target.log','w')
ip_file.write("\n".join(ip_list))
ip_file.close()
path = str(path).translate(None, ';|&`\n')
rate = str(rate).translate(None, ';|&`\n')
if not os.path.exists(path):return
os.system("%s -p1-65535 -iL target.log -oL tmp.log --randomize-hosts --rate=%s"%(path,rate))
result_file = open('tmp.log', 'r')
result_json = result_file.readlines()
result_file.close()
del result_json[0]
del result_json[-1]
open_list = {}
for res in result_json:
try:
ip = res.split()[3]
port = res.split()[2]
if ip in open_list:
open_list[ip].append(port)
else:
open_list[ip] = [port]
except:pass
os.remove('target.log')
os.remove('tmp.log')
return open_list
except:
pass

很粗暴,通过读写文件存储中间结果,然后写入变量后删除临时文件

总结

大概流程就是这样吧,好多东西只有当自己亲手研究时候,才能发现其中的优点以及存在的问题

巡风我认为的问题

  1. 好多代码没有重用,就比如资产扫描和漏洞扫描里的get_code函数在两个模块写了两次
  2. 使用了urllib2,可以用request代替
  3. 心跳间距过长,可能导入配置后得最少30秒才能启动扫描

优点

  1. 使用两个模块的心跳检测来触发扫描,这样可以逻辑分离,并且可以通过前端修改配置来通过心跳检测来触发扫描
  2. 支持插件热更新,原理是通过心跳检测新版本然后下载,通过前端触发安装
  3. 模块分开编写,容易修改、重构
  4. 自定义payload方便,有固定的格式,并且可以通过心跳检测更新配置
  5. 支持调用masscan,把扫描目标写入target.log和tmp.log来临时保存,读取后删除文件