浅析端口扫描原理

2021-05-27 22:05:00
端口扫描

文章首发于:安全客

自觉对于端口扫描只是浅显的停留在nmap的几条命令,因此花了点时间了解了一下端口扫描器是如何运作的,并且使用python写了一个简单的端口扫描器,笔者目的不在于替代nmap等主流端扫的作用,只是为了更深入地了解端口扫描器运作的原理。

本文以nmap中常见的扫描技术的原理展开叙述。

端口

每一个ip地址可以有2^16=65535个端口,其中分成了两类,一类是TCP,另一类是UDP;TCP是面向连接的,可靠的字节流服务,UDP是面向非连接的,不可靠的数据报服务;在实现端口扫描器时需要针对这两种分别进行扫描。

每一个端口只能被用于一个服务,例如常见的80端口就被用于挂载http服务,3306则是mysql,而nmap中对于端口的定义存在着六种状态:

  • open(开放的)
  • close(关闭的)
  • filtered(被过滤的)
  • unfiltered(未被过滤的)
  • open|filtered(开放或者被过滤的)
  • closed|filtered(关闭或者被过滤的)

对于这6种状态我在后文中会陆续提到;端口扫描器要做的就是扫出各个端口的状态,并且探测到存活的端口中存在着的服务。

TCP

因为扫描TCP端口时需要用到TCP的知识,因此,在此之前有必要再复习一下TCP协议的那些事,也就是TCP的三次握手:

  • 第一次握手:

客户端主动发送SYN给服务端,SYN序列号假设为J(此时服务器是被动接受)。

  • 第二次握手

服务端接受到客户端发送的SYN(J)后,发送一个SYN(J+1)和ACK(K)给客户端。

  • 第三次握手

客户端接受到新的SYN(K)和ACK(J+1)后,也发送一个ACK(K+1)给服务端,此时连接建立,双方可以进行通信。

而发送TCP包的连接状态是由一个名为标志位(flags)来决定的,它存在一下几种:

  • F : FIN - 结束; 结束会话
  • S : SYN - 同步; 表示开始会话请求
  • R : RST - 复位;中断一个连接
  • P : PUSH - 推送; 数据包立即发送
  • A : ACK - 应答
  • U : URG - 紧急
  • E : ECE - 显式拥塞提醒回应
  • W : CWR - 拥塞窗口减少

UDP

关于UDP的话就不多说拉,只需要知道它是一个无连接协议,因此也是一种不可靠的协议,因为你并不清楚你发的数据是否到达目标。

TCP SYN SCAN

对应于nmap中的-sS。

关于SYN前面稍稍提了一下,那么下面对此细节进行展开。

SYN扫描也称为半连接扫描,那么说到这里顺嘴提一下SYN FLOOD,它是利用了TCP协议的缺陷,在客户端伪造源地址发送SYN给服务端时,服务端会响应该SYN并且发送SYN跟ACK包,但因为客户端地址是伪造的,真实请求的客户端不认为该包是其发送的,因此不再响应从服务端从发送来的包,服务端收不到响应会进行重试3-5次并且等待一个SYN Time(一般30秒-2分钟)后,丢弃这个连接,这一过程会消耗服务端的资源,

而当大量伪造源地址的TCP请求使用此种方式去向服务端发送SYN包时,服务端会因为资源的大量消耗导致请求缓慢,此时用户无法正常使用服务。

回到端扫,项目采用的是scapy,那么用它来发送一个SYN包应该怎么做?

找到其文档会发现它已经直截了当地给出了答案:

sr1(IP(dst="101.132.132.179")/TCP(dport=80,flags="S"))

这里flag="S"也就是将标志位置为SYN,说明此时发送的是SYN,而收到的内容:

Begin emission:
Finished sending 1 packets.
...*
Received 4 packets, got 1 answers, remaining 0 packets
<IP  version=4 ihl=5 tos=0x4 len=44 id=0 flags=DF frag=0 ttl=48 proto=tcp chksum=0x743c src=101.132.132.179 dst=192.168.43.172 |<TCP  sport=http dport=ftp_data seq=1048042036 ack=1 dataofs=6 reserved=0 flags=SA window=29200 chksum=0x36f7 urgptr=0 options=[('MSS', 1318)] |>>

