前几天,老张写了两篇关于FTP的文章:
给大家介绍了FTP的通信机制,然后又带大家写了一个玩具版的FTP服务端代码。
今天继续给大家带来FTP系列的第三篇《窥探FTP通信细节》,通过抓包FTP的通信,将FTP的扒的底裤都不剩。
环境准备:
-
FTP客户端测试脚本:依然选择Python自带的Ftplib来编写测试脚本
-
Wireshark:一个网络层的抓包工具
-
一台主机:用于运行FTP客户端脚本和Wireshark。ip为192.168.16.1
-
一台Linux虚拟机:运行Vsftpd作为FTP服务端。ip为192.168.16.129
-
FTP服务器的模式:选择主动模式,使用Binary模式传输数据。
注:为什么不用老张的玩具版?使用成熟通用的Vsftpd是为了能够更好的帮助大家理解,避免不必要的歧义。
OK,请各位系好安全带,马上开车了!
1. 连接FTP服务器,建立命令通道通道
import ftplib
ftp = ftplib.FTP()
ftp.connect("192.168.16.129", 21)
此时使用Wireshark抓包,可以看到:
经过TCP的三次握手,命令通道建立。此时FTP服务器会向客户端发送一条220状态码的消息,表示命令通道已建立。但是注意,此时还没有登录鉴权。
2. 客户端发送账号密码
ftp.connect("192.168.16.129", 21)
ftp.login("root", "root") # 此处密码并非正式密码,抓包时老张也机智的把密码抹去了
此时报文消息如下:
可以看到账号和密码是通过两条消息分别发送的。
3. 客户端设置本次连接使用主动模式:
ftp.set_pasv(False)
此行为完全是客户端本地行为,没有同服务器之间进行信息交换。
4. 将本地文件上传至服务器:
# 将本地文件上传至服务器
with open("client", 'rb') as f:
ftp.storbinary("STOR upload_from_client", f, 1024)
虽然看起来只有一个STOR命令,但是此时却是客户端和服务器之间信息交换最繁忙的时候,为了能够讲清楚,老张将整个过程拆解了一下。
4.1 传输上传命令:
首先,客户端通过命令通道通知服务端,本次数据传输将使用Binary模式。
然后,客户端将自己为数据通道准备的host及port发送给服务器。
注:关于端口号port的传输格式,可以参考上一篇的代码实现。
最后,客户端才发送上传命令,通知服务器文件需要保存在默认文件夹,使用“upload_from_client”作为文件名。
4.2 建立数据通道,传递数据:
可以看到,数据通道是需要时才会建立,并不是一开始就建立好的,并且在数据传输完成之后立刻关闭。
还有另一个细节,主动模式下,服务器在收到上传命令后,响应上传命令和建立数据通道是同步进行的,这一点是我们的单线程玩具版不能比拟的。
4.3 服务器通知客户端,上传完成:
5. 下载服务器文件至本地:
# 下载服务器文件
with open("download_from_server", "wb") as f:
ftp.retrbinary("RETR server", f.write)
同上传流程类似,这里我们继续拆解。
5.1 传输下载命令:
到这里有没有发现,其实下载和上传的流程是几乎一模一样的。
5.2 建立数据通道,传递数据:
必须指出的是,每次客户端建立数据通道使用的端口号并不是固定不变的。
5.3 服务器通知客户端,下载完成:
一旦数据传输完成,服务器依然会发送一条消息通知客户端。
6. 程序退出,命令通道关闭:
以上就是FTP的通信细节,不知道各位同学看完有没有一丝疑惑?没错,老张之前也给大家强调过FTP协议是明文传输,你所有的秘密都不是秘密!