Nascan是巡风主要是做目标的资产识别(信息收集)

nascan.py 文件位于 nascan/nascan.py

  1. # coding:utf-8
  2. # author:wolf@YSRC
  3. import thread
  4. from lib.common import *
  5. from lib.start import *
  6. if __name__ == "__main__":
  7. try:
  8. CONFIG_INI = get_config() # 读取配置
  9. log.write('info', None, 0, u'获取配置成功')
  10. STATISTICS = get_statistics() # 读取统计信息
  11. MASSCAN_AC = [0]
  12. NACHANGE = [0]
  13. thread.start_new_thread(monitor, (CONFIG_INI,STATISTICS,NACHANGE)) # 心跳线程
  14. thread.start_new_thread(cruise, (STATISTICS,MASSCAN_AC)) # 失效记录删除线程
  15. socket.setdefaulttimeout(int(CONFIG_INI['Timeout']) / 2) # 设置连接超时
  16. ac_data = []
  17. while True:
  18. now_time = time.localtime()
  19. now_hour = now_time.tm_hour
  20. now_day = now_time.tm_mday
  21. now_date = str(now_time.tm_year) + str(now_time.tm_mon) + str(now_day)
  22. cy_day, ac_hour = CONFIG_INI['Cycle'].split('|')
  23. log.write('info', None, 0, u'扫描规则: ' + str(CONFIG_INI['Cycle']))
  24. if (now_hour == int(ac_hour) and now_day % int(cy_day) == 0 and now_date not in ac_data) or NACHANGE[0]: # 判断是否进入扫描时段
  25. ac_data.append(now_date)
  26. NACHANGE[0] = 0
  27. log.write('info', None, 0, u'开始扫描')
  28. s = start(CONFIG_INI)
  29. s.masscan_ac = MASSCAN_AC
  30. s.statistics = STATISTICS
  31. s.run()
  32. time.sleep(60)
  33. except Exception, e:
  34. print e

  

读取了配置,get_config() 跟进去

nascan/lib/common.py

  1. def get_config():
  2. config = {}
  3. config_info = mongo.na_db.Config.find_one({"type": "nascan"})
  4. for name in config_info['config']:
  5. if name in ['Discern_cms', 'Discern_con', 'Discern_lang', 'Discern_server']:
  6. config[name] = format_config(name, config_info['config'][name]['value'])
  7. else:
  8. config[name] = config_info['config'][name]['value']
  9. return config

  

就是读取了mongodb里面Config表下的内容。

回到nascan.py

get_statistics()则是读取统计信息,返回时间。

也是位于nascan/lib/common.py

  1. def get_statistics():
  2. date_ = datetime.datetime.now().strftime('%Y-%m-%d')
  3. now_stati = mongo.na_db.Statistics.find_one({"date": date_})
  4. if not now_stati:
  5. now_stati = {date_: {"add": 0, "update": 0, "delete": 0}}
  6. return now_stati
  7. else:
  8. return {date_: now_stati['info']}

  

MASSCAN_AC 是系统来判断是否支持masscan扫描。为1的话就是masscan正在扫描。

 NACHANGE 是用来看现在的扫描列表和开始的列表有没有变化,有变化设为1

 

  1. thread.start_new_thread(monitor, (CONFIG_INI,STATISTICS,NACHANGE)) # 心跳线程
  2. thread.start_new_thread(cruise, (STATISTICS,MASSCAN_AC)) # 失效记录删除线程
  3. socket.setdefaulttimeout(int(CONFIG_INI['Timeout']) / 2) # 设置连接超时

 

  

进入monitor心跳线程

位于nascan/lib/common.py

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

  

再次调用了get_config(),获取了配置信息,如果Config表的base64编码值如果有变化将NACHANGE[0]改成NACHANGE[1]。系统更新config,然后睡眠30秒,表示需要重新扫描。

 

返回nascan.py

Cruise()函数,位于nascan/lib/common.py

 

  1. def cruise(STATISTICS,MASSCAN_AC):
  2. while True:
  3. now_str = datetime.datetime.now()
  4. week = int(now_str.weekday())
  5. hour = int(now_str.hour)
  6. if week >= 1 and week <= 5 and hour >= 9 and hour <= 18: # 非工作时间不删除
  7. try:
  8. data = mongo.NA_INFO.find().sort("time", 1)
  9. for history_info in data:
  10. while True:
  11. if MASSCAN_AC[0]: # 如果masscan正在扫描即不进行清理
  12. time.sleep(10)
  13. else:
  14. break
  15. ip = history_info['ip']
  16. port = history_info['port']
  17. try:
  18. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  19. sock.connect((ip, int(port)))
  20. sock.close()
  21. except Exception, e:
  22. time_ = datetime.datetime.now()
  23. date_ = time_.strftime('%Y-%m-%d')
  24. mongo.NA_INFO.remove({"ip": ip, "port": port})
  25. log.write('info', None, 0, '%s:%s delete' % (ip, port))
  26. STATISTICS[date_]['delete'] += 1
  27. del history_info["_id"]
  28. history_info['del_time'] = time_
  29. history_info['type'] = 'delete'
  30. mongo.NA_HISTORY.insert(history_info)
  31. except:
  32. pass
  33. time.sleep(3600)

 

  

记录失效目标并删除线程,对目标(ip:port)进行sock连接,如果连接不上就删除INFO里面ipport。然后写进history表里。

 

回到nascan.py

  1. if (now_hour == int(ac_hour) and now_day % int(cy_day) == 0 and now_date not in ac_data) or NACHANGE[0]: # 判断是否进入扫描时段

  

(now_hour == int(ac_hour) and now_day % int(cy_day) == 0 and now_date not in ac_data)

是判断是否到达扫描的周期时间。

或者就是NACHANGE[0]的值为1,任何一个成立都可以重新扫描。

进入Start()函数

nascan/lib/start.py

start类中,__init__初始化了传递过来的配置信息。直接看run(),处理目标IP地址和使用masscan进行初步扫描等。

 

  1. def run(self):
  2. global AC_PORT_LIST
  3. all_ip_list = []
  4. for ip in self.scan_list:
  5. if "/" in ip: ip = cidr.CIDR(ip)
  6. if not ip:continue
  7. ip_list = self.get_ip_list(ip)
  8. for white_ip in self.white_list:
  9. if white_ip in ip_list:
  10. ip_list.remove(white_ip)
  11. if self.mode == 1:
  12. self.masscan_path = self.config_ini['Masscan'].split('|')[2]
  13. self.masscan_rate = self.config_ini['Masscan'].split('|')[1]
  14. ip_list = self.get_ac_ip(ip_list)
  15. self.masscan_ac[0] = 1
  16. AC_PORT_LIST = self.masscan(ip_list) # 如果安装了Masscan即使用Masscan进行全端口扫描
  17. if not AC_PORT_LIST: continue
  18. self.masscan_ac[0] = 0
  19. for ip_str in AC_PORT_LIST.keys(): self.queue.put(ip_str) # 加入队列
  20. self.scan_start() # 开始扫描
  21. else:
  22. all_ip_list.extend(ip_list)
  23. if self.mode == 0:
  24. if self.icmp: all_ip_list = self.get_ac_ip(all_ip_list)
  25. for ip_str in all_ip_list: self.queue.put(ip_str) # 加入队列
  26. self.scan_start() # TCP探测模式开始扫描

 

  

if “/” in ip: ip = cidr.CIDR(ip) ,支持这样的格式:127.0.0.1/24

if self.mode == 1 判断是否支持masscan扫描,如果支持就使用Masscan进行全端口扫描。如果没有开启,将ip添加到all_ip_list这个列表中。

 

masscan()函数

nascan/lib/start.py

  1. def masscan(self, ip):
  2. try:
  3. if len(ip) == 0: return
  4. sys.path.append(sys.path[0] + "/plugin")
  5. m_scan = __import__("masscan")
  6. result = m_scan.run(ip, self.masscan_path, self.masscan_rate)
  7. return result
  8. except Exception, e:
  9. print e
  10. print 'No masscan plugin detected'

  

调用了/plugin/masscan.py

  1. def run(ip_list,path,rate):
  2. try:
  3. ip_file = open('target.log','w')
  4. ip_file.write("\n".join(ip_list))
  5. ip_file.close()
  6. path = str(path).translate(None, ';|&')
  7. rate = str(rate).translate(None, ';|&')
  8. if not os.path.exists(path):return
  9. os.system("%s -p1-65535 -iL target.log -oL tmp.log --randomize-hosts --rate=%s"%(path,rate))
  10. result_file = open('tmp.log', 'r')
  11. result_json = result_file.readlines()
  12. result_file.close()
  13. del result_json[0]
  14. del result_json[-1]
  15. open_list = {}
  16. for res in result_json:
  17. try:
  18. ip = res.split()[3]
  19. port = res.split()[2]
  20. if ip in open_list:
  21. open_list[ip].append(port)
  22. else:
  23. open_list[ip] = [port]
  24. except:pass
  25. os.remove('target.log')
  26. os.remove('tmp.log')
  27. return open_list
  28. except:
  29. pass

  

先过滤了;|&三个特殊字符。然后拼接到命令中

masscan -p1-65535 -iL target.log -oL tmp.log –randomize-hosts –rate=20000