发现flags=SA,即SYN+ACK,那么如何判断端口是否开放?

文档中给出的方法是判断响应包是否具有SA标识,有则默认端口开放,但是对于我们来说仅仅到这一步并不太满意,会发现nmap文档中早已给出答案:

它发送一个SYN报文, 就像您真的要打开一个连接,然后等待响应。 SYN/ACK表示端口在监听 (开放),而 RST (复位)表示没有监听者。如果数次重发后仍没响应, 该端口就被标记为被过滤。如果收到ICMP不可到达错误 (类型3,代码1,2,3,9,10,或者13),该端口也被标记为被过滤。

概括一下可以分成三种状态,open,closed,filtered,而我们很容易冲描述中判断出对应的行为应该给与什么样的状态:

行为 状态
数次重发未响应 filtered
收到ICMP不可达错误 filtered
SYN/ACK open
RST closed

对于判断响应是否具有TCP层 or ICMP层,scapy中也贴心的给出了相应的函数(getlayer和haslayer),而在收到SYN-ACK报文后要做的是发送RST报文中断连接而非传统TCP连接中的ACK报文,而当收到RA时表示端口关闭。

这里的RA也可以用16进制表示为0x14,其算法为:

URG=0,ACK=1,PSH=0,RST=1、SYN=0、FIN=0

010100=0x14

一个demo:

def tcp_syn_scan(host,port):
    send = sr1(IP(dst=host) / TCP(dport=port, flags="S"),retry=-2, timeout=2, verbose=0)
    if (send is None):
        # filtered
        pass
    elif send.haslayer(TCP):
        if send.getlayer(TCP).flags == 0x12:#SA
            sr1(IP(dst=host) / TCP(dport=port, flags="R"), timeout=2, verbose=0)
            print("[+] %s %d Open" % (host, port))
        elif send.getlayer(TCP).flags == 0x14: #RA
            # closed
            pass
    elif send.haslayer(ICMP):
        if send.getlayer(TCP).type == 3 and send.getlayer(TCP).code in [1, 2, 3, 9, 10, 13]:
            # filtered
            pass

TCP CONNECT SCAN

对应于nmap中的-sT。

相对于SYN的半连接扫描,TCP CONNECT SCAN又称为全连接扫描,顾名思义这种扫描方式需要与目标建立完整的TCP连接,前面讲SYN扫描时说到了最后发送的是一个RST报文,其是为了躲避防火墙的检测,而在全连接扫描中则会留下记录,全连接扫描可以说是SYN扫描的下位选择,因为他们得到的信息相同,只有在SYN扫描不可用的情况下才会选择全连接扫描。

那么如果端口不开放,服务端同样返回的是一个RST报文,同样的RA。

因为基本上与SYN扫描类似,区别就是需要建立完整的TCP连接,只需改动一行,将R改成AR:

sr1(IP(dst=host) / TCP(dport=port, flags="AR"), timeout=2, verbose=0)

TCP Null,FIN,and Xmas扫描

nmap中将这三类扫描置为一类,看看文档中对于这一类扫描的描述:

如果扫描系统遵循该RFC,当端口关闭时,任何不包含SYN,RST,或者ACK位的报文会导致 一个RST返回,而当端口开放时,应该没有任何响应。只要不包含SYN,RST,或者ACK, 任何其它三种(FIN,PSH,and URG)的组合都行

因为三类扫描实际上原理相似,因此只着重讲一个。

首先看到nmap中对于这三类扫描的flags值:

case XMAS_SCAN: pspec->pd.tcp.flags = TH_FIN|TH_URG|TH_PUSH; break;
case NULL_SCAN: pspec->pd.tcp.flags = 0; break;
case FIN_SCAN: pspec->pd.tcp.flags = TH_FIN; break;

并且它们的回显置为了无回显:

noresp_open_scan = true;

很明显这三种扫描在行为上是一致的,当扫描到端口关闭时服务端会给出一个RST,而当端口开放时会得到一个未响应的结果(因为不设置SYN,RST,或者ACK位的报文发送到开放端口时服务端会丢弃该报文),那么就有如下行为/状态表:

