When NAS Vendors Forget How TLS Works

by

Ben R (@XploitBengineer)

January 

Back in June 2025, I decided I'd take a crack at Pwn2Own Ireland. In 2023, our team had 6 entries and I worked on the Synology BC500. This year, I went hunting in Network Attached Storage (NAS) territory, specifically targeting the Synology DiskStation and a QNAP NAS.
Over four days of research I identified a number of vulnerabilites that, when chained together, lead to RCE on both devices. We had working exploits, documentation, everything ready for Pwn2Own Ireland.

Unfortunately, a few months later, when the Pwn2Own rules were released, a rule change rendered these exploits useless.

> Entries requiring ARP spoofing, DNS spoofing, MITM, or any assumptions involving control over external infrastructure are out of scope.

Below, I detail two of the identified authentication bypass vulnerabilities.

QNAP Improper Certificate Validation in SMTP client

A key difference between this device vs. previous IoT devices I'd looked at was the complexity of the web server. Rather than a few PHP files or a simple web API, this device has a full-blown browser-based virtual desktop, making it quite hard to trace the code paths of web requests. I decided to find something fairly simple that I could follow, and settled on the login flow.

When a user attempts to log in to the device, a standard username and password is required. There is, however, configurable password reset options. The QNAP documentation for the cloud-based operating system describes how to enable email-based password resets. This configuration is also available on physical devices. To enable email-based password resets a user must:

1. Set up email-based notifications. This involves setting up an external email provider that will be used to **send** QNAP notifications.
2. Enable email-based password resets.
3. Configure an email for a user to **receive** password reset emails.

Afterwards, when attempting to log in with the username for an account with email-based password resets enabled, the 'Forgot password' button on the login page will trigger a password reset flow.

The CGI binary `reset_password.cgi` is invoked to handle this functionality:

```c
if (is_reset_user_pw)
    result = change_user_password(http_request: cgi_input_data)
else if (strcmp(func_name, "verify_token") == 0)
    result = validate_user_token(cgi_input_data)
else if (strcmp(func_name, "send_mail") == 0)
    result = send_password_reset_email(cgi_input_data)
else if (strcmp(func_name, "get_status") == 0)
    result = check_user_token_status(cgi_input_data)
```

When clicking the password reset button, the `send_password_reset_email` routine is executed. This function first checks that SMTP-based password reset is enabled, then confirms the username provided is for an existing account, and finally checks whether the account has email-based password reset configured.

If it does, the function attempts to load a file from disk `/var/.rp/<user-uid>/token-vcode-file`.  This file contains four fields: `token`, `vcode`, `create_time`, and `retry_remaining`.
`token-vcode-file` is created the first time a password reset is attempted.
Its purpose is not relevant for the bug except for the fact that token and vcode are the two secret values required to successfully reset a password.

After this, an SMTP message is built from a template in `./home/httpd/cgi-bin/html/email_reset_pw.html`.

```html
❯ cat ./home/httpd/cgi-bin/html/email_reset_pw.html
To: $MAIL_TO$ # this is the email added to the user's account
From: $MAIL_FROM$ # this is the email configured as the notification sender
Subject: $MAIL_SUBJECT$
MIME-Version:1.0
Content-type:text/html;charset=utf-8
Content-Transfer-Encoding: 8bit

[...]
 $MAIL_1$
</div>
<div style="margin: 18px 0 28px; width: 100%; height: 3px; background-color: #cecece;"></div>
<div style="text-align:left; color: #010101; font-size: 12px; line-height: 24px;">
<p>$MAIL_2$</p>
</div>
<div style="text-align:left; color: #010101; font-size: 12px; line-height: 24px;">
<p>$MAIL_3$</p>
<a href="$MAIL_URL$">$MAIL_URL$</a> 
</div>
</div>
</div>
<div style="width: 700px; text-align: right; padding: 10px 0 0; border: 0; margin: 0 auto;">
<a href="http://www.qnap.com" target="_blank" style="font-family:Verdana; text-decoration: none; font-size: 10px; color:#7B7A7A">&copy;$YEAR$ QNAP Systems, Inc.</a>
</div>
</div>
```

Once the template has been populated, the program calls `/usr/sbin/sendmail`, passing the built email. This is actually a symlink to `/usr/sbin/ssmtp`. Unfortunately, there is a flaw in the sSMTP.

