CVE-2017-12083
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.
Circle with Disney 2.0.1
5.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N
CWE-416: Use After Free
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/, 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 < 0 && ( errno == EINTR || errno == EAGAIN ) )
continue;
if ( rr <= 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 >= *bufsizeP ) {
*bufsizeP = *buflenP + len + 500;
*bufP = (char*) e_realloc( (void*) *bufP, *bufsizeP ); //[3]
}
if ( len > 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, 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:
<device uid="ab:cd:ef:12:34:56">
<ip>192.168.1.5</ip>
<hostname>purgatory</hostname>
<displayName>purgatory</displayName>
<manufacturer>Unknown</manufacturer>
<mode>None</mode>
<isGo>false</isGo>
</device>
</devices>
<contact>
<phone>+12223334444</phone>
<countryCode/>
<email>lilith@totesnotvalid.com</email>
<name>Lilith Wyatt</name>
</contact>
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 < 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: lilith@totesnotvalid.com</email
Content-Type: application/json
Content-Length: 1030
Connection: close
[^_^] Thanks for hangin out!<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.
2017-09-12 - Vendor Disclosure
2017-10-31 - Public Release
Discovered Lilith <(^.^<) Wyatt and Claudio Bozzato of Cisco Talos.