Created
September 9, 2025 19:34
-
-
Save richinseattle/c4c725da66aa05831e5c26b122f887d8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <stdio.h> | |
| #include <string.h> | |
| #include <sys/types.h> | |
| #include <sys/socket.h> | |
| #include <arpa/inet.h> | |
| #include <stdlib.h> | |
| #include <unistd.h> | |
| #include <sys/stat.h> | |
| #include <sys/mman.h> | |
| #include <fcntl.h> | |
| #include <sys/wait.h> | |
| #include <sys/queue.h> | |
| #define __USE_GNU // hsearch_r | |
| #include <search.h> | |
| #include <stdarg.h> | |
| #include <libgen.h> | |
| #include <assert.h> | |
| /* | |
| 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 [email protected]\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 <port>\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; | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment