#include #include #include #include #include #include #include #include #include #include #include #include #define __USE_GNU // hsearch_r #include #include #include #include /* T A L O S V U L N D E V ░▀█▀░█▀█░█░░░█▀█░█▀▀░░░█░█░█░█░█░░░█▀█░█▀▄░█▀▀░█░█ ░░█░░█▀█░█░░░█░█░▀▀█░░░▀▄▀░█░█░█░░░█░█░█░█░█▀▀░▀▄▀ ░░▀░░▀░▀░▀▀▀░▀▀▀░▀▀▀░░░░▀░░▀▀▀░▀▀▀░▀░▀░▀▀░░▀▀▀░░▀░ I N T E R V I E W C H A L L E N G E 2 0 1 6 Congratulations, you got the code, now time to find some bugs! There are three vulnerabilities in this code, find them and document them. There is one addtional vulnerability that has been edited out of the code. Find it in the binary! original binary built with the default options of gcc on Ubuntu 15.10 gcc version 5.2.1 20151010 (Ubuntu 5.2.1-22ubuntu2) */ #define _error(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0); #define http_error(stream, e) \ do { \ fprintf(stream, \ "HTTP/1.1 %s\r\n" \ "Server: vhttpd\r\n" \ "Content-Type: text/plain\r\n" \ "Content-Length: %d\r\n" \ "Connection: close\r\n" \ "\r\n" \ "%s\r\n\r\n", \ e, (int)strlen(e) + 4, e); \ } while(0); #define _clienterror(code, msg) do { http_error(global.stream, ""#code " " msg); global.cleanup_func(global.stream); exit(EXIT_FAILURE); } while (0); #define _clientwarning(code, msg) do { http_error(global.stream, ""#code " " msg); global.cleanup_func(global.stream); } while (0); #define BUFSIZE 0x10000 #define XOR_KEY (unsigned char *)(banner + 51 + 151 + 69) #define XOR_KEY_LEN 8 //#define PHF_HASH "\x0F\x37\x29\xCD\xFD\x28\xCD\x3A\x0F\x28\x2A\xC6\xEB\x17\xC5\x3C\xCD\x39\x35\x21\x30\xE3\x14\x39\x0F\x36\x2B\xCE\xFB\x29\xC5\x24\x09\xE6\xF2\xD6\x40\x32\xD8\x28\xC8\xFD\x35\xD2\x37\xEC\xDA\x41\xD4" #define PHF_HASH_LEN 49 #define PHF_HASH "\x0F\x37\x25\xCD\xFD\x24\xCD\x3A\x0F\x28\x2E\xC6\xEB\x13\xC5\x3C\xCD\x39\x39\x21\x30\xE7\x14\x39\x0F\x36\x2F\xCE\xFB\x25\xC5\x24\x09\xE6\xF6\xD6\x40\x36\xD8\x28\xC8\xFD\x39\xD2\x37\xF0\xDA\x41\xD4" /** constants */ const char *banner = // XOR key = offset 271 = row 3, beginning of V " T A L O S V U L N D E V \n" // len = 51 "░▀█▀░█▀█░█░░░█▀█░█▀▀░░░█░█░█░█░█░░░█▀█░█▀▄░█▀▀░█░█\n" // len = 151 "░░█░░█▀█░█░░░█░█░▀▀█░░░▀▄▀░█░█░█░░░█░█░█░█░█▀▀░▀▄▀\n" "░░▀░░▀░▀░▀▀▀░▀▀▀░▀▀▀░░░░▀░░▀▀▀░▀▀▀░▀░▀░▀▀░░▀▀▀░░▀░\n" " I N T E R V I E W C H A L L E N G E 2 0 1 6 \n"; const char *instructions = "Objectives:\n" "\n" "- Find the request that allows you to retrieve the source code for this binary\n" " (The embedded archive is encrypted with the secret path)\n" "- Identify three vulnerabilities in the source code\n" "- Identify one additional vulnerability that is only in the binary\n" "- Write trigger inputs for all vulnerabilites\n" "- Exploit the memory corruption that exists only in the binary\n" " - Choose the level of mitigations that shows your ability\n" "\n" "Please document your process through the stages of the challenge\n" "and deliver a report with your findings and notes to vulndev@cisco.com\n" "\n" "Good luck!\n"; /** static globals */ static struct { struct hsearch_data headerTable; char default_message[0xb8]; void (*cleanup_func)(void*); FILE *stream; char default_mimetype[64]; char request[BUFSIZE]; } global; /** externals */ extern void *_binary_vhttpd_src_zip_start; extern void *_binary_vhttpd_src_zip_end; extern void *_binary_vhttpd_src_zip_size; extern char **environ; #define CGI_MAX_ARGS 32 char **cgi_args; #define HEX_TO_DECIMAL(char1, char2) \ (((char1 >= 'A') ? (((char1 & 0xdf) - 'A') + 10) : (char1 - '0')) * 16) + \ (((char2 >= 'A') ? (((char2 & 0xdf) - 'A') + 10) : (char2 - '0'))) // caller must free unsigned char *xor_multi(unsigned char *buf, int buf_len, unsigned char *key, int key_len) { int i; unsigned char *hash = (unsigned char *)malloc(buf_len); if(!hash) _error("malloc"); for (i = 0; i < buf_len; i++) hash[i] = (buf[i] ^ key[i % key_len]) + 0x42; return hash; } // algorithm borrowed from boa webserver int unescape_uri(char *uri, char **query_string) { char c, d; char *uri_old; uri_old = uri; while ((c = *uri_old)) { if (c == '%') { uri_old++; if ((c = *uri_old++) && (d = *uri_old++)) *uri++ = HEX_TO_DECIMAL(c, d); else return 0; /* NULL in chars to be decoded */ } else if (c == '?') { /* query string */ if (query_string) *query_string = ++uri_old; /* stop here */ *uri = '\0'; return(1); break; } else if (c == '#') { /* fragment */ /* legal part of URL, but we do *not* care. * However, we still have to look for the query string */ if (query_string) { ++uri_old; while((c = *uri_old)) { if (c == '?') { *query_string = ++uri_old; break; } ++uri_old; } } break; } else { *uri++ = c; uri_old++; } } *uri = '\0'; return 1; } int xdup(int fd, int target) { int result; if((result = dup2(fd, target)) < 0) _error("dup2"); return result; } // opaque struct behind hsearch typedef struct _ENTRY { unsigned int used; ENTRY entry; } _ENTRY; void cgi_set_environment() { struct hsearch_data *e_table; _ENTRY *e; ENTRY *p; int i; cgi_args = calloc(CGI_MAX_ARGS, sizeof(char*const)); e_table = &global.headerTable; for(i = 0; i < global.headerTable.size; i++) { if(e_table->table[i].used != 0) { p = &e_table->table[i].entry; setenv(p->key, p->data, 1); } } } int cgi_exec(int io, char* path, ...) { int i; int st, pid; char** args; va_list l; pid = fork(); if (pid == -1) { _error("fork"); } else if (pid > 0) { if (pid != waitpid(pid, &st, 0)) _error("waitpid"); return st; } assert(pid == 0); // dup some fd back to us xdup(io, STDIN_FILENO); xdup(io, STDOUT_FILENO); xdup(io, STDERR_FILENO); va_start(l, path); cgi_args[0] = basename(path); for(i = 1; (cgi_args[i] = va_arg(l, char*const)); i++); va_end(l); // now go and execv if (execv(path, cgi_args) < 0) _error("execve"); return 0; } void do_cgi_request(FILE *stream, char *uri) { int path_len; char path[FILENAME_MAX]; unsigned char *uri_hash; int uri_len; const char *mimetype = "application/octet-stream"; char *query_string; struct stat sbuf; int st; // download source if((uri_len = strlen(uri)) == PHF_HASH_LEN) { uri_hash = xor_multi((unsigned char *)uri, uri_len, XOR_KEY, XOR_KEY_LEN); if(!memcmp(uri_hash, PHF_HASH, PHF_HASH_LEN)) { fprintf(stream, "HTTP/1.1 200 OK\r\n"); fprintf(stream, "Server: vhttpd\r\n"); fprintf(stream, "Content-Type: %s\r\n", mimetype); fprintf(stream, "Content-Length: %d\r\n", (int)(long)&_binary_vhttpd_src_zip_size); fprintf(stream, "Connection: close\r\n"); fprintf(stream, "Accept-Ranges: bytes\r\n"); //fprintf(stream, "Content-Name: vhttpd-src.zip\r\n"); fprintf(stream, "\r\n"); fflush(stream); fwrite(&_binary_vhttpd_src_zip_start, (int)(long)&_binary_vhttpd_src_zip_size, 1, stream); fflush(stream); free(uri_hash); return; } free(uri_hash); } // separate path from args if ((query_string = strchr(uri, '?'))) { *query_string = 0; query_string++; unescape_uri(uri, NULL); } else query_string = (char *)""; // make path relative path_len = snprintf(path, sizeof(path), ".%s", uri); if (path_len < 0) _clienterror(404, "Not Found"); // stat cgi binary if (stat(path, &sbuf) == -1) _clienterror(404, "Not Found"); // check perms if(!(S_ISREG(sbuf.st_mode) && (S_IXUSR & sbuf.st_mode))) _clienterror(403, "Forbidden"); setenv("QUERY_STRING", query_string, 1); fprintf(stream, "HTTP/1.1 200 OK\r\n"); fprintf(stream, "Server: vhttpd\r\n"); fprintf(stream, "Content-Type: %s\r\n", mimetype); fprintf(stream, "Connection: close\r\n"); fprintf(stream, "Accept-Ranges: bytes\r\n"); fprintf(stream, "\r\n"); fflush(stream); // exec cgi cgi_set_environment(); if(cgi_exec(fileno(stream), getenv("SHELL"), "-c", path, NULL) < 0) _clientwarning(500, "Internal Server Error"); return; } char *get_http_header(char *name) { ENTRY e = {name, NULL}; ENTRY* ep; if (!(hsearch_r(e, FIND, &ep, &global.headerTable))) _error("hsearch_r"); return ep? ep->data : NULL; } void add_http_header(char *name, char *value) { int res; ENTRY e, *ep; e.key = strdup(name); e.data = strdup(value); if (!(hsearch_r(e, ENTER, &ep, &global.headerTable))) _error("hsearch_r"); } int parse_http_headers(FILE *stream) { char *buf = malloc(BUFSIZE); char *val = NULL; // parse HTTP headers while(1) { if(!fgets(buf, BUFSIZE, stream)) break; if(!strcmp(buf, "\r\n") || !strcmp(buf, "\n")) break; // chomp off newlines if((val = strchr(buf, '\r'))) *val = 0; if((val = strchr(buf, '\n'))) *val = 0; if((val = strstr(buf, ": "))) { *val = 0; val += 2; add_http_header(buf, val); if(!strcmp(buf, "Content-Length"))) buf = malloc(atoi(val)); } } free(buf); return 1; } void abort_cleanup(void* data) { FILE* stream = data; fclose(stream); } int parse_http_request(int c_fd) { int res_fd; // for requested resource FILE *stream; char* method; char* version; char* uri; int uri_len; int path_len; char path[FILENAME_MAX]; const char* mimetype = global.default_mimetype; char* query_string; struct stat sbuf; unsigned char *filemap; // open connection as stream if ((stream = fdopen(c_fd, "r+")) == NULL) _error("fdopen"); // initialize some global state strcpy(global.default_mimetype, "text/plain"); global.cleanup_func = abort_cleanup; global.stream = stream; // HTTP request fgets(global.request, sizeof(global.request), stream); fprintf(stderr, "[vhttpd] %s", global.request); // tokenize it if(!(method = strtok(global.request, " "))) _clienterror(400, "Bad Request"); if(!(uri = strtok(NULL, " "))) _clienterror(400, "Bad Request"); if(!(version = strtok(NULL, " "))) _clienterror(400, "Bad Request"); if (strtok(NULL, " ") != NULL) _clienterror(400, "Bad Request"); // make sure it's valid if(!strlen(method) || !strlen(uri) || !strlen(version)) _clienterror(400, "Bad Request"); uri_len = strlen(uri); // only handle GET or POST if (strcmp(method, "GET") != 0 && strcmp(method, "POST") != 0) _clienterror(501, "Not Implemented"); // filter directory traversal if(strstr(uri, "..")) { http_error(stream, "400 Bad Request"); fprintf(stream, "bad kitty!\n"); fclose(stream); return 400; } // initialize hash table for headers if (!hcreate_r(512, &global.headerTable)) _clienterror(500, "Internal Server Error"); if(!parse_http_headers(stream)) _clienterror(400, "Bad Request"); // parse URI if (strstr(uri, "/cgi-bin/") == uri) { do_cgi_request(stream, uri); } else if (strstr(uri, "/robots.txt") == uri) { fprintf(stream, "User-agent: *\r\n"); fprintf(stream, "Disallow: /cgi-bin/\r\n"); fprintf(stream, "Disallow: /\r\n"); fflush(stream); } else { // make relative path and look for the file if(uri_len == 1) path_len = snprintf(path, sizeof(path), "./index.html"); else path_len = snprintf(path, sizeof(path), ".%s", uri); path[path_len] = '\x00'; // remove query string from path if ((query_string = strchr(path, '?'))) *query_string = 0; // stat file and make sure it's there if (stat(path, &sbuf) != 0) _clienterror(404, "Not Found"); // figure out the mimetype if (!strcmp(uri + uri_len - 5, ".html")) mimetype = "text/html"; else if (!strcmp(uri + uri_len - 4, ".htm")) mimetype = "text/html"; else if (!strcmp(uri + uri_len - 4, ".ico")) mimetype = "image/x-ico"; else if (!strcmp(uri + uri_len - 4, ".bmp")) mimetype = "image/bmp"; else if (!strcmp(uri + uri_len - 4, ".jpg")) mimetype = "image/jpg"; else if (!strcmp(uri + uri_len - 4, ".gif")) mimetype = "image/gif"; else if (!strcmp(uri + uri_len - 4, ".png")) mimetype = "image/png"; else if (!strcmp(uri + uri_len - 4, ".zip")) mimetype = "application/zip"; // serve static content fprintf(stream, "HTTP/1.1 200 OK\r\n"); fprintf(stream, "Server: vhttpd\r\n"); fprintf(stream, "Content-Length: %d\r\n", (int)sbuf.st_size); fprintf(stream, "Content-Type: %s\r\n", mimetype); fprintf(stream, "\r\n"); fflush(stream); // Use mmap to return arbitrary-sized response body res_fd = open(path, O_RDONLY); filemap = (unsigned char *)mmap(0, sbuf.st_size, PROT_READ, MAP_PRIVATE, res_fd, 0); fwrite(filemap, sbuf.st_size, 1, stream); fflush(stream); munmap(filemap, sbuf.st_size); } fclose(stream); hdestroy_r(&global.headerTable); return 0; } int main(int argc, char **argv) { unsigned short port; int s_fd, c_fd; int optval; struct sockaddr_in s_addr, c_addr; socklen_t addrlen; struct stat st; pid_t pid; // instructions fprintf(stderr, "\n%s\n%s\n", banner, instructions); // check args if (argc != 2) { fprintf(stderr, "error: Please select a port for the web server\n" "\n" "usage: %s \n", argv[0]); exit(EXIT_FAILURE); } port = atoi(argv[1]); // make sure cgi-bin exists if (stat("./cgi-bin", &st) == -1) mkdir("./cgi-bin", 0755); symlink("/usr/bin/env", "./cgi-bin/env"); // open socket if((s_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) _error("socket"); // close sock on exec if (fcntl(s_fd, F_SETFD, 1) == -1) _error("fcntl"); // reuse socket optval = 1; if((setsockopt(s_fd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int))) == -1) _error("setsockopt"); // bind memset(&s_addr, 0, sizeof(s_addr)); s_addr.sin_family = AF_INET; s_addr.sin_addr.s_addr = htonl(INADDR_ANY); s_addr.sin_port = htons(port); if (bind(s_fd, (struct sockaddr *) &s_addr, sizeof(s_addr)) < 0) _error("bind"); if (listen(s_fd, 20) < 0) _error("listen"); fprintf(stderr, "[vhttpd] listening on %s:%d\n", inet_ntoa(s_addr.sin_addr), port); // ignore child signals signal(SIGCHLD, SIG_IGN); // accept while (1) { // wait for a connection addrlen = sizeof(c_addr); c_fd = accept(s_fd, (struct sockaddr *) &c_addr, &addrlen); if (c_fd < 0) _error("accept"); // log connection fprintf(stderr, "\n[vhttpd] Connection accepted from %s:\n", inet_ntoa(c_addr.sin_addr)); // fork to make remote exploits easier :) pid = fork(); if(pid == -1) { close(c_fd); _error("fork"); } else if(pid > 0) { close(c_fd); continue; } assert(pid == 0); parse_http_request(c_fd); close(c_fd); break; } return 0; }