Lucene search

K
talosTalos IntelligenceTALOS-2017-0435
HistoryOct 31, 2017 - 12:00 a.m.

Circle with Disney Apid Use-Between-Reallocs Information Disclosure Vulnerability

2017-10-3100:00:00
Talos Intelligence
www.talosintelligence.com
19

CVSS2

5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

NONE

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:N/A:N

CVSS3

5.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

CHANGED

Confidentiality Impact

LOW

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N

EPSS

0.001

Percentile

51.0%

Summary

An exploitable information disclosure vulnerability exists in the apid daemon of the Circle with Disney running firmware 2.0.1. A specially crafted set of packets can make the Disney Circle dump strings from an internal database into an HTTP response. An attacker needs network connectivity to the Internet to trigger this vulnerability.

Tested Versions

Circle with Disney 2.0.1

Product URLs

<https://meetcircle.com/&gt;

CVSSv3 Score

5.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N

CWE

CWE-416: Use After Free

Details

Circle with Disney is a network device used to monitor and restrict internet use of children on a given network. When connected to a given network and configured, it immediately begins arp poisoning all filtered devices on the network, such that it can validate and restrict all traffic as is seen fit by the parent/administrator of the device.

The apid binary is a web server listening on the Disney Circle, that serves as the main API for user functionality, it is forked from <https://acme.com/software/mini_httpd/&gt;, a rather robust web server thatā€™s optimized for embedded devices. Through the apid server, all configurations and queries are made from the ā€˜Circle Homeā€™ application from the administratorā€™s phone.

For all the under-the-hood and low level processing of HTTP headers, the majority of the code is from mini_httpd, with a few modifications by Circle (once it gets into the Api parsing, however, the code is naturally all written specifically for the Circle). Interestingly, mini_httpd has no support for an HTTP request with any Body parameters, it sort of just stops reading when it finds the end of the HTTP headers (ā€œ\r\n\r\nā€). Because the Circle needs Body params for certain api calls (/api/CONFIG/restore and /api/UPLOAD_FIRMWARE), it became necessary for Circle to add this functionality, and they did this using the memory management functions already included in mini_httpd.

Both within mini_httpd and apid, the following code is used to parse the HTTP headers:

//handle_request( void ){ 
start_request();
   	for (;;){
        	char buf[10000];
        	int rr = my_read( buf, sizeof(buf) - 1 );
       		
	if ( rr &lt; 0 && ( errno == EINTR || errno == EAGAIN ) )
         		continue;
        	if ( rr &lt;= 0 )
            		break;
        	
	(void) alarm( READ_TIMEOUT );  // alarm in loop, lol
        	add_to_request( buf, rr );

       		if ( strstr( request, "\r\n\r\n" ) != (char*) 0 ||
             	    strstr( request, "\n\n" ) != (char*) 0 )
            		break;
    	}

The code will loop and continuously read in 0x2710 bytes until it finds either ā€œ\r\n\r\nā€ or ā€œ\n\nā€, denoting the end of the HTTP headers, and for each section that it reads in, itā€™ll add it to the request variable via add_to_request, which is listed below:

static void add_to_request( char* str, size_t len ){
   		 add_data( &request, &request_size, &request_len, str, len );
	}

Which then gets to the heart of the matter:

static void add_data( char** bufP, size_t* bufsizeP, size_t* buflenP, char* str, size_t len ){
    if ( *bufsizeP == 0 ) {   	// [1]
        *bufsizeP = len + 500;
        *buflenP = 0;
        *bufP = (char*) e_malloc( *bufsizeP );
    }
    else if ( *buflenP + len &gt;= *bufsizeP ) {
        *bufsizeP = *buflenP + len + 500;
        *bufP = (char*) e_realloc( (void*) *bufP, *bufsizeP );  //[3]
    }

    if ( len &gt; 0 )       //[2]
    {
        (void) memmove( &((*bufP)[*buflenP]), str, len );
        *buflenP += len;
    }

    (*bufP)[*buflenP] = '\0';
    }

At [1] we check to see if thereā€™s already an existing buffer with the userā€™s request. Assuming this is the first iteration of the read loop in handle_request(void), the function will malloc a buffer of sizeof(new_data) + 500. After this buffer has been mallocā€™ed, it just copies the userā€™s data into this new heap chunk [2]. When we already have user data inside the buffer though (and this will happen for any size request that is bigger than 10000 bytes), instead of mallocing another buffer, the program just reallocs the old buffer to a new size of (old_size + sizeof(new_data) + 500) [3].

An issue exists with how mini_httpd was extended to handle Body parameters, due to how Circle reused the add_to_request function to handle the rest of the HTTP request. This, by itself, would not normally be an issue, however, between the first and second calls to add_to_request, a set of variables is assigned to HTTP headers for further use.

Within a loop, the server looks for a given HTTP header, and then assigns a pointer to the address of that headerā€™s value within the original request. For example:

loc_406040:              #
la      $a1, aOrigin     # 'Origin:'    
jal     strncasecmp     # [1]
li      $a2, 7           # n
bnez    $v0, loc_4059D0  # [2]
addiu   $v1, $fp, 0x27C0+addr_of_request
[...]
move    $a0, $v1         # s
la      $a1, asc_42D810  # " \t"
jal     strspn   # skip spaces/tabs
sw      $v1, 0x27C0+query_addr_tmp($sp)  # addr of 'http://....'
lw      $v1, 0x27C0+query_addr_tmp($sp)
lui     $a0, 0x45        
addu    $v0, $v1, $v0
j       loc_4059D0
sw      $v0, Origin  # [3] 

It will look for ā€˜Origin:ā€™ in every line of the request[1], and then assign the value [3] if itā€™s found [2]. This is done for ā€˜Originā€™, ā€˜Cookieā€™, ā€˜Hostā€™,ā€™Authorizationā€™, ā€˜User Agentā€™, and ā€˜Content-Lengthā€™, among other less important values.

After these pointers have been assigned, if itā€™s an HTTP POST request, Apid then reads in bytes equal to the given ā€˜Content-Lengthā€™, using the add_to_request function just like before, acting on the same exact buffer as before, and this is where the problem lies. As per man malloc:

void *realloc(void *ptr, size_t size);
[...]
 The realloc() function returns a pointer to the newly allocated memory, which is suitably aligned for any built-in type and may be different from ptr, or NULL if the request fails.

Since the heap implementation is a uClibc version of dlmalloc <https://github.com/kraj/uClibc/blob/master/libc/stdlib/malloc-standard/malloc.c&gt;, the conditions for when a realloc will return a different pointer than the one provided are not too complicated. Since the user controls the size of the HTTP request, the user can control the size of call to realloc to some degree. Going back to the realloc_loop disassembly (since this is Circle code now):

loc_4056A4:     # start of loop         
lw      $v0, -0x58E8($s1)       # s1 == content_length
addiu   $a0, $sp, 0x438+memcpy_src
subu    $a1, $v0, $s0   	 #  s0==len_of_Body_data
sltu    $v0, $s0, $v0    		 # v0==Content-Length	 
beqz    $v0, loc_4056FC  # was set to 0...
sltiu   $v1, $a1, 0x400
[...]
li      $a1, 0x3FF
[...] 
loc_4056C8:              # a0=dstBuff
jal     read_bytes       # a1=num_bytes
nop
bgez    $v0, loc_405684
nop
[...]
sw      $v0, 0x438+size_of_malloc($sp)  # don't_get_here
move    $a1, $s3         # curr_malloc_buff_size
addiu   $a2, $s2, -0x58B4  # malloc_read_offset
addiu   $a3, $sp, 0x438+memcpy_src
jal     malloc_and_copy # add_data()
add	$so, $v0
j  loc_4056A4  # go back to top of loop

Just like before, the HTTP server will keep reading in bytes from the SSL socket until it hits the end of the request, and then for every 0x3FF bytes, we will hit the add_data function from before. Since the user controls the size of the Body parameters (via Content-Length), the user can cause realloc to be hit as many times as seen fit, in ever increasing chunks of size (old_size + 0x3FF + 0x1F4 ). After enough of these reallocations, the heap pointer returned from realloc will actually change:

[^_^] Malloc/realloc (ret_ptr:0x44a754, malloc_size:0x44a750, curr_size:0x52bf4, 	lolidk:*0x7fd98e00)
memcpy src: 'QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ'
-----------
[^_^] 0x4041a4 | Realloc(0xc3d208,0x531e7) = 0xc3d208 [1]
[...]
[^_^] Malloc/realloc (ret_ptr:0x44a754, malloc_size:0x44a750, curr_size:0x8c25f, 	lolidk:*0x7fd98e00)
memcpy src: 'QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ'
-----------
[^_^] 0x4041a4 | Realloc(0x76e7e008,0x8c76c) = 0x76e7e008 [2]

As shown above, after reaching a big enough size, we can actually make the reallocation result in an mmapā€™ed allocation instead of a normal heap allocation, and we can continuously repeat this. Which brings us back to those HTTP headers we mentioned before. Out of all the HTTP headers that are parsed and assigned to variables, five of them result in dangling pointers if we shift the underlying memory location with increasing reallocs: ā€˜Hostā€™,ā€™Cookieā€™,ā€™Originā€™,ā€User-Agentā€™, and ā€˜Authorizationā€™.

Only ā€˜Originā€™ and ā€˜User Agentā€™ are ever referenced in the code after their initial assignment, as a lot of features were stripped from mini_httpd when being forked into Apid. User-Agent gets read into a buffer with snprintf and then parsed and checked for a regex of ā€œMSIEā€, which doesnā€™t really do much, since we already have control of ā€˜User-Agentā€™. Unfortunately, the only real opportunity is that the value of ā€˜Originā€™ is returned in the HTTP response to the user (assuming the value of ā€˜Originā€™ is initially valid). An example of this would be:

[^_^] gotta response!
HTTP/1.1 200 Ok
Server: apid 1.0
Date: Mon, 28 Aug 2017 18:19:31 GMT
Vary: Accept-Encoding, Origin
Access-Control-Allow-Origin: http://localhost
Content-Type: application/json
Content-Length: 54
Connection: close

Where Access-Control-Allow-Origin: http://localhost corresponds to the line in my initial HTTP request Origin: http://localhost. The value is written into the request response with snprintf(response_buffer,ā€Access-Control-Allow-Origin: %sā€, Origin), which is taken straight from the dangling pointer from before. While this Use-After-Free is very limited in itā€™s actual use, thereā€™s still the potential of being able to read something out of the heap remotely, assuming that we can get something to allocate into the freed slot that ā€œOriginā€ is pointing to.

The obvious thought of information to leak would be passwords or tokens or PINs, etc. Unfortunately for the purposes of this exploit, not much is actually done on the heap in this daemon. Aside from the two instances of our realloc loop, thereā€™s a call to strdup in between, another realloc_loop on another buffer (the response), and then anything that occurs inside of the API parsing. After looking through all the unauthenticated API calls, it was found that the ā€˜/api/USERINFOā€™ api call will repeatedly use new[] void * and delete[] * to allocate space for reading /mnt/shares/usr/bin/configure.xml, which contains a decent amount of sensitive data. A sample of my configure.xml should suffice to explain:

 &lt;device uid="ab:cd:ef:12:34:56"&gt;
  	   &lt;ip&gt;192.168.1.5&lt;/ip&gt;
  	   &lt;hostname&gt;purgatory&lt;/hostname&gt;
  	   &lt;displayName&gt;purgatory&lt;/displayName&gt;
  	   &lt;manufacturer&gt;Unknown&lt;/manufacturer&gt;
  	   &lt;mode&gt;None&lt;/mode&gt;
  	   &lt;isGo&gt;false&lt;/isGo&gt;
   	 &lt;/device&gt;
  	&lt;/devices&gt;
       &lt;contact&gt;
       &lt;phone&gt;+12223334444&lt;/phone&gt;
	&lt;countryCode/&gt;
	&lt;email&gt;[email protected]&lt;/email&gt;
	&lt;name&gt;Lilith Wyatt&lt;/name&gt;
&lt;/contact&gt;

Regardless of what information it is, we still need a way to force the databaseā€™s buffer to be read into where ā€œOriginā€ is pointing. Coming full circle, as mentioned before, this is uClibcā€™s version of dlmalloc, so itā€™s pretty standard. If we can get our initial request to be within the same heap bin as the database read, then it should be smooth sailing after our realloc effectively frees our buffer.

We donā€™t know the size of the database, but it can be quickly brute forced, as the minimum size of the database is 2380 bytes (with the version in our test being about 4000), and also because of the binsizes of dlmalloc:

/*
  Indexing
    Bins for sizes &lt; 512 bytes contain chunks of all the same size, spaced
    8 bytes apart. Larger bins are approximately logarithmically spaced:
    64 bins of size       8
    32 bins of size      64
    16 bins of size     512
     8 bins of size    4096
     4 bins of size   32768
     2 bins of size  262144
     1 bin  of size what's left
    The bins top out around 1MB because we expect to service large
    requests via mmap.
*/

So we only really need to brute force in 512 byte intervals after 2048, and then every 4096 bytes after that.

If we had control of a malloc() and free() instead of realloc(), things would be simpler, as the size of our buffer would remain constant, and it would remain in the same bin, from start to finish. With realloc() however, this is not the case. If a realloced buffer does not shift its underlying memory (i.e. input ptr != output ptr), itā€™s still possible for it to shift bins. Put another way, even if a realloc crosses the boundary of a bin, the input pointer could still be equivalent to the output pointer. And this is a problem, since we need the realloc to shift memory in order for the UAF to work.

