35

端口扫描系统实践心得

 5 years ago
source link: https://www.freebuf.com/articles/es/201210.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

*本文作者:Humou0,本文属 FreeBuf 原创奖励计划,未经许可禁止转载

端口扫描对任何一名网络安全从业者来说都不陌生,但作为一名小白,在甲方做扫描系统时踩了不少坑,在网络上找相关资料时没有发现太多相关的文章,于是想写下这篇文章和大家分享一下代码,顺便讨教一下主机存活判断和指纹识别的问题,欢迎大佬们批评和指正。

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用户

2EZ7NzE.jpg!webeyAbiaJ.jpg!web

nmap通过向目标IP的80端口和443端口分别发送SYN包来判断主机是否存活,由于目标主机的80和443端口均未开放,所以均返回RST包
nmap扫描结果:0 hosts up

root用户

ErUZby6.jpg!webnmQrim3.jpg!web

nmap发送ARP请求并得到响应
nmap扫描结果:1 hosts up

非局域网环境:

非root用户

AZv6VrA.jpg!webQVbIVzF.jpg!web

同样的,nmap向目标主机的80和443端口发送SYN包,通过返回的确认包得到目标主机存活。所以扫描结果为:1 hosts up。

root用户

RJFJVn2.jpg!web

bYJzaay.jpg!web

这次,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,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK