In this blog post, we take a look at a strcpy()-based overflow and non-control-data exploitation in a QNAP NAS.
TL;DR
We found stack-based buffer overflows in the upload handlers of the QNAP file management application named File Station.
The most impactful one affects the upload_and_move function and is reachable by any low-privileged user.
Since classic control-flow hijacking was blocked by ASLR, NX, stack canaries, and the CGI execution model, we had to find a different way for exploitation.
So we abused the overflow for corrupting adjacent stack objects, which allowed us to overwrite the upload ID, which is later used as a source path in root-privileged rename() and unlink() operations.
In the end this led to the following:
- Arbitrary file read for files on the same filesystem
- Arbitrary file delete across the QNAP NAS
- Remote code execution (RCE)
Intro
QNAP NAS devices have been targets at Pwn2Own, QNAP runs a public bug bounty program, and according to QNAP’s own 2025 bounty program progress report, around 150 researchers have reported more than 200 vulnerabilities in 2025.
So, it is fair to assume that these devices are analyzed regularly by skilled security researchers. This made the device an interesting target for a simple question:
Can we still achieve remote code execution on a QNAP NAS in 2026?
To answer this, we looked at the default attack surface of a QNAP NAS running QTS and focused on bugs that are reachable from the network without administrative privileges.
Target and Scope
The analyzed QNAP NAS TS-253E-8G ran QTS version 5.2.8.3359 in its default configuration. No additional apps, plug-ins, or add-ons were installed.
We focused on bugs reachable from the network without administrative privileges, which left two relevant attacker models:
- An unauthenticated network attacker
- A low-privileged authenticated user, for example a normal NAS user with access to a home directory
An image of the analyzed QNAP NAS is shown below.
QNAP TS-253E-8G
Attack Surface
The default network attack surface of the tested NAS is relatively small.
Only the TCP ports 80, 443, and 8080, which are related to the web server, as well as the UDP port 8097 exposed by the binary bcclient, are accessible.
As expected, the web interface was the largest and most interesting attack surface. The QTS web stack uses native CGI binaries behind an Apache reverse proxy and a thttpd web server, which can be abstracted as follows:
QNAP NAS web execution path
Vulnerability Discovery
We started by decompiling native CGI binaries in Ghidra and mapping the authentication and authorization logic. The first goal was to identify handlers that are reachable either without authentication or with low-privileged users.
Afterwards, we searched for security-sensitive sinks and potential classic command injection or SQLi bugs. However, we noticed that many classic injection patterns were handled defensively and input validation was present in all interesting paths.
This made targeting memory safety and filesystem logic more interesting, and we decided to focus more on that.
Fuzzing Setup
After manual and AI-assisted code analysis via frontier LLMs such as Anthropic’s Opus or OpenAI’s GPT models, no interesting attack paths were discovered. So we decided to setup binary fuzzing in parallel.
For this, we fuzzed the native CGI binaries via AFL++ in QEMU mode directly on the NAS, which simplified the setup a lot. For example, the expected filesystem layout, shared libraries, and runtime environment were already present and did not need to be handled manually in a lab environment.
CGI fuzzing is also relatively convenient.
The web server passes request metadata through environment variables, and for POST requests the body is provided through standard input.
Therefore, a harness can model a request by setting the relevant CGI environment and feed the body as stdin to the target binary.
*Fuzzing Harness (click to expand)*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
static int redirect_to_devnull(int fd, int flags) {
int d = open("/dev/null", flags);
if (d < 0) return -1;
if (dup2(d, fd) < 0) { close(d); return -1; }
close(d);
return 0;
}
static unsigned char *read_file_capped(const char *path, size_t cap, size_t *out_sz) {
*out_sz = 0;
FILE *f = fopen(path, "rb");
if (!f) return NULL;
unsigned char *buf = (unsigned char *)calloc(1, cap + 1);
if (!buf) { int e = errno; fclose(f); errno = e; return NULL; }
size_t got = fread(buf, 1, cap, f);
if (ferror(f)) { int e = errno; fclose(f); free(buf); errno = e; return NULL; }
fclose(f);
buf[got] = 0;
*out_sz = got;
return buf;
}
int main(int argc, char **argv) {
int quiet = 0;
const char *input = NULL;
if (argc == 2) {
input = argv[1];
} else if (argc == 3 &&
(!strcmp(argv[1], "-q") || !strcmp(argv[1], "--quiet"))) {
quiet = 1;
input = argv[2];
} else {
fprintf(stderr, "usage: %s [-q|--quiet] <input_file>\n", argv[0]);
return 1;
}
if (quiet) {
(void)redirect_to_devnull(STDOUT_FILENO, O_WRONLY);
(void)redirect_to_devnull(STDERR_FILENO, O_WRONLY);
}
const char *afl_file = getenv("AFL_FILE");
if (afl_file && *afl_file) {
input = afl_file;
}
const size_t CAP = 1u << 20;
size_t got = 0;
unsigned char *buf = read_file_capped(input, CAP, &got);
if (!buf) {
fprintf(stderr, "read file failed: '%s': %s\n", input, strerror(errno));
return 1;
}
for (size_t i = 0; i < got; i++) {
if (buf[i] == '\r' || buf[i] == '\n') buf[i] = 0;
}
const char *TARGET = "/home/httpd/cgi-bin/authLogin.cgi";
setenv("QUERY_STRING", (char *)buf, 1);
setenv("CONTENT_LENGTH", "0", 1);
setenv("SERVER_SOFTWARE", "http server 1.0", 1);
setenv("SERVER_NAME", "127.0.0.1", 1);
setenv("GATEWAY_INTERFACE", "CGI/1.1", 1);
setenv("SERVER_PROTOCOL", "HTTP/1.0", 1);
setenv("SERVER_PORT", "58080", 1);
setenv("REQUEST_METHOD", "GET", 1);
setenv("REMOTE_ADDR", "192.168.122.1", 1);
(void)redirect_to_devnull(STDIN_FILENO, O_RDONLY);
char *const args[] = {(char *)TARGET, NULL};
execv(TARGET, args);
perror("execv");
free(buf);
return 1;
}
With this setup, we fuzzed several selected CGI binaries such as the File Station handlers while manually reviewing related code paths in parallel:
Sample fuzzing state on the QNAP NAS
Crash Found!
After a few days without an interesting result, one of the fuzzers reported a crash in a related upload path while we were exploring the web application manually. The crash was only reachable after creating a shared file or shared folder in the web interface. Without this application state, the vulnerable path was not executed.
Saved crash
The root cause was a stack-based buffer overflow in utilRequest.cgi in combination with libuLinux_cgi.so.
The vulnerable library function parsed the multipart filename and copied it back into a caller-provided fixed-size stack buffer using strcpy().
Conceptually, the simplified code looked like this:
Simplified vulnerable code
The parser removes path components by returning the part after the last / or \ character.
But this does not limit the filename length.
A filename can still be valid and longer than the destination buffer.
This pattern existed in multiple upload handlers:
func=upload, using a 256-byte stack bufferfunc=chunked_upload, using a 272-byte stack bufferfunc=upload_and_move, using a 256-byte stack buffer
The upload and chunked_upload paths are also reachable through share.cgi if an externally shared folder has upload permissions enabled and a share ID.
For upload_and_move, this is not needed and reachable by any low-privileged user.
Can It Be Exploited?
The affected CGI binary was not an easy exploitation target. The usual mitigations were enabled and we had to deal with ASLR, NX, and stack canaries.
The stack canary is the first major obstacle.
The buffer overflow is triggered through strcpy(), and all stack canaries on our QNAP NAS start with a NULL byte.
Therefore, even if an attacker knew the canary, a strcpy() cannot conveniently overwrite the canary and continue writing the saved frame pointer and return address afterwards, because the write stops at the first NULL byte:
Stack canary with NULL-byte
The next obvious question is whether the canary can be brute-forced. In many forking server models, child processes inherit the same canary from the parent. That can make byte-by-byte brute-forcing possible if the service does not restart and crashes are observable.
However, on the QNAP NAS the thttpd web server executes CGI binaries for every request via execve():
Process execution via execve() in libhttpd.c
Therefore, the CGI binary is executed as a new process image, and every request gets a fresh stack canary. An information leak in one request would not directly provide a valid canary for the next request, and brute-forcing does not converge:
Per-Request individual Stack Canary
Thus, the traditional control-flow hijack exploitation would not work.
However, we looked at the stack layout of the vulnerable functions and found something interesting …
Non-control-data exploitation
While looking closer at the vulnerable function of the upload_and_move handler, we found an interesting stack layout.
The multipart filename is copied into a fixed 256-byte stack buffer.
If the filename is at least 256 bytes long, it overflows into adjacent stack objects:
Stack overflow via overlong filename
At first, this looked like the same dead end.
Nevertheless, the stack layout was different enough to be useful.
Adjacent to the overflowing filename buffer was another stack buffer that stored the upload ID generated by CGI_Get_Upload_ID:
Adjacent stack object
By overflowing the filename buffer via WFM2_CGI_Upload_Ex, we could overwrite the upload ID before it was consumed by later code:
Overflowing the filename and overwriting the adjacent stack object (upload ID)
The upload ID later becomes part of the source path for file operations on the temporary upload file.
With control over the upload ID, we could place a relative path such as ../../../../../etc/shadow.
Later in the request flow, utilRequest.cgi uses this path in file operations after switching privileges with BECOME_ROOT().
Afterwards, the path is used by rename() or unlink() operations in the root user context:
Simplified vulnerable code
This turns a classic stack overflow into a promising non-control-data exploitation candidate.
Arbitrary file delete
As shown in the image above, the error handling around the move operation made the primitive broader than expected.
If the attacker-controlled source path points to a file on a different filesystem, rename() fails with a cross-device error.
The error path then calls unlink() on the same attacker-controlled path.
Because this happens after the privilege switch, the file is deleted as root.
Moreover, if the filesize in the HTTP request does not match the actual file size of the source (attacker-controlled path due to non-control data exploitation), the source file gets deleted too:
File size mismatch of request parameter and overflowed stack parameter
This gives a low-privileged user an arbitrary file delete primitive across the QNAP system.
As a proof of concept, we deleted /etc/shadow using a non-admin user:
Arbitrary file delete proof of concept
Arbitrary file read
If the attacker-controlled source path is on the same filesystem as the destination share, rename() succeeds.
Instead of deleting the file, QTS moves it into the attacker’s writable directory.
The moved object is then accessible through normal File Station functionality.
In addition, the file permissions are changed with chmod(file, 0700), which makes the moved file readable and writable by the attacker in their own directory.
This gives an arbitrary file read primitive for files on the same filesystem.
However, the upload_and_move logic expects the correct file size.
If the wrong size is supplied, the operation can fail and the file may be deleted instead of being moved.
For unknown file sizes, we used a two-step workaround:
-
Move the containing directory into the attacker’s share. For directories, a size value of zero is sufficient.
-
Trigger a File Station copy job on the moved directory. The copy job status leaks metadata such as the filename and total size via the user’s job list. If the directory contains a single file (others could be deleted via the arbitrary file delete), the total size is the exact file size. With that value known, the attacker can move the individual file and read it:
Arbitrary file read proof of concept
From file delete to remote code execution
We found two candidates to achieve remote code execution through arbitrary file deletion.
First, on the tested QNAP system, one practical route involved /bin/naslogin.
If /bin/naslogin is deleted, utelnetd falls back to /bin/sh as the login program:
Fallback to /bin/sh
On systems where Telnet is enabled, this results in a root shell after the service restarts or the system reboots.
RCE via arbitrary file delete
Second, deleting the files /etc/config/uLinux.conf or /mnt/HDA_ROOT/.config/uLinux.conf will force the NAS to set the default password for the QNAP device on the next start.
This allows gaining root access via SSH through the default user and password after deleting this file on the next system start.
Conclusion
We were able to identify classic strcpy()-based buffer overflow vulnerabilities in QNAP NAS devices.
Exploiting these vulnerabilities, however, required advanced techniques such as non-control-data exploitation. Ultimately, this gave us arbitrary file-read and file-deletion primitives, which could lead to remote code execution.
In the end, we were able to discover exploitable vulnerabilities through manual analysis that had remained undetected by QNAP’s security program, including its AI-assisted code reviews.
In an upcoming blog post, we will describe additional QNAP vulnerabilities that we detected using AI and custom harnesses. We will also discuss our assumptions about why certain bugs could be identified and exploited by AI, as well as where and why AI still has weaknesses in other areas.
The vulnerabilities covered in this blog post are summarized below:
| Product | Vulnerability Type | SySS ID | CVE ID |
|---|---|---|---|
| QNAP NAS (File Station 5) | Stack-based Buffer Overflow (CWE-121) | SYSS-2026-013 | CVE-2026-26239 |
| QNAP NAS (File Station 5) | Stack-based Buffer Overflow (CWE-121) | SYSS-2026-014 | CVE-2026-26240 |
| QNAP NAS (File Station 5) | Stack-based Buffer Overflow (CWE-121) | SYSS-2026-015 | CVE-2026-26241 |
A full demonstration of the exploitation can be found on our YouTube channel: