端口扫描原理及实现简易端口扫描脚本

本文目的

通过本文,你将学习到端口扫描的原理,TCP全连接扫描(Connect)、半连接扫描(SYN)的代码实现

简易全连接扫描

通过轮询每个端口,发送TCP请求,如果能建立三次握手,那就说明端口是打开的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#coding:utf-8

import socket
from datetime import datetime

remote_server_ip = input('输入要扫描的IP:')

socket.setdefaulttimeout(0.5) #设置超时为0.5秒

def scan_port(port):
try:
s = socket.socket(2,1) #2:socket.AF_INET 1:socket.SOCK_STREAM
#AF_INET代表ipv4,SOCK_STREAM代表流式socket,对于发送的是TCP请求
#其实这两个参数不写也没事,因为默认的就是AF_INET和SOCK_STREAM
res = s.connect_ex((remote_server_ip,port))#连接到address处的套接字,参数为元组格式。有返回值,连接成功时返回0,出错时返回错误编码

if res == 0: # 如果端口开启
print('Port %s: OPEN' % port)
s.close()#关闭套接字

except Exception as e:
print(str(e.message))

# Check what time the scan started
t1 = datetime.now()

for i in range(1,65536): #全端口扫描
scan_port(i)

print('Singleprocess Scanning Completed in %s' % (datetime.now() - t1))

等了很久都没出扫描结果,按照半秒一个端口,6万多个端口要扫8个多小时。。这扫描速度也太慢了。我们要想个办法加快扫描速度,于是给代码加上多线程

多线程全连接扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#coding:utf-8

import socket
from datetime import datetime
from multiprocessing.dummy import Pool as ThreadPool

remote_server_ip = input('输入要扫描的IP:')
ports = []

socket.setdefaulttimeout(0.5) #设置超时为0.5秒

def scan_port(port):
try:
s = socket.socket(2,1) #2:socket.AF_INET 1:socket.SOCK_STREAM
#AF_INET代表ipv4,SOCK_STREAM代表流式socket,对于发送的是TCP请求
#其实这两个参数不写也没事,因为默认的就是AF_INET和SOCK_STREAM
res = s.connect_ex((remote_server_ip,port))#连接到address处的套接字,参数为元组格式。有返回值,连接成功时返回0,出错时返回错误编码

if res == 0: # 如果端口开启
print('Port %s: OPEN' % port)
s.close()#关闭套接字

except Exception as e:
print(str(e.message))

for i in range(1,65536): #全端口扫描
ports.append(i)

# Check what time the scan started
t1 = datetime.now()
pool = ThreadPool(processes = 1000) #设置线程池为1000
results = pool.map(scan_port,ports)
pool.close()
pool.join()

print('Multiprocess Scanning Completed in %s' % (datetime.now() - t1))

最终结果:扫描全端口也就34秒,因为是在本地,如果在外网可能还会慢些

扫描出的结果来看,10808、10809是v2ray的端口,33060是mysql8的端口,其他的端口用netstat -ano查了一下进程号,8680是微信的端口,50309是TIM的端口,7680是个系统程序svchost.exe用的端口,看不出是什么程序

全连接扫描

那么除了提高线程数量,我们还有没有办法再加快端口扫描的速度,答案是有的。

半连接扫描

上面的端口扫描是等到TCP完全建立连接后才确认端口是开启状态,也就是走完了整个三次握手的流程,也称全连接扫描。全连接扫描的优点是准确度比较高,缺点也比较明显,速度较慢,并且由于完整地建立了TCP,这个连接过程会被目标主机记录下来。

下面介绍一种半连接方式,也称TCP SYN扫描。当端口开启时,向端口发送TCP SYN请求,服务器会返回一个ACK响应;当端口关闭时,服务器会返回RST响应。这边我们使用Scapy来实现

1
2
3
4
5
6
7
8
9
10
11
12
from scapy.all import *
from datetime import datetime

remote_server_ip = input('输入要扫描的IP:')

def scan(dst,port):
ans, unans = sr(IP(dst=dst)/TCP(flags="S", dport=port),inter=0,retry=0,timeout=1)
ans.summary(lfilter = lambda s,r: r.sprintf("%TCP.flags%") == "SA",prn=lambda s,r: r.sprintf("%TCP.sport% is open"))

t1 = datetime.now()
scan(remote_server_ip,(1,65535))
print('Scanning Completed in %s' % (datetime.now() - t1))

SYN扫描

要注意的是scapy需要以root权限安装,并且用了scapy的脚本也需要以root权限运行!

单线程SYN扫描全端口也不过36秒,和开了1000个线程进行TCP全连接扫描全端口的时间差不多。那么scapy扫描能不能多线程扫描呢?答案是可以的。我们进入scapy的源码进行查看。我们可以找到程序调用链,位于sendrecv.py->sr()->sndrcv()->SndRcvHandler()。最后定位到SndRcvHandler这个函数,用于收发数据包。看到有段注释,可以看到是支持多线程模式的,只不过默认是关闭的。在发送大流量数据包时,多线程模式可能会破坏数据包的时间戳,但可能会加速。

看一下scapy源码

在实测的结果是,打开多线程开关即设置threaded为True,结果扫不出结果了,无语,可能是因为开启了多线程后时间戳乱了,拿不到正确的结果,注释里其实给了解决办法,就是在发包前记录时间戳,但是写代码太麻烦了,就不去试了。

无语😶

局限性

  1. 只支持扫描TCP的服务端口,不支持UDP
  2. 有些端口扫不出来,像135,445这些系统服务的端口只能通过别的方法扫出来

端口扫描原理及实现简易端口扫描脚本
https://wanf3ng.github.io/2021/02/04/端口扫描原理及实现简易端口扫描脚本/
作者
wanf3ng
发布于
2021年2月4日
许可协议