masscan扫描好了后保存tmp.log文件里然后读取结果。

不管开没开masscan,都会进入scan_start()

跟进到ThreadNum,位于/nascan/lib/start.py 

 

  1. class ThreadNum(threading.Thread):
  2. def __init__(self, queue):
  3. threading.Thread.__init__(self)
  4. self.queue = queue
  5.  
  6. def run(self):
  7. while True:
  8. try:
  9. task_host = self.queue.get(block=False)
  10. except:
  11. break
  12. try:
  13. if self.mode:
  14. port_list = AC_PORT_LIST[task_host]
  15. else:
  16. port_list = self.config_ini['Port_list'].split('|')[1].split('\n')
  17. _s = scan.scan(task_host, port_list)
  18. _s.config_ini = self.config_ini # 提供配置信息
  19. _s.statistics = self.statistics # 提供统计信息
  20. _s.run()
  21. except Exception, e:
  22. print e
  23. finally:
  24. self.queue.task_done()

 

  

run()函数,把IP地址和端口号列表传到另一个scan()函数中。

位于/nascan/lib/scan.py

  1. class scan:
  2. def __init__(self, task_host, port_list):
  3. self.ip = task_host
  4. self.port_list = port_list
  5. self.config_ini = {}
  6.  
  7. def run(self):
  8. self.timeout = int(self.config_ini['Timeout'])
  9. for _port in self.port_list:
  10. self.server = ''
  11. self.banner = ''
  12. self.port = int(_port)
  13. self.scan_port() # 端口扫描
  14. if not self.banner:continue
  15. self.server_discern() # 服务识别
  16. if self.server == '':
  17. web_info = self.try_web() # 尝试web访问
  18. if web_info:
  19. log.write('web', self.ip, self.port, web_info)
  20. time_ = datetime.datetime.now()
  21. mongo.NA_INFO.update({'ip': self.ip, 'port': self.port},
  22. {"$set": {'banner': self.banner, 'server': 'web', 'webinfo': web_info,
  23. 'time': time_}})

  

scan类的run函数。先进行了端口扫描,scan_port()函数

位于/nascan/lib/scan.py

  1. def scan_port(self):
  2. try:
  3. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  4. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  5. sock.connect((self.ip, self.port))
  6. time.sleep(0.2)
  7. except Exception, e:
  8. return
  9. try:
  10. self.banner = sock.recv(1024)
  11. sock.close()
  12. if len(self.banner) <= 2:
  13. self.banner = 'NULL'
  14. except Exception, e:
  15. self.banner = 'NULL'
  16. log.write('portscan', self.ip, self.port, None)
  17. banner = ''
  18. hostname = self.ip2hostname(self.ip)
  19. time_ = datetime.datetime.now()
  20. date_ = time_.strftime('%Y-%m-%d')
  21. try:
  22. banner = unicode(self.banner, errors='replace')
  23. if self.banner == 'NULL': banner = ''
  24. mongo.NA_INFO.insert({"ip": self.ip, "port": self.port, "hostname": hostname, "banner": banner, "time": time_})
  25. self.statistics[date_]['add'] += 1
  26. except:
  27. if banner:
  28. history_info = mongo.NA_INFO.find_and_modify(
  29. query={"ip": self.ip, "port": self.port, "banner": {"$ne": banner}}, remove=True)
  30. if history_info:
  31. mongo.NA_INFO.insert(
  32. {"ip": self.ip, "port": self.port, "hostname": hostname, "banner": banner, "time": time_})
  33. self.statistics[date_]['update'] += 1
  34. del history_info["_id"]
  35. history_info['del_time'] = time_
  36. history_info['type'] = 'update'
  37. mongo.NA_HISTORY.insert(history_info)

  

通过socket连接,获得端口服务返回的banner信息,然后进入server_discern()函数,通过正则表达式,依次比较,获得服务类型。

server_discern()函数

位于/nascan/lib/scan.py

  1. def server_discern(self):
  2. for mark_info in self.config_ini['Discern_server']: # 快速识别
  3. try:
  4. name, default_port, mode, reg = mark_info
  5. if mode == 'default':
  6. if int(default_port) == self.port:
  7. self.server = name
  8. elif mode == 'banner':
  9. matchObj = re.search(reg, self.banner, re.I | re.M)
  10. if matchObj:
  11. self.server = name
  12. if self.server:break
  13. except:
  14. continue
  15. if not self.server and self.port not in [80,443,8080]:
  16. for mark_info in self.config_ini['Discern_server']: # 发包识别
  17. try:
  18. name, default_port, mode, reg = mark_info
  19. if mode not in ['default','banner']:
  20. dis_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  21. dis_sock.connect((self.ip, self.port))
  22. mode = mode.decode('string_escape')
  23. reg = reg.decode('string_escape')
  24. dis_sock.send(mode)
  25. time.sleep(0.3)
  26. dis_recv = dis_sock.recv(1024)
  27. matchObj = re.search(reg, dis_recv, re.I | re.M)
  28. if matchObj:
  29. self.server = name
  30. break
  31. except:
  32. pass
  33. if self.server:
  34. log.write("server", self.ip, self.port, self.server)
  35. mongo.NA_INFO.update({"ip": self.ip, "port": self.port}, {"$set": {"server": self.server}})

  

对于没识别出来的服务类型,端口号又不是常见端口号,会重新发包,发送特定包才会返回应答banner的服务类型。

 

最后如果还没识别出来,进入try_web()函数

位于/nascan/lib/scan.py

  1. def try_web(self):
  2. title_str, html = '', ''
  3. try:
  4. if self.port == 443:
  5. info = urllib2.urlopen("https://%s:%s" % (self.ip, self.port), timeout=self.timeout)
  6. else:
  7. info = urllib2.urlopen("http://%s:%s" % (self.ip, self.port), timeout=self.timeout)
  8. html = info.read()
  9. header = info.headers
  10. except urllib2.HTTPError, e:
  11. html = e.read()
  12. header = e.headers
  13. except:
  14. return
  15. if not header: return
  16. if 'Content-Encoding' in header and 'gzip' in header['Content-Encoding']: # 解压gzip
  17. html_data = StringIO.StringIO(html)
  18. gz = gzip.GzipFile(fileobj=html_data)
  19. html = gz.read()
  20. try:
  21. html_code = self.get_code(header, html).strip()
  22. if html_code and len(html_code) < 12:
  23. html = html.decode(html_code).encode('utf-8')
  24. except: pass
  25. try:
  26. title = re.search(r'<title>(.*?)</title>', html, flags=re.I | re.M)
  27. if title: title_str = title.group(1)
  28. except: pass
  29. try:
  30. web_banner = str(header) + "\r\n\r\n" + html
  31. self.banner = web_banner
  32. history_info = mongo.NA_INFO.find_one({"ip": self.ip, "port": self.port})
  33. if 'server' not in history_info:
  34. tag = self.get_tag()
  35. web_info = {'title': title_str, 'tag': tag}
  36. return web_info
  37. else:
  38. if abs(len(history_info['banner'].encode('utf-8')) - len(web_banner)) > len(web_banner) / 60:
  39. del history_info['_id']
  40. history_info['del_time'] = datetime.datetime.now()
  41. mongo.NA_HISTORY.insert(history_info)
  42. tag = self.get_tag()
  43. web_info = {'title': title_str, 'tag': tag}
  44. date_ = datetime.datetime.now().strftime('%Y-%m-%d')
  45. self.statistics[date_]['update'] += 1
  46. log.write('info', None, 0, '%s:%s update web info'%(self.ip, self.port))
  47. return web_info
  48. except:
  49. return
  50.  
  51. def ip2hostname(self,ip):
  52. try:
  53. hostname = socket.gethostbyaddr(ip)[0]
  54. return hostname
  55. except:
  56. pass
  57. try:
  58. query_data = "\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x20\x43\x4b\x41\x41" + \
  59. "\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41" + \
  60. "\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x00\x00\x21\x00\x01"
  61. dport = 137
  62. _s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  63. _s.settimeout(3)
  64. _s.sendto(query_data, (ip, dport))
  65. x = _s.recvfrom(1024)
  66. tmp = x[0][57:]
  67. hostname = tmp.split("\x00", 2)[0].strip()
  68. hostname = hostname.split()[0]
  69. return hostname
  70. except:
  71. pass
  72.  
  73. def get_code(self, header, html):
  74. try:
  75. m = re.search(r'<meta.*?charset=(.*?)"(>| |/)', html, flags=re.I)
  76. if m: return m.group(1).replace('"', '')
  77. except:
  78. pass
  79. try:
  80. if 'Content-Type' in header:
  81. Content_Type = header['Content-Type']
  82. m = re.search(r'.*?charset=(.*?)(;|$)', Content_Type, flags=re.I)
  83. if m: return m.group(1)
  84. except:
  85. pass

  

这个函数就是尝试用web访问,如果有结果的话就保存下来,没有的话就不管了。

 

回到nascan

大概每隔一分钟探测是否要进行扫描。

 

 

参考文章:

https://landgrey.me/xunfeng-nascan-analysis/

https://www.cnblogs.com/yangxiaodi/p/8011563.html

 

版权声明:本文为zhengjim原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/zhengjim/p/9436008.html