```c
uint64_t smtp_ssl_connect(char* smtp_server_hostname, int32_t smtp_server_port)
    OPENSSL_init_ssl(0x200002, 0)
    OPENSSL_init_ssl(0, 0)
    int64_t ssl_context = SSL_CTX_new(TLS_client_method()) // [1]
    
[...]
    
    if (smtp_tls_enabled == 1)
    {
        log_smtp_message(6, "Creating SSL connection to host")
        if (smtp_tls_mode == 1)
        {
            smtp_tls_enabled = 0
            char smtp_response_buffer[0x1010]
            if (is_smtp_response_code_2xx(fd, &smtp_response_buffer) == 0)
            {
                log_smtp_message(3, "Invalid response SMTP Server (ST…")
                SSL_CTX_free(ssl_context)
                return 0xffffffff
            }

            smtp_write(zx.q(fd), "EHLO %s", smtp_mail_session)
            if (is_smtp_response_code_2xx(fd, &smtp_response_buffer) == 0)
                log_smtp_message(3, "Invalid response: %s (%s)", &smtp_response_buffer, smtp_mail_session)
            else
            {
                smtp_write(zx.q(fd), "STARTTLS")
                
                if (is_smtp_response_code_2xx(fd, &smtp_response_buffer) == 0)
                {
                    log_smtp_message(3, "STARTTLS not working")
                    SSL_CTX_free(ssl_context)
                    return 0xffffffff
                }
            }
            smtp_tls_enabled = 1
        }
        
        ssl_connection = SSL_new(ssl_context)
        if (ssl_connection == 0)
        {
            log_smtp_message(3, "SSL not working")
            return 0xffffffff
        }
        
        SSL_set_fd(ssl_connection, zx.q(fd))
        if (SSL_connect(ssl_connection) s< 0)
        {
            SSL_CTX_free(ssl_context)
            return 0xffffffff
        }
[...]
    SSL_CTX_free(ssl_context)
    return zx.q(fd)
```

The sSMTP code creates an SSL connection but completely forgets to verify the server's certificate.
There is no call to `SSL_CTX_set_verify(ssl_context, SSL_VERIFY_PEER, NULL)`.
This means sSMTP will happily perform a TLS handshake without checking if the certificate chain is valid or if the root CA is trusted. As a result, a man-in-the-middle could spoof DNS to the SMTP server, and sSMTP will successfully establish a session with the malicious server and send the password reset email to it.

I wrote an exploit for this vulnerabilty that models an attacker exploiting the NAS after taking control over the default gateway (e.g. the LANs router) and does the following:

1. Trigger a password reset for the administrator account.
2. Use the MiTM to resolve DNS requests for `smtp.protonmail.ch` to an an attacker-controlled SMTP server.
3. Use the intercepted email to reset the administrator's password.
4. Log in as the administrator, enable SSH, and login to the NAS over SSH.

I notified QNAP of this vulnerability in October 2025, they responded in December stating that they were already aware of the vulnerability, however at time of writing a fix has not been released.

Synology Improper Certificate Validation in SMTP API library

The second target I looked at was the Synology DS224+, since the Pwn2Own target list hadn't been released I wasn't sure which specific device to work on.

After the success of password-reset attacks on the QNAP, I decided to look in the same place on the Synology. The binary that handles password resets is `./usr/syno/sbin/synopasswordmail`. The setup for email-based password resets is almost identical to the QNAP, except that administrator accounts cannot have email-based password resets enabled.

`synopasswordmail` calls `SYNOMailSendMessageWithoutBlock` to send the SMTP. This flows down to the `SYNOMailSendMessageWithHeaderEx` function, which dynamically opens the library `libsynosmtp.so` and uses `SYNOSMTPSendEmail` to actually trigger the SMTP message.

For most mail servers, standard SMTP protocol is used. However for Google and Microsoft emails, their HTTPS APIs are used instead. This is achieved by calling `SYNOSMTPApiSend`:

```c
return SYNOSMTPApiSend(arg1, arg2, "https://www.googleapis.com/gmail/v1/users/me/messages/send", "application/json") __tailcall
```

This function has an absolute rookie error. They explicitly disabled both certificate verification and hostname verification in libcurl.

Again, this means a MiTM attacker can spoof DNS and masquerade as a Google API server, receive the password reset email, and log in as the user.

This vulnerability has not been disclosed to Synology, so at time of reading this may be an active 0-day. Since MiTM is not a valid attack vector
for Pwn2Own categories containing Synology devices, it is assumed they do not consider this category of vulnerability in their threat model.

Conclusion

I'm fairly amazed at the terrible security of these NAS devices. After many years in Pwn2Own, and now active sponsors, it's quite disappointing that such obvious vulnerabilities still exist in these devices. Curiously, the only categories that had this rule change were categories with Synology devices.

Please click on "Preferences" to confirm your cookie preferences. By default, the essential cookies are always activated. View our Cookie Policy for more information.