行为 状态
未响应 Open/filtered
返回RST Closed
ICMP不可达错误 filtered

这种扫描我在扫描本地时准确率不低,但扫描服务器时因为服务器大部分端口都是filtered,且它没能识别出来是open还是filtered,当然了其优点是比SYN扫描更为隐蔽,且能够躲过一些无状态防火墙和报文过滤路由器。

TCP Null

对应于nmap中的-sN。

Null扫描,不设置任何标志位(tcp标志头是0),那么做个测试,将flags位置空看看会得到什么结果?

当尝试扫描一个开放的端口时,会没有响应;当将该端口关闭后再次扫描,会得到如下结果:

###[ TCP ]### 
     sport     = opsession_prxy
     dport     = ftp_data
     seq       = 0
     ack       = 0
     dataofs   = 5
     reserved  = 0
     flags     = RA
     window    = 0
     chksum    = 0xfe1c
     urgptr    = 0
     options   = []

demo:

def tcp_null_scan(host,port):
    send = sr1(IP(dst=host) / TCP(dport=port, flags=0x00), timeout=10, verbose=0)
    if (send is None):
        print("[+] %s %d \033[92m Open/Filtered \033[0m" % (host, port))
    elif send.haslayer(TCP):
        if send.getlayer(TCP).flags == 0x14:
            # closed
            pass
    elif send.haslayer(ICMP):
        if send.getlayer(TCP).type == 3 and send.getlayer(TCP).code in [1, 2, 3, 9, 10, 13]:
            # filtered
            pass

FIN SCAN

对应于nmap中的-sF。

与null的差异就是只设置TCP FIN标志位。

其实与null无甚差异,因此扫描代码也只需要将flags位置为F:

sr1(IP(dst=host)/TCP(dport=port,flags="F"),timeout=10,verbose=0)

Xmans SCAN

对应于nmap中的-sX。

根据nmap源码中给出也就是设置FIN\URG\PUSH三个标志位,其余遵循先前的行为状态规则。

sr1(IP(dst=host)/TCP(dport=port,flags="FUP"),timeout=10,verbose=0)

TCP ACK SCAN

对应于nmap中的-sA。

这种扫描技术无法确定端口的开放性,因为它做的就是发送一个ACK报文,但服务端在收到该报文时,无论端口是否开放(open/closed),只要未被过滤,都会返回一个RST报文,将它们认为是unfiltered,而当扫描一个端口未得到响应时则认为是filtered,同样的ICMP不可达也认为是filtered。

咋一看貌似无法用于确定端口的状态,也确实如此,这种扫描技术实际上是用于发现防火墙的规则,确定它们是有状态的还是无状态的,并且得到filtered的端口。

其行为状态表如下:

行为 状态
收到RST报文 unfiltered(open/closed)
未响应 filtered
ICMP不可达 filtered

只设置ACK标志位即可。

demo:

def tcp_ack_scan(host,port):
    send = sr1(IP(dst=host) / TCP(dport=port, flags="A"), timeout=10, verbose=0)
    if (send is None):
        # filtered
        pass
    elif send.haslayer(TCP):
        if send.getlayer(TCP).flags == 0x04:
            print("[+] %s %d \033[92m Unfiltered \033[0m" % (host, port))
    elif send.haslayer(ICMP):
        if send.getlayer(TCP).type == 3 and send.getlayer(TCP).code in [1, 2, 3, 9, 10, 13]:
            # filtered
            pass

TCP WINDOW SCAN

对应于nmap中的-sW。

同上一条的ACK扫描一般,窗口扫描也是发送一个ACK报文,但它能够区别出端口的开放(open/closed);现在尝试发送一个ACK报文到目标开放端口,查看一下返回内容:

###[ TCP ]###
     sport     = http
     dport     = ftp_data
     seq       = 2170377956
     ack       = 1
     dataofs   = 6
     reserved  = 0
     flags     = SA
     window    = 29200
     chksum    = 0x7761
     urgptr    = 0
     options   = [('MSS', 1318)]

有一个名为window的字段,这在前面是被忽略的一个字段,现在再次发送ACK报文到目标关闭的端口,会发现该值为0,因此窗口扫描技术是可以根据window值是正值 or 0来判断目标端口是open还是closed。

当然在大部分情况因为系统不支持的原因,得到的只是一个无响应或者是window值为0,从而令所有端口为filtered或者closed;例如用nmap去扫描的话得到的结果是所有端口都为filtered,而我本地发送报文到本地开放端口往往得到的也是一个window为0的结果,并且nmap文档中也提到了该扫描偶尔会得到一个相反的结果,如扫描1000个端口仅有3个端口关闭,此时这3个端口可能才是开放的端口。

其行为/状态表如下:

行为 状态
window>0 open
window=0 closed
未响应 Filtered
ICMP不可达 filtered

demo:

def tcp_window_scan(host,port):
    send = sr1(IP(dst=host) / TCP(dport=port, flags="A"), timeout=2, verbose=0)
    if type(send) == None:
        # filtered
        pass
    elif send.haslayout(TCP):
        if send.getlayer(TCP).window > 0:
            print("[+] %s %d \033[92m Open \033[0m" % (host, port))
        elif send.getlayer(TCP).window == 0:
    # closed
    elif send.haslayer(ICMP):
        if send.getlayer(TCP).type == 3 and send.getlayer(TCP).code in [1, 2, 3, 9, 10, 13]:
            # filtered
            pass

TCP Maimon SCAN

对应于nmap中的-sM。

该扫描技术本人没使用过,也是看nmap文档才了解到,首先该扫描技术与前面提到的Null,FIN,Xmas扫描一般,可以看到nmap中也是将它与这三种扫描技术归到同一类:

case FIN_SCAN:
case XMAS_SCAN:
case MAIMON_SCAN:
case NULL_SCAN:
    noresp_open_scan = true;

然而nmap中给它设置的标志位是TH_FIN|TH_ACK,而据文档所述,在RFC 793 (TCP)标准下无论端口开放还是关闭都应该响应RST给这样的探测,然而在许多基于BSD的系统中,如果端口开放,它只是丢弃该探测报文而非响应。

那么其行为/状态表与前面提到的Null等三项扫描技术一致。

send = sr1(IP(dst=host) / TCP(dport=port, flags="AF"), timeout=2, verbose=0)

UDP SCAN

对应于nmap中的-sU。

显然UDP扫描相较于TCP扫描而言使用的较少,就我个人来说我也不习惯去扫描UDP端口,而例如常见的DNS,SNMP,和DHCP (注册的端口是53,161/162,和67/68)同样也是一些不可忽略的端口,在某些情况下会有些用处。

同样的在nmap文档中给出了说明:

UDP扫描发送空的(没有数据)UDP报头到每个目标端口。 如果返回ICMP端口不可到达错误(类型3,代码3), 该端口是closed(关闭的)。 其它ICMP不可到达错误(类型3, 代码1,2,9,10,或者13)表明该端口是filtered(被过滤的)。 偶尔地,某服务会响应一个UDP报文,证明该端口是open(开放的)。 如果几次重试后还没有响应,该端口就被认为是 open|filtered(开放|被过滤的)。 这意味着该端口可能是开放的,也可能包过滤器正在封锁通信。

那么整理一下大概是如下:

行为 状态
ICMP端口不可到达错误(类型3,代码3) closed
ICMP不可到达错误(类型3, 代码1,2,9,10,或者13) filtered
响应 open
重试未响应 open|filtered

那么UDP不受欢迎的原因一个在于目前主流的服务大部分基于TCP,而其次便在于UDP扫描的特殊性,在一次扫描过后通常无法得到一个正确的响应,偶尔某服务会返回一个UDP报文说明该端口开放,因此需要进行多次探测;对于ICMP不可到达错误,需要知道的是一般主机在默认情况下限制ICMP端口不可到达消息,如一秒限制发送一条不可达消息。

可以看一下分别对本机的53端口跟一个不存在UDP服务的端口发送UDP报文查看它们的区别。

Open:

###[ UDP ]### 
     sport     = domain
     dport     = domain
     len       = 8
     chksum    = 0x172

closed:

