端口扫描系统实践心得
作者:admin | 时间:2019-4-25 02:31:37 | 分类:黑客技术 隐藏侧边栏展开侧边栏
端口扫描对任何一名网络安全从业者来说都不陌生,但作为一名小白,在甲方做扫描系统时踩了不少坑,在网络上找相关资料时没有发现太多相关的文章,于是想写下这篇文章和大家分享一下代码,顺便讨教一下主机存活判断和指纹识别的问题,欢迎大佬们批评和指正。
0×00 目的
对于外网,能够监控对外开放端口情况,并及时的发现向外暴露的高危端口,以便安全人员进行响应处理。对于内网,日常 的端口扫描以及指纹识别,不仅能够帮助梳理公司资产,并且能够帮助进行后续内网的漏洞扫描。
0×01 存活主机判断
开始做端口扫描时,所考虑的第一步便是存活主机判断。最初的设想便是使用nmap的-sP参数,对IP地址进行存活判断。
代码如下:
def ip_alive_check(ip_str): cmd = "/usr/bin/nmap -sP "+ip_str
output = os.popen(cmd).readlines()
flag = False for line in list(output): if not line: continue if str(line).lower().find("1 host up") >= 0:
flag = True break return flag
但随后便发现这个办法存在问题,引用Nmap官方文档如下:
-sP选项在默认情况下, 发送一个ICMP回声请求和一个TCP报文到80端口。
如果非特权用户执行,就发送一个SYN报文 (用connect()系统调用)到目标机的80端口。
当特权用户扫描局域网上的目标机时,会发送ARP请求(-PR), ,除非使用了–send-ip选项。
-sP选项可以和除-P0)之外的任何发现探测类型-P* 选项结合使用以达到更大的灵活性。
一旦使用了任何探测类型和端口选项,默认的探测(ACK和回应请求)就被覆盖了。
当防守严密的防火墙位于运行Nmap的源主机和目标网络之间时, 推荐使用那些高级选项。
否则,当防火墙捕获并丢弃探测包或者响应包时,一些主机就不能被探测到。
抓包如下:
局域网环境:
非root用户
nmap通过向目标IP的80端口和443端口分别发送SYN包来判断主机是否存活,由于目标主机的80和443端口均未开放,所以均返回RST包
nmap扫描结果:0 hosts up
root用户
nmap发送ARP请求并得到响应
nmap扫描结果:1 hosts up
非局域网环境:
非root用户
同样的,nmap向目标主机的80和443端口发送SYN包,通过返回的确认包得到目标主机存活。所以扫描结果为:1 hosts up。
root用户
这次,nmap不仅向目标主机的80和443端口发送了SYN包,还向目标主机发送了ICMP Echo请求以及Timestamp请求,nmap会综合这四种方式的响应情况来判断目标主机是否存活。显然这次的扫描结果为:1 hosts up。
通过对nmap -sP参数的分析便可得知,实际上对存活主机的判断并不准确。许多主机的防火墙会过滤掉ICMP包,而且80和443端口也不一定会保证对外开放。
而nmap官方文档中提到的高级选项在实际的使用中也都不能保证准确性,所以对于存活主机的判断,一直没有找到比较好的解决办法,在实际的扫描中便没有用上这一步骤,还请各路大佬指点指点有没有什么成本比较低的解决方案。
0×02 Masscan扫描端口
直接对全端口使用nmap进行扫描速度较慢,所以选择使用号称,三分钟扫遍全网的masscan。
masscan采用的是无状态的扫描技术即无需关心TCP状态,不占用系统TCP/IP协议栈资源,忘记syn,ack,fin,timewait ,不进行会话组包,而nmap则是需要记录TCP/IP的状态,并且OS能够处理的TCP/IP连接数存在上限,这就导致了nmap扫描的速度不如masscan。
代码如下:
class Masscan(object): def __init__(self, args): self.masscan_bin = config.MASSCAN_BIN # Masscan路径 例如:/usr/bin/masscan self.result_xml = '/tmp/masscan/'+args['hosts'] # 暂存的masscan扫描结果名称 self.rate = config.MASSCAN_RATE # 发包速率,例如:10000 self.retries = config.MASSCAN_RETRIES # 发送重试的次数 例如:3 self.wait = config.MASSCAN_WAIT # 指定发送完包之后的等待时间,例如:5 self.ports = args['ports'] # 端口 self.hosts = args['hosts'] # IP def scan(self): cmd = 'mkdir -p /tmp/masscan/' os.system(cmd)
command = ( '{masscan_bin} -oX {result_xml} --rate={rate} --retries={retries} --wait={wait} -p {ports} {hosts}' ).format(
masscan_bin=self.masscan_bin,
result_xml=self.result_xml,
rate=self.rate,
retries=self.retries,
wait=self.wait,
hosts=self.hosts,
ports=self.ports
)
process = subprocess.Popen(
command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True )
logger.info(b'\nStarting masscan,the command is '+str(command)) try:
_, stderr = process.communicate() if not stderr.startswith(b'\nStarting masscan'):
logger.failure('Masscan Error\n{}'.format(stderr))
os._exit(1) except KeyboardInterrupt:
logger.failure('User aborted')
os._exit(1) def parse_result_xml(self, d_ip): result = {} try:
xml_size = os.path.getsize(self.result_xml) if xml_size > 0 and xml_size < 10000:
tree = ET.parse(self.result_xml)
root = tree.getroot() for host in root.iter('host'):
ip = host.find('address').attrib['addr']
port = host.find('ports').find('port').attrib['portid'] if result.setdefault(ip):
result[ip].append(port) else:
result[ip] = [port] elif xml_size >= 10000:
result = {d_ip: ['1-65535']} else:
result = {} except Exception as e:
logger.info('----------')
logger.info(str(e))
logger.info('ParseError!!!')
logger.info('----------')
logger.info(self.result_xml) pass cmd_rm = 'rm -rf ' + self.result_xml
os.system(cmd_rm) return result
在使用masscan得注意速率问题,在带宽有限的情况下,速率过高则会导致丢包的情况发生从而导致扫描结果漏报。具体的速率根据实际的带宽情况慢慢调教即可。
在缓存masscan的扫描结果时,选择了直接写在tmp目录下,解析完后再删除。也可以使用redis进行缓存。
在实际的测试中发现了一个问题,masscan在扫描时,可能是因为目标主机防火墙的抗DDos功能,对masscan所发送的SYN包均会回复ACK包,所以masscan会误报部分IP开放特别大量端口的情况。选择了对xml文件的大小加了个判断,如果过大,直接将结果置为1-65535,扔给nmap重新扫一下。
0×03 Nmap扫描识别指纹
Masscan虽然扫描速度够快,但是在指纹识别这一块却是远远比不了nmap,于是在masscan扫描完成后,使用nmap对端口进行指纹识别,以及确认结果以防止masscan误报(实际上masscan的误报好像挺少的)。
代码如下:
class Nmap(object): def __init__(self, masscan_result): self.nm = nmap.PortScanner(nmap_search_path=(config.NMAP_BIN,)) # config.NMAP_BIN:nmap的路径,例如:/usr/bin/nmap self.nmap_args = config.NMAP_ARGS # nmap 扫描时的参数 例如:-Pn -sV -sS --host-timeout 1200 self.targets = []
self.result = [] for host, ports in masscan_result.items():
self.targets.append({host: ','.join(ports)}) def scan(self, args): for target in self.targets: for host, ports in target.items(): try:
self.nm.scan(host, ports, self.nmap_args) if host not in self.nm.all_hosts(): continue if self.nm[host].has_key('tcp'): for port, data in self.nm[host]['tcp'].items():
state = data['state']
product = data['product']
name = data['name']
ip = args['hosts']
address = args['address'] if product:
service = product else:
service = name
version = data['version'] if state == "open":
x = save_it(ip, port, address, service, version)
x.detect_new_port() # 储存结果时,进行一下判断,看是否是新增端口 if self.nm[host].has_key('udp'): for port, data in self.nm[host]['udp'].items():
state = data['state']
product = data['product']
name = data['name']
ip = args['hosts']
address = args['address'] if product:
service = product else:
service = name
version = data['version'] if state == "open":
x = save_it(ip, port, address, service, version)
x.detect_new_port() # 储存结果时,进行一下判断,看是否是新增端口 except Exception as e:
logger.info('the exception i nmap.scan is ' + str(e)) continue
在设置nmap的扫描参数时,别忘了带上-Pn或者-P0跳过判断主机存活的步骤,因为nmap默认是会先对主机进行存活判断再进行扫描,可能会因为误判而导致漏扫。
0×04 告警
告警部分则是和扫描一样,使用django的celery进行定时任务,所以扫描和告警存在一定的时间间隔,于是在告警前便使用socket对库中的扫描结果进行一下验证。
def detect_port(ip, port): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1) try:
s.connect((ip, int(port)))
s.shutdown(2) return True except Exception as e: return False
0×05 后续
多进程则是由celery所依赖的billiard库实现
from billiard import Pool
p = Pool(5) for ip in ip_data:
p.apply_async(port_scan, args=(ip, log_name,))
p.close()
p.join()
如果IP数量较大,可以将端口扫描、资产发现、漏洞扫描等集成起来,做成agent,搭配rabbitmq实现分布式的扫描系统。
扫描时别忘了避开交换机和打印机等比较容易脆弱的设备,在实际进行扫描时就曾遇见过某型号打印机存在缺陷,一扫就自己疯狂打印(都吓到了晚上正在加班的同事),最后无奈只能避开。
经过一段时间的使用,带宽足够,速率合适的情况下,masscan扫描的准确性还挺不错的。但即使nmap的指纹库已经较为丰富,在识别web应用程序、中间件这些时,还是有些不够用,不便于后续的漏洞扫描。
*本文作者:Humou0