So, if we increase the size of our buffer to a point where we know that the memory location will shift after a realloc, it wonā€™t be in the same bin as the database, and we wonā€™t get anything useful from the UAF. But if we keep the HTTP request buffer at a size comparable to the databaseā€™s buffer, we donā€™t trigger the effective ā€œfree(req_ptr)ā€ in the first place. The only option left is to try and force realloc to shift without having to increase itā€™s size past a bin boundary, and this requires another allocation, which we can achieve using strdup.

The most surefire way to force realloc() to shift memory is to have another chunk of memory allocated immediately after it. Given the context of this code flow, the only available allocation between the two realloc() loops was a call to strdup (starting from immediately after the first realloc loop on the HTTP headers):

jal     malloc_and_copy   # realloc loop
addiu   $a3, $sp, 0x27C0+ssl_buffer
lw      exp, -0x58AC($s0)  # expanding+request
addiu   $a1, $s1, -0x2830  # needle (\r\n\r\n)
jal     strstr
move    $a0, exp         # haystack
bnez    $v0, loc_4058F8
move    $a0, exp         # haystack

loc_4058F8:              	    # looks for end of
jal     Extract_http_line  # first line (\r\n) and then
                     	               # null terminates
nop                      
beqz    $v0, loc_405D2C  # v0 == 'POST /api/.... HTTP/1.1\r\n'
lui     $a1, 0x43
jal     strdup        
move $a0, $v0

The above code will look for the first instance of ā€˜\r\nā€™, and then null terminate it, resulting in a cstring of the HTTP method, path, query string and version. This new cstring is then taken and strdupā€™ed, after which the new duplication is parsed further. Thankfully for our purposes, strdup() will allocate size for a copy of the argument string. Since apid does no validation of the ā€˜HTTP/1.1ā€™ portion of the request, we can pad it such that the length of the first line is within the same heap bin as the database.

req="POST  /api/USERINFO?api=1.0 AAAAAAAAAAAA[....]\r\n"

Also, since the Apid server doesnā€™t need many HTTP heaers, we can minimize the rest of the request such that the strdup() and the HTTP header realloc() both land within the same bin, such that the heap looks something like this:

     	[0xc2c560]		                   [0xc2c560+len(req)]					
[POST /api/USERINFO...\r\n\r\n][strdup(ā€œPOST /api/USER...\r\nā€)] 
			    ^
			    ||
		          (Origin*)

And then, when the second realloc loop occurs, any slight increase of the HTTP requestā€™s size will cause it to move past the strdup, without changing which heap bin it is in:

[0xc2c560]   [0xc2c560+len(req)]	                 [ā€¦.] 				
[Free Buffer][strdup(ā€œPOST /api/USER...\r\nā€)][POST /api/USERINFO...\r\n\r\n]
	    ^
                ||
          (Origin*)

And after the database is read in, the heap looks like such:

[0xc2c560]        [0xc2c560+len(req)]	           [ā€¦.] 				
[Configure.xml][strdup(ā€œPOST /api/USER...\r\nā€)][POST /api/USERINFO...\r\n\r\n]
	    ^
                ||
          (Origin*)

Allowing for us to disclose information from the database. It should be noted though, that due to the constraint of having the ā€œOrigin:ā€ header below the gigantic ā€œPOST /apiā€¦ā€¦\r\nā€ line in the request means that we have a limited space in configure.xml to read from, but the most valuable information (phone number/name/email) occurs at the bottom of the file.

# python get_bodied.py
[O_O] GOGOGO (Connected to circle...)
[~_~] S-s-s-sendding!!!?! len: 0x105d
[o_o] gotta response!
[O_O] gotta another response!
4:51 GMT
Vary: Accept-Encoding, Origin
Access-Control-Allow-Origin: [email protected]&lt;/email
Content-Type: application/json
Content-Length: 1030
Connection: close
[^_^] Thanks for hangin out!&lt;3

While itā€™s a limited information disclosure, it should be noted that, because of the constraints placed upon this bug (remote/unauthenticated), this exploit can be used in conjunction with TALOS-2017-0437, and can target any Circle in the world that has internet connectivity. Couple this with the fact that anyone can make a Circle send itā€™s owner an SMS with the deviceā€™s PIN inside (via /api/PASSCODE/sms), that can be used for further escalation, it would be plausible for the ownerā€™s phone number to be a valuable link in an exploit chain involving social engineering.

Timeline

2017-09-12 - Vendor Disclosure
2017-10-31 - Public Release

CVSS2

5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

NONE

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:N/A:N

CVSS3

5.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

CHANGED

Confidentiality Impact

LOW

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N

EPSS

0.001

Percentile

51.0%

Related for TALOS-2017-0435