###[ ICMP ]### 
     type      = dest-unreach
     code      = port-unreachable
     chksum    = 0xfc44
     reserved  = 0
     length    = 0
     nexthopmtu= 0
###[ UDP in ICMP ]### 
           sport     = domain
           dport     = ntp
           len       = 8
           chksum    = 0x0

一如TCP扫描一般,ICMP可以用getlayer(ICMP).type和code来获取到它们对应的状态,那么同样给出一个demo:

def udp_scan(host,port):
    send = sr1(IP(dst=host) / UDP(dport=port), retry=-2, timeout=2, verbose=0)

    if (send is None):
        # filtered
        pass
    elif send.haslayer(ICMP):
        if send.getlayer(ICMP).type == 3:
            if send.getlayer(ICMP).code in [1, 2, 9, 10, 13]:
                # filtered
                pass
            elif send.getlayer(ICMP).code == 3:
                # closed
                pass
    elif send.haslayer(UDP):
        print("[+] %s U%d \033[92m Open \033[0m" % (host, port))

IP SCAN

对应于nmap中的-sO。

严格来说这并不是端口扫描,IP SCAN所得到的结果是目标机器支持的IP协议 (TCP,ICMP,IGMP,等等)。

协议扫描扫的不是端口,它是在IP协议域的8位上循环,仅发送IP报文,而不是前面使用到的TCP or UDP报文,同时报文头(除了流行的TCP、ICMP、UDP协议外)会是空的,根据nmap中说的是如果得到对应协议的响应则将该协议标记为open, ICMP协议不可到达 错误(类型 3,代号 2) 导致协议被标记为 closed。其它ICMP不可到达协议(类型 3,代号 1,3,9,10,或者13) 导致协议被标记为 filtered (虽然同时他们证明ICMP是 open )。如果重试之后仍没有收到响应, 该协议就被标记为open|filtered。

scapy中可以简单的使用IP来发送默认的256个协议。

ans,unans = sr(IP(dst=host,proto=(0,255))/"SCAPY")
ans.show()
//0000 127.0.0.1 > 127.0.0.1 ip / Raw ==> 127.0.0.1 > 127.0.0.1 ip / Raw

版本探测

对应于nmap的-sV。

同协议探测,版本探测严格来讲不在端口扫描的范围内,但却也互相关联,尽管在探测到常见的80端口,3306端口等等会第一时间辨认出运行在其上的服务,然而有些人的想法是难以猜透的,在10086端口放个web服务也不是不可能的,总会有奇奇怪怪的人在一些奇奇怪怪的端口运行着一些常见的服务。

在扫描到开放的端口后,版本探测所做的就是识别出这些端口对应的服务,在nmap中的nmap-service-probes文件中存放着不同服务的探测报文和解析识别响应的匹配表达式,例如使用nmap进行版本探测,会得到如下结果:

PORT   STATE SERVICE VERSION
80/tcp open  http    nginx 1.14.0 (Ubuntu)
443/tcp open  ssl/http nginx 1.14.0 (Ubuntu)

对于版本探测的实现相对简单的方式就是通过无阻塞套接字和探查,然后对响应进行匹配以期得到一个正确的探测结果,在使用nmap探测一个nc监听的端口时会发现收到了一个GET / HTTP/1.0

那么同样可以使用python的socket发送它来得到服务响应。

ip = "ip"
port = 80

PROBE = 'GET / HTTP/1.0\r\n\r\n'
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
result = sock.connect_ex((ip, port))
if result == 0:
    try:
        sock.sendall(PROBE.encode())
        response = sock.recv(256)
        if response:
            print(response)
    except ConnectionResetError:
        pass
else:
    pass
sock.close()
//b'HTTP/1.1 200 OK\r\nServer: nginx/1.14.0 (Ubuntu)\r\nDate: Mon

只需要一个合理的正则便够匹配出正确的服务和版本信息。

Reference

https://xz.aliyun.com/t/5376#toc-24

https://nmap.org/man/zh/

https://www.osgeo.cn/scapy/

https://www.imooc.com/article/286803



本文原创于HhhM的博客,转载请标明出处。



CopyRight © 2019-2020 HhhM
Power By Django & Bootstrap
已运行
粤ICP